Title: CSS Painting API Level 1 Status: ED Group: houdini ED: https://drafts.css-houdini.org/css-paint-api-1/ Previous Version: http://www.w3.org/TR/2016/WD-css-paint-api-1-20160607/ Shortname: css-paint-api Level: 1 Abstract: Editor: Shane Stephens, shanestephens@google.com Editor: Ian Kilpatrick, ikilpatrick@chromium.org Editor: Dean Jackson, dino@apple.com
spec:css-break-3; type:dfn; text:fragment
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: TypeErrorIntroduction {#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 <
partial interface CSS { [SameObject] readonly attribute Worklet paintWorklet; };The {{PaintWorkletGlobalScope}} is the global execution context of the {{paintWorklet}}.
[Global=(Worklet,PaintWorklet),Exposed=PaintWorklet] interface PaintWorkletGlobalScope : WorkletGlobalScope { void registerPaint(DOMString name, VoidFunction paintCtor); };
class MyPaint { static get inputProperties() { return ['--foo']; } static get alpha() { return true; } paint(ctx, size, styleMap) { // Paint code goes here. } }
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 |alphaValue| be the result of Get(|paintCtor|, "alpha").
8. Let |alpha| be true
if |alphaValue| is undefined, otherwise let it be the result
of converting |alphaValue| to a boolean. If an exception is thrown,
rethrow the exception and abort all these steps.
Note: Setting alpha
is false
allows user agents to anti-alias text
an addition to performing "visibility" optimizations, e.g. not painting an image behind
the paint image as the paint image is opaque.
9. If the result of IsConstructor(|paintCtor|) is false, throw a TypeError
and abort all these steps.
10. Let |prototype| be the result of Get(|paintCtor|, "prototype").
11. If the result of Type(|prototype|) is not Object, throw a TypeError and
abort all these steps.
12. Let |paint| be the result of Get(|prototype|, "paint").
13. If the result of IsCallable(|paint|) is false, throw a TypeError and
abort all these steps.
14. Let |definition| be a new paint image definition with:
- paint image name being |name|
- paint class constructor being |paintCtor|
- paint function being |paint|
- paint constructor valid flag being true
- paint input property list being |inputProperties|.
- paint context alpha flag being |alpha|.
15. Set |paintImageDefinitionMap|[|name|] to |definition|.
16. Queue a task to run the following steps:
1. Let |paintInputPropertiesMap| be the associated document's paint input
properties map.
2. If |paintInputPropertiesMap|[|name|] exists run the following substeps:
1. Let |existingInputProperties| be the result of get
|paintInputPropertiesMap|[|name|].
2. If |existingInputProperties| and |inputProperties| are not equivalent, set |paintInputPropertiesMap|[|name|] to "invalid"
.
3. Otherwise, set |paintInputPropertiesMap|[|name|] to |inputProperties|.
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 may be able to set an option 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 <> )
background-image: paint(my_logo);
[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. When the user agent is to create a PaintRenderingContext2D object for a given |width|, |height| and |alpha| it must run the following steps: 1. Create a new {{PaintRenderingContext2D}}. 2. Set bitmap dimensions for the context's output bitmap to |width| and |height|. 3. Set the {{PaintRenderingContext2D}}'s alpha flag to |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 an image {#drawing-an-image} ==================================== If 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 must draw
a paint image and display the result on the current frame.
[Exposed=PaintWorklet] interface PaintSize { readonly attribute double width; readonly attribute double height; };When the user agent wants to draw a paint image of a <
"invalid"
, let
the image output be an invalid image and abort all these steps.
8. Let |workletGlobalScope| be a {{PaintWorkletGlobalScope}} from the list of worklet's
WorkletGlobalScopes from the paint {{Worklet}}.
The user agent may also create a WorkletGlobalScope given the paint
{{Worklet}} and use that.
Note: The user agent may use any policy for which {{PaintWorkletGlobalScope}} to
select or create. It may use a single {{PaintWorkletGlobalScope}} or multiple and
randomly assign between them.
9. Run invoke a paint callback given |name|, |concreteObjectSize|, |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|, |concreteObjectSize|,
|workletGlobalScope|, it must run the following steps:
1. Let |paintImageDefinitionMap| be |workletGlobalScope|'s paint image definitions map.
2. If |paintImageDefinitionMap|[|name|] does not exist, run the following
substeps:
1. Queue a task to run the following substeps:
1. Let |paintInputPropertiesMap| be the associated document's paint input
properties map.
2. Set |paintInputPropertiesMap|[|name|] to "invalid"
.
2. Let the image output be an invalid image and abort all these steps.
Note: This handles the case where there may 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 may succeed, but wont
succeed on a subsequent frame when draw a paint image is called.
3. Let |definition| be the result of get |paintImageDefinitionMap|[|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 substeps:
1. If the paint 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 paint class constructor on |definition|.
3. Let |paintInstance| be the result of Construct(|paintCtor|).
If Construct throws an exception, set the |definition|'s paint 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 paint input property list.
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|.
- "alpha" - The paint context alpha flag given by |definition|.
Note: The |renderingContext| must 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
|concreteObjectSize|.
10. Let |paintFunctionCallback| be |definition|'s paint function.
11. Invoke |paintFunctionCallback| with arguments «|renderingContext|, |paintSize|,
|styleMap|», and with |paintInstance| as the callback this value.
12. 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 user agent should consider long running paint functions similar to long running
script in the main execution context. For example, they should show a "unresponsive
script" dialog or similar. In addition user agents should provide tooling within their
debugging tools to show authors how expensive their paint classes are.
Note: The contents of the resulting image are not designed to be accessible. Authors should
communicate any useful information through the standard accessibility APIs.
Examples {#examples}
====================
Example 1: A colored circle. {#example-1}
-----------------------------------------
<div id="myElement"> CSS is awesome. </div> <style> #myElement { --circle-color: red; background-image: paint(circle); } </style> <script> CSS.paintWorklet.import('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; // 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.
<div id="myElement"> </div> <style> #myElement { --image: url('#someUrlWhichIsLoading'); background-image: paint(image-with-placeholder); } </style> <script> CSS.registerProperty({ name: '--image', syntax: '<image>' }); CSS.paintWorklet.import('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: Conic-gradient {#example-3} -------------------------------------- Issue: Add conic-gradient as a use case once we have function arguments. Example 4: Different color based on size {#example-4} -----------------------------------------------------
<h1> Heading 1 </h1> <h1> Another heading </h1> <style> h1 { background-image: paint(heading-color); } </style> <script> CSS.paintWorklet.import('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); } });