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
spec:infra; type:dfn; text:list spec:webidl; type:dfn; text:converting spec:html; type:dfn; text:set bitmap dimensions; text:reset the rendering context to its default state
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 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 <
"paintworklet"
.
partial namespace 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; };
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. } }
DOMStrings
.
- A PaintRenderingContext2DSettings object.
A document paint definition is a [=struct=] which describes the information
needed by the [=document=] about the author defined <DOMStrings
.
- A input argument syntaxes which is a [=list=] of
[=syntax definitions=].
- 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 <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.
7. Filter |inputProperties| so that it only contains [=supported CSS properties=] and
[=custom properties=].
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
.
8. Let |inputArguments| be an empty sequence<DOMString>
.
9. Let |inputArgumentsIterable| be the result of [=Get=](|paintCtor|, "inputArguments").
10. If |inputArgumentsIterable| is not undefined, then set |inputArguments| to the result of
[=converting=] |inputArgumentsIterable| to a sequence<DOMString>
. If an
exception is thrown, rethrow the exception and abort all these steps.
11. Let |inputArgumentSyntaxes| be an [=list/empty=] [=list=].
12. [=list/For each=] |item| in |inputArguments| perform the following substeps:
1. Attempt to [=consume a syntax definition=] from |item|.
If failure was returned, [=throw=] a [=TypeError=] and abort all these steps.
Otherwise, let |parsedSyntax| be the returned [=syntax definition=].
2. [=list/Append=] |parsedSyntax| to |inputArgumentSyntaxes|.
13. Let |contextOptionsValue| be the result of [=Get=](|paintCtor|, "contextOptions").
14. 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.
15. If the result of [=IsConstructor=](|paintCtor|) is false, [=throw=] a [=TypeError=]
and abort all these steps.
16. Let |prototype| be the result of [=Get=](|paintCtor|, "prototype").
17. If the result of [=Type=](|prototype|) is not Object, [=throw=] a [=TypeError=] and
abort all these steps.
18. Let |paintValue| be the result of [=Get=](|prototype|, "paint").
19. Let |paint| be the result of [=converting=] |paintValue| to the [=Function=]
[=callback function=] type. Rethrow any exceptions from the conversion.
20. Let |definition| be a new [=paint definition=] with:
- [=paint definition/class constructor=] being |paintCtor|.
- [=paint function=] being |paint|.
- [=paint definition/constructor valid flag=] being true.
- [=paint definition/input properties=] being |inputProperties|.
- [=paint definition/PaintRenderingContext2DSettings object=] being |paintRenderingContext2DSettings|.
21. [=map/Set=] |paintDefinitionMap|[|name|] to |definition|.
22. [=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:
- [=document paint definition/input properties=] being |inputProperties|.
- [=document paint definition/input argument syntaxes=] being
|inputArgumentSyntaxes|.
- [=document paint definition/PaintRenderingContext2DSettings object=] being |paintRenderingContext2DSettings|.
3. If |documentPaintDefinitionMap|[|name|] [=map/exists=], run the following steps:
1. Let |existingDocumentDefinition| be the result of [=map/get=]
|documentPaintDefinitionMap|[|name|].
2. If |existingDocumentDefinition| is "invalid"
, abort all these steps.
3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is
[=document paint definition/input properties=], input argument syntaxes, and PaintRenderingContext2DSettings object are different), then:
[=map/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, [=map/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() = paint( <The <>, < >? )
<style> .logo { background-image: paint(company-logo); } .chat-bubble { background-image: paint(chat-bubble, blue); } </style>
[Exposed=PaintWorklet] interface PaintRenderingContext2D { }; PaintRenderingContext2D includes CanvasState; PaintRenderingContext2D includes CanvasTransform; PaintRenderingContext2D includes CanvasCompositing; PaintRenderingContext2D includes CanvasImageSmoothing; PaintRenderingContext2D includes CanvasFillStrokeStyles; PaintRenderingContext2D includes CanvasShadowStyles; PaintRenderingContext2D includes CanvasRect; PaintRenderingContext2D includes CanvasDrawPath; PaintRenderingContext2D includes CanvasDrawImage; PaintRenderingContext2D includes CanvasPathDrawingStyles; PaintRenderingContext2D includes 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 [=PaintRenderingContext2D/output bitmap=] is the [=concrete object size=] of the object it is rendering to. A {{PaintRenderingContext2D}} object also has an alpha flag, which can be set to true or false. Initially, when the context is created, its alpha flag must be set to true. When a {{PaintRenderingContext2D}} object has its alpha flag set to false, then its alpha channel must be fixed to 1.0 (fully opaque) for all pixels, and attempts to change the alpha component of any pixel must be silently ignored. The size of the [=PaintRenderingContext2D/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 [=PaintRenderingContext2D/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 [=PaintRenderingContext2D/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.
registerPaint('currentcolor', class { paint(ctx, size) { ctx.fillStyle = 'currentColor'; ctx.fillRect(0, 0, size.width, size.height); } });
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.
[Exposed=PaintWorklet] interface PaintSize { readonly attribute double width; readonly attribute double height; };
"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.
// 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.
"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 [=paint definition/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 definition/class constructor=] on |definition|.
3. Let |paintInstance| be the result of [=Construct=](|paintCtor|).
If [=construct=] throws an exception,
set the |definition|'s [=paint definition/constructor valid flag=] to false,
let the image output be an [=invalid image=] and abort all these
steps.
4. [=map/Set=] |paintClassInstanceMap|[|name|] to |paintInstance|.
6. Let |inputProperties| be |definition|'s [=paint definition/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 |snappedConcreteObjectSize|.
- "height" - The height given by |snappedConcreteObjectSize|.
- "paintRenderingContext2DSettings" - The
[=paint definition/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.
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>
--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} ================================================ * The timing of paint callbacks can be used as a high-bandwidth channel for detecting "visited" state for links. (details) This is not a fundamentally new privacy leak, as visited state leaks from many interactions, but absent any further mitigations, this is a particularly high-bandwidth channel of the information. No official mitigations are planned at this time, as this privacy leak needs to be addressed more directly to fix all such channels. Changes {#changes} ================== Changes since the 9 August 2018 CR publication: * Filtered the list of input properties to a paint worklet to be only known or custom properties. * Added alpha flag to {{PaintRenderingContext2D}} to control whether the rendering surface is forced opaque or allows transparency. * Fix definition for the size of the output bitmap: > The size of the output bitmap is the concrete object size of the object it is rendering to