Computer Keyboard - Głównie JavaScript
by

JS w CSS oraz rysowanie w CSS

Grafika z napisem {JS} w {CSS} oraz przykładowym kodem CSS
Jakub T. Jankiewicz, licencja CC-BY-SA. źródło na GitHub-ie użyto czcionki Racing Sans One

Ten wpis nie będzie o React-cie i wstawianiu CSS-a wewnątrz kodu JavaScript. Ale o czymś zupełnie odwrotnym. Będzie to o sposobie wstawiania kodu JS wewnątrz plików CSS. Dokładnie chodzi o obiekty JSON, ale pewnie gdy zastosujemy pojedyncze wyrażenie bez średników to też będzie działać. Pokaże też jak użyć części specyfikacji o nazwie Houdini do rysowania w CSS za pomocą języka JavaScript.

Houdini

Jest to zestaw API, dzięki którym można się podpiąć pod silnik CSS. Za ich pomocą będziemy mogli dodawać nowe funkcjonalności do CSS, których nie ma jeszcze w przeglądarkach lub nigdy nie będą dostępne. Houdini to nazwa, która zawiera kilka API, z których jedne są już zaimplementowane w niektórych przeglądarkach. Inne są w trakcie wdrażania, a jeszcze inne są w planach lub w trakcie definiowania specyfikacji. Specyfikacja i implementacje mogą się jeszcze zmienić. Na stronie ishoudinireadyyet.com możesz sprawdzić status prac nad Houdini, w różnych przeglądarkach.

UWAGA: z chwilą pisania tego artykułu, aby móc użyć niektórych przykładów w Google Chrome, trzeba włączyć opcje „Experimental Web Platform features” otwierając link: chrome://flags/.

Workery

Workers czyli z angielskiego robotnicy jest to sposób dodawania nowego wątku do języka JavaScript. Więc tak na prawdę nie jest jedno-wątkowy i można tworzyć nowe. Nowy worker tworzy się podając scieżkę do pliku.

Mamy kilka rodzaji workerów czyli:

  • Web Workers - zwykły wątek.
  • Shared Workers - jest to worker do którego można się odwoływać z różnych kontekstów np. stron, iframów czy innych workerów.
  • Service Workers - pisałem tym workerze we wpisie Serwer WWW w przeglądarce jest to Worker który działa po zamknięciu strony i może być np. odpowiedzialny za cachowanie stron (np. w aplikacji typu PWA) lub wysyłać powiadomienia ze strony (nawet jak zamknęliśmy stronę, a wyraziliśmy zgodę na powiadomienia).

Data URI

Jest to sposób do definiowania kodu html z danymi jako URI (rozwijając nazwę URL dostajemy Locator, a tutaj dane nie wskazują na lokalizacje, więc używamy URI czyli Identifier), który zaczyna się od data: i może wyglądać tak:

data:text/html,<button>click</button><script>document.querySelector('button').addEventListener('click', () => alert('hello'));</script>

Dzięki temu możemy np. testować sobie kod JavaScript mając prosty edytor (czyli pasek adresu). Zdarza mi się testować w ten sposób proste właściwości JS albo CSS.

Data URI jest ograniczone co do długości, ale można utworzyć go z obiektu Blob. Nie posiada wtedy naszego kodu, ale ma tylko identyfikator (hash), który wskazuje na obiekt w pamięci.

Tutaj przykład funkcji blobify (nazwa może nie do końca poprawna powinno być coś nawiązujące do URI) Bazuje ona na kodzie z tego pytania na Stack Overflow: „How to create a Web Worker from a string”

// source: https://github.com/jcubic/favloader
function blobify(fn) {
    // ref: https://stackoverflow.com/a/10372280/387194
    var str = '(' + fn.toString() + ')()';
    var URL = window.URL || window.webkitURL;
    var blob;
    try {
        blob = new Blob([str], {type: 'application/javascript'});
    } catch (e) { // Backwards-compatibility
        window.BlobBuilder = window.BlobBuilder ||
            window.WebKitBlobBuilder ||
            window.MozBlobBuilder;
        blob = new BlobBuilder();
        blob.append(str);
        blob = blob.getBlob();
    }
    return URL.createObjectURL(blob);
}

Funkcja powstała do projektu Favloader (jest to biblioteka, która dodaje możliwość animacji favicon-ki, która nie zatrzymuje się jak zmienimy zakładkę. Używa do tego celu workera, który trzeba było dodać w tym samym piku, ponieważ ciężko by było użyć ścieżki, gdy używa się np. CDN).

