Verwenden der CSS Painting API

Die CSS Paint API ist darauf ausgelegt, Entwicklern zu ermöglichen, programmatisch Bilder zu definieren, die überall dort verwendet werden können, wo ein CSS-Bild aufgerufen werden kann, wie z. B. CSS background-image, border-image, mask-image, usw.

Um programmatisch ein Bild zu erstellen, das von einem CSS-Stylesheet verwendet wird, müssen wir ein paar Schritte durchlaufen:

  1. Definieren Sie ein Paint Worklet mithilfe der Funktion registerPaint()
  2. Registrieren Sie das Worklet
  3. Integrieren Sie die paint() CSS-Funktion

Um diese Schritte zu veranschaulichen, beginnen wir mit dem Erstellen eines halb-hervorgehobenen Hintergrunds, wie in dieser Überschrift:

Text mit der Aufschrift 'My Cool Header' mit einem soliden gelben Hintergrundbildblock am unteren linken Drittel der Überschrift

Hinweis: Sehen Sie sich das Beispiel der CSS Painting API für eine vollständige funktionsfähige Demo sowie den Quellcode an.

CSS Paint Worklet

In einer externen Skriptdatei verwenden wir die Funktion registerPaint(), um unser CSS Paint Worklet zu benennen. Es nimmt zwei Parameter an. Der erste ist der Name, den wir dem Worklet geben — dies ist der Name, den wir in unserem CSS als Parameter der paint()-Funktion verwenden, wenn wir dieses Styling auf ein Element anwenden möchten. Der zweite Parameter ist die Klasse, die alle wesentlichen Funktionen definiert, einschließlich der Kontextoptionen und des zu malenden Inhalts auf die zweidimensionale Leinwand, die unser Bild sein wird.

js
registerPaint(
  "headerHighlight",
  class {
    /*
     * define if alpha transparency is allowed alpha
     * is set to true by default. If set to false, all
     * colors used on the canvas will be fully opaque
     */
    static get contextOptions() {
      return { alpha: true };
    }

    /*
     * ctx is the 2D drawing context
     * a subset of the HTML Canvas API.
     */
    paint(ctx) {
      ctx.fillStyle = "hsl(55 90% 60% / 100%)";
      ctx.fillRect(0, 15, 200, 20); /* order: x, y, w, h */
    }
  },
);

In diesem Klassenbeispiel haben wir eine einzelne Kontextoption mit der Funktion contextOptions() definiert: Wir gaben ein Objekt zurück, das besagt, dass Alphatransparenz zulässig ist.

Wir haben dann die paint()-Funktion verwendet, um auf unsere Leinwand zu malen.

Eine paint()-Funktion kann drei Argumente annehmen. Hier haben wir ein Argument bereitgestellt: den Rendering-Kontext (wir werden später mehr darauf eingehen), oft als ctx bezeichnet. Der 2D-Rendering-Kontext ist ein Unterbereich der HTML Canvas API; die Version, die Houdini zur Verfügung steht (genannt PaintRenderingContext2D), ist ein weiterer Unterbereich, der die meisten Funktionen der vollständigen Canvas API mit der Ausnahme der CanvasImageData, CanvasUserInterface, CanvasText und CanvasTextDrawingStyles APIs enthält.

Wir definieren den fillStyle als hsl(55 90% 60% / 100%), was ein Gelbton ist, und rufen dann fillRect() auf, um ein Rechteck dieser Farbe zu erstellen. Die Parameter von fillRect() sind, in Reihenfolge, x-Achsen-Ursprung, y-Achsen-Ursprung, Breite und Höhe. fillRect(0, 15, 200, 20) führt zur Erstellung eines Rechtecks, das 200 Einheiten breit und 20 Einheiten hoch ist, 0 Einheiten vom linken und 15 Einheiten vom oberen Rand des Inhaltskastens entfernt.

Wir können die CSS-Eigenschaften background-size und background-position verwenden, um dieses Hintergrundbild neu zu skalieren oder zu verschieben, aber dies ist die Standardgröße und -platzierung des gelben Kastens, den wir in unserem Paint Worklet erstellt haben.

