diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 00000000..a9cdee70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,2 @@ +* please tag the issue title with the spec's shortname, like `[css-foo]` +* please link to the spec section you're talking about, or at least the spec diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..04f41dc4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6e204604 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Cascading Style Sheets (CSS) Working Group + +Contributions to this repository are intended to become part of Recommendation-track documents governed by the +[W3C Patent Policy](https://www.w3.org/Consortium/Patent-Policy-20040205/) and +[Software and Document License](https://www.w3.org/Consortium/Legal/copyright-software). To make substantive contributions to specifications, you must either participate +in the relevant W3C Working Group or make a non-member patent licensing commitment. + +If you are not the sole contributor to a contribution (pull request), please identify all +contributors in the pull request comment. + +To add a contributor (other than yourself, that's automatic), mark them one per line as follows: + +``` ++@github_username +``` + +If you added a contributor by mistake, you can remove them in a comment with: + +``` +-@github_username +``` + +If you are making a pull request on behalf of someone else but you had no part in designing the +feature, you can remove yourself with the above syntax. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..0f7c218c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,4 @@ +All documents in this Repository are licensed by contributors +under the +[W3C Software and Document License](https://www.w3.org/Consortium/Legal/copyright-software). + diff --git a/README.markdown b/README.markdown index dc1b3b40..68254b6e 100755 --- a/README.markdown +++ b/README.markdown @@ -1,9 +1,22 @@ ### [CSS-TAG Houdini Task Force](https://wiki.css-houdini.org/) Specifications -This is the repository containing all the [CSS/TAG Houdini Task Force specifications](https://drafts.css-houdini.org/). +This is the repository containing the [CSS/TAG Houdini Task Force specifications](https://drafts.css-houdini.org/). In addition to this git repository, a Mercurial mirror is maintained at `https://hg.css-houdini.org/drafts`, if for whatever reason you prefer Mercurial. Specification issues are raised and discussed in GitHub Issues in this repository. We also maintain the [public-houdini mailing list](http://lists.w3.org/Archives/Public/public-houdini/) for general-interest topics. + +New specifications are generally first incubated in the WICG, in particular: +- [Animation Worklet](https://github.com/WICG/animation-worklet) + +# Tests + +For normative changes, a corresponding +[web-platform-tests](https://github.com/web-platform-tests/wpt) PR is highly appreciated. Typically, +both PRs will be merged at the same time. Note that a test change that contradicts the spec should +not be merged before the corresponding spec change. If testing is not practical, please explain why +and if appropriate [file an issue](https://github.com/web-platform-tests/wpt/issues/new) to follow +up later. Add the `type:untestable` or `type:missing-coverage` label as appropriate. + diff --git a/box-tree-api/Overview.bs b/box-tree-api/Overview.bs index 572a9ef6..2b71cd5b 100644 --- a/box-tree-api/Overview.bs +++ b/box-tree-api/Overview.bs @@ -6,15 +6,15 @@ ED: https://drafts.css-houdini.org/box-tree-api-1/ Shortname: box-tree-api Level: 1 Abstract: Layout as described by CSS produces boxes that control how content is displayed and positioned. This specification describes an API for accessing information about these boxes. -Editor: Tab Atkins, jackalmage@gmail.com -Editor: Peter Linss, peter.linss@hp.com -Editor: Ian Kilpatrick, ikilpatrick@chromium.org -Editor: Rossen Atanassov, rossen.atanassov@microsoft.com -Editor: Shane Stephens, shanestephens@google.com +Editor: Tab Atkins-Bittner, Google, http://xanthir.com/contact/, w3cid 42199 +Editor: Peter Linss, peter.linss@hp.com, w3cid 4200 +Editor: Ian Kilpatrick, ikilpatrick@chromium.org, w3cid 73001 +Editor: Rossen Atanassov, rossen.atanassov@microsoft.com, w3cid 49885 +Former Editor: Shane Stephens, shanestephens@google.com, w3cid 47691

Introduction

@@ -72,6 +72,7 @@ Boxes are not explicitly exposed by this API.

API

+[Exposed=Window]
 interface DeadFragmentInformation {
 	readonly attribute Node node;
 	readonly attribute double width;
@@ -79,7 +80,7 @@ interface DeadFragmentInformation {
 	readonly attribute double top;
 	readonly attribute double left;
 	readonly attribute boolean isOverflowed;
-	readonly attribute sequence<DeadFragmentInformation>? children;
+	readonly attribute FrozenArray<DeadFragmentInformation>? children;
 	readonly attribute DeadFragmentInformation? nextSibling;
 	readonly attribute DeadFragmentInformation? previousSibling;
 	readonly attribute DeadFragmentInformation? nextInBox;
diff --git a/composited-scrolling-and-animation/Explainer.md b/composited-scrolling-and-animation/Explainer.md
deleted file mode 100644
index a1a9c1b9..00000000
--- a/composited-scrolling-and-animation/Explainer.md
+++ /dev/null
@@ -1,182 +0,0 @@
-# Compositor Worker explained
-
-## tl;dr
-
-requestAnimationFrame on the "compositor" thread.
-
-## So, what's the problem here?
-
-Scripted effects (driven by requestAnimationFrame, response to onscroll, etc)
-are flexible and powerful, but are subject to main thread jank (where jank
-refers to unpredictable interruptions in the rate that animations are serviced
-on a thread due to other, unrelated work on that thread). And although script
-can run on a web worker, DOM access and CSS property animations are not
-permitted. Despite their susceptibility to main thread jank, main thread
-animations are widely used; they're the only way to create common effects such
-as position-sticky, image carousels, custom scroll animations, iphone-style
-contact lists, and physics-based animations. In a perfect world, the main thread
-would always be responsive enough to guarantee that an animation callback would
-be serviced every frame. In reality, this is often extremely hard to achieve,
-both for user agents and developers of large sites composed of disparate, 3rd
-party components not under the author's control. The result is a lot of janky
-pages.
-
-Why can't we update CSS properties from another thread? Updating CSS properties
-could effect a style recalc or a layout and those operations must happen on the
-main thread. That said, there are certain 'layout-free' properties that can be
-modified without these side effects. These properties include transform,
-opacity, background color, background position, etc. Updating the scroll offset
-is also layout-free. Clearly identifying these layout-free properties and
-allowing them to be animated from a separate scheduling domain would provide a
-simple and powerful way to achieve smooth animations.
-
-Indeed, all major browsers have had the ability to asynchronously update
-layout-free properties for years. Some examples include WebAnimations, CSS
-animations, and transitions involving layout-free properties as well as
-compositor driven scrolling. A few new approaches are in the works such as
-position:sticky and snap points, but it's unfortunate to have to wait for specs
-and consistent browser implementations to get these new "acclerated" effects.
-
-## Goal
-
-Allow new accelerated, input/time-based effects to be authored in script.
-Address
-[this](https://github.com/w3c/css-houdini-drafts/blob/master/scroll-customization-api/UseCases.md) list of use cases.
-
-## High level approach
-
- - Introduce CompositorWorker whose global scope exposes requestAnimationFrame
-   which runs at the rate of threaded scrolling and animation.
- - Allow DOM elements to be wrapped in a CompositorProxy which may be sent to a
-   CompositorWorker and which exposes a limited set of layout-free properties
-   and input events.
-
-## A tiny, but expressive kernel
-
-This small set of primitives would permit a surprisingly large number of
-existing and proposed browser features to be implemented as polyfills.
-
- - Portions of [Web Animations](http://dev.w3.org/fxtf/web-animations/)
- - [Touch-based Animation Scrubbing](https://docs.google.com/document/d/1vRUo_g1il-evZs975eNzGPOuJS7H5UBxs-iZmXHux48/edit)
- - [position:sticky](http://updates.html5rocks.com/2012/08/Stick-your-landings-position-sticky-lands-in-WebKit)
- - [Smooth Scrolling](http://dev.w3.org/csswg/cssom-view/), Sections 4, 5, 7, 12, and 13.
- - Accelerated CSS animations. ([I/O talk](http://www.youtube.com/watch?v=hAzhayTnhEI))
- - [Snap Points](https://www.w3.org/TR/css-snappoints-1/)
-
-## Examples
-
-### Example 1. Hello, World!
-
-Here we create a trivial, time-based animation.
-
-Main Thread
-```JavaScript
-// The list of properties determine which properties may be
-// modified on the CompositorWorker.
-var proxy = new CompositorProxy(element, ['transform']);
-
-// If a requested property may not modified, then |supports|
-// will return false as follows.
-console.log('transform may be modified: ' + proxy.supports('transform'));
-
-// The UA is free to drive a CompositorWorker from a thread of its
-// choosing, provided it fires requestAnimationFrame callbacks in
-// sync with compositor driven scrolling, modulo script exceeding
-// frame budget. If, for whatever reason, a CompositorWorker’s
-// requested compositor frame callbacks are unable to complete in
-// time, the worker’s onerror handler will be called with a
-// TimeoutError. See comment below about the application of effects.
-// In future, we will almost certainly specify other behaviors when
-// the worker’s frame callbacks cannot complete (e.g., kill the
-// worker), but the initial behavior will simply be to let the
-// effect fall out of sync with other compositor-driven effects.
-var worker = new CompositorWorker(‘my_script.js’);
-worker.postMessage(proxy);
-```
-On the CompositorWorker
-```JavaScript
-onmessage = function(e) {
-    var proxy = e.data;
-    var tick = function(timestamp) {
-        var t = proxy.transform;
-        t.m42 = 100.0 * Math.sin(timestamp / 1000.0);
-        proxy.transform = t;
-        requestAnimationFrame(tick);
-    };
-    // All requestAnimationFrame callbacks are processed as a unit;
-    // their effects will all be applied in the same frame.
-    requestAnimationFrame(tick);
-};
-```
-### Example 2. Parallax
-
-Although we could implement parallax by checking the scroll position
-every frame, it's more efficient if we only update when we've actually
-scrolled. Here's a proposal for how that might look.
-
-Main Thread
-```JavaScript
-var scroller_proxy = new CompositorProxy(scroller, ['scrollTop']);
-var background_proxy = new CompositorProxy(background, ['transform']);
-var worker = new CompositorWorker('parallax.js');
-worker.postMessage({
-  'scroller': scroller_proxy,
-  'background': background_proxy
-});
-```
-
-On the CompositorWorker
-```JavaScript
-onmessage = function(e) {
-  var scroller = e.data.scroller;
-  var background = e.data.background;
-  var update = function(timestamp) {
-    var t = background.transform;
-    t.m42 = 0.8 * scroller.scrollTop;
-    background.transform = t;
-    // In this case, |update| will be called iff a property
-    // of scroller's has been updated.
-    requestAnimationFrame(update, [scroller]);
-  };
-  requestAnimationFrame(update, [scroller]);
-};
-```
-
-### Example 3. Input
-
-This example shows how we might implement a simple drawer. The way input will be provided to a CompositorWorker is very much an open question, so treat the following as speculative and likely to change.
-
-Main Thread
-```JavaScript
-var drawer_proxy = new CompositorProxy(drawer, [transform]);
-var worker = new CompositorWorker('drawer.js');
-worker.postMessage(drawer_proxy);
-```
-
-On the CompositorWorker
-```JavaScript
-onmessage = function(e) {
-  var drawer = e.data;
-  drawer.addEventListener('touchstart', function(e) {
-    drawer.initialX = e.touches[0].pageX;
-  });
-  drawer.addEventListener('touchmove', function(e) {
-    var t = drawer.transform;
-    t.m41 = e.touches[0].pageX - drawer.initialX;
-    drawer.transform = t;
-  });
-};
-```
-
-## Common Concerns
-
-### Are we marrying ourselves to implementation details?
-
-Virtually all user agents support (via CSS) accelerated opacity and transform animations, and they’re going to have to support them for the foreseeable future. Threaded scrolling is also increasingly common. By whatever means browsers are currently able to guarantee that things can slide around, scroll and fade in and out efficiently, they could potentially permit these effects to be driven by JavaScript on a web worker. It doesn’t, for example, tie us to the idea of a composited layer or a layer tree, concepts that may not be meaningful in all browser implementations. The animated elements might, say, be redrawn by the GPU each frame. But this doesn’t matter. These implementation details are orthogonal to the animation proxy concept.
-
-### What happens when script runs long?
-
-On some platforms or user agents, it may be unacceptable or impossible to slow down native scrolling. We must have a fallback. There are a number of reasonable failure modes, and one that seems like a natural default.
- * _Default_: Fall out of sync. In this case, the user agent would attempt to retrieve a value from the various CompositorWorker requestAnimationFrame callbacks, but if they have not finished within the frame budget, they will be applied in a subsequent frame, falling out of sync with, say, compositor driven scrolling. When this happens the worker's onerror function will be called with a TimeoutError so that the author has the option to react.
- * Force all compositor-driven effects to the main thread. In this case, the effects will remain synchronized, though they will become susceptible to main thread jank. Again, we would communicate failure via onerror.
- * Abort the effect. I.e., if we cannot keep up with the compositor, do not call requestAnimationFrame for that CompositorWorker again. This would be useful in cases where scroll/animation performance is critical, but the effect is a nice-to-have. As always, failure is communicated via onerror.
diff --git a/css-scroll-api/Makefile b/css-animation-worklet-1/Makefile
old mode 100755
new mode 100644
similarity index 94%
rename from css-scroll-api/Makefile
rename to css-animation-worklet-1/Makefile
index 8658b1f2..63fe58ab
--- a/css-scroll-api/Makefile
+++ b/css-animation-worklet-1/Makefile
@@ -6,7 +6,7 @@
 
 SOURCEFILE=Overview.bs
 OUTPUTFILE=Overview.html
-PREPROCESSOR=bikeshed.py
+PREPROCESSOR=bikeshed
 REMOTE_PREPROCESSOR_URL=https://api.csswg.org/bikeshed/
 
 all: $(OUTPUTFILE)
diff --git a/css-animation-worklet-1/Overview.bs b/css-animation-worklet-1/Overview.bs
new file mode 100644
index 00000000..4f883932
--- /dev/null
+++ b/css-animation-worklet-1/Overview.bs
@@ -0,0 +1,1286 @@
+
+Title:  CSS Animation Worklet API
+Status: ED
+Group: houdini
+ED: https://drafts.css-houdini.org/css-animation-worklet-1/
+TR: https://www.w3.org/TR/css-animation-worklet-1/
+Shortname: css-animation-worklet
+Level: 1
+Abstract:
+Editor: Majid Valipour, majidvp@google.com, w3cid 81464
+Editor: Robert Flack, flackr@chromium.org, w3cid 98451
+Editor: Stephen McGruer, smcgruer@chromium.org, w3cid 96463
+Ignored Terms: AnimationWorklet
+
+ + + +
+urlPrefix: https://heycam.github.io/webidl/; type: dfn;
+    text: NotSupportedError
+    urlPrefix: #dfn-;
+        text: callback this value
+        text: exception
+        text: throw
+        url: throw; text: thrown
+    urlPrefix: #;
+        url: Function; text: Function
+        url: VoidFunction; text: VoidFunction
+    url: invoke-a-callback-function; text: Invoke
+    url: construct-a-callback-function; text: constructing
+    url: es-type-mapping; text: converting
+urlPrefix: https://html.spec.whatwg.org/#; type: dfn;
+    url: run-the-animation-frame-callbacks; text: running the animation frame callbacks
+urlPrefix: http://w3c.github.io/html/infrastructure.html#; type: dfn;
+    text: structuredserialize
+    text: structureddeserialize
+urlPrefix: https://www.w3.org/TR/css3-transitions/#; type: dfn;
+    text: animatable properties
+urlPrefix: https://drafts.csswg.org/web-animations#; type: dfn;
+    url: the-documents-default-timeline; text: default document timeline
+    url: concept-animation; text: animation
+    text: effect value
+    text: effect stack
+    text: target property
+    text: timeline
+    text: animation effect
+    text: current time
+    text: local time
+    text: inherited time
+    text: ready
+    text: play state
+    text: playback rate
+    text: set the target effect of an animation
+    text: set the timeline of an animation
+    text: finished
+    text: idle
+    text: paused
+    text: pending
+    text: running
+    text: composite operation
+    text: animation class
+    text: replace state
+    text: active
+    text: persisted
+    text: removed
+    text: start delay
+    text: end delay
+    text: fill mode
+    text: iteration start
+    text: iteration count
+    text: iteration duration
+    text: playback direction
+    text: timing function
+    text: set the start time
+    text: set the current time
+    text: update the timing properties of an animation effect
+
+
+
+urlPrefix: https://w3c.github.io/web-animations/level-2/#;
+    type: dfn;
+        text: group effect
+        text: child effect
+urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
+    text: IsCallable
+    text: IsConstructor
+    text: HasProperty
+    url: ecmascript-data-types-and-values; text: Type
+    url: map-objects; text:map object
+    url: get-o-p; text: Get
+    url: set-o-p-v-throw; text: Set
+    url: samevalue; text: SameValue
+    urlPrefix: native-error-types-used-in-this-standard-
+        text: TypeError
+urlPrefix: https://www.w3.org/TR/hr-time-2/#dom-; type: dfn
+    text: DOMHighResTimeStamp
+urlPrefix: https://wicg.github.io/scroll-animations/#; type: interface
+    url: scrolltimeline; text: ScrollTimeline
+    url: dictdef-scrolltimelineoptions; text: ScrollTimelineOptions
+    url: dom-scrolltimeline-scrollsource; text: scrollSource
+urlPrefix: https://wicg.github.io/scroll-animations/#; type: dfn
+    url: current-time-algorithm; text: current time of the ScrollTimeline;
+
+ +
+{
+    "explainer": {
+        "href": "https://github.com/w3c/css-houdini-drafts/blob/master/css-animation-worklet-1/README.md",
+        "title": "Animation Worklet Explainer",
+        "deliveredBy": [
+            "https://github.com/w3c/css-houdini-drafts"
+        ]
+    },
+    "principles": {
+        "href": "https://github.com/w3c/css-houdini-drafts/blob/master/css-animation-worklet-1/principles.md",
+        "title": "Animation Worklet Design Principles and Goals",
+        "deliveredBy": [
+            "https://github.com/w3c/css-houdini-drafts"
+        ]
+    }
+}
+
+ +Introduction {#intro} +===================== +This section is not normative. + +This document introduces a new primitive that provides extensibility in web animations and enables +high performance interactive procedural animations on the web. For details on the rationale and +motivation see both [[explainer]] and [[principles]]. + +The [=Animation Worklet=] API provides a method to create scripted animations that control a set +of [=animation effects=]. The API is designed to make it possible for user agents to run such +animations in their own dedicated thread to provide a degree of performance isolation from main +thread. + +Relationship to the Web Animations API {#relationship-to-web-animations} +------------------------------------------------------------------------ +This section is not normative. + +Animations running inside an [=Animation Worklet=] execution context expose the {{Animation}} +interface from the Web Animations specification on the main javascript execution context. This means +they can be controlled and inspected from main thread using the same Web Animation APIs. + + +Animation Worklet {#animation-worklet-desc} +=========================================== +Animation Worklet is a {{Worklet}} responsible for all classes related to custom +animations. The worklet can be accessed via {{animationWorklet}} attribute. + + +[Exposed=Window] +partial namespace CSS { + [SameObject] readonly attribute Worklet animationWorklet; +}; + + +The {{animationWorklet}}'s [=worklet global scope type=] is {{AnimationWorkletGlobalScope}}. + +{{AnimationWorkletGlobalScope}} represents the global execution context of {{animationWorklet}}. + + +[ Global=(Worklet,AnimationWorklet), Exposed=AnimationWorklet ] +interface AnimationWorkletGlobalScope : WorkletGlobalScope { + undefined registerAnimator(DOMString name, AnimatorInstanceConstructor animatorCtor); +}; + +callback AnimatorInstanceConstructor = any (any options, optional any state); + + + +Animator {#animator-desc} +========================= + +An Animator represents a custom animation that is running inside +{{AnimationWorkletGlobalScope}}. Each Animator is associated with an [=animation=] instance (of +type {{WorkletAnimation}}) in the document and determines how that animation progresses its +[=animation effect=]. The animate function contains the logic responsible for +translating the animation current time into appropriate progress of the animation effect. An +animator can only be instantiated by construction of a {{WorkletAnimation}} in the document. + + +Two animators types are supported: [=Stateless Animator=] and [=Stateful Animator=] each +providing a different state management strategy. + + +Stateless Animator {#stateless-animator-desc} +--------------------------------------------- + +A Stateless Animator is a type of animator does not depend on any local state either +stored on the instance or global scope. Effectively, the animate function of an +[=Stateless Animator=] can be treated as a pure function with the expectation that given the same +input, it produces the same output. + + +
+ This is how an stateless animator class should look. +
+        class FooAnimator {
+            constructor(options) {
+                // Called when a new animator is instantiated.
+            }
+            animate(currentTime, effect) {
+                // Animation frame logic goes here.
+            }
+        }
+    
+
+ +Note: The statelessness allows animation worklet to perform optimization such as producing multiple +animation frames in parallel, sharing a single animator instance for multiple animations, and +performing very cheap teardown and setup. Using [=Stateless Animator=] is highly recommended to +enable such optimizations. + +Stateful Animator {#stateful-animator-desc} +------------------------------------------- + +A Stateful Animator is a type of animator that can have local state and animation worklet +guarantees that it maintains this state as long as the stateful animator fulfills the contract +required by its interface and as described following. + + +[=Animation worklet=] maintains a set of {{WorkletGlobalScope}}s which may exist across different +threads or processes. Animation worklet may temporarily terminate a global scope (e.g., to preserve +resources) or move a running [=animator instance=] across different global scopes (e.g., if its +effect is mutable only in a certain thread). Animation worklet guarantees that a stateful animator +instance's state is maintained even if the instance is respawned in a different global scope. + +The basic mechanism for maintaining the state is that the animation worklet snapshots the local +state that is exposed via the [=state function=] and then reifies it so that it can be passed into +the constructor when the animator instance is respawned at a later time in a potentially different +global scope. The [=migrate an animator instance=] algorithm specifies this process in details. + +A user-defined stateful animator is expected to fulfill the required contract which is that its +state function returns an object representing its state that can be serialized using structured +serialized algorithm and that it can also recreate its state given that same object passed to its +constructor. + +
+ This is how a stateful animator class should look. +
+        class BarAnimator {
+            constructor(options, state) {
+              // Called when a new animator is instantiated (either first time or after being respawned).
+              this.currentVelocity  = state ? state.velocity : 0;
+            }
+            animate(currentTime, effect) {
+                // Animation frame logic goes here and can rely on this.currentVelocity.
+                this.currentVelocity += 0.1;
+            }
+            state() {
+              // The returned object should be serializable using structured clonable algorithm.
+              return {
+                velocity: this.currentVelocity;
+              }
+            }
+        }
+    
+
+ + +Animator Definition {#animator-definition-desc} +----------------------------------------------- + +An animator definition is a [=struct=] which describes the author defined custom +animation logic. It consists of: + + - : animator name + :: A <>#. + + - : class constructor + :: A {{AnimatorInstanceConstructor}} [=callback function=] type. + + - : animate function + :: A [=Function=] [=callback function=] type. + + - : state function + :: A [=Function=] [=callback function=] type. + + - : stateful flag + :: A boolean flag + + +A stateful animator definition is an [=animator definition=] whose +[=animator definition/stateful flag=] is true. + + +A document animator definition is a [=struct=] which describes the information needed by +the [=document=] about the author defined custom animation. It consists of: + + - : stateful flag + :: A boolean flag + +Registering an Animator Definition {#registering-animator-definition} +--------------------------------------------------------------------- +The [=document=] has a [=map=] of document animator definitions. The map gets populated +when {{registerAnimator(name, animatorCtor)}} is called. + +An {{AnimationWorkletGlobalScope}} has a [=map=] of animator definitions. The map gets +populated when {{registerAnimator(name, animatorCtor)}} is called. + +Note that to register a [=stateful animator definition=] it is simply enough for the registered +class to have a state function. + +
+ +When the registerAnimator(|name|, |animatorCtor|) +method is called in a {{AnimationWorkletGlobalScope}}, the user agent must run the +following steps: + + 1. If |name| is not a valid <>, [=throw=] a [=TypeError=] and abort all these + steps. + + 2. Let |animatorDefinitions| be the {{AnimationWorkletGlobalScope}}'s + [=animator definitions=] [=map=]. + + 3. If |animatorDefinitions|[|name|] [=map/exists=], [=throw=] a [=NotSupportedError=] + and abort all these steps. + + 4. If the result of [=IsConstructor=](|animatorCtor|) is false, [=throw=] a + [=TypeError=] and abort all these steps. + + 5. Let |prototype| be the result of [=Get=](|animatorCtor|, "prototype"). + + 6. Let |animateFuncValue| be the result of [=Get=](|prototype|, "animate"). + + 7. Let |animateFunc| be the result of [=converting=] |animateFuncValue| to the [=Function=] + [=callback function=] type. If an exception is thrown, rethrow the exception and abort + all these steps. + + 8. Let |stateFuncValue| be the result of [=Get=](|prototype|, "state"). + + 9. Let |stateFunc| be the result of [=converting=] |stateFuncValue| to the [=Function=] + [=callback function=] type, If an exception is thrown, set |stateful| to be false, + otherwise set |stateful| to be true and |stateFunc| to be undefined. + + 10. Let |definition| be a new [=animator definition=] with: + + - [=animator name=] being |name| + + - [=class constructor=] being |animatorCtor| + + - [=animate function=] being |animateFunc| + + - [=state function=] being |stateFunc| + + - [=animator definition/stateful flag=] being |stateful| + + + 9. [=map/set=] the |animatorDefinitions|[|name|] to |definition|. + + 10. [=Queue a task=] to run the following steps: + + 1. Let |documentAnimatorDefinitions| be the associated [=document's=] + [=document animator definitions=] [=map=]. + + 2. Let |documentDefinition| be a new [=document animator definition=] with: + + - [=animator definition/stateful flag=] being |stateful| + + 3. If |documentAnimatorDefinitions|[|name|] [=map/exists=], run the following steps: + + 1. Let |existingDocumentDefinition| be the result of [=map/get=] + |documentAnimatorDefinitions|[|name|]. + + 2. If |existingDocumentDefinition| is "invalid", abort all these steps. + + 3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is + their [=document animator definition/stateful flag=]s are + different), then: + + [=map/set=] |documentAnimatorDefinitions|[|name|] to "invalid". + + Log an error to the debugging console stating that the same class was registered + with different stateful flag. + + 4. Otherwise, [=map/set=] |documentAnimatorDefinitions|[|name|] to + |documentDefinition|. + +
+ + +Animator Effect {#animator-effect-desc} +--------------------------------------- + +A Animator Effect represents the underlying [=animation effect=] inside animation +worklet. + +It has a corresponding effect property which is a reference to the underlying +[=animation effect=]. It also has corresponding properties for the following +[=animation effect=]'s properties: + * [=local time=], + * [=start delay=], + * [=end delay=], + * [=fill mode=], + * [=iteration start=], + * [=iteration count=], + * [=iteration duration=], + * [=playback direction=], and + * [=timing function=]. + +[=Animator Effect=] is represented by the {{WorkletAnimationEffect}} interface +inside {{AnimationWorkletGlobalScope}}. + + + +[ Exposed=AnimationWorklet ] +interface WorkletAnimationEffect { + EffectTiming getTiming(); + ComputedEffectTiming getComputedTiming(); + attribute double? localTime; +}; + + + +Note: {{WorkletAnimationEffect}} is basically a restricted version of {{AnimationEffect}} interface + which does not have {{AnimationEffect/updateTiming}} but additionally allows local time to be set. + +
+ +: getTiming() +:: Returns the specified timing properties using the corresponding properties. + +: getComputedTiming() +:: Returns the calculated timing properties using the corresponding properties. + +: localTime +:: Getting the attribute returns the corresponding [=local time=]. + Setting the attribute updates the local time given this effect as |effect| + and the attribute value as |time|: + + 1. If the |time| is the same as |effect|'s [=local time=] then skip following steps. + + 2. Set the |effect|'s [=local time=] to |time|. + + 3. Set the |effect|'s animator instance's [=sync requested flag=] to true. + +
+ + +Animator Instance {#animator-instance-section} +============================================== + +An animator instance is a [=struct=] which describes a fully realized custom +[=animator=] in an {{AnimationWorkletGlobalScope}}. It has a reference to an +[=animator definition=] and owns the instance specific state such as animation effect and +timeline. It consists of: + + - : [=animator name=] + :: A string used to identify the animator definition. + + - : [=frame requested flag=] + :: A boolean flag that indicates if the animator needs to animate. + + - : sync requested flag + :: A flag that indicates if the animator needs to sync its output. + + - : effect + :: An [=Animator Effect=]. + + - : animator current time + :: A time value equivalent to the corresponding [=worklet animation=]'s current time. + + - : animator timeline + :: The [=timeline=] of the corresponding [=worklet animation=]. + + - : animator serialized options + :: The serializable object representing the options to be used when constructing the animator + instance. + +A stateful animator instance is an [=animator instance=] whose corresponding +definition is a [=stateful animator definition=]. + + + +Creating an Animator Instance {#creating-animator-instance} +----------------------------------------------------------- + +Each [=animator instance=] lives in an {{AnimationWorkletGlobalScope}}. + +Each {{AnimationWorkletGlobalScope}} has an animator instance set. The set is populated +when the user agent constructs a new [=animator instance=] in the {{AnimationWorkletGlobalScope}} +scope. Each [=animator instance=] corresponds to a worklet animation in the document scope. + +
+ +To create a new animator instance given a |name|, |timeline|, |effect|, +|serializedOptions|, |serializedState|, and |workletGlobalScope|, the user agent must run +the following steps: + + 1. Let the |definition| be the result of looking up |name| on the |workletGlobalScope|'s + [=animator definitions=]. + + If |definition| does not exist abort the following steps. + + 2. Let |animatorCtor| be the [=class constructor=] of |definition|. + + 3. Let |options| be [=StructuredDeserialize=](|serializedOptions|). + + 4. Let |state| be [=StructuredDeserialize=](|serializedState|). + + 5. Let |animatorInstance| be the result of [=constructing=] |animatorCtor| with + «|options|, |state|» as arguments. If an exception is thrown, rethrow the exception and + abort all these steps. + + 6. Let |animatorEffect| be the result of [=constructing=] a {{WorkletAnimationEffect}} + with its [=corresponding effect=] being |effect|. + + 7. Set the following on |animatorInstance| with: + - [=animator name=] being |name| + - [=frame requested flag=] being false + - [=sync requested flag=] being false + - [=animator current time=] being unresolved + - [=effect=] being |animatorEffect| + - [=animator timeline=] being |timeline| + - [=animator serialized options=] being |options| + + 8. Add |animatorInstance| to |workletGlobalScope|'s [=animator instance set=]. + +
+ + +Running Animators {#running-animators} +-------------------------------------- + +When the user agent wants to produce a new animation frame, if for any [=animator instance=] the +associated [=frame requested flag=] is true then the the user agent must +[=run animators=] for the current frame in all its associated global scopes. + +Note: The user agent is not required to run animations on every visual frame. It is legal to defer + generating an animation frame until a later frame. This allow the user agent to + provide a different service level according to their policy. + +
+ +When the user agent wants to run animators in a given |workletGlobalScope|, it +must run the following steps: + + 1. Iterate over all [=animator instance=]s in the |workletGlobalScope|'s animator instance + set. For each such |animator| the user agent must perform the following steps: + + 1. Let |animatorName| be |animator|'s [=animator name=] + + 2. Let the |definition| be the result of looking up |animatorName| on the + |workletGlobalScope|'s [=animator definitions=]. + + If |definition| does not exist then abort the following steps. + + 3. If the [=frame requested flag=] for |animator| is false or the effect belonging + to the |animator| will not be visible within the visual viewport of the current frame + the user agent may abort all the following steps. + + Issue: Consider giving user agents permission to skip running individual animator + instances to throttle slow animators. + + 4. Let |animateFunction| be |definition|'s [=animate function=]. + + 5. Let |currentTime| be [=animator current time=] of |animator|. + + 6. Let |effect| be [=effect=] of |animator|. + + 7. [=Invoke=] |animateFunction| with arguments «|currentTime|, |effect|», + and with |animator| as the [=callback this value=]. + + 2. If any [=animator instance=]s in the |workletGlobalScope|'s [=animator instance set=] + has its [=sync requested flag=] set to true then [=sync local times to document=] + given |workletGlobalScope|. + +
+ +Note: Although inefficient, it is legal for the user agent to [=run animators=] multiple times +in the same frame. + + +Issue: should be explicit as to what happens if the animateFunction throws an exception. At least +we should have wording that the localTime values of the effects are ignored to avoid incorrect +partial updates. + +Removing an Animator Instance {#removing-animator} +-------------------------------------------------- + +
+ +To remove an animator instance given |animator| and |workletGlobalScope| the user agent +must run the following steps: + +1. Remove |animator| from |workletGlobalScope|'s [=animator instance set=]. + +
+ + +Migrating an Animator Instance {#migrating-animator} +---------------------------------------------------- + +The migration process allows [=stateful animator instance=] to be migrated to a different +{{AnimationWorkletGlobalScope}} without losing their local state. + +
+ +To migrate an animator instance from one {{AnimationWorkletGlobalScope}} to another, +given |animator|, |sourceWorkletGlobalScope|, |destinationWorkletGlobalScope|, the user agent +must run the following steps : + + 1. Let |serializedState| be undefined. + + 2. [=Queue a task=] on |sourceWorkletGlobalScope| to run the following steps: + + 1. Let |animatorName| be |animator|'s [=animator name=] + + 2. Let |definition| be the result of looking up |animatorName| on |sourceWorkletGlobalScope|'s + [=animator definitions=]. + + If |definition| does not exist then abort the following steps. + + 3. Let |stateful| be the [=animator definition/stateful flag=] of |definition|. + + 4. If |stateful| is false then abort the following steps. + + 5. Let |stateFunction| be |definition|'s [=state function=]. + + 6. Let |state| be the result of [=Invoke=] |stateFunction| with |animator| as the + [=callback this value=]. If any exception is thrown, rethrow the exception and abort + the following steps. + + 7. Set |serializedState| to be the result of [=StructuredSerialize=](|state|). + If any exception is thrown, then abort the following steps. + + 8. Run the procedure to [=remove an animator instance=] given |animator|, and + |sourceWorkletGlobalScope|. + + 2. Wait for the above task to complete. If the task is aborted, abort the following steps. + + 3. [=Queue a task=] on |destinationWorkletGlobalScope| to run the following steps: + + 1. Run the procedure to [=create a new animator instance=] given: + - The |animator|'s [=animator name=] as name. + - The |animator|'s [=animator timeline=] as timeline. + - The |animator|'s [=effect=] as effect. + - The |animator|'s [=animator serialized options=] as options. + - The |serializedState| as state. + - The |destinationWorkletGlobalScope| as workletGlobalScope. + +
+ +If an animator state getter throws the user agent will remove the animator but does not recreate it. +This effectively removes the animator instance. + + +Requesting Animation Frames {#requesting-animation-frames} +---------------------------------------------------------- + +Each [=animator instance=] has an associated frame requested flag. It is initially set +to false. Different circumstances can cause the [=frame requested flag=] to be set to +true. These include the following: + - Changes in the [=current time=] of the animator's [=timeline=] + - Changes in the [=current time=] of the animator's corresponding [=worklet animation=] + +Performing [=run animators=] resets the [=frame requested flag=] on animators to false. + + +Web Animations Integration {#web-animation-integration} +======================================================= + + +Worklet Animation {#worklet-animation-desc} +------------------------------------------- +Worklet animation is a kind of [=animation=] that delegates animating its animation +effect to an [=animator instance=]. It controls the lifetime and playback state of its +[=corresponding animator instance=]. + +Being an [=animation=], [=worklet animation=] has an [=animation effect=] and a +[=timeline=]. However unlike other animations the worklet animation's [=current time=] does +not directly determine the animation effect's [=local time=] (via its [=inherited time=]). +Instead the associated [=animator instance=] controls the animation effect's [=local time=] +directly. Note that this means that the [=timeline's=] current time does not fully determine the +animation's output. + +[=Worklet animation=] has the following properties in addition to the {{Animation}} interface: + - : animation animator name + :: A string that identifies its [=animator definition=]. + - : serialized options + :: A serializable options object that is used whe constructing a new animator instance. + - : corresponding animator instance + :: A [=Animator Instance=]. + + +The existence of [=corresponding animator instance=] for a [=worklet animation=] depends on +the animation [=play state=]. See [[#web-animation-overrides]] for details on when and this +correspondence changes. + + +[Exposed=Window] +interface WorkletAnimation : Animation { + constructor(DOMString animatorName, + optional (AnimationEffect or sequence<AnimationEffect>)? effects = null, + optional AnimationTimeline? timeline, + optional any options); + readonly attribute DOMString animatorName; +}; + + + +
+ Overview of the WorkletAnimation timing model. +
+ Overview of the worklet animation timing model. +
+ The animation current time is input to the animator instance, which produces a local time value + for the animation effect. If the animator instance is running in a parallel global scope the + implementation may also choose to use the local time value to produce the animation output and + update the visuals in parallel. + +
+
+ + +Creating a Worklet Animation {#creating-worklet-animation} +---------------------------------------------------------- + +
+WorkletAnimation(|animatorName|, |effects|, |timeline|, |options|) + +Creates a new {{WorkletAnimation}} object using the following procedure: + + 1. Let |documentAnimatorDefinitions| be the associated [=document's=] document animator + definitions [=map=]. + + 2. If |documentAnimatorDefinitions|[|animatorName|] does not [=map/exists=], [=throw=] an + [=TypeError=] and abort the following steps. + + 3. If |documentAnimatorDefinitions|[|animatorName|] is "invalid", [=throw=] an + [=TypeError=] and abort the following steps. + + 4. Let |workletAnimation| be a new {{WorkletAnimation}} object. + + 5. Run the procedure to [=set the timeline of an animation=] on |workletAnimation| passing + |timeline| as the new timeline or, if a |timeline| argument is not provided, + passing the [=default document timeline=] of the {{Document}} associated with the + {{Window}} that is the [=current global object=]. + + 6. Let |effect| be the result corresponding to the first matching condition from below. + : If |effects| is a {{AnimationEffect}} object, + :: Let effect be |effects|. + : If |effects| is a [=list=] of {{AnimationEffect}} objects, + :: Let |effect| be a new {{WorkletGroupEffect}} with its children set to |effects|. + : Otherwise, + :: Let |effect| be undefined. + + 7. Let |serializedOptions| be the result of [=StructuredSerialize=](|options|). + Rethrow any exceptions. + + 8. Set the [=serialized options=] of |workletAnimation| to |serializedOptions|. + + 9. Set the [=animation animator name=] of |workletAnimation| to |animatorName|. + + 10. Run the procedure to [=set the target effect of an animation=] on |workletAnimation| + passing |effect| as the new effect. Note that this may trigger action to + [=set animator instance of worklet animation=]. See [[#web-animation-overrides]] for more + details. + +
+ + +Worklet Animation Timing and Sync Model {#timing-and-sync-model} +---------------------------------------------------------------- + +This section describes how [=worklet animation's=] timing model differs from other +[=animations=]. + +As described in [[#worklet-animation-desc]], the [=worklet animation's=] [=current time=] does +not determine its [=animation effect's=] [=local time=]. Instead the associated +[=animator instance=] controls the animation effect's [=local time=] directly. This means that the +animation effect's local time is controlled from a {{WorkletGlobalScope}} which may be in a parallel +execution context. + +Here are a few implications of the above semantics: + + - Setting the [=current time=] or [=start time=] of a [=worklet animation=] does not + necessarily change its output, but may change the animation [=play state=]. + - Similarly, invoking {{Animation/finish()}} or updating a [=worklet animation's=] playback + rate does not necessarily change its output, but may change the animation [=play state=] + - Querying the animation effect's local time using {{AnimationEffect/getComputedTiming()}} + may return stale information, in the case where the [=animator instance=] is running in a + parallel execution context. + + +If a Worklet Animation animation is executing in a parallel worklet execution context, the last +known state of its Animator Effects should be periodically synced back to the main javascript +execution context. The synchronization of [=effect values=] from the parallel worklet execution +context to the main javascript execution context must occur before +[=running the animation frame callbacks=] as part of the document lifecycle. + +Note that due to the asynchronous nature of this animation model a script running in the main +javascript execution context may see a stale value when reading a [=target property=] that is +being animated by a Worklet Animation, compared to the value currently being used to produce the +visual frame that is visible to the user. This is similar to the effect of asynchronous scrolling +when reading scroll offsets in the main javascript execution context. + + +
+ +To sync local times to document for a given |workletGlobalScope| the user agent +must perform the action that corresponds to the first matching condition from the +following: + + + : If the |workletGlobalScope| is not running in a parallel execution context + :: perform the following steps immediately: + + : If the |workletGlobalScope| is running in a parallel execution context + :: [=queue a task=] to run the following steps before running the animation frame + callbacks as part of the document lifecycle: + + 1. Iterate over all [=animator instance=]s in the animation worklet's global scope + [=animator instance set=]. For each such |animator| perform the following steps: + + 1. If |animator|'s [=sync requested flag=] is false skip the rest of the steps. + + 2. Let |animatorEffect| be |animator|'s [=effect=]. + + 3. Let |effect| be |animatorEffect|'s [=corresponding effect=]. + + 4. Set |effect|'s local time to |animatorEffect|'s local time. + + 5. Set |animator|'s [=sync requested flag=] to false. + +
+ +
+ +To sync animation timings to worklet for a given |workletAnimation| the user agent +must perform the following steps: + + 1. If |workletAnimation| does not have a [=corresponding animator instance=], abort the + following steps. + + 2. Let |animator| be |workletAnimation|'s [=corresponding animator instance=]. + + 2. Let |workletGlobalScope| be the {{AnimationWorkletGlobalScope}} associated with + |workletAnimation|. + + 3. : If the |workletGlobalScope| is not running in a parallel execution context + :: perform the following steps immediately. + + : If the |workletGlobalScope| is running in a parallel execution context + :: [=queue a task=] to run the following steps: + + 1. Set |animator|'s [=animator current time=] to |workletAnimation|'s [=current time=] + + 2. Let |animatorEffect| be |animator|'s [=effect=]. + + 3. Let |effect| be |animatorEffect|'s [=corresponding effect=]. + + 4. Set the following properties on |animatorEffect| to be the same as |effect|: + * [=start delay=], + * [=end delay=], + * [=fill mode=], + * [=iteration start=], + * [=iteration count=], + * [=iteration duration=], + * [=playback direction=], and + * [=timing function=]. + + +
+ + +Note: Notice that the local time is not synced from the document to animation worklet. + + +Issue(811): Come with appropriate mechanism's for [=animator instance=] to get notified when its + animation currentTime is changing e.g., via reverse(), finish() or playbackRate change. So that + it can react appropriately. + + +Web Animations Overrides {#web-animation-overrides} +--------------------------------------------------- + +In addition to the existing conditions on when the [=animation=] is considered [=ready=], a +[=worklet animation=] is only considered [=ready=] when the following condition is also true: + + - the user agent has completed any setup required to create the [=worklet animation's=] + [=corresponding animator instance=]. + +When a given worklet animation's [=play state=] changes from [=idle=] to [=finished=], +[=running=], or [=paused=], run the procedure to +[=associate animator instance of worklet animation=] given the worket animation as +|workletAnimation|. + +When a given worklet animation's [=play state=] changes from [=finished=], [=running=] or +[=paused=] to [=idle=], run the procedure to +[=disassociate animator instance of worklet animation=]given the worklet animation as +|workletAnimation|. + +When a given worklet animation's [=replace state=] changes from [=active=] to either +[=persisted=] or [=removed=] run the procedure to +[=disassociate animator instance of worklet animation]= given the worklet animation as +|workletAnimation|. + + +Issue: In web-animation play state is updated before the actual change in particular some operations +such as play() are asynchronous. We should really invoke these Animator related operation after the +appropriate animation operation is complete instead of when play state has changed. This will +require either finding (or introducing) q new hook in web animation or having override for each such +async operation. + + +When the procedure to [=set the target effect of an animation=] for a given worklet animation +is called, then [=set animator instance of worklet animation=] given the worklet animation as +|workletAnimation|. + +When the procedure to [=set the timeline of an animation=] for a given |workletAnimation| +is called, then [=set animator instance of worklet animation=] given the worklet animation as +|workletAnimation|. + +When the procedure to [=set the current time=] or [=set the start time=] for a given worklet +animation is called, then [=sync animation timings to worklet=] given the worklet animation as +|workletAnimation|. + +When the procedure to [=update the timing properties of an animation effect=] for a given effect is +called and that effect is owned be a worklet animation, then +[=sync animation timings to worklet=] given that worklet animation as |workletAnimation|. + + +
+ +To associate animator instance of worklet animation given |workletAnimation|, +the user agent must run the following steps: + + 1. If |workletAnimation| has a [=corresponding animator instance=], abort the following steps. + 2. Let |workletGlobalScope| be the {{AnimationWorkletGlobalScope}} associated with + |workletAnimation|. + 3. [=Queue a task=] on |workletGlobalScope| to run the procedure to create a new animator + instance, passing: + * The |workletAnimation|'s [=animation animator name=] as name. + * The |workletAnimation|'s [=timeline=] as timeline. + * The |workletAnimation|'s [=animation effect=] as effect. + * The |workletAnimation|'s [=serialized options=] as options. + * The |workletGlobalScope| as workletGlobalScope. + 4. If the procedure was successful, set the resulting [=animator instance=] to be the + [=corresponding animator instance=] of |workletAnimation|. + +
+ +
+ +To disassociate animator instance of worklet animation given +|workletAnimation|, the user age must run the following steps: + + 1. If |workletAnimation| does not have a [=corresponding animator instance=], abort the + following steps. + 2. Let |workletGlobalScope| be the {{AnimationWorkletGlobalScope}} associated with + |workletAnimation|. + 3. Let |animatorInstance| be |workletAnimation|'s [=corresponding animator instance=]. + 4. [=Queue a task=] on the |workletGlobalScope| to run the procedure to remove an animator + instance, passing |animatorInstance| as instance and |workletGlobalScope| as + workletGlobalScope. + 5. Set |workletAnimation|'s [=corresponding animator instance=] as undefined. + +
+ +
+ +To set animator instance of worklet animation given +|workletAnimation|, the user agent must run the following steps: + + 1. [=disassociate animator instance of worklet animation=] given |workletAnimation|. + 2. [=associate animator instance of worklet animation=] given |workletAnimation|. + +
+ +Effect Stack and Composite Order {#effect-stack-composite-order} +---------------------------------------------------------------- + +As with other animations, [=worklet animations=] participate in the [=effect stack=]. A worklet +animation does not have a specific [=animation class=] which means it has the same composite order +as other Javascript created web animations. + + + + + +Additional Related Concepts {#related-concepts} +=============================================== + + +Worklet Group Effect {#worklet-group-effect} +-------------------------------------------- +This section is not normative. + + +[=Group effect=] is a type of [=animation effect=] that enbales multiple child animation +effects to be animated together as a group. + +{{WorkletGroupEffect}} is a type of [=group effect=] that allows its [=child effect's=] +[=local times=] to be mutated individually. + +When a {{WorkletGroupEffect}} is set as the [=animation effect=] of a [=worklet animation=], +its [=corresponding animator instance=] can directly control the [=child effects=]' local +times. This allows a single worklet animation to coordinate multiple effects - see +[[#example-2]] for an example of such a use-case. + + +[Exposed=AnimationWorklet] +interface WorkletGroupEffect { + sequence<WorkletAnimationEffect> getChildren(); +}; + + + +Issue(w3c/csswg-drafts#2071): The above interface exposes a conservative subset of GroupEffect +proposed as part of web-animation-2. We should instead move this into a delta spec against the +web-animation. + + +Note: Group Effect is currently an experimental feature and not well specified in web animations. So +the concept of {{WorkletGroupEffect}} may change dramatically as [=Group Effect=] get specified. +See https://github.com/yi-gu/group_effect/blob/master/README.md for more details. + + +ScrollTimeline {#scroll-timeline} +--------------------------------- +This section is not normative. + + +{{ScrollTimeline}} is a new concept being proposed for addition to web animation API. It defines +an animation timeline whose time value depends on the scroll position of a scroll container. +[=Worklet animations=] can have a scroll timeline and thus drive their scripted effects based +on a scroll offset. + +Note: Access to input: We are interested on exposing additional user input beside +scrolling (e.g., touch/pointer input) to these animations so that authors can create jank-free +input driven animations which are not really possible today. We are still trying to figure out the +right abstractions and mechanisms to do this. + + + +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. + +Examples {#examples} +==================== + + +Example 1: Spring timing. {#example-1} +--------------------------------------- +Here we use Animation Worklet to create animation with a custom spring timing. + + + + +<div id='target'></div> + +<script> +await CSS.animationWorklet.addModule('spring-animator.js'); +targetEl = document.getElementById('target'); + +const effect = new KeyframeEffect( + targetEl, + {transform: ['translateX(0)', 'translateX(50vw)']}, + {duration: 1000} +); +const animation = new WorkletAnimation('spring', effect, document.timeline, {k: 2, ratio: 0.7}); +animation.play(); +</script> + + + + + +registerAnimator('spring', class SpringAnimator { + constructor(options = {k: 1, ratio: 0.5}) { + this.timing = createSpring(options.k, options.ratio); + } + + animate(currentTime, effect) { + let delta = this.timing(currentTime); + // scale this by target duration + delta = delta * (effect.getTimings().duration / 2); + effect.localTime = delta; + // TODO: Provide a method for animate to mark animation as finished once + // spring simulation is complete, e.g., this.finish() + // See issue https://github.com/w3c/css-houdini-drafts/issues/808 + } +}); + +function createSpring(springConstant, ratio) { + // Normalize mass and distance to 1 and assume a reasonable init velocit + // but these can also become options to this animator. + const velocity = 0.2; + const mass = 1; + const distance = 1; + + // Keep ratio < 1 to ensure it is under-damped. + ratio = Math.min(ratio, 1 - 1e-5); + + const damping = ratio * 2.0 * Math.sqrt(springConstant); + const w = Math.sqrt(4.0 * springConstant - damping * damping) / (2.0 * mass); + const r = -(damping / 2.0); + const c1 = distance; + const c2 = (velocity - r * distance) / w; + + // return a value in [0..distance] + return function springTiming(timeMs) { + const time = timeMs / 1000; // in seconds + const result = Math.pow(Math.E, r * time) * + (c1 * Math.cos(w * time) + c2 * Math.sin(w * time)); + return distance - result; + } +} + + + +Example 2: Twitter header. {#example-2} +--------------------------------------- +An example of twitter profile header effect where two elements (avatar, and header) are updated in +sync with scroll offset with an additional feature where avatar can have additional physic based +movement based on the velocity and acceleration of the scrolling. + + + +<div id='scrollingContainer'> + <div id='header' style='height: 150px'></div> + <div id='avatar'><img></div> +</div> + +// In document scope. +<script> +const headerEl = document.getElementById('header'); +const avatarEl = document.getElementById('avatar'); +const scrollingContainerEl = document.getElementById('scrollingContainer'); + + +const scrollTimeline = new ScrollTimeline({ + scrollSource: scrollingContainerEl, + orientation: 'block', + timeRange: 1000, + startScrollOffset: 0, + endScrollOffset: headerEl.clientHeight +}); + +const effects = [ + /* avatar scales down as we scroll up */ + new KeyframeEffect(avatarEl, + {transform: ['scale(1)', 'scale(0.5)']}, + {duration: scrollTimeline.timeRange}), + /* header loses transparency as we scroll up */ + new KeyframeEffect(headerEl, + {opacity: [0, 0.8]}, + {duration: scrollTimeline.timeRange}) +]; + +await CSS.animationWorklet.addModule('twitter-header-animator.js'); +const animation = new WorkletAnimation('twitter-header', effects, scrollTimeline); + +animation.play(); +</script> + + + + +// Inside AnimationWorkletGlobalScope. +registerAnimator('twitter-header', class HeaderAnimator { + constructor(options, state = {velocity: 0, acceleration: 0}) { + // `state` is either undefined (first time) or it is the previous state (after an animator + // is migrated between global scopes). + this.velocity = state.velocity; + this.acceleration = state.acceleration; + } + + + animate(currentTime, effect) { + const scroll = currentTime; // scroll is in [0, 1000] range + + if (this.prevScroll) { + this.velocity = scroll - this.prevScroll; + this.acceleration = this.velocity - this.prevVelocity; + } + this.prevScroll = scroll; + this.prevVelocity = velocity; + + + // Drive the output group effect by setting its children local times individually. + effect.children[0].localTime = scroll; + + effect.children[1].localTime = curve(velocity, acceleration, scroll); + } + + state() { + // Invoked before any migration attempts. The returned object must be structure clonable + // and will be passed to constructor to help animator restore its state after migration to the + // new scope. + return { + this.velocity, + this.acceleration + } + } + +}); + +curve(scroll, velocity, acceleration) { + + return /* compute an return a physical movement curve based on scroll position, and per frame + velocity and acceleration. */ ; +} + + + +Example 3: Parallax backgrounds. {#example-3} +--------------------------------------------- +A simple parallax background example. + + +<style> +.parallax { + position: fixed; + top: 0; + left: 0; + opacity: 0.5; +} +</style> +<div id='scrollingContainer'> + <div id="slow" class="parallax"></div> + <div id="fast" class="parallax"></div> +</div> + +<script> +await CSS.animationWorklet.addModule('parallax-animator.js'); + +const parallaxSlowEl = document.getElementById('slow'); +const parallaxFastEl = document.getElementById('fast'); +const scrollingContainerEl = document.getElementById('scrollingContainer'); + +const scrollTimeline = new ScrollTimeline({ + scrollSource: scrollingContainerEl, + orientation: 'block', + timeRange: 1000 +}); +const scrollRange = scrollingContainerEl.scrollHeight - scrollingContainerEl.clientHeight; + +const slowParallax = new WorkletAnimation( + 'parallax', + new KeyframeEffect(parallaxSlowEl, + {'transform': ['translateY(0)', 'translateY(' + -scrollRange + 'px)']}, + {duration: scrollTimeline.timeRange}), + scrollTimeline, + {rate : 0.4} +); +slowParallax.play(); + +const fastParallax = new WorkletAnimation( + 'parallax', + new KeyframeEffect(parallaxFastEl, + {'transform': ['translateY(0)', 'translateY(' + -scrollRange + 'px)']}, + {duration: scrollTimeline.timeRange}), + scrollTimeline, + {rate : 0.8} +); +fastParallax.play(); +</script> + + + + +// Inside AnimationWorkletGlobalScope. +registerAnimator('parallax', class ParallaxAnimator { + constructor(options) { + this.rate_ = options.rate; + } + + animate(currentTime, effect) { + effect.localTime = currentTime * this.rate_; + } +}); + diff --git a/css-animation-worklet-1/README.md b/css-animation-worklet-1/README.md new file mode 100644 index 00000000..f88e06b9 --- /dev/null +++ b/css-animation-worklet-1/README.md @@ -0,0 +1,665 @@ +# Animation Worklet Explainer +--- + +# Overview + +Animation Worklet is a new primitive that provides extensibility in web animations and enables high +performance procedural animations on the web. The feature is developed as part of the +[CSS Houdini task force](https://github.com/w3c/css-houdini-drafts/wiki). + +The Animation Worklet API provides a method to create scripted animations that control a set of +animation effects. These animations are executed inside an isolated execution environment, *worklet* +which makes it possible for user agents to run such animations in their own dedicated thread to +provide a degree of performance isolation from main thread. The API is compatible with Web +Animations and uses existing constructs as much as possible. + +# Background + +Scripted interactive effects (written in response to `requestAnimationFrame`, `pointer events` or +async `onscroll` events) are rich but are subject to main thread jankiness. On the other hand, +accelerated CSS transitions and animations can be fast (for a subset of *accelerated* properties) +but are not rich enough to enable [many common use cases](#motivating-use-cases) and currently have +no way to access key user input (pointer events, gestures, scroll). This is why scripted effects are +still very popular for implementing common effects such as hidey-bars, parallax, pull-to-refresh, +drag-and-drop, swipe to dismiss and etc. Animation Worklet provides is key building block for +enabling creation of smooth rich interactive visual effects on the web while also exposing an +extensibility hook in web animations. + + +See the [Animation Worklet design principles and goals](principles.md) for a more extended overview +of the motivations behind Animation Worklet and how the design will be evolved to support a growing +set of use cases. Also see [the status document](status.md) for high level implementation status and +timeline. [Here][roc-thread] you may find an earlier high level discussion on general approaches to +address this problem. + + +# Motivating Use Cases + +* Scroll driven effects: + * Hidey-bar ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/twitter-header/)): animation depends on both scroll and time input. + * Parallax ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/parallax-scrolling/)): simplest scroll-driven effect. + * Custom paginated slider ([demo](http://aw-playground.glitch.me/amp-scroller.html)). + * Pull-to-refresh: animation depends on both touch and time inputs. + * Custom scrollbars. + * High-fidelity location tracking and positioning + * [More examples](https://github.com/w3c/css-houdini-drafts/blob/master/scroll-customization-api/UseCases.md) of scroll-driven effects. +* Gesture driven effects: + * [Image manipulator](https://github.com/w3c/csswg-drafts/issues/2493#issuecomment-422153926) that scales, rotates etc. + * Swipe to Action. + * Drag-N-Drop. + * Tiled panning e.g., Google maps. +* Stateful script driven effects: + * Spring-Sticky effect ([demo](http://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-sticky/)). + * Touch-driven physical environments. + * Expando ([demo](http://googlechromelabs.github.io/houdini-samples/animation-worklet/expando/)): Procedural animations with multiple elements. +* Animated scroll offsets: + * Having multiple scrollers scroll in sync e.g. diff viewer keeping old/new in sync when you + scroll either ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/sync-scroller/)) + * Custom smooth scroll animations (e.g., physic based fling curves) +* Animation Extensibility: + * Custom animation timings (particularly those that are not calculable a priori e.g., [spring demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-timing/)) + * Custom animation sequencing which involves complex coordination across multiple effects. + + +Not all of these usecases are immediately enabled by the current proposed API. However Animation +Worklet provides a powerfull primitive (off main-thread scripted animation) which when combined with +other upcoming features (e.g., +[Event in Worklets](https://github.com/w3c/css-houdini-drafts/issues/834), +[ScrollTimeline](https://wicg.github.io/scroll-animations/), +[GroupEffect](https://github.com/w3c/csswg-drafts/issues/2071)) can address all these usecases and +allows many of currently main-thread rAF-based animations to move off thread with significant +improvement to their smoothness. +See [Animation Worklet design principles and goals](principles.md) for a more extended discussion +of this. + + +***Note***: Demos work best in the latest Chrome Canary with the experimental +web platform features enabled (`--enable-experimental-web-platform-features` +flag) otherwise they fallback to using main thread rAF to emulate the behaviour. + + +# Animation Worklet + +Animation Worklet attempts to address the above usecases by introducing a new primitive that enables +extensibility in the web's core animation model [WebAnimations][WA]): custom frame-by-frame animate +function! + + +## How It Works + +Normally, an active animation takes its timeline time and according to its running state (e.g., +playing, finished) and playback rate, computes its own **current time** which it then uses to set +its keyframe effect **local time**. Here is a simple example of a simple animation: + +```js +const effect = new KeyframeEffect(targetEl, + {transform: ['translateX(0)', 'translateX(50vw)']}, + {duration: 1000} +); +const animation = new Animation(effect, document.timeline); +animation.play(); +``` + + +Animation Worklet allows this transformation from **current time** to **local time** to be +customized via a special Javascript function `animate`. Similar to other Houdini worklets, these +animate functions are called inside a restricted [worklet][worklet] context (`AnimationWorkletGlobalScope`) +which means the don't have access to main document. Another implication is that implementor can run +these off-thread to ensure smooth animations even when main thread is busy which is a key +performance goal for animations. + +To leverage this machinery, web developer creates a special Animation subclass, `WorkletAnimation`. +The only difference is that the WorkletAnimation constructor takes a `name` argument that identifies +the custom animate function to be used. Animation Worklet then creates a corresponding *animater* +instance that represent this particlar animation and then on each animation frame calls its +`animate` function to determine the local time which ultimately drives the keyframe effect. + + +![Overview of the WorkletAnimation Timing Model](img/WorkletAnimation-timing-model.svg) + +Here the same simple example but using Animation Worklet instead. + +**index.html** +```js +// Load your custom animator in the worklet +await CSS.animationWorklet.addModule('animator.js'); + +const effect = new KeyframeEffect(targetEl, + {transform: ['translateX(0)', 'translateX(50vw)']}, + {duration: 1000} +); +const animation = new WorkletAnimation('my-awesome-animator', effect); +animation.play(); +``` + +**animator.js** +``` +registerAnimator('my-awesome-animator', class Passthrough extends StatelessAnimator { + animate(currentTime, effect) { + // The simplest custom animator that does exactly what regular animations do! + effect.localTime = currentTime; + } +}); +``` + + +A few notable things: + + - WorkletAnimation behaves the same as regular animations e.g., it can be played/paused/canceled + - WorkletAnimation can optionally accept an options bag to help the corresponding Animator + configure itself during construction. + - Animator controls the output of the animation by setting the AnimationEffect.localTime + - There is two types of Animators: Stateless and Statefull explicitly marked using superclasses. + +Below are a few more complex example each trying to show a different aspect of Animation Worklet. + +# Examples + +## Spring Timing + +Here we use Animation Worklet to create animation with a custom spring timing. + + +```html + +
+ + +``` + +spring-animator.js: + +```js +registerAnimator('spring', class SpringAnimator extends StatelessAnimator { + constructor(options = {k: 1, ratio: 0.5}) { + this.timing = createSpring(options.k, options.ratio); + } + + animate(currentTime, effect) { + let delta = this.timing(currentTime); + // scale this by target duration + delta = delta * (effect.getTimings().duration / 2); + effect.localTime = delta; + // TODO: Provide a method for animate to mark animation as finished once + // spring simulation is complete, e.g., this.finish() + // See issue https://github.com/w3c/css-houdini-drafts/issues/808 + } +}); + +function createSpring(springConstant, ratio) { + // Normalize mass and distance to 1 and assume a reasonable init velocit + // but these can also become options to this animator. + const velocity = 0.2; + const mass = 1; + const distance = 1; + + // Keep ratio < 1 to ensure it is under-damped. + ratio = Math.min(ratio, 1 - 1e-5); + + const damping = ratio * 2.0 * Math.sqrt(springConstant); + const w = Math.sqrt(4.0 * springConstant - damping * damping) / (2.0 * mass); + const r = -(damping / 2.0); + const c1 = distance; + const c2 = (velocity - r * distance) / w; + + // return a value in [0..distance] + return function springTiming(timeMs) { + const time = timeMs / 1000; // in seconds + const result = Math.pow(Math.E, r * time) * + (c1 * Math.cos(w * time) + c2 * Math.sin(w * time)); + return distance - result; + } +} +``` + +Note that ideally once sping simulation is finished, the worklet animation would also dispatch +the `finish` event. Adding the necessary mechanism to enable this is tracked +[here](https://github.com/w3c/css-houdini-drafts/issues/808). + +## Twitter Header + +Note: This assumes experimental [ScrollTimeline][scroll-timeline] feature. + +An example of twitter profile header effect where two elements (avatar, and header) are updated in +sync with scroll offset. + +```html + +
+ +
+
+ + +``` + +twitter-header-animator.js: +```js +registerAnimator('twitter-header', class TwitterHeader extends StatelessAnimator { + constructor(options) { + this.timing_ = new CubicBezier('ease-out'); + } + + clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + animate(currentTime, effect) { + const scroll = currentTime; // [0, 1] + + // Drive the output group effect by setting its children local times. + effect.children[0].localTime = scroll; + // Can control the child effects individually +    effect.children[1].localTime = this.timing_(this.clamp(scroll, 0, 1)); + } +}); +``` + +## Swipe-to-Action + +Another usecase for Animation Worklet is to enable interactive input-driven animation effects that +are driven both by input events and time. + +To enable this we need a way to receive pointer events in worklet (e.g. via [CSS custom +variables](https://github.com/w3c/css-houdini-drafts/issues/869) or [other +mechanisms][input-for-worker]) and +also allow [playback controls](https://github.com/w3c/css-houdini-drafts/issues/808) inside +worklets. Both of these are natural planned additions to Animation Worklet. + + +Consider a simple swipe-to-action effect which follows the user swipe gesture and when finger lifts +then continues to completion (either dismissed or returned to origin) with a curve that matches the +swipe gesture's velocity. (See this [example](https://twitter.com/kzzzf/status/917444054887124992)) + +With Animation Worklet, this can be modeled as a stateful animator which consumes both time and +pointer events and have the following state machines: + +![SwipeToCompletionAnimation](img/swipe-to-dismiss-state.png) + + +Here are the three main states: + +1. Animation is idle, where it is `paused` so that it is not actively ticking +2. As soon as the user touches down, the animation moves the target to follow the user touchpoint + while staying `paused` (optionally calculate the movement velocity, and overall delta). +3. As soon as the user lift their finger the animation will the switch to 'playing' so that it is + ticked by time until it reaches its finished state. The final state may be decided on overall + delta and velocity and the animation curve adapts to the movement velocity. + +Note that while in (3), if the user touches down we go back to (2) which ensures responsiveness to +user touch input. + +To make this more concrete, here is how this may be implemented (assuming strawman proposed APIs for +playback controls and also receiving pointer events). Note that all the state machine transitions +and various state data (velocity, phase) and internal to the animator. Main thread only needs to +provide appropriate keyframes that can used to translate the element on the viewport as appropriate +(e.g., `Keyframes(target, {transform: ['translateX(-100vw)', 'translateX(100vw)']})`). + + +```javascript +registerAnimator('swipe-to-dismiss', class SwipeAnimator extends StatefulAnimator { + constructor(options, state = {velocity: 0, phase: 'idle'}) { + this.velocity = state.velocity; + this.phase = state.phase; + + if (phase == 'idle') { + // Pause until we receive pointer events. + this.pause(); + } + + // Assumes we have an API to receive pointer events for our target. + this.addEventListener("eventtargetadded", (event) => { + for (type of ["pointerdown", "pointermove", "pointerup"]) { + event.target.addEventListener(type,onPointerEvent ); + } + }); + } + + onpointerevent(event) { + if (event.type == "pointerdown" || event.type == "pointermove") { + this.phase = "follow_pointer"; + } else { + this.phase = "animate_to_completion"; + // Also decide what is the completion phase (e.g., hide or show) + } + + this.pointer_position = event.screenX; + + // Allow the animation to play for *one* frame to react to the pointer event. + this.play(); + } + + animate(currentTime, effect) { + if (this.phase == "follow_pointer") { + effect.localTime = position_curve(this.pointer_position); + update_velocity(currentTime, this.pointer_position); + // Pause, no need to produce frames until next pointer event. + this.pause(); + } else if (this.phase = "animate_to_completion") { + effect.localTime = time_curve(currentTime, velocity); + + if (effect.localTime == 0 || effect.localTime == effect.duration) { + // The animation is complete. Pause and become idle until next user interaction. + this.phase = "idle"; + this.pause(); + } else { + // Continue producing frames based on time until we complete or the user interacts again. + this.play(); + } + } + + } + + position_curve(x) { + // map finger position to local time so we follow user's touch. + } + + time_curve(time, velocity) { + // Map current time delta and given movement velocity to appropriate local time so that over + // time we animate to a final position. + } + + update_velocity(time, x) { + this.velocity = (x - last_x) / (time - last_time); + this.last_time = time; + this.last_x = x; + } + + state() { + return { + phase: this.phase, + velocity: this.velocity + } + } +}); +``` + +```javascript + +await CSS.animationWorklet.addModule('swipe-to-dismiss-animator.js'); +const target = document.getElementById('target'); +const s2d = new WorkletAnimation( + 'swipe-to-dismiss', + new KeyframeEffect(target, {transform: ['translateX(-100vw)', 'translateX(100vw)']})); +s2d.play(); +``` + + +# Why Extend Animation? + +In [WebAnimation][WA], [Animation][animation] is the main controller. It handles the playback commands +(play/pause/cancel) and is responsible for processing the progressing time (sourced from Timeline) and +driving keyframes effect which defines how a particular target css property is animated and +ultimately pixels moving on the screen. + +By allowing extensibility in Animation we can have the most flexibility in terms of what is possible +for example animation can directly control the following: + - Control animation playback e.g., implement open-ended animations with non-deterministic timings + (e.g., physical-based) or provide "trigger" facilities + - Flexibility in transforming other forms of input into "time" e.g., consume touch events and drive + animations + - Ability to handle multiple timelines e.g., animations that seamlessly transition btween being + touch/scroll driven to time-driven + - Control how time is translated e.g., new custom easing functions + - Drive multiple effects and control how they relate to each other e.g., new effect sequencing + + +While there is benefit in extensibility in other parts of animation stack (custom timeline, custom +effect, custom timing), custom animations provides the largest value in terms of flexibility and +addressing key usecases so it is the one we are tackling first. + +Animation Worklet can be easily augmented in future to support other Houdini style extensibility +features as well. + + +TODO: Also discuss other models that we have considered (e.g., CompositorWorker) that bypassed +web animation altogether. + + + +# Key Concepts + +## Animation Worklet Global Scope +A [worklet global scope](https://drafts.css-houdini.org/worklets/#the-global-scope) that is created +by Animation Worklet. Note that Animation Worklet creates multiple such scopes and uses them to +execute user defined effects. In particular global scopes are regularly switched to enforce +stateless and stateful animator contracts. + + +## Animator + +Animator is a Javascript class that encapsulates the custom animation logic. Similar to other +Houdinig worklets, animators are registered inside the worklet global scope with a unique name which +can be used to uniquely identify them. + + +## WorkletAnimation +`WorkletAnimation` is a subclass of Animation that can be used to create an custom animation that +runs inside a standalone animation worklet scope. A worklet animation has a corresponding animator +instance in a animation worklet scope which is responsible to drive its keyframe effects. Here are +the key differences compared to a regular web animation: + - Name: The name identifies the custom animator class registered in the animation worklet scope. + - Options: `WorkletAnimation` may have a custom properties bag that is cloned and provided to the + corresponding animator constructor when it is being instantiated. + +Note that worklet animations expose same API surface as other web animations and thus they may be +created, played, paused, inspected, and generally controlled from the main document scope. Here is +how various methods roughly translate: + + - `cancel()`: cancels the animation and the corresponding animator instance is removed. + - `play()`: starts the animation and the corresponding animator instance gets constructed and + may get its `animate` function called periodically as a result of changes in its timelines. + - pause(): pauses the animation and the corresponding animator instance no longer receives + `animate` calls. + - finish(), reverse() or mutating playbackRate: these affect the currentTime which is seens by + the animator instance. (We are considering possiblity of having a `onPlaybackRateChanged` + callback) + +## Statefull and Statelss Animators + +Sometimes animation effects require maintaining internal state (e.g., when animation needs to depend +on velocity). Such animators have to explicitly declare their statefulness but by inheritting from +`StatefulAnimator` superclass. + +The animators are not guaranteed to run in the same global scope (or underlying thread) for their +lifetime duration. For example user agents are free to initially run the animator on main thread +but later decide to migrate it off main thread to get certain performance optimizations or to tear +down scopes to save resources. + +Animation Worklet helps stateful animators to maintain their state across such migration events. +This is done through a state() function which is called and animator exposes its state. Here is +an example: + +```js +// in document scope +new WorkletAnimation('animation-with-local-state', keyframes); +``` + +```js +registerAnimator('animation-with-local-state', class FoorAnimator extends StatefulAnimator { + constructor(options, state = {velocity: 0, acceleration: 0}) { + // state is either undefined (first time) or the state after an animator is migrated across + // global scope. + this.velocity = state.velocity; + this.acceleration = state.acceleration; + } + + animate(time, effect) { + if (this.lastTime) { + this.velocity = time - this.prevTime; + this.acceleration = this.velocity - this.prevVelocity; + } + this.prevTime = time; + this.prevVelocity = velocity; + + effect.localTime = curve(velocity, acceleration, currentTime); + } + + state() { + // Invoked before any migration attempts. The returned object must be structure clonable + // and will be passed to constructor to help animator restore its state after migration to the + // new scope. + return { + this.velocity, + this.acceleration + } + } + + curve(velocity, accerlation, t) { + return /* compute some physical movement curve */; + } +}); +``` + + +## Threading Model + +Animation Worklet is designed to be thread-agnostic. Rendering engines may create one or more +parallel worklet execution contexts separate from the main javascript execution context, e.g., on +their own dedicated threads. Rendering engines may then choose to assign Animation Worklet +animations to run in such contexts. Doing so allows Animation Worklet animations to avoid being +impacted by main thread jank. + +Rendering engines may wish to make a best-effort attempt to execute animate callbacks synchronously +with visual frame production to ensure smooth animation. However it is legal for rendering engines +to produce visual frames without blocking to receive animation updates from a worklet (i.e., letting +the effects slip behind). For example, this could occur when the animate function callback is +unable to complete before the frame deadline. + +We believe that scripted animations which are run in a parallel execution environment and which +limit themselves to animating properties which do not require the user agent to consult main thread +will have a much better chance of meeting the strict frame budgets required for smooth playback. + + +Note that due to the asynchronous nature of this animation model a script running in the main +javascript execution context may see a stale value when reading a target property that is +being animated in a Worklet Animation, compared to the value currently being used to produce the +visual frame that is visible to the user. This is similar to the effect of asynchronous scrolling +when reading scroll offsets in the main javascript execution context. + + +
+ Overview of the animation worklet threading model. +
+ Overview of the animation worklet threading model.
+ + A simplified visualization of how animators running in a parallel execution environment can sync + their update to main thread while remaining in sync with visual frame production. +
+
+ + +# Related Concepts + +The following concepts are not part of Animation Worklet specification but Animation Worklet is +designed to take advantage of them to enable a richer set of usecases. These are still in early +stages of the standardization process so their API may change over time. + +## ScrollTimeline +[ScrollTimeline][scroll-timeline] is a concept introduced in +scroll-linked animation proposal. It defines an animation timeline whose time value depends on +scroll position of a scroll container. `ScrollTimeline` can be used an an input timeline for +worklet animations and it is the intended mechanisms to give read access to scroll position. + +We can later add additional properties to this timeline (e.g., scroll phase (active, inertial, +overscroll), velocity, direction) that can further be used by Animation Worklet. + +## GroupEffect + +[GroupEffect][group-effect] is a concept introduced in Web Animation Level 2 specification. It +provides a way to group multiple effects in a tree structure. `GroupEffect` can be used as the +output for Worklet Animations making it possible for it to drive complext effects spanning multiple +elements. Also with some minor [proposed changes](group-effect-changes) to Group Effect timing +model, Animation Worklet can enable creation of new custom sequencing models (e.g., with conditions +and state). + +## Event Dispatching to Worker and Worklets +[Event Dispatching to Worker/Worklets][input-for-worker] is a proposal in WICG which allows workers +and worklets to passively receive DOM events and in particular Pointer Events. This can be +benefitial to Animation Worklet as it provides an ergonomic and low latency way for Animation +Worklet to receive pointer events thus enabling it to implement input driven animations more +effectively. + + +# WEBIDL + +`WorkletAnimation` extends `Animation` and adds a getter for its timelines. +Its constructor takes: + - `animatiorId` which should match the id of an animator which is registered in +the animation worklet scope. + - A sequence of effects which are passed into a `GroupEffect` constructor. + - A sequence of timelines, the first one of which is considered primary timeline and passed to + `Animation` constructor. + + +```webidl + +[Constructor (DOMString animatorName, + optional (AnimationEffectReadOnly or array)? effects = null, + AnimationTimeline? timeline, + optional WorkletAnimationOptions)] +interface WorkletAnimation : Animation { + readonly attribute DOMString animatorName; +} +``` + +`AnimationEffectReadOnly` gets a writable `localTime` attribute which may be used to drive the +effect from the worklet global scope. + +```webidl +partial interface AnimationEffectReadOnly { + [Exposed=Worklet] + // Intended for use inside Animation Worklet scope to drive the effect. + attribute double localTime; +}; + +``` + +# Specification +The [draft specification](https://drafts.css-houdini.org/css-animationworklet) is +the most recent version. + + +[roc-thread]: https://lists.w3.org/Archives/Public/public-houdini/2015Mar/0020.html +[cw-proposal]: https://github.com/w3c/css-houdini-drafts/blob/master/composited-scrolling-and-animation/Explainer.md +[WA]: https://drafts.csswg.org/web-animations/ +[animation]: https://drafts.csswg.org/web-animations/#animations +[worklet]: https://drafts.css-houdini.org/worklets/#worklet-section +[input-for-worker]: https://discourse.wicg.io/t/proposal-exposing-input-events-to-worker-threads/3479 +[group-effect]: https://w3c.github.io/web-animations/level-2/#the-animationgroup-interfaces +[group-effect-changes]: https://github.com/yi-gu/group_effects +[scroll-timeline]: https://wicg.github.io/scroll-animations/#scrolltimeline \ No newline at end of file diff --git a/css-animation-worklet-1/WIP.md b/css-animation-worklet-1/WIP.md new file mode 100644 index 00000000..9b36c15a --- /dev/null +++ b/css-animation-worklet-1/WIP.md @@ -0,0 +1,103 @@ +# Open API Questions +--- + + +## Creation/Registration timing + +What should happen if an animation is created before the animator is registered? + +## Timelines + +* observe-only timelines? i.e., have access to timeline but the animate is not triggered when its + value changes. + +* Should we have a Timeline.currentTime and Timeline.localTime, where the latter is + the former but offset by startTime & scaled by playbackRate? + +* Access to the actual scroll position in the ScrollTimeline + +* Access to scroll phase (inactive, active, inertial etc.) + + +## Updating Elements + +For some effects we need to be able to add new participating elements without +restarting the effect. Here is an initial idea on how this can work. + +```js +// Effects and data can change after some time. +// We might want to break this out into separate functions or optional +// updates so you can just update options or just effects and options +// without having to pass other parameters again. +anim.update({ + [ /* new list of effects? */], + [ /* new list of timelines */], + {/* options */} +}); +``` + +```js +// In worklet scope +class MyAnimator{ + update(options) { + // this is a V2 concept, + } +} +``` + +## CSS Notation + +We are not proposing including this in the initial spec, but including some +preliminary thoughts here so that we can keep the eventual declarative CSS +specification in mind. + +index.html: +```html +<-- animator instance is declared here, with its timelines --> +
+ <-- effect timing is declared here and assigned to above animator --> +
+
+
+
+``` + +style.css: +```css +#main { + animation: worklet('twitter-header') + animation-timeline: scroll(#scroller_element.....) /* https://wicg.github.io/scroll-animations/#animation-timeline */ +} + +/* These are descendants of the animation */ +#main .header { + animation-group: 'twitter-header' 'header' #... + /* This syntax should be similar to what the plan is for Web Animation Group Effects */ +} + +#main .avatar { + animation-group: 'twitter-header' 'avatar' #... +} +``` + +This is equivalent to calling: + +```js +new WorkletAnimation('twitter-header', + [ + new KeyFrameEffect(.header[0], [], {}), + new KeyFrameEffect(.avatar, [], {}), + new KeyFrameEffect(.header[1], [], {}), + ], + [new ScrollingTimeline(#selector, {...})], + { elements: [ + /* This is admittedly a bit magical. */ + {'name': 'header'}, + {'name': 'avatar'}, + {'name': 'header'}, + ]} +).play(); + +``` + +Adding new elements that match the selector will be equivalent to invoking `update`. diff --git a/css-animation-worklet-1/img/AnimationWorklet-threading-model.svg b/css-animation-worklet-1/img/AnimationWorklet-threading-model.svg new file mode 100644 index 00000000..d463680b --- /dev/null +++ b/css-animation-worklet-1/img/AnimationWorklet-threading-model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css-animation-worklet-1/img/WorkletAnimation-timing-model.svg b/css-animation-worklet-1/img/WorkletAnimation-timing-model.svg new file mode 100644 index 00000000..c29d115e --- /dev/null +++ b/css-animation-worklet-1/img/WorkletAnimation-timing-model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css-animation-worklet-1/img/swipe-to-dismiss-state.png b/css-animation-worklet-1/img/swipe-to-dismiss-state.png new file mode 100644 index 00000000..1f01b8e7 Binary files /dev/null and b/css-animation-worklet-1/img/swipe-to-dismiss-state.png differ diff --git a/css-animation-worklet-1/principles.md b/css-animation-worklet-1/principles.md new file mode 100644 index 00000000..4278df31 --- /dev/null +++ b/css-animation-worklet-1/principles.md @@ -0,0 +1,136 @@ +# Animation Worklet Design Principles and Goals +***for rich interactive effects on the Web Platform*** + + +## Problem and Motivation + +**Fact**: Fact: It is difficult to build smooth rich interactive effects on the web. + +**Why do we care?** +Silky smooth rich interactive user interfaces are now a [basic user expectation][performance] on +modern computing platforms. For web platform to remain competitive it should be capable of high +fidelity, smooth, responsive UI. + + +**Where is the difficulty?** +Three key aspects of rich interactive effects are: smoothness, responsiveness to input (a.k.a. R & A +of [RAILS model][rails]) and their rich interaction model. + +The two main methods for creating animations on the web fall short in at least one of these aspects: + +- CSS (Web) Animations: Aimed at supporting stateless declarative time-driven effects. The + expressiveness is sufficient for common time-based effects. The resulting animation can be smooth + [1](#footnote1). However it's unclear how this model can be scaled to handle the + multi-dimensional inputs, conditional values, and statefulness required by the use cases below. +- requestAnimationFrame: Aimed at creating scripted animation effects. It can support rich + interaction models but it is difficult to make smooth or responsive. This expressiveness of + Javascript coupled with access to all input methods, application state and dom makes this API + capable of building rich interactive effects. However these can only run on main thread alongside + all other scripts[2](#footnote2) which severely hampers their responsiveness and + smoothness. Chrome [studies](https://tdresser.github.io/input-latency-deep-reports/) have shown that script is the main culprit to user responsiveness issues. + +Animation Worklet aims to help bridge the gap between these two. + +## Animation Worklet Vision + +[Animation Worklet][specification] aims to rectify this shortcomings by enabling animations that can +be: + +* rich (imperative, stateful) +* fast-by-default (isolated from main thread) +* respond to rich input e.g., touch, gesture, scroll. + +Animation Worklet is a primitive in the [extensible web](https://extensiblewebmanifesto.org/) spirit. +It exposes browser's fast path to applications in a way that it was never before and reduces browser +magic. + +Examples of rich interactive effects that are (or will be made) possible with Animation Worklet: + + +* Scroll driven effects: + * [Hidey-bar](https://googlechromelabs.github.io/houdini-samples/animation-worklet/twitter-header/): animation depends on both time and scroll input. + * [Parallax](https://googlechromelabs.github.io/houdini-samples/animation-worklet/parallax-scrolling/): Simplest scroll-drive effect. + * [Custom paginated slider](http://aw-playground.glitch.me/amp-scroller.html). + * Pull-to-refresh: animation depends on both touch and time inputs. + * Custom scrollbars. + * [More examples](https://github.com/w3c/css-houdini-drafts/blob/master/scroll-customization-api/UseCases.md) of scroll-driven effects. +* Gesture driven effects: + * [Image manipulator](https://github.com/w3c/csswg-drafts/issues/2493#issuecomment-422153926) that scales, rotates etc. + * Swipe to dismiss. + * Drag-N-Drop. + * Tiled panning e.g., Google maps. +* Stateful script driven effects: + * [Spring-based emulations](https://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-timing/). + * [Spring-Sticky effect](http://googlechromelabs.github.io/houdini-samples/animation-worklet/spring-sticky/). + * Touch-driven physical environments. + * [Expando](http://googlechromelabs.github.io/houdini-samples/animation-worklet/expando/): Procedural animations with multiple elements. +* Animated scroll offsets: + * Having multiple scrollers scroll in sync e.g. diff viewer keeping old/new in sync when you + scroll either ([demo](https://googlechromelabs.github.io/houdini-samples/animation-worklet/sync-scroller/)) + * Custom smooth scroll animations (e.g., physic based fling curves) + + +## First Principle - Richness + +Animation Worklet enables developers to create custom animations by providing an `animate` function +that runs inside animation worklet global scope. The animation logic can take advantage of the full +expressive power of JavaScript, maintain local state, modify and coordinated across many elements. +This new extension point in browser animation system enables richer effects that go well beyond what +can be achieved today with Web Animations and closer to what is possible with requestAnimationFrame. + +**Further explorations in this direction:** Allow richer access to scrolling machinery (e.g., scroll +customization), custom paint worklets, and outputs beyond existing KeyframeEffect interface. + + +## Second Principle - Performance + +Animation Worklet is designed to be thread agnostic. In particular, it can run off main thread +keeping its performance isolated from main thread (also reducing main thread load). + +Animation Worklet API encourages the developers to isolate their critical UI work inside limited +worklet scope with well-specified input and output. This allows user-agent to make much better +scheduling decision about this work and in particular, be much better at maintaining a strict +frame-budget to successfully run these animations on its fast path. Presently the above performance +guarantees are only accessible to a limited set of declarative time-based effects. + +**Further explorations in this direction**: Introduce more sophisticated per-animation scheduling +where a slow animation may run at slower frame-rate without affecting other well-behaved animations, +experiment with translating animation code to native code or even GL shaders moving the computation +to GPU for even stronger performance guarantees! + + +## Third Principle - Interactivity + +Animation Worklet is designed to enable support for animations whose input goes beyond just time, a +single-dimensional variable. + +Web animation timing model is stateless and driven by a single dimensional variable, time. + +Although this model works well for declarative time-based animation, it falls short when it comes to +interactive input-driven effects that are inherently stateful. While it is possible to map simple +forms of input (e.g., [single dimensional scroll](https://wicg.github.io/scroll-animations/#intro)) +into time, it is much more difficult (almost impossible) to do so for multi-dimensional stateful +input such as multi-touch and gesture input. + +Animation Worklet has the necessary expressive power and richness to easily accommodate the full +richness of multi-dimensional input such as touch, gesture, scroll etc. For example it is trivial to +react to scroll phase change, pointer state change, addition/removal of new pointer or state, +calculate pointer velocity, acceleration and other computed values inside an animation worklet. + +**Further explorations in this direction:** Expose pointer and gesture as input to animation +([current proposal](https://github.com/w3c/csswg-drafts/issues/2493#issuecomment-422109535)) + + +# Appendix + + +## Footnotes + +* 1: If authors limit themselves to cheap-to-update properties. In Chrome + these are composited properties e.g., transform, opacity, filter but other engines may have a + slightly difference subset. + + +[performance]: https://paul.kinlan.me/what-news-readers-want/ +[rails]: https://developers.google.com/web/fundamentals/performance/rail#goals-and-guidelines +[specification]: https://github.com/w3c/css-houdini-drafts/tree/master/css-animationworklet diff --git a/css-animation-worklet-1/status.md b/css-animation-worklet-1/status.md new file mode 100644 index 00000000..bdaa1dd3 --- /dev/null +++ b/css-animation-worklet-1/status.md @@ -0,0 +1,18 @@ +# Implementation Status + +## Chrome + +This is a rough sketch of how Chrome plans to deliver Animation Worklet features: + +1. Animation Worktlet Prototype (done): scripted custom animation, single effect, only fast + properties, off-thread. +2. Animation Worktlet [Origin Trial][ot-blogpost] (in progress, [signup][ot-signup]): good + performance, scroll input (ScrollTimeline), basic web-animation controls (play/cancel). +3. Animation Worktlet MVP (in development): animate all properties (slow path ones running in sync + with main thread), multiple effects (i.e., GroupEffect), full web-animation integration. +4. Animation Worktlet V2 (future): touch/gesture input, multiple inputs in single animation, + sophisticated scheduling, other outputs. + +[ot-blogpost]: https://developers.google.com/web/updates/2018/10/animation-worklet +[ot-signup]:https://docs.google.com/forms/d/e/1FAIpQLSfO0_ptFl8r8G0UFhT0xhV17eabG-erUWBDiKSRDTqEZ_9ULQ/viewform + diff --git a/css-layout-api/EXPLAINER.md b/css-layout-api/EXPLAINER.md new file mode 100644 index 00000000..6aaf3107 --- /dev/null +++ b/css-layout-api/EXPLAINER.md @@ -0,0 +1,478 @@ +CSS Layout API Explained +======================== + +The CSS Layout API is being developed to improve the extensibility of CSS. + +Specifically the API is designed to give web developers the ability to write their own layout +algorithms in addition to the native algorithms user agents ship with today. + +For example user agents today currently ship with: + - Block Flow Layout + - Flexbox Layout + +However with the CSS Layout API web developers could write their own layouts which implement: + - Constraint based layouts + - Masonry layouts + - Line spacing and snapping + +Initial Concepts - Writing Modes +-------------------------------- + +This API uses terminology which may be foreign to many web developers initially. Everything in the +CSS Layout API is computed in the [logical coordinate +system](https://drafts.csswg.org/css-writing-modes-3/#text-flow). + +This has the primary advantage that when you write your layout using this system it will +automatically work for writing modes which are right-to-left (e.g. Arabic or Hebrew), or for writing +modes which are vertical (many Asian scripts including Chinese scripts, Japanese and Korean). + +For a developer who is used to left-to-right text, the way to translate this back into "physical" +coordinates is: + +| Logical | Physical | +| -----------:|:-------- | +| inlineSize | width | +| inlineStart | left | +| inlineEnd | right | +| blockSize | height | +| blockStart | top | +| blockEnd | bottom | + +Getting Started +--------------- + +First you'll need to add a module script into the layout worklet. + +```js +if ('layoutWorklet' in CSS) { + await CSS.layoutWorklet.addModule('my-layout-script.js'); + console.log('layout script installed!'); +} +``` + +See the worklets [explainer](../worklets/EXPLAINER.md) for a more involved explanation of worklets. + +After the promise returned from the `addModule` method resolves the layouts defined in the script +will apply to the page. + +A Centering Layout +------------------ + +The global script context for the layout worklet has exactly one entry method exposed to developers: +`registerLayout`. + +There are a lot of things going on in the following example so we'll step through them one-by-one +below. You should read the code below with its explanatory section. + +```js +registerLayout('centering', class { + async layout(children, edges, constraints, styleMap) { + // (1) Determine our (inner) available size. + const availableInlineSize = constraints.fixedInlineSize - edges.inline; + const availableBlockSize = constraints.fixedBlockSize ? + constraints.fixedBlockSize - edges.block : + null; + + let maxChildBlockSize = 0; + + const childFragments = []; + for (let child of children) { + // (2) Perform layout upon the child. + const fragment = await child.layoutNextFragment({ + availableInlineSize, + availableBlockSize, + }); + + // Determine the max fragment size so far. + maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize); + + // Position our child fragment. + fragment.inlineOffset = edges.inlineStart + + (constraints.fixedInlineSize - fragment.inlineSize) / 2; + fragment.blockOffset = edges.blockStart + + Math.max(0, (constraints.fixedBlockSize - fragment.blockSize) / 2); + + childFragments.push(fragment); + } + + // (3) Determine our "auto" block size. + const autoBlockSize = maxChildBlockSize + edges.block; + + // (4) Return our fragment. + return { + autoBlockSize, + childFragments, + } + } +}); +``` + +The `layout` function is your callback into the browsers layout phase in the +rendering engine. You are given: + - `children`, the list of children boxes you should perform layout upon. + - `edges`, the size of *your* borders, scrollbar, and padding in the logical coordinate system. + - `constraints`, the constraints which the fragment you produce should meet. + - `style`, the _readonly_ style for the current layout. + +Layout eventually will return a dictionary will what the resulting fragment of that layout should +be. + +The above example would be used in CSS by: +```css +.centering { + display: layout(centering); +} +``` + +### Step (1) - Determine our (inner) available size ### + +The first thing that you'll probably want to do for most layouts is to determine your "inner" size. + +The `constraints` object passed into the layout function pre-calculates your inline-size (width), +and potentially your block-size (height) if there is enough information to do so (e.g. the element +has `height: 100px` specified). + +See [developer.mozilla.org](https://developer.mozilla.org) for an explanation of what +[width](https://developer.mozilla.org/en-US/docs/Web/CSS/width) and +[height](https://developer.mozilla.org/en-US/docs/Web/CSS/height), etc will resolve to. + +The `edges` object represents the border, scrollbar, and padding of your element. In order to +determine our "inner" size we subtract the `edges.all` from our calculated sizes. For example: + +```js +const availableInlineSize = constraints.fixedInlineSize - edges.inline; +const availableBlockSize = constraints.fixedBlockSize ? + constraints.fixedBlockSize - edges.block : + null; +``` + +We keep `availableBlockSize` null if `constraints.fixedBlockSize` wasn't able to be computed. + +### Step (2) - Perform layout upon the child ### + +Performing layout on a child can be done with the `layoutNextFragment` method. E.g. + +```js +const fragment = await child.layoutNextFragment({ + availableInlineSize, + availableBlockSize, +}); +``` + +The first argument is the "constraints" which you are giving to the child. They can be: + - `availableInlineSize` & `availableBlockSize` - A child fragment will try and "fit" within this + given space. + - `fixedInlineSize` & `fixedBlockSize` - A child fragment will be "forced" to be this size. + - `percentageInlineSize` & `percentageBlockSize` - Percentages will be resolved against this size. + +As layout may be paused or run on a different thread, the API is asynchronous. + +The result of performing layout on a child is a `LayoutFragment`. A fragment is read-only except for +setting the offset relative to the parent fragment. + +```js +fragment instanceof LayoutFragment; // true + +// The resolved size of the fragment. +fragment.inlineSize; +fragment.blockSize; + +// We can set the offset relative to the current layout. +fragment.inlineOffset = 10; +fragment.blockOffset = 20; +``` + +### Step (3) - Determine our "auto" block size ### + +Now that we know how large our biggest child is going to be, we can calculate our "auto" block size. +This is the size the element will be if there are no other block-size constraints (e.g. `height: +100px`). + +In this layout algorithm, we just add the `edges.block` size to the largest child we found: +```js +const autoBlockSize = maxChildBlockSize + edges.block; +``` + +### Step (4) - Return our fragment ### + +Finally we return a dictionary which represents the fragment we wish the rendering engine to create +for us. E.g. +```js +const result = { + autoBlockSize, + childFragments, +}; +``` + +The important things to note here are that you need to explicitly say which `childFragments` you +would like to render. If you give this an empty array you won't render any of your children. + +Querying Style +-------------- + +While not present in the "centering" example, it is possible to query the style of the element you +are performing layout for, and all children. E.g. + +```html + + +
+
+
+``` + +```js +registerLayout('style-read', class { + static inputProperties = ['--a-number']; + static childInputProperties = ['--a-string']; + + async layout(children, edges, constraints, styleMap) { + // We can read our own style: + styleMap.get('--a-number').value === 42; + + // And our children: + children[0].styleMap.get('--a-string').toString() === 'hello'; + } +}); +``` + +You can use this to implement properties which your layout depends on, a similar thing that native +layouts use is `flex-grow` for flexbox, or `grid-template-areas` for grid. + +Text Layout +----------- + +By default layouts force all of their children to be blockified. This means for example if you have: +```html +
+ I am some text +
+
+``` + +The engine will conceptually force the text `I am some text` to be surrounded by a `
`. E.g. +```html +
+
I am some text
+
+
+``` + +This is important as the above `centering` layout would have to deal with text _fragmentation_, a +few native layouts use this trick to simplify their algorithms, for example grid and flexbox. + +### Text Fragmentation ### + +In the above `centering` example, we forced each `LayoutChild` to produce exactly one +`LayoutFragment`. + +We are able to ensure children do not blockify by setting the `childDisplay` to `normal`, e.g. +```js +registerLayout('example', class { + static layoutOptions = {childDisplay: 'normal'}; +}); +``` + +Now a `LayoutChild` which represents some text is able to produce more than one `Fragment`. E.g. + +```text +|---- Inline Available Size ----| +The quick brown fox jumped over the lazy dog. +``` + +```js +child instanceof LayoutChild; + +const fragment1 = yield child.layoutNextFragment(constraints); +const fragment2 = yield child.layoutNextFragment(constraints, fragment1.breakToken); + +fragment2.breakToken == null; +``` + +In the above example the text child produces two fragments. Containing: +1. `The quick brown fox jumped over` +2. `the lazy dog.` + +The critical detail here to be aware of is the concept of a `BreakToken`. The `BreakToken` contains +all of the information necessary to continue/resume the layout where the child finished. + +We pass the `BreakToken` to add back into the `layout()` call in order to produce the next fragment. + +### A Basic Text Layout ### + +```js +registerLayout('basic-inline', class { + static layoutOptions = {childDisplay: 'normal'}; + + async layout(children, edges, constraints, styleMap) { + // Determine our (inner) available size. + const availableInlineSize = constraints.fixedInlineSize - edges.inline; + const availableBlockSize = constraints.fixedBlockSize !== null ? + constraints.fixedBlockSize - edges.block : null; + + const constraints = { + availableInlineSize, + availableBlockSize, + }; + + const childFragments = []; + + let blockOffset = edges.blockStart; + let child = children.shift(); + let childBreakToken = null; + while (child) { + // Layout the next line, the produced line will try and respect the + // availableInlineSize given, you could use this to achieve a column + // effect or similar. + const fragment = await child.layoutNextFragment(constraints, childBreakToken); + childFragments.push(fragment); + + // Position the fragment, note we coulld do something special here, like + // placing all the lines on a "rythimic grid", or similar. + fragment.inlineOffset = edges.inlineStart; + fragment.blockOffset = blockOffset; + + blockOffset += fragment.blockSize; + + if (fragment.breakToken) { + childBreakToken = fragment.breakToken; + } else { + // If a fragment doesn't have a break token, we move onto the next + // child. + child = children.shift(); + childBreakToken = null; + } + } + + // Determine our "auto" block size. + const autoBlockSize = blockOffset + edges.blockEnd; + + // Return our fragment. + return { + autoBlockSize, + childFragments, + }; + } +}); +``` + +The above example is slightly more complex than the previous centering layout because of the ability +for text children to fragment. + +That said it has all the same steps as before: + 1. Resolving the (inner) available size. + 2. Performing layout and positioning children fragments. + 3. Resolving the "auto" block size. + 4. Returning the fragment. + +Scrolling +--------- + +We have been handling scrolling in the above example but we haven't talked about it yet. + +The `edges` object passed into `layout()` respects the `overflow` property. +For example if we are `overflow: hidden`, `edges` object won't include the scrollbar width. + +For `overflow: auto` the engine will typically perform a layout without a scrollbar, then if it +detects overflow, with a scrollbar. As long as you respect the layout "edges" your layout algorithm +should work as expected. + +Block Fragmentation +------------------- + +Some native layouts on the web support what is known as block fragmentation. For example: + +```html + +
+This is some text. + + +
+This is some more text. +
+``` + +In the above example the `multicol` div may produce three (3) fragments. + 1. `{fragment}This is some text.{/fragment}` + 2. `{fragment}{fragment type=table}{/fragment} This is{/fragment}` + 3. `{fragment}some more text.{/fragment}` + +We can make our children fragment by passing them a constraint space with a fragmentation line. E.g. + +```js +registerLayout('special-multi-col', class { + async layout(children, edges, constraints, styleMap, breakToken) { + for (let child of children) { + // Create a constraint space with a fragmentation line. + const childConstraints = { + availableInlineSize, + availableBlockSize, + blockFragmentationOffset: availableBlockSize, + blockFragmentationType: 'column', + }); + + const fragment = await child.layoutNextFragment(childConstraints); + } + + // ... + } +}); +``` + +In the above example each of the children will attempt to fragment in the block direction when they +exceed `blockFragmentationOffset`. The type is a `'column'` which will mean it works in conjunction +with rules like `break-inside: avoid-column`. + +We can also allow our own layout to be fragmented by respecting the fragmentation line. E.g. + +```js +registerLayout('basic-inline', class { + async layout(children, edges, constraints, styleMap, breakToken) { + + // We can check if we need to fragment in the block direction. + if (constraints.blockFragmentationType != 'none') { + // We need to fragment! + } + + // We can get the start child to start layout at with the breakToken. E.g. + let child = null; + let childToken = null; + if (breakToken) { + childToken = breakToken.childTokens[0]; // We can actually have multiple + // children break. But for now + // we'll just use one. + child = childToken.child; + } else { + child = children[0]; + } + + // SNIP! + + return { + autoBlockSize, + childFragments, + breakToken: { + data: /* you can place arbitary data here */, + childTokens: [childToken] + } + } + } +}); +``` + +The additional complexity here is that you need to create and receive your own break tokens. + +Closing Words +------------- + +This is a complex API and it uses foreign terminology. But we really want to give you, the web +developer, the power that the rendering engines have when it comes to layout. Enjoy! :) + diff --git a/css-layout-api/Overview.bs b/css-layout-api/Overview.bs index 7a202065..6074248e 100644 --- a/css-layout-api/Overview.bs +++ b/css-layout-api/Overview.bs @@ -1,15 +1,2467 @@ + + + + + +
+urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
+    text: constructor
+    text: Construct
+    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
+urlPrefix: https://www.w3.org/TR/CSS21/; type:dfn
+    urlPrefix: box.html#;
+        url: box-dimensions; text: box model edges
+    urlPrefix: visudet.html#;
+        text: static position
+urlPrefix: https://html.spec.whatwg.org/#; type: dfn
+    text: structuredserializeforstorage
+    text: structureddeserialize
+
+ +Introduction {#intro} +===================== + +This section is not normative. + +The layout stage of CSS is responsible for generating and positioning [=fragments=] from the [=box +tree=]. + +This specification describes an API which allows developers to layout a [=box=] in response to +computed style and [=box tree=] changes. + +For a high level overview of this API, see the EXPLAINER. + +Layout API Containers {#layout-api-containers} +============================================== + +A new alternative value is added +to the <> production: layout(<>). + +
+
layout() +
+ This value causes an element to generate a [=layout API container=] box. +
+ +A layout API container is the box generated by an element with a <> +[=computed value=] ''layout()''. + +A [=layout API container=] establishes a new layout API formatting context for its +contents. This is the same as establishing a block formatting context, except that the layout +provided by the author is used instead of the block layout. +For example, floats do not intrude into the layout API container, and the layout API container's +margins do not collapse with the margins of its contents. + +[=Layout API containers=] form a containing block for their contents exactly like block +containers do. [[!CSS21]] + +Note: In a future level of the specification there may be a way to override the containing block + behaviour. + +The 'overflow' property applies to [=layout API containers=]. This is discussed in +[[#interaction-overflow]]. + +As the layout is entirely up to the author, properties which are used in other layout modes (e.g. +flex, block) may not apply. For example an author may not respect the 'margin' property on children. + +
+The HTML below shows an example of setting the ''display'' to a ''layout()'' function, if the CSS +Layout API is supported. + +
+<!DOCTYPE html>
+<style>
+@supports (display: layout(centering)) {
+  .centering-layout { display: layout(centering); }
+}
+</style>
+<div class="centering-layout"></div>
+
+
+ +Layout API Container Painting {#painting} +----------------------------------------- + +[=Layout API Container=] children paint exactly the same as inline blocks [[!CSS21]], except that +the order in which they are returned from the layout method (via +{{FragmentResultOptions/childFragments}}) is used in place of raw document order, and 'z-index' +values other than ''z-index/auto'' create a stacking context even if 'position' is ''static''. + +Box Tree Transformations {#layout-api-box-tree} +----------------------------------------------- + +The inflow children of a [=layout API container=] can act in different ways depending on the value +of [=document layout definition/layout options'=] {{LayoutOptions/childDisplay}} (set by +layoutOptions on the class). + +If the value of [=document layout definition/layout options'=] {{LayoutOptions/childDisplay}} is +"block" the 'display' value of that child is [=blockified=]. This is similar to +children of [=flex containers=] or [=grid containers=]. See [[!css3-display]]. + +If the value of [=document layout definition/layout options'=] {{LayoutOptions/childDisplay}} is +"normal", no [=blockification=] occurs. Instead children with a <> +[=computed value=] of ''inline'' (a [=root inline box=]) will produce a single {{LayoutFragment}} +representing each line when {{LayoutChild/layoutNextFragment()}} is called. + +Note: This allows authors to adjust the available inline size of each line, and position each line + separately. + +Children of a {{LayoutChild}} which represents [=root inline box=] also have some additional +transformations. + + - A [=block-level=] box inside a [=inline-level=] box is [=inlinified=] I.e. its + <> is set to ''inline''. + + - A [=float=] inside a [=inline-level=] box is not taken out of flow. Instead it must be treated as + inflow, and be [=inlinified=]. + +In both of the above cases the children become [=atomic inlines=]. + +Note: User agents would not perform any "inline splitting" or fragmenting when they encounter a + [=block-level=] box. + +
+ Note: In the example below "inline-span" would be represented as a single {{LayoutChild}} with + both "block" and "float" being [=atomic inlines=]. +
+        <span id="inline-span">
+          Text
+          <div id="block"></div>
+          <div id="float"></div>
+          Text
+        </span>
+    
+
+ +Layout Worklet {#layout-worklet} +================================ + +The {{layoutWorklet}} attribute allows access to the {{Worklet}} responsible for all the classes +which are related to layout. + +The {{layoutWorklet}}'s [=worklet global scope type=] is {{LayoutWorkletGlobalScope}}. + +
+partial namespace CSS {
+    [SameObject] readonly attribute Worklet layoutWorklet;
+};
+
+ +The {{LayoutWorkletGlobalScope}} is the global execution context of the {{layoutWorklet}}. + +
+[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet]
+interface LayoutWorkletGlobalScope : WorkletGlobalScope {
+    undefined registerLayout(DOMString name, VoidFunction layoutCtor);
+};
+
+ +
+ Web developers can feature detect by: +
+    if ('layoutWorklet' in CSS) {
+      console.log('CSS Layout API available!');
+    }
+    
+
+ +Concepts {#concepts} +-------------------- + +This section describes internal data-structures created when {{registerLayout(name, layoutCtor)}} is +called. + +A layout definition is a [=struct=] which describes the information needed by the +{{LayoutWorkletGlobalScope}} about the author defined layout (which can be referenced by the +''layout()'' function). It consists of: + + - class constructor which is the class [=constructor=]. + + - layout function which is the layout [=function=] callback. + + - intrinsic sizes function which is the intrinsic sizes + [=function=] callback. + + - constructor valid flag. + + - input properties which is a [=list=] of + DOMStrings. + + - child input properties which is a [=list=] of + DOMStrings. + + - layout options a {{LayoutOptions}}. + +A document layout definition is a [=struct=] which describes the information needed by +the [=document=] about the author defined layout (which can be referenced by the ''layout()'' +function). It consists of: + + - input properties which is a [=list=] of + DOMStrings + + - child input properties which is a [=list=] of + DOMStrings. + + - layout options a {{LayoutOptions}}. + +Registering A Layout {#registering-layout} +------------------------------------------ + +The section describes how a web developer uses {{registerLayout(name, layoutCtor)}} to register a +layout. + +
+[Exposed=LayoutWorklet]
+dictionary LayoutOptions {
+  ChildDisplayType childDisplay = "block";
+  LayoutSizingMode sizing = "block-like";
+};
+
+[Exposed=LayoutWorklet]
+enum ChildDisplayType {
+    "block", // default - "blockifies" the child boxes.
+    "normal",
+};
+
+[Exposed=LayoutWorklet]
+enum LayoutSizingMode {
+    "block-like", // default - Sizing behaves like block containers.
+    "manual", // Sizing is specified by the web developer.
+};
+
+ +The [=document=] has a [=map=] of document layout definitions. Initially this map is +empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. + +The {{LayoutWorkletGlobalScope}} has a [=map=] of layout definitions. Initially this map +is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. + +Each [=box=] representing a [=layout API container=] has a [=map=] of layout class +instances. Initially this map is empty; it is populated when the user agent calls either +[=determine the intrinsic sizes=] or [=generate a fragment=] for a [=box=]. + +Each [=box=] representing a [=layout API container=] has a styleMap internal slot. +This is a {{StylePropertyMapReadOnly}} which contains the properties listed in +inputProperties. + +The user agent clear the [=styleMap=] internal slot for a [=box=] when: + + - The [=computed values=] of [=document layout definition/input properties=] for the [=box=] + changes. + + - When the [=box=] is removed from the [=box tree=]. + + - Every 1000 layout passes. + + Note: The above rule exists to ensure that web developers do not rely on being able to store + non-regeneratable state on the {{StylePropertyMapReadOnly}} object. + The 1000 limit was picked as a high upper bound, this limit may improve (downwards) over + time. + +
+ Note: The shape of the class should be: +
+        registerLayout('example', class {
+            static inputProperties = ['--foo'];
+            static childrenInputProperties = ['--bar'];
+            static layoutOptions = {
+              childDisplay: 'normal',
+              sizing: 'block-like'
+            };
+
+            async intrinsicSizes(children, edges, styleMap) {
+                // Intrinsic sizes code goes here.
+            }
+
+            async layout(children, edges, constraints, styleMap, breakToken) {
+                // Layout code goes here.
+            }
+        });
+    
+
+
+ +The algorithm below is run when the {{registerLayout(name, layoutCtor)}} is called. It notifies the +user agent layout engine about the new user defined layout. + +
+When the registerLayout(|name|, |layoutCtor|) 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 |layoutDefinitionMap| be {{LayoutWorkletGlobalScope}}'s [=layout definitions=] map. + + 3. If |layoutDefinitionMap|[|name|] [=map/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=](|layoutCtor|, "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 layout 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 |childInputProperties| be an empty sequence<DOMString>. + + 9. Let |childInputPropertiesIterable| be the result of [=Get=](|layoutCtor|, + "childInputProperties"). + + 10. If |childInputPropertiesIterable| is not undefined, then set |childInputProperties| to the + result of [=converting=] |childInputPropertiesIterable| to a + sequence<DOMString>. If an exception is [=thrown=], rethrow the exception + and abort all these steps. + + 11. Filter |childInputProperties| so that it only contains [=supported CSS properties=] and [=custom + properties=]. + + 12. Let |layoutOptionsValue| be the result of [=Get=](|layoutCtor|, "layoutOptions"). + + 13. Let |layoutOptions| be the result of [=converting=] |layoutOptionsValue| to a + {{LayoutOptions}}. If an exception is [=thrown=], rethrow the exception and abort all these + steps. + + 14. Let |prototype| be the result of [=Get=](|layoutCtor|, "prototype"). + + 15. If the result of [=Type=](|prototype|) is not Object, [=throw=] a [=TypeError=] and abort + all these steps. + + 16. Let |intrinsicSizesValue| be the result of [=Get=](|prototype|, "intrinsicSizes"). + + 17. Let |intrinsicSizes| be the result of [=converting=] |intrinsicSizesValue| to the + [=Function=] [=callback function=] type. Rethrow any exceptions from the conversion. + + 18. Let |layoutValue| be the result of [=Get=](|prototype|, "layout"). + + 19. Let |layout| be the result of [=converting=] |layoutValue| to the [=Function=] [=callback + function=] type. Rethrow any exceptions from the conversion. + + 20. Let |definition| be a new [=layout definition=] with: + + - [=class constructor=] being |layoutCtor|. + + - [=layout function=] being |layout|. + + - [=intrinsic sizes function=] being |intrinsicSizes|. + + - [=constructor valid flag=] being true. + + - [=layout definition/child input properties=] being |childInputProperties|. + + - [=layout definition/input properties=] being |inputProperties|. + + - [=layout definition/layout options=] being |layoutOptions|. + + 21. [=map/Set=] |layoutDefinitionMap|[|name|] to |definition|. + + 22. [=Queue a task=] to run the following steps: + + 1. Let |documentLayoutDefinitionMap| be the associated [=document's=] [=document layout + definitions=] [=map=]. + + 2. Let |documentDefinition| be a new [=document layout definition=] with: + + - [=document layout definition/child input properties=] being |childInputProperties|. + + - [=document layout definition/input properties=] being |inputProperties|. + + - [=document layout definition/layout options=] being |layoutOptions|. + + 3. If |documentLayoutDefinitionMap|[|name|] [=map/exists=], run the following steps: + + 1. Let |existingDocumentDefinition| be the result of [=map/get=] + |documentLayoutDefinitionMap|[|name|]. + + 2. If |existingDocumentDefinition| is "invalid", abort all these steps. + + 3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is + [=document layout definition/input properties=], [=document layout definition/child + input properties=], and [=document layout definition/layout options=] are + different), then: + + [=map/Set=] |documentLayoutDefinitionMap|[|name|] to "invalid". + + Log an error to the debugging console stating that the same class was registered + with different inputProperties, childInputProperties, or + layoutOptions. + + 4. Otherwise, [=map/set=] |documentLayoutDefinitionMap|[|name|] to |documentDefinition|. +
+ +Terminology {#terminology} +-------------------------- + +We define the following terms to be clear about which layout algorithm (formatting context) we are +talking about. + +The current layout is the layout algorithm for the [=box=] we are currently performing +layout for. + +The parent layout is the layout algorithm for the [=box's=] direct parent, (the layout +algorithm which is requesting the [=current layout=] to be performed). + +A child layout is the layout algorithm for a {{LayoutChild}} of the [=current layout=]. + +Layout API {#layout-api} +======================== + +This section describes the objects of the Layout API provided to web developers. + +Layout Children {#layout-children} +---------------------------------- + +A {{LayoutChild}} represents a inflow CSS generated [=box=] before layout has occurred. (The box or +boxes will all have a computed value of 'display' that is not ''none''). + +The {{LayoutChild}} does not contain any layout information itself (like inline or block size) but +can be used to generate {{LayoutFragment}}s which do contain layout information. + +An author cannot construct a {{LayoutChild}} with this API, this happens at a separate stage of the +user agent rendering engine (post style resolution). + +An array of {{LayoutChild}}ren is passed into the layout/intrinsicSizes methods which represents the +children of the current box which is being laid out. + +
+[Exposed=LayoutWorklet]
+interface LayoutChild {
+    readonly attribute StylePropertyMapReadOnly styleMap;
+
+    Promise<IntrinsicSizes> intrinsicSizes();
+    Promise<LayoutFragment> layoutNextFragment(LayoutConstraintsOptions constraints, ChildBreakToken breakToken);
+};
+
+ +The {{LayoutChild}} has internal slot(s): + + - \[[box]] a CSS [=box=]. + + - \[[styleMap]] a {{StylePropertyMapReadOnly}}, this is the + computed style for the child, it is populated with only the properties listed in + childInputProperties. + + - [[unique id]] the [=unique id=] of the current [=layout + api context=]. This slot is used so that a {{LayoutChild}} used outside the current layout + pass is invalid. + +The {{LayoutChild/[[styleMap]]}} may be pre-populated when the [=computed value=] for properties +listed in the in [=layout definition/child input properties=] for the {{LayoutChild/[[box]]}}. + +
+The example below shows the basic usage of a {{LayoutChild}}. +
+registerLayout('example-layout-child', class {
+  static childInputProperties = ['--foo'];
+
+  async layout(children, edges, constraints, styleMap) {
+
+    // An array of LayoutChildren is passed into both the layout function,
+    // and intrinsic sizes function below.
+    const child = children[0];
+
+    // You can query the any properties listed in "childInputProperties".
+    const fooValue = child.styleMap.get('--foo');
+
+    // And perform layout!
+    const fragment = await child.layoutNextFragment({});
+
+  }
+
+  async intrinsicSizes(children, edges, styleMap) {
+
+    // Or request the intrinsic size!
+    const childIntrinsicSize = await children[0].intrinsicSizes();
+
+  }
+});
+
+
+ +A {{LayoutChild}} could be generated by: + + - An [=element=]. + + - A [=root inline box=]. + + - A ::before or ::after pseudo-element. + + Note: Other pseudo-elements such as ::first-letter or ::first-line do not + generate a {{LayoutChild}} for layout purposes. They are additional + styling information for a text node. + + - An [=anonymous box=]. For example an anonymous box may be inserted as a result of: + + - A text node which has undergone [=blockification=]. (Or more generally a [=root inline box=] + which has undergone [=blockification=]). + + - An element with ''display: table-cell'' which doesn't have a parent with ''display: table''. + +
+ Note: As an example the following would be placed into three {{LayoutChild}}ren: +
+        <style>
+          #box::before { content: 'hello!'; }
+        </style>
+        
+        <div id="box">A block level box with text.</div>
+        <img src="..." />
+    
+
+ +
+ Note: As an example the following would be placed into a single {{LayoutChild}} as they share a + [=root inline box=]: +
+        This is a next node, <span>with some additional styling,
+        that may</span> break over<br>multiple lines.
+    
+
+ +Multiple non-[=atomic inlines=] are placed within the same {{LayoutChild}} to allow rendering +engines to perform text shaping across element boundaries. + +
+ Note: As an example the following should produce one {{LayoutFragment}} but is from + three non-[=atomic inlines=]: +
+        ع<span style="color: blue">ع</span>ع
+    
+
+ +Note: When accessing the {{LayoutChild/styleMap}} the user agent can create a new + {{StylePropertyMapReadOnly}} if none exists yet. + +
+The styleMap, on getting from a {{LayoutChild}} |this|, the +user agent must perform the following steps: + + 1. If |this|' {{[[styleMap]]}} is null, then: + + 1. Let |box| be |this|' {{LayoutChild/[[box]]}}. + + 2. Let |definition| be the result of [=get a layout definition=]. + + 3. Let |childInputProperties| be |definition|'s [=layout definition/child input + properties=]. + + 4. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the + [=computed values=] for properties listed in |childInputProperties| for |box|. + + 5. Set |this|' {{LayoutChild/[[styleMap]]}} internal slot to |styleMap|. + + Note: If the user agent always pre-populates {{LayoutChild/[[styleMap]]}} then this branch + of the algorithm won't be reached. + + 2. Return |this|' {{StylePropertyMapReadOnly}} contained in the {{LayoutChild/[[styleMap]]}} + internal slot. +
+ +Note: The {{intrinsicSizes()}} method allows the web developer to query the intrinsic sizes of the + {{LayoutChild}}. + +
+When the intrinsicSizes() method is called on a {{LayoutChild}} +|this|, the user agent must perform the following steps: + + 1. Let |p| be a new promise. + + 2. Let |context| be the [=current layout's=] [=layout API context=]. + + 3. If |this|' {{LayoutChild/[[unique id]]}} is not equal to |context|'s [=unique id=], reject + |p| with a "{{InvalidStateError}}" {{DOMException}}, and abort all these steps. + + Note: This is to ensure that only {{LayoutChild}}ren passed in as arguments to either the + layout or intrinsicSizes method are used. + + 4. Let |task| be a new [=layout API work task=] with: + + - [=layout api work task/layout child=] being |this|. + + - [=layout api work task/task type=] being "intrinsic-sizes". + + - [=layout api work task/promise=] being |p|. + + 5. [=list/Append=] |task| to |context|'s [=work queue=]. + + 6. Return |p|. +
+ +Note: The {{layoutNextFragment()}} method allows the web developer to produce a {{LayoutFragment}} + for a given {{LayoutChild}} (the result of performing layout). + +
+When the layoutNextFragment(|constraints|, |breakToken|) method is +called on a {{LayoutChild}} |this|, the user agent must perform the following steps: + + 1. Let |p| be a new promise. + + 2. Let |context| be the [=current layout's=] [=layout API context=]. + + 3. If |this|' {{LayoutChild/[[unique id]]}} is not equal to |context|'s [=unique id=], reject + |p| with a "{{InvalidStateError}}" {{DOMException}}, and abort all these steps. + + Note: This is to ensure that only {{LayoutChild}}ren passed in as arguments to either the + layout or intrinsicSizes method are used. + + 4. If |breakToken|'s {{ChildBreakToken/[[unique id]]} is not equal to |context|'s [=unique id=], + reject |p| with a "{{InvalidStateError}}" {{DOMException}}, and abort all these steps. + + 5. If |context|'s [=layout API context/mode=] is "intrinsic-sizes", reject |p| with + a "{{NotSupportedError}}" {{DOMException}}. + + Note: This is to ensure that inside a intrinsicSizes callback, + {{LayoutChild/layoutNextFragment()}} cannot be called. + + 6. Let |task| be a new [=layout API work task=] with: + + - [=layout api work task/layout constraints=] being |constraints|. + + - [=layout api work task/layout child=] being |this|. + + - [=layout api work task/child break token=] being |breakToken|. + + - [=layout api work task/task type=] being "layout". + + - [=layout api work task/promise=] being |p|. + + 7. [=list/Append=] |task| to |context|'s [=work queue=]. + + 8. Return |p|. +
+ +### LayoutChildren and the Box Tree ### {#layout-child-box-tree} + +Each [=box=] has a \[[layoutChildMap]] internal slot, which is a +[=map=] of {{LayoutWorkletGlobalScope}}s to {{LayoutChild}}ren. + +Note: [=Get a layout child=] returns a {{LayoutChild}} object for the correct + {{LayoutWorkletGlobalScope}} and creates one if it doesn't exist yet. + +
+When the user agent wants to get a layout child given |workletGlobalScope|, |name|, + |box|, and |uniqueId|, it must run the following steps: + + 1. Assert that: + - |box| is currently attached to the [=box tree=]. + - |box|'s [=containing block=] is a [=layout API container=]. + - The [=containing block's=] ''layout()'' function's first argument is |name|. + + 2. Let |layoutChildMap| be |box|'s {{[[layoutChildMap]]}}. + + 3. If |layoutChildMap|[|workletGlobalScope|] does not exist, run the following + steps: + + 1. Let |definition| be the result of [=get a layout definition=] given |name|, and + |workletGlobalScope|. + + Assert that [=get a layout definition=] succeeded, and |definition| is not + "invalid". + + 2. Let |childInputProperties| be |definition|'s child input + properties. + + 3. Let |layoutChild| be a new {{LayoutChild}} with internal slot(s): + - {{LayoutChild/[[box]]}} set to |box|. + - {{[[styleMap]]}} set to a new {{StylePropertyMapReadOnly}} populated with + only the [=computed values=] for properties listed in + |childInputProperties|. + + 4. Set |layoutChildMap|[|workletGlobalScope|] to |layoutChild|. + + 4. Let |layoutChild| be the result of get |layoutChildMap|[|workletGlobalScope|]. + + 5. Set |layoutChild|'s {{LayoutChild/[[unique id]]}} internal slot to |uniqueId|. + + 6. Return |layoutChild|. +
+ +When a [=box=] is inserted into the [=box tree=] the user agent may pre-populate the +{{[[layoutChildMap]]}} for all {{LayoutWorkletGlobalScope}}s. + +When a [=box=] is removed from the [=box tree=] the user agent must clear the +{{[[layoutChildMap]]}}. + +The user agent must clear the {{[[layoutChildMap]]}} internal slot every 1000 layout +passes. + +Note: The above rule exists to ensure that web developers do not rely on being able to store + non-regeneratable state on the {{LayoutChild}} object. + The 1000 limit was picked as a high upper bound, this limit may improve (downwards) over time. + +
+When the user agent wants to update a layout child style given |box|, it must +run the following steps: + + 1. Assert that: + - |box| is currently attached to the [=box tree=]. + + 2. If |box|'s [=containing block=] is not a [=layout API container=], abort all these + steps. + + 3. Let |layoutChildMap| be |box|'s {{[[layoutChildMap]]}}. + + 4. For each |layoutChild| in |layoutChildMap|: + + 1. |layoutChild|'s {{[[styleMap]]}} to null. +
+ +When the [=computed values=] of [=document layout definition/child input properties=] for a [=box=] +changes the user agent must run the [=update a layout child style=] algorithm. + +Layout Fragments {#layout-fragments} +------------------------------------ + +A {{LayoutFragment}} represents a CSS [=fragment=] of a {{LayoutChild}} after layout has occurred on +that child. This is produced by the {{LayoutChild/layoutNextFragment()}} method. + +
+[Exposed=LayoutWorklet]
+interface LayoutFragment {
+    readonly attribute double inlineSize;
+    readonly attribute double blockSize;
+
+    attribute double inlineOffset;
+    attribute double blockOffset;
+
+    readonly attribute any data;
+
+    readonly attribute ChildBreakToken? breakToken;
+};
+
+ +The {{LayoutFragment}} has internal slot(s): + - [[unique id]] the [=unique id=] of the [=layout api + context=] which produced this child fragment. This slot is used so that a {{LayoutFragment}} + from a previous layout pass is invalid. + +
+ +The {{LayoutFragment}} has {{LayoutFragment/inlineSize}} and {{LayoutFragment/blockSize}} +attributes, which are set by the respective child's layout algorithm. They represent the border +box size of the CSS [=fragment=], and are relative to the [=current layout's=] writing mode. + +The {{LayoutFragment/inlineSize}} and {{LayoutFragment/blockSize}} attributes cannot be changed. If +the [=current layout=] requires a different {{LayoutFragment/inlineSize}} or +{{LayoutFragment/blockSize}} the author must perform {{LayoutChild/layoutNextFragment()}} again with +different arguments in order to get different results. + +The author inside the current layout can position a resulting {{LayoutFragment}} by setting its +{{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} attributes. If not set by the +author they default to zero. The {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} +attributes represent the position of the {{LayoutFragment}} relative to its parent's border +box, before transform or positioning (e.g. if a fragment is [=relatively positioned=]) has +been applied. + +
+ An example of position a fragment in different writing modes. +
+ A simple visualization showing positioning a {{LayoutFragment}} using + {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} in different writing + modes. +
+
+ + +
+The example below shows the basic usage of a {{LayoutFragment}}. +
+registerLayout('example-layout-fragment', class {
+  async layout(children, edges, constraints, styleMap) {
+
+    // You must perform layout to generate a fragment.
+    const fragment = await child.layoutNextFragment({});
+
+    // You can query the size of the fragment produced:
+    console.log(fragment.inlineSize);
+    console.log(fragment.blockSize);
+
+    // You can set the position of the fragment, e.g. this will set it to the
+    // top-left corner:
+    fragment.inlineOffset = edges.inlineStart;
+    fragment.blockOffset = edges.blockStart;
+
+    // Data may be passed from the child layout:
+    console.log(fragment.data);
+
+    // If the child fragmented, you can use the breakToken to produce the next
+    // fragment in the chain.
+    const nextFragment = await child.layoutNextFragment({}, fragment.breakToken);
+  }
+});
+
+
+ +A [=layout API container=] can communicate with other [=layout API containers=] by using the +{{LayoutFragment/data}} attribute. This is set by the {{FragmentResultOptions/data}} member in the +{{FragmentResultOptions}} dictionary. + +The {{LayoutFragment}}'s {{LayoutFragment/breakToken}} specifies where the {{LayoutChild}} last +fragmented. If the {{LayoutFragment/breakToken}} is null the {{LayoutChild}} wont produce any more +{{LayoutFragment}}s for that token chain. The {{LayoutFragment/breakToken}} can be passed to the +{{LayoutChild/layoutNextFragment()}} function to produce the next {{LayoutFragment}} for a +particular child. The {{LayoutFragment/breakToken}} cannot be changed. +If the [=current layout=] requires a different {{LayoutFragment/breakToken}} the author must perform +{{LayoutChild/layoutNextFragment()}} again with different arguments. + +Intrinsic Sizes {#intrinsic-sizes} +---------------------------------- + +
+[Exposed=LayoutWorklet]
+interface IntrinsicSizes {
+  readonly attribute double minContentSize;
+  readonly attribute double maxContentSize;
+};
+
+ +A {{IntrinsicSizes}} object represents the [=min-content size=] and [=max-content size=] of a CSS +[=box=]. It has {{IntrinsicSizes/minContentSize}} and {{IntrinsicSizes/maxContentSize}} attributes +which represent the border box min/max-content contribution of the {{LayoutChild}} for the +[=current layout=]. The attributes are relative to the inline direction of the [=current layout's=] +writing mode. + +The {{IntrinsicSizes/minContentSize}} and {{IntrinsicSizes/maxContentSize}} cannot be changed. They +must not change for a {{LayoutChild}} within the current layout pass. + +
+The example below shows the border-box intrinsic sizes of two children. + +
+<style>
+.child-0 {
+  width: 380px;
+  border: solid 10px;
+}
+
+.child-1 {
+  border: solid 5px;
+}
+
+.box {
+  display: layout(intrinsic-sizes-example);
+  font: 25px/1 Ahem;
+}
+</style>
+
+<div class="box">
+  <div class="child-0"></div>
+  <div class="child-1">XXX XXXX</div>
+</div>
+
+ +
+registerLayout('intrinsic-sizes-example', class {
+    async intrinsicSizes(children, edges, styleMap) {
+      const childrenSizes = await Promise.all(children.map((child) => {
+          return child.intrinsicSizes();
+      }));
+
+      childrenSizes[0].minContentSize; // 400, (380+10+10) child has a fixed size.
+      childrenSizes[0].maxContentSize; // 400, (380+10+10) child has a fixed size.
+
+      childrenSizes[1].minContentSize; // 100, size of "XXXX".
+      childrenSizes[1].maxContentSize; // 200, size of "XXX XXXX".
+    }
+
+    layout() {}
+});
+
+
+ +Layout Constraints {#layout-constraints} +---------------------------------------- + +A {{LayoutConstraints}} object is passed into the layout method which represents the all the +constraints for the [=current layout=] to perform layout within. + +
+[Exposed=LayoutWorklet]
+interface LayoutConstraints {
+    readonly attribute double availableInlineSize;
+    readonly attribute double availableBlockSize;
+
+    readonly attribute double? fixedInlineSize;
+    readonly attribute double? fixedBlockSize;
+
+    readonly attribute double percentageInlineSize;
+    readonly attribute double percentageBlockSize;
+
+    readonly attribute double? blockFragmentationOffset;
+    readonly attribute BlockFragmentationType blockFragmentationType;
+
+    readonly attribute any data;
+};
+
+enum BlockFragmentationType { "none", "page", "column", "region" };
+
+ +The {{LayoutConstraints}} object has {{LayoutConstraints/availableInlineSize}} and +{{LayoutConstraints/availableBlockSize}} attributes. This represents the [=available space=] for the +[=current layout=] to respect. + +Note: Some layouts may need to produce a {{LayoutFragment}} which exceed this size. For example a + [=replaced element=]. The [=parent layout=] should expect this to occur and deal with it + appropriately. + +A [=parent layout=] may require the [=current layout=] to be exactly a particular size. If the +{{LayoutConstraints/fixedInlineSize}} or {{LayoutConstraints/fixedBlockSize}} are specified the +[=current layout=] should produce a {{LayoutFragment}} with a the specified size in the appropriate +direction. + +The {{LayoutConstraints}} object has {{LayoutConstraints/percentageInlineSize}} and +{{LayoutConstraints/percentageBlockSize}} attributes. These represent the size that percentages +should be resolved against while performing layout. + +The {{LayoutConstraints}} has a {{LayoutConstraints/blockFragmentationType}} attribute. The +[=current layout=] should produce a {{LayoutFragment}} which fragments at the +{{LayoutConstraints/blockFragmentationOffset}} if possible. + +The [=current layout=] can choose not to fragment a {{LayoutChild}} based on the +{{LayoutConstraints/blockFragmentationType}}, for example if the child has a property like +''break-inside: avoid-page;''. + +
+The example below shows the basic usage of the {{LayoutConstraints}} object. + +
+// The class below is registered with a "block-like" sizingMode, and can use the fixedInlineSize,
+// fixedBlockSize attributes.
+registerLayout('layout-constraints-example', class {
+    async layout(children, edges, constraints, styleMap) {
+
+        // Calculate the available size.
+        const availableInlineSize = constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.inline : null;
+
+        // Web developers should resolve any percentages against the percentage sizes.
+        const value = constraints.percentageInlineSize * 0.5;
+
+    }
+});
+
+
+ +
+ +The [=create a layout constraints object=] algorithm is used to create the {{LayoutConstraints}} +object. Depending on the {{LayoutOptions/sizing}} it will either pre-calculate the +{{LayoutConstraints/fixedInlineSize}} and {{LayoutConstraints/fixedBlockSize}} upfront. + +
+When the user agent wants to create a layout constraints object given |sizingMode|, |box|, +and |internalLayoutConstraints|, it must run the following steps: + + 1. If |sizingMode| is "block-like" then: + + 1. Let |fixedInlineSize| be the result of calculating |box|'s border-box + [=inline size=] (relative to |box|'s writing mode) exactly like block containers do. + + 2. Let |fixedBlockSize| be null if |box|'s [=block size=] is unable to be calculated at this + stage, (e.g. [=block size=] is ''height/auto''), otherwise the result of calculating + |box|'s border-box [=block size=] exactly like block containers do. + + 3. Return a new {{LayoutConstraints}} object with: + + - {{LayoutConstraints/fixedInlineSize}}, and {{LayoutConstraints/availableInlineSize}} + set to |fixedInlineSize|. + + - {{LayoutConstraints/percentageInlineSize}} set to |internalLayoutConstraints|' + percentage resolution size in the inline axis (relative to |box|'s writing mode). + + - {{LayoutConstraints/fixedBlockSize}} set to |fixedBlockSize|. + + - {{LayoutConstraints/availableBlockSize}} set to |fixedBlockSize| if not null, + otherwise |internalLayoutConstraints|' [=available space=] in the block axis + (relative to |box|'s writing mode). + + - {{LayoutConstraints/percentageBlockSize}} set to |internalLayoutConstraints|' + percentage resolution size in the block axis (relative to |box|'s writing mode). + + 2. If |sizingMode| is "manual" then: + + 1. Return a new {{LayoutConstraints}} object with: + - {{LayoutConstraints/fixedInlineSize}}/{{LayoutConstraints/fixedBlockSize}} set to + |internalLayoutConstraints|' fixed inline/block size (relative to |box|'s writing + mode) imposed by the [=parent layout=]. Either may be null. + + Note: See [[#interaction-sizing]] for different scenarios when this can occur. + + - {{LayoutConstraints/availableInlineSize}}/{{LayoutConstraints/availableBlockSize}} set + to |internalLayoutConstraints|' [=available space=]. + + - {{LayoutConstraints/percentageInlineSize}}/{{LayoutConstraints/percentageBlockSize}} + set to |internalLayoutConstraints|' percentage resolution size. +
+ +### Constraints for Layout Children ### {#layout-constraints-children} + +The {{LayoutConstraintsOptions}} dictionary represents the set of constraints which can be passed to +a {{LayoutChild}} to produce a {{LayoutFragment}}. + +
+dictionary LayoutConstraintsOptions {
+    double availableInlineSize;
+    double availableBlockSize;
+
+    double fixedInlineSize;
+    double fixedBlockSize;
+
+    double percentageInlineSize;
+    double percentageBlockSize;
+
+    double blockFragmentationOffset;
+    BlockFragmentationType blockFragmentationType = "none";
+
+    any data;
+};
+
+ +Note: The [=translate a LayoutConstraintsOptions to internal constraints=] describes how to convert + a {{LayoutConstraintsOptions}} object into a user agents internal representation. + +
+When the user agent wants to translate a LayoutConstraintsOptions to internal constraints +given |options|, it must run the following steps: + + 1. Let the [=available space=] in the inline direction (with respect to the [=current layout=], + be the result of: + + - If |options|' {{LayoutConstraintsOptions/availableInlineSize}} is not null, and + {{LayoutConstraintsOptions/availableInlineSize}} is greater than zero, let the result be + {{LayoutConstraintsOptions/availableInlineSize}}. + + - Otherwhise, let the result be zero. + + 2. Let the [=available space=] in the block direction (with respect to the [=current layout=]), + be the result of: + + - If |options|' {{LayoutConstraintsOptions/availableBlockSize}} is not null, and + {{LayoutConstraintsOptions/availableBlockSize}} is greater than zero, let the result be + {{LayoutConstraintsOptions/availableBlockSize}}. + + - Otherwhise, let the result be zero. + + 3. Let the override size in the inline direction (with respect to the [=current layout=], be the + result of: + + - Let the result be |options|' {{LayoutConstraintsOptions/fixedInlineSize}}. + + Note: If the {{LayoutConstraintsOptions/fixedInlineSize}} is null, no override size is + applied. + + 4. Let the override size in the block direction (with respect to the [=current layout=], be the + result of: + + - Let the result be |options|' {{LayoutConstraintsOptions/fixedBlockSize}}. + + Note: If the {{LayoutConstraintsOptions/fixedBlockSize}} is null, no override size is + applied. + + 5. Let the percentage resultion size in the inline direction (with respect to the [=current + layout=], be the result of: + + - If |options|' {{LayoutConstraintsOptions/percentageInlineSize}} is not null, and + {{LayoutConstraintsOptions/percentageInlineSize}} is greater than zero, let the result + be {{LayoutConstraintsOptions/percentageInlineSize}}. + + - If |options|' {{LayoutConstraintsOptions/availableInlineSize}} is not null, and + {{LayoutConstraintsOptions/availableInlineSize}} is greater than zero, let the result be + {{LayoutConstraintsOptions/availableInlineSize}}. + + - Otherwhise, let the result be zero. + + 6. Let the percentage resultion size in the block direction (with respect to the [=current + layout=], be the result of: + + - If |options|' {{LayoutConstraintsOptions/percentageBlockSize}} is not null, and + {{LayoutConstraintsOptions/percentageBlockSize}} is greater than zero, let the result + be {{LayoutConstraintsOptions/percentageBlockSize}}. + + - If |options|' {{LayoutConstraintsOptions/availableBlockSize}} is not null, and + {{LayoutConstraintsOptions/availableBlockSize}} is greater than zero, let the result be + {{LayoutConstraintsOptions/availableBlockSize}}. + + - Otherwhise, let the result be zero. + + 7. If the [=child layout=] is a [=layout API container=], then let the store the data (passed by + {{LayoutConstraints/data}}) be the result of: + + - Invoking [=StructuredSerializeForStorage=] on |options|' + {{LayoutConstraintsOptions/data}}. +
+ +
+The example below shows the basic usage of the {{LayoutConstraintsOptions}} dictionary. + +
+// The class below is registered with a "block-like" sizingMode, and can use the
+// fixedInlineSize, fixedBlockSize attributes.
+registerLayout('child-layout-constraints-example', class {
+    async layout(children, edges, constraints, styleMap) {
+
+        // The call below gives the child an "available" space. It will try and
+        // fit within this.
+        const fragment = children[0].layoutNextFragment({
+            availableInlineSize: 100,
+            availableBlockSize: 200,
+        });
+
+        // The call below gives the child a "fixed" size, it will be forced to
+        // this size ignoring any style set.
+        const fragment = children[0].layoutNextFragment({
+            fixedInlineSize: 20,
+            fixedBlockSize: 30,
+        });
+
+    }
+});
+
+
+ +Breaking and Fragmentation {#breaking-and-fragmentation} +-------------------------------------------------------- + +A {{LayoutChild}} can produce multiple {{LayoutFragment}}s. A {{LayoutChild}} may fragment in the +block direction if a {{LayoutConstraints/blockFragmentationType}} is not none. Additionally +{{LayoutChild}} which represents [=inline-level=] content, may fragment line by line if the +layout options' {{LayoutOptions/childDisplay}} (set by +layoutOptions) is "normal". + +
+[Exposed=LayoutWorklet]
+interface ChildBreakToken {
+    readonly attribute BreakType breakType;
+    readonly attribute LayoutChild child;
+};
+
+[Exposed=LayoutWorklet]
+interface BreakToken {
+    readonly attribute FrozenArray<ChildBreakToken> childBreakTokens;
+    readonly attribute any data;
+};
+
+dictionary BreakTokenOptions {
+    sequence<ChildBreakToken> childBreakTokens;
+    any data = null;
+};
+
+enum BreakType { "none", "line", "column", "page", "region" };
+
+ +The {{ChildBreakToken}} has internal slot(s): + - [[unique id]] the [=unique id=] of the [=layout api + context=] which produced this child break token. This slot is used so that a + {{ChildBreakToken}} from a previous layout pass is invalid. + +
+ +A subsequent {{LayoutFragment}} is produced by using the previous {{LayoutFragment}}'s +{{LayoutFragment/breakToken}}. This tells the [=child layout=] to produce a {{LayoutFragment}} +starting at the point encoded in the {{ChildBreakToken}}. + +Edges {#edges} +-------------- + +A {{LayoutEdges}} object is passed into the layout method. This represents the sum of all the [=box +model edges=] (border, scrollbar, padding), for the current box which is being laid out. + +
+[Exposed=LayoutWorklet]
+interface LayoutEdges {
+  readonly attribute double inlineStart;
+  readonly attribute double inlineEnd;
+
+  readonly attribute double blockStart;
+  readonly attribute double blockEnd;
+
+  // Convenience attributes for the sum in one direction.
+  readonly attribute double inline;
+  readonly attribute double block;
+};
+
+ +Each of the accessors represents the width in CSS pixels of an edge in each of the [=abstract +dimensions=] ({{LayoutEdges/inlineStart}}, {{LayoutEdges/inlineEnd}}, {{LayoutEdges/blockStart}}, +{{LayoutEdges/blockEnd}}). + +The {{LayoutEdges/inline}}, and {{LayoutEdges/block}} on the {{LayoutEdges}} object are convenience +attributes which represent the sum in that direction. + +
+ An example of layout edges. +
+ A visualization showing the {{LayoutEdges}} object for a [=box=] with different sized + scrollbar, border, and padding. +
+
+ +
+This example shows an node styled by CSS, and what its respective {{LayoutEdges}} could contain. + +
+<style>
+.container {
+  width: 50px;
+  height: 50px;
+}
+
+.box {
+  display: layout(box-edges);
+
+  padding: 10%;
+  border: solid 2px;
+  overflow-y: scroll;
+}
+</style>
+
+<div class="container">
+  <div class="box"></div>
+</div>
+
+ +
+registerLayout('box-edges', class {
+    async layout(children, edges, constraints, styleMap, breakToken) {
+        edges.inlineStart; // 2 + 5 (as 10% * 50px = 5px).
+        edges.blockEnd; // 7 (2 + 5)
+        edges.inlineEnd; // UA-dependent, due to scrollbar.
+                         //  Could be 2 + 5 + 0 or 2 + 5 + 16 for example.
+        edges.block; // 14 (2 + 5 + 5 + 2).
+    }
+}
+
+
+ +Interactions with other Modules {#interactions-with-other-modules} +================================================================== + +This section describes how other CSS modules interact with the CSS Layout API. + +Sizing {#interaction-sizing} +---------------------------- + +User agents must use the {{LayoutConstraints}} object to communicate to the [=current layout=] the +size they would like the fragment to be. + +If the user agent wishes to force a size on the box, it can use the +{{LayoutConstraints/fixedInlineSize}} and {{LayoutConstraints/fixedBlockSize}} attributes to do so. + +The [=layout API container=] can be passed size information in different ways depending on the value +of layout options' {{LayoutOptions/sizing}} (set by +layoutOptions on the class). + +If the value of layout options' {{LayoutOptions/sizing}} is +"block-like", then the {{LayoutConstraints}} passed to the [=layout API container=]: + - Must calculate and set {{LayoutConstraints/fixedInlineSize}} based off the rules + specified in [[!css-sizing-3]] and the formatting context in which it participates, e.g. + + - As a [=block-level=] box in a [=block formatting context=], it is sized like a [=block + box=] that establishes a formatting context, with an ''width/auto'' [=inline size=] + calculated as for non-replaced block boxes. + + - As an [=inline-level=] box in an [=inline formatting context=], it is sized as an atomic + inline-level box (such as an inline-block). + + - Must calculate and set {{LayoutConstraints/fixedBlockSize}} based off the rules + specified in [[!css-sizing-3]], and the formatting context in which it participates. If the + [=layout API container=] has an ''height/auto'' [=block size=], and cannot be determined + ahead of time, {{LayoutConstraints/fixedBlockSize}} must be set to null. + +If the value of layout options' {{LayoutOptions/sizing}} is +"manual", then the user-agent must not pre-calculate +{{LayoutConstraints/fixedInlineSize}} and/or {{LayoutConstraints/fixedBlockSize}} ahead of time, +except when it is being forced to a particular size by the formatting context in which it +participates, for example: + + - If the [=layout API container=] is within a [=block formatting context=], is inflow, and has + an ''width/auto'' inline size, the user agent must set the + {{LayoutConstraints/fixedInlineSize}} to the [=stretch-fit inline size=]. + +
+ Note: In the example below the [=layout API container=] has its inline size set to 50. + +
+        <style>
+          #container {
+            width: 100px;
+            height: 100px;
+            box-sizing: border-box;
+            padding: 5px;
+          }
+          #layout-api {
+            display: layout(foo);
+            margin: 0 20px;
+          }
+        </style>
+        <div id="container">
+          <div id="layout-api"></div>
+        </div>
+    
+
+ +### Positioned layout sizing ### {#interaction-sizing-positiong-layout} + +If a [=layout API container=] is out-of-flow positioned the user agent must solve the +positioned size equations ([[css-position-3#abs-non-replaced-width]], +[[css-position-3#abs-non-replaced-height]]), and set the appropriate +{{LayoutConstraints/fixedInlineSize}} and {{LayoutConstraints/fixedBlockSize}}. + +
+ Note: In the example below the [=layout API container=] has its inline and block size fixed to + 80. + +
+        <style>
+          #container {
+            position: relative;
+            width: 100px;
+            height: 100px;
+          }
+          #layout-api {
+            display: layout(foo);
+            top: 10px;
+            bottom: 10px;
+            left: 10px;
+            right: 10px;
+            position: absolute;
+          }
+        </style>
+        <div id="container">
+          <div id="layout-api"></div>
+        </div>
+    
+
+ +Positioning {#interaction-positioning} +-------------------------------------- + +All positioning in this level of the specification is handled by the user agent. + +As a result: + - Out-of-flow children do not appear as {{LayoutChild}}ren. + + - [=Layout API containers=] establish [=containing blocks=] exactly like block + containers do. [[!CSS21]] + + - The {{LayoutFragment/inlineOffset}} and {{LayoutFragment/blockOffset}} represent the position of + the fragment before any positioning and transforms have occured. + + - The [=static position=] of an absolutely-positioned child of a [=layout API container=] is set + to the [=inline-start=], [=block-start=] padding edge of the [=layout API container=]. Auto + margins are treated as zero for the child. + +
+ Note: In the example below: + - "child-relative" would be the only child passed to the author's layout. If it was positioned + at ({{LayoutFragment/inlineOffset}} = 20, {{LayoutFragment/blockOffset}} + = 30), its final position would be (25, 40) as the + relative positioning was handled by the user agent. + + - "child-absolute" would not appear as a {{LayoutChild}}, and instead would be laid out and + positioned by the user agent. + + - The examples above also apply in a similar way to sticky and fixed positioned children. + +
+        <style>
+          #container {
+            display: layout(foo);
+            position: relative; /* container is a containing block */
+            width: 100px;
+            height: 100px;
+          }
+          #child-relative {
+            position: relative;
+            left: 5px;
+            top: 10px;
+          }
+        </style>
+        <div id="container">
+          <div id="child-relative"></div>
+          <div id="child-absolute"></div>
+        </div>
+    
+
+ +Overflow {#interaction-overflow} +-------------------------------- + +The [=scrollable overflow=] for a [=layout API container=] is handled by the user agent in this +level of the specification. + +A [=layout API container=] should calculate its scrollable overflow exactly like block containers +do. + +Even if the author's [=layout API container=] positions a fragment into the [=scrollable overflow=] +region, relative positioning or transforms may cause the fragment to shift such that its +[=scrollable overflow=] region, causing no overflow to occur. + +Fragmentation {#interaction-fragmentation} +------------------------------------------ + +A [=parent layout=] can ask the [=current layout=] to [=fragment=] by setting the +{{LayoutConstraints/blockFragmentationType}} and {{LayoutConstraints/blockFragmentationOffset}}. + +E.g. [[css-multicol-1]] layout would set a {{LayoutConstraints/blockFragmentationType}} to +"column" and set the {{LayoutConstraints/blockFragmentationOffset}} to where it needs the +child to fragment. + +Alignment {#interaction-alignment} +---------------------------------- + +The first/last baseline sets of a [=layout API container=] is generated exactly like block +containers do (see [[css-align-3#baseline-export]]). Except that the order of the in-flow children +should be determined by the in which they are returned form the layout method (via +{{FragmentResultOptions/childFragments}}) instead of the document order. + +
+Note: In a future level of the specification there will be the ability for the author to define the +baselines themselves. This will be of the form: + +To query baseline information from a {{LayoutChild}}. +
+const fragment = await child.layoutNextFragment({
+  fixedInlineSize: availableInlineSize,
+  baselineRequests: ['alphabetic', 'middle'],
+});
+fragment.baselines.get('alphabetic') === 25 /* or something */;
+
+ +To produce baseline information for a [=parent layout=]: +
+registerLayout('baseline-producing', class {
+  async layout(children, edges, constraints, styleMap) {
+    const result = {baselines: {}};
+
+    for (let baselineRequest of constraints.baselineRequests) {
+      // baselineRequest === 'alphabetic', or something else.
+      result.baselines[baselineRequest] = 25;
+    }
+
+    return result;
+  }
+});
+
+
+ +Layout {#layout} +================ + +This section describes how the CSS Layout API interacts with the user agent's layout engine. + +Processing Model {#processing-model} +------------------------------------ + +A layout API work task is a [=struct=] which describes the information needed by the user +agent layout engine to perform layout work. It consists of: + + - layout constraints a {{LayoutConstraintsOptions}}. + + - layout child a {{LayoutChild}}. + + - child break token a {{ChildBreakToken}}. + + - task type which is either "layout", or + "intrinsic-sizes" + + - promise a promise object. + +A layout API context is a [=struct=] which describes the information needed by the +[=current layout=] to produce either a fragment or determine the intrinsic-sizes for a [=box=]. It +consits of: + + - work queue which is a [=list=] of [=layout API work + tasks=]. The user agent will alternate between processing these tasks, and running the + microtask queue. + + - unique id a internal unique identifier. This is used for + determining that objects exposed to the web developer are only used within the correct + layout pass. E.g. {{LayoutFragment}}s returned in the {{FragmentResultOptions}} dictionary + belong to the current layout pass. + + - mode which is either "layout", or + "intrinsic-sizes". This is used for determining what the user agent layout + engine is producing, and if a call to {{LayoutChild/layoutNextFragment()}} is valid. + +
+When the user agent wants to create a layout API context given |mode|, it must +run the following steps: + + 1. Return a new [=layout API context=] with: + + - [=work queue=] being a new [=list/empty=] [=list=]. + + - [=unique id=] being a unique id. + + - [=mode=] being |mode|. + +
+ +Performing Layout {#performing-layout} +-------------------------------------- + +The section describes how a user agent calls the web developer defined layout to produces intrinsic +sizes, and fragments. + +
+// This is the final return value from the author defined layout() method.
+dictionary FragmentResultOptions {
+    double inlineSize = 0;
+    double blockSize = 0;
+    double autoBlockSize = 0;
+    sequence<LayoutFragment> childFragments = [];
+    any data = null;
+    BreakTokenOptions breakToken = null;
+};
+
+[Exposed=LayoutWorklet]
+interface FragmentResult {
+    constructor(optional FragmentResultOptions options = {});
+    readonly attribute double inlineSize;
+    readonly attribute double blockSize;
+};
+
+dictionary IntrinsicSizesResultOptions {
+    double maxContentSize;
+    double minContentSize;
+};
+
+ +The {{FragmentResult}} has internal slot(s): + + - \[[box]] a CSS [=box=]. + + - [[inline size]] the inline size of the resulting + fragment. + + - [[block size]] the block size of the resulting + fragment. + + - [[child fragments]] the list of child fragments. + + - \[[data]] some optional serialized data. + + - [[internal break token]] an internal representation of + the break information for this fragment. + + - [[unique id]] the [=unique id=] of the current + [=layout api context=]. This slot is used so that a {{FragmentResult}} used outside the + current layout pass is invalid. + +
+ +The web developer defined layout method can return either a {{FragmentResultOptions}} or a +{{FragmentResult}}. The {{FragmentResult}} can be used for determining the final size of the +fragment or detecting if the provided {{FragmentResultOptions}} would result in triggering a +fallback to [=flow layout=]. + +
+This example show the web developer using the {{FragmentResult}} instead of just returning the +{{FragmentResultOptions}} object. + +
+registerLayout('feature-detection', class {
+    async layout(children, edges, constraints, styleMap, breakToken) {
+
+      let result;
+      try {
+        result = new FragmentResult({
+          childFragments: [],
+          autoBlockSize: 100
+        });
+      } catch (e) {
+        // The above call may throw, if the dictionary was just returned, it
+        //  would fallback to flow layout.
+      }
+
+      // The web developer can test what size the fragment will be.
+      result.blockSize;
+
+      // Instead of returning the dictionary, we can just return this object.
+      return result;
+    }
+}
+
+
+ +
+The inlineSize, on getting from a {{FragmentResult}} |this|, +the user agent must perform the following steps: + + 1. Return |this|' {{FragmentResult/[[inline size]]}} internal slot. +
+ +
+The blockSize, on getting from a {{FragmentResult}} |this|, +the user agent must perform the following steps: + + 1. Return |this|' {{FragmentResult/[[block size]]}} internal slot. +
+ +
+When the FragmentResult(|options|) constructor is called, +the user agent must perform the following steps: + + 1. Let |context| be the [=current layout's=] [=layout API context=]. + + 2. Return the result of [=construct a fragment result=] given |context|, and |options|. + +
+ +Note: The [=construct a fragment result=] algorithm performs a series of validation checks (the + web developer isn't using an object from a previous invocation, and determines the final size of + the resulting fragment. + +
+When the user agent wants to construct a fragment result given |context|, and |options| +the user agent must perform the following steps: + + 1. Let |uniqueId| be |context|'s [=unique id=]. + + 2. Let |box| be the [=current layout's=] [=box=]. + + 3. Let |breakTokenOptions| be |options|'s {{FragmentResultOptions/breakToken}}. + + 4. [=list/For each=] |childFragment| in |options|'s {{FragmentResultOptions/childFragments}}, + perform the following stubsteps: + + 1. If |childFragment|'s {{LayoutFragment/[[unique id]]}} internal slot is not equal to + |uniqueId|, then [=throw=] a [=TypeError=], and abort all these steps. + + 5. [=list/For each=] |childBreakToken| in |breakTokenOptions|'s + {{BreakTokenOptions/childBreakTokens}}, perform the following stubsteps: + + 1. If |childBreakToken|'s {{ChildBreakToken/[[unique id]]}} internal slot is not equal to + |uniqueId|, then [=throw=] a [=TypeError=], and abort all these steps. + + 6. If |sizingMode| is "block-like": + + - Then: + + 1. Let |inlineSize| be the result of calculating |box|'s border-box [=inline + size=] (relative to |box|'s writing mode) exactly like block containers do. + + 2. Let |blockSize| be the result of calculating |box|'s border-box + [=block size=] (relative to |box|'s writing mode) exactly like block containers do, + given |fragment|'s {{FragmentResultOptions/autoBlockSize}} as the "intrinsic + block size". + + - Otherwise (|sizingMode| is "manual"): + + 1. Let |inlineSize| be |fragment|'s {{FragmentResultOptions/inlineSize}}. + + 2. Let |blockSize| be |fragment|'s {{FragmentResultOptions/blockSize}}. + + 7. Let |clonedData| be the result of invoking [=StructuredSerializeForStorage=] on |options|'s + {{FragmentResultOptions/data}}. + + 8. Let |clonedBreakTokenData| be the result of invoking [=StructuredSerializeForStorage=] on + |breakTokenOptions|'s {{BreakTokenOptions/data}}. + + 9. Let |internalBreakToken| be the internal representation of the [=fragmentation break=] + containing |clonedBreakTokenData|, and |breakTokenOptions|. + + 10. Return a new {{FragmentResult}} with: + + - {{FragmentResult/[[box]]}} being |box|. + + - {{FragmentResult/[[inline size]]}} being |inlineSize|. + + - {{FragmentResult/[[block size]]}} being |blockSize|. + + - {{FragmentResult/[[child fragments]]}} being |options|'s + {{FragmentResultOptions/childFragments}}. + + - {{FragmentResult/[[data]]}} being |clonedData|. + + - {{FragmentResult/[[internal break token]]}} being |internalBreakToken|. + + - {{FragmentResult/[[unique id]]}} being |uniqueId|. + +
+ +### Determining Intrinsic Sizes ### {#determining-intrinsic-sizes} + +The [=determine the intrinsic sizes=] algorithm defines how a user agent is to query the author +defined layout for a [=box's=] [=intrinsic sizes=] information. + +Note: The [=determine the intrinsic sizes=] algorithm allows for user agents to cache an arbitrary + number of previous invocations to reuse. + +
+When the user agent wants to determine the intrinsic sizes of a [=layout API formatting +context=] for a given |box|, |childBoxes| it must run the following steps: + + 1. Let |layoutFunction| be the ''layout()'' for the [=computed value=] of <> for + |box|. + + 2. Let |name| be the first argument of the |layoutFunction|. + + 3. Let |documentDefinition| be the result of [=get a document layout definition=] given |name|. + + If [=get a document layout definition=] returned failure, or if |documentDefinition| is + "invalid", then let |box| fallback to the [=flow layout=] and abort all these + steps. + + 4. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of [=worklet's + WorkletGlobalScopes=] from the layout {{Worklet}}, following the rules defined in + [[#global-scope-selection]]. + + The user agent may also [=create a WorkletGlobalScope=] at this time, given the + layout {{Worklet}}. + + 5. Run [=invoke an intrinsic sizes callback=] given |name|, |box|, |childBoxes|, and + |workletGlobalScope| optionally [=in parallel=]. + + Note: If the user agent runs [=invoke an intrinsic sizes callback=] on a thread [=in + parallel=], it should select a layout worklet global scope which can be used on that + thread. +
+ +
+When the user agent wants to invoke an intrinsic sizes callback given |name|, |box|, +|childBoxes|, and |workletGlobalScope|, it must run the following steps: + + 1. Let |definition| be the result of [=get a layout definition=] given |name|, and + |workletGlobalScope|. + + If [=get a layout definition=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. + + 2. Let |layoutInstance| be the result of [=get a layout class instance=] given |box|, + |definition|, |workletGlobalScope|. + + If [=get a layout class instance=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. + + 3. Let |inputProperties| be |definition|'s [=layout definition/input properties=]. + + 4. Let |children| be a new [=list=]. + + 5. [=list/For each=] |childBox| in |childBoxes| perform the following substeps: + + 1. Let |layoutChild| be the result of [=get a layout child=] given |workletGlobalScope|, + |name|, |childBox|, and |context|'s [=unique id=]. + + 2. [=list/Append=] |layoutChild| to |children|. + + 6. Let |edges| be a new {{LayoutEdges}} populated with the [=computed value=] for all the [=box + model edges=] for |box|. + + 7. Let |styleMap| be the result of [=get a style map=] given |box|, and |inputProperties|. + + 8. At this stage the user agent may re-use the [=intrinsic sizes=] from a previous invocation if + |children|, |edges|, and |styleMap| are equivalent to that previous invocation. If so let + the intrinsic sizes the cached intrinsic sizes and abort all these steps. + + 9. Let |context| be the result of [=create a layout API context=] given + "intrinsic-sizes". + + 10. Let |intrinsicSizesFunction| be |definition|'s [=intrinsic sizes function=]. + + 11. Let |value| be the result of [=Invoke=](|intrinsicSizesFunction|, |layoutInstance|, + «|children|, |edges|, |styleMap|»). + + If an exception is [=thrown=] the let |box| fallback to the [=flow layout=] and abort all + these steps. + + 12. If |value| is a promise: + + - Then: + + 1. Let |intrinsicSizesValue| be the result of [=run a work queue=] given |value|, and + |context|'s [=work queue=]. + + If [=run a work queue=] returns failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. + + - Otherwise: + + 1. Let |intrinsicSizesValue| be |value|. + + 13. Let |intrinsicSizes| be the result of [=converting=] |intrinsicSizesValue| to a + {{IntrinsicSizesResultOptions}}. If an exception is [=thrown=], let |box| fallback to the + [=flow layout=] and abort all these steps. + + 14. Set the [=intrinsic sizes=] of |box|: + + - Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/minContentSize}} be the [=min-content + size=] of |box|. + + - Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/maxContentSize}} be the [=max-content + size=] of |box|. +
+ +### Generating Fragments ### {#generating-fragments} + +The [=generate a fragment=] algorithm defines how a user agent is to generate a [=box's=] +[=fragment=] for an author defined layout. + +Note: The [=generate a fragment=] algorithm allows for user agents to cache an arbitrary number of + previous invocations to reuse. + +
+When the user agent wants to generate a fragment of a [=layout API formatting context=] +for a given |box|, |childBoxes|, |internalLayoutConstraints|, and an optional |internalBreakToken| +it must run the following steps: + + 1. Let |layoutFunction| be the ''layout()'' for the [=computed value=] of <> for + |box|. + + 2. Let |name| be the first argument of the |layoutFunction|. + + 3. Let |documentDefinition| be the result of [=get a document layout definition=] given |name|. + + If [=get a document layout definition=] returned failure, or if |documentDefinition| is + "invalid", then let |box| fallback to the [=flow layout=] and abort all these + steps. + + 4. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of [=worklet's + WorkletGlobalScopes=] from the layout {{Worklet}}, following the rules defined in + [[#global-scope-selection]]. + + The user agent may also [=create a WorkletGlobalScope=] at this time, given the + layout {{Worklet}}. + + 5. Run [=invoke a layout callback=] given |name|, |box|, |childBoxes|, + |internalLayoutConstraints|, |internalBreakToken|, and |workletGlobalScope| optionally [=in + parallel=]. + + Note: If the user agent runs [=invoke a layout callback=] on a thread [=in parallel=], it + should select a layout worklet global scope which can be used on that thread. +
+ +
+When the user agent wants to invoke a layout callback given |name|, |box|, |childBoxes|, +|internalLayoutConstraints|, |internalBreakToken|, and |workletGlobalScope|, it must run the +following steps: + + 1. Let |definition| be the result of [=get a layout definition=] given |name|, and + |workletGlobalScope|. + + If [=get a layout definition=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. + + 2. Let |layoutInstance| be the result of [=get a layout class instance=] given |box|, + |definition|, |workletGlobalScope|. + + If [=get a layout class instance=] returned failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. + + 3. Let |context| be the result of [=create a layout API context=] given "layout". + + 4. Let |sizingMode| be |definition|'s layout options' + {{LayoutOptions/sizing}} property. + + 5. Let |inputProperties| be |definition|'s input properties. + + 6. Let |children| be a new [=list=]. + + 7. For each |childBox| in |childBoxes| perform the following substeps: + + 1. Let |layoutChild| be the result of [=get a layout child=] given |workletGlobalScope|, + |name|, |childBox|, and |context|'s [=unique id=]. + + 2. Append |layoutChild| to |children|. + + 8. Let |edges| be a new {{LayoutEdges}} populated with the [=computed value=] for all the [=box + model edges=] for |box|. + + 9. Let |layoutConstraints| be the result of [=create a layout constraints object=] given + |internalLayoutConstraints|, |box|, and |sizingMode|. + + 10. Let |styleMap| be the result of [=get a style map=] given |box|, and |inputProperties|. + + 11. Let |breakToken| be a new {{BreakToken}} populated with the appropriate information from + |internalBreakToken|. + + If |internalBreakToken| is null, let |breakToken| be null. + + 12. At this stage the user agent may re-use a [=fragment=] from a previous invocation if + |children|, |styleMap|, |layoutConstraints|, |breakToken| are equivalent to that previous + invocation. If so let the fragment output be that cached fragment and abort all these steps. + + 13. Let |layoutFunction| be |definition|'s [=layout function=]. + + 14. Let |value| be the result of [=Invoke=](|layoutFunction|, |layoutInstance|, «|children|, + |edges|, |layoutConstraints|, |styleMap|, |breakToken|»). + + If an exception is [=thrown=] the let |box| fallback to the [=flow layout=] and abort all + these steps. + + 15. If |value| is a promise: + + - Then: + + 1. Let |fragmentResultValue| be the result of [=run a work queue=] given |value|. + + If [=run a work queue=] returns failure, let the |box| fallback to the [=flow + layout=] and abort all these steps. + + - Otherwise: + + 1. Let |fragmentResultValue| be |value|. + + 16. If |fragmentResultValue| is a [=platform object=]: + + - Then: + + 1. Let |fragmentResult| be the result [=converting=] |fragmentResultValue| to a + {{FragmentResult}}. + + If an exception is [=thrown=], let |box| fallback to the [=flow layout=] and abort all + these steps. + + - Otherwise: + + 1. Let |fragmentResultOptions| be the result of [=converting=] |fragmentResultValue| to + a {{FragmentResultOptions}}. + + If an exception is [=thrown=], let |box| fallback to the [=flow layout=] and abort + all these steps. + + 2. Let |fragmentResult| be the result of [=construct a fragment result=] given + |fragmentResultOptions|. + + If an exception is [=thrown=], let |box| fallback to the [=flow layout=] and abort + all these steps. + + 17. Return an internal representation of a [=fragment=] with: + + - The [=inline size=] set to |fragmentResult|'s {{FragmentResult/[[inline size]]}}. + + - The [=block size=] set to |fragmentResult|'s {{FragmentResult/[[inline size]]}}. + + - The child fragments set to |fragmentResult|'s {{FragmentResult/[[child fragments]]}}. + + The ordering is important as this dictates their paint order (described in + [[#layout-api-containers]]). Their position relative to the border box of the + fragment should be based off the author specified {{LayoutFragment/inlineOffset}} and + {{LayoutFragment/blockOffset}}. + + - The [=fragmentation break=] information set to |fragmentResult|'s + {{FragmentResult/[[internal break token]]}}. + + - Store |fragmentResult|'s {{FragmentResult/[[data]]}} with the [=fragment=]. + +
+ +### Global Scope Selection ### {#global-scope-selection} + +When the user agent needs to select a {{LayoutWorkletGlobalScope}} from the layout [=worklet's +WorkletGlobalScopes=] [=list=] it must: + + - Select from at least two {{LayoutWorkletGlobalScope}}s, unless the user agent is + under memory constraints. + + - Not re-use the same {{LayoutWorkletGlobalScope}} 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]]. + +### Utility Algorithms ### {#utility-algorithms} + +The section specifies algorithms common to the [=determine the intrinsic sizes=] and [=generate +a fragment=] algorithms. + +Note: [=Get a document layout definition=] returns a [=document layout definition=] from the + owning [=document=]. + +
+When the user agent wants to get a document layout definition given |name|, it +must run the following steps: + + 1. Let |documentLayoutDefinitionMap| be the associated [=document's=] [=document layout + definitions=] map. + + 2. If |documentLayoutDefinitionMap|[|name|] does not exist, return failure and + abort all these steps. + + 3. Return the result of get |documentLayoutDefinitionMap|[|name|]. +
+ +Note: [=Get a layout definition=] returns a [=layout definition=] for a given + {{LayoutWorkletGlobalScope}}, it the desired definition doesn't exist it will "invalidate" the + [=document layout definition=], (so that the layout can't be used again), and return failure. + +
+When the user agent wants to get a layout definition given |name|, and +|workletGlobalScope|, it must run the following steps: + + 1. Let |layoutDefinitionMap| be |workletGlobalScope|'s [=layout definitions=] map. + + 2. If |layoutDefinitionMap|[|name|] does not exist, run the following steps: + + 1. [=Queue a task=] to run the following steps: + + 1. Let |documentLayoutDefinitionMap| be the associated [=document's=] [=document layout + definition=] map. + + 2. Set |documentLayoutDefinitionMap|[|name|] to "invalid". + + 3. The user agent should log an error to the debugging console stating that a + class wasn't registered in all {{LayoutWorkletGlobalScope}}s. + + 2. Return failure, and abort all these steps. + + 3. Return the result of [=get=] |layoutDefinitionMap|[|name|]. +
+ +Note: [=Get a layout class instance=] returns an instance of the web developer provided class. + (Registered in {{registerLayout()}}). If one isn't present yet, it will create a new one. This + algorithm may fail, as the constructor may throw an exception. + +
+When the user agent wants to get a layout class instance given |box|, |definition|, and +|workletGlobalScope|, it must run the following steps: + + 1. Let |layoutClassInstanceMap| be |box|'s [=layout class instances=] map. + + 2. Let |layoutInstance| be the result of [=get=] |layoutClassInstanceMap|[|workletGlobalScope|]. + If |layoutInstance| is null, run the following steps: + + 1. If the [=constructor valid flag=] on |definition| is false, then return failure and + abort all these steps. + + 2. Let |layoutCtor| be the [=class constructor=] on |definition|. + + 3. Let |layoutInstance| be the result of [=Construct=](|layoutCtor|). + + If [=construct=] throws an exception, set the |definition|'s [=constructor valid flag=] + to false, then return failure and abort all these steps. + + 4. Set |layoutClassInstanceMap|[|workletGlobalScope|] to |layoutInstance|. + + 4. Return |layoutInstance|. +
+ +
+When the user agent wants to get a style map given |box|, and |inputProperties|, it +must run the following steps: + + 1. If |box|'s [=styleMap=] is null, then: + + 1. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the + [=computed values=] for properties listed in |inputProperties| for |box|. + + 2. Set |box|'s [=styleMap=] internal slot to |styleMap|. + + 2. Return |box|'s {{StylePropertyMapReadOnly}} contained in the [=styleMap=] internal slot. + +
+ +[=Run a work queue=] is designed to allow user agents to work in both a single threaded, and +multi-threaded environment. + +Note: [=Run a work queue=] processes [=layout api work task=]s enqueued with + {{LayoutChild/intrinsicSizes()}} and {{LayoutChild/layoutNextFragment()}}. It will continue + processing tasks until the promise from the web developers layout or intrinsicSizes method is + resolved, or the queue is empty after running the microtask queue. + + The result of running the queue will either be the result of the layout or intrinsicSizes + method, or failure. + +
+When the user agent wants to run a work queue given |promise|, and |workQueue|, it +must run the following steps: + + 1. If |promise| is not a promise, return failure. + + 2. [=While=] |workQueue| is not [=list/empty=], and |promise| is pending: + + 1. [=list/For each=] |task| in |workQueue|: + + 1. Let |layoutChild| be |task|'s [=layout api work task/layout child=]. + + 2. Let |box| be |layoutChild|'s [=box=] in the {{LayoutChild/[[box]]}} internal slot. + + 3. Let |childPromise| be |task|'s [=layout api work task/promise=]. + + 2. If |task|'s [=layout api work task/task type=] is "layout", + + - Then [=queue a task=], or run synchronously, the following substeps: + + 1. Let |childConstraints| be |task|'s [=layout api work task/layout constraints=]. + + 2. Let |childBreakToken| be |task|'s [=layout api work task/child break token=]. + + 3. Let |targetRealm| be |layoutChild|'s [=Realm=]. + + 4. Let |internalFragment| be the result of the user agent producing a + [=fragment=] based on |box|, |childConstraints|, and |childBreakToken|. + + Invoking [=translate a LayoutConstraintsOptions to internal constraints=] + given |childConstraints|, must be run to translate the given + {{LayoutConstraintsOptions}} into the internal constraints for the user + agent's layout engine. + + 5. Let |fragment| be a new {{LayoutFragment}} with: + + - {{LayoutFragment/inlineSize}} being |internalFragment|'s [=inline size=] + relative to the [=current layout's=] writing mode. + + - {{LayoutFragment/blockSize}} being |internalFragment|'s [=block size=] + relative to the [=current layout's=] writing mode. + + - {{LayoutFragment/inlineOffset}} initially set to 0. + + - {{LayoutFragment/blockOffset}} initially set to 0. + + - {{LayoutFragment/breakToken}} being a new {{ChildBreakToken}} representing + |internalFragment|'s internal break token, if any. + + - If |internalFragment| has a |clonedData| object stored with it, let + {{LayoutFragment/data}} being the result of + [=StructuredDeserialize=](|clonedData|, |targetRealm|), otherwise null. + + 6. Resolve |childPromise| with |fragment|. + + - Otherwise [=queue a task=], or run synchronously, the following substeps: + + 1. Let |internalIntrinsicSizes| be the result of the user agent calculating the + border box min/max content contribution of |box|. + + 2. Let |intrinsicSizes| be a new {{IntrinsicSizes}} with: + + - {{IntrinsicSizes/minContentSize}} being |internalIntrinsicSizes|' + border box min-content contribution, relative to the [=current + layout's=] writing mode. + + - {{IntrinsicSizes/maxContentSize}} being |internalIntrinsicSizes|'s + border box max-content contribution, relative to the [=current + layout's=] writing mode. + + 3. Resolve |childPromise| with |intrinsicSizes|. + + 2. Wait (optionally [=in parallel=]) for all of the above tasks to complete. + + Note: If the tasks were perform synchronously, then this step is a no-op. + + 3. [=list/Empty=] |workQueue|. + + 4. [=Perform a microtask checkpoint=]. + + 3. If |promise| isn't fulfilled (it is pending, or got rejected), return failure. + + 4. Return the fulfilled value of |promise|. + +
+ +Examples {#examples} +==================== + +
+The layout algorithm below performs a block-like layout (positioning fragments sequentially in the +block direction), while centering its children in the inline direction. + +
+registerLayout('block-like', class {
+    async intrinsicSizes(children, edges, styleMap) {
+      const childrenSizes = await Promise.all(children.map((child) => {
+          return child.intrinsicSizes();
+      }));
+
+      const maxContentSize = childrenSizes.reduce((max, childSizes) => {
+          return Math.max(max, childSizes.maxContentSize);
+      }, 0) + edges.inline;
+
+      const minContentSize = childrenSizes.reduce((max, childSizes) => {
+          return Math.max(max, childSizes.minContentSize);
+      }, 0) + edges.inline;
+
+      return {maxContentSize, minContentSize};
+    }
+
+    async layout(children, edges, constraints, styleMap) {
+        // Determine our (inner) available size.
+        const availableInlineSize = constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.block : null;
+
+        const childFragments = [];
+        const childConstraints = { availableInlineSize, availableBlockSize };
+
+        const childFragments = await Promise.all(children.map((child) => {
+            return child.layoutNextFragment(childConstraints);
+        }));
+
+        let blockOffset = edges.blockStart;
+        for (let fragment of childFragments) {
+            // Position the fragment in a block like manner, centering it in the
+            // inline direction.
+            fragment.blockOffset = blockOffset;
+            fragment.inlineOffset = Math.max(
+                edges.inlineStart,
+                (availableInlineSize - fragment.inlineSize) / 2);
+
+            blockOffset += fragment.blockSize;
+        }
+
+        const autoBlockSize = blockOffset + edges.blockEnd;
+
+        return {
+            autoBlockSize,
+            childFragments,
+        };
+    }
+});
+
+
+ +
+The layout algorithm performs a flexbox-like distribution of spare space in the inline direction. It +creates child layout constraints which specify that a child should be a fixed inline size. + +
+registerLayout('flex-distribution-like', class {
+    async intrinsicSizes(children, edges, styleMap) {
+      const childrenSizes = await Promise.all(children.map((child) => {
+          return child.intrinsicSizes();
+      }));
+
+      const maxContentSize = childrenSizes.reduce((sum, childSizes) => {
+          return sum + childSizes.maxContentSize;
+      }, 0) + edges.inline;
+
+      const minContentSize = childrenSizes.reduce((max, childSizes) => {
+          return sum + childSizes.minContentSize;
+      }, 0) + edges.inline;
+
+      return {maxContentSize, minContentSize};
+    }
+
+    async layout(children, edges, constraints, styleMap) {
+        // Determine our (inner) available size.
+        const availableInlineSize =
+            constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.block : null;
+
+        const childConstraints = { availableInlineSize, availableBlockSize };
+
+        const unconstrainedChildFragments = await Promise.all(children.map((child) => {
+            return child.layoutNextFragment(childConstraints);
+        }));
+
+        const unconstrainedSizes = [];
+        const totalSize = unconstrainedChildFragments.reduce((sum, fragment, i) => {
+            unconstrainedSizes[i] = fragment.inlineSize;
+            return sum + fragment.inlineSize;
+        }, 0);
+
+        // Distribute spare space between children.
+        const remainingSpace = Math.max(0, inlineSize - totalSize);
+        const extraSpace = remainingSpace / children.length;
+
+        const childFragments = await Promise.all(children.map((child, i) => {
+            return child.layoutNextFragment({
+                fixedInlineSize: unconstrainedSizes[i] + extraSpace,
+                availableBlockSize
+            });
+        }));
+
+        // Position the fragments.
+        let inlineOffset = 0;
+        let maxChildBlockSize = 0;
+        for (let fragment of childFragments) {
+            fragment.inlineOffset = inlineOffset;
+            fragment.blockOffset = edges.blockStart;
+
+            inlineOffset += fragment.inlineSize;
+            maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize);
+        }
+
+        return {
+            autoBlockSize: maxChildBlockSize + edges.block,
+            childFragments,
+        };
+    }
+});
+
+
+ +
+This example shows a simple layout which indents child fragments for a certain number of +lines. + +This example also demonstrates using the previous {{LayoutFragment/breakToken}} of a +{{LayoutFragment}} to produce the next fragment for the {{LayoutChild}}. + +It also demonstrates using the {{BreakToken}} to respect the {{LayoutConstraints}}' +{{LayoutConstraints/blockFragmentationType}}, it resumes it layout from the previous {{BreakToken}}. +It returns a {{FragmentResultOptions}} with a {{FragmentResultOptions/breakToken}} which is used to +resume the layout. + +
+registerLayout('indent-lines', class {
+    static layoutOptions = {childDisplay: 'normal'};
+    static inputProperties = ['--indent', '--indent-lines'];
+
+    async layout(children, edges, constraints, styleMap, breakToken) {
+        // Determine our (inner) available size.
+        const availableInlineSize =
+            constraints.fixedInlineSize - edges.inline;
+        const availableBlockSize = constraints.fixedBlockSize ?
+            constraints.fixedBlockSize - edges.block : null;
+
+        // Detrermine the number of lines to indent, and the indent amount.
+        const indent = resolveLength(constraints, styleMap.get('--indent'));
+        let lines = styleMap.get('--indent-lines').value;
+
+        const childFragments = [];
+
+        let childBreakToken = null;
+        if (breakToken) {
+            childBreakToken = breakToken.childBreakTokens[0];
+
+            // Remove all the children we have already produced fragments for.
+            children.splice(0, children.indexOf(childBreakToken.child));
+        }
+
+        let blockOffset = edges.blockStart;
+        let child = children.shift();
+        while (child) {
+            const shouldIndent = lines-- > 0;
+
+            // Adjust the inline size for the indent.
+            const childAvailableInlineSize = shouldIndent ?
+                availableInlineSize - indent : availableInlineSize;
+
+            const childConstraints = {
+                availableInlineSize: childAvailableInlineSize,
+                availableBlockSize,
+                percentageInlineSize: availableInlineSize,
+                blockFragmentationType: constraints.blockFragmentationType,
+            };
+
+            const fragment = await child.layoutNextFragment(childConstraints,
+                                                            childBreakToken);
+            childFragments.push(fragment);
+
+            // Position the fragment.
+            fragment.inlineOffset = shouldIndent ?
+                edges.inlineStart + indent : edges.inlineStart;
+            fragment.blockOffset = blockOffset;
+            blockOffset += fragment.blockSize;
+
+            // Check if we have gone over the block fragmentation limit.
+            if (constraints.blockFragmentationType != 'none' &&
+                blockOffset > constraints.blockSize) {
+                break;
+            }
+
+            if (fragment.breakToken) {
+                childBreakToken = fragment.breakToken;
+            } else {
+                // If a fragment doesn't have a break token, we move onto the
+                // next child.
+                child = children.shift();
+                childBreakToken = null;
+            }
+        }
+
+        const autoBlockSize = blockOffset + edges.blockEnd;
+
+        // Return our fragment.
+        const result = {
+            autoBlockSize,
+            childFragments: childFragments,
+        }
+
+        if (childBreakToken) {
+            result.breakToken = {
+                childBreakTokens: [childBreakToken],
+            };
+        }
+
+        return result;
+    }
+});
+
+
+ +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. diff --git a/css-layout-api/README.md b/css-layout-api/README.md new file mode 100644 index 00000000..93e86dde --- /dev/null +++ b/css-layout-api/README.md @@ -0,0 +1 @@ +See [EXPLAINER](EXPLAINER.md). diff --git a/css-layout-api/README_old.md b/css-layout-api/README_old.md new file mode 100644 index 00000000..c4226ad1 --- /dev/null +++ b/css-layout-api/README_old.md @@ -0,0 +1,530 @@ +# CSS Layout API + +The CSS Layout API is designed to give authors the ability to write their own layout algorithms in +additon to the native ones user agents ship with today. + +For example the user agents currently ship with + - Block Flow Layout + - Flexbox Layout + +With the CSS Layout API, authors could write their own layouts which implement + - Constraint based layouts + - Masonary layouts + - Line spacing + snapping + +This document aims to give a high level overview to the Layout API. + +### Concepts + +##### The `Box` + +A `Box` refers to a CSS box, that is a node that has some sort of style. This can refer to: + + - An element with an associated style, (an element that has `display: none` for these purposes does + not have a style). + + - The `::before` and `::after` pseudo elements with an associated style, (note for layout purposes + the `::first-letter`, `::first-line`, `::selection` are *not* independent boxes, they are more a + special kind of selector that can override style on *part* of another box). + + - A `TextNode` with some style. + +This is effectively the DOM tree but with some extra things. One important thing to note is that a +`Box` doesn't have any layout information, it is the _input_ to layout. + +For the layout API specifically a box is represented like: + +```webidl +interface Box { + readonly attribute StylePropertyMapReadonly styleMap; + FragmentRequest doLayout(ConstraintSpace space, OpaqueBreakToken breakToken); +}; +``` + +The `styleMap` contains the required computed style for that `Box`. + +##### The `Fragment` + +A `Fragment` refers to a CSS fragment, that is it is the part of the layout result of a box. This +could be for example: + + - A whole box which has undergone layout. E.g. the result of laying out an `` tag. + + - A portion of a box which has undergone layout. E.g. the result of laying out the first column of + a multicol layout. `
` + + - A portion of a `TextNode` which has undergone layout, for example the first line, or the first + portion of a line with the same style. + + - The `::first-letter` fragment of a `TextNode`. + +One can think of this as the _leaf_ representation you can get out of: +```js +let range = document.createRange(); +range.selectNode(element); +console.log(range.getClientRects()); +``` + +For the layout API specifically a fragment is represented like: + +```webidl +interface Fragment { + readonly attribute double inlineSize; + readonly attribute double blockSize; + + attribute double inlineStart; // inlineOffset instead? + attribute double blockStart; + + readonly attribute sequence unpositionedBoxes; + + readonly attribute OpaqueBreakToken? breakToken; + + readonly attribute BaselineOffset dominantBaseline; + readonly attribute BaselineOffset? ideographicBaseline; + // other baselines go here. +}; +``` + +One important thing to note is that you can't change the `inlineSize` or `blockSize` of a fragment +once have received it from a child layout. The _only_ thing you can change is its position (with +`inlineStart` or `blockStart`) relative to the parent. + +See below for a description of baselines. + +The `unpositionedBoxes` attribute is a list of `Box`es which couldn't be positioned by the child. +The current layout can choose to layout and position these, or it can pass them up to its parent. + +#### The `ConstraintSpace` + +A `ConstraintSpace` is a 2D representation of the layout space given to a layout. A constraint space +has: + - A `inlineSize` and `blockSize`. If present, these describe a fixed width in which the layout can + produce a `Fragment`. The layout should produce a `Fragment` which fits inside these bounds. If + it exceeds these bounds, the `Fragment` may be paint clipped, etc, as determined by its parent. + + - A `inlineScrollOffset` and `blockScrollOffset`. If present, these describe that if the resulting + `Fragment` exceeds these offsets, it must call `willInlineScroll()` / `willBlockScroll()`. This + will result in the constraint space being updated (and also reset to its initial state?). These + methods will potentially change the `inlineSize` or `blockSize` to allow room for a scrollbar. + + - A `inlineFragmentOffset` and `blockFragmentingOffset`. If present, these describe that if the + resulting `Fragment` must fragment at this particular point. + + - A list of exclusions. Described more in-depth below. + +The `ConstraintSpace` is represented as: + +```webidl +partial interface ConstraintSpace { + readonly attribute double? inlineSize; + readonly attribute double? blockSize; + + readonly attribute double? inlineScrollOffset; + readonly attribute double? blockScrollOffset; + + readonly attribute double? inlineFragmentOffset; // Is inline fragment offset needed? + readonly attribute double? blockFragmentOffset; + + void willInlineScroll(); + void willBlockScroll(); +}; +``` + +This may be better represented as: + +```webidl +partial interface ConstraintSpace { + readonly attribute ExtentConstraint inlineConstraint; + readonly attribute ExtentConstraint inlineConstraint; + + void willInlineScroll(); + void willBlockScroll(); +}; + +enum ExtentConstraintType = 'fixed' | 'scroll' | 'fragment'; + +interface ExtentConstraint { + readonly attribute ExtentConstraintType type; + readonly attribute double offset; +}; +``` + +| Actually this doesn't really work? As you can have an inlineSize, which also can overflow. | +| --- | + +Exclusions can be added to the constraint space which children should avoid. E.g. + +```webidl +partial interface ConstraintSpace { + void addExclusion(Fragment fragment, optional FlowEnum flow); + void addExclusion(Exclusion fragment, optional FlowEnum flow); + readonly attribute sequence exclusions; +}; + +dictionary Exclusion { + double inlineSize; + double blockSize; + + double inlineStart; + double blockStart; + + double inlineEnd; + double blockEnd; +}; +``` + +The author can iterate through the available space via the `layoutOpportunities()` api. + +```webidl +partial interface ConstraintSpace { + Generator layoutOpportunities(); +}; + +interface LayoutOpportunity { + readonly attribute double inlineSize; + readonly attribute double blockSize; + + readonly attribute double inlineStart; + readonly attribute double blockStart; + + readonly attribute double inlineEnd; + readonly attribute double blockEnd; +} +``` + +Here is a cute little gif which shows the layout opportunities for a `ConstraintSpace` with two +exclusions. + +![layout opportunities](https://raw.githubusercontent.com/w3c/css-houdini-drafts/master/images/layout_opp.gif) + +The layoutOpportunities generator will return a series of max-rects for a given constraint space. +These are ordered by `inlineStart`, `inlineSize` then `blockStart`. + +| How do we represent non-rect exclusions? Initial thought is to always jump by `1em` of author | +| specified amount. | +| --- | + +###### Advanced exclusions + +Not everything in CSS avoids all exclusions. For example: + +![inline text avoiding floats](https://raw.githubusercontent.com/w3c/css-houdini-drafts/master/images/exclusions_1.png) + +The green block-level element doesn't avoid the intruding floats, but its inline-level children do. + +Should authors be able to annotate exclusions with a tag, then just `LayoutOpportunities` based on +those tags? For example: + +```webidl +partial interface ConstraintSpace { + void addExclusion(Fragment exclusion, optional FlowEnum flow, optional sequence tags); + + // calling layoutOpportunities(['left']), only provides layout opportunities which avoids + // exclusions tagged with left. + Generator layoutOpportunities(optional sequence tags); +}; +``` + +#### Breaking and `BreakToken`s + +TODO write about how break tokens work. + +#### Pseudo-elements and style overrides + +`::first-letter` and `::first-line` are a little bit special in terms of CSS; they aren't really +elements just a different style applied to a fragment(s). + +In order to handle these do we allow override styles when performing layout on a child? For example: +```webidl +partial interface Box { + FragmentRequest doLayout(ConstraintSpace space, OpaqueBreakToken breakToken, Object overrideStyles); +} +``` + +```js +registerLayout('handle-first-line', class { + *layout(constraintSpace, children, styleMap, opt_breakToken) { + // ... + + let child = children[i]; + let fragment = yield child.doLayout(childConstraintSpace, breakToken, { + // This would be queried from styleMap? + // This would only allow computed-style values? + fontSize: new CSSLengthValue({px: 18}), + }); + + // ... + } +}); +``` + +TODO: These is a problem with the above example? + +Similarly we have a class of CSS layout algorithms which _force_ a particular style on their +children, (flex & grid). Do we handle these in a similar way? For example: + +```js +registerLayout('kinda-like-flex', class { + *layout(constraintSpace, children, styleMap, opt_breakToken) { + // ... + + let child = children[i]; + let fragment = yield child.doLayout(childConstraintSpace, breakToken, { + inlineSize: 180, // Only accepts numbers in px. + }); + + // ... + + } +}); +``` + +We need something like this, needs to be here, or on the constraintSpace somehow. + +#### Utility functions + +We need a set of utility function which do things like resolve a computed-inline-size against +another length etc. These functions will probably become clear over-time from internal +implementations and people writing algorithms against this API but for starters we'll probably need: + +```webidl +[NoInterfaceObject] +interface LayoutUtilities { + // Resolves the inline-size according to an algorithm to be defined in the spec. This doesn't + // limit authors to having their own layout units and resolving the lengths differently. This is + // just a helper. + number resolveInlineSize(ConstraintSpace constraintSpace, StylePropertyMapReadonly styleMap); + + // Resolves the size against a different length. + number resolveSize(CSSValue property, number size); + + // Resolves the size against a different length for the minimum amount. + number resolveMinimumSize(CSSValue property, number size); +} +``` + +This is just some basic ones, we'll need more. + +#### Flags! + +We need to indicate to the engine when we want a particular layout behaviour placed on us. For +example if we are a: + - formatting context + - should "blockify" children (like flex, grid) + - magically handle abs-pos + +TODO there are probably others here. + +For example if we should establish a formatting context, implicitly this means that the +constraintSpace we are given cannot have any pre-defined exclusions. + +We need to decide on the defaults here, and if we allow changing the default. + +A simple API proposal: +```js +registerLayout('weee!', class { + static formattingContext = false; // default is true? + static handleAbsPos = false; // default is true? + static blockifyChildren = true; // default is false? + + *layout() { + // etc. + } +}); +``` + +#### Adding and removing children + +We need a callback for when child boxes are added / removed. Rendering engines today have +optimizations in place for when this occurs; for example in grid, the user agent will place its +children into a "Tracks" data structure for layout. + +TODO: add API proposal here. + +#### Baselines + +TODO: add explaination why we need a more powerful API than just offset here. + +### Performing Layout + +The Layout API is best described with a simple dummy example: + +```js +registerLayout('really-basic-block', class { + *layout(constraintSpace, children, styleMap, opt_breakToken) { + let inlineSize = 0; + let blockSize = 0; + const childFragments = []; + + for (let child of children) { + let fragment = yield child.doLayout(constraintSpace); + + // Position the new fragment. + fragment.inlineStart = 0; + fragment.blockStart = blockSize; + blockSize += fragment.blockSize; + + // Add it as an exclusion to the constraintSpace + constraintSpace.addExclusion(fragment, 'block-end'); + + // Update the running totals for our size. + inlineSize = Math.max(inlineSize, fragment.inlineSize); + childFragments.push(fragment); + } + + return { + inlineSize: inlineSize, + blockSize: blockSize, + children: childFragments, + }; + } +}); +``` + +The first thing to notice about the API is that the layout method on the class returns a generator. +This is to allow two things: + 1. User agents implementing parallel layout. + 2. User agents implementing asynchronous layout. + +The generator returns a `FragmentRequest`. Inside of the authors layout funciton, this object is +completely opaque. This is a token for the user-agent to perform layout _at some stage_ for the +particular box it was generated for. + +When a `FragmentRequest` is returned from the generator, the user-agent needs to produce a +`Fragment` for it, and return it via. the generator `next()` call. + +As a concrete example, the user agent could implement the logic driving the author defined layout +as: + +```js +function performLayout(constraintSpace, box) { + // Get the author defined layout instance. + const layoutInstance = getLayoutInstanceForBox(box); + + // Access the generator returned by *layout(); + const layoutGenerator = layoutInstance.layout(constraintSpace, box.children, box.styleMap); + + // Loop through all of the fragment requests. + let fragmentRequestObj = layoutGenerator.next(); + while (!fragmentRequestObj.done) { + const fragmentRequest = []; + const fragmentResult = []; + + // Coorce fragmentRequestObj into an array. + if (fragmentRequestObj.value.length) { + fragmentRequest.push(...fragmentRequestObject.value); + } else { + fragmentRequest.push(fragmentRequestObject.value); + } + + for (let i = 0; i < fragmentRequest.length; i++) { + fragmentResult.push(performLayout(fragmentRequest[i])); + } + + // Request the next fragment. + fragmentRequestObj = layoutGenerator.next( + fragmentResult.length == 1 : fragmentResult[0] : fragmentResult); + } + + // The last value from the generator should be the final return value. + const fragmentDict = fragmentRequest.value; + return new Fragment(fragmentDict); +} +``` + +### Example layout algorithms + +```js +// 'multicol' does a simple multi-column layout. +registerLayout('multicol', class { + *layout(constraintSpace, children, styleMap, opt_breakToken) { + const inlineSize = resolveInlineSize(constraintSpace, styleMap); + + // Try and decide the number of size of columns. + const columnCountValue = styleMap.get('column-count'); + const columnInlineSizeValue = styleMap.get('column-width'); + + let columnCount = 1; + let columnInlineSize = inlineSize; + + if (columnCountValue) { + columnCount = columnCountValue.value; + } + + if (columnInlineSizeValue) { + columnInlineSize = resolveSize(columnInlineSizeValue, inlineSize); + } + + if (constraintSpace.inlineScrollOffset && + columnInlineSize * columnCount > constraintSpace.inlineScrollOffset) { + // NOTE: under this condition, we need to start again to re-resolve lengths? + constraintSpace.willInlineScroll(); + return; // Or just continue here? + } + + // Create a constraint space which is just the inlineSize of the column. + const colConstraintSpace = new ConstraintSpace({ + inlineSize: columnInlineSize + }); + + // Perform layout on all the children, taking into account the children + // which may fragment in the inline direction. + const childFragments = []; + let childBlockSize = 0; + let layoutOpp; + for (let child of children) { + let breakToken; + do { + const fragment = yield child.doLayout(colConstraintSpace, breakToken); + breakToken = fragment.breakToken; + + const gen = colConstraintSpace.layoutOpportunities(); + + layoutOpp = gen.next().value; + if (layoutOpp.inlineSize < fragment.inlineSize()) { + layoutOpp = gen.next().value; + } + + fragment.inlineStart = opp.inlineStart; + fragment.blockStart = opp.blockStart; + colConstraintSpace.addExclusion(fragment, 'inline-flow'); + + childFragments.push(fragment); + } while (breakToken); + } + + // FIXME: This might be wrong, need a helper method on constraintSpace which returns the max + // blockEnd of all the exclusions. + const childBlockSize = + colConstraintSpace.layoutOpportunities().next().value.blockStart; + + // Next, a clever person would nicely balance the columns, we are going + // to do something really simple. :) + const columnBlockSize = Math.ceil(childBlockSize / columnCount); + const columnGap = resolveSize(styleMap.get('column-gap'), inlineSize); + let size = 0; + let columnNum = 0; + let columnEndOffset = 0; + for (let fragment of childFragments) { + if (size && fragment.blockSize + size > columnBlockSize) { + size = 0; + columnNum++; + columnEndOffset += size; + } + + fragment.inlineStart += columnNum * (columnGap + columnInlineSize); + fragment.blockStart -= columnEndOffset; + size = Math.max(size, fragment.blockStart + fragment.blockSize); + } + + const blockSize = + resolveBlockSize(constraintSpace, styleMap, columnBlockSize); + + return { + inlineSize: inlineSize, + blockSize: blockSize, + fragments: childFragments, + }; + } +}); +``` diff --git a/css-layout-api/images/constraint_space_1.html b/css-layout-api/images/constraint_space_1.html new file mode 100644 index 00000000..55f6ee7a --- /dev/null +++ b/css-layout-api/images/constraint_space_1.html @@ -0,0 +1,38 @@ + + + + + + +
+ 120px + 120px + + diff --git a/css-layout-api/images/constraint_space_1.png b/css-layout-api/images/constraint_space_1.png new file mode 100644 index 00000000..86b26d8c Binary files /dev/null and b/css-layout-api/images/constraint_space_1.png differ diff --git a/css-layout-api/images/constraint_space_2.html b/css-layout-api/images/constraint_space_2.html new file mode 100644 index 00000000..a8b68074 --- /dev/null +++ b/css-layout-api/images/constraint_space_2.html @@ -0,0 +1,40 @@ + + + + + + +
+ 120px + 100px + scrollTrigger + + diff --git a/css-layout-api/images/constraint_space_2.png b/css-layout-api/images/constraint_space_2.png new file mode 100644 index 00000000..a1a246c1 Binary files /dev/null and b/css-layout-api/images/constraint_space_2.png differ diff --git a/css-layout-api/images/constraint_space_3.html b/css-layout-api/images/constraint_space_3.html new file mode 100644 index 00000000..ec520c05 --- /dev/null +++ b/css-layout-api/images/constraint_space_3.html @@ -0,0 +1,43 @@ + + + + + + +
+ 120px + 100px + scrollTrigger + scrollTrigger + + diff --git a/css-layout-api/images/constraint_space_3.png b/css-layout-api/images/constraint_space_3.png new file mode 100644 index 00000000..bbe29ee6 Binary files /dev/null and b/css-layout-api/images/constraint_space_3.png differ diff --git a/css-layout-api/images/constraint_space_4.html b/css-layout-api/images/constraint_space_4.html new file mode 100644 index 00000000..d003d4ef --- /dev/null +++ b/css-layout-api/images/constraint_space_4.html @@ -0,0 +1,41 @@ + + + + + + +
+ 140px + 100px + fragmentation + + diff --git a/css-layout-api/images/constraint_space_4.png b/css-layout-api/images/constraint_space_4.png new file mode 100644 index 00000000..374e201d Binary files /dev/null and b/css-layout-api/images/constraint_space_4.png differ diff --git a/css-layout-api/img/edges.png b/css-layout-api/img/edges.png new file mode 100644 index 00000000..6d01d1fa Binary files /dev/null and b/css-layout-api/img/edges.png differ diff --git a/css-layout-api/img/layout-fragment-offsets.png b/css-layout-api/img/layout-fragment-offsets.png new file mode 100644 index 00000000..c42a15df Binary files /dev/null and b/css-layout-api/img/layout-fragment-offsets.png differ diff --git a/css-paint-api/EXPLAINER.md b/css-paint-api/EXPLAINER.md new file mode 100644 index 00000000..96a5402c --- /dev/null +++ b/css-paint-api/EXPLAINER.md @@ -0,0 +1,274 @@ +CSS Paint API Explained +======================= + +The CSS Paint API is being developed to improve the extensibility of CSS. + +Specifically this allows developers to write a paint function which allows us to draw directly into +an elements background, border, or content. + +This work was motivated for a couple of reasons: + +### Reduction of DOM ### + +We noticed that developers are using an increasing amount of DOM to create visual effects. As an +example the [<paper-button>](https://www.webcomponents.org/element/PolymerElements/paper-button/paper-button) +uses multiple divs to achieve the "ripple" effect. + +Instead of using addition divs the developer could just draw directly into the background-image of +the element instead. + +This means that the memory and cpu usage of the page would go down, the rendering engine doesn't +have to keep in memory the additional DOM nodes, in addition to performing style-recalc, layout, +painting for all these additional divs. + +### Efficiency Gains ### + +Providing a "hook" into the rendering engine allows for efficiency gains which are difficult to +achieve with current APIs. + +#### Invalidation #### + +As the CSS paint API Invalidation is based off style changes, this check can occur in the same pass +as the rest of the box tree. For example: + +```css +my-button { + --property-which-invalidates-paint: no-hover; +} + +my-button:hover { + --property-which-invalidates-paint: hover; +} +``` + +To achieve the same effect with current APIs you have to rebuild the engines invalidation logic +which is potentially less efficient. + +#### Painting #### + +Once a box has been invalidated, a rendering engine isn't required to paint it that frame. For +example some rendering engines just paint what is visible within the "visual viewport". This means +that only a smaller amount of work is needed to be done. + +A naive implementation with existing APIs may try and paint everything within the document. + +### Extensibility of CSS ### + +We believe that allowing developers to extend CSS is good for the ecosystem. As an example if a +developer wanted an additional feature they could implement it themselves. E.g. if the developer +wanted a new type of dashed border, they shouldn't have to wait for browsers to implement this. + +They should have the power to implement this themselves with the same capability as the rendering +engine. + +Getting Started +--------------- + +First you'll need to add a module script into the paint worklet. + +```js +if ('paintWorklet' in CSS) { + await CSS.paintWorklet.addModule('my-paint-script.js'); + console.log('paint script installed!'); +} +``` + +See the worklets explainer for a more involved explaination of worklets. In short worklets: + - Are similar to workers in that the script runs in a separate global script context. + - A script inside a worklet has no DOM, Network, Database, etc access. + - The global script context lifetime is not defined (you should expect the script context to be killed at any point). + - May have multiple copies of the script context spawned on multiple CPU cores. + +As a couple of concrete example as to how the user agent may use these capabilities: + - When a "tab" is backgrounded the script context(s) of the paint worklet may be killed to free up memory. + - A multi-threaded user-engine may spawn multiple backing script contexts to perform the paint phase of the rendering engine in parallel. + +Painting a circle +----------------- + +The global script context of the paint worklet has exactly one method exposed to developers: `registerPaint`. + +```js +registerPaint('circle', class { + static inputProperties = ['--circle-color']; + + paint(ctx, size, style) { + // Change the fill color. + const color = style.get('--circle-color'); + ctx.fillStyle = color; + + // Determine the center point and radius. + const x = size.width / 2; + const y = size.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(); + } +}); +``` + +There are a few things going on in this example so lets step through them one-by-one. + +The `paint` function is your callback into the browsers paint phase in the rendering engine. You are given: + - `ctx`, a rendering context, similar to a `CanvasRenderingContext2D`. + - `size`, the size of the area in which you should paint. + - `style`, the computed style of the element which are you currently painting. + +The `style` is a Typed-OM style map. It will _only_ contain the CSS properties that you listed under +the static `inputProperties` accessor. + +This is to allow the engine to cache results of your paint class. For example if +`--some-other-property` changes the user-agent knows that this doesn't affect your paint class, and +can re-use the stored result. + +The only time when your paint class _must_ be called is when the element it is painting into is +within the viewport, and the size or CSS input properties have changed. + +Why classes? +------------ + +Classes allow composition of paint handlers. As an example: + +```js +class RectPainter { + static inputProperties = ['--rect-color']; + + paint(ctx, size, style) { + // Change the fill color. + ctx.fillStyle = style.get('--circle-color'); + + // Draw the solid rect. + ctx.fillRect(0, 0, size.width, size.height); + } +} + +class BorderRectPainter extends RectPainter { + static inputProperties = ['--border-color', ...super.inputProperties]; + + paint(ctx, size, style) { + super.paint(ctx, size, style); + + ctx.strokeStyle = style.get('--border-color'); + ctx.lineWidth = 4; + + ctx.strokeRect(0, 0, size.width, size.height); + } +} + +registerPaint('border-rect', BorderRectPainter); +``` + +Classes also give the developer a specific point in time to perform pre-initialization work. As an +example: + +```js +registerPaint('lots-of-paths', class { + + constructor() { + this.paths = performPathPreInit(); + } + + performPathPreInit() { + // Lots of work here to produce list of Path2D object to be reused. + } + + paint(ctx, size, style) { + ctx.stroke(this.paths[i]); + } +}); +``` + +In this example `performPathPreInit` is doing CPU intensive work which should only be done once. +Without classes this would typically be done when the script is first run, instead this is performed +when the class instance is first created (which may be much later in time). + +Drawing Images +-------------- + +The API works in conjunction with the [CSS Properties and Values](https://drafts.css-houdini.org/css-properties-values-api/) +proposal and the [CSS Typed OM](https://drafts.css-houdini.org/css-typed-om/) proposal. + +Using the Properties and Values `registerProperty` method, developers can create a custom CSS +property with the `` type. E.g. + +```js +registerProperty({ + name: '--profile-image', + syntax: '' +}); +``` + +This tells the rendering engine to treat the CSS property `--profile-image` as an image; and as a +result the style engine will parse and begin loading that image. + +You can then directly draw this image from within your paint method: + +```js +registerPaint('avatar', class { + static inputProperties = ['--profile-image']; + + paint(ctx, size, styleMap) { + // Determine the center point and radius. + const x = size.width / 2; + const y = size.height / 2; + const radius = Math.min(x, y); + + ctx.save(); + // Set up a round clipping path for the profile image. + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.clip(); + + // Draw the image inside the round clip. + ctx.drawImage(styleMap.get('--profile-image')); + ctx.restore(); + + // Draw a badge or something on top of the image. + drawBadge(ctx); + } +}); +``` + +The above example would be used in CSS by: +```css +.avatar-img { + background: paint(avatar); + --profile-image: url("profile-image.png"); +} +``` + +Paint Arguments +--------------- + +It is also possible with this API to have additional arguments to the `paint()` function, for +example: + +```js +registerPaint('circle-args', class { + static inputArguments = ['']; + + paint(ctx, size, _, args) { + const color = args[0].cssText; + ctx.fillStyle = color; + + const x = size.width / 2; + const y = size.height / 2; + const radius = Math.min(x, y); + + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.fill(); + } +}); +``` + +```js +my-element { + background: + paint(circle-args, red) center/50% no-repeat, + paint(cirlce-args, blue); +} +``` diff --git a/css-paint-api/Overview.bs b/css-paint-api/Overview.bs index a6c5de4c..f7570b54 100644 --- a/css-paint-api/Overview.bs +++ b/css-paint-api/Overview.bs @@ -1,320 +1,1022 @@ + +
-urlPrefix: https://heycam.github.io/webidl/; type: dfn;
-    text: NotSupportedError
-    urlPrefix: #dfn-;
-        text: exception
-        text: throw
-        url: throw; text: thrown
-urlPrefix: https://html.spec.whatwg.org/multipage/scripting.html; type: dfn;
-    text: reset the rendering context to its default state
-    text: scratch bitmap
-    text: set bitmap dimensions
-urlPrefix: http://www.ecma-international.org/ecma-262/6.0/#sec-; type: dfn;
+urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
+    text: constructor
     text: Construct
     text: IsArray
     text: IsCallable
     text: IsConstructor
-    text: HasProperty
+    url: ecmascript-data-types-and-values; text: type
     url: get-o-p; text: Get
+    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 an element based on that element's geometry (as generated by the layout stage) and computed style. - -This specification describes an API which allows developers to paint a part of an element in response to geometry / computed style changes. +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. -Note: In a future version of the spec, support may be added for defining the clip, global alpha, filter on a portion of an element (for example on the background layers). +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. -Paint Invalidation {#paint-invalidation} -======================================== +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). -A document has an associated paint name to input properties map. Initially it is empty and is populated when {{registerPaint(name, paintCtor)}} is called. +Paint Worklet {#paint-worklet} +============================== -Each <> function for a fragment has an associated paint valid flag. It may be either paint-valid or paint-invalid. It is initially set to paint-invalid. +The {{paintWorklet}} attribute allows access to the {{Worklet}} responsible for all the classes +which are related to painting. -When the geometry (as determined by layout) of a fragment changes, each <> function's paint valid flag should be set to paint-invalid. +The {{paintWorklet}}'s [=worklet global scope type=] is {{PaintWorkletGlobalScope}}. -When the computed style for an element changes, the user agent must run the following steps: - 1. For each <> function on the element, perform the following substeps: - 1. Let name be the first argument of the <> function. - 2. Let inputProperties be the result of performing a lookup in paint name to input properties map with |name| as the key. - 3. For each property in |inputProperties|, if the |property|'s computed value has changed, set the paint valid flag on the |fragment| to paint-invalid. +The {{paintWorklet}}'s worklet destination type is "paintworklet". -[[#drawing-an-image]] results in the paint valid flag for a <> function to be set to paint-valid. +
+partial namespace CSS {
+    [SameObject] readonly attribute Worklet paintWorklet;
+};
+
-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. +A {{PaintWorkletGlobalScope}} is a global execution context of the {{paintWorklet}}. -Registering Custom Paint {#registering-custom-paint} -==================================================== +A {{PaintWorkletGlobalScope}} has a {{PaintWorkletGlobalScope/devicePixelRatio}} property which is +identical to the Window.{{Window/devicePixelRatio}} property.
-callback VoidFunction = void ();
-
-partial interface RenderWorkletGlobalScope {
-    void registerPaint(DOMString name, VoidFunction paintCtor);
-    void unregisterPaint(DOMString name);
+[Global=(Worklet,PaintWorklet),Exposed=PaintWorklet]
+interface PaintWorkletGlobalScope : WorkletGlobalScope {
+    undefined registerPaint(DOMString name, VoidFunction paintCtor);
+    readonly attribute unrestricted double devicePixelRatio;
 };
 
-The {{RenderWorkletGlobalScope}} has a map of name to paint instance map. Initially this map is empty; it is populated when {{registerPaint(name, paintCtor)}} is called. +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: This is how the class should look. -
-        callback interface PaintClass {
-            readonly attribute sequence<DOMString> inputProperties;
-            void paint(PaintRenderingContext2D ctx, Geometry geom, StylePropertyMap inputProperties);
-            void overflow(StylePropertyMap inputProperties);
-        };
+    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.
+            }
+        }
     
-When the registerPaint(name, paintCtor) method is called, the user agent must run the following steps: - 1. If the |name| is not a valid <>, throw a NotSupportedError and abort all these steps. - 2. If the |name| exists as a key in the name to paint instance map, throw a NotSupportedError and abort all these steps. - 3. If the result of IsConstructor(argument=|paintCtor|) is false, throw a NotSupportedError and abort all these steps. - 4. Let prototype be the result of Get(O=|paintCtor|, P="prototype"). - 5. If the result of IsCallable(argument=Get(O=|prototype|, P="paint")) is false, throw a NotSupportedError and abort all these steps. - 6. If the result of HasProperty(O=|prototype|, P="overflow") and IsCallable(argument=Get(O=|prototype|, P="overflow")) is false, throw a NotSupportedError and abort all these steps. - 7. Let inputProperties be the result of Get(O=|paintCtor|, P="inputProperties"). - 8. If the result of IsArray(argument=|inputProperties|) is false, throw a NotSupportedError and abort all these steps. - 9. Add the key-value pair (|name| - |inputProperties|) to the paint name to input properties map of the associated document. - 10. Let paintInstance be the result of Construct(|paintCtor|). - 11. Add the key-value pair (|name| - |paintInstance|) to the name to paint instance map of the {{RenderWorkletGlobalScope}}. +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 function=] type. + + - 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 + [=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 <> 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|] [=map/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. + + 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. -Note: The list of CSS properties provided by the input properties getter can either be custom or native CSS properties. + 11. Let |inputArgumentSyntaxes| be an [=list/empty=] [=list=]. -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. + 12. [=list/For each=] |item| in |inputArguments| perform the following substeps: -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 {{Geometry}} and {{StylePropertyMap}} as inputs. + 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=]. -Issue(w3c/css-houdini-drafts#31): Allow author to specify the intrinsic size. + 2. [=list/Append=] |parsedSyntax| to |inputArgumentSyntaxes|. -When the unregisterPaint(name) method is called, the user agent must run the following steps: - 1. Remove the key-value pair associated with the |name| key in the paint name to input properties map of the associated document. - 2. Remove the key-value pair associated with the |name| key in the name to paint instance map of the {{RenderWorkletGlobalScope}}. + 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 Notation {#paint-notation} ================================
-    paint() = paint( <> )
+    paint() = paint( <>, <>? )
 
The <> function is an additional notation to be supported by the <> type.
-
background-image: paint(my_logo);
+
+        <style>
+            .logo { background-image: paint(company-logo); }
+            .chat-bubble { background-image: paint(chat-bubble, blue); }
+        </style>
+    
-Issue(w3c/css-houdini-drafts#33): What to do about cursor. +For the 'cursor' property, the <> function should be treated as an [=invalid image=] and +fallback to the next supported <>. -Issue: How do we do things like conic-gradient? I.e. paint functions which accept arguments as inputs? +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=Worklet]
+[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 CanvasText;
-PaintRenderingContext2D implements CanvasDrawImage;
-PaintRenderingContext2D implements CanvasPathDrawingStyles;
-PaintRenderingContext2D implements CanvasTextDrawingStyles;
-PaintRenderingContext2D implements CanvasPath;
+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 {{CanvasHitRegion}}, {{CanvasImageData}} or - {{CanvasUserInterface}} APIs. - -A {{PaintRenderingContext2D}} object has a scratch bitmap. This is initialised when the -object is created. The size of the scratch bitmap is the size of the fragment it is rendering -plus the size specified by the overflow method. - -The logical origin (0,0) is not necessarily placed at the origin of the scratch bitmap. If -the fragment which is being painted has an associated overflow, the logical origin is placed at -(overflow-left,overflow-top) of the scratch bitmap. - -Issue: Add image explaining origin vs. logical origin. - -The size of the scratch bitmap does not necessarily represent the size of the actual bitmap + 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 scratch 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 scratch -bitmap repeatably while the visual viewport is being zoomed for example. +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. + +
+ 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 |overflowOffset| it must run the following steps: +
+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 scratch bitmap to |width| and |height|. - 3. Set the logical origin of the scratch bitmap to |overflowOffset|. + 2. [=Set bitmap dimensions=] for the context's [=PaintRenderingContext2D/output bitmap=] to the rounded values of |width| and |height|. + 3. Set the {{PaintRenderingContext2D}}'s [=PaintRenderingContext2D/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 + [=PaintRenderingContext2D/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. +See Issue 819. + 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=Worklet]
-interface Geometry {
+[Exposed=PaintWorklet]
+interface PaintSize {
     readonly attribute double width;
     readonly attribute double height;
 };
 
-If a <> function for a fragment is paint-invalid and the fragment is within the visual viewport, -then user agent must draw an image for the current frame. +
+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 [=map/exist=], let the image output + be an [=invalid image=] and abort all these steps. + + 5. Let |documentDefinition| be the result of [=map/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 [=map/exist=], run the following steps: + + 1. [=Queue a task=] to run the following steps: -Note: The user agent may choose to draw an image for <> functions not within the visual viewport. + 1. Let |documentPaintDefinitionMap| be the associated [=document=]'s [=document + paint definitions=] map. -When the user agent wants to draw an image of a <> for a fragment into its appropriate stacking level (as defined by the property the CSS property it's associated with) it must run the following steps: - 1. If the paint valid flag for the <> function on the |fragment| is paint-valid the user agent may use the drawn image from the previous invocation. - If so it can abort all these steps. + 2. [=map/Set=] |documentPaintDefinitionMap|[|name|] to "invalid". - 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. + 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 name be the first argument of the <> function. + 2. Let the image output be an [=invalid image=] and abort all these steps. - 3. If no paint class was registered with |name|, the resulting image output will be an invalid image and the user agent must 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. - 4. Let inputProperties be the result of looking up |name| on the associated document's paint name to input properties map. + 3. Let |definition| be the result of [=get=] |paintDefinitionMap|[|name|]. - 5. Let styleMap be a new {{StylePropertyMap}} populated with only the computed value's for properties listed in |inputProperties|. + 4. Let |paintClassInstanceMap| be |workletGlobalScope|'s [=paint class instances=] map. - 6. Let overflow be the result invoke a method on a class inside a Worklet given "overflow" as the methodPropertyKey and [|styleMap|] as the arguments with the following options: - - To create a worklet global scope the user agent will: + 5. Let |paintInstance| be the result of [=get=] |paintClassInstanceMap|[|name]|. If + |paintInstance| is null, run the following steps: - Return a new {{RenderWorkletGlobalScope}}. - - To lookup a class instance on a worklet global scope given a |workletGlobalScope|, the user agent will: + 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. - Return the result of looking up |name| on the |workletGlobalScope|'s name to paint instance map. + 2. Let |paintCtor| be the [=paint definition/class constructor=] on |definition|. - Note: User agents may have to compute overflow before entering their paint phase in order to determine which fragments to paint (overflow changes what could be seen on the output device). - User agents may opt into running the steps up to this point, to determine overflow, then continuing later to determine the drawn image for the fragments which need painting. + 3. Let |paintInstance| be the result of [=Construct=](|paintCtor|). - 7. Let renderingContext be the result of create a {{PaintRenderingContext2D}} object given: - - "overflowOffset" - The logical offset for the scratch bitmap specified by |overflow|. - - "width" - The width of the |fragment| plus the additional width specified by |overflow|. - - "height" - The height of the |fragment| plus the additional height specified by |overflow|. + 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. - 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. + 4. [=map/Set=] |paintClassInstanceMap|[|name|] to |paintInstance|. - Issue: TODO add note we should specify the output of overflow. + 6. Let |inputProperties| be |definition|'s [=paint definition/input properties=]. - 8. Let geometry be a new {{Geometry}} initialized to the width and height of the |fragment|. + 7. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the + [=computed value=]'s for properties listed in |inputProperties|. - Issue(w3c/css-houdini-drafts#23): Decide geometry information should be in level 1. + 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|. - 9. To produce the image output, invoke a method on a class inside a Worklet given "paint" as the methodPropertyKey and [|renderingContext|, |geometry|, |styleMap|] as the arguments with the following options: - - To create a worklet global scope the user agent will: + 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. - Return a new {{RenderWorkletGlobalScope}}. - - To lookup a class instance on a worklet global scope given a |workletGlobalScope|, the user agent will: + 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. - Return the result of looking up |name| on the |workletGlobalScope|'s name to paint instance map. + 9. Let |paintSize| be a new {{PaintSize}} initialized to the width and height defined by + |snappedConcreteObjectSize|. - If an exception is thrown the resulting image output will be an invalid image. + 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. - Otherwise the image output should be produced from the |renderingContext| given to the paint method. +
+ 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. - Note: User agents should provide tooling within their debugging tools to show authors a partial output of the image, if an exception is thrown. +
+                // paint.js
+                registerPaint('simple', class {
+                    paint(ctx, size) {
+                        ctx.fillStyle = 'green';
+                        ctx.fillRect(0, 0, size.width, size.height);
+                    }
+                });
+            
- 10. Set the paint valid flag for the <> function on the |fragment| to paint-valid. +
+                <style>
+                    .div-1 {
+                        width: 50px;
+                        height: 50px;
+                        background-image: paint(simple);
+                    }
+                    .div-2 {
+                        width: 100px;
+                        height: 100px;
 
-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.
+                        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. +
-Note: The contents of the image are not designed to be accessible. Authors should communicate any useful information through the standard accessibility APIs. +Global Scope Selection {#global-scope-selection} +------------------------------------------------ -Issue(w3c/css-houdini-drafts#24): Determine how to side-load images or other data. +When the user agent needs to select a {{PaintWorkletGlobalScope}} from the paint [=worklet's +WorkletGlobalScopes=] [=list=] it must: -Issue: What information should we provide for read-modify-write use-cases? - Are read-modify-write use-cases important for v1? - For example, if you are sliding the previous paint output out? - For providing the previous paint output we should provide an ImageBitmap if you ask. + - Select from at least two {{PaintWorkletGlobalScope}}s, unless the user agent is under + memory constraints. -Issue: Describe what happens if a callback doesn't hit a frame timing boundary. I.e. just renders a transparent image instead? + - 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: A colored circle. {#example-1} ------------------------------------------ +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>
+
-// Inside RenderWorkletGlobalScope.
+// circle.js
 registerPaint('circle', class {
-    static get inputProperties() { return ['--circle-color']; }
+  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) {
-        // Change the fill color.
-        var color = properties.get('--circle-color');
-        ctx.fillStyle = color;
+        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>
 
-        // Determine the center point and radius.
-        var x = geom.width / 2;
-        var y = geom.height / 2;
-        var radius = Math.min(x, y);
+<script>
+    CSS.paintWorklet.addModule('arc.js');
+</script>
+
- // Draw the circle \o/ - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.fill(); +
+// 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} +-------------------------------------------------------- +
-<div id="myElement">
-    CSS is awesome.
-</div>
+<h1>
+    Heading 1
+</h1>
+<h1>
+    Another heading
+</h1>
+
+<style>
+h1 {
+    background-image: paint(heading-color);
+}
+</style>
 
-<style>
-#myElement {
-    --circle-color: red;
-    background-image: paint(circle);
+<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>
+</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);
+    }
+});
 
-Issue: Add conic-gradient as a use case. +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. -Issue: Add image with placeholder as a use case. E.g. mountainy-thingy. +* Fix definition for the size of the output bitmap: -Issue: Add image url with var() input to custom paint as a use case. + > The size of the output bitmap is the concrete object size of the object it is rendering to size of the fragment it is rendering. diff --git a/css-paint-api/README.md b/css-paint-api/README.md new file mode 100644 index 00000000..93e86dde --- /dev/null +++ b/css-paint-api/README.md @@ -0,0 +1 @@ +See [EXPLAINER](EXPLAINER.md). diff --git a/css-paint-api/README_V2.md b/css-paint-api/README_V2.md new file mode 100644 index 00000000..d7678ec1 --- /dev/null +++ b/css-paint-api/README_V2.md @@ -0,0 +1,61 @@ +```js +registerPainter('something', class extends ElementPainter { + visualOverflow(geometry, styleMap) { + return { + // Size of overflow. + }; + } + + drawBackground(ctx, geometry, styleMap) { + ctx.save(); + ctx.globalAlpha = 0.5; // Local to just the background layer, are there other canvas ctx params which + super(ctx, geom, styleMap); + ctx.restore(); + ctx.draw(styleMap.get('--other-background-image')); + } + + configureBackgroundContext(ctx, geometry, styleMap) { + ctx.clipPath = new Path2D(); // ctx here would be subset that could be describing imperitively. + } + + drawBorder(ctx, geometry, styleMap) { + + } + + configureBorderContext(ctx, geometry, styleMap) { + ctx.clipPath = new Path2D(); // ctx here would be subset that could be describing imperitively. + } + + drawForeground(ctx, geometry, styleMap) { // TODO add issue for this callback, use cases. + ctx.drawImage(styleMap.get('--something-in-content-layer')); + + super(ctx, geometry, styleMap); + + ctx.drawImage(styleMap.get('--something-in-content-layer')); + } + + configureForegroundContext(ctx, geometry, styleMap) { + ctx.clipPath = new Path2D(); // ctx here would be subset that could be describing imperitively. + } + + // still need this as creates stacking context. + configureContext(ctx, geometry, styleMap) { + ctx.clipPath = new Path2D(); // ctx here would be subset that could be describing imperitively. + } +}); +``` + +```css +.class { + painter: something; // implies this creates a stacking context. +} +``` + + +```js +PaintV2RenderingContext.drawImage(styleMap.get('background-image'), ....); +PaintV2RenderingContext.opacity +PaintV2RenderingContext.clipPath = Path2D +PaintV2RenderingContext.filter = styleMap.get('--filter-type'); or 'blur(2px)'; + +``` diff --git a/css-paint-api/arc/arc.js b/css-paint-api/arc/arc.js new file mode 100644 index 00000000..29cdab08 --- /dev/null +++ b/css-paint-api/arc/arc.js @@ -0,0 +1,64 @@ +registerPaint('arc', class { + static get inputArguments() { + return [ + '', + '', // startAngle + '', // endAngle + '', // radius + '', // lineWidth + ]; + } + + constructor() { + this.regex = /[a-z]+/g; + } + + 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) { + const value = angle.value || parseFloat(angle.cssText); + const unit = angle.unit || angle.cssText.match(this.regex)[0]; + + switch (unit) { + case 'deg': + return value * Math.PI / 180; + case 'rad': + return value; + case 'grad': + return value * Math.PI / 200; + case 'turn': + return value * Math.PI / 0.5; + default: + throw Error(`Unknown angle unit: ${unit}`); + } + } + + convertLength(length) { + switch (length.type) { + case 'px': + return length.value; + default: + throw Error(`Unkown length type: ${length.type}`); + } + } +}); diff --git a/css-paint-api/arc/index.html b/css-paint-api/arc/index.html new file mode 100644 index 00000000..e2a65e40 --- /dev/null +++ b/css-paint-api/arc/index.html @@ -0,0 +1,18 @@ + + + +
+ + diff --git a/css-paint-api/chat/chat.js b/css-paint-api/chat/chat.js new file mode 100644 index 00000000..fda29c44 --- /dev/null +++ b/css-paint-api/chat/chat.js @@ -0,0 +1,100 @@ +registerPaint('chat', class { + static get inputProperties() { return ['background-image', '--chat-images-num']; } + + constructor() { + this.radii = [ + 0.5, + 1 / (2 + 2/Math.SQRT2), + 0.25, + 0.25, + 1 / Math.SQRT2 - 0.5, + ]; + + const fudge = 0.03349; + this.positions = [ + [ + {x: 0.5, y: 0.5}, + ], + [ + {x: this.radii[1], y: this.radii[1]}, + {x: 1-this.radii[1], y: 1-this.radii[1]}, + ], + [ + {x: 0.5, y: 0.25 + fudge}, + {x: 0.75, y: 0.75 - fudge}, + {x: 0.25, y: 0.75 - fudge}, + ], + [ + {x: 0.25, y: 0.25}, + {x: 0.75, y: 0.25}, + {x: 0.25, y: 0.75}, + {x: 0.75, y: 0.75}, + ], + [ + {x: this.radii[4], y: this.radii[4]}, + {x: 1-this.radii[4], y: this.radii[4]}, + {x: 0.5, y: 0.5}, + {x: 1-this.radii[4], y: 1-this.radii[4]}, + {x: this.radii[4], y: 1-this.radii[4]}, + ], + ]; + + this.colors = [ + '#E91E63', + '#9C27B0', + '#2196F3', + '#8BC34A', + '#FF9800', + ]; + } + + paint(ctx, geom, styleMap) { + ctx.fillStyle = '#FFF'; + ctx.fillRect(0, 0, geom.width, geom.height); + + // Chrome can only use background-images as source images at the moment. + const images = styleMap.getAll('background-image'); + + const num = styleMap.get('--chat-images-num').value; + const num_low = Math.floor(num); + const num_high = Math.ceil(num); + + const dist = (num - num_low); + const size = Math.min(geom.width, geom.height); + + const r_low = this.radii[num_low-1]; + const r_high = this.radii[num_high-1]; + + const pos_low = this.positions[num_low-1]; + const pos_high = this.positions[num_high-1]; + + for (let i = 0; i < num_high; i++) { + if (!pos_high) break; + const low = pos_low && pos_low[i]; + const high = pos_high[i]; + + let x, y, r; + if (num_low != num_high && i == num_high - 1) { + x = size * high.x; + y = size * high.y; + r = size * dist * r_high; + } else { + x = size * ((1-dist) * low.x + dist * high.x); + y = size * ((1-dist) * low.y + dist * high.y); + r = size * ((1-dist) * r_low + dist * r_high); + } + + ctx.fillStyle = this.colors[i]; + ctx.beginPath(); + ctx.arc(x, y, 0.95 * r, 0, Math.PI*2); + ctx.fill(); + + ctx.save(); + ctx.beginPath(); + ctx.arc(x, y, 0.9 * r, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(images[i], x - r, y - r, 2*r, 2*r); + ctx.restore(); + } + } +}); diff --git a/css-paint-api/chat/index.html b/css-paint-api/chat/index.html new file mode 100644 index 00000000..587335f1 --- /dev/null +++ b/css-paint-api/chat/index.html @@ -0,0 +1,50 @@ + + + +
+ + + + diff --git a/css-paint-api/circle/circle.js b/css-paint-api/circle/circle.js new file mode 100644 index 00000000..553be1a3 --- /dev/null +++ b/css-paint-api/circle/circle.js @@ -0,0 +1,18 @@ +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(); + } +}); diff --git a/css-paint-api/circle/index.html b/css-paint-api/circle/index.html new file mode 100644 index 00000000..60dffdae --- /dev/null +++ b/css-paint-api/circle/index.html @@ -0,0 +1,30 @@ + + + + + + + diff --git a/css-paint-api/issues-cr-2018.bs b/css-paint-api/issues-cr-2018.bs new file mode 100644 index 00000000..65ee503f --- /dev/null +++ b/css-paint-api/issues-cr-2018.bs @@ -0,0 +1,57 @@ +Draft: https://www.w3.org/TR/2018/CR-css-paint-api-1-20180809/ +Title: CSS Painting API Level 1 +---- +Issue 1. +Summary: Broken LInks +From: Chris Lilley +Comment: https://github.com/w3c/css-houdini-drafts/issues/787 +Response: https://github.com/w3c/css-houdini-drafts/issues/787#issuecomment-411123499 +Closed: Accepted +Resolved: Editorial +---- +Issue 2. +Summary: CSS Paint API leaks browsing history +From: Deian Stefan +Comment: https://github.com/w3c/css-houdini-drafts/issues/791 +Response: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546460085 +Changes: https://github.com/w3c/css-houdini-drafts/commit/3c72275054d9d541e6526e2988567ad4d209f257 +Closed: Accepted +Resolved: Editor discretion +Verified: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546466424 +---- +Issue 3. +Summary: Typo: use snappedConcreteObjectSize in paint callback +From: Chris Harrelson +Comment: https://github.com/w3c/css-houdini-drafts/issues/800 +Changes: https://github.com/w3c/css-houdini-drafts/commit/735ea42edf09f8d69de1eddbc7e7691528f56963 +Closed: Accepted +Resolved: Editorial +---- +Issue 4. +Summary: Disconnect between css-paint-api-1 and HTML specs on CanvasImageSource +From: Alan Jeffrey +Comment: https://github.com/w3c/css-houdini-drafts/issues/819 +Response: https://github.com/w3c/css-houdini-drafts/issues/819#issuecomment-424360647 +Open: Waiting for HTML spec edits +---- +Issue 5. +Summary: Improve passing of large-scale data to PaintWorklet +From: John Wiesz +Comment: https://github.com/w3c/css-houdini-drafts/issues/872 +Response: https://github.com/w3c/css-houdini-drafts/issues/872#issuecomment-499903766 +Open: Waiting on edits to add new features to Properties an Values API +---- +Issue 6. +Summary: Cycle possibe using inputProperties() +From: Stephen McGruer +Comment: https://github.com/w3c/css-houdini-drafts/issues/877 +Response: https://github.com/w3c/css-houdini-drafts/issues/877#issuecomment-499922873 +Open: Needs Edits +---- +Issue 7. +Summary: Two-way communication between main thread and worklet +From: Samad Aghaei +Comment: https://github.com/w3c/css-houdini-drafts/issues/881 +Response: https://github.com/w3c/css-houdini-drafts/issues/881#issuecomment-546456591 +Closed: OutOfScope +---- diff --git a/css-paint-api/issues-cr-2018.html b/css-paint-api/issues-cr-2018.html new file mode 100644 index 00000000..9c7a0ccb --- /dev/null +++ b/css-paint-api/issues-cr-2018.html @@ -0,0 +1,128 @@ + + +CSS Painting API Level 1 Disposition of Comments for 2018-08-09 CR + + +

CSS Painting API Level 1 Disposition of Comments for 2018-08-09 CR

+ +

Dated Draft: https://www.w3.org/TR/2018/CR-css-paint-api-1-20180809/ + +

Editor's Draft: http://drafts.csswg.org/css-paint-api-1/ + +

The following color coding convention is used for comments:

+ +
    +
  • Accepted or Rejected and positive response +
  • Rejected and no response +
  • Rejected and negative response +
  • Deferred +
  • Out-of-Scope or Invalid and not verified +
+ +

Open issues are marked like this

+ +

An issue can be closed as Accepted, OutOfScope, +Invalid, Rejected, or Retracted. +Verified indicates commentor's acceptance of the response.

+
+Issue 1. #
+Summary:  Broken LInks
+From:     Chris Lilley
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/787
+Response: https://github.com/w3c/css-houdini-drafts/issues/787#issuecomment-411123499
+Closed:   Accepted
+Resolved: Editorial
+
+Issue 2. #
+Summary:  CSS Paint API leaks browsing history
+From:     Deian Stefan
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/791
+Response: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546460085
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/3c72275054d9d541e6526e2988567ad4d209f257
+Closed:   Accepted
+Resolved: Editor discretion
+Verified: https://github.com/w3c/css-houdini-drafts/issues/791#issuecomment-546466424
+
+Issue 3. #
+Summary:  Typo: use snappedConcreteObjectSize in paint callback
+From:     Chris Harrelson
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/800
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/735ea42edf09f8d69de1eddbc7e7691528f56963
+Closed:   Accepted
+Resolved: Editorial
+
+Issue 4. #
+Summary:  Disconnect between css-paint-api-1 and HTML specs on CanvasImageSource
+From:     Alan Jeffrey
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/819
+Response: https://github.com/w3c/css-houdini-drafts/issues/819#issuecomment-424360647
+Open:     Waiting for HTML spec edits
+
+Issue 5. #
+Summary:  Improve passing of large-scale data to PaintWorklet
+From:     John Wiesz
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/872
+Response: https://github.com/w3c/css-houdini-drafts/issues/872#issuecomment-499903766
+Open:     Waiting on edits to add new features to Properties an Values API
+
+Issue 6. #
+Summary:  Cycle possibe using inputProperties()
+From:     Stephen McGruer
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/877
+Response: https://github.com/w3c/css-houdini-drafts/issues/877#issuecomment-499922873
+Open:     Needs Edits
+
+Issue 7. #
+Summary:  Two-way communication between main thread and worklet
+From:     Samad Aghaei
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/881
+Response: https://github.com/w3c/css-houdini-drafts/issues/881#issuecomment-546456591
+Closed:   OutOfScope
+ diff --git a/css-paint-api/issues-list-2018-04-10.html b/css-paint-api/issues-list-2018-04-10.html new file mode 100644 index 00000000..34669236 --- /dev/null +++ b/css-paint-api/issues-list-2018-04-10.html @@ -0,0 +1,108 @@ + + +CSS Paint API Level 1 Disposition of Comments for 2018-04-10 WD + + +

CSS Paint API Level 1 Disposition of Comments for 2018-04-10 WD

+ +

Dated Draft: https://www.w3.org/TR/2018/WD-css-paint-api-1-20180410/ + +

Editor's Draft: http://drafts.csswg.org/css-paint-api-1/ + +

The following color coding convention is used for comments:

+ +
    +
  • Accepted or Rejected and positive response +
  • Rejected and no response +
  • Rejected and negative response +
  • Deferred +
  • Out-of-Scope or Invalid and not verified +
+ +

Open issues are marked like this

+ +

An issue can be closed as Accepted, OutOfScope, +Invalid, Rejected, or Retracted. +Verified indicates commentor's acceptance of the response.

+
+Issue 1. #
+Summary:  Filter out unsupported properties from inputProperties.
+From:     Darren Shen
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/523
+Response: https://github.com/w3c/css-houdini-drafts/issues/523#issuecomment-351219067
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/50fa9b8bedde46462113d756d6895701d3d743d0
+Closed:   Accepted
+Resolved: Editor discretion
+
+Issue 2. #
+Summary:  Allow inputArguments define optional arguments.
+From:     zheeeng
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/763
+Response: https://github.com/w3c/css-houdini-drafts/issues/763#issuecomment-401653404
+Closed:   Deferred
+
+Issue 3. #
+Summary:  Need to convert paint function to WebIDL Function type.
+From:     Shiino Yuki
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/743
+Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324
+Changes:  https://github.com/w3c/css-houdini-drafts/commit/f9f174dca09f5149c3df558d1c8e74517df19f5e
+Closed:   Accepted
+Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html 
+
+Issue 4. #
+Summary:  Use WebIDL callback interface inside of registerPaint.
+From:     Shiino Yuki
+Comment:  https://github.com/w3c/css-houdini-drafts/issues/743
+Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324
+Closed:   Rejected
+Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html 
+Verified: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379646250
+ diff --git a/css-paint-api/issues-list-2018-04-10.txt b/css-paint-api/issues-list-2018-04-10.txt new file mode 100644 index 00000000..41cef9ac --- /dev/null +++ b/css-paint-api/issues-list-2018-04-10.txt @@ -0,0 +1,38 @@ +Draft: https://www.w3.org/TR/2018/WD-css-paint-api-1-20180410/ +Title: CSS Paint API Level 1 + +---- +Issue 1. +Summary: Filter out unsupported properties from inputProperties. +From: Darren Shen +Comment: https://github.com/w3c/css-houdini-drafts/issues/523 +Response: https://github.com/w3c/css-houdini-drafts/issues/523#issuecomment-351219067 +Changes: https://github.com/w3c/css-houdini-drafts/commit/50fa9b8bedde46462113d756d6895701d3d743d0 +Closed: Accepted +Resolved: Editor discretion +---- +Issue 2. +Summary: Allow inputArguments define optional arguments. +From: zheeeng +Comment: https://github.com/w3c/css-houdini-drafts/issues/763 +Response: https://github.com/w3c/css-houdini-drafts/issues/763#issuecomment-401653404 +Closed: Deferred +---- +Issue 3. +Summary: Need to convert paint function to WebIDL Function type. +From: Shiino Yuki +Comment: https://github.com/w3c/css-houdini-drafts/issues/743 +Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324 +Changes: https://github.com/w3c/css-houdini-drafts/commit/f9f174dca09f5149c3df558d1c8e74517df19f5e +Closed: Accepted +Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html +---- +Issue 4. +Summary: Use WebIDL callback interface inside of registerPaint. +From: Shiino Yuki +Comment: https://github.com/w3c/css-houdini-drafts/issues/743 +Response: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379738324 +Closed: Rejected +Resolved: https://lists.w3.org/Archives/Public/public-houdini/2018Apr/0002.html +Verified: https://github.com/w3c/css-houdini-drafts/issues/743#issuecomment-379646250 +---- diff --git a/css-parser-api/Overview.bs b/css-parser-api/Overview.bs index f72ce478..82f9a00d 100644 --- a/css-parser-api/Overview.bs +++ b/css-parser-api/Overview.bs @@ -1,13 +1,19 @@ + +Introduction {#intro} +===================== + +This spec is intentionally left blank, +as it is currently being developed in the WICG +at https://github.com/wicg/css-parser-api/ +(live spec). diff --git a/css-parser-api/readme.md b/css-parser-api/readme.md new file mode 100644 index 00000000..e8a82d7f --- /dev/null +++ b/css-parser-api/readme.md @@ -0,0 +1,5 @@ +CSS Parser Explainer +==================== + +This specification is not currently being worked on in the Houdini TF. +Instead, it will be worked on in a [WICG repository](https://github.com/WICG/CSS-Parser-API). diff --git a/css-properties-values-api/Overview.bs b/css-properties-values-api/Overview.bs index e3f4d3aa..1caace06 100644 --- a/css-properties-values-api/Overview.bs +++ b/css-properties-values-api/Overview.bs @@ -1,17 +1,20 @@ -
-{
-  "css-paint-api": {
-    "title": "CSS Painting API"
-  },
-  "css-layout-api": {
-    "title": "CSS Layout API"
-  }
-}
+
 
 Introduction {#intro}
@@ -47,116 +61,801 @@ can only impact document layout or paint by being re-incorporated into the value
 of other properties via a var() reference.
 
 This specification extends [[css-variables]], allowing the registration of properties
-that have a value type, an initial value, and a defined inheritance behaviour. This
-specification also provides an additional javascript-mediated means via which custom
-properties can modify the computed value of native properties.
+that have a value type, an initial value, and a defined inheritance behaviour,
+via two methods:
 
-This specification is complementary to [[css-paint-api]] and [[css-layout-api]], which
+* A JS API, the {{registerProperty()}} method
+* A CSS at-rule, the ''@property'' rule
+
+This specification is complementary to [[css-paint-api-1]] and [[css-layout-api-1]], which
 allow custom properties to directly impact paint and layout behaviours respectively.
-Accordingly, it is recommended not to use the computed value modification facilities of this
-specification directly for layout, but only to impact cascading in a non-native manner.
 
-[[css-variables]] defines a new <> function that can be used to
-insert the values of custom properties into other CSS property values. Where
-possible, this mechanism should be preferred above the computed value modification
-facilities of this specification.
 
-Registering custom properties {#registering-custom-properties}
-==============================================================
+
+
+
+Registered Custom Properties {#behavior-of-custom-properties}
+=============================================================
+
+A [=custom property=] can become a registered custom property,
+making it act more like a UA-defined property:
+giving it a syntax that's checked by the UA,
+an initial value,
+and a specific inheritance behavior.
+This can be done by the ''@property'' rule,
+or the {{registerProperty()}} JS function.
+
+A [=custom property=] is considered to be registered for a {{Document}}
+if there is a valid ''@property'' rule
+defined for its name
+in one of the document's stylesheets,
+or its name is [=map/contains|contained=]
+in the document's {{[[registeredPropertySet]]}} slot
+(that is, {{registerProperty()}} was called to register it).
+
+A [=registered custom property=] acts similarly to an unregistered [=custom property=],
+except as defined below.
+
+Determining the Registration {#determining-registration}
+--------------------------------------------------------
+
+A [=registered custom property=] has a custom property registration
+that contains all the data necessary to treat it like a real property.
+It's a [=struct=] consisting of:
+
+* a property name (a [=custom property name string=])
+* a syntax (a [=syntax string=])
+* an inherit flag (a [=boolean=])
+* optionally, an initial value (a [=string=] which successfully [=CSS/parses=] according to the syntax)
+
+If the {{Document}}’s {{[[registeredPropertySet]]}} slot
+[=set/contains=] a record with the [=custom property’s=] name,
+the registration is that record.
+
+Otherwise,
+if the {{Document}}’s active stylesheets contain at least one valid ''@property'' rule
+representing a registration with the [=custom property’s=] name,
+the last such one in document order is the registration.
+
+Otherwise there is no registration,
+and the [=custom property=] is not a [=registered custom property=].
+
+Parse-Time Behavior {#parsing-custom-properties}
+------------------------------------------------
+
+[=Registered custom properties=] parse exactly like unregistered [=custom properties=];
+almost anything is allowed.
+The registered syntax of the property is not checked at parse time.
+
+Note: However,
+the syntax is checked at computed-value time,
+before substitution via ''var()''.
+See [[#calculation-of-computed-values]].
+
+
+ Why aren't custom properties syntax-checked? + + When parsing a page's CSS, + UAs commonly make a number of optimizations + to help with both speed and memory. + + One of those optimizations + is that they only store the properties that will actually have an effect; + they throw away invalid properties, + and if you write the same property multiple times in a single declaration block, + all but the last valid one will be thrown away. + (This is an important part of CSS's error-recovery + and forward-compatibility behavior.) + + This works fine if the syntax of a property never changes over the lifetime of a page. + If a custom property is registered, however, + it can change its syntax, + so that a property that was previously invalid + suddenly becomes valid. + + The only ways to handle this are to either store every declaration, + even those that were initially invalid + (increasing the memory cost of pages), + or to re-parse the entire page's CSS + with the new syntax rules + (increasing the processing cost of registering a custom property). + Neither of these are very desirable. + + Further, + UA-defined properties have their syntax determined + by the version of the UA the user is viewing the page with; + this is out of the page author's control, + which is the entire reason for CSS's error-recovery behavior + and the practice of writing multiple declarations for varying levels of support. + A custom property, on the other hand, + has its syntax controlled by the page author, + according to whatever stylesheet or script they've included in the page; + there's no unpredictability to be managed. + Throwing away syntax-violating custom properties + would thus only be, at best, a convenience for the page author, + not a necessity like for UA-defined properties. +
+ +[=Specified Value=]-Time Behavior {#specified-value} +---------------------------------------------------- + +Just like unregistered [=custom properties=], +all [=registered custom properties=], regardless of registered syntax, +accept the [=CSS-wide keywords=], +such as ''inherit'' or ''revert''. +Their behavior is defined in [[css-cascade-4#defaulting-keywords]]. + +[=Computed Value=]-Time Behavior {#calculation-of-computed-values} +------------------------------------------------------------------ + +The [=computed value=] of a [=registered custom property=] +is determined by the syntax of its [=registration=]. + +If the [=registration’s=] syntax is the [=universal syntax definition=], +the [=computed value=] is the same as for unregistered [=custom properties=] +(either the specified value with variables substituted, +or the [=guaranteed-invalid value=]). + +Otherwise, attempt to [=CSS/parse=] the property's value +according to its registered syntax. +If this fails, +the [=computed value=] is the [=guaranteed-invalid value=]. +If it succeeds, +the [=computed value=] depends on the specifics of the syntax: + +For "<length>", +"<length-percentage>", +"<angle>", +"<time>", +"<resolution>", +"<integer>", +"<number>", +and "<percentage>" values: + +* If the specified value is a [=dimension=] literal + (such as ''50em'' or ''.2s''), + the computed value is the same value, + but with the unit converted to the corresponding [=canonical unit=] + for the type of value. +* If the specified value is any other numeric literal + (such as ''5'' or ''20%''), + the computed value is as specified. + (In particular, percentages are never resolved against anything.) +* If the specified value is a function that evaluates to one of those types + (such as a [=math function=]), + the computed value is defined by that function. + +For "<color>" values, +the value is computed by [=resolving color values=]. + +For "<custom-ident>", ident, or "*" values, +the computed value is as specified. + +For "<url>" values, +the computed value is one of the following: + +* if the URL is a relative URL, + the computed value is the resolved absolute URL as described in [[!css3-values]]. +* otherwise, the computed value is as specified. + +
+ URL behavior examples +
+ Because URLs resolve against the base URL of the stylesheet they appear in, we can + end up with multiple relative URLs that resolve against different base URLs, even though + they appear in the same property. + + For example, suppose '--url-foo' and '--url-bar' are registered + custom properties with ''<url>'' syntax, and that we have a stylesheet at + /style/foo/foo.css: + +
+		div {
+			--url-foo: url("foo.png");
+		}
+		
+ + and another stylesheet at /style/bar/bar.css +
+		div {
+			--url-bar: url("bar.png");
+		}
+		
+ + and finally a document at /index.html: +
+		<link href="/style/foo/foo.css" rel="stylesheet" type="text/css">
+		<link href="/style/bar/bar.css" rel="stylesheet" type="text/css">
+		<div style="background-image: var(--url-foo), var(---url-bar);">
+		</div>
+		
+ + Here, the ''var(--url-foo)'' reference would produce a URL that resolves against + /style/foo, and the ''var(--url-bar)'' reference would produce a URL that resolves + against /style/bar. + + On the other hand, + if both '--url-foo' and '--url-bar' were unregistered, + they would substitute their literal values + (relative URLs) + into the /index.html stylesheet, + which would then resolve the URLs against /index.html instead. +
+
+ +For "<image>" values, +the computed value is the [=computed <image>=]. + +For "<transform-function>" and "<transform-list>" values, +the computed value is as specified but with all lengths resolved to their computed values. + +For values with [[#multipliers|multipliers]], +the computed value is a list of the computed values of the base type. + +For syntaxes specified with [[#combinator|the | combinator]], +the computed value is given by applying the computed-value rules +for the first clause that matches the value. + + +Animation Behavior {#animation-behavior-of-custom-properties} +------------------------------------------------------------- + +Note: As defined by [[css3-animations]] and [[css3-transitions]], it is possible to +specify animations and transitions that reference custom properties. + +When referenced by animations and transitions, +custom property values [=interpolate=] [=by computed value=], +in accordance with the type that they parsed as. + +Note: This implies that a list of values, +such as `+` or `#`, +will interpolate as a simple list, +matching up each component index-by-index, +and failing if the number of components doesn't match. + +As an exception to the above rule, +a value that parsed as a ``, +a ``, +or a `+` +instead interpolates as per the 'transform' property. + +Note: If, +for whatever reason, +a custom property is defined with a syntax of `#`, +this will thus first interpolate as a simple list, +and then each list item will interpolate as a 'transform' value. + +Note: Registering (or changing the registration) of a custom property +can change its computed value, +which can start or interrupt a CSS transition. + +Conditional Rules {#conditional-rules} +-------------------------------------- + +As stated in [[#parsing-custom-properties]], +both unregistered and [=registered=] [=custom properties=] +accept (almost) all possible values at parse-time. +[=Registered=] [=custom properties=] only apply their syntax at [=computed value=] time. + +So, all [=custom properties=], +regardless of whether they're [=registered=] or unregistered, +will test as "true" in an ''@supports'' rule, +so long as you don't violate the (very liberal) generic syntax for [=custom properties=]. + +
+ For example, + even if a custom property is registered + with syntax: "<color>";, + a rule like `@supports (--foo: 1em) {...}` + will still evaluate as true and apply those styles, + because the declaration does successfully parse as a valid property. +
+ + +Substitution via ''var()'' {#substitution} +------------------------------------------ + +Like unregistered custom properties, +the value of a registered custom property can be substituted into another value with the ''var()'' function. +However, registered custom properties substitute as their [[#calculation-of-computed-values|computed value]], +rather than the original token sequence used to produce that value. + +Any ''var()'' function that references a registered custom property +must be replaced with an equivalent token sequence, +which is equal to the token sequence that would have been produced +by [=serialize a CSS value|serializing=] the computed value, +and [[css-syntax-3#tokenization|tokenizing]] the resulting string. + +
+ Suppose that '--x' is registered with ''<length>'' syntax, + and that '--y'is an unregistered custom property. + +
+
+	div {
+		font-size: 10px;
+		--x: 8em;
+		--y: var(--x);
+	}
+	
+ + Because the computed value of '--x' (when serialized) is "80px", + the computed value of '--y' is + a <> with a value of "80" and unit "px". +
+ +### Fallbacks In ''var()'' References ### {#fallbacks-in-var-references} + +References to registered custom properties using the ''var()'' function may +provide a fallback. However, the fallback value must match the +syntax definition of the custom property being referenced, otherwise the +declaration is invalid at computed-value time. + +Note: This applies regardless of whether or not the fallback is being used. + + +### Dependency Cycles via Relative Units ### {#dependency-cycles} + +[=Registered custom properties=] follow the same rules for dependency cycle resolution +as unregistered [=custom properties=], +with the following additional constraints: + +For any registered custom property +with a <> or <> syntax component: + +* If the property contains any of the following units: + ''em'', ''ex'', ''cap'', ''ch'', ''ic'', ''lh''; + then add an edge between the property + and the ''font-size'' of the current element. +* If the property contains the ''lh'' unit, + add an edge between the property + and the ''line-height'' of the current element. +* If the property contains any of the following units: ''rem'', ''rlh''; + then add an edge between the property + and the 'font-size'' of the root element. +* If the property contains the ''rlh'' unit, + add an edge between the property + and the 'line-height'' of the root element. + +
+ For example, given this registration: + +
+	CSS.registerProperty({
+	  name: "--my-font-size",
+	  syntax: "<length>",
+	  initialValue: "0px",
+	  inherits: false
+	});
+	
+ + the following will produce a dependency cycle: + +
+	div {
+		--my-font-size: 10em;
+		font-size: var(--my-font-size);
+	}
+	
+ + and ''font-size'' will behave as if the value ''unset'' was specified. +
+ + + + + +The @property Rule {#at-property-rule} +================================================= + +The ''@property'' rule represents a [=custom property registration=] +directly in a stylesheet +without having to run any JS. +Valid ''@property'' rules result in a [=registered custom property=], +as if {{registerProperty()}} had been called with equivalent parameters. + +The syntax of ''@property'' is: + +
+	@property <> {
+		<>
+	}
+	
+ +A valid ''@property'' rule represents a [=custom property registration=], +with the property name being the serialization of the <> +in the rule's prelude. + +''@property'' rules require a 'syntax' and 'inherits' descriptor; +if either are missing, +the entire rule is invalid and must be ignored. +The 'initial-value' descriptor is optional +only if the syntax is the [=universal syntax definition=], +otherwise the descriptor is required; +if it's missing, the entire rule is invalid and must be ignored. + +Unknown descriptors are invalid and ignored, +but do not invalidate the ''@property'' rule. + +Note: As specified in [[#determining-registration]], +if multiple valid ''@property'' rules are defined for the same <>, +the last one in stylesheet order "wins". +A custom property registration from {{registerProperty()|CSS.registerProperty()}} +further wins over any ''@property'' rules +for the same <>. + +A ''@property'' is invalid if it occurs in a stylesheet inside of a [=shadow tree=], +and must be ignored. + +Issue(939): This will likely change in the future, +as the behavior of concept-defining at-rules in shadow trees +becomes more consistently defined. + +The 'syntax' Descriptor {#the-syntax-descriptor} +------------------------------------------------ + +
+	Name: syntax
+	Value: <>
+	For: @property
+	Initial: n/a (see prose)
+	
+ +Specifies the syntax of the [=custom property registration=] +represented by the ''@property'' rule, +controlling how the property's value is parsed at [=computed value=] time. + +The 'syntax' descriptor is required for the ''@property'' rule to be valid; +if it's missing, the ''@property'' rule is invalid. + +If the provided string is not a valid [=syntax string=] +(if it returns failure when [=consume a syntax definition=] is called on it), +the descriptor is invalid and must be ignored. + + +The 'inherits' Descriptor {#inherits-descriptor} +------------------------------------------------ + +
+	Name: inherits
+	Value: true | false
+	For: @property
+	Initial: n/a (see prose)
+	
+ +Specifies the inherit flag of the [=custom property registration=] +represented by the ''@property'' rule, +controlling whether or not the property inherits by default. + +The 'inherits' descriptor is required for the ''@property'' rule to be valid; +if it's missing, the ''@property'' rule is invalid. + + +The 'initial-value' Descriptor {#initial-value-descriptor} +---------------------------------------------------------- + +
+	Name: initial-value
+	Value: <>
+	For: @property
+	Initial: the [=guaranteed-invalid value=] (but see prose)
+	
+ +Specifies the initial value of the [=custom property registration=] +represented by the ''@property'' rule, +controlling the property’s [=initial value=]. + +If the value of the 'syntax' descriptor is the [=universal syntax definition=], +then the 'initial-value' descriptor is optional. +If omitted, the [=initial value=] of the property is the [=guaranteed-invalid value=]. + +Otherwise, +if the value of the 'syntax' descriptor is not the [=universal syntax definition=], +the following conditions must be met for the the ''@property'' rule to be valid: + + * The 'initial-value' descriptor must be present. + * The 'initial-value' descriptor's value must [=consume a syntax definition|parse successfully=] + according to the grammar specified by the [=syntax definition=]. + * The 'initial-value' must be [=computationally independent=]. + +If the above conditions are not met, the ''@property'' rule is invalid. + + + + +Registering Custom Properties in JS {#registering-custom-properties} +==================================================================== + +To register a custom property via JS, +the {{CSS}} object is extended with a {{registerProperty()}} method:
-dictionary PropertyDescriptor {
-  DOMString name;
-  DOMString syntax;
-  boolean   inherits;
-  DOMString initialValue;
+dictionary PropertyDefinition {
+	required DOMString name;
+	         DOMString syntax       = "*";
+	required boolean   inherits;
+	         DOMString initialValue;
 };
 
-partial interface CSS {
-  void registerProperty(PropertyDescriptor descriptor);
-  void unregisterProperty(DOMString name);
+partial namespace CSS {
+	undefined registerProperty(PropertyDefinition definition);
 };
 
-The {{PropertyDescriptor}} dictionary {#the-propertydescriptor-dictionary} +Additional, the {{Document}} object gains a new \[[registeredPropertySet]] private slot, +which is a set of records that describe registered custom properties. + +The {{registerProperty()}} Function {#the-registerproperty-function} +-------------------------------------------------------------------- + +The registerProperty(PropertyDefinition definition) method +registers a custom property according to the configuration options provided in +definition. +When it is called, +it executes the register a custom property algorithm, +passing the options in its definition argument +as arguments of the same names. + +
+ To register a custom property + with |name| being a string, + and optionally + |syntax| being a string, + |inherits| being a boolean, + and |initialValue| being a string, + execute these steps: + + 1. Let |property set| + be the value of the + current global object's + associated Document's + {{[[registeredPropertySet]]}} slot. + + 2. If |name| is not a [=custom property name string=], + throw a {{SyntaxError}} + and exit this algorithm. + + If |property set| + already contains an entry with |name| as its property name + (compared codepoint-wise), + throw an {{InvalidModificationError}} + and exit this algorithm. + + 3. Attempt to [=consume a syntax definition=] from |syntax|. + If it returns failure, throw a {{SyntaxError}}. + Otherwise, let |syntax definition| be the returned syntax definition. + + 4. If |syntax definition| is the universal syntax definition, + and |initialValue| is not present, + let |parsed initial value| be empty. + This must be treated identically to the "default" initial value of custom properties, + as defined in [[!css-variables]]. + Skip to the next step of this algorithm. + + Otherwise, + if |syntax definition| is the universal syntax definition, + [=CSS/parse=] |initialValue| as a <>. + If this fails, + throw a {{SyntaxError}} + and exit this algorithm. + Otherwise, + let |parsed initial value| be the parsed result. + Skip to the next step of this algorithm. + + Otherwise, if |initialValue| is not present, + throw a {{SyntaxError}} + and exit this algorithm. + + Otherwise, + [=CSS/parse=] {{PropertyDefinition/initialValue}} + according to |syntax definition|. + If this fails, + throw a {{SyntaxError}} + and exit this algorithm. + + Otherwise, let |parsed initial value| be the parsed result. + If |parsed initial value| is not computationally independent, + throw a {{SyntaxError}} + and exit this algorithm. + + 5. Set |inherit flag| to the value of |inherits|. + + 6. Let |registered property| be a [=struct=] + with a property name of |name|, + a syntax of |syntax definition|, + an initial value of |parsed initial value|, + and an inherit flag of |inherit flag|. + [=set/Append=] |registered property| + to |property set|. +
+ +A property value is computationally independent +if it can be converted into a computed value +using only the value of the property on the element, +and "global" information that cannot be changed by CSS. + +
+ For example, ''5px'' is computationally independent, + as converting it into a computed value doesn't change it at all. + Similarly, ''1in'' is computationally independent, + as converting it into a computed value + relies only on the "global knowledge" that ''1in'' is ''96px'', + which can't be altered or adjusted by anything in CSS. + + On the other hand, ''3em'' is not computationally independent, + because it relies on the value of 'font-size' on the element + (or the element's parent). + Neither is a value with a ''var()'' function, + because it relies on the value of a custom property. +
+ +When a custom property is registered with a given type, +the process via which specified values for that property are turned into computed values +is defined fully by the type selected, +as described in [[#calculation-of-computed-values]]. + + Note: A way to unregister properties may be added in the future. + +Registering a custom property must not affect the [=cascade=] in any way. +Regardless of what syntax is specified for a registered property, +at parse time it is still parsed as normal for a [=custom property=], +accepting nearly anything. +If the [=specified value=] for a [=registered custom property=] +violates the registered syntax, +however, +the property becomes [=invalid at computed-value time=] +(and thus resets to the registered initial value). + +
+ By default, all custom property declarations that can be parsed as a sequence of tokens + are valid. Hence, the result of this stylesheet: + +
+	.thing {
+		--my-color: green;
+		--my-color: url("not-a-color");
+		color: var(--my-color);
+	}
+	
+ + is to set the 'color' property of elements of class "thing" to ''inherit''. + The second '--my-color' declaration overrides the first at parse time (both are valid), + and the ''var()'' reference in the 'color' property is found to be invalid at computed-value time + (because ''url("not-a-color")'' is not a color). + At this stage of the CSS pipeline (computation time), + the only available fallback is the initial value of the property, + which in the case of color is ''inherit''. + Although there was a valid usable value (green), + this was removed during parsing because it was superseded by the URL. + + If we call: + +
+	CSS.registerProperty({
+		name: "--my-color",
+		syntax: "<color>",
+		initialValue: "black",
+		inherits: false
+	});
+	
+ + the parsing doesn't significantly change, + regardless of whether the registration occurs before or after the stylesheet above. + The only difference is that it's the '--my-color' property that becomes [=invalid at computed-value time=] instead + and gets set to its initial value of ''black''; + then 'color' is validly set to ''black'', + rather than being [=invalid at computed-value time=] + and becoming ''inherit''. +
+ +The {{PropertyDefinition}} Dictionary {#the-propertydefinition-dictionary} -------------------------------------------------------------------------- -A PropertyDescriptor dictionary represents author-specified configuration -options for a custom property. {{PropertyDescriptor}} dictionaries contain the +A PropertyDefinition dictionary represents author-specified configuration +options for a custom property. {{PropertyDefinition}} dictionaries contain the following members: -: name +: name :: The name of the custom property being defined. -: syntax +: syntax :: A string representing how this custom property is parsed. -: inherits +: inherits :: True if this custom property should inherit down the DOM tree; False otherwise. -: initialValue +: initialValue :: The initial value of this custom property. -The {{registerProperty()}} function {#the-registerproperty-function} --------------------------------------------------------------------- - -The registerProperty(PropertyDescriptor descriptor) method -registers a custom property according the to configuration options provided in -descriptor. - -Attempting to register properties with a {{PropertyDescriptor/name}} that doesn't -correspond to the <> production must cause {{registerProperty()}} -to throw a {{SyntaxError}}. - -The list of types supported in the {{PropertyDescriptor/syntax}} member are listed -in . Currently, only simple -type references are supported. Attempting to register properties with a -{{PropertyDescriptor/syntax}} that is not supported must cause {{registerProperty()}} -to throw a {{SyntaxError}}. - -Note: for example, the syntax string could be "<length>" or "<number>". -Note: in future levels we anticipate supporting more sophisticated parse strings, e.g. -"<length> || <number>" + -Attempting to call {{registerProperty()}} with an {{PropertyDescriptor/initialValue}} that is -not parseable using the provided {{PropertyDescriptor/syntax}} must cause it to -throw a {{SyntaxError}}. +Syntax Strings {#syntax-strings} +================================ -When a custom property is registered with a given type, the process via which specified -values for that property are turned into computed values is defined -fully by the type selected, as described in -. +A syntax string describes the value types accepted by a registered +custom property. Syntax strings consists of +[=syntax component names=], that are +optionally [[#multipliers|multiplied]] and [[#combinator|combined]]. -Note: As defined by [[css3-animations]] and [[css3-transitions]], it is possible to -specify animations and transitions that reference custom properties. +A syntax string can be parsed into a syntax definition, which is either: -When referenced by animations and transitions, custom properties will interpolate -in a manner defined by their types. -If the start and end of an interpolation have matching types, then they -will interpolate as specified in [[!css3-animations]] or the corresponding property. -Otherwise, the interpolation falls back to the default 50% flip described in -[[!css3-animations]]. + 1. A list of syntax components, each of which accept the value types + specified in [[#supported-names]], or + 2. The universal syntax definition ('*'), which accepts any valid token + stream. -If {{registerProperty()}} is called with a descriptor name that matches an already registered property, -then an {{InvalidModificationError}} is thrown and the re-registration fails. +Note: Regardless of the syntax specified, all custom properties accept +CSS-wide keywords, and process these values +appropriately. -Properties can be unregistered using -unregisterProperty(DOMString name). -If this function is called with a name that doesn't match an existing property -then a {{NotFoundError}} is thrown. +
+ For example, the following are all valid syntax strings. + + : "<length>" + :: accepts length values + : "<length> | <percentage>" + :: accepts lengths, percentages, percentage calc expressions, and length calc + expressions, but not calc expressions containing a combination of length + and percentage values. + : "<length-percentage>" + :: accepts all values that "<length> | <percentage>" would + accept, as well as calc expressions containing a combination of both length + and percentage values. + : "big | bigger | BIGGER" + :: accepts the ident big, or the ident bigger, or + the ident BIGGER. + : "<length>+" + :: accepts a space-separated list of length values. + : "*" + :: accepts any valid token stream +
-Successful calls to both {{registerProperty()}} and {{unregisterProperty()}} -trigger a reparse of the specified value of the newly registered or unregistered -property, followed by an invalidation of the computed style of all {{document}}s -created on the in-scope {{Window}}. +Note: The internal grammar of syntax strings is a subset of +[[css-values-3#value-defs|the CSS Value Definition Syntax]]. Future levels of this specification are expected +to expand the complexity of the allowed grammar, allowing custom properties +that more closely resemble the full breadth of what CSS properties allow. -Issue(63): Phrasing? How do I write this correctly? +The remainder of this chapter describes the internal grammar of the syntax +strings. -Supported syntax strings {#supported-syntax-strings} ----------------------------------------------------- +Supported Names {#supported-names} +---------------------------------- -The following syntax strings are supported: +This section defines the supported syntax component names, and the +corresponding types accepted by the resulting syntax component. : "<length>" :: Any valid <> value @@ -166,328 +865,437 @@ The following syntax strings are supported: :: Any valid <> value : "<length-percentage>" :: Any valid <> or <> value, any valid <> - expression combining <> and <> components. + expression combining <> and <> components. +: "<color>" +:: Any valid <> value +: "<image>" +:: Any valid <> value +: "<url>" +:: Any valid <> value +: "<integer>" +:: Any valid <> value +: "<angle>" +:: Any valid <> value +: "<time>" +:: Any valid <
-[Constructor(LengthValue x, LengthValue y)] -interface PositionValue : StyleValue { - readonly attribute LengthValue x; - readonly attribute LengthValue y; -}; +
+ To determine whether two {{CSSNumericValue}}s |value1| and |value2| + are equal numeric values, + perform the following steps: -
+ 1. If |value1| and |value2| are not members of the same interface, + return `false`. -{{PositionValue}} objects represent values for properties that take <> -productions, for example 'background-position'. + 2. If |value1| and |value2| are both {{CSSUnitValue}}s, + return `true` if they have equal {{CSSUnitValue/unit}} and {{CSSUnitValue/value}} internal slots, + or `false` otherwise. -The x attribute contains the position offset -from the left edge of the container, expressed as a length. + 3. If |value1| and |value2| are both + {{CSSMathSum}}s, {{CSSMathProduct}}s, {{CSSMathMin}}s, or {{CSSMathMax}}s: -The y attribute contains the position offset -from the top edge of the container, expressed as a length. + 1. If |value1|’s {{CSSMathSum/values}} and |value2|s {{CSSMathSum/values}} internal slots + have different [=list/sizes=], + return `false`. -Note that <> productions accept a complicated combination of keywords -and values. When specified as such in a stylesheet or via the untyped CSSOM, -the cssString attribute will contain the specified -string. However, this string is normalized as two Lengths into the x and y values of the -{{StyleValue}} object. + 2. If any [=list/item=] in |value1|'s {{CSSMathSum/values}} internal slot + is not an [=equal numeric value=] + to the [=list/item=] in |value2|’s {{CSSMathSum/values}} internal slot + at the same index, + return `false`. -New {{PositionValue}} objects can only be constructed via pairs of lengths, and -will only return the direct serialization of these lengths in the -cssString attribute. + 3. Return `true`. -
+ 4. Assert: |value1| and |value2| are both {{CSSMathNegate}}s or {{CSSMathInvert}}s. -For example, the following style sheet: + 5. Return whether |value1|’s {{CSSMathNegate/value}} + and |value2|’s {{CSSMathNegate/value}} + are [=equal numeric values=]. +
-
-.example {
-  background-position: center bottom 10px;
-}
-
+
+ The to(|unit|) method converts an existing {{CSSNumericValue}} |this| + into another one with the specified |unit|, + if possible. + When called, it must perform the following steps: -Will produce the following behavior: + 1. Let |type| be the result of [=creating a type=] from |unit|. + If |type| is failure, + [=throw=] a {{SyntaxError}}. -
-// "center bottom 10px"
-document.querySelector('.example').styleMap.get('background-position').cssString;
+    2. Let |sum| be the result of [=creating a sum value=] from |this|.
+        If |sum| is failure,
+        [=throw=] a {{TypeError}}.
 
-// 50% - as a SimpleLength
-document.querySelector('.example').styleMap.get('background-position').x;
+    3. If |sum| has more than one [=list/item=],
+        [=throw=] a {{TypeError}}.
+        Otherwise, let |item| be the result of [=create a CSSUnitValue from a sum value item|creating a CSSUnitValue=]
+        from the sole [=list/item=] in |sum|,
+        then [=convert a CSSUnitValue|converting=] it to |unit|.
+        If |item| is failure,
+        [=throw=] a {{TypeError}}.
 
-// calc(100% - 10px) - as a CalcLength
-document.querySelector('.example').styleMap.get('background-position').y;
-
+ 4. Return |item|. +
+ +
+ When asked to create a CSSUnitValue from a sum value item |item|, + perform the following steps: + + 1. If |item| has more than one [=map/entry=] in its [=sum value/unit map=], + return failure. + + 2. If |item| has no [=map/entries=] in its [=sum value/unit map=], + return a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot + is set to "number", + and whose {{CSSUnitValue/value}} internal slot + is set to |item|’s [=sum value/value=]. + + 3. Otherwise, |item| has a single [=map/entry=] in its [=sum value/unit map=]. + If that [=map/entry’s=] [=map/value=] is anything other than `1`, + return failure. + 4. Otherwise, return a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot + is set to that [=map/entry’s=] [=map/key=], + and whose {{CSSUnitValue/value}} internal slot + is set to |item|’s [=sum value/value=].
-Mapping of properties to accepted types -======================================= -This section provides a table of which types of {{StyleValue}} a given property can accept. -Note that most, but not all properties take {{KeywordValue}}. -Shorthand properties and values are not supported. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropertyAllowable {{StyleValue}} types
'align-content'{{KeywordValue}}
'align-items'{{KeywordValue}}
'align-self'{{KeywordValue}}
'alignment-baseline'{{KeywordValue}}
'all'{{KeywordValue}}
'alt'StringValue | {{KeywordValue}}
'animation-delay'TimeValue | {{KeywordValue}}
'animation-direction'{{KeywordValue}}
'animation-fill-mode'{{KeywordValue}}
'animation-iteration-count'{{NumberValue}} | {{KeywordValue}}
'animation-name'CustomIdentValue | {{KeywordValue}}
'animation-play-state'{{KeywordValue}}
'animation-timing-function'TransitionTimingFunctionValue | {{KeywordValue}}
'appearance'{{KeywordValue}}
'background-attachment'{{KeywordValue}}
'background-blend-mode'{{KeywordValue}}
'background-clip'{{KeywordValue}}
'background-color'ColorValue | {{KeywordValue}}
'background-image'URIValue | {{KeywordValue}}
'background-origin'{{KeywordValue}}
'background-position'{{PositionValue}} | {{KeywordValue}}
'background-repeat'{{KeywordValue}}
'background-size'PairValue<{{LengthValue}}> (or SizeValue?)| {{KeywordValue}}
'baseline-shift'{{LengthValue}} | {{KeywordValue}}
'border-boundary'{{KeywordValue}}
'border-collapse'{{KeywordValue}}
'border-color'ColorValue | {{KeywordValue}}
'border-top-color' 'border-right-color' 'border-bottom-color' 'border-left-color'ColorValue | {{KeywordValue}}
'border-image-outset'FourValues<{{LengthValue}}|{{NumberValue}}>
'border-image-repeat'PairValue<{{KeywordValue}}>
'border-image-slice'BorderImageSliceValue
'border-image-source'URIValue | {{KeywordValue}}
'border-image-width'FourValues<{{LengthValue}}|{{NumberValue}}|{{KeywordValue}}>
'border-top-style' 'border-right-style' 'border-bottom-style' 'border-left-style'{{KeywordValue}}
'border-top-right-radius' 'border-bottom-right-radius' 'border-bottom-left-radius' 'border-top-left-radius'PairValue<{{LengthValue}}> | {{KeywordValue}}
'border-top-width' 'border-right-width' 'border-bottom-width' 'border-left-width'{{LengthValue}} | {{KeywordValue}}
'bottom'{{LengthValue}} | {{KeywordValue}}
'caption-side'{{KeywordValue}}
'clear'{{KeywordValue}}
'clip'ShapeValue | {{KeywordValue}}
'color'ColorValue | {{KeywordValue}}
'content'StringValue | URIValue | CounterValue | AttrValue | {{KeywordValue}}
'counter-increment' 'counter-reset'CounterValue | {{KeywordValue}}
'cue-after' 'cue-before'URIValue | {{KeywordValue}}
'cursor'URIValue | {{KeywordValue}}
'direction'{{KeywordValue}}
'display'{{KeywordValue}}
'empty-cells'{{KeywordValue}}
'float'{{KeywordValue}}
'font-family'{{KeywordValue}}
'font-size'{{LengthValue}} | {{KeywordValue}}
'font-style'{{KeywordValue}}
'font-variant'{{KeywordValue}}
'font-weight'FontWeightValue | {{KeywordValue}}
'height'{{LengthValue}} | {{KeywordValue}}
'left'{{LengthValue}} | {{KeywordValue}}
'letter-spacing'{{LengthValue}} | {{KeywordValue}}
'line-height'{{LengthValue}} | {{NumberValue}} | {{KeywordValue}}
'list-style-image'URIValue | {{KeywordValue}}
'list-style-position'{{KeywordValue}}
'list-style-type'{{KeywordValue}}
'margin-top' 'margin-right' 'margin-bottom' 'margin-left'{{LengthValue}} | {{KeywordValue}}
'max-height' 'max-width' 'min-height' 'min-width'{{LengthValue}} | {{KeywordValue}}
'orphans'{{NumberValue}} | {{KeywordValue}}
'outline-color'ColorValue | {{KeywordValue}}
'outline-style'{{KeywordValue}}
'outline-width'{{LengthValue}} | {{KeywordValue}}
'overflow'{{KeywordValue}}
'padding-top' 'padding-right' 'padding-bottom' 'padding-left'{{LengthValue}} | {{KeywordValue}}
'page-break-after' 'page-break-before'{{KeywordValue}}
'page-break-inside'{{KeywordValue}}
'pause-after' 'pause-before'TimeValue | PercentageValue | {{KeywordValue}}
'pitch-range'{{NumberValue}} | {{KeywordValue}}
'pitch'FrequencyValue | {{KeywordValue}}
'play-during'PlayDuringValue | {{KeywordValue}}
'position'{{KeywordValue}}
'quotes'PairValue | {{KeywordValue}}
'richness'{{NumberValue}} | {{KeywordValue}}
'right'{{LengthValue}} | {{KeywordValue}}
'speak-header'{{KeywordValue}}
'speak-numeral'{{KeywordValue}}
'speak-punctuation'{{KeywordValue}}
'speak'{{KeywordValue}}
'speech-rate'{{NumberValue}} | {{KeywordValue}}
'stress'{{NumberValue}} | {{KeywordValue}}
'table-layout'{{KeywordValue}}
'text-align'{{KeywordValue}}
'text-decoration'{{KeywordValue}}
'text-indent'{{LengthValue}} | {{KeywordValue}}
'text-transform'{{KeywordValue}}
'top'{{LengthValue}} | {{KeywordValue}}
'unicode-bidi'{{KeywordValue}}
'vertical-align'{{LengthValue}} | {{KeywordValue}}
'visibility'{{KeywordValue}}
'voice-family'VoiceValue | {{KeywordValue}}
'volume'{{NumberValue}} | PercentageValue | {{KeywordValue}}
'white-space'{{KeywordValue}}
'widows'{{NumberValue}} | {{KeywordValue}}
'width'{{LengthValue}} | {{KeywordValue}}
'word-spacing'{{LengthValue}} | {{KeywordValue}}
'z-index'{{NumberValue}} | {{KeywordValue}}
- -Issue: Spec up ColorValue, URIValue, ShapeValue, StringValue, CounterValue, TimeValue, PercentageValue, FrequencyValue, VoiceValue, CustomIdentValue, TransitionTimingFunctionValue. What is attr() in other specs? - -Issue: Do we want to make a generic PairValue and QuadValue, or have more specific classes for background-size, border-image-repeat, border-radius-*, border-image-outset, border-image-width? - -Issue: BorderImageSliceValue represents [ | ]{1,4} && fill? - -Issue: What do we do for play-during, which takes [ mix || repeat ]? - -Issue: Do we want a pair type for things like quotes which take [ ] - -Issue: Do we need to say that LengthValues must be positive e.g. for border-widths here? - -Issue: Do we want a font value class? How about a FontWeightValue (for 100, 200 etc) class? - -Issue: Add more CSS3 properties. This table currently only contains CSS2.1 properties and CSS3 properties alphabetically to border-radius. - -{{StyleValue}} normalization {#stylevalue-normalization} -======================================================== - -Issue: write me +
+ The toSum(...|units|) method converts an existing {{CSSNumericValue}} |this| + into a {{CSSMathSum}} of only {{CSSUnitValue}}s with the specified units, + if possible. + (It's like {{CSSNumericValue/to()}}, + but allows the result to have multiple units in it.) + If called without any units, + it just simplifies |this| into a minimal sum of {{CSSUnitValue}}s. + + When called, it must perform the following steps: + + 1. [=list/For each=] |unit| in |units|, + if the result of [=creating a type=] from |unit| is failure, + [=throw=] a {{SyntaxError}}. + + 2. Let |sum| be the result of [=creating a sum value=] from |this|. + If |sum| is failure, + [=throw=] a {{TypeError}}. + + 3. Let |values| be the result of [=create a CSSUnitValue from a sum value item|creating a CSSUnitValue=] + [=list/for each=] [=list/item=] in |sum|. + If any [=list/item=] of |values| is failure, + [=throw=] a {{TypeError}}. + + 4. If |units| is [=list/empty=], + sort |values| in [=code point=] order according to the {{CSSUnitValue/unit}} internal slot of its [=list/items=], + then return a new {{CSSMathSum}} object + whose {{CSSMathSum/values}} internal slot is set to |values|. + + 5. Otherwise, + let |result| initially be an empty [=list=]. + [=list/For each=] |unit| in |units|: + + 1. Let |temp| initially be a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot + is set to |unit| + and whose {{CSSUnitValue/value}} internal slot + is set to `0`. + + 2. [=list/For each=] |value| in |values|: + + 1. Let |value unit| be |value|’s {{CSSUnitValue/unit}} internal slot. + + 2. If |value unit| is a [=compatible unit=] with |unit|, + then: + + 1. [=convert a CSSUnitValue|Convert=] |value| to |unit|. + 2. Increment |temp|’s {{CSSUnitValue/value}} internal slot + by the value of |value|’s {{CSSUnitValue/value}} internal slot. + 3. [=list/Remove=] |value| from |values|. + + 3. [=list/Append=] |temp| to |result|. + + 6. If |values| is not [=list/empty=], + [=throw=] a {{TypeError}}. + |this| had units that you didn't ask for. + + 7. Return a new {{CSSMathSum}} object + whose {{CSSMathSum/values}} internal slot + is set to |result|. +
+ +
+ The type() method + returns a representation of the [=type=] of |this|. + + When called, it must perform the following steps: + + 1. Let |result| be a new {{CSSNumericType}}. + + 2. For each |baseType| → |power| in the [=type=] of |this|, + 1. If |power| is not 0, + set |result|[|baseType|] to |power|. + + 3. If the [=percent hint=] of |this| is not null, + 1. Set {{CSSNumericType/percentHint}} to the [=percent hint=] of |this|. + + 4. Return |result|. +
+ +
+ A sum value + is an abstract representation of a {{CSSNumericValue}} + as a sum of numbers with (possibly complex) units. + Not all {{CSSNumericValue}}s can be expressed as a [=sum value=]. + + A [=sum value=] is a [=list=]. + Each entry in the list is a [=tuple=] of a value, + which is a number, + and a unit map, + which is a [=ordered map|map=] of units (strings) to powers (integers). + +
+ Here are a few examples of CSS values, + and their equivalent [=sum values=]: + + * ''1px'' becomes `«(1, «["px" → 1]»)»` + * ''calc(1px + 1in)'' becomes `«(97, «["px" → 1]»)»` + (because ''in'' and ''px'' are [=compatible units=], + and ''px'' is the [=canonical unit=] for them) + * ''calc(1px + 2em)'' becomes `«(1, «["px" → 1]»), (2, «["em" → 1]»)»` + * ''calc(1px + 2%)'' becomes `«(1, «["px" → 1]»), (2, «["percent" → 1]»)»` + (percentages are allowed to add to other units, + but aren't resolved into another unit, + like they are in a [=type=]) + * ''calc(1px * 2em)'' becomes `«(2, «["em" → 1, "px" → 1]»)»` + * ''calc(1px + 1deg)'' can't be represented as a [=sum value=] + because it's an invalid computation + * ''calc(1px * 2deg)'' becomes `«(2, «["deg" → 1, "px" → 1]»)»` +
+ + To create a sum value from a {{CSSNumericValue}} |this|, + the steps differ based on |this|’s class: + +
+ : {{CSSUnitValue}} + :: +
+ 1. Let |unit| be the value of |this|’s {{CSSUnitValue/unit}} internal slot, + and |value| be the value of |this|’s {{CSSUnitValue/value}} internal slot. + 2. If |unit| is a member of a set of [=compatible units=], + and is not the set's [=canonical unit=], + multiply |value| by the conversion ratio between |unit| and the [=canonical unit=], + and change |unit| to the [=canonical unit=]. + 3. If |unit| is `"number"`, + return «(|value|, «[ ]»)». + 3. Otherwise, return «(|value|, «[|unit| → 1]»)». +
+ + : {{CSSMathSum}} + :: +
+ 1. Let |values| initially be an empty [=list=]. + + 2. [=list/For each=] |item| in this’s {{CSSMathSum/values}} internal slot: + + 1. Let |value| be the result of [=creating a sum value=] from |item|. + If |value| is failure, + return failure. + + 2. [=list/For each=] |subvalue| of |value|: + + 1. If |values| already contains an [=list/item=] + with the same [=sum value/unit map=] as |subvalue|, + increment that [=list/item=]’s [=sum value/value=] + by the [=sum value/value=] of |subvalue|. + + 2. Otherwise, [=list/append=] |subvalue| to |values|. + + 3. [=create a type from a unit map|Create a type=] + from the [=sum value/unit map=] + of each [=list/item=] of |values|, + and [=add=] all the types together. + If the result is failure, + return failure. + + 4. Return |values|. +
+ + : {{CSSMathNegate}} + :: +
+ 1. Let |values| be the result of [=creating a sum value=] + from this’s {{CSSMathNegate/value}} internal slot. + + 2. If |values| is failure, + return failure. + + 3. Negate the [=sum value/value=] of each [=list/item=] of |values|. + + 4. Return |values|. +
+ + : {{CSSMathProduct}} + :: +
+ 1. Let |values| initially be the [=sum value=] «(1, «[ ]»)». + (I.e. what you'd get from ''1''.) + + 2. [=list/For each=] |item| in this’s {{CSSMathProduct/values}} internal slot: + + 1. Let |new values| be the result of [=creating a sum value=] from |item|. + Let |temp| initially be an empty [=list=]. + + 2. If |new values| is failure, + return failure. + + 3. [=list/For each=] |item1| in |values|: + + 1. [=list/For each=] |item2| in |new values|: + + 1. Let |item| be a [=tuple=] with its [=sum value/value=] + set to the product of the [=sum value/values=] of |item1| and |item2|, + and its [=sum value/unit map=] + set to the [=product of two unit maps|product=] of the [=sum value/unit maps=] of |item1| and |item2|, + with all [=map/entries=] with a zero value removed. + + 2. Append |item| to |temp|. + + 4. Set |values| to |temp|. + + 3. Return |values|. +
+ + : {{CSSMathInvert}} + :: +
+ 1. Let |values| be the result of [=creating a sum value=] + from this’s {{CSSMathInvert/value}} internal slot. + + 2. If |values| is failure, + return failure. + + 3. If the length of [=values=] is more than one, + return failure. + + 3. Invert (find the reciprocal of) the [=sum value/value=] of the [=list/item=] in |values|, + and negate the [=map/value=] of each [=map/entry=] in its [=sum value/unit map=]. + + 4. Return |values|. +
+ + : {{CSSMathMin}} + :: +
+ 1. Let |args| be the result of [=creating a sum value=] + [=list/for each=] [=list/item=] in this’s {{CSSMathMin/values}} internal slot. + + 2. If any [=list/item=] of |args| is failure, + or has a length greater than one, + return failure. + + 3. If not all of the [=sum value/unit maps=] among the [=list/items=] of |args| are identical, + return failure. + + 4. Return the [=list/item=] of |args| whose sole [=list/item=] has the smallest [=sum value/value=]. +
+ + : {{CSSMathMax}} + :: +
+ 1. Let |args| be the result of [=creating a sum value=] + [=list/for each=] [=list/item=] in this’s {{CSSMathMax/values}} internal slot. + + 2. If any [=list/item=] of |args| is failure, + or has a length greater than one, + return failure. + + 3. If not all of the [=sum value/unit maps=] among the [=list/items=] of |args| are identical, + return failure. + + 4. Return the [=list/item=] of |args| whose sole [=list/item=] has the largest [=sum value/value=]. +
+
+
+ +
+ To create a type from a unit map |unit map|: + + 1. Let |types| be an initially empty [=list=]. + + 2. [=map/For each=] |unit| → |power| in |unit map|: + + 1. Let |type| be the result of [=creating a type=] from |unit|. + 2. Set |type|’s sole [=map/value=] to |power|. + 3. [=list/Append=] |type| to |types|. + + 3. Return the result of [=CSSNumericValue/multiplying=] all the [=list/items=] of |types|. +
+ +
+ The product of two unit maps |units1| and |units2| + is the result given by the following steps: + + 1. Let |result| be a copy of |units1|. + + 2. [=map/For each=] |unit| → |power| in |units2|: + + 1. If |result|[|unit|] [=map/exists=], + increment |result|[|unit|] by |power|. + 3. Otherwise, set |result|[|unit|] to |power|. + + 3. Return |result|. +
+ +The {{CSSNumericValue/parse()}} method allows a {{CSSNumericValue}} +to be constructed directly from a string containing CSS. +Note that this is a static method, +existing directly on the {{CSSNumericValue}} interface object, +rather than on {{CSSNumericValue}} instances. + +
+ The parse(|cssText|) method, + when called, + must perform the following steps: + + 1. [=Parse a component value=] from |cssText| + and let |result| be the result. + If |result| is a syntax error, + [=throw=] a {{SyntaxError}} + and abort this algorithm. + + 2. If |result| is not a <>, <>, <>, + or a [=math function=], + [=throw=] a {{SyntaxError}} + and abort this algorithm. + + 3. [=Reify a numeric value=] |result|, + and return the result. +
+ + + +### Numeric Value Typing ### {#numeric-typing} + +Each {{CSSNumericValue}} has an associated type, +which is a [=ordered map|map=] of [=base types=] to integers, +and an associated [=percent hint=]. +The base types are +"length", +"angle", +"time", +"frequency", +"resolution", +"flex", +and "percent". +The ordering of a [=type=]’s entries always matches this [=base type=] ordering. +The percent hint +is either null or a [=base type=] other than "percent". + +Note: As new unit types are added to CSS, +they'll be added to this list of [=base types=], +and to the CSS [=math functions=]. + +
+ To create a type from a string |unit|, + follow the appropriate branch of the following: + +
+ : |unit| is "number" + :: Return «[ ]» (empty map) + : |unit| is "percent" + :: Return «[ "percent" → 1 ]» + : |unit| is a <> unit + :: Return «[ "length" → 1 ]» + : |unit| is an <> unit + :: Return «[ "angle" → 1 ]» + : |unit| is a <
+ + In all cases, the associated [=percent hint=] is null. +
+ +
+ To add two types |type1| and |type2|, + perform the following steps: + + 1. Replace |type1| with a fresh copy of |type1|, + and |type2| with a fresh copy of |type2|. + Let |finalType| be a new [=type=] + with an initially empty [=ordered map=] + and an initially null [=percent hint=]. + + 2. +
+ : If both |type1| and |type2| have non-null [=percent hints=] + with different values + :: The types can't be added. + Return failure. + + : If |type1| has a non-null [=percent hint=] |hint| and |type2| doesn't + :: [=Apply the percent hint=] |hint| to |type2|. + + Vice versa if |type2| has a non-null [=percent hint=] and |type1| doesn't. + + : Otherwise + :: Continue to the next step. +
+ + + 3. +
+ : If all the [=map/entries=] of |type1| with non-zero values + are [=map/contained=] in |type2| with the same value, + and vice-versa + :: Copy all of |type1|’s [=map/entries=] to |finalType|, + and then copy all of |type2|’s [=map/entries=] to |finalType| + that |finalType| doesn't already [=map/contain=]. + Set |finalType|’s [=percent hint=] to |type1|’s [=percent hint=]. + Return |finalType|. + + : If |type1| and/or |type2| [=map/contain=] "percent" with a non-zero value, + and |type1| and/or |type2| [=map/contain=] a key *other than* "percent" with a non-zero value + :: For each [=base type=] other than "percent" |hint|: + + 1. Provisionally [=apply the percent hint=] |hint| to both |type1| and |type2|. + + 2. If, afterwards, all the [=map/entries=] of |type1| with non-zero values + are [=map/contained=] in |type2| with the same value, + and vice versa, + then copy all of |type1|’s [=map/entries=] to |finalType|, + and then copy all of |type2|’s [=map/entries=] to |finalType| + that |finalType| doesn't already [=map/contain=]. + Set |finalType|’s [=percent hint=] to |hint|. + Return |finalType|. + + 3. Otherwise, revert |type1| and |type2| to their state at the start of this loop. + + If the loop finishes without returning |finalType|, + then the types can't be added. + Return failure. + + Note: You can shortcut this in some cases + by just checking the sum of all the [=map/values=] + of |type1| vs |type2|. + If the sums are different, + the types can't be added. + + : Otherwise + :: The types can't be added. + Return failure. +
+
+ +
+ To apply the percent hint |hint| to a |type|, + perform the following steps: + + 1. If |type| doesn't [=map/contain=] |hint|, set |type|[|hint|] to 0. + 2. If |type| [=map/contains=] "percent", add |type|["percent"] to |type|[|hint|], + then set |type|["percent"] to 0. + 4. Set |type|’s [=percent hint=] to |hint|. +
+ +
+ To multiply two types |type1| and |type2|, + perform the following steps: + + 1. Replace |type1| with a fresh copy of |type1|, + and |type2| with a fresh copy of |type2|. + Let |finalType| be a new [=type=] + with an initially empty [=ordered map=] + and an initially null [=percent hint=]. + + 2. If both |type1| and |type2| have non-null [=percent hints=] + with different values, + the types can't be multiplied. + Return failure. + + 3. If |type1| has a non-null [=percent hint=] |hint| and |type2| doesn't, + [=apply the percent hint=] |hint| to |type2|. + + Vice versa if |type2| has a non-null [=percent hint=] and |type1| doesn't. + + 4. Copy all of |type1|’s [=map/entries=] to |finalType|, + then [=map/for each=] |baseType| → |power| of |type2|: + + 1. If |finalType|[|baseType|] [=map/exists=], + increment its value by |power|. + 2. Otherwise, set |finalType|[|baseType|] to |power|. + + Set |finalType|’s [=percent hint=] to |type1|’s [=percent hint=]. + + 5. Return |finalType|. +
+ +
+ To invert a type |type|, + perform the following steps: + + 1. Let |result| be a new [=type=] + with an initially empty [=ordered map=] + and an initially null [=percent hint=] + 1. [=map/For each=] |unit| → |exponent| of |type|, + set |result|[|unit|] to (-1 * |exponent|). + 3. Return |result|. +
+ +A [=type=] is said to match a CSS production in some circumstances: + +* A [=type=] matches <> + if its only non-zero [=map/entry=] is «[ "length" → 1 ]» + and its [=percent hint=] is null. + Similarly for <>, <