Title:  CSS Painting API Level 1
Status: ED
Group: houdini
ED: https://drafts.css-houdini.org/css-paint-api-1/
TR: http://www.w3.org/TR/css-paint-api-1/
Previous Version: https://www.w3.org/TR/2018/WD-css-paint-api-1-20180410/
Previous Version: https://www.w3.org/TR/2016/WD-css-paint-api-1-20160607/
Shortname: css-paint-api
Level: 1
Abstract:
    An API for allowing web developers to define a custom CSS <> with javascript, which will
    respond to style and size changes.
    See EXPLAINER.
Former Editor: Shane Stephens, shanestephens@google.com, w3cid 47691
Editor: Ian Kilpatrick, ikilpatrick@chromium.org, w3cid 73001
Editor: Dean Jackson, dino@apple.com, w3cid 42080
Ignored Terms: PaintWorklet
urlPrefix: https://heycam.github.io/webidl/; type: dfn;
    text: InvalidModificationError
    urlPrefix: #dfn-;
        url: throw; text: thrown
    urlPrefix: #idl-;
        text: boolean
        text: DOMException
    url: es-type-mapping; text: converting
urlPrefix: https://html.spec.whatwg.org/multipage/; type: dfn;
    urlPrefix: scripting.html
        text: reset the rendering context to its default state
        text: output bitmap
        text: set bitmap dimensions
        url: concept-canvas-alpha; text: alpha
urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
    text: constructor
    text: Construct
    text: IsArray
    text: IsCallable
    text: IsConstructor
    url: ecmascript-data-types-and-values; text: type
    url: get-o-p; text: Get
    url: terms-and-definitions-function; text: function
    urlPrefix: native-error-types-used-in-this-standard-
        text: TypeError