Wir haben versucht, das Beispiel einfach zu halten. Für mehr Optionen sehen Sie sich die Dokumentation von <canvas> an. Wir fügen später in diesem Tutorial ein wenig Komplexität hinzu.

Registrierung des Worklets

Um das Paint Worklet zu verwenden, müssen wir es mit addModule() registrieren und in unser CSS einfügen, wobei sichergestellt wird, dass der CSS-Selektor mit einem DOM-Knoten in unserem HTML übereinstimmt.

Das Setup und Design unseres Paint Worklets fand im oben gezeigten externen Skript statt. Wir müssen dieses worklet aus unserem Hauptskript registrieren.

js
CSS.paintWorklet.addModule("nameOfPaintWorkletFile.js");

Dies kann mithilfe der addModule()-Methode des Paint Worklets in einem <script> innerhalb des Haupt-HTML oder in einer vom Dokument verlinkten externen JavaScript-Datei erfolgen.

Verwendung des Paint Worklets

In unserem Beispiel wird das Paint Worklet neben der Hauptskriptdatei gespeichert. Um es zu nutzen, registrieren wir es zunächst:

js
CSS.paintWorklet.addModule("header-highlight.js");

Referenzieren des Paint Worklets in CSS

Sobald wir ein registriertes Paint Worklet haben, können wir es in CSS verwenden. Verwenden Sie die CSS-paint()-Funktion wie jede andere <image> Type, mit dem gleichen String-Identifikator, den wir in der registerPaint()-Funktion des Paint Worklets verwendet haben.

css
.fancy {
  background-image: paint(headerHighlight);
}

Zusammensetzen

Wir können anschließend die fancy-Klasse zu jedem Element hinzufügen, um einen gelben Kasten als Hintergrund hinzuzufügen:

html
<h1 class="fancy">My Cool Header</h1>

Das folgende Beispiel wird in Browsern, die die CSS Painting API unterstützen, so aussehen wie das Bild oben.

Auch wenn Sie nicht mit dem Skript des Worklets spielen können, können Sie background-size und background-position ändern, um die Größe und Position des Hintergrundbildes zu ändern.

PaintSize

Im obigen Beispiel haben wir ein 20x200-Einheiten-Rechteck erstellt, das 15 Einheiten vom oberen Rand des Elements gefüllt ist, wobei es unabhängig von der Größe des Elements gleich bleibt. Wenn der Text klein ist, sieht der gelbe Kasten wie ein riesiges Unterstrich aus. Wenn der Text groß ist, kann der Kasten wie ein Balken über den ersten drei Buchstaben aussehen. Es wäre besser, wenn das Hintergrundbild relativ zur Größe des Elements wäre — wir können die paintSize-Eigenschaft des Elements verwenden, um sicherzustellen, dass das Hintergrundbild proportional zur Größe des Boxmodells des Elements ist.

Der Hintergrund ist 50 % der Höhe und 60 % der Breite des Elements

Im obigen Bild ist der Hintergrund proportional zur Größe des Elements. Das 3. Beispiel hat width: 50%; auf dem Block-Level-Element gesetzt, wodurch das Element und damit das Hintergrundbild schmaler wird.

Das Paint Worklet

Der Code, um das zu tun, sieht folgendermaßen aus:

js
registerPaint(
  "headerHighlight",
  class {
    static get contextOptions() {
      return { alpha: true };
    }

    /*
     * ctx is the 2D drawing context
     * size is the paintSize, the dimensions (height and width) of the box being painted
     */
    paint(ctx, size) {
      ctx.fillStyle = "hsl(55 90% 60% / 100%)";
      ctx.fillRect(0, size.height / 3, size.width * 0.4, size.height * 0.6);
    }
  },
);

Dieses Codebeispiel hat zwei Unterschiede zu unserem ersten Beispiel:

  1. Wir haben ein zweites Argument hinzugefügt, nämlich die Paint-Größe.
  2. Wir haben die Dimensionen und die Positionierung unseres Rechtecks geändert, sodass diese relativ zur Größe des Elementboxmodells und nicht über absolute Werte sind.

