1. Introduction
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 <image> function.
Note: In a future version of the spec, support may be added for defining the clip, global alpha, filter on a portion of a box (for example on the background layers).
2. Paint Invalidation
A document has a map of paint input properties. Initially it is empty and
is populated when registerPaint(name, paintCtor)
is called.
Each <paint()> function for a box has an associated paint valid flag. It may be either paint-valid or paint-invalid. It is initially set to paint-invalid.
When the size (as determined by layout) of a box changes, each <paint()> function’s paint valid flag should be set to paint-invalid.
When the computed style for a box changes, the user agent must run the following steps:
-
For each <paint()> function on the box, perform the following substeps:
-
Let paintFunction be the current <paint()> function on the box.
-
Let name be the first argument of the <paint()> function.
-
Let paintInputPropertiesMap be the associated document’s paint input properties map.
-
Let inputProperties be the result of get paintInputPropertiesMap[name].
-
For each property in inputProperties, if the property’s computed value has changed, set the paint valid flag on the paintFunction to paint-invalid.
-
Performing draw a paint image results in the paint valid flag for a <paint()> function on a box to be set to paint-valid.
Note: In a future version of the spec, support may be added for partial invalidation. The user agent will be able to specify a region of the rendering context which needs to be re-painted by the paint class.
3. 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 Window { [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. } }
4. Concepts
A paint image definition describes an author defined <image> which can be referenced by the <paint()> function. It consists of:
-
A paint image name.
-
A paint class constructor which is the class constructor.
-
A paint function which is the paint function callback.
-
A paint constructor valid flag.
-
A paint input property list.
-
A paint context alpha flag.
5. Registering Custom Paint
The PaintWorkletGlobalScope
has a map of paint image definitions. Initially
this map is empty; it is populated when registerPaint(name, paintCtor)
is called.
The 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 <paint()> 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:
-
If the name is an empty string, throw a TypeError and abort all these steps.
-
Let paintImageDefinitionMap be
PaintWorkletGlobalScope
's paint image definitions map. -
If paintImageDefinitionMap[name] exists throw a NotSupportedError and abort all these steps.
-
Let inputProperties be an empty
sequence<DOMString>
-
Let inputPropertiesIterable be the result of Get(paintCtor, "inputProperties").
-
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
.
-
Let alphaValue be the result of Get(paintCtor, "alpha").
-
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
isfalse
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. -
If the result of IsConstructor(paintCtor) is false, throw a TypeError and abort all these steps.
-
Let prototype be the result of Get(paintCtor, "prototype").
-
If the result of Type(prototype) is not Object, throw a TypeError and abort all these steps.
-
Let paint be the result of Get(prototype, "paint").
-
If the result of IsCallable(paint) is false, throw a TypeError and abort all these steps.
-
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.
-
-
Set paintImageDefinitionMap[name] to definition.
-
Queue a task to run the following steps:
-
Let paintInputPropertiesMap be the associated document’s paint input properties map.
-
If paintInputPropertiesMap[name] exists run the following substeps:
-
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.
6. Paint Notation
paint() = paint( <ident> )
The <paint()> function is an additional notation to be supported by the <image> type.
For the cursor property, the <paint()> function should be treated as an invalid image and fallback to the next supported <image>.
Support additional arbitrary arguments for the paint function. This is difficult to specify, as you need to define a sane grammar. A better way would be to expose a token stream which you can parse into Typed OM objects. This would allow a full arbitrary set of function arguments, and be future proof. <https://github.com/w3c/css-houdini-drafts/issues/100>
7. The 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 CanvasHitRegion
, 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:
-
Create a new
PaintRenderingContext2D
. -
Set bitmap dimensions for the context’s output bitmap to width and height.
-
Set the
PaintRenderingContext2D
's alpha flag to alpha. -
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.
8. Drawing an image
If a <paint()> function for a fragment is paint-invalid and the fragment is within the visual viewport, then user agent must draw a paint image for the current frame. The user agent may not defer the draw a paint image operation until a subsequent frame.
Note: The user agent may choose to draw a paint image for <paint()> functions not within the visual viewport.
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.
The draw a paint image function should be invoked by the user agent during the object size negotiation algorithm which is responsible for rendering an <image>.
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 may be able 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 concrete object size given by the user agent.
[Exposed=PaintWorklet] interface PaintSize { readonly attribute double width; readonly attribute double height; };
When the user agent wants to draw a paint image of a <paint()> function for a box into its appropriate stacking level (as defined by the property the CSS property its associated with), given its concreteObjectSize (concrete object size) it must run the following steps:
-
Let paintFunction be the <paint()>> function on the box which the user agent wants to draw.
-
If the paint valid flag for the paintFunction is paint-valid the user agent may use the drawn image from the previous invocation. If so it may abort all these steps and use the previously drawn image.
Note: The user agent for implementation reasons may also continue with all these steps in this case. It can do this every frame, or multiple times per frame.
-
Set the paint valid flag for the paintFunction to paint-valid.
-
Let name be the first argument of the paintFunction.
-
Let paintInputPropertiesMap be the associated document’s paint input properties map.
-
If paintInputPropertiesMap[name] does not exist, let the image output be an invalid image and abort all these steps.
-
If the result of get paintInputPropertiesMap[name] is
"invalid"
, let the image output be an invalid image and abort all these steps. -
Let workletGlobalScope be a
PaintWorkletGlobalScope
from the list of worklet’s WorkletGlobalScopes from the paintWorklet
.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 singlePaintWorkletGlobalScope
or multiple and randomly assign between them. -
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:
-
Let paintImageDefinitionMap be workletGlobalScope’s paint image definitions map.
-
If paintImageDefinitionMap[name] does not exist, run the following substeps:
-
Queue a task to run the following substeps:
-
Let paintInputPropertiesMap be the associated document’s paint input properties map.
-
Set paintInputPropertiesMap[name] to
"invalid"
.
-
-
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. -
-
Let definition be the result of get paintImageDefinitionMap[name].
-
Let paintClassInstanceMap be workletGlobalScope’s paint class instances map.
-
Let paintInstance be the result of get paintClassInstanceMap[|name]|. If paintInstance is null run the following substeps:
-
If the paint constructor valid flag on definition is false, let the image output be an invalid image and abort all these steps.
-
Let paintCtor be the paint class constructor on definition.
-
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.
-
Set paintClassInstanceMap[name] to paintInstance.
-
-
Let inputProperties be definition’s paint input property list.
-
Let styleMap be a new
StylePropertyMapReadOnly
populated with only the computed value’s for properties listed in inputProperties. -
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.
-
-
Let paintSize be a new
PaintSize
initialized to the width and height defined by concreteObjectSize. -
Let paintFunctionCallback be definition’s paint function.
-
Invoke paintFunctionCallback with arguments «renderingContext, paintSize, styleMap», and with paintInstance as the callback this value.
-
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.
9. Examples
9.1. Example 1: A colored circle.
<div id="myElement"> CSS is awesome. </div> <style> #myElement { --circle-color: red; background-image: paint(circle); } </style> <script> 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(); } });
9.2. Example 2: Image placeholder.
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> document.registerProperty({ name: '--image', syntax: '<image>' }); 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; } } });
9.3. Example 3: Conic-gradient
Add conic-gradient as a use case once we have function arguments.
9.4. Example 4: Different color based on size
<h1> Heading 1 </h1> <h1> Another heading </h1> <style> h1 { background-image: paint(heading-color); } </style> <script> 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); } });