Introduction {#intro} ===================== The paint stage of CSS is responsible for painting the background, content and highlight of a box based on that box's size (as generated by the layout stage) and computed style. This specification describes an API which allows developers to paint a part of a box in response to size / computed style changes with an additional <> function. Note: In a future version of the spec, support could be added for defining the clip, global alpha, filter on a portion of a box (for example on the background layers). Paint Worklet {#paint-worklet} ============================== The {{paintWorklet}} attribute allows access to the {{Worklet}} responsible for all the classes which are related to painting. The {{paintWorklet}}'s worklet global scope type is {{PaintWorkletGlobalScope}}.
partial interface CSS {
    [SameObject] readonly attribute Worklet paintWorklet;
};
A {{PaintWorkletGlobalScope}} is a global execution context of the {{paintWorklet}}. A {{PaintWorkletGlobalScope}} has a {{PaintWorkletGlobalScope/devicePixelRatio}} property which is identical to the Window.{{Window/devicePixelRatio}} property.
[Global=(Worklet,PaintWorklet),Exposed=PaintWorklet]
interface PaintWorkletGlobalScope : WorkletGlobalScope {
    void registerPaint(DOMString name, VoidFunction paintCtor);
    readonly attribute unrestricted double devicePixelRatio;
};
The {{PaintRenderingContext2DSettings}} contains the settings for the rendering context associated with the paint canvas. The {{PaintRenderingContext2DSettings}} provides a supported subset of canvas rendering context 2D settings. In the future, it may be extended to support color management in paint canvas.
dictionary PaintRenderingContext2DSettings {
    boolean alpha = true;
};
Note: The shape of the class should be:
        class MyPaint {
            static get inputProperties() { return ['--foo']; }
            static get inputArguments() { return ['<color>']; }
            static get contextOptions() { return {alpha: true}; }

            paint(ctx, size, styleMap) {
                // Paint code goes here.
            }
        }
    
Concepts {#concepts} ==================== A paint definition is a struct which describes the information needed by the {{PaintWorkletGlobalScope}} about the author defined <> (which can be referenced by the <> function). It consists of: - class constructor which is the class constructor. - paint function which is the paint function callback. - constructor valid flag. - input properties which is a list of DOMStrings. - A PaintRenderingContext2DSettings object. A document paint definition is a struct which describes the information needed by the document about the author defined <> function (which can be referenced by the paint function). It consists of: - A input properties which is a list of DOMStrings. - A input argument syntaxes which is a list of parsed [[css-properties-values-api-1#supported-syntax-strings]]. - A PaintRenderingContext2DSettings object. Registering Custom Paint {#registering-custom-paint} ==================================================== The document has a map of document paint definitions. Initially this map is empty; it is populated when {{registerPaint(name, paintCtor)}} is called. A {{PaintWorkletGlobalScope}} has a map of paint definitions. Initially this map is empty; it is populated when {{registerPaint(name, paintCtor)}} is called. A {{PaintWorkletGlobalScope}} has a map of paint class instances. Initially this map is empty; it is populated when draw a paint image is invoked by the user agent. Instances of paint classes in the paint class instances map may be disposed and removed from the map by the user agent at any time. This may be done when a <> function no longer is used, or the user agent needs to reclaim memory.
When the registerPaint(|name|, |paintCtor|) method is called, the user agent must run the following steps: 1. If the |name| is an empty string, throw a TypeError and abort all these steps. 2. Let |paintDefinitionMap| be {{PaintWorkletGlobalScope}}'s paint definitions map. 3. If |paintDefinitionMap|[|name|] exists throw a "InvalidModificationError" DOMException and abort all these steps. 4. Let |inputProperties| be an empty sequence<DOMString>. 5. Let |inputPropertiesIterable| be the result of Get(|paintCtor|, "inputProperties"). 6. If |inputPropertiesIterable| is not undefined, then set |inputProperties| to the result of converting |inputPropertiesIterable| to a sequence<DOMString>. If an exception is thrown, rethrow the exception and abort all these steps. Note: The list of CSS properties provided by the input properties getter can either be custom or native CSS properties. Note: The list of CSS properties may contain shorthands. Note: In order for a paint image class to be forwards compatible, the list of CSS properties can also contains currently invalid properties for the user agent. For example margin-bikeshed-property. 7. Let |inputArguments| be an empty sequence<DOMString>. 8. Let |inputArgumentsIterable| be the result of Get(|paintCtor|, "inputArguments"). 9. If |inputArgumentsIterable| is not undefined, then set |inputArguments| to the result of converting |inputArgumentsIterable| to a sequence<DOMString>. If an execption is thrown, rethrow the execption and abort all these steps. 10. Let |inputArgumentSyntaxes| be an empty list. 11. For each |item| in |inputArguments| perform the following substeps: 1. Let |parsedSyntax| be the result of parsing |item| according to the rules in [[css-properties-values-api-1#supported-syntax-strings]]. If it fails to parse throw a TypeError and abort all these steps. 2. Append |parsedSyntax| to |inputArgumentSyntaxes|. 12. Let |contextOptionsValue| be the result of Get(|paintCtor|, "contextOptions"). 13. Let |paintRenderingContext2DSettings| be the result of converting |contextOptionsValue| to a {{PaintRenderingContext2DSettings}}. If an exception is thrown, rethrow the exception and abort all these steps. Note: Setting paintRenderingContext2DSettings.alpha is false allows user agents to anti-alias text in addition to performing "visibility" optimizations, e.g. not painting an image behind the paint image as the paint image is opaque. 14. If the result of IsConstructor(|paintCtor|) is false, throw a TypeError and abort all these steps. 15. Let |prototype| be the result of Get(|paintCtor|, "prototype"). 16. If the result of Type(|prototype|) is not Object, throw a TypeError and abort all these steps. 17. Let |paint| be the result of Get(|prototype|, "paint"). 18. If the result of IsCallable(|paint|) is false, throw a TypeError and abort all these steps. 19. Let |definition| be a new paint definition with: - class constructor being |paintCtor|. - paint function being |paint|. - constructor valid flag being true. - input properties being |inputProperties|. - PaintRenderingContext2DSettings object being |paintRenderingContext2DSettings|. 20. Set |paintDefinitionMap|[|name|] to |definition|. 21. Queue a task to run the following steps: 1. Let |documentPaintDefinitionMap| be the associated document's document paint definitions map. 2. Let |documentDefinition| be a new document paint definition with: - input properties being |inputProperties|. - input argument syntaxes being |inputArgumentSyntaxes|. - PaintRenderingContext2DSettings object being |paintRenderingContext2DSettings|. 3. If |documentPaintDefinitionMap|[|name|] exists, run the following steps: 1. Let |existingDocumentDefinition| be the result of get |documentPaintDefinitionMap|[|name|]. 2. If |existingDocumentDefinition| is "invalid", abort all these steps. 3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is input properties, input argument syntaxes, and PaintRenderingContext2DSettings object are different), then: Set |documentPaintDefinitionMap|[|name|] to "invalid". Log an error to the debugging console stating that the same class was registered with different inputProperties, inputArguments, or paintRenderingContext2DSettings. 4. Otherwise, set |documentPaintDefinitionMap|[|name|] to |documentDefinition|. Note: The list of input properties should only be looked up once, the class doesn't have the opportunity to dynamically change its input properties. Note: In a future version of the spec, the author could have the ability to receive a different type of RenderingContext. In particular the author may want a WebGL rendering context to render 3D effects. There are complexities in setting up a WebGL rendering context to take the {{PaintSize}} and {{StylePropertyMap}} as inputs.
Paint Notation {#paint-notation} ================================
    paint() = paint( <>, <>? )
The <> function is an additional notation to be supported by the <> type.
        <style>
            .logo { background-image: paint(company-logo); }
            .chat-bubble { background-image: paint(chat-bubble, blue); }
        </style>
    
For the 'cursor' property, the <> function should be treated as an invalid image and fallback to the next supported <>. At computed value time the <> function does not need to match the grammar registered by {{registerPaint()}}. Instead this will result in an invalid image when the parsing occurs inside draw a paint image. The 2D rendering context {#2d-rendering-context} ================================================
[Exposed=PaintWorklet]
interface PaintRenderingContext2D {
};
PaintRenderingContext2D implements CanvasState;
PaintRenderingContext2D implements CanvasTransform;
PaintRenderingContext2D implements CanvasCompositing;
PaintRenderingContext2D implements CanvasImageSmoothing;
PaintRenderingContext2D implements CanvasFillStrokeStyles;
PaintRenderingContext2D implements CanvasShadowStyles;
PaintRenderingContext2D implements CanvasRect;
PaintRenderingContext2D implements CanvasDrawPath;
PaintRenderingContext2D implements CanvasDrawImage;
PaintRenderingContext2D implements CanvasPathDrawingStyles;
PaintRenderingContext2D implements CanvasPath;
Note: The {{PaintRenderingContext2D}} implements a subset of the {{CanvasRenderingContext2D}} API. Specifically it doesn't implement the {{CanvasImageData}}, {{CanvasUserInterface}}, {{CanvasText}}, or {{CanvasTextDrawingStyles}} APIs. A {{PaintRenderingContext2D}} object has a output bitmap. This is initialised when the object is created. The size of the output bitmap is the size of the fragment it is rendering. The size of the output bitmap does not necessarily represent the size of the actual bitmap that the user agent will use internally or during rendering. For example, if the visual viewport is zoomed the user agent may internally use bitmaps which correspond to the number of device pixels in the coordinate space, so that the resulting rendering is of high quality. Additionally the user agent may record the sequence of drawing operations which have been applied to the output bitmap such that the user agent can subsequently draw onto a device bitmap at the correct resolution. This also allows user agents to re-use the same output of the output bitmap repeatably while the visual viewport is being zoomed for example. Whenever "currentColor" is used as a color in the {{PaintRenderingContext2D}} API, it is treated as opaque black.
The code below will produce a solid black rectange.
        registerPaint('currentcolor', class {
            paint(ctx, size) {
                ctx.fillStyle = 'currentColor';
                ctx.fillRect(0, 0, size.width, size.height);
            }
        });
    
When the user agent is to create a PaintRenderingContext2D object for a given |width|, |height|, and |paintRenderingContext2DSettings|, it must run the following steps: 1. Create a new {{PaintRenderingContext2D}}. 2. Set bitmap dimensions for the context's output bitmap to the rounded values of |width| and |height|. 3. Set the {{PaintRenderingContext2D}}'s alpha flag to |paintRenderingContext2DSettings|'s {{alpha}}. 4. Return the new {{PaintRenderingContext2D}}. Note: The initial state of the rendering context is set inside the set bitmap dimensions algorithm, as it invokes reset the rendering context to its default state and clears the output bitmap.
Drawing a CSSImageValue {#drawing-a-cssimagevalue} -------------------------------------------------- The {{CanvasImageSource}} typedef is extended to also include the {{CSSImageValue}} type to be used as an image source. For interfaces which use the {{CanvasDrawImage}} mixin: - When a {{CanvasImageSource}} object represents an {{CSSImageValue}}, the result of invoking the value's underlying image algorithm must be used as the source image for the purposes of {{CanvasDrawImage/drawImage}}. Note: This should eventually be moved to the canvas section of the HTML specification. Drawing an image {#drawing-an-image} ==================================== If a <> function image for a box is within the visual viewport, the user agent must display an image output from an invocation of the draw a paint image algorithm. Note: The user agent doesn't have to run draw a paint image each frame for a <> function within the visual viewport. It can cache results, (potentially using additional invalidation steps) to display the correct image output. Note: The user agent can optionally defer drawing images which are outside the visual viewport.
If an author updates a style inside a requestAnimationFrame, e.g.
        requestAnimationFrame(function() {
            element.styleMap.set('--custom-prop-invalidates-paint', 42);
        });
    
And the element is inside the visual viewport, the user agent is required to draw a paint image and display the result for the current frame.
The draw a paint image function is invoked by the user agent during the object size negotiation algorithm which is responsible for rendering an <>, with |snappedConcreteObjectSize| defined as follows. Let |concreteObjectSize| be the concrete object size of the box. The |snappedConcreteObjectSize| is usually the same as the |concreteObjectSize|. However, the user agent may adjust the size such that it paints to pixel boundaries. If it does, the user agent should adjust the |snappedConcreteObjectSize| by the proportional change from its original size such that the <> function can adjust the drawing accordingly. For the purposes of the object size negotiation algorithm, the paint image has no intrinsic dimensions. Note: In a future version of the spec, the author could have the ability to specify the intrinsic dimensions of the paint image. This will probably be exposed as a callback allowing the author to define static intrinsic dimensions or dynamically updating the intrinsic dimensions based on computed style and size changes. The {{PaintSize}} object represents the size of the image that the author should draw. This is the |snappedConcreteObjectSize| given by the user agent. Note: See [[css-images-3#object-sizing-examples]] for examples on how the concrete object size is calculated. The draw a paint image function may be speculatively invoked by the user agent at any point, with any |snappedConcreteObjectSize|. The resulting image is not displayed. Note: User agents may use any heuristic to speculate a possible future value for |snappedConcreteObjectSize|, for example speculating that the size remains unchanged. Note: Although the image is not displayed, it may still be cached, and subsequent invocations of <> may use the cached image.
[Exposed=PaintWorklet]
interface PaintSize {
    readonly attribute double width;
    readonly attribute double height;
};
When the user agent wants to draw a paint image of a <> function for a |box| into its appropriate stacking level (as defined by the property the CSS property its associated with), given |snappedConcreteObjectSize| it must run the following steps: 1. Let |paintFunction| be the <> function on the |box| which the user agent wants to draw. 2. Let |name| be the first argument of the |paintFunction|. 3. Let |documentPaintDefinitionMap| be the associated document's document paint definitions map. 4. If |documentPaintDefinitionMap|[|name|] does not exist, let the image output be an invalid image and abort all these steps. 5. Let |documentDefinition| be the result of get |documentPaintDefinitionMap|[|name|]. 6. If |documentDefinition| is "invalid", let the image output be an invalid image and abort all these steps. 7. Let |inputArgumentSyntaxes| be |documentDefinition|'s input argument syntaxes. 8. Let |inputArguments| be the list of all the |paintFunction| arguments after the "paint name" argument. 9. If |inputArguments| do not match the registered grammar given by |inputArgumentSyntaxes|, let the image output be an invalid image and abort all these steps.
This step may fail in the following cases:
                // paint.js
                registerPaint('failing-argument-syntax', class {
                    static get inputArguments() { return ['<length>']; }
                    paint(ctx, size, styleMap, args) { /* paint code here. */ }
                });
            
                <style>
                    .example-1 {
                        background-image: paint(failing-argument-syntax, red);
                    }
                    .example-2 {
                        background-image: paint(failing-argument-syntax, 1px, 2px);
                    }
                </style>
                <div class=example-1></div>
                <div class=example-2></div>
                <script>
                    CSS.paintWorklet.addModule('paint.js');
                </script>
            
example-1 produces an invalid image as "red" does not match the registered grammar. example-2 produces an invalid image as there are too many function arguments.
10. Let |workletGlobalScope| be a {{PaintWorkletGlobalScope}} from the list of worklet's WorkletGlobalScopes from the paint {{Worklet}}, following the rules defined in [[#global-scope-selection]]. The user agent may also create a WorkletGlobalScope at this time, given the paint {{Worklet}}. 11. Run invoke a paint callback given |name|, |inputArguments|, |snappedConcreteObjectSize|, |workletGlobalScope| optionally in parallel. Note: If the user agent runs invoke a paint callback on a thread in parallel, it should select a paint worklet global scope which can be used on that thread.
When the user agent wants to invoke a paint callback given |name|, |inputArguments|, |snappedConcreteObjectSize|, and |workletGlobalScope|, it must run the following steps: 1. Let |paintDefinitionMap| be |workletGlobalScope|'s paint definitions map. 2. If |paintDefinitionMap|[|name|] does not exist, run the following steps: 1. Queue a task to run the following steps: 1. Let |documentPaintDefinitionMap| be the associated document's document paint definitions map. 2. Set |documentPaintDefinitionMap|[|name|] to "invalid". 3. The user agent should log an error to the debugging console stating that a class wasn't registered in all {{PaintWorkletGlobalScope}}s. 2. Let the image output be an invalid image and abort all these steps. Note: This handles the case where there could be a paint worklet global scope which didn't receive the {{registerPaint(name, paintCtor)}} for |name| (however another global scope did). A paint callback which is invoked on the other global scope could succeed, but wont succeed on a subsequent frame when draw a paint image is called. 3. Let |definition| be the result of get |paintDefinitionMap|[|name|]. 4. Let |paintClassInstanceMap| be |workletGlobalScope|'s paint class instances map. 5. Let |paintInstance| be the result of get |paintClassInstanceMap|[|name]|. If |paintInstance| is null, run the following steps: 1. If the constructor valid flag on |definition| is false, let the image output be an invalid image and abort all these steps. 2. Let |paintCtor| be the class constructor on |definition|. 3. Let |paintInstance| be the result of Construct(|paintCtor|). If construct throws an exception, set the |definition|'s constructor valid flag to false, let the image output be an invalid image and abort all these steps. 4. Set |paintClassInstanceMap|[|name|] to |paintInstance|. 6. Let |inputProperties| be |definition|'s input properties. 7. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the computed value's for properties listed in |inputProperties|. 8. Let |renderingContext| be the result of create a PaintRenderingContext2D object given: - "width" - The width given by |concreteObjectSize|. - "height" - The height given by |concreteObjectSize|. - "paintRenderingContext2DSettings" - The PaintRenderingContext2DSettings object given by |definition|. Note: The |renderingContext| is not be re-used between invocations of paint. Implicitly this means that there is no stored data, or state on the |renderingContext| between invocations. For example you can't setup a clip on the context, and expect the same clip to be applied next time the paint method is called. Note: Implicitly this also means that |renderingContext| is effectively "neutered" after a paint method is complete. The author code may hold a reference to |renderingContext| and invoke methods on it, but this will have no effect on the current image, or subsequent images. 9. Let |paintSize| be a new {{PaintSize}} initialized to the width and height defined by |snappedConcreteObjectSize|. 10. At this stage the user agent may re-use an image from a previous invocation if |paintSize|, |styleMap|, |inputArguments| are equivalent to that previous invocation. If so let the image output be that cached image and abort all these steps.
In the example below, both div-1 and div-2 have paint functions which have equivalent javascript arguments. A user-agent can cache the result of one invocation and use it for both elements.
                // paint.js
                registerPaint('simple', class {
                    paint(ctx, size) {
                        ctx.fillStyle = 'green';
                        ctx.fillRect(0, 0, size.width, size.height);
                    }
                });
            
                <style>
                    .div-1 {
                        width: 50px;
                        height: 50px;
                        background-image: paint(simple);
                    }
                    .div-2 {
                        width: 100px;
                        height: 100px;

                        background-size: 50% 50%;
                        background-image: paint(simple);
                    }
                </style>
                <div class=div-1></div>
                <div class=div-2></div>
                <script>
                    CSS.paintWorklet.addModule('paint.js');
                </script>
            
11. Let |paintFunctionCallback| be |definition|'s paint function. 12. Invoke |paintFunctionCallback| with arguments «|renderingContext|, |paintSize|, |styleMap|, |inputArguments|», and with |paintInstance| as the callback this value. If |paintFunctionCallback| does not complete within an acceptable time (as determined by the user agent, i.e. it is a "long running script") the user agent may terminate the script, let the image output be an invalid image, and abort all these steps. Note: User agents could provide tooling within their debugging tools to show authors how expensive their paint classes are. User agents could also how an "unresponsive script" dialog in this case if appropriate. 13. The image output is to be produced from the |renderingContext| given to the method. If an exception is thrown the let the image output be an invalid image. Note: The contents of the resulting image are not designed to be accessible. Authors can communicate any useful information through the standard accessibility APIs.
Global Scope Selection {#global-scope-selection} ------------------------------------------------ When the user agent needs to select a {{PaintWorkletGlobalScope}} from the paint worklet's WorkletGlobalScopes list it must: - Select from at least two {{PaintWorkletGlobalScope}}s, unless the user agent is under memory constraints. - Not re-use the same {{PaintWorkletGlobalScope}} more than 1000 times in a row. Note: The 1000 limit was picked as a high upper bound, this limit may improve (downwards) over time. Note: These rules exist to ensure that authors do not rely on being able to store state on the global object or non-regeneratable state on the class. See [[worklets-1#code-idempotency]]. Examples {#examples} ==================== Example 1: Colored Circle {#example-1} -------------------------------------- The example below makes use of the fact that <> functions are able to be animated. E.g. when the textarea is focused in the example below, the --circle-color property will transition from deepskyblue to purple. This ability isn't limited to just transitions, it also applies to CSS animations, and the Web Animations API.
<!DOCTYPE html>
<style>
  #example {
    --circle-color: deepskyblue;

    background-image: paint(circle);
    font-family: sans-serif;
    font-size: 36px;
    transition: --circle-color 1s;
  }

  #example:focus {
    --circle-color: purple;
  }
</style>

<textarea id="example">
  CSS is awesome.
</textarea>

<script>
    CSS.registerProperty({
      name: '--circle-color',
      syntax: '<color>',
      initialValue: 'black',
      inherits: false
    });
    CSS.paintWorklet.addModule('circle.js');
</script>
// circle.js
registerPaint('circle', class {
  static get inputProperties() { return ['--circle-color']; }
  paint(ctx, geom, properties) {
    // Change the fill color.
    const color = properties.get('--circle-color');
    ctx.fillStyle = color.cssText;

    // Determine the center point and radius.
    const x = geom.width / 2;
    const y = geom.height / 2;
    const radius = Math.min(x, y);

    // Draw the circle \o/
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
    ctx.fill();
  }
});
Example 2: Image Placeholder {#example-2} ----------------------------------------- It is possible for an author to use paint to draw a placeholder image while an image is being loaded.
<!DOCTYPE html>
<style>
#example {
    --image: url('#someUrlWhichIsLoading');
    background-image: paint(image-with-placeholder);
}
</style>

<div id="example"></div>

<script>
    CSS.registerProperty({
        name: '--image',
        syntax: '<image> | none',
        initialValue: 'none',
    });
    CSS.paintWorklet.addModule('image-placeholder.js');
</script>
// image-placeholder.js
registerPaint('image-with-placeholder', class {
    static get inputProperties() { return ['--image']; }
    paint(ctx, geom, properties) {
        const img = properties.get('--image');

        switch (img.state) {
            case 'ready':
                // The image is loaded! Draw the image.
                ctx.drawImage(img, 0, 0, geom.width, geom.height);
                break;
            case 'pending':
                // The image is loading, draw some mountains.
                drawMountains(ctx);
                break;
            case 'invalid':
            default:
                // The image is invalid (e.g. it didn't load), draw a sad face.
                drawSadFace(ctx);
                break;
        }
    }
});
Example 3: Arcs {#example-3} ----------------------------
<!DOCTYPE html>
<style>
#example {
  width: 200px;
  height: 200px;

  background-image:
    paint(arc, purple, 0.4turn, 0.8turn, 40px, 15px),
    paint(arc, blue, -20deg, 170deg, 30px, 20px),
    paint(arc, red, 45deg, 220deg, 50px, 10px);
}
</style>

<div id="example"></div>

<script>
    CSS.paintWorklet.addModule('arc.js');
</script>
// arc.js
registerPaint('arc', class {
  static get inputArguments() {
    return [
      '<color>',
      '<angle>',  // startAngle
      '<angle>',  // endAngle
      '<length>', // radius
      '<length>', // lineWidth
    ];
  }

  paint(ctx, geom, _, args) {
    ctx.strokeStyle = args[0].cssText;

    // Determine the center point.
    const x = geom.width / 2;
    const y = geom.height / 2;

    // Convert the start and end angles to radians.
    const startAngle = this.convertAngle(args[1]) - Math.PI / 2;
    const endAngle = this.convertAngle(args[2]) - Math.PI / 2;

    // Convert the radius and lineWidth to px.
    const radius = this.convertLength(args[3]);
    const lineWidth = this.convertLength(args[4]);

    ctx.lineWidth = lineWidth;

    ctx.beginPath();
    ctx.arc(x, y, radius, startAngle, endAngle, false);
    ctx.stroke();
  }

  convertAngle(angle) {
    switch (angle.unit) {
      case 'deg':
        return angle.value * Math.PI / 180;
      case 'rad':
        return angle.value;
      case 'grad':
        return angle.value * Math.PI / 200;
      case 'turn':
        return angle.value * Math.PI / 0.5;
      default:
        throw Error(`Unknown angle unit: ${angle.unit}`);
    }
  }

  convertLength(length) {
    switch (length.type) {
      case 'px':
        return length.value;
      default:
        throw Error(`Unkown length type: ${length.type}`);
    }
  }
});
Example 4: Different Colors (based on size) {#example-4} --------------------------------------------------------
<h1>
    Heading 1
</h1>
<h1>
    Another heading
</h1>

<style>
h1 {
    background-image: paint(heading-color);
}
</style>

<script>
    CSS.paintWorklet.addModule('heading-color.js');
</script>
// heading-color.js
registerPaint('heading-color', class {
    static get inputProperties() { return []; }
    paint(ctx, geom, properties) {
        // Select a color based on the width and height of the image.
        const width = geom.width;
        const height = geom.height;
        const color = colorArray[(width * height) % colorArray.length];

        // Draw just a solid image.
        ctx.fillStyle = color;
        ctx.fillRect(0, 0, width, height);
    }
});
Example 5: Drawing outside an element's area {#example-5} --------------------------------------------------------- It is possible to draw outside an element's area by using the 'border-image' property.
<style>
#overdraw {
    --border-width: 10;

    border-style: solid;
    border-width: calc(var(--border-width) * 1px);

    border-image-source: paint(overdraw);
    border-image-slice: 0 fill;
    border-image-outset: calc(var(--border-width) * 1px);

    width: 200px;
    height: 200px;
}
</style>
<div id="overdraw"></div>
<script>
    CSS.paintWorklet.addModule('overdraw.js');
</script>
// overdraw.js
registerPaint('overdraw', class {
    static get inputProperties() { return ['--border-width']; }
    paint(ctx, geom, properties) {
        const borderWidth = parseInt(properties.get('--border-width'));
        ctx.shadowColor = 'rgba(0,0,0,0.25)';
        ctx.shadowBlur = borderWidth;

        ctx.fillStyle = 'rgba(255, 255, 255, 1)';
        ctx.fillRect(borderWidth,
                     borderWidth,
                     geom.width - 2 * borderWidth,
                     geom.height - 2 * borderWidth);
    }
});
Security Considerations {#security-considerations} ================================================== There are no known security issues introduced by these features. Privacy Considerations {#privacy-considerations} ================================================ There are no known privacy issues introduced by these features.