Wir können den zweiten Parameter in die paint()-Funktion übergeben, um Zugriff auf die Breite und Höhe des Elements über die .width- und .height-Eigenschaften zu erhalten.

Unser Header hat jetzt ein Highlight, das sich entsprechend seiner Größe ändert.

Verwendung des Paint Worklets

HTML

html
<h1 class="fancy">Largest Header</h1>
<h6 class="fancy">Smallest Header</h6>
<h3 class="fancy half">50% width header</h3>

CSS

Auch wenn Sie nicht mit dem Skript des Worklets experimentieren können, können Sie die font-size und width des Elements ändern, um die Größe des Hintergrundbildes zu beeinflussen.

css
.fancy {
  background-image: paint(headerHighlight);
}
.half {
  width: 50%;
}

JavaScript

js
CSS.paintWorklet.addModule("header-highlight.js");

Ergebnis

In Browsern, die die CSS Paint API unterstützen, sollten die Elemente im folgenden Beispiel gelbe Hintergründe proportional zu ihrer Schriftgröße erhalten.

Benutzerdefinierte Eigenschaften

Zusätzlich zum Zugriff auf die Größe des Elements kann das Worklet auch Zugriff auf CSS-Benutzereigenschaften und reguläre CSS-Eigenschaften haben.

js
registerPaint(
  "cssPaintFunctionName",
  class {
    static get inputProperties() {
      return ["PropertyName1", "--customPropertyName2"];
    }
    static get inputArguments() {
      return ["<color>"];
    }
    static get contextOptions() {
      return { alpha: true };
    }

    paint(drawingContext, elementSize, styleMap) {
      // Paint code goes here.
    }
  },
);

Die drei Parameter der paint()-Funktion beinhalten den Zeichnungs-Kontext, die Paint-Größe und die Eigenschaften. Um auf Eigenschaften zugreifen zu können, nutzen wir die statische inputProperties()-Methode, die einen Live-Zugriff auf CSS-Eigenschaften bietet, inklusive regulärer Eigenschaften und benutzerdefinierter Eigenschaften, und gibt ein Array von Eigenschaften-Namen zurück. Wir werfen einen Blick auf inputArguments im letzten Abschnitt.

Lassen Sie uns eine Liste von Elementen erstellen mit einem Hintergrundbild, das zwischen drei verschiedenen Farben und drei Breiten wechselt.

Die Breite und Farbe des Hintergrundbildes ändert sich basierend auf den benutzerdefinierten Eigenschaften

Um dies zu erreichen, definieren wir zwei benutzerdefinierte CSS-Eigenschaften, --boxColor und --widthSubtractor.

Das Paint Worklet

In unserem Worklet können wir auf diese benutzerdefinierten Eigenschaften Bezug nehmen.

js
registerPaint(
  "boxbg",
  class {
    static get contextOptions() {
      return { alpha: true };
    }

    /*
     * use this function to retrieve any custom properties (or regular properties, such as 'height')
     * defined for the element, return them in the specified array
     */
    static get inputProperties() {
      return ["--boxColor", "--widthSubtractor"];
    }

    paint(ctx, size, props) {
      /*
       * ctx -> drawing context
       * size -> paintSize: width and height
       * props -> properties: get() method
       */
      ctx.fillStyle = props.get("--boxColor");
      ctx.fillRect(
        0,
        size.height / 3,
        size.width * 0.4 - props.get("--widthSubtractor"),
        size.height * 0.6,
      );
    }
  },
);

Wir haben die inputProperties()-Methode in der registerPaint()-Klasse verwendet, um die Werte von zwei benutzerdefinierten Eigenschaften abzurufen, die auf ein Element angewendet werden, das boxbg hat und diese dann innerhalb unserer paint()-Funktion verwendet. Die inputProperties()-Methode kann alle Eigenschaften zurückgeben, die das Element beeinflussen, und nicht nur benutzerdefinierte Eigenschaften.

Nutzung des Paint Worklets

HTML

html
<ul>
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>item 4</li>
  <li>item 5</li>
  <li>item 6</li>
  <li>item 7</li>
  <li>item 8</li>
  <li>item 9</li>
  <li>item 10</li>
  <li>item 11</li>
  <li>item 12</li>
  <li>item 13</li>
  <li>item 14</li>
  <li>item 15</li>
  <li>item 16</li>
  <li>item 17</li>
  <li>item</li>
</ul>

CSS

In unserem CSS definieren wir die benutzerdefinierten Eigenschaften --boxColor und --widthSubtractor.

css
li {
  background-image: paint(boxbg);
  --boxColor: hsl(55 90% 60% / 100%);
}

li:nth-of-type(3n) {
  --boxColor: hsl(155 90% 60% / 100%);
  --widthSubtractor: 20;
}

li:nth-of-type(3n + 1) {
  --boxColor: hsl(255 90% 60% / 100%);
  --widthSubtractor: 40;
}

JavaScript

In unserem <script> registrieren wir das Worklet:

js
CSS.paintWorklet.addModule("boxbg.js");

Ergebnis

Auch wenn Sie nicht mit dem Skript des Worklets spielen können, können Sie die Werte der benutzerdefinierten Eigenschaften in den DevTools ändern, um die Farben und die Breite des Hintergrundbildes anzupassen.

Hinzufügen von Komplexität

Die obigen Beispiele mögen nicht besonders spannend erscheinen, da Sie sie auf verschiedene Weisen mit bestehenden CSS-Eigenschaften nachbilden könnten, z. B. durch Positionierung eines dekorativen generierten Inhalts mit ::before, oder indem Sie background: linear-gradient(yellow, yellow) 0 15px / 200px 20px no-repeat; einfügen. Was die CSS Painting API so interessant und leistungsfähig macht, ist, dass Sie komplexe Bilder erstellen können, die Variablen nutzen und sich automatisch anpassen.

Werfen wir einen Blick auf ein komplexeres Mal-Beispiel.

Das Paint Worklet

js
registerPaint(
  "headerHighlight",
  class {
    static get inputProperties() {
      return ["--highColor"];
    }
    static get contextOptions() {
      return { alpha: true };
    }

    paint(ctx, size, props) {
      /* set where to start the highlight & dimensions */
      const x = 0;
      const y = size.height * 0.3;
      const blockWidth = size.width * 0.33;
      const highlightHeight = size.height * 0.85;
      const color = props.get("--highColor");

      ctx.fillStyle = color;

      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(blockWidth, y);
      ctx.lineTo(blockWidth + highlightHeight, highlightHeight);
      ctx.lineTo(x, highlightHeight);
      ctx.lineTo(x, y);
      ctx.closePath();
      ctx.fill();

      /* create the dashes */
      for (let start = 0; start < 8; start += 2) {
        ctx.beginPath();
        ctx.moveTo(blockWidth + start * 10 + 10, y);
        ctx.lineTo(blockWidth + start * 10 + 20, y);
        ctx.lineTo(
          blockWidth + start * 10 + 20 + highlightHeight,
          highlightHeight,
        );
        ctx.lineTo(
          blockWidth + start * 10 + 10 + highlightHeight,
          highlightHeight,
        );
        ctx.lineTo(blockWidth + start * 10 + 10, y);
        ctx.closePath();
        ctx.fill();
      }
    } // paint
  },
);

Verwendung des Paint Worklets

Wir können dann ein kleines HTML erstellen, das dieses Bild als Hintergrund akzeptiert:

html
<h1 class="fancy">Largest Header</h1>
<h3 class="fancy">Medium size header</h3>
<h6 class="fancy">Smallest Header</h6>

Wir geben jedem Header einen anderen Wert für die benutzerdefinierte Eigenschaft --highColor

css
.fancy {
  background-image: paint(headerHighlight);
}
h1 {
  --highColor: hsl(155 90% 60% / 70%);
}
h3 {
  --highColor: hsl(255 90% 60% / 50%);
}
h6 {
  --highColor: hsl(355 90% 60% / 30%);
}

Und wir registrieren unser Worklet

js
CSS.paintWorklet.addModule("header-highlight.js");