Dalej w kodzie będziemy używać tej funkcji, aby utworzyć worker (a dokładnie paint worklet). Dlaczego nie użyjemy osobnego pliku? Moim zdaniem nadmierne rozdzielanie całej aplikacji na pliki jest błędem. Ciężko cokolwiek potem znaleźć, gdy trzeba skakać między plikami, a czasami nawet katalogami. Więc warto zależne funkcjonalności mieć w jednym pliku. Przydaje się to też, gdy brakuje innej możliwości np. na Codpen (link na końcu).

Przykładowy URL wygląda tak:

"blob:https://jcubic.pl/84778d44-1597-49dd-a5f4-f759dd0fa445"

Więc nie jest to do końca data URI, ale działa dokładnie tak samo, tylko dane mogą mieć nieograniczoną długość, ponieważ nie ma ich w URL-u.

UWAGA: coś takiego nie zadziała z Service Workerem, który wymaga fizycznego pliku na dysku.

Paint Worklet

Jest to rodzaj Workera, do zdań specjalnych. Tworzy się go za pomocą:

CSS.paintWorklet.addModule('plik.js');

Wewnątrz workera mamy dostęp do API, takiego samego jak API Canvas, gdzie mamy możliwość dowolnego rysowania. Możemy zarejestrować specjalnego Rysownika (ang. Paint), którym jest klasa i użyć go w CSS.

selector {
    background-image: paint(circle);
}

Poniżej kod prostego paint workletu, który rysuje punkt bazując na zmiennych CSS (to jedyny sposób aby wysyłać informacje do Paint Workletu)

CSS.paintWorklet.addModule(blobify(function() {
    class Circle {

        static get inputProperties() {
            return ['--pointer-x', '--pointer-y', '--pointer-options'];
        }

        paint(context, geom, properties) {
            var x = properties.get('--pointer-x').value || 0;
            var y = properties.get('--pointer-y').value || 0;
            const prop = properties.get('--pointer-options');
            // destructure object props with defaults
            const {
                background,
                color,
                width
            } = Object.assign({
                color: 'white',
                background: 'black',
                width: 10
            }, JSON.parse(prop.toString()));
            // draw circle at point
            context.fillStyle = color;
            console.log({x,y, color, background, width})
            context.beginPath();
            context.arc(x, y, Math.floor(width / 2), 0, 2 * Math.PI, false);
            context.closePath();
            context.fill();
        }
    }
    registerPaint('circle', Circle);
}));

JSON w CSS

Jak może zauważyłeś, w powyższym kodzie mamy:

const prop = properties.get('--pointer-options');
JSON.parse(prop.toString());

czyli parsujemy zmienną CSS (a dokładnie jej wartość) jak JSON i to działa (przynajmniej w Chromium):

Nasz CSS wygląda tak:

div {
    height: 100vh;
    background-image: paint(circle);
    --pointer-x: 20px;
    --pointer-y: 10px;
    --pointer-options: {
        "color": "rebeccapurple",
        "width": 20
    };
}

Prawdopodobnie będzie działać wszystko od dwukropka do średnika, więc pewnie można też użyć JS i funkcji eval (nie próbowałem).

Punkty x oraz y możemy zmieniać jak ruszamy myszką, nasz worklet się uruchomi, gdy zmieni się wartość określona jako wejście Workletu.

document.querySelector('div').addEventListener('mousemove', (e) => {
    const style = e.target.style;
    style.setProperty('--pointer-x', event.clientX + 'px');
    style.setProperty('--pointer-y', event.clientY + 'px');
});

Dodatkowo powinniśmy jeszcze zarejestrować poszczególne typy dla zmiennych:

CSS.registerProperty({
    name: '--pointer-x',
    syntax: '<length>',
    inherits: false,
    initialValue: '10px'
});
CSS.registerProperty({
    name: '--pointer-y',
    syntax: '<length>',
    inherits: false,
    initialValue: '10px'
});
CSS.registerProperty({
    name: '--pointer-options',
    inherits: false,
    initialValue: '{}'
});

Tutaj link do demo na CodePen.

źródło strony (aby zobaczyć kod na GitHubie, musisz kliknąć przycisk raw)
Komentarze

Hasło, które podasz umożliwi ponowne zalogowanie się i np. usunięcie komentarza, jest dobrowolne. Email jest szyfrowany i używany do wysyłania powiadomień o odpowiedziach do komentarzy oraz do pobierania awatara dzięki usłudze Gravatar.com.