Das Ergebnis sieht folgendermaßen aus:

Auch wenn Sie das Worklet selbst nicht bearbeiten können, können Sie mit dem CSS und HTML herumspielen. Versuchen Sie vielleicht scale und rotate auf den Headers?

Sie könnten auch versuchen, die obigen Hintergrundbilder ohne die CSS Paint API zu erstellen. Es ist möglich, aber Sie müssten für jede Farbe, die Sie erstellen möchten, einen anderen, ziemlich komplexen linearen Verlauf angeben. Mit der CSS Paint API kann ein Worklet wiederverwendet werden, mit in diesem Fall verschiedenen übergebenen Farben.

Übergabe von Parametern

Hinweis: Das folgende Beispiel erfordert, dass das Experimentelle Webplattform-Feature-Flag in Chrome oder Edge aktiviert ist, indem Sie about://flags besuchen.

Mit der CSS Paint API haben wir nicht nur Zugriff auf benutzerdefinierte Eigenschaften und reguläre Eigenschaften, sondern wir können auch benutzerdefinierte Argumente an die paint()-Funktion übergeben.

Wir können diese zusätzlichen Argumente angeben, wenn wir die Funktion im CSS aufrufen. Angenommen, wir möchten unseren Hintergrund statt zu füllen manchmal umranden — lassen Sie uns ein extra Argument für diesen Fall übergeben.

css
li {
  background-image: paint(hollowHighlights, stroke);
}

Nun können wir die Methode inputArguments() in der registerPaint()-Klasse verwenden, um auf das benutzerdefinierte Argument zuzugreifen, das wir unserer paint()-Funktion hinzugefügt haben.

js
static get inputArguments() { return ['*']; }

Wir haben dann Zugriff auf dieses Argument.

js
paint(ctx, size, props, args) {

  // use our custom arguments
  const hasStroke = args[0].toString();

  // if stroke arg is 'stroke', don't fill
  if (hasStroke === 'stroke') {
    ctx.fillStyle = 'transparent';
    ctx.strokeStyle = color;
  }
  // …
}

Wir können auch angeben, dass wir einen bestimmten Argumenttyp wollen.

Angenommen, wir fügen ein zweites Argument hinzu, um festzulegen, wie viele Pixel die Umrandung breit sein soll:

css
li {
  background-image: paint(hollowHighlights, stroke, 10px);
}

Wenn wir unsere Liste mit Argumentwerten abrufen, können wir spezifisch nach einer <length>-Einheit fragen.

js
static get inputArguments() { return ['*', '<length>']; }

In diesem Fall haben wir spezifisch nach dem <length>-Attribut gefragt. Das erste Element im zurückgegebenen Array wird ein CSSUnparsedValue sein. Das zweite wird ein CSSStyleValue sein.

Wenn das benutzerdefinierte Argument ein CSS-Wert ist, zum Beispiel eine Einheit, können wir die Typisierte OM CSSStyleValue-Klasse (und Unterklassen) durch das Verwenden des Schlüsselsworts des Wertetyps aufrufen, wenn wir es in der registerPaint()-Funktion abrufen.

Jetzt können wir auf die Typ- und Werteeigenschaften zugreifen, was bedeutet, dass wir die Anzahl der Pixel und einen Zahlentyp direkt zur Verfügung haben. (Zugegeben, ctx.lineWidth nimmt einen Gleitkommawert anstatt eines Wertes mit Längeneinheiten, aber zum Zwecke des Beispiels…)

js
paint(ctx, size, props, args) {

  const strokeWidth = args[1];

  if (strokeWidth.unit === 'px') {
    ctx.lineWidth = strokeWidth.value;
  } else {
    ctx.lineWidth = 1.0;
  }

  // …
}

Es lohnt sich, den Unterschied zwischen der Verwendung von benutzerdefinierten Eigenschaften zur Steuerung verschiedener Teile dieses Worklets und den hier aufgeführten Argumenten zu beachten. Benutzerdefinierte Eigenschaften (und tatsächlich alle Eigenschaften auf der Style-Map) sind global — sie können anderswo innerhalb unseres CSS (und JS) verwendet werden.

Sie könnten zum Beispiel eine --mainColor haben, die nützlich wäre, um die Farbe innerhalb einer paint()-Funktion festzulegen, aber auch anderswo in Ihrem CSS verwendet werden kann. Wenn Sie es nur für Paint ändern wollten, könnte dies sich als schwierig erweisen. Hier kommt die Funktion des benutzerdefinierten Arguments ins Spiel. Ein anderer Weg, darüber nachzudenken, ist, dass Argumente gesetzt sind, um das, was Sie tatsächlich zeichnen, zu steuern, während Eigenschaften gesetzt sind, um das Styling zu steuern.

Die Listenelemente haben ein Hintergrundbild, das entweder pink, lila oder grün ist, mit unterschiedlichen Linienbreiten, und das grüne ist gefüllt.

Jetzt können wir wirklich anfangen, die Vorteile dieser API zu sehen, wenn wir eine Vielzahl von Zeichnungsparametern durch unser CSS über sowohl benutzerdefinierte Eigenschaften als auch zusätzliche paint()-Funktionsargumente steuern können, dann können wir anfangen, wiederverwendbare und hochkontrollierbare Stilfunktionen zu erstellen.

Das Paint Worklet

js
registerPaint(
  "hollowHighlights",
  class {
    static get inputProperties() {
      return ["--boxColor"];
    }
    // Input arguments that can be passed to the `paint` function
    static get inputArguments() {
      return ["*", "<length>"];
    }

    static get contextOptions() {
      return { alpha: true };
    }

    paint(ctx, size, props, args) {
      // ctx   -> drawing context
      // size  -> size of the box being painted
      // props -> list of custom properties available to the element
      // args  -> list of arguments set when calling the paint() function in the CSS

      // where to start the highlight & dimensions
      const x = 0;
      const y = size.height * 0.3;
      const blockWidth = size.width * 0.33;
      const blockHeight = size.height * 0.85;

      // the values passed in the paint() function in the CSS
      const color = props.get("--boxColor");
      const strokeType = args[0].toString();
      const strokeWidth = parseInt(args[1]);

      // set the stroke width
      ctx.lineWidth = strokeWidth ?? 1.0;
      // set the fill type
      if (strokeType === "stroke") {
        ctx.fillStyle = "transparent";
        ctx.strokeStyle = color;
      } else if (strokeType === "filled") {
        ctx.fillStyle = color;
        ctx.strokeStyle = color;
      } else {
        ctx.fillStyle = "none";
        ctx.strokeStyle = "none";
      }

      // block
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(blockWidth, y);
      ctx.lineTo(blockWidth + blockHeight, blockHeight);
      ctx.lineTo(x, blockHeight);
      ctx.lineTo(x, y);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
      // dashes
      for (let i = 0; i < 4; i++) {
        let start = i * 2;
        ctx.beginPath();
        ctx.moveTo(blockWidth + start * 10 + 10, y);
        ctx.lineTo(blockWidth + start * 10 + 20, y);
        ctx.lineTo(blockWidth + start * 10 + 20 + blockHeight, blockHeight);
        ctx.lineTo(blockWidth + start * 10 + 10 + blockHeight, blockHeight);
        ctx.lineTo(blockWidth + start * 10 + 10, y);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
      }
    } // paint
  },
);

Verwendung des Paint Worklets

Wir können verschiedene Farben, Linienbreiten festlegen und auswählen, ob das Hintergrundbild gefüllt oder hohl sein soll:

css
li {
  --boxColor: hsl(155 90% 60% / 50%);
  background-image: paint(hollowHighlights, stroke, 5px);
}

li:nth-of-type(3n) {
  --boxColor: hsl(255 90% 60% / 50%);
  background-image: paint(hollowHighlights, filled, 3px);
}

li:nth-of-type(3n + 1) {
  --boxColor: hsl(355 90% 60% / 50%);
  background-image: paint(hollowHighlights, stroke, 1px);
}

In unserem <script> registrieren wir das Worklet:

js
CSS.paintWorklet.addModule("hollow.js");

Siehe auch