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 d76238ed..00000000
--- a/composited-scrolling-and-animation/Explainer.md
+++ /dev/null
@@ -1,188 +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.
-
-Updating CSS properties could effect a style recalc or a layout and those
-operations happen on the main thread in most user agents making them poor
-candidates for asynchronous update. That said, there are certain properties that
-can be modified asynchronously by all user agents today. These properties include
-transform, opacity, and scroll offset. Clearly identifying these 'accelerated'
-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
-accelerated properties for years. Some examples include WebAnimations, CSS
-animations, and transitions involving accelerated 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 realize these effects.
-
-## Goal
-
-Allow new accelerated, input/time-based effects to be authored in script.
-Address at least [this](https://github.com/w3c/css-houdini-drafts/blob/master/composited-scrolling-and-animation/UseCases.md) list of use cases as well as supporting [these scroll customization use cases](https://github.com/w3c/css-houdini-drafts/blob/master/scroll-customization-api/UseCases.md).
-
-## 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 accelerated 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
-
-The following examples are written to an API shape that is likely to change in the future. The goal here is to cleary articulate the concepts, not to propose final syntax.
-
-### 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?
-
-In particular, two worries are
- 1. The subset of accelerated properties we expose to a CompositorWorker won't be valid in the future.
- 2. We expose browser internals in such a way that it constrains future development.
- 
-To the first point, virtually all user agents support accelerated opacity and transform animations, as well as threaded scrolling. Since the web relies on the performance characteristics of these properties, it's very unlikely that they will become slow. So the real concern is the ability to add new properties. As long as we choose an extensible API shape that permits simple feature detection, adding support for more properties in the future will be possible.
-
-To the second point, we need not mention browser internals to permit asynchronous updates of accelerated properties. The examples above don’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 or processed in software. But this doesn’t matter. These implementation details are orthogonal to the CompositorWorker 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/composited-scrolling-and-animation/UseCases.md b/composited-scrolling-and-animation/UseCases.md
deleted file mode 100644
index 70cf1fd1..00000000
--- a/composited-scrolling-and-animation/UseCases.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# Compositor Scrolling and Animation Use Cases
-
-Parallax
------
-- the position of elements on the page is related to the scroll position of their container (or maybe another container)
-- not a direct link between scroll offset and position. Rather it is some factor, possibly with damping or a curve.
-- postion is the most common output, but it could also be opacity or a filter effect such as blur (or really any rendering property)
-
-Scroll header
-----
-- A header at the top of the document
-- When approaching scrollTop=0 header smoothly animates into a more substantial one. Eg. images, opacity, text size animate with scroll position.
-- May also only be shown when scrolling down (like hidey bars)
-- Examples:
-  - The top bar on [Twitter user pages](https://twitter.com/LEGO_Group)
-  - Polymer's [core-scroll-header-panel](http://polymer.github.io/core-scroll-header-panel/components/core-scroll-header-panel/demos/demo9.html) and [paper-scroll-header-panel](https://elements.polymer-project.org/elements/paper-scroll-header-panel?view=demo:demo/index.html)
-
-Video sync
------
-- Video whose time point is determined as a function of the scroll offset.
-- Must be synced perfectly with scrolling, eg. scrolling down one pixel may advance the video by one frame, and that video frame may move everything up one pixel to counter the scroll.  To the user it must appear as if the content didn't move with the scroll.
-
-Linked scrollers
------
-- Multiple elements are to be scrolled together but possibly at different rates
-- Like parallax except not overlapping, use input may occur on any of the linked elements
-- One example [here](http://stackoverflow.com/questions/19786080/how-to-synchronize-scroll-between-two-elements-with-different-height).  [AV club](http://www.avclub.com/review/weeknd-navigates-trippy-perception-and-pop-reality-224412) also appears to do this.
-- Artificial example [here](http://fiddle.jshell.net/kunknown/VVaEq/2/show/)
-
-Drag and Drop
------
-- The ability to tightly tie the position of an element to touch input.
-- A common effect is to have other content move to make space for the dragged content as it is moved about.
-
-Drawers
------
-- Similar to drag and drop, we would like to tie an element to touch input.
-- If a draw animation is initiated, we would also like to be able to catch the drawer before the animation completes.
-- I.e., we want to be able to immediately interrupt a compositor-driven animation based on input.
-
-Performant Effect Libraries
------
-- Currently, if a library provides an effect that is driven on the main thread, the performance of that effect depends entirely on the performance of the embedding content. It would be valuable to be able to author a library that is more performance isolated and can bill itself as such.
-
-Element Location Tracking
------
-- It would be nice to keep track of the location of an element with respect to another element (in particular, a scroll clip), to get an asynchronous, as-accurate-as-possible set of records of the element's location history.
- 
-
diff --git a/css-animation-worklet-1/Makefile b/css-animation-worklet-1/Makefile
new file mode 100644
index 00000000..63fe58ab
--- /dev/null
+++ b/css-animation-worklet-1/Makefile
@@ -0,0 +1,20 @@
+# $Id: Makefile,v 1.5 2008/02/06 14:05:15 mike Exp $
+#
+# FIXME: New documentation needed.
+#
+# Use "make REMOTE=1" to use remote bikeshed
+
+SOURCEFILE=Overview.bs
+OUTPUTFILE=Overview.html
+PREPROCESSOR=bikeshed
+REMOTE_PREPROCESSOR_URL=https://api.csswg.org/bikeshed/
+
+all: $(OUTPUTFILE)
+
+$(OUTPUTFILE): $(SOURCEFILE)
+ifneq (,$(REMOTE))
+	curl $(REMOTE_PREPROCESSOR_URL) -F file=@$(SOURCEFILE) > "$@"
+else
+	$(PREPROCESSOR) -f spec "$<" "$@"
+endif
+
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 eb75e1b7..6074248e 100644 --- a/css-layout-api/Overview.bs +++ b/css-layout-api/Overview.bs @@ -1,462 +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} ===================== -The layout stage of CSS is responsible for generating and positioning fragments from a tree -of boxes. +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. -This specification describes an API which allows developers to layout a box in response to -computed style and box tree changes. +The 'overflow' property applies to [=layout API containers=]. This is discussed in +[[#interaction-overflow]]. -Layout Invalidation {#layout-invalidation} -========================================== +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. -
-TODO, list all the ways that layout can be invalidated, namely: - - Computed style change. - - Child computed style change. - - Child add/remove change. - - etc. +
+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>
+
-Registering A Layout {#registering-a-layout} -============================================ +Layout API Container Painting {#painting} +----------------------------------------- -
-callback VoidFunction = void ();
+[=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 interface RenderWorkletGlobalScope { - void registerLayout(DOMString name, VoidFunction layoutCtor); +
+partial namespace CSS {
+    [SameObject] readonly attribute Worklet layoutWorklet;
 };
 
-The {{RenderWorkletGlobalScope}} has a map of name to layout constructor map. Initially this -map is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. +The {{LayoutWorkletGlobalScope}} is the global execution context of the {{layoutWorklet}}. -Issue: Write full register algorithm, and checks required. +
+[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet]
+interface LayoutWorkletGlobalScope : WorkletGlobalScope {
+    undefined registerLayout(DOMString name, VoidFunction layoutCtor);
+};
+
-
- This is what the shape of the class should be: -
-        callback interface LayoutClass {
-            readonly attribute sequence<DOMString> inputProperties;
-            readonly attribute sequence<DOMString> childInputProperties;
-
-            LayoutResult layout(
-                    ConstraintSpace space,
-                    sequence<Box> children,
-                    StylePropertyMap styleMap,
-                    BreakToken break);
-            void childrenChange(/*
-                    added children,
-                    removed children,
-                    idx updated children,
-                    similar to Array.observe */);
-        };
+
+ Web developers can feature detect by: +
+    if ('layoutWorklet' in CSS) {
+      console.log('CSS Layout API available!');
+    }
     
-Layout Notation {#layout-notation} -================================== +Concepts {#concepts} +-------------------- -
-    layout() = layout( <> )
-    inline-layout() = inline-layout( <> )
-
+This section describes internal data-structures created when {{registerLayout(name, layoutCtor)}} is +called. -The <> and <> function is an additional notation to be supported by the ''display'' property. +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: -Issue: Resolve this with css-display-3 once required. + - class constructor which is the class [=constructor=]. -Layout {#layout} -================ + - layout function which is the layout [=function=] callback. -Concepts {#layout-concepts} ---------------------------- + - intrinsic sizes function which is the intrinsic sizes + [=function=] callback. -
-[Constructor(), Constructor(ConstraintSpace)]
-interface ConstraintSpace {
-    attribute double? width;
-    attribute double? height;
+ - constructor valid flag.
 
-    readonly attribute sequence<ExclusionArea> exclusions;
+ - input properties which is a [=list=] of
+     DOMStrings.
 
-    // This is probably wrong.
-    void addExclusion(Fragment fragment);
-    void addExclusion(ExclusionArea rect);
+ - 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";
 };
 
-interface ExclusionArea {
+[Exposed=LayoutWorklet]
+enum ChildDisplayType {
+    "block", // default - "blockifies" the child boxes.
+    "normal",
 };
 
-[Constructor(double width, double height, double x, double y)]
-interface ExclusionRect : ExclusionArea {
-    readonly attribute double width;
-    readonly attribute double height;
-    readonly attribute double x;
-    readonly attribute double y;
+[Exposed=LayoutWorklet]
+enum LayoutSizingMode {
+    "block-like", // default - Sizing behaves like block containers.
+    "manual", // Sizing is specified by the web developer.
 };
 
-A {{ConstraintSpace}} represents the available space to perform the current layout in. +The [=document=] has a [=map=] of document layout definitions. Initially this map is +empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. -The {{ConstraintSpace}} has {{ConstraintSpace/width}} and {{ConstraintSpace/height}} attributes, -which if defined specify the dimensions in which the layout can perform it's layout. If the -{{ConstraintSpace/width}} or {{ConstraintSpace/height}} are null, the layout can assume that it has -an infinite space to perform it's layout in that direction. +The {{LayoutWorkletGlobalScope}} has a [=map=] of layout definitions. Initially this map +is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called. -The {{ConstraintSpace}} has a list of {{exclusions}} which specify which areas the layout should not -position children within. +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=]. -A {{ExclusionRect}} represents a rectangular area in which any placed fragments should not -intersect with. +Each [=box=] representing a [=layout API container=] has a styleMap internal slot. +This is a {{StylePropertyMapReadOnly}} which contains the properties listed in +inputProperties. -Issue: More types of exclusions than just Rects? What about shapes? v2? +The user agent clear the [=styleMap=] internal slot for a [=box=] when: -
-interface Box {
-    readonly attribute StylePropertyMap styleMap;
-    Fragment doLayout(ConstraintSpace space, OpaqueBreakToken breakToken);
-};
-
+ - The [=computed values=] of [=document layout definition/input properties=] for the [=box=] + changes. -A {{Box}} represents a box. {{Box}}es are passed to the current layout as children of the -current layout. It is the current layout's responsibility to generate {{Fragment}}s from the child -{{Box}}es. + - When the [=box=] is removed from the [=box tree=]. -
-interface Fragment {
-    readonly attribute double width;
-    readonly attribute double height;
+  - 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.
+            }
+        });
+    
+
+
- readonly attribute double minContent; - readonly attribute double maxContent; +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. - readonly attribute sequence<Fragment> unpositionedFragments; +
+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. - attribute double x; - attribute double y; + 2. Let |layoutDefinitionMap| be {{LayoutWorkletGlobalScope}}'s [=layout definitions=] map. - readonly attribute OpaqueBreakToken? breakToken; + 3. If |layoutDefinitionMap|[|name|] [=map/exists=] [=throw=] a "{{InvalidModificationError}}" + {{DOMException}} and abort all these steps. - readonly attribute double baseline; -}; -
+ 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|. -A {{Fragment}} represents a fragment. It is the result of performing -<> on a {{Box}}. + - [=layout definition/input properties=] being |inputProperties|. -The {{Fragment}} has {{Fragment/width}} and {{Fragment/height}} attributes, which are set by the -respective {{Box}}'s layout algorithm. They cannot be changed. If the current layout wishes a -different {{Fragment/width}} or {{Fragment/height}} the author must perform {{doLayout()}} again -with a different {{ConstraintSpace}} in order to get a different result. + - [=layout definition/layout options=] being |layoutOptions|. -The {{Fragment}} has {{Fragment/minContent}} and {{Fragment/maxContent}} attributes, which are set -by the respective {{Box}}'s layout algorithm. They cannot be changed. + 21. [=map/Set=] |layoutDefinitionMap|[|name|] to |definition|. -The {{Fragment}} has a list of {{Fragment/unpositionedFragments}}. These are fragments which the -containing fragment could not position itself, and should be positioned by either the current -layout, or a parent layout. + 22. [=Queue a task=] to run the following steps: -The author inside the current layout can position the {{Fragment}} by setting it's {{Fragment/x}} -and {{Fragment/y}}. The resulting position and size of the fragment must not intersect with -any of the {{exclusions}} listed in the current layout's {{ConstraintSpace}}. + 1. Let |documentLayoutDefinitionMap| be the associated [=document's=] [=document layout + definitions=] [=map=]. -The {{Fragment}}'s {{Fragment/breakToken}} specifies where the {{Box}} last fragmented. + 2. Let |documentDefinition| be a new [=document layout definition=] with: -The {{Fragment}}'s {{Fragment/baseline}} specifies where the baseline is positioned. + - [=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.
-interface OpaqueBreakToken {
-};
+[Exposed=LayoutWorklet]
+interface LayoutChild {
+    readonly attribute StylePropertyMapReadOnly styleMap;
 
-[Constructor(OpaqueBreakTokenchildFragmentBreakToken, Box childBox)]
-interface BreakToken : OpaqueBreakToken {
-    readonly attribute OpaqueBreakToken childFragmentBreakToken;
-    readonly attribute Box childBox;
+    Promise<IntrinsicSizes> intrinsicSizes();
+    Promise<LayoutFragment> layoutNextFragment(LayoutConstraintsOptions constraints, ChildBreakToken breakToken);
 };
 
-A {{OpaqueBreakToken}} represents a continuation token that may be given as an argument to -{{doLayout()}} to produce the next {{Fragment}} for that box. +The {{LayoutChild}} has internal slot(s): -A {{BreakToken}} is used in a {{LayoutResult}} to indicate where the current layout last broke. + - \[[box]] a CSS [=box=]. -Performing layout {#performing-layout} --------------------------------------- + - \[[styleMap]] a {{StylePropertyMapReadOnly}}, this is the + computed style for the child, it is populated with only the properties listed in + childInputProperties. -
-dictionary LayoutResult {
-    double minContent;
-    double maxContent;
-    double width;
-    double height;
-    sequence<Fragment> fragments;
-    sequence<Fragment> unpositionedFragments;
-    BreakToken breakToken;
-    double baseline;
-};
+   - [[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();
+
+  }
+});
 
+
-{{LayoutClass/layout()}} is invoked by the user agent when generate a layout for a box. +A {{LayoutChild}} could be generated by: -The user agent passes in: - - The current children for the box, with only {{LayoutClass/childInputProperties}} on - {{Box/styleMap}} - - The available space defined by a {{ConstraintSpace}} - - The computed style of the box, with only {{LayoutClass/inputProperties}} - - The {{BreakToken}} if any, for where the box was last fragmented. + - An [=element=]. -The author defined code should produce a {{LayoutResult}}. + - A [=root inline box=]. -The {{LayoutResult}} consists of: - - A {{LayoutResult/minContent}} which represents the fragment's min-content inline-size - contribution. - - A {{LayoutResult/maxContent}} which represents the fragment's max-content inline-size - contribution. - - A {{LayoutResult/width}} which represents the fragment's resulting width. - - A {{LayoutResult/height}} which represents the fragment's resulting height. - - A list of {{LayoutResult/fragments}} which represents the fragment's direct child fragments. - - A list of {{LayoutResult/unpositionedFragments}} which represents the fragment's children which - should be positioned by a parent fragment. - - A {{LayoutResult/breakToken}} which represents where the current layout's box last broke. - - A {{LayoutResult/baseline}} which represents the baseline of the fragment. + - A ::before or ::after pseudo-element. -Issue: Write the following into the algorithm. + 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. -If any {{Fragment}}s appear in both the list of {{LayoutResult/fragments}} or the list of -{{LayoutResult/unpositionedFragments}} the user agent should throw an error. + - An [=anonymous box=]. For example an anonymous box may be inserted as a result of: -The user agent should check that a consistent set of {{Fragment}}s generated from a {{Box}} is -returned in either the list of {{LayoutResult/fragments}} or {{LayoutResult/unpositionedFragments}}. -(Consistent being that a {{Fragment/breakToken}} from one {{Fragment}} was used to generate another -{{Fragment}} in the set). + - A text node which has undergone [=blockification=]. (Or more generally a [=root inline box=] + which has undergone [=blockification=]). -If any {{Fragment}}s appear more than once, the user agent should throw an error. + - An element with ''display: table-cell'' which doesn't have a parent with ''display: table''. -When the user agent wants to generate a layout of a <> or <> -for a box it must run the following steps: +
+ 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="..." />
+    
+
-Issue: TODO specify these steps. +
+ 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.
+    
+
-Overflow {#overflow} --------------------- +Multiple non-[=atomic inlines=] are placed within the same {{LayoutChild}} to allow rendering +engines to perform text shaping across element boundaries. -Overflow is determined if the resulting {{LayoutResult/width}} or {{LayoutResult/height}} exceed the -space available given by the current {{ConstraintSpace}}. +
+ Note: As an example the following should produce one {{LayoutFragment}} but is from + three non-[=atomic inlines=]: +
+        ع<span style="color: blue">ع</span>ع
+    
+
-Overflow is not triggered if the list of returned {{LayoutResult/fragments}} exceed or are -placed outside the space available given by the {{ConstraintSpace}}. +Note: When accessing the {{LayoutChild/styleMap}} the user agent can create a new + {{StylePropertyMapReadOnly}} if none exists yet. -If the user agent requires displaying a scroll-bar with a non-zero width the user agent -must call layout again with a different {{ConstraintSpace}} reflecting this consumption of -space. If, the subsequent layout doesn't produce an overflow, the user-agent can display an -un-scrollable scroll-bar. +
+The styleMap, on getting from a {{LayoutChild}} |this|, the +user agent must perform the following steps: -Note: Under this scheme the layout may be called at least three times, for example; once for the - initial layout, second time for the overflow in the vertical direction, and a third time if it - overflows in the horizontal direction. Pray that we never overflow in the z-direction. + 1. If |this|' {{[[styleMap]]}} is null, then: -Issue: This may be completely the wrong way to do this. Should fragments be responsible for deciding - if they have scrollbars or not? (I don't think so, but wanted to put this out there). Might also - be better for an early return to be possible for this case. I.e. "Hey I'm going to overflow! - Give me a new constraint space!". + 1. Let |box| be |this|' {{LayoutChild/[[box]]}}. -Examples {#examples} -==================== + 2. Let |definition| be the result of [=get a layout definition=]. -Example 1: A simple block layout {#example-1} ---------------------------------------------- + 3. Let |childInputProperties| be |definition|'s [=layout definition/child input + properties=]. -
-// Inside RenderWorkletGlobalScope
-
-// Note this is meant to be similar (*not* the same) as a block layout.
-// Everything is done in 'width' & 'height' for easy reading.
-registerLayout('simple-flow', class {
-    static get inputProperties() { return ['width', 'height'] }
-    static get childrenInputProperties() { return ['x', 'y', 'position'] }
-
-    layout(children, constraintSpace, styleMap, breakToken) {
-        const absoluteChildren = [];
-        const fixedChildren = [];
-        const fragments = [];
-
-        // Resolve our width using the available width in 'constraintSpace', and
-        // our computed width property.
-        let width = resolveWidth(constraintSpace, styleMap.get('width'));
-
-        // Create a new constraint space for our children to consume.
-        let childConstraintSpace = new ConstraintSpace(constraintSpace);
-        childConstraintSpace.width = width;
-
-        // Track the used height, min and max content.
-        let height = 0;
-        let minContent = 0;
-        let maxContent = 0;
-
-        for (let child of children) {
-            // Check if the child is out of flow positioned.
-            const childPosition = child.styleMap.get('position');
-
-            if (childPosition == 'absolute') {
-                absoluteChildren.push(child);
-                continue;
-            }
+        4. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the
+            [=computed values=] for properties listed in |childInputProperties| for |box|.
 
-            if (childPosition == 'fixed') {
-                fixedChildren.push(child);
-                continue;
-            }
+        5. Set |this|' {{LayoutChild/[[styleMap]]}} internal slot to |styleMap|.
 
-            // Layout the in flow child.
-            const childFragment = child.doLayout(childConstraintSpace);
+        Note: If the user agent always pre-populates {{LayoutChild/[[styleMap]]}} then this branch
+            of the algorithm won't be reached.
 
-            // Position the child.
-            childFragment.x = 0;
-            childFragment.y = height;
+    2. Return |this|' {{StylePropertyMapReadOnly}} contained in the {{LayoutChild/[[styleMap]]}}
+        internal slot.
+
- // Update our current height, min and max content. - height += childFragment.height; - minContent = Math.max(childFragment.minContent, minContent); - maxContent = Math.max(childFragment.maxContent, maxContent); - } +Note: The {{intrinsicSizes()}} method allows the web developer to query the intrinsic sizes of the + {{LayoutChild}}. - // Resolve the height. - height = resolveHeight(constraintSpace, styleMap.get('height'), height); +
+When the intrinsicSizes() method is called on a {{LayoutChild}} +|this|, the user agent must perform the following steps: - return { - minContent: minContent, - maxContent: maxContent, - width: width, - height: height, - fragments: fragments, - unPositionedChildren: absoluteChildren.concat(fixedChildren), - breakToken: null - }; - } + 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);
+  }
 });
 
+
-
-<div id="myElement">
-    <div>
-        CSS is awesome.
-    </div>
-</div>
+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.
 
-<style>
-#myElement {
-    display: layout('simple-flow');
-}
-</style>
+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;
+};
 
-Example 2: A simple line layout {#example-2} --------------------------------------------- +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. -
-// Inside RenderWorkletGlobalScope
-
-// Note this is meant to be similar (*not* the same) as a inline layout.
-// Everything is done in 'width' & 'height' for easy reading.
-registerLayout('simple-inline-flow', class {
-    static get inputProperties() { return ['width', 'height'] }
-    static get childrenInputProperties() { return [] }
-
-    layout(children, constraintSpace, styleMap, breakToken) {
-        // Resolve our width using the available width in 'constraintSpace', and
-        // our computed width property.
-        const width = resolveWidth(constraintSpace, styleMap.get('width'));
-        const fragments = [];
-        let height = 0;
-
-        // TODO compute these.
-        let minContent = 0;
-        let maxContent = 0;
-
-        let childFragment = null;
-        let lineFragments = [];
-        let lineHeight = 0;
-        let remainingLineWidth = width; // NOTE: should be helper on constraint space?
-
-        const childIter = chidlren.values();
-        let child = childIter.next().value;
-        let breakToken = null;
+The {{IntrinsicSizes/minContentSize}} and {{IntrinsicSizes/maxContentSize}} cannot be changed. They
+must not change for a {{LayoutChild}} within the current layout pass.
 
-        while (child) {
-            // Create a new constraint space for the child, with all the current
-            // positioned children.
-            const childConstraintSpace = new ConstraintSpace(constraintSpace);
-            childConstraintSpace.addExclusion(new ExclusionRect(width, height, 0, 0));
-            childConstraintSpace.addExclusions(lineFragments);
-
-            // Perform layout on the child.
-            childFragment = child.doLayout(childConstraintSpace, breakToken);
-            fragments.push(childFragment);
-
-            // Check if we need to position the fragment on the next line.
-            if (childFragment.width > remainingLineWidth) {
-                // Need to start a new line.
-                lineFragments = [];
-                height += lineHeight;
-                lineHeight = 0;
-                remainingLineWidth = width;
-            }
+
+The example below shows the border-box intrinsic sizes of two children. - // Position the fragment horizontally. - childFragment.x = width - remainingLineWidth; +
+<style>
+.child-0 {
+  width: 380px;
+  border: solid 10px;
+}
 
-            lineFragments.push(childFragment);
-            lineHeight = Math.max(lineHeight, childFragment.height);
-            remainingLineWidth -= childFragment.width;
+.child-1 {
+  border: solid 5px;
+}
 
-            // Update the line fragments positions, based on the new lineHeight.
-            for (let frag of lineFragments) {
-                frag.y = lineHeight - frag.height;
-            }
+.box {
+  display: layout(intrinsic-sizes-example);
+  font: 25px/1 Ahem;
+}
+</style>
 
-            // Step to the next child if required.
-            if (childFragment.breakToken) {
-                breakToken = childFragment.breakToken;
-            } else {
-                child = childIter.next().value;
-                breakToken = null;
-            }
-        }
+<div class="box">
+  <div class="child-0"></div>
+  <div class="child-1">XXX XXXX</div>
+</div>
+
- // Resolve the height. - height = resolveHeight(constraintSpace, styleMap.get('height'), height); +
+registerLayout('intrinsic-sizes-example', class {
+    async intrinsicSizes(children, edges, styleMap) {
+      const childrenSizes = await Promise.all(children.map((child) => {
+          return child.intrinsicSizes();
+      }));
 
-        return {
-            minContent: minContent,
-            maxContent: maxContent,
-            width: width,
-            height: height,
-            fragments: fragments,
-            unpositionedFragments: [],
-            breakToken: null
-        };
+      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 index aa1d6c86..93e86dde 100644 --- a/css-layout-api/README.md +++ b/css-layout-api/README.md @@ -1,530 +1 @@ -# 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 node 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, - }; - } -}); -``` +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 f6c08fc1..f7570b54 100644 --- a/css-paint-api/Overview.bs +++ b/css-paint-api/Overview.bs @@ -3,42 +3,53 @@ Title: CSS Painting API Level 1 Status: ED Group: houdini ED: https://drafts.css-houdini.org/css-paint-api-1/ -Previous Version: http://www.w3.org/TR/2016/WD-css-paint-api-1-20160607/ +TR: http://www.w3.org/TR/css-paint-api-1/ +Previous Version: https://www.w3.org/TR/2018/WD-css-paint-api-1-20180410/ +Previous Version: https://www.w3.org/TR/2016/WD-css-paint-api-1-20160607/ Shortname: css-paint-api Level: 1 -Abstract: -Editor: Shane Stephens, shanestephens@google.com -Editor: Ian Kilpatrick, ikilpatrick@chromium.org -Editor: Dean Jackson, dino@apple.com +Abstract: + An API for allowing web developers to define a custom CSS <> with javascript, which will + respond to style and size changes. + See EXPLAINER. +Former Editor: Shane Stephens, shanestephens@google.com, w3cid 47691 +Editor: Ian Kilpatrick, ikilpatrick@chromium.org, w3cid 73001 +Editor: Dean Jackson, dino@apple.com, w3cid 42080 +Ignored Terms: PaintWorklet
+ +
-urlPrefix: https://heycam.github.io/webidl/; type: dfn;
-    text: NotSupportedError
-    urlPrefix: #dfn-;
-        text: callback this value
-        text: exception
-        text: throw
-        url: throw; text: thrown
-    url: es-invoking-callback-functions; text: Invoke
-urlPrefix: https://html.spec.whatwg.org/multipage/scripting.html; type: dfn;
-    text: reset the rendering context to its default state
-    text: output bitmap
-    text: set bitmap dimensions
-    url: concept-canvas-alpha; text: alpha
 urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
     text: constructor
     text: Construct
     text: IsArray
     text: IsCallable
     text: IsConstructor
-    text: HasProperty
+    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
 
@@ -49,42 +60,11 @@ Introduction {#intro} The paint stage of CSS is responsible for painting the background, content and highlight of a box based on that box's size (as generated by the layout stage) and computed style. -This specification describes an API which allows developers to paint a part of an box in +This specification describes an API which allows developers to paint a part of a box in response to size / computed style changes with an additional <> function. -Note: In a future version of the spec, support may be added for defining the clip, global alpha, - filter on a portion of an box (for example on the background layers). - -Paint Invalidation {#paint-invalidation} -======================================== - -A document has an associated paint name to input properties map. Initially it is -empty and is populated when {{registerPaint(name, paintCtor)}} is called. - -Each <> function for a box has an associated paint valid flag. It may be either -paint-valid or paint-invalid. It is initially set to paint-invalid. - -When the size (as determined by layout) of a |box| changes, each <> function's paint -valid flag should be set to paint-invalid. - -When the computed style for an |box| changes, the user agent must run the following steps: - 1. For each <> function on the |box|, perform the following substeps: - 1. Let |paintFunction| be the current <> function on the |box|. - - 2. Let |name| be the first argument of the <> function. - - 3. Let |inputProperties| be the result of lookuping up |name| on paint name to input - properties map. - - 4. For each |property| in |inputProperties|, if the |property|'s computed value has - changed, set the paint valid flag on the |paintFunction| to paint-invalid. - -Performing draw a paint image results in the paint valid flag for a <> -function on a box to be set to paint-valid. - -Note: In a future version of the spec, support may be added for partial invalidation. The user agent - will be able to specify a region of the rendering context which needs to be re-painted by the - paint class. +Note: In a future version of the spec, support could be added for defining the clip, global alpha, + filter on a portion of a box (for example on the background layers). Paint Worklet {#paint-worklet} ============================== @@ -92,149 +72,254 @@ Paint Worklet {#paint-worklet} The {{paintWorklet}} attribute allows access to the {{Worklet}} responsible for all the classes which are related to painting. -The {{paintWorklet}}'s worklet global scope type is {{PaintWorkletGlobalScope}}. +The {{paintWorklet}}'s [=worklet global scope type=] is {{PaintWorkletGlobalScope}}. + +The {{paintWorklet}}'s worklet destination type is "paintworklet".
-partial interface Window {
+partial namespace CSS {
     [SameObject] readonly attribute Worklet paintWorklet;
 };
 
-The {{PaintWorkletGlobalScope}} is the global execution context of the {{paintWorklet}}. +A {{PaintWorkletGlobalScope}} is a global execution context of the {{paintWorklet}}. -
-callback VoidFunction = void ();
+A {{PaintWorkletGlobalScope}} has a {{PaintWorkletGlobalScope/devicePixelRatio}} property which is
+identical to the Window.{{Window/devicePixelRatio}} property.
 
+
 [Global=(Worklet,PaintWorklet),Exposed=PaintWorklet]
 interface PaintWorkletGlobalScope : WorkletGlobalScope {
-    void registerPaint(DOMString name, VoidFunction paintCtor);
+    undefined registerPaint(DOMString name, VoidFunction paintCtor);
+    readonly attribute unrestricted double devicePixelRatio;
+};
+
+ +The {{PaintRenderingContext2DSettings}} contains the settings for the rendering context associated +with the paint canvas. The {{PaintRenderingContext2DSettings}} provides a supported subset of canvas +rendering context 2D settings. In the future, it may be extended to support color management in +paint canvas. +
+dictionary PaintRenderingContext2DSettings {
+    boolean alpha = true;
 };
 
- Note: This is how the class should look. -
-        callback interface PaintClass {
-            readonly attribute sequence<DOMString> inputProperties;
-            void paint(PaintRenderingContext2D ctx, PaintSize geom, 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.
+            }
+        }
     
Concepts {#concepts} ==================== -A paint image definition describes an author defined <> which can be referenced by -the <> function. It consists of: +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. - - A paint image name + - input properties which is a [=list=] of + DOMStrings. - - A class constructor which is the class constructor + - A PaintRenderingContext2DSettings object. - - A paint function which is the paint function callback +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 class constructor valid flag + - 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 {{PaintWorkletGlobalScope}} has a paint name to paint image definition map. Initially +The [=document=] has a [=map=] of document paint definitions. Initially this map is empty; it is populated when {{registerPaint(name, paintCtor)}} is called. -The {{PaintWorkletGlobalScope}} has a paint name to instance map. Initially this map is -empty; it is populated when draw a paint image is invoked by the user agent. +A {{PaintWorkletGlobalScope}} has a [=map=] of paint definitions. Initially this map +is empty; it is populated when {{registerPaint(name, paintCtor)}} is called. -Instances of paint classes in the paint name to instance map may be disposed and removed from +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 not a valid <>, throw a TypeError and abort all these - 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. - 2. If the |name| exists as a key in the paint name to paint image definition map, - throw a NotSupportedError and abort all these steps. + 7. Filter |inputProperties| so that it only contains [=supported CSS properties=] and + [=custom properties=]. - 3. Let |inputProperties| be the result of Get(|paintCtor|, "inputProperties"). + Note: The list of CSS properties provided by the input properties getter can either be custom or + native CSS properties. - 4. If |inputProperties| is not undefined, and the result of IsArray(|inputProperties|) is - false, throw a TypeError and abort all these steps. + Note: The list of CSS properties may contain shorthands. - If |inputProperties| is undefined, let |inputProperties| be a new empty array. + 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. - 5. For each |item| in |inputProperties| perform the following substeps: + 8. Let |inputArguments| be an empty sequence<DOMString>. - 1. If the result of Type(|item|) is not String, throw a TypeError - and abort all these steps. + 9. Let |inputArgumentsIterable| be the result of [=Get=](|paintCtor|, "inputArguments"). - Note: The list of CSS properties provided by the input properties getter can either be - custom or native CSS properties. + 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 may contain shorthands. + 11. Let |inputArgumentSyntaxes| be an [=list/empty=] [=list=]. - 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. + 12. [=list/For each=] |item| in |inputArguments| perform the following substeps: - 5. If the result of IsConstructor(|paintCtor|) is false, throw a TypeError + 1. Attempt to [=consume a syntax definition=] from |item|. + If failure was returned, [=throw=] a [=TypeError=] and abort all these steps. + Otherwise, let |parsedSyntax| be the returned [=syntax definition=]. + + 2. [=list/Append=] |parsedSyntax| to |inputArgumentSyntaxes|. + + 13. Let |contextOptionsValue| be the result of [=Get=](|paintCtor|, "contextOptions"). + + 14. Let |paintRenderingContext2DSettings| be the result of [=converting=] + |contextOptionsValue| to a {{PaintRenderingContext2DSettings}}. + If an exception is [=thrown=], rethrow the exception and abort all these steps. + + Note: Setting paintRenderingContext2DSettings.alpha is false allows user agents + to anti-alias text in addition to performing "visibility" optimizations, e.g. not + painting an image behind the paint image as the paint image is opaque. + + 15. If the result of [=IsConstructor=](|paintCtor|) is false, [=throw=] a [=TypeError=] and abort all these steps. - 6. Let |prototype| be the result of Get(|paintCtor|, "prototype"). + 16. Let |prototype| be the result of [=Get=](|paintCtor|, "prototype"). - 7. If the result of Type(|prototype|) is not Object, throw a TypeError and + 17. If the result of [=Type=](|prototype|) is not Object, [=throw=] a [=TypeError=] and abort all these steps. - 8. Let |paint| be the result of Get(|prototype|, "paint"). + 18. Let |paintValue| be the result of [=Get=](|prototype|, "paint"). - 9. If the result of IsCallable(|paint|) is false, throw a TypeError and - abort all these steps. + 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|. - 10. Let |definition| be a new paint image definition with: + - [=document paint definition/input argument syntaxes=] being + |inputArgumentSyntaxes|. - - paint image name being |name| - - - class constructor being |constructor| + - [=document paint definition/PaintRenderingContext2DSettings object=] being |paintRenderingContext2DSettings|. - - paint function being |paint| + 3. If |documentPaintDefinitionMap|[|name|] [=map/exists=], run the following steps: - - class constructor valid flag being true - - 11. Add the key-value pair (|name| - |inputProperties|) to the paint name to input properties - map of the associated document. + 1. Let |existingDocumentDefinition| be the result of [=map/get=] + |documentPaintDefinitionMap|[|name|]. - 12. Add the key-value pair (|name| - |definition|) to the paint name to paint image - definition map of the associated document. + 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 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 +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>
+    
-For the 'cursor' property, the <> function should be treated as an invalid image and +For the 'cursor' property, the <> function should be treated as an [=invalid image=] and fallback to the next supported <>. -Issue(w3c/css-houdini-drafts#100): Support additional arbitrary arguments for the paint function. - This is difficult to specify, as you need to define a sane grammar. A better way would be to - expose a token stream which you can parse into Typed OM objects. This would allow a full - arbitrary set of function arguments, and be future proof. +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} ================================================ @@ -243,59 +328,98 @@ The 2D rendering context {#2d-rendering-context} [Exposed=PaintWorklet] interface PaintRenderingContext2D { }; -PaintRenderingContext2D implements CanvasState; -PaintRenderingContext2D implements CanvasTransform; -PaintRenderingContext2D implements CanvasCompositing; -PaintRenderingContext2D implements CanvasImageSmoothing; -PaintRenderingContext2D implements CanvasFillStrokeStyles; -PaintRenderingContext2D implements CanvasShadowStyles; -PaintRenderingContext2D implements CanvasRect; -PaintRenderingContext2D implements CanvasDrawPath; -PaintRenderingContext2D implements CanvasDrawImage; -PaintRenderingContext2D implements CanvasPathDrawingStyles; -PaintRenderingContext2D implements CanvasPath; +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}}, - {{CanvasUserInterface}}, {{CanvasText}} or {{CanvasTextDrawingStyles}} APIs. - -A {{PaintRenderingContext2D}} object has a output bitmap. This is initialised when the -object is created. The size of the output bitmap is the size of the fragment it is -rendering. - -The size of the output bitmap does not necessarily represent the size of the actual bitmap + 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. -The {{PaintRenderingContext2D}} object should have its alpha flag set to true. That is the -rendering context is initially fully transparent. - Additionally the user agent may record the sequence of drawing operations which have been applied to -the output bitmap such that the user agent can subsequently draw onto a device bitmap at the -correct resolution. This also allows user agents to re-use the same output of the output -bitmap repeatably while the visual viewport is being zoomed for example. +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| it must run the following steps: +|height|, and |paintRenderingContext2DSettings|, it must run the following steps: 1. Create a new {{PaintRenderingContext2D}}. - 2. Set bitmap dimensions for the context's output bitmap to |width| and |height|. - 3. Return the new {{PaintRenderingContext2D}}. + 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=]. +
-Note: The initial state of the rendering context is set inside the set bitmap dimensions - algorithm, as it invokes reset the rendering context to its default state and clears the - output bitmap. +Drawing a CSSImageValue {#drawing-a-cssimagevalue} +-------------------------------------------------- + +The {{CanvasImageSource}} typedef is extended to also include the {{CSSImageValue}} type to be used +as an image source. + +For interfaces which use the {{CanvasDrawImage}} mixin: + - When a {{CanvasImageSource}} object represents an {{CSSImageValue}}, the result of invoking + the value's underlying image algorithm must be used as the source image for the purposes of + {{CanvasDrawImage/drawImage}}. + +Note: This should eventually be moved to the canvas section of the HTML specification. +See Issue 819. Drawing an image {#drawing-an-image} ==================================== -If a <> function for a fragment is paint-invalid and the fragment is within the -visual viewport, then user agent must draw a paint image for the current frame. The -user agent may not defer the draw a paint image operation until a subsequent frame. +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 may choose to draw a paint image for <> functions not within - the visual viewport. +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. @@ -304,23 +428,41 @@ Note: The user agent may choose to draw a paint image for <> fun element.styleMap.set('--custom-prop-invalidates-paint', 42); });
- And the element is inside the visual viewport, the user agent must draw - a paint image and display the result on the current frame. + 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 should be invoked by the user agent during the object size -negotiation algorithm which is responsible for rendering an <>. +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. +For the purposes of the [=object size negotiation=] algorithm, the paint image has no +[=intrinsic dimensions=]. -Note: In a future version of the spec, the author may be able to specify the intrinsic - dimensions of the paint image. This will probably be exposed as a callback allowing the - author to define static intrinsic dimensions or dynamically updating the intrinsic - dimensions based on computed style and size changes. +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 concrete object size given by the user agent. +the |snappedConcreteObjectSize| given by the user agent. + +Note: See [[css-images-3#object-sizing-examples]] for examples on how the [=concrete object + size=] is calculated. + +The [=draw a paint image=] function may be speculatively invoked by the user agent at any point, +with any |snappedConcreteObjectSize|. The resulting image is not displayed. + +Note: User agents may use any heuristic to speculate a possible future value for + |snappedConcreteObjectSize|, for example speculating that the size remains unchanged. + +Note: Although the image is not displayed, it may still be cached, and subsequent invocations of + <> may use the cached image.
 [Exposed=PaintWorklet]
@@ -330,66 +472,143 @@ interface PaintSize {
 };
 
+
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 it's associated -with), given it's |concreteObjectSize| (concrete object size) it must run the -following steps: - 1. Let |paintFunction| be the current <> function on the |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. +
- 2. If the paint valid flag for the |paintFunction| is paint-valid the user agent - may use the drawn image from the previous invocation. If so it may abort - all these steps and use the previously drawn image. +
+When the user agent wants to invoke a paint callback given |name|, |inputArguments|, +|snappedConcreteObjectSize|, and |workletGlobalScope|, it must run the following steps: - 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. + 1. Let |paintDefinitionMap| be |workletGlobalScope|'s [=paint definitions=] map. - 3. Let |name| be the first argument of the |paintFunction|. + 2. If |paintDefinitionMap|[|name|] does not [=map/exist=], run the following steps: - 4. Let |workletGlobalScope| be a {{PaintWorkletGlobalScope}} from the list of worklet's - WorkletGlobalScopes from the paint {{Worklet}}. + 1. [=Queue a task=] to run the following steps: - The user agent may also create a WorkletGlobalScope given the paint - {{Worklet}} and use that. + 1. Let |documentPaintDefinitionMap| be the associated [=document=]'s [=document + paint definitions=] map. - Note: The user agent may use any policy for which {{PaintWorkletGlobalScope}} to - select or create. It may use a single {{PaintWorkletGlobalScope}} or multiple and - randomly assign between them. + 2. [=map/Set=] |documentPaintDefinitionMap|[|name|] to "invalid". - 5. Let |definition| be the result of looking up |name| on the |workletGlobalScope|'s paint - name to paint image definition map. + 3. The user agent should log an error to the debugging console stating that a + class wasn't registered in all {{PaintWorkletGlobalScope}}s. - If |definition| does not exist, let the image output be an invalid image and abort - all these steps. + 2. Let the image output be an [=invalid image=] and abort all these steps. - 6. Let |paintInstance| be the result of looking up |name| on |workletGlobalScope|'s paint - name to instance map. If |paintInstance| is null run the following substeps: + 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. - 1. If the class constructor valid flag on |definition| is false, let the image output - be an invalid image and abort all these steps. + 3. Let |definition| be the result of [=get=] |paintDefinitionMap|[|name|]. - 2. Let |paintCtor| be the class constructor on |definition|. + 4. Let |paintClassInstanceMap| be |workletGlobalScope|'s [=paint class instances=] map. - 3. Let |paintInstance| be the result of Construct(|paintCtor|). + 5. Let |paintInstance| be the result of [=get=] |paintClassInstanceMap|[|name]|. If + |paintInstance| is null, run the following steps: - If Construct throws an exception, set the |definition|'s constructor valid - flag to false, let the image output be an invalid image and abort all these + 1. If the [=paint definition/constructor valid flag=] on |definition| is false, let the image output be an + [=invalid image=] and abort all these steps. + + 2. Let |paintCtor| be the [=paint definition/class constructor=] on |definition|. + + 3. Let |paintInstance| be the result of [=Construct=](|paintCtor|). + + If [=construct=] throws an exception, + set the |definition|'s [=paint definition/constructor valid flag=] to false, + let the image output be an [=invalid image=] and abort all these steps. - 4. Add the key-value pair (|name| - |paintInstance|) to the paint name to instance - map of the |workletGlobalScope|. + 4. [=map/Set=] |paintClassInstanceMap|[|name|] to |paintInstance|. - 7. Let |inputProperties| be the result of looking up |name| on the associated document's - paint name to input properties map. + 6. Let |inputProperties| be |definition|'s [=paint definition/input properties=]. - 8. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the - computed value's for properties listed in |inputProperties|. + 7. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with only the + [=computed value=]'s for properties listed in |inputProperties|. - 9. Let |renderingContext| be the result of create a PaintRenderingContext2D object given: - - "width" - The width given by |concreteObjectSize|. - - "height" - The height given by |concreteObjectSize|. + 8. Let |renderingContext| be the result of [=create a PaintRenderingContext2D object=] given: + - "width" - The width given by |snappedConcreteObjectSize|. + - "height" - The height given by |snappedConcreteObjectSize|. + - "paintRenderingContext2DSettings" - The + [=paint definition/PaintRenderingContext2DSettings object=] given by |definition|. - Note: The |renderingContext| must not be re-used between invocations of paint. Implicitly - this means that there is no stored data, or state on the |renderingContext| between + 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. @@ -398,97 +617,186 @@ following steps: invoke methods on it, but this will have no effect on the current image, or subsequent images. - 10. Let |paintSize| be a new {{PaintSize}} initialized to the width and height defined by - |concreteObjectSize|. + 9. Let |paintSize| be a new {{PaintSize}} initialized to the width and height defined by + |snappedConcreteObjectSize|. + + 10. At this stage the user agent may re-use an image from a previous invocation if |paintSize|, + |styleMap|, |inputArguments| are equivalent to that previous invocation. If so let the image + output be that cached image and abort all these steps. + +
+ In the example below, both div-1 and div-2 have paint + functions which have equivalent javascript arguments. A user-agent can cache the result + of one invocation and use it for both elements. + +
+                // paint.js
+                registerPaint('simple', class {
+                    paint(ctx, size) {
+                        ctx.fillStyle = 'green';
+                        ctx.fillRect(0, 0, size.width, size.height);
+                    }
+                });
+            
+ +
+                <style>
+                    .div-1 {
+                        width: 50px;
+                        height: 50px;
+                        background-image: paint(simple);
+                    }
+                    .div-2 {
+                        width: 100px;
+                        height: 100px;
+
+                        background-size: 50% 50%;
+                        background-image: paint(simple);
+                    }
+                </style>
+                <div class=div-1></div>
+                <div class=div-2></div>
+                <script>
+                    CSS.paintWorklet.addModule('paint.js');
+                </script>
+            
+
+ + 11. Let |paintFunctionCallback| be |definition|'s [=paint function=]. + + 12. [=Invoke=] |paintFunctionCallback| with arguments «|renderingContext|, |paintSize|, + |styleMap|, |inputArguments|», and with |paintInstance| as the [=callback this value=]. + + If |paintFunctionCallback| does not complete within an acceptable time (as determined by the + user agent, i.e. it is a "long running script") the user agent may terminate the + script, let the image output be an [=invalid image=], and abort all these steps. + + Note: User agents could provide tooling within their debugging tools to show authors how + expensive their paint classes are. User agents could also how an "unresponsive script" + dialog in this case if appropriate. - 11. Let |paintFunction| be |definition|'s paint function. + 13. The image output is to be produced from the |renderingContext| given to the method. - 12. Invoke |paintFunction| with arguments «|renderingContext|, |paintSize|, |styleMap|», - and with |paintInstance| as the callback this value. + If an exception is [=thrown=] the let the image output be an [=invalid image=]. - 13. The image output is to be produced from the |renderingContext| given to the method. +Note: The contents of the resulting image are not designed to be accessible. Authors can communicate + any useful information through the standard accessibility APIs. +
+ +Global Scope Selection {#global-scope-selection} +------------------------------------------------ + +When the user agent needs to select a {{PaintWorkletGlobalScope}} from the paint [=worklet's +WorkletGlobalScopes=] [=list=] it must: - If an exception is thrown the let the image output be an invalid image. + - Select from at least two {{PaintWorkletGlobalScope}}s, unless the user agent is under + memory constraints. - 14. Set the paint valid flag for the |paintFunction| to paint-valid. + - Not re-use the same {{PaintWorkletGlobalScope}} more than 1000 times in a row. -Note: The user agent should consider long running paint functions similar to long running - script in the main execution context. For example, they should show a "unresponsive - script" dialog or similar. In addition user agents should provide tooling within their - debugging tools to show authors how expensive their paint classes are. + Note: The 1000 limit was picked as a high upper bound, this limit may improve (downwards) + over time. -Note: The contents of the resulting image are not designed to be accessible. Authors should - communicate any useful information through the standard accessibility APIs. +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.
-<div id="myElement">
-    CSS is awesome.
-</div>
+<!DOCTYPE html>
+<style>
+  #example {
+    --circle-color: deepskyblue;
 
-<style>
-#myElement {
-    --circle-color: red;
     background-image: paint(circle);
-}
-</style>
+    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 PaintWorkletGlobalScope.
+// circle.js
 registerPaint('circle', class {
-    static get inputProperties() { return ['--circle-color']; }
-    paint(ctx, geom, properties) {
-        // Change the fill color.
-        const color = properties.get('--circle-color');
-        ctx.fillStyle = color;
-
-        // Determine the center point and radius.
-        const x = geom.width / 2;
-        const y = geom.height / 2;
-        const radius = Math.min(x, y);
-
-        // Draw the circle \o/
-        ctx.beginPath();
-        ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
-        ctx.fill();
-    }
+  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} ------------------------------------------- +Example 2: Image Placeholder {#example-2} +----------------------------------------- It is possible for an author to use paint to draw a placeholder image while an image is being loaded.
-<div id="myElement">
-</div>
-
-<style>
-#myElement {
+<!DOCTYPE html>
+<style>
+#example {
     --image: url('#someUrlWhichIsLoading');
     background-image: paint(image-with-placeholder);
 }
-</style>
-
-<script>
-document.registerProperty({
-    name: '--image',
-    syntax: '<image>'
-});
-</script>
+</style>
+
+<div id="example"></div>
+
+<script>
+    CSS.registerProperty({
+        name: '--image',
+        syntax: '<image> | none',
+        initialValue: 'none',
+    });
+    CSS.paintWorklet.addModule('image-placeholder.js');
+</script>
 
-// Inside PaintWorkletGlobalScope.
-registerPaint('circle', class {
+// image-placeholder.js
+registerPaint('image-with-placeholder', class {
     static get inputProperties() { return ['--image']; }
     paint(ctx, geom, properties) {
         const img = properties.get('--image');
@@ -512,31 +820,115 @@ registerPaint('circle', class {
 });
 
-Example 3: Conic-gradient {#example-3} --------------------------------------- +Example 3: Arcs {#example-3} +---------------------------- + +
+<!DOCTYPE html>
+<style>
+#example {
+  width: 200px;
+  height: 200px;
+
+  background-image:
+    paint(arc, purple, 0.4turn, 0.8turn, 40px, 15px),
+    paint(arc, blue, -20deg, 170deg, 30px, 20px),
+    paint(arc, red, 45deg, 220deg, 50px, 10px);
+}
+</style>
+
+<div id="example"></div>
+
+<script>
+    CSS.paintWorklet.addModule('arc.js');
+</script>
+
-Issue: Add conic-gradient as a use case once we have function arguments. +
+// 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 color based on size {#example-4} ------------------------------------------------------ +Example 4: Different Colors (based on size) {#example-4} +--------------------------------------------------------
-<h1>
-Heading 1
-</h1>
-<h1>
-Another heading
-</h1>
-
-<style>
+<h1>
+    Heading 1
+</h1>
+<h1>
+    Another heading
+</h1>
+
+<style>
 h1 {
     background-image: paint(heading-color);
 }
-</style>
+</style>
+
+<script>
+    CSS.paintWorklet.addModule('heading-color.js');
+</script>
 
-// Inside PaintWorkletGlobalScope.
+// heading-color.js
 registerPaint('heading-color', class {
     static get inputProperties() { return []; }
     paint(ctx, geom, properties) {
@@ -551,3 +943,80 @@ registerPaint('heading-color', class {
     }
 });
 
+ +Example 5: Drawing outside an element's area {#example-5} +--------------------------------------------------------- + +It is possible to draw outside an element's area by using the 'border-image' property. + +
+<style>
+#overdraw {
+    --border-width: 10;
+
+    border-style: solid;
+    border-width: calc(var(--border-width) * 1px);
+
+    border-image-source: paint(overdraw);
+    border-image-slice: 0 fill;
+    border-image-outset: calc(var(--border-width) * 1px);
+
+    width: 200px;
+    height: 200px;
+}
+</style>
+<div id="overdraw"></div>
+<script>
+    CSS.paintWorklet.addModule('overdraw.js');
+</script>
+
+ +
+// overdraw.js
+registerPaint('overdraw', class {
+    static get inputProperties() { return ['--border-width']; }
+    paint(ctx, geom, properties) {
+        const borderWidth = parseInt(properties.get('--border-width'));
+        ctx.shadowColor = 'rgba(0,0,0,0.25)';
+        ctx.shadowBlur = borderWidth;
+
+        ctx.fillStyle = 'rgba(255, 255, 255, 1)';
+        ctx.fillRect(borderWidth,
+                     borderWidth,
+                     geom.width - 2 * borderWidth,
+                     geom.height - 2 * borderWidth);
+    }
+});
+
+ +Security Considerations {#security-considerations} +================================================== + +There are no known security issues introduced by these features. + +Privacy Considerations {#privacy-considerations} +================================================ + +* The timing of paint callbacks can be used as a high-bandwidth channel for detecting "visited" state for links. + (details) + This is not a fundamentally new privacy leak, + as visited state leaks from many interactions, + but absent any further mitigations, + this is a particularly high-bandwidth channel of the information. + + No official mitigations are planned at this time, + as this privacy leak needs to be addressed more directly + to fix all such channels. + +Changes {#changes} +================== + +Changes since the 9 August 2018 CR publication: + +* Filtered the list of input properties to a paint worklet to be only known or custom properties. + +* Added alpha flag to {{PaintRenderingContext2D}} to control whether the rendering surface is forced opaque or allows transparency. + +* Fix definition for the size of the output bitmap: + + > The size of the output bitmap is the concrete object size of the object it is rendering to 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/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 a7199536..82f9a00d 100644 --- a/css-parser-api/Overview.bs +++ b/css-parser-api/Overview.bs @@ -1,14 +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-properties-values-api/Overview.bs b/css-properties-values-api/Overview.bs index b416e3a3..1caace06 100644 --- a/css-properties-values-api/Overview.bs +++ b/css-properties-values-api/Overview.bs @@ -3,16 +3,18 @@ Title: CSS Properties and Values API Level 1 Status: ED Group: houdini ED: https://drafts.css-houdini.org/css-properties-values-api-1/ +TR: https://www.w3.org/TR/css-properties-values-api-1/ +Previous Version: https://www.w3.org/TR/2017/WD-css-properties-values-api-1-20171109/ Previous Version: http://www.w3.org/TR/2016/WD-css-properties-values-api-1-20160607/ Shortname: css-properties-values-api Level: 1 Abstract: This CSS module defines an API for registering new CSS properties. Properties registered using this API are provided with a parse syntax that defines a type, inheritance behaviour, and an initial value. -Editor: Tab Atkins, jackalmage@gmail.com -Editor: Shane Stephens, shanestephens@google.com -Editor: Daniel Glazman, daniel.glazman@disruptive-innovations.com -Editor: Alan Stearns, stearns@adobe.com -Editor: Elliot Sprehn, esprehn@chromium.org -Editor: Greg Whitworth, gwhit@microsoft.com +Editor: Tab Atkins-Bittner, Google, http://xanthir.com/contact/, w3cid 42199 +Former Editor: Shane Stephens, shanestephens@google.com, w3cid 47691 +Editor: Daniel Glazman, daniel.glazman@disruptive-innovations.com, w3cid 13329 +Editor: Alan Stearns, stearns@adobe.com, w3cid 46659 +Former Editor: Elliot Sprehn, esprehn@chromium.org +Editor: Greg Whitworth, gwhit@microsoft.com, w3cid 69511 Ignored Terms: boolean, Animatable, Map, Context, isolated worker, SyntaxError, Ignored Terms: InvalidModificationError, NotFoundError, StylePropertyMapReadOnly, Ignored Terms: worklet global scope @@ -21,22 +23,29 @@ Ignored Terms: construct, name map of inputs Ignored Vars: arguments, methodPropertyKey, inputStyleMap, workletGlobalScope Ignored Terms: WorkletGlobalContext Repository: w3c/css-houdini-drafts -
- -
-{
-  "css-paint-api": {
-    "title": "CSS Painting API"
-  },
-  "css-layout-api": {
-    "title": "CSS Layout API"
-  }
-}
+Markup Shorthands: css on, markdown on
 
Introduction {#intro} @@ -52,144 +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. +that have a value type, an initial value, and a defined inheritance behaviour, +via two methods: + +* A JS API, the {{registerProperty()}} method +* A CSS at-rule, the ''@property'' rule -This specification is complementary to [[css-paint-api]] and [[css-layout-api]], which +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. -Registering custom properties {#registering-custom-properties} -============================================================== -
-dictionary PropertyDescriptor {
-  required DOMString name;
-           DOMString syntax       = "*";
-           boolean   inherits     = false;
-           DOMString initialValue;
-};
 
-partial interface CSS {
-  void registerProperty(PropertyDescriptor descriptor);
-  void unregisterProperty(DOMString name);
-};
-
+ + +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} +---------------------------------------------------- -The {{PropertyDescriptor}} dictionary {#the-propertydescriptor-dictionary} --------------------------------------------------------------------------- +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} +------------------------------------------------------------- -A PropertyDescriptor dictionary represents author-specified configuration -options for a custom property. {{PropertyDescriptor}} dictionaries contain the -following members: +Note: As defined by [[css3-animations]] and [[css3-transitions]], it is possible to +specify animations and transitions that reference custom properties. -: name -:: The name of the custom property being defined. +When referenced by animations and transitions, +custom property values [=interpolate=] [=by computed value=], +in accordance with the type that they parsed as. -: syntax -:: A string representing how this custom property is parsed. +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. -: inherits -:: True if this custom property should inherit down the DOM tree; False otherwise. +As an exception to the above rule, +a value that parsed as a ``, +a ``, +or a `+` +instead interpolates as per the 'transform' property. -: initialValue -:: The initial value of this custom 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. -The {{registerProperty()}} function {#the-registerproperty-function} --------------------------------------------------------------------- +Note: Registering (or changing the registration) of a custom property +can change its computed value, +which can start or interrupt a CSS transition. -The registerProperty(PropertyDescriptor descriptor) method -registers a custom property according the to configuration options provided in -descriptor. +Conditional Rules {#conditional-rules} +-------------------------------------- -Attempting to register properties with a {{PropertyDescriptor/name}} that doesn't -correspond to the <> production must cause {{registerProperty()}} -to throw a {{SyntaxError}}. +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. +
-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>". +Substitution via ''var()'' {#substitution} +------------------------------------------ -Note: in future levels we anticipate supporting more sophisticated parse strings, e.g. -"<length> || <number>" +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. -Attempting to call {{registerProperty()}} with an {{PropertyDescriptor/initialValue}} that is -not parseable using the provided {{PropertyDescriptor/syntax}} must cause it to -throw a {{SyntaxError}}. If no {{PropertyDescriptor/initialValue}} is provided and the -{{PropertyDescriptor/syntax}} is '*', then a special initial value used. This initial -value must be considered parseable by {{registerProperty()}} but invalid at computed -value time. Initial values that are not computationally idempotent must also cause -{{registerProperty()}} to throw a {{SyntaxError}}. +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.
-For example, "3cm" is a computationally idempotent length, and hence valid as an initial value. -However, "3em" is not (depending on the environment, 3em could compute to -multiple different values). Additionally, "var(--foo)" is not computationally idempotent. + 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".
-Issue: define computational idempotency. +### 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. -Issue(121): Is computational idempotency the right thing to do here? We could also just -resolve any relative values once (against all the other initial values) and use -that. OR! We could allow specified values and just fix our engines... +Note: This applies regardless of whether or not the fallback is being used. -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 -. -If {{registerProperty()}} is called with a descriptor name that matches an already registered property, -then an {{InvalidModificationError}} is thrown and the re-registration fails. +### Dependency Cycles via Relative Units ### {#dependency-cycles} -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. +[=Registered custom properties=] follow the same rules for dependency cycle resolution +as unregistered [=custom properties=], +with the following additional constraints: -Successful calls to both {{registerProperty()}} and {{unregisterProperty()}} -change the set of registered properties. When the set of registered properties -changes, previously syntactically invalid property values can become valid and vice versa. -This can change the set of declared values which requires the cascade to -be recomputed. +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.
-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);
-}
-
+ 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. +
-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 computation time -(because url("not-a-color") is not a color). At computation time the only -available fallback is the default value, which in the case of color is "inherit". -if we call: -
-registerProperty({
-name: "--my-color",
-syntax: "<color>"
-});
+
+
+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 PropertyDefinition {
+	required DOMString name;
+	         DOMString syntax       = "*";
+	required boolean   inherits;
+	         DOMString initialValue;
+};
+
+partial namespace CSS {
+	undefined registerProperty(PropertyDefinition definition);
+};
 
-then the second --my-color declaration becomes syntactically invalid, which means that -the cascade uses the first declaration. The color therefore switches to green. +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|.
-Supported syntax strings {#supported-syntax-strings} ----------------------------------------------------- +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 PropertyDefinition dictionary represents author-specified configuration +options for a custom property. {{PropertyDefinition}} dictionaries contain the +following members: + +: name +:: The name of the custom property being defined. + +: syntax +:: A string representing how this custom property is parsed. + +: inherits +:: True if this custom property should inherit down the DOM tree; False otherwise. + +: initialValue +:: The initial value of this custom property. + -The following syntax strings are supported: + + +Syntax Strings {#syntax-strings} +================================ + +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]]. + +A syntax string can be parsed into a syntax definition, which is either: + + 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. + +Note: Regardless of the syntax specified, all custom properties accept +CSS-wide keywords, and process these values +appropriately. + +
+ 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 +
+ +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. + +The remainder of this chapter describes the internal grammar of the syntax +strings. + +Supported Names {#supported-names} +---------------------------------- + +This section defines the supported syntax component names, and the +corresponding types accepted by the resulting syntax component. : "<length>" :: Any valid <> value @@ -199,7 +865,7 @@ 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>" @@ -218,145 +884,410 @@ The following syntax strings are supported: :: Any valid <> value : "<custom-ident>" :: Any valid <> value -: Any string, the contents of which matches the <> production +: Any sequence which [[css-syntax-3#would-start-an-identifier|starts an identifier]], + [[css-syntax-3#consume-name|can be consumed as a name]], and matches the <> production :: That identifier -: Any one of the preceding strings, followed by '+' -:: A list of values of the type specified by the string -: Any combination of the preceding, separated by '|' -:: Any value that matches one of the items in the combination, matched in specified order. -: "*" -:: Any valid token stream - -Note: [[css3-values]] maintains a distinction between properties that accept -only a length, and properties that accept both a length and a percentage, -however the distinction doesn't currently cleanly line up with the productions. -Accordingly, this specification introduces the length-percentage production -for the purpose of cleanly specifying this distinction. - -Regardless of the syntax specified, all custom properties will accept -CSS-wide keywords as well as ''revert'', and process these values -appropriately. -Note: This does not apply to the {{PropertyDescriptor/initialValue}} member -of the {{PropertyDescriptor}} dictionary. + Note: <>s are compared codepoint-wise with each other; + this is different than the normal behavior of UA-defined CSS + which limits itself to ASCII + and is ASCII case-insensitive. + So, specifying an ident like Red + means that the precise value ''Red'' is accepted; + ''red'', ''RED'', and any other casing variants are not matched by this. + It is recommended that idents be restricted to ASCII and written in lower-case, + to match CSS conventions. + +: "<transform-list>" +:: A list of valid <> values. Note that + "<transform-list>" is a pre-multiplied data type name + equivalent to "<transform-function>+" + +Note: A syntax string of "*" will produce the + universal syntax definition, which is not a syntax component. + Therefore, "*" may not be [[#multipliers|multiplied]] or + [[#combinator|combined]] with anything else. + +The '+' and '#' Multipliers {#multipliers} +------------------------------------------ + +Any syntax component name except +pre-multiplied data type names may be immediately followed by a multiplier: + +: U+002B PLUS SIGN (+) +:: Indicates a space-separated list. + +: U+0023 NUMBER SIGN (#) +:: Indicates a comma-separated list.
-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 list of length values. + : "<length>+" + :: accepts a space-separated list of length values + : "<color>#" + :: accepts a comma-separated list of color values +
+Note: The multiplier must appear immediately after the syntax component name +being multiplied. + +The '|' Combinator {#combinator} +-------------------------------- + +Syntax strings may use U+007C VERTICAL LINE (|) to provide multiple +syntax component names. Such syntax strings will result in a +syntax definition with multiple syntax components. + +When a syntax definition with multiple syntax components is used +to parse a CSS value, the syntax components are matched in the order specified. + +Note: That is, given the syntax string "red | <color>", +matching the value ''red'' against it will parse as an identifier, +while matching the value ''blue'' will parse as a <>. + +
+ : "<length> | auto" + :: accepts a length, or auto + : "foo | <color># | <integer>" + :: accepts foo, a comma-separated list of color values, or a single integer
-Calculation of Computed Values {#calculation-of-computed-values} ----------------------------------------------------------------- +Parsing The Syntax String {#parsing-syntax} +------------------------------------------- -The syntax of a custom property fully determines how computed values are -generated from specified values for that property. +### Definitions ### {#parsing-definitions} -The CSS-wide keywords and ''revert'' generate computed values as -described in [[!css3-values]] and [[!css-cascade-4]] respectively. Otherwise: +: data type name +:: A sequence of code points consisting of a U+003C LESS-THAN SIGN (<), followed be zero or more name code points, and terminated by U+003E GREATER-THAN SIGN (>). -For <length> values, the computed value is the absolute length expressed in pixels. +: pre-multiplied data type name +:: A [=data type name=] that represents another [=syntax component=] with a [[#multipliers|multiplier]] already included. -For <length-percentage> values, the computed value is one of the following: -* if the specified value contains only length units, the computed value is the absolute length - expressed in pixels. -* if the specified value contains only percentages, the computed value is a - simple percentage. -* otherwise, the computed value is a calc expression containing an absolute - length expressed in pixels, and a percentage value. +: syntax component +:: An object consisting of a syntax component name, and an optional [[#multipliers|multiplier]]. -For <custom-ident>, ident, <color>, <image>, <url>, <integer>, -<angle>, <time>, <resolution>, <transform-function> or "*" values, the -computed value is identical to the specified value. +: syntax component name +:: A sequence of code points which is either a data type name, + or a sequence that can produce a <>. -For <number> and <percentage> values which are not calc expressions, the -computed value is identical to the specified value. Calc expressions that are -<number> and <percentage> values get reduced during computation to simple -numbers and percentages respectively. +: syntax definition +:: An object consisting of a list of syntax components. -For values specified by a syntax string that include "|" clauses, the computed -value is given by applying the calculation rules for the first clause that -matches to the specified value. +: universal syntax definition +:: A special syntax definition which accepts any valid token stream. -For list values, the computed value is a list of the computed values of the -primitives in the list. +### Consume a Syntax Definition ### {#consume-syntax-definition} -Behavior of Custom Properties {#behavior-of-custom-properties} -============================================================== +
+ This section describes how to consume a syntax definition from a [=string=] |string|. + It either produces a syntax definition + with a list of syntax components, or the universal syntax definition. -Animation Behavior of Custom Properties {#animation-behavior-of-custom-properties} ----------------------------------------------------------------------------------- + 1. [=Strip leading and trailing ASCII whitespace=] from |string|. -Note: As defined by [[css3-animations]] and [[css3-transitions]], it is possible to -specify animations and transitions that reference custom properties. + 2. If |string|’s [=string/length=] is 0, + return failure. -When referenced by animations and transitions, custom properties 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]]. -Otherwise, the interpolation falls back to the default 50% flip described in -[[!css3-animations]]. + 3. If |string|’s [=string/length=] is 1, + and the only [=code point=] in |string| is U+002A ASTERISK (*), + return the [=universal syntax definition=]. -Issue: Intermediate interpolated results of animations on custom properties must -be able to generate a token stream representing their value. We should ensure that -this is standard across implementations to avoid interop issues. + 4. Let |stream| be an [=input stream=] created from the [=code points=] of |string|, + preprocessed as specified in [[css-syntax-3]]. + Let |definition| be an initially empty [=list=] of syntax components. -Conditional Rules {#conditional-rules} --------------------------------------- + 5. Consume a syntax component from |stream|. + If failure was returned, + return failure; + otherwise, + [=list/append=] the returned value to |definition|. -''@supports'' rules and the {{CSS/supports(conditionText)}} method behave as specified -in [[!css-variables]]. + Consume as much whitespace as possible from |stream|. -Note: In other words, for the purpose of determining whether a value is -supported by a given custom property, the type registered for the custom property is -ignored and any value consisting of at least one token is considered valid. + Consume the next input code point in |stream|: + : EOF + :: return |definition|. -Issue(118): should @supports pay attention to type when considering custom properties? + : U+007C VERTICAL LINE (|) + :: Repeat step 5. -Examples {#examples} -==================== + : Anything else: + :: Return failure. -Example 1: Using custom properties to add animation behavior {#example-1} -------------------------------------------------------------------------- +
+ +### Consume a Syntax Component ### {#consume-syntax-component} + +
+ To consume a syntax component from a stream of code points |stream|: + + Consume as much whitespace as possible from |stream|. + + Let |component| be a new syntax component with its |name| and |multiplier| initially + empty. + + Consume the next input code point in |stream|: + + : U+003C LESS-THAN SIGN (<) + :: Consume a data type name from |stream|. + If it returned a [=string=], set |component|'s |name| to the returned value. + Otherwise, return failure. -
-<script>
-CSS.registerProperty({
-  name: "--stop-color",
-  syntax: "<color>",
-  inherits: false,
-  initialValue: "rgba(0,0,0,0)"
-});
-</script>
+	:   name-start code point
+	:   U+005C REVERSE SOLIDUS (\)
+	::  If the stream [=starts with an identifier=],
+		reconsume the current input code point from |stream|
+		then [=consume a name=] from |stream|,
+		and set |component|’s |name| to the returned value.
+		Otherwise return failure.
 
-<style>
+		If |component|’s |name| does not [=CSS/parse=] as a <>,
+		return failure.
 
-.button {
-  --stop-color: red;
-  background: linear-gradient(var(--stop-color), black);
-  transition: --stop-color 1s;
-}
+	:   anything else
+	::  Return failure.
 
-.button:hover {
-  --stop-color: green;
-}
+	If |component|’s |name| is a [=pre-multiplied data type name=],
+	return |component|.
 
-</style>
+	If the [=next input code point=] in |stream|
+	is U+002B PLUS SIGN (+)
+	or U+0023 NUMBER SIGN (#),
+	consume the next input code point from |stream|,
+	and set |component|’s |multiplier| to the [=current input code point=].
 
+	Return |component|.
+
+ +### Consume a Data Type Name ### {#consume-data-type-name} + +
+ To consume a data type name from a stream of code points: + + Note: This algorithm assumes that a U+003C LESS-THAN SIGN (<) code point has already been consumed from the stream. + + Let |name| initially be a [=string=] containing a single U+003C LESS-THAN SIGN (<) code point. + + Repeatedly consume the next input code point: + + : U+003E GREATER-THAN SIGN (>) + :: Append the code point to |name|. + If |name| is a [=supported syntax component name=], + return |name|. + Otherwise return failure. + + : name code point + :: Append the code point to |name|. + + : anything else + :: Return failure. +
+ + + + + + + + + +CSSOM {#cssom} +============== + +
+ The value specified for a registered custom property is not + interpreted until computed-value time. This means that only APIs that + retrieve computed values are affected. Other APIs must ignore the + {{[[registeredPropertySet]]}} slot of the associated {{Document}}, and + treat all custom properties as unregistered. +
+ +The CSSPropertyRule Interface {#the-css-property-rule-interface} +----------------------------------------------------------------------------- + +The {{CSSPropertyRule}} interface represents an ''@property'' rule. + +
+[Exposed=Window]
+interface CSSPropertyRule : CSSRule {
+    readonly attribute CSSOMString name;
+    readonly attribute CSSOMString syntax;
+    readonly attribute boolean inherits;
+    readonly attribute CSSOMString? initialValue;
+};
+
+ +
+
name +
+ The custom property name associated with the ''@property'' rule. + +
syntax +
+ The syntax associated with the ''@property'', exactly as specified. + +
inherits +
+ The inherits descriptor associated with the ''@property'' rule. + +
initialValue +
+ The initial value associated with the ''@property'' rule, + which may not be present. +
+ +
+To serialize a CSSPropertyRule, return the concatenation of +the following: + + 1. The string "@property" followed by a single SPACE (U+0020). + 2. The result of performing serialize an identifier on the rule's + name, followed by a single SPACE (U+0020). + 3. The string "{ ", i.e., a single + LEFT CURLY BRACKET (U+007B), followed by a SPACE (U+0020). + 4. The string "syntax:", followed by a single SPACE (U+0020). + 5. The result of performing serialize a string on the rule's + 'syntax', followed by a single SEMICOLON (U+003B), followed by a + SPACE (U+0020). + 6. The string "inherits:", followed by a single SPACE (U+0020). + 7. For the rule's 'inherits' attribute, one of the following depending on + the attribute's value: +
+ : true + :: The string "true" followed by a single + SEMICOLON (U+003B), followed by a SPACE (U+0020). + : false + :: The string "false" followed by a single + SEMICOLON (U+003B), followed by a SPACE (U+0020). +
+ 8. If the rule's 'initial-value' is present, follow these substeps: + 1. The string "initial-value:". + 2. The result of performing serialize a CSS value in the rule's + 'initial-value' followed by a single SEMICOLON (U+003B), followed by + a SPACE (U+0020). + 9. A single RIGHT CURLY BRACKET (U+007D). +
+ +{{CSSStyleValue}} Reification {#css-style-value-reification} +------------------------------------------------------------ + +
+ To reify a registered custom property value given a property + |property| and [=syntax definition=] |syntax|, run these steps: + + For specified values, [=reify a list of component values=] from the value, + and return the result. + + For computed values: + + 1. If the value is a <>, <>, <>, <>, + <
+ + + + + +Examples {#examples} +==================== + +Example 1: Using custom properties to add animation behavior {#example-1} +------------------------------------------------------------------------- + + + <script> + CSS.registerProperty({ + name: "--stop-color", + syntax: "<color>", + inherits: false, + initialValue: "rgba(0,0,0,0)" + }); + </script> + + <style> + .button { + --stop-color: red; + background: linear-gradient(var(--stop-color), black); + transition: --stop-color 1s; + } + + .button:hover { + --stop-color: green; + } + </style> + + +Example 2: Using ''@property'' to register a property {#example-2} +------------------------------------------------------------------ + + <script> + CSS.paintWorklet.addModule('circle.js'); + </script> + <style> + @property --radius { + syntax: "<length>"; + inherits: false; + initial-value: 0px; + } + + div { + width: 100px; + height: 100px; + --radius: 10px; + background: paint(circle); + transition: --radius 1s; + } + + div:hover { + --radius: 50px; + } + </style> + <div></div> + + +
+	// circle.js
+	registerPaint('circle', class {
+			static get inputProperties() { return ['--radius']; }
+			paint(ctx, geom, properties) {
+				let radius = properties.get('--radius').value;
+				ctx.fillStyle = 'black';
+				ctx.beginPath();
+				ctx.arc(geom.width / 2, geom.height / 2, radius, 0, 2 * Math.PI);
+				ctx.fill();
+			}
+	});
 
Security Considerations {#security-considerations} diff --git a/css-properties-values-api/Overview-2.bs b/css-properties-values-api/apply-hook-ideas.txt similarity index 100% rename from css-properties-values-api/Overview-2.bs rename to css-properties-values-api/apply-hook-ideas.txt diff --git a/css-typed-om/Overview-2.bs b/css-typed-om-2/Overview.bs similarity index 82% rename from css-typed-om/Overview-2.bs rename to css-typed-om-2/Overview.bs index 84029d9d..4afac178 100644 --- a/css-typed-om/Overview-2.bs +++ b/css-typed-om-2/Overview.bs @@ -2,11 +2,12 @@ Title: CSS Typed OM Level 2 Status: DREAM Group: houdini -ED: https://drafts.css-houdini.org/css-typed-om-1/ -Shortname: css-typed-om-2 +ED: https://drafts.css-houdini.org/css-typed-om-2/ +Shortname: css-typed-om Level: 2 Abstract: -Editor: Shane Stephens, shanestephens@google.com +Editor: Tab Atkins-Bittner, Google, http://xanthir.com/contact/, w3cid 42199 +Former Editor: Shane Stephens, shanestephens@google.com, w3cid 47691 Repository: w3c/css-houdini-drafts
@@ -31,3 +32,5 @@ Issue(142): Do we want a font value class? How about a FontWeightValue (for 100, Issue(144): Add more CSS3 properties. This table currently only contains CSS2.1 properties and CSS3 properties alphabetically to border-radius. Issue(136): Need an array-buffer custom property type. + +Issue(310): consider using properties in addition to get/set. diff --git a/css-typed-om/Overview.bs b/css-typed-om/Overview.bs index 79be972a..ac32d641 100644 --- a/css-typed-om/Overview.bs +++ b/css-typed-om/Overview.bs @@ -3,1495 +3,5537 @@ Title: CSS Typed OM Level 1 Status: ED Group: houdini ED: https://drafts.css-houdini.org/css-typed-om-1/ +TR: https://www.w3.org/TR/css-typed-om-1/ +Previous Version: https://www.w3.org/TR/2018/WD-css-typed-om-1-20180410/ +Previous Version: https://www.w3.org/TR/2017/WD-css-typed-om-1-20170801/ Previous Version: https://www.w3.org/TR/2016/WD-css-typed-om-1-20160607/ Shortname: css-typed-om Level: 1 Abstract: Converting CSSOM value strings into meaningfully typed JavaScript representations and back can incur a significant performance overhead. This specification exposes CSS values as typed JavaScript objects to facilitate their performant manipulation. -Editor: Shane Stephens, shanestephens@google.com +Editor: Tab Atkins-Bittner, Google, http://xanthir.com/contact/, w3cid 42199 +Editor: François Remy, Microsoft, w3cid 53348 +Former Editor: Shane Stephens, shanestephens@google.com, w3cid 47691 +Former Editor: Naina Raisinghani, Google, nainar@google.com, w3cid 100915 Repository: w3c/css-houdini-drafts +Inline Github Issues: title Ignored Vars: type, unit -Ignored Terms: Drawable +Ignored Terms: Drawable, paint image definition, list-valued property, list-valued property iteration, parse a CSSStyleValue, values to iterate over, CSS, PseudoElement, style +Markup Shorthands: markdown yes + +
 urlPrefix: http://www.ecma-international.org/ecma-262/6.0/#sec-; type: dfn; spec: ecma-262
     text: RangeError; url: native-error-types-used-in-this-standard-rangeerror
 
Introduction {#intro} ===================== -Converting CSSOM value strings into meaningfully typed JavaScript representations and -back can incur a significant performance overhead. This specification exposes CSS values -as typed JavaScript objects to facilitate their performant manipulation. - -The API exposed by this specification is designed for performance rather than ergonomics. -Some particular considerations: -* retrieved JavaScript representations of CSS values are not mutable - instead updates - must explicitly be set using the API. -* objects are organized for consistency rather than ease of access. For example, even - though lengths are often numeric pixel values, a specified {{CSSLengthValue}} can't be - treated as a number without first explicitly casting it to a {{CSSSimpleLength}}, as calc - expressions and keywords are also valid lengths. +CSS stylesheets are parsed into abstract UA-internal data structures, +the internal representations of CSS, +which various specification algorithms manipulate. + +[=Internal representations=] can't be directly manipulated, +as they are implementation-dependent; +UAs have to agree on how to *interpret* the [=internal representations=], +but the representations themselves are purposely left undefined +so that UAs can store and manipulate CSS +in whatever way is most efficient for them. + +Previously, the only way to read or write to the [=internal representations=] +was via strings-- +stylesheets or the CSSOM allowed authors to send strings to the UA, +which were parsed into [=internal representations=], +and the CSSOM allowed authors to request +that the UA serialize their [=internal representations=] back into strings. + +This specification introduces a new way to interact with [=internal representations=], +by representing them with specialized JS objects +that can be manipulated and understood more easily and more reliably +than string parsing/concatenation. +This new approach is both easier for authors +(for example, numeric values are reflected with actual JS numbers, +and have unit-aware mathematical operations defined for them) +and in many cases are more performant, +as values can be directly manipulated +and then cheaply translated back into [=internal representations=] +without having to build and then parse strings of CSS. + + {{CSSStyleValue}} objects {#stylevalue-objects} ============================================ -
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
 interface CSSStyleValue {
-  attribute DOMString cssText;
-  static (CSSStyleValue or sequence&lt;CSSStyleValue>)? parse(DOMString property, DOMString cssText);
+    stringifier;
+    [Exposed=Window] static CSSStyleValue parse(USVString property, USVString cssText);
+    [Exposed=Window] static sequence<CSSStyleValue> parseAll(USVString property, USVString cssText);
 };
-</pre>
+
 
-{{CSSStyleValue}} objects are the base class of all CSS Values accessible via the Typed OM API. Unsupported values
-are also represented as {{CSSStyleValue}} objects.
+{{CSSStyleValue}} objects are the base class of all CSS values accessible via the Typed OM API.
 
-The cssText attribute provides a normalized
-representation (see ) of the value
-contained by a {{CSSStyleValue}} object.
+The stringification behavior of {{CSSStyleValue}} objects
+is defined in [[#stylevalue-serialization]].
 
-The parse(DOMString property, DOMString cssText)
-method attempts to parse cssText as a valid {{CSSStyleValue}} or sequence<{{CSSStyleValue}}> for
-property, returning null on failure.
+The parse(|property|, |cssText|) method,
+when invoked,
+must [=parse a CSSStyleValue=]
+with property |property|, cssText |cssText|, and parseMultiple set to false,
+and return the result.
 
-{{CSSTokenStreamValue}} objects {#tokenstreamvalue-objects}
-----------------------------------------------
+The parseAll(|property|, |cssText|) method,
+when invoked,
+must [=parse a CSSStyleValue=]
+with property |property|, cssText |cssText|, and parseMultiple set to true,
+and return the result.
 
-
-interface CSSTokenStreamValue : CSSStyleValue {
-  iterable<(DOMString or CSSVariableReferenceValue)>;
-};
+
+ To parse a CSSStyleValue given a [=string=] |property|, a [=string=] |cssText|, + and a |parseMultiple| flag, run these steps: -interface CSSVariableReferenceValue { - attribute DOMString variable; - attribute CSSTokenStreamValue fallback; -}; -
+ 1. If |property| is not a [=custom property name string=], + set |property| to |property| [=ASCII lowercased=]. + + 2. If |property| is not a [=valid CSS property=], + [=throw=] a {{TypeError}}. + + 3. Attempt to [=CSS/parse=] |cssText| according to |property|’s grammar. + If this fails, + [=throw=] a {{TypeError}}. + Otherwise, + let |whole value| be the parsed result. -{{CSSTokenStreamValue}} objects represent values that reference custom properties. They represent a list of string fragments and variable references. +
+ The behavior of custom properties are different when modified via JavaScript + than when defined in style sheets. + + When a custom property is defined with an invalid syntax in a style sheet, + then the value is recorded as "unset", to avoid having to reparse every style sheet when a custom property is registered. + + Conversely, when a custom property is modified via the JavaScript API, + any parse errors are propagated to the progamming environment via a {{TypeError}}. + This allows more immediate feedback of errors to developers. +
+ + 4. [=Subdivide into iterations=] |whole value|, + according to |property|, + and let |values| be the result. + + 5. [=list/For each=] |value| in |values|, + replace it with the result of [=reifying=] |value| for |property|. + + Issue: Define the global. + + 6. If |parseMultiple| is false, + return |values|[0]. + Otherwise, return |values|. + + +
+ To subdivide into iterations + a CSS value |whole value| + for a property |property|, + execute the following steps: + + 1. If |property| is a [=single-valued property=], + return a [=list=] containing |whole value|. + + 2. Otherwise, + divide |whole value| into individual iterations, + as appropriate for |property|, + and return a [=list=] containing the iterations in order. + +
+ How to divide a [=list-valued property=] into iterations + is intentionally undefined and hand-wavey at the moment. + Generally, + you just split it on top-level commas + (corresponding to a top-level `#` term in the grammar), + but some legacy properties (such as 'counter-reset') + don't separate their iterations with commas. + + It's expected to be rigorously defined in the future, + but at the moment is explicitly a "you know what we mean" thing. +
+
+ + +Direct {{CSSStyleValue}} Objects {#direct-cssstylevalue} +------------------------------------------------ + +Values that can't yet be directly supported by a more specialized {{CSSStyleValue}} subclass +are instead represented as {{CSSStyleValue}} objects. + +Each {{CSSStyleValue}} object is associated with a particular CSS property, +via its {{[[associatedProperty]]}} internal slot, +and a particular, immutable, [=internal representation=]. +These objects are said to "represent" the particular [=internal representation=] +they were [=reified=] from, +such that if they are set back into a stylesheet for the same property, +they reproduce an equivalent [=internal representation=]. + +These {{CSSStyleValue}} objects are only considered valid for the property that they were parsed for. +This is enforced by {{CSSStyleValue}} objects having a \[[associatedProperty]] internal slot, +which is either `null` (the default) +or a [=string=] specifying a property name. + +Note: This slot is checked by {{StylePropertyMap}}.{{set()}}/{{append()}} + + The {{StylePropertyMap}} {#the-stylepropertymap} ================================================ -
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
 interface StylePropertyMapReadOnly {
-  CSSStyleValue? get(DOMString property);
-  sequence&lt;CSSStyleValue> getAll(DOMString property);
-  boolean has(DOMString property);
-  iterable&lt;DOMString, (CSSStyleValue or sequence&lt;CSSStyleValue> or DOMString)>;
-  sequence&lt;DOMString> getProperties();
-  stringifier;
+    iterable<USVString, sequence<CSSStyleValue>>;
+    any get(USVString property);
+    /* 'any' means (undefined or CSSStyleValue) here,
+       see https://github.com/heycam/webidl/issues/60 */
+    sequence<CSSStyleValue> getAll(USVString property);
+    boolean has(USVString property);
+    readonly attribute unsigned long size;
 };
 
+[Exposed=Window]
 interface StylePropertyMap : StylePropertyMapReadOnly {
-  void append(DOMString property, (CSSStyleValue or DOMString)... values);
-  void delete(DOMString property);
-  void set(DOMString property, (CSSStyleValue or DOMString)... values);
+    undefined set(USVString property, (CSSStyleValue or USVString)... values);
+    undefined append(USVString property, (CSSStyleValue or USVString)... values);
+    undefined delete(USVString property);
+    undefined clear();
 };
-</pre>
+
+
+{{StylePropertyMap}} is an alternate way to represent a [=CSS declaration block=] as an object
+(when fetched via the [[cssom]],
+[=CSS declaration blocks=] are instead represented as {{CSSStyleDeclaration}} objects.)
+
+
+
+
+ A {{StylePropertyMapReadOnly}} object + has a \[[declarations]] internal slot, + which is a [=map=] reflecting the [=CSS declaration block=]'s [=CSSStyleDeclaration/declarations=]. + + Note: The [=CSSStyleDeclaration/declarations=] are not yet defined using [[infra]] terminology, + but for the purpose of this spec + it's assumed to be a [=map=] whose keys are [=strings=] (representing property names) + and whose values are [=internal representations=] for those properties. + + Unless otherwise stated, + the initial ordering of the {{[[declarations]]}} internal slot + is based on the key of each entry: + + 1. Standardized properties + (not [=custom properties=] or vendor-prefixed properties), + [=ASCII lowercased=] + and then sorted in increasing code-point order. + 2. Vendor-prefixed/experimental properties + (those whose name starts with a single dash), + [=ASCII lowercased=] + and then sorted in increasing code-point order. + 3. [=Custom properties=], + sorted in increasing code-point order. + (These are never lower-cased; + they are preserved exactly as written.) +
-A {{StylePropertyMapReadOnly}} object has an associated property model, -which is a list of property - sequence<{{CSSStyleValue}}> pairs. This list -is initialized differently depending on where the {{CSSStyleValue}} is used -(see -, -, and -). +
+ The [=value pairs to iterate over=] + for a {{StylePropertyMapReadOnly}} object |this| + are obtained as follows: + + 1. Let |declarations| be |this|'s {{[[declarations]]}} slot. + + 2. Let |value pairs| be an empty [=list=]. + + 3. [=map/For each=] |prop| → |value| in |declarations|: + 1. Let |iterations| be the result of [=subdivide into iterations|dividing into iterations=] |value|. + 2. [=Reify=] each [=list/item=] of |iterations|, + and let |objects| be the result. + 3. Append |prop|/|objects| to |value pairs|. + + 4. Return |value pairs|. +
+ +Some CSS properties are list-valued properties, +such as 'background-image' or 'animation'; +their value is a list of parallel grammar terms, +almost always comma-separated +(the only exceptions are certain legacy properties like 'counter-reset'), +indicating multiple distinct "values" interpreted in the same way. +Other properties, +such as 'color', +are single-valued properties; +they take only a single (possibly complex) value. + +Issue(644): Define precisely which properties are list-valued and which aren't, +probably in an appendix.
-The sequence of {{CSSStyleValue}}s associated with a property do -not represent multiple successive definitions of that property's value. -Instead, sequences represent values associated with list-valued properties. + There are multiple examples of CSS properties + that have transitioned from being [=single-valued=] to [=list-valued=]. + To ensure that code written at a time when a property was [=single-valued=] + does not break when it becomes [=list-valued=] in the future, + the {{StylePropertyMap}} is a multi-map; + it stores list of values for each key, + but allows you to interact with it as if there was only a single value for each key as well. + + This means that multiple values for a single property in a {{StylePropertyMap}} + do not represent multiple successive definition of that property's value; + instead, they represent multiple comma-separated sub-values in a single property value, + like each "layer" in a 'background-image' property. +
+ +
+ The get(|property|) method, + when called on a {{StylePropertyMap}} |this|, + must perform the following steps: + + 1. If |property| is not a [=custom property name string=], + set |property| to |property| [=ASCII lowercased=]. + + 2. If |property| is not a [=valid CSS property=], + [=throw=] a {{TypeError}}. + + 3. Let |props| be the value of |this|’s {{[[declarations]]}} internal slot. + + 4. If |props|[|property|] [=map/exists=], + [=subdivide into iterations=] |props|[|property|], + then [=reify=] the first item of the result + and return it. + + Otherwise, return `undefined`. + + Issue: Define the global. +
+ +
+ The getAll(|property|) method, + when called on a {{StylePropertyMap}} |this|, + must perform the following steps: + + 1. If |property| is not a [=custom property name string=], + set |property| to |property| [=ASCII lowercased=]. + + 2. If |property| is not a [=valid CSS property=], + [=throw=] a {{TypeError}}. + + 3. Let |props| be the value of |this|’s {{[[declarations]]}} internal slot. + + 4. If |props|[|property|] [=map/exists=], + [=subdivide into iterations=] |props|[|property|], + then [=reify=] each [=list/item=] of the result, + and return the list. + + Otherwise, return an empty [=list=]. + + Issue: Define the global. +
+ +
+ The has(|property|) method, + when called on a {{StylePropertyMap}} |this|, + must perform the following steps: + + 1. If |property| is not a [=custom property name string=], + set |property| to |property| [=ASCII lowercased=]. + + 2. If |property| is not a [=valid CSS property=], + [=throw=] a {{TypeError}}. + + 3. Let |props| be the value of |this|’s {{[[declarations]]}} internal slot. + + 4. If |props|[|property|] [=map/exists=], + return `true`. + Otherwise, return `false`. +
+ +
+ The size attribute, + on getting from a {{StylePropertyMap}} |this|, + must perform the following steps: + + 1. Return the [=map/size=] of the value of |this|’s {{[[declarations]]}} internal slot. +
+ +
+ The set(|property|, ...|values|) method, + when called on a {{StylePropertyMap}} |this|, + must perform the following steps: + + 1. If |property| is not a [=custom property name string=], + set |property| to |property| [=ASCII lowercased=]. + + 2. If |property| is not a [=valid CSS property=], + [=throw=] a {{TypeError}}. + + 3. If |property| is a [=single-valued property=] and |values| has more than one [=list/item=], + [=throw=] a {{TypeError}}. + + 4. If any of the [=list/items=] in |values| have a non-null {{[[associatedProperty]]}} internal slot, + and that slot's value is anything other than |property|, + [=throw=] a {{TypeError}}. + + 5. If the [=list/size=] of |values| is two or more, + and one or more of the [=list/items=] are a {{CSSUnparsedValue}} or {{CSSVariableReferenceValue}} object, + [=throw=] a {{TypeError}}. + + Note: Having 2+ values implies that you're setting multiple items of a list-valued property, + but the presence of a ''var()'' function in the string-based OM disables all syntax parsing, + including splitting into individual iterations + (because there might be more commas inside of the ''var()'' value, + so you can't tell how many items are actually going to show up). + This step's restriction preserves the same semantics in the Typed OM. + + 5. Let |props| be the value of |this|’s {{[[declarations]]}} internal slot. + + 6. If |props|[|property|] [=map/exists=], + [=map/remove=] it. + + 7. Let |values to set| be an empty [=list=]. + + 8. For each |value| in |values|, + [=create an internal representation=] for |property| and |value|, + and append the result to |values to set|. + + 9. Set |props|[|property|] to |values to set|. + + Note: The property is deleted then added back + so that it gets put at the end of the [=ordered map=], + which gives the expected behavior in the face of [=shorthand properties=]. +
+ +
+ The append(|property|, ...|values|) method, + when called on a {{StylePropertyMap}} |this|, + must perform the following steps: + + 1. If |property| is not a [=custom property name string=], + set |property| to |property| [=ASCII lowercased=]. + + 2. If |property| is not a [=valid CSS property=], + [=throw=] a {{TypeError}}. + + 3. If |property| is not a [=list-valued property=], + [=throw=] a {{TypeError}}. + + 4. If any of the [=list/items=] in |values| have a non-null {{[[associatedProperty]]}} internal slot, + and that slot's value is anything other than |property|, + [=throw=] a {{TypeError}}. + + 5. If any of the [=list/items=] in |values| + are a {{CSSUnparsedValue}} or {{CSSVariableReferenceValue}} object, + [=throw=] a {{TypeError}}. + + Note: When a property is set via string-based APIs, + the presence of ''var()'' in a property + prevents the entire thing from being interpreted. + In other words, + everything *besides* the ''var()'' is a plain [=component value=], + not a meaningful type. + This step's restriction preserves the same semantics in the Typed OM. + + 5. Let |props| be the value of |this|’s {{[[declarations]]}} internal slot. + + 6. If |props|[|property|] does not [=map/exist=], + [=map/set=] |props|[|property|] to an empty [=list=]. + + 7. If |props|[|property|] contains a ''var()'' reference, + [=throw=] a {{TypeError}}. + + 7. Let |temp values| be an empty [=list=]. + + 8. For each |value| in |values|, + [=create an internal representation=] with |property| and |value|, + and [=list/append=] the returned value to |temp values|. + + + + 9. [=list/Append=] the entries of |temp values| to |props|[|property|]. +
+ +
+ The delete(|property|) method, + when called on a {{StylePropertyMap}} |this|, + must perform the following steps: + + 1. If |property| is not a [=custom property name string=], + set |property| to |property| [=ASCII lowercased=]. -This approach allows single-valued properties to become list-valued in the -future without breaking code that relies on calling -get() and/or -set() for those properties. + 2. If |property| is not a [=valid CSS property=], + [=throw=] a {{TypeError}}. + + 3. If |this|’s {{[[declarations]]}} internal slot [=map/contains=] |property|, + [=map/remove=] it.
-When invoked, the -append(DOMString property (CSSStyleValue or DOMString)... values) -method follows the following steps: +
+ The clear() method, + when called on a {{StylePropertyMap}} |this|, + must perform the following steps: -Issue(143): first need to check whether the property is a valid property. + 1. [=map/Remove=] all of the declarations in |this|’s {{[[declarations]]}} internal slot. +
-1. - : if property is not list-valued - :: throw a TypeError +
+ To create an internal representation, + given a [=string=] |property| + and a [=string=] or {{CSSStyleValue}} |value|: -1. - : if the property model has no entry for property - :: initialize an empty sequence in the property model for property + : If |value| is a direct {{CSSStyleValue}}, + :: + Return |value|'s associated value. -1. - : for each value in values, if value is a {{CSSStyleValue}}, and its type is a type that property can't accept - :: throw a TypeError + : If |value| is a {{CSSStyleValue}} subclass, + :: If |value| does not [=match the grammar=] of a [=list-valued property iteration=] of |property|, + [=throw=] a {{TypeError}}. -1. - : for each value in values, if value is a DOMString - :: set value to the result of invoking parse(), providing property - and value as inputs. + If any component of |property|’s CSS grammar has a limited numeric range, + and the corresponding part of |value| is a {{CSSUnitValue}} that is outside of that range, + replace that value with the result of wrapping it in a fresh {{CSSMathSum}} + whose {{CSSMathSum/values}} internal slot contains only that part of |value|. -1. - : if any value in values is null - :: throw a TypeError - : else - :: concatenate values onto the end of the entry in the property model + Return the |value|. + : If |value| is a {{USVString}}, + :: [=Parse a CSSStyleValue=] with property |property|, cssText |value|, and parseMultiple set to `false`, + and return the result. -Issue(145): should refactor out value type-checking, as it'll be needed by the rest of the setters too + Note: This can throw a {{TypeError}} instead. +
-Issue(147): need a robust description of what "a type that property can't accept" means. +
+ CSS properties express their valid inputs with grammars, + which are written with the assumption of being matched against strings parsed into CSS tokens, + as defined in [[css-syntax-3#tokenization]]. + {{CSSStyleValue}} objects can also be matched against these grammars, however. + + A {{CSSStyleValue}} is said to + match a grammar + based on the following rules: + + * A {{CSSKeywordValue}} matches an [=ident=] specified in a grammar + if its {{CSSKeywordValue/value}} internal slot + matches the [=ident=]. + + If case-folding rules are in effect normally for that [=ident=] + (such as ''Auto'' matching the keyword ''auto'' specified in the grammar for 'width'), + they apply to this comparison as well. + * A {{CSSTransformValue}} matches <>. + * A {{CSSNumericValue}} matches what its type [=CSSNumericValue/matches=]. + * A {{CSSURLImageValue}} matches <>. + * Any subclass of {{CSSImageValue}} matches <>. + * A {{CSSUnparsedValue}} matches any grammar. + * A direct {{CSSStyleValue}} object (not a subclass) + with a non-null {{CSSStyleValue/[[associatedProperty]]}} slot + matches the grammar of the property specified in its {{CSSStyleValue/[[associatedProperty]]}} slot, + regardless of what it is. + + Note: As the ability to create more complex values in Typed OM increases, + this section will become more complex. +
-Issue(148): add detailed descriptions of the rest of the methods on {{StylePropertyMap}} +A [=string=] is a custom property name string +if it starts with two dashes (U+002D HYPHEN-MINUS), like --foo. +(This corresponds to the <> production, +but applies to [=strings=], +rather than [=identifiers=]; +it can be used without invoking the CSS parser.) -Issue(149): describe that these are not live objects +A [=string=] is a valid CSS property +if it is a [=custom property name string=], +or is a CSS property name recognized by the user agent. Computed {{StylePropertyMapReadOnly}} objects {#computed-stylepropertymapreadonly-objects} --------------------------------------------------------------------------
-partial interface Window {
-  StylePropertyMapReadOnly getComputedStyleMap(Element element, optional DOMString? pseudoElt);
+partial interface Element {
+    [SameObject] StylePropertyMapReadOnly computedStyleMap();
 };
 
-Computed StylePropertyMap objects represent the computed style of an -{{Element}} or pseudo element, and are accessed by calling the -getComputedStyleMap(Element, optional DOMString?) -method. +Computed StylePropertyMap objects +represent the [=computed values=] of an {{Element}}, +and are accessed by calling the {{Element/computedStyleMap()}} method. + +Every {{Element}} has a \[[computedStyleMapCache]] internal slot, +initially set to `null`, +which caches the result of the {{Element/computedStyleMap()}} method +when it is first called. + +
+ The computedStyleMap() method must, + when called on an {{Element}} |this|, + perform the following steps: + + 1. If |this|'s {{Element/[[computedStyleMapCache]]}} internal slot is set to `null`, + set its value to a new {{StylePropertyMapReadOnly}} object, + whose {{[[declarations]]}} internal slot are the name and [=computed value=] of + every longhand CSS property supported by the User Agent, + every registered [=custom property=], + and every non-registered [=custom property=] which is not set to its initial value + on |this|, + in the standard order. + + The [=computed values=] in the {{[[declarations]]}} of this object + must remain up-to-date, + changing as style resolution changes the properties on |this| + and how they're computed. + + Note: In practice, since the values are "hidden" behind a `.get()` method call, + UAs can delay computing anything until a given property is actually requested. + + 2. Return |this|'s {{Element/[[computedStyleMapCache]]}} internal slot. +
+ +Note: like {{Window/getComputedStyle()|Window.getComputedStyle()}}, +this method can expose information from stylesheets with the [=CSSStyleSheet/origin-clean flag=] unset. -When constructed, the property model for computed StylePropertyMap -objects is initialized to contain an entry for every valid CSS property supported by the User Agent. +Note: The {{StylePropertyMapReadOnly}} returned by this method represents the *actual* [=computed values=], +not the [=resolved value=] concept used by {{Window/getComputedStyle()|Window.getComputedStyle()}}. +It can thus return different values than {{Window/getComputedStyle()|Window.getComputedStyle()}} +for some properties (such as 'width'). -Note: The StylePropertyMap returned by getComputedStyleMap represents computed style, - not resolved style. In this regard it provides different values than those - in objects returned by getComputedStyle. +Note: Per [WG resolution](https://github.com/w3c/css-houdini-drafts/issues/350#issuecomment-294690156), +pseudo-element styles are intended to be obtainable +by adding this method to the new {{PseudoElement}} interface +(rather than using a `pseudoElt` argument like {{Window/getComputedStyle()|Window.getComputedStyle()}} does). -Specified {{StylePropertyMap}} objects {#specified-stylepropertymap-objects} +Declared & Inline {{StylePropertyMap}} objects {#declared-stylepropertymap-objects} ----------------------------------------------------------------------------
 partial interface CSSStyleRule {
-  [SameObject] readonly attribute StylePropertyMap styleMap;
+    [SameObject] readonly attribute StylePropertyMap styleMap;
 };
-
-Specified StylePropertyMap objects represent style property-value pairs embedded -in a style rule, and are accessed via the styleMap -attribute of {{CSSStyleRule}} objects. - -When constructed, the property model for specified StylePropertyMap -objects is initialized to contain -an entry for each property that is paired with at least one valid value inside the -{{CSSStyleRule}} that the object represents. The value for a given property is -the last valid value provided by the {{CSSStyleRule}} object. - - -Inline {{StylePropertyMap}} objects {#inline-stylepropertymap-objects} ----------------------------------------------------------------------- - -
-partial interface Element {
-  [SameObject] readonly attribute StylePropertyMap styleMap;
+partial interface mixin ElementCSSInlineStyle {
+    [SameObject] readonly attribute StylePropertyMap attributeStyleMap;
 };
 
-Inline StylePropertyMap objects represent inline style declarations attached -directly to {{Element}}s. They are accessed via the styleMap -attribute of {{Element}} objects. +Declared StylePropertyMap objects +represent style property-value pairs embedded in a style rule or inline style, +and are accessed via the styleMap attribute of {{CSSStyleRule}} objects, +or the attributeStyleMap attribute +of objects implementing the {{ElementCSSInlineStyle}} interface mixin +(such as {{HTMLElement}}s). + +When constructed, the {{[[declarations]]}} internal slot for [=declared StylePropertyMap=] objects +is initialized to contain an entry +for each property with a valid value inside the {{CSSStyleRule}} +or inline style +that the object represents, +in the same order as the {{CSSStyleRule}} or inline style. -When constructed, the property model for inline StylePropertyMap objects -is initialized to contain an -entry for each property that is paired with at least one valid value in the string -representing the style attribute for the Element that the object is associated with. -The value for a given property is the last valid value provided in the string. {{CSSStyleValue}} subclasses {#stylevalue-subclasses} ================================================== -{{CSSKeywordValue}} objects {#keywordvalue-objects} ------------------------------------------------- + + +{{CSSUnparsedValue}} objects {#unparsedvalue-objects} +---------------------------------------------- -
-[Constructor(DOMString)]
-interface CSSKeywordValue : CSSStyleValue {
-  attribute DOMString keywordValue;
+
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSUnparsedValue : CSSStyleValue {
+    constructor(sequence<CSSUnparsedSegment> members);
+    iterable<CSSUnparsedSegment>;
+    readonly attribute unsigned long length;
+    getter CSSUnparsedSegment (unsigned long index);
+    setter CSSUnparsedSegment (unsigned long index, CSSUnparsedSegment val);
 };
-</pre>
-
-{{CSSKeywordValue}} objects represent {{CSSStyleValue}}s that are keywords. The constructor for {{CSSKeywordValue}}
-should check that they contain valid CSS, but they do not check whether the contained string is a valid
-keyword for a particular property. Property setters are required to enforce keyword validity.
 
-{{CSSNumberValue}} objects {#numbervalue-objects}
-----------------------------------------------
+typedef (USVString or CSSVariableReferenceValue) CSSUnparsedSegment;
 
-<pre class='idl'>
-[Constructor(double), Constructor(DOMString cssText)]
-interface CSSNumberValue : CSSStyleValue {
-  attribute double value;
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSVariableReferenceValue {
+    constructor(USVString variable, optional CSSUnparsedValue? fallback = null);
+    attribute USVString variable;
+    readonly attribute CSSUnparsedValue? fallback;
 };
-</pre>
+
+
+{{CSSUnparsedValue}} objects represent property values that reference custom properties.
+They are comprised of a list of string fragments and variable references.
+
+They have a \[[tokens]] internal slot,
+which is a [=list=] of {{USVString}}s and {{CSSVariableReferenceValue}} objects.
+This list is the object's [=values to iterate over=].
+
+The length attribute returns the [=list/size=]
+of the {{CSSUnparsedValue/[[tokens]]}} internal slot.
+
+
+ The [=supported property indices|supported property indexes=] + of a {{CSSUnparsedValue}} |this| + are the integers greater than or equal to 0, + and less than the [=list/size=] of |this|’s {{CSSUnparsedValue/[[tokens]]}} internal slot. + + To [=determine the value of an indexed property=] + of a {{CSSUnparsedValue}} |this| + and an index |n|, + let |tokens| be |this|’s {{CSSUnparsedValue/[[tokens]]}} internal slot, + and return |tokens|[|n|]. + + To [=set the value of an existing indexed property=] + of a {{CSSUnparsedValue}} |this|, + an index |n|, + and a value |new value|, + let |tokens| be |this|’s {{CSSUnparsedValue/[[tokens]]}} internal slot, + and set |tokens|[|n|] to |new value|. + + To [=set the value of a new indexed property=] + of a {{CSSUnparsedValue}} |this|, + an index |n|, + and a value |new value|, + let |tokens| be |this|’s {{CSSUnparsedValue/[[tokens]]}} internal slot. + If |n| is not equal to the [=list/size=] of |tokens|, + [=throw=] a {{RangeError}}. + Otherwise, + [=list/append=] |new value| to |tokens|. +
-{{CSSNumberValue}} objects represent values for simple number-valued properties like 'z-index' or 'opacity'. +
+ The getter for the variable attribute + of a {{CSSVariableReferenceValue}} |this| + must return its {{CSSVariableReferenceValue/variable}} internal slot. -{{CSSNumberValue}} objects are not range-restricted. Any valid number can be represented by a {{CSSNumberValue}}, -and that value will not be clamped, rounded, or rejected when set on a specified StylePropertyMap or -inline StylePropertyMap. Instead, clamping and/or rounding will occur during computation of style. + The {{CSSVariableReferenceValue/variable}} attribute + of a {{CSSVariableReferenceValue}} |this| must, on setting a + variable |variable|, perform the following steps: -
-The following code is valid + 1. If |variable| is not a [=custom property name string=], + [=throw=] a {{TypeError}}. -
-  myElement.styleMap.set("opacity", new CSSNumberValue(3));
-  myElement.styleMap.set("z-index", new CSSNumberValue(15.4));
+    2. Otherwise, set |this|’s {{CSSVariableReferenceValue/variable}} internal slot
+        to |variable|.
+
- console.log(myElement.styleMap.get("opacity").value); // 3 - console.log(myElement.styleMap.get("z-index").value); // 15.4 +
+ The CSSVariableReferenceValue(|variable|, |fallback|) constructor must, + when called, + perform the following steps: - var computedStyle = getComputedStyleMap(myElement); - var opacity = computedStyle.get("opacity"); - var zIndex = computedStyle.get("z-index"); -
+ 1. If |variable| is not a [=custom property name string=], + [=throw=] a {{TypeError}}. -After execution, the value of opacity is 1 ('opacity' is range-restricted), -and the value of zIndex is 15 ('z-index' is rounded to an integer value). + 2. Return a new {{CSSVariableReferenceValue}} + with its {{CSSVariableReferenceValue/variable}} internal slot + set to |variable| + and its {{CSSVariableReferenceValue/fallback}} internal slot + set to |fallback|. -Issue(140): where does a description of parsing values go? For example, where do we indicate that - calc(4 + 8) will create a {{CSSNumberValue}} with a value of 12? -{{CSSLengthValue}} objects {#lengthvalue-objects} ----------------------------------------------- + + +{{CSSKeywordValue}} objects {#keywordvalue-objects} +--------------------------------------------------- + +{{CSSKeywordValue}} objects represent CSS keywords and other [=identifiers=].
-enum LengthType {
-  "px", "percent",
-  "em", "ex", "ch", "rem",
-  "vw", "vh", "vmin", "vmax",
-  "cm", "mm", "q", "in", "pc", "pt"
+[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)]
+interface CSSKeywordValue : CSSStyleValue {
+    constructor(USVString value);
+    attribute USVString value;
 };
+
-dictionary CSSCalcDictionary { - double px; - double percent; - double em; - double ex; - double ch; - double rem; - double vw; - double vh; - double vmin; - double vmax; - double cm; - double mm; - double q; - double in; - double pc; - double pt; -}; +
+ The CSSKeywordValue(|value|) constructor must, + when called, + perform the following steps: -interface CSSLengthValue : CSSStyleValue { - CSSLengthValue add(CSSLengthValue value); - CSSLengthValue subtract(CSSLengthValue value); - CSSLengthValue multiply(double value); - CSSLengthValue divide(double value); - static CSSLengthValue from(DOMString cssText); - static CSSLengthValue from(double value, LengthType type); - static CSSLengthValue from(CSSCalcDictionary dictionary); -}; + 1. If |value| is an empty string, [=throw=] a {{TypeError}}. + 2. Otherwise, return a new {{CSSKeywordValue}} + with its {{CSSKeywordValue/value}} internal slot + set to |value|. +
-[Constructor(DOMString cssText), - Constructor(CSSLengthValue), - Constructor(CSSCalcDictionary) -] -interface CSSCalcLength : CSSLengthValue { - attribute double? px; - attribute double? percent; - attribute double? em; - attribute double? ex; - attribute double? ch; - attribute double? rem; - attribute double? vw; - attribute double? vh; - attribute double? vmin; - attribute double? vmax; - attribute double? cm; - attribute double? mm; - attribute double? q; - attribute double? in; - attribute double? pc; - attribute double? pt; -}; +Any place that accepts a {{CSSKeywordValue}} +also accepts a raw {{USVString}}, +by using the following typedef and algorithm: -// lengths that are *just* keywords don't become CSSSimpleLengths or CSSCalcLengths. -// Instead they are represented as CSSKeywordValue objects. -[Constructor(DOMString cssText), - Constructor(CSSLengthValue), - Constructor(double value, LengthType type)] -interface CSSSimpleLength : CSSLengthValue { - attribute double value; - readonly attribute LengthType type; -}; +
+typedef (DOMString or CSSKeywordValue) CSSKeywordish;
 
-{{CSSLengthValue}} objects represent lengths: +
+ To rectify a keywordish value |val|, + perform the following steps: -* {{CSSSimpleLength}} objects represent lengths that contain a single unit type (for example "42px"). -* {{CSSCalcLength}} objects represent lengths that contain multiple units (for example - "calc(56em + 10%)"). + 1. If |val| is a {{CSSKeywordValue}}, + return |val|. -{{CSSLengthValue}} objects are not range-restricted. Any valid combination of primitive lengths can be represented by a {{CSSLengthValue}}, -that value will be accepted unaltered when set on a specified StylePropertyMap or -inline StylePropertyMap. Instead, clamping and/or rounding will occur during computation of style. + 2. If |val| is a {{DOMString}}, + return a new {{CSSKeywordValue}} + with its {{CSSKeywordValue/value}} internal slot + set to |val|. +
-Note that lengths which incorporate variable references will instead be represented as -{{CSSTokenStreamValue}} objects, and keywords as {{CSSKeywordValue}} objects. +
+ The value attribute of a {{CSSKeywordValue}} |this| must, + on setting a value |value|, + perform the following steps: -The following methods are defined for {{CSSLengthValue}} objects: + 1. If |value| is an empty string, [=throw=] a {{TypeError}}. -: add(CSSLengthValue value) -:: Adds the provided value to the length represented by the object, - and returns the result as a new {{CSSLengthValue}}. This will construct - a {{CSSSimpleLength}} or {{CSSCalcLength}} depending on whether the result can - be expressed in terms of a single unit. + 2. Otherwise, set |this|’s {{CSSKeywordValue/value}} internal slot, + to |value|. +
-: subtract(CSSLengthValue value) -:: Subtracts the provided value from the length represented by the object, - and returns the result as a new {{CSSLengthValue}}. This will construct - a {{CSSSimpleLength}} or {{CSSCalcLength}} depending on whether the result can - be expressed in terms of a single unit. -: multiply(double value) -:: Multiplies the length represented by the object by the provided value, - and returns the result as a new {{CSSLengthValue}}. This will construct - a {{CSSSimpleLength}} if the object is a {{CSSSimpleLength}}, or a {{CSSCalcLength}} - if the object is a {{CSSCalcLength}}. + -: divide(double value) -:: Divides the length represented by the object by the provided value, - and returns the result as a new {{CSSLengthValue}}. This will construct - a {{CSSSimpleLength}} if the object is a {{CSSSimpleLength}}, or a {{CSSCalcLength}} - if the object is a {{CSSCalcLength}}. The function will throw a RangeError when the value given is 0. -: from(DOMString cssText) -:: Parses the provided cssText as a length value. Will return a - {{CSSSimpleLength}} when possible, or a {{CSSCalcLength}} otherwise. The function will throw a {{SyntaxError}}, when the DOMString it is passed doesn't represent a valid length. +Numeric Values: {#numeric-objects} +---------------------------------- -: from(double value, LengthType type) -:: Constructs a {{CSSSimpleLength}} with the given value and unit - type. +{{CSSNumericValue}} objects represent CSS values that are numeric in nature +(<>s, <>s, <>s). +There are two interfaces that inherit from {{CSSNumericValue}}: -: from(CSSCalcDictionary dictionary) -:: Constructs a {{CSSCalcLength}} with units and values as defined by the provided - dictionary. +* {{CSSUnitValue}} objects represent values that contain a single unit type + (for example "42px"). +* {{CSSMathValue}} objects represent math expressions, + which can contain more than one value/unit + (for example "calc(56em + 10%)"). -{{CSSAngleValue}} objects {#anglevalue-objects} ----------------------------------------------- +{{CSSNumericValue}} objects are not range-restricted. +Any valid numeric value can be represented by a {{CSSNumericValue}}, +and that value will not be clamped, rounded, or rejected +when set on a [=declared StylePropertyMap=]. +Instead, clamping and/or rounding will occur during computation of style. -
-enum CSSAngleUnit {
-  "deg", "rad", "grad", "turn"
-};
+
+ The following code is valid -[Constructor(double value, CSSAngleUnit unit)] -interface CSSAngleValue : CSSStyleValue { - readonly attribute double degrees; - readonly attribute double radians; - readonly attribute double gradians; - readonly attribute double turns; -}; -
+
+        myElement.attributeStyleMap.set("opacity", CSS.number(3));
+        myElement.attributeStyleMap.set("z-index", CSS.number(15.4));
 
-{{CSSAngleValue}} objects represent CSS angles. Once constructed, a
-CSSAngleValue provides attributes that reflect the size of the angle in each of the
-CSS angle units represented by the {{CSSAngleUnit}} enum.
+        console.log(myElement.attributeStyleMap.get("opacity").value); // 3
+        console.log(myElement.attributeStyleMap.get("z-index").value); // 15.4
 
+        var computedStyle = myElement.computedStyleMap();
+        var opacity = computedStyle.get("opacity");
+        var zIndex = computedStyle.get("z-index");
+    
-{{CSSTransformValue}} objects {#transformvalue-objects} ------------------------------------------------------------ + After execution, the value of `opacity` is `1` ('opacity' is range-restricted), + and the value of `zIndex` is `15` ('z-index' is rounded to an integer value). + -
-[Constructor(),
- Constructor(sequence<CSSTransformComponent> transforms)]
-interface CSSTransformValue : CSSStyleValue {
-  iterable<CSSTransformComponent>;
-  readonly attribute boolean is2D;
-  readonly attribute DOMMatrixReadOnly matrix;
-};
+Note: "Numeric values" which incorporate variable references
+will instead be represented as {{CSSUnparsedValue}} objects,
+and keywords as {{CSSKeywordValue}} objects.
 
-interface CSSTransformComponent {
-  readonly attribute DOMString cssText;
-  readonly attribute boolean is2D;
-  readonly attribute DOMMatrixReadOnly matrix;
-};
+Any place that accepts a {{CSSNumericValue}}
+also accepts a raw {{double}},
+by using the following typedef and algorithm:
 
-[Constructor(CSSLengthValue x, CSSLengthValue y),
- Constructor(CSSLengthValue x, CSSLengthValue y, CSSLengthValue z)]
-interface CSSTranslation : CSSTransformComponent {
-  readonly attribute CSSLengthValue x;
-  readonly attribute CSSLengthValue y;
-  readonly attribute CSSLengthValue z;
-};
+
+typedef (double or CSSNumericValue) CSSNumberish;
+
-[Constructor(double degrees), - Constructor(CSSAngleValue angle), - Constructor(double x, double y, double z, double degrees), - Constructor(double x, double y, double z, CSSAngleValue angle)] -interface CSSRotation : CSSTransformComponent { - readonly attribute double x; - readonly attribute double y; - readonly attribute double z; - readonly attribute double angle; -}; +
+ To rectify a numberish value |num|, + perform the following steps: -[Constructor(double x, double y), - Constructor(double x, double y, double z)] -interface CSSScale : CSSTransformComponent { - readonly attribute double x; - readonly attribute double y; - readonly attribute double z; -}; + 1. If |num| is a {{CSSNumericValue}}, + return |num|. -[Constructor(double ax, double ay)] -interface CSSSkew : CSSTransformComponent { - readonly attribute double ax; - readonly attribute double ay; -}; + 2. If |num| is a {{double}}, + return a new {{CSSUnitValue}} + with its {{CSSUnitValue/value}} internal slot set to |num| + and its {{CSSUnitValue/unit}} internal slot set to "number". +
-[Constructor(CSSLengthValue length)] -interface CSSPerspective : CSSTransformComponent { - readonly attribute CSSLengthValue length; -}; -[Constructor(DOMMatrixReadOnly matrix)] -interface CSSMatrix : CSSTransformComponent { -}; -
+### Common Numeric Operations, and the {{CSSNumericValue}} Superclass ### {#numeric-value} + +All numeric CSS values +(<>s, <>s, and <>s) +are represented by subclasses of the {{CSSNumericValue}} interface. + + + enum CSSNumericBaseType { + "length", + "angle", + "time", + "frequency", + "resolution", + "flex", + "percent", + }; + + dictionary CSSNumericType { + long length; + long angle; + long time; + long frequency; + long resolution; + long flex; + long percent; + CSSNumericBaseType percentHint; + }; + + [Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] + interface CSSNumericValue : CSSStyleValue { + CSSNumericValue add(CSSNumberish... values); + CSSNumericValue sub(CSSNumberish... values); + CSSNumericValue mul(CSSNumberish... values); + CSSNumericValue div(CSSNumberish... values); + CSSNumericValue min(CSSNumberish... values); + CSSNumericValue max(CSSNumberish... values); + + boolean equals(CSSNumberish... value); + + CSSUnitValue to(USVString unit); + CSSMathSum toSum(USVString... units); + CSSNumericType type(); + + [Exposed=Window] static CSSNumericValue parse(USVString cssText); + }; + + +The methods on the {{CSSNumericValue}} superclass +represent operations that all numeric values can perform. + +The following are the arithmetic operations you can perform on dimensions: + +
+ The add(...|values|) method, + when called on a {{CSSNumericValue}} |this|, + must perform the following steps: + + 1. Replace each [=list/item=] of |values| + with the result of [=rectifying a numberish value=] for the [=list/item=]. + + 2. If |this| is a {{CSSMathSum}} object, + [=list/prepend=] the [=list/items=] in |this|’s {{CSSMathSum/values}} internal slot + to |values|. + Otherwise, + prepend |this| to |values|. + + 3. If all of the [=list/items=] in |values| are {{CSSUnitValue}}s + and have the same {{CSSUnitValue/unit}}, + return a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot is set to |this|’s {{CSSUnitValue/unit}} internal slot, + and {{CSSUnitValue/value}} internal slot is set to the sum of + the {{CSSUnitValue/value}} internal slots + of the [=list/items=] in |values|. + This addition must be done "left to right" - + if |values| is « 1, 2, 3, 4 », + the result must be (((1 + 2) + 3) + 4). + (This detail is necessary to ensure interoperability + in the presence of floating-point arithmetic.) + + 4. Let |type| be the result of [=adding=] the [=types=] of every [=list/item=] in |values|. + If |type| is failure, + [=throw=] a {{TypeError}}. + + 5. Return a new {{CSSMathSum}} object + whose {{CSSMathSum/values}} internal slot + is set to |values|. +
+ +
+ The sub(...|values|) method, + when called on a {{CSSNumericValue}} |this|, + must perform the following steps: + 1. Replace each [=list/item=] of |values| + with the result of [=rectifying a numberish value=] for the [=list/item=], + then [=CSSMath/negating=] the value. -{{CSSTransformValue}} objects represent values for the 'transform' property. A -{{CSSTransformValue}} represents a list of {{CSSTransformComponent}}s. + 2. Return the result of calling the {{CSSNumericValue/add()}} internal algorithm + with |this| and |values|. +
-{{CSSTransformComponent}} objects have the following properties: +
+ To negate a {{CSSNumericValue}} |this|: -The is2D attribute is -true if the component represents a 2D transform function, -and false otherwise. The transform function which the component represents is -stored in string form in the cssText -attribute. + 1. If |this| is a {{CSSMathNegate}} object, + return |this|’s {{CSSMathNegate/value}} internal slot. -
-Each {{CSSTransformComponent}} can correspond to one of a number of underlying -transform functions. For example, a {{CSSTranslation}} with an x value of "10px" -and y & z values of 0 could be: -* translate(10px) -* translate(10px, 0) -* translateX(10px) -* translate3d(10px, 0, 0) + 2. If |this| is a {{CSSUnitValue}} object, + return a new {{CSSUnitValue}} + with the same {{CSSUnitValue/unit}} internal slot as |this|, + and a {{CSSUnitValue/value}} internal slot set to the negation of |this|’s. + 3. Otherwise, + return a new {{CSSMathNegate}} object + whose {{CSSMathNegate/value}} internal slot + is set to |this|.
-When a {{CSSRotation}} is constructed with a double (as opposed to a -{{CSSAngleValue}}), the angle is taken to be in degrees. +
+ The mul(...|values|) method, + when called on a {{CSSNumericValue}} |this|, + must perform the following steps: + + 1. Replace each [=list/item=] of |values| + with the result of [=rectifying a numberish value=] for the [=list/item=]. + + 2. If |this| is a {{CSSMathProduct}} object, + [=list/prepend=] the [=list/items=] in |this|’s {{CSSMathProduct/values}} internal slot + to |values|. + Otherwise, + prepend |this| to |values|. + + 3. If all of the [=list/items=] in |values| are {{CSSUnitValue}}s + with {{CSSUnitValue/unit}} internal slot set to "number", + return a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot is set to "number", + and {{CSSUnitValue/value}} internal slot is set to the product of + the {{CSSUnitValue/value}} internal slots + of the [=list/items=] in |values|. + + This multiplication must be done "left to right" - + if |values| is « 1, 2, 3, 4 », + the result must be (((1 × 2) × 3) × 4). + (This detail is necessary to ensure interoperability + in the presence of floating-point arithmetic.) + + 4. If all of the [=list/items=] in |values| are {{CSSUnitValue}}s + with {{CSSUnitValue/unit}} internal slot set to "number" + except one which is set to |unit|, + return a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot is set to |unit|, + and {{CSSUnitValue/value}} internal slot is set to the product of + the {{CSSUnitValue/value}} internal slots + of the [=list/items=] in |values|. + + This multiplication must be done "left to right" - + if |values| is « 1, 2, 3, 4 », + the result must be (((1 × 2) × 3) × 4). + + 5. Let |type| be the result of [=CSSNumericValue/multiplying=] the [=types=] of every [=list/item=] in |values|. + If |type| is failure, + [=throw=] a {{TypeError}}. + + 6. Return a new {{CSSMathProduct}} object + whose {{CSSMathProduct/values}} internal slot + is set to |values|. +
-
-The following two CSSRotations are equivalent: -
-    CSSRotation(angle);
-    CSSRotation(CSSAngleValue(angle, "deg"));
-
+
+ The div(...|values|) method, + when called on a {{CSSNumericValue}} |this|, + must perform the following steps: + + 1. Replace each [=list/item=] of |values| + with the result of [=rectifying a numberish value=] for the [=list/item=], + then [=CSSMath/inverting=] the value. + + 2. Return the result of calling the {{CSSNumericValue/mul()}} internal algorithm + with |this| and |values|.
-When a {{CSSTransformValue}} is read from a {{StylePropertyMap}}, each -{{CSSTransformComponent}} will maintain the relevant transform function in -its cssText attribute. However, newly constructed {{CSSTransformValue}}s -will always generate cssText according to the following rules: -* {{CSSSkew}} will always serialize to skew(ax, ay) -* {{CSSPerspective}} will always serialize to perspective(length) -* {{CSSTranslation}}, {{CSSRotation}}, and {{CSSScale}} each have two - constructors. In each case, the constructor with fewer arguments constructs - a {{CSSTransformComponent}} for which {{CSSTransformComponent/is2D}} will be true, - with cssText employing the 2D version of the relevant transform - function (translate, rotate, scale). The constructor with more - arguments constructs a {{CSSTransformComponent}} for which - {{CSSTransformComponent/is2D}} will be false, with cssText - employing the 3D version of the relevant transform function (translate3d, - rotate3d, scale3d). -* {{CSSMatrix}} objects are constructed around a {{DOMMatrixReadOnly}}, and - well serialize to a matrix function if the contained {{DOMMatrixReadOnly}}'s - {{DOMMatrixReadOnly/is2D}} attribute is false, and a matrix3d function - otherwise. - -is2D is true if the {{CSSTransformComponent/is2D}} -attribute of every {{CSSTransformComponent}} referenced by the {{CSSTransformValue}} -returns true, and false otherwise. - -{{CSSPositionValue}} objects {#positionvalue-objects} --------------------------------------------------- +
+ To invert a {{CSSNumericValue}} |this|: -
+    1. If |this| is a {{CSSMathInvert}} object,
+        return |this|’s {{CSSMathInvert/value}} internal slot.
 
-[Constructor(CSSLengthValue x, CSSLengthValue y)]
-interface CSSPositionValue : CSSStyleValue {
-  readonly attribute CSSLengthValue x;
-  readonly attribute CSSLengthValue y;
-};
+    2. If |this| is a {{CSSUnitValue}} object with {{CSSUnitValue/unit}} internal slot set to "number":
+        1. If |this|’s {{CSSUnitValue/value}} internal slot is set to 0 or -0,
+            [=throw=] a {{RangeError}}.
+        2. Else return a new {{CSSUnitValue}}
+            with the {{CSSUnitValue/unit}} internal slot set to "number",
+            and a {{CSSUnitValue/value}} internal slot set to 1 divided by |this|’s {CSSUnitValue/value}} internal slot.
 
-
+ 3. Otherwise, + return a new {{CSSMathInvert}} object + whose {{CSSMathInvert/value}} internal slot + is set to |this|. +
-{{CSSPositionValue}} objects represent values for properties that take <> -productions, for example 'background-position'. +
+ The min(...|values|) method, + when called on a {{CSSNumericValue}} |this|, + must perform the following steps: + + 1. Replace each [=list/item=] of |values| + with the result of [=rectifying a numberish value=] for the [=list/item=]. + + 2. If |this| is a {{CSSMathMin}} object, + [=list/prepend=] the [=list/items=] in |this|’s {{CSSMathMin/values}} internal slot + to |values|. + Otherwise, + prepend |this| to |values|. + + 3. If all of the [=list/items=] in |values| are {{CSSUnitValue}}s + and have the same {{CSSUnitValue/unit}}, + return a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot is set to |this|’s {{CSSUnitValue/unit}} internal slot, + and {{CSSUnitValue/value}} internal slot is set to the minimum of + the {{CSSUnitValue/value}} internal slots + of the [=list/items=] in |values|. + + 4. Let |type| be the result of [=adding=] the [=types=] of every [=list/item=] in |values|. + If |type| is failure, + [=throw=] a {{TypeError}}. + + 5. Return a new {{CSSMathMin}} object + whose {{CSSMathMin/values}} internal slot + is set to |values|. +
-The x attribute contains the position offset -from the left edge of the container, expressed as a length. +
+ The max(...|values|) method, + when called on a {{CSSNumericValue}} |this|, + must perform the following steps: + + 1. Replace each [=list/item=] of |values| + with the result of [=rectifying a numberish value=] for the [=list/item=]. + + 2. If |this| is a {{CSSMathMax}} object, + [=list/prepend=] the [=list/items=] in |this|’s {{CSSMathMax/values}} internal slot + to |values|. + Otherwise, + prepend |this| to |values|. + + 3. If all of the [=list/items=] in |values| are {{CSSUnitValue}}s + and have the same {{CSSUnitValue/unit}}, + return a new {{CSSUnitValue}} + whose {{CSSUnitValue/unit}} internal slot is set to |this|’s {{CSSUnitValue/unit}} internal slot, + and {{CSSUnitValue/value}} internal slot is set to the maximum of + the {{CSSUnitValue/value}} internal slots + of the [=list/items=] in |values|. + + 4. Let |type| be the result of [=adding=] the [=types=] of every [=list/item=] in |values|. + If |type| is failure, + [=throw=] a {{TypeError}}. + + 5. Return a new {{CSSMathMax}} object + whose {{CSSMathMax/values}} internal slot + is set to |values|. +
-The y attribute contains the position offset -from the top edge of the container, expressed as a length. +
+ The equals(...|values|) method, + when called on a {{CSSNumericValue}} |this|, + must perform the following steps: -Note that <> productions accept a complicated combination of keywords -and values. When specified as such in a stylesheet or via the untyped CSSOM, -the cssText attribute will contain the specified -string. However, this string is normalized as two Lengths into the x and y values of the -{{CSSStyleValue}} object. + 1. Replace each [=list/item=] of |values| + with the result of [=rectifying a numberish value=] for the [=list/item=]. -New {{CSSPositionValue}} objects can only be constructed via pairs of lengths, and -will only return the direct serialization of these lengths in the -cssText attribute. + 2. For each [=list/item=] in |values|, + if the [=list/item=] is not an [=equal numeric value=] to |this|, + return `false`. -
+ 3. Return `true`. +
-For example, the following style sheet: +
+ This notion of equality is purposely fairly exacting; + all the values must be the exact same type and value, in the same order. + For example, `CSSMathSum(CSS.px(1), CSS.px(2))` + is not equal to `CSSMathSum(CSS.px(2), CSS.px(1))`. + + This precise notion is used because it allows structural equality to be tested for very quickly; + if we were to use a slower and more forgiving notion of equality, + such as allowing the arguments to match in any order, + we'd probably want to go all the way + and perform other simplifications, + like considering ''96px'' to be equal to ''1in''; + this looser notion of equality might be added in the future. +
-
-.example {
-  background-position: center bottom 10px;
-}
-
+
+ To determine whether two {{CSSNumericValue}}s |value1| and |value2| + are equal numeric values, + perform the following steps: -Will produce the following behavior: + 1. If |value1| and |value2| are not members of the same interface, + return `false`. -
-// "center bottom 10px"
-document.querySelector('.example').styleMap.get('background-position').cssText;
+    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.
 
-// 50% - as a CSSSimpleLength
-document.querySelector('.example').styleMap.get('background-position').x;
+    3. If |value1| and |value2| are both
+        {{CSSMathSum}}s, {{CSSMathProduct}}s, {{CSSMathMin}}s, or {{CSSMathMax}}s:
 
-// calc(100% - 10px) - as a CSSCalcLength
-document.querySelector('.example').styleMap.get('background-position').y;
-
+ 1. If |value1|’s {{CSSMathSum/values}} and |value2|s {{CSSMathSum/values}} internal slots + have different [=list/sizes=], + return `false`. + + 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`. + 3. Return `true`. + + 4. Assert: |value1| and |value2| are both {{CSSMathNegate}}s or {{CSSMathInvert}}s. + + 5. Return whether |value1|’s {{CSSMathNegate/value}} + and |value2|’s {{CSSMathNegate/value}} + are [=equal numeric values=].
-{{CSSResourceValue}} objects {#resourcevalue-objects} ------------------------------------------------------ +
+ 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: + + 1. Let |type| be the result of [=creating a type=] from |unit|. + If |type| 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. 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}}. + + 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=]. +
-enum CSSResourceState {"unloaded", "loading", "loaded", "error"}; +
+ 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. -interface CSSResourceValue { - readonly attribute CSSResourceState state; -}; + 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}}. -{{CSSResourceValue}} objects represent CSS values that may require an asynchronous network fetch -before being usable. + 2. Let |sum| be the result of [=creating a sum value=] from |this|. + If |sum| is failure, + [=throw=] a {{TypeError}}. -A {{CSSResourceValue}} is in one of the following states, as reflected in the value of the -state attribute: + 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}}. -: ''unloaded'' -:: The resource is not ready and is not actively being fetched -: ''loading'' -:: The resource is not ready, but is in the process of being fetched -: ''loaded'' -:: The resource is ready for rendering -: ''error'' -:: The resource can't be fetched, or the fetched resource is invalid + 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|. -
-For example, images that match the <> production can be used immediately, but will not result -in a visual change until the image data is fetched. {{CSSResourceValue}} objects represent this by -providing values that track loaded state via the {{CSSResourceState}} enum. -
+ 5. Otherwise, + let |result| initially be an empty [=list=]. + [=list/For each=] |unit| in |units|: -{{CSSImageValue}} objects {#imagevalue-objects} ------------------------------------------------ + 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|:
 
-interface CSSImageValue : CSSResourceValue {
-  readonly attribute double intrinsicWidth;
-  readonly attribute double intrinsicHeight;
-};
+            1. Let |value unit| be |value|’s {{CSSUnitValue/unit}} internal slot.
 
-[Constructor(DOMString url)]
-interface CSSURLImageValue : CSSImageValue {
-  readonly attribute DOMString url;
-};
+            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|. -{{CSSImageValue}} objects represent values for properties that take <> productions, -for example 'background-image', 'list-style-image', and 'border-image-source'. + 3. [=list/Append=] |temp| to |result|. -{{CSSImageValue}} objects that do not require network data (for example linear and radial gradients) -are initialized with state ''loaded''. + 6. If |values| is not [=list/empty=], + [=throw=] a {{TypeError}}. + |this| had units that you didn't ask for. -If state is ''unloaded'', ''loading'', or ''error'', then -intrinsicWidth and intrinsicHeight -are 0. Otherwise, the attributes are set to the intrinsic width and height of the referenced image. + 7. Return a new {{CSSMathSum}} object + whose {{CSSMathSum/values}} internal slot + is set to |result|. +
-Issue: Does the loading lifecycle need to be described here? +
+ The type() method + returns a representation of the [=type=] of |this|. -{{CSSURLImageValue}} objects represent {{CSSImageValue}}s that match the <> production. For these -objects, the url attribute contains the URL that references the image. + When called, it must perform the following steps: -{{CSSFontFaceValue}} objects {#fontfacevalue-objects} ------------------------------------------------------ + 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|.
 
-[Constructor(DOMString fontFaceName)]
-interface CSSFontFaceValue : CSSResourceValue {
-  readonly attribute DOMString fontFaceName;
-};
+    3. If the [=percent hint=] of |this| is not null,
+        1. Set {{CSSNumericType/percentHint}} to the [=percent hint=] of |this|.
 
-
+ 4. Return |result|. +
-{{CSSFontFaceValue}} objects represent font faces that can be used to render text. As -font data may need to be fetched from a remote source, {{CSSFontFaceValue}} is a subclass -of {{CSSResourceValue}}. - -Mapping of properties to accepted types {#mapping-of-properties-to-accepted-types} -================================================================================== -This section provides a table of which types of {{CSSStyleValue}} a given property can accept. -Note that most, but not all properties take {{CSSKeywordValue}}. -Shorthand properties and values are not supported. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropertyAllowable {{CSSStyleValue}} types
'align-content'{{CSSKeywordValue}}
'align-items'{{CSSKeywordValue}}
'align-self'{{CSSKeywordValue}}
'alignment-baseline'{{CSSKeywordValue}}
'all'{{CSSKeywordValue}}
'animation-delay'TimeValue | {{CSSKeywordValue}}
'animation-direction'{{CSSKeywordValue}}
'animation-duration'TimeValue | {{CSSKeywordValue}}
'animation-fill-mode'{{CSSKeywordValue}}
'animation-iteration-count'{{CSSNumberValue}} | {{CSSKeywordValue}}
'animation-name'CustomIdentValue | {{CSSKeywordValue}}
'animation-play-state'{{CSSKeywordValue}}
'animation-timing-function'TransitionTimingFunctionValue | {{CSSKeywordValue}}
'appearance'{{CSSKeywordValue}}
'backface-visibility'{{CSSKeywordValue}}
'background-attachment'{{CSSKeywordValue}}
'background-blend-mode'{{CSSKeywordValue}}
'background-clip'{{CSSKeywordValue}}
'background-color'ColorValue | {{CSSKeywordValue}}
'background-image'{{CSSImageValue}}
'background-origin'{{CSSKeywordValue}}
'background-position'{{CSSPositionValue}} | {{CSSKeywordValue}}
'background-repeat'{{CSSKeywordValue}}
'background-size'PairValue<{{CSSLengthValue}}> (or SizeValue?)| {{CSSKeywordValue}}
'baseline-shift'{{CSSLengthValue}} | {{CSSKeywordValue}}
'border-boundary'{{CSSKeywordValue}}
'border-collapse'{{CSSKeywordValue}}
'border-color'ColorValue | {{CSSKeywordValue}}
'border-top-color' 'border-right-color' 'border-bottom-color' 'border-left-color'ColorValue | {{CSSKeywordValue}}
'border-image-outset'FourValues<{{CSSLengthValue}}|{{CSSNumberValue}}>
'border-image-repeat'PairValue<{{CSSKeywordValue}}>
'border-image-slice'BorderImageSliceValue
'border-image-source'{{CSSImageValue}}
'border-image-width'FourValues<{{CSSLengthValue}}|{{CSSNumberValue}}|{{CSSKeywordValue}}>
'border-top-style' 'border-right-style' 'border-bottom-style' 'border-left-style'{{CSSKeywordValue}}
'border-top-right-radius' 'border-bottom-right-radius' 'border-bottom-left-radius' 'border-top-left-radius'PairValue<{{CSSLengthValue}}> | {{CSSKeywordValue}}
'border-top-width' 'border-right-width' 'border-bottom-width' 'border-left-width'{{CSSLengthValue}} | {{CSSKeywordValue}}
'bottom'{{CSSLengthValue}} | {{CSSKeywordValue}}
'break-after'{{CSSKeywordValue}}
'break-before'{{CSSKeywordValue}}
'break-inside'{{CSSKeywordValue}}
'caption-side'{{CSSKeywordValue}}
'clear'{{CSSKeywordValue}}
'clip'ShapeValue | {{CSSKeywordValue}}
'color'ColorValue | {{CSSKeywordValue}}
'column-count'{{CSSNumberValue}} | {{CSSKeywordValue}}
'column-fill'{{CSSKeywordValue}}
'column-gap'{{CSSLengthValue}} | {{CSSKeywordValue}}
'column-rule-color'ColorValue | {{CSSKeywordValue}}
'column-rule-style'{{CSSKeywordValue}}
'column-rule-width'{{CSSLengthValue}} | {{CSSKeywordValue}}
'column-span'{{CSSKeywordValue}}
'column-width'{{CSSLengthValue}} | {{CSSKeywordValue}}
'content'StringValue | URIValue | CounterValue | AttrValue | {{CSSKeywordValue}}
'counter-increment' 'counter-reset'CounterValue | {{CSSKeywordValue}}
'cue-after' 'cue-before'URIValue | {{CSSKeywordValue}}
'cursor'URIValue | {{CSSKeywordValue}}
'direction'{{CSSKeywordValue}}
'display'{{CSSKeywordValue}}
'empty-cells'{{CSSKeywordValue}}
'float'{{CSSKeywordValue}}
'font-family'{{CSSKeywordValue}}
'font-size'{{CSSLengthValue}} | {{CSSKeywordValue}}
'font-style'{{CSSKeywordValue}}
'font-variant'{{CSSKeywordValue}}
'font-weight'FontWeightValue | {{CSSKeywordValue}}
'height'{{CSSLengthValue}} | {{CSSKeywordValue}}
'left'{{CSSLengthValue}} | {{CSSKeywordValue}}
'letter-spacing'{{CSSLengthValue}} | {{CSSKeywordValue}}
'line-height'{{CSSNumberValue}} | {{CSSLengthValue}} | {{CSSKeywordValue}}
'list-style-image'{{CSSImageValue}}
'list-style-position'{{CSSKeywordValue}}
'list-style-type'{{CSSKeywordValue}}
'margin-top' 'margin-right' 'margin-bottom' 'margin-left'{{CSSLengthValue}} | {{CSSKeywordValue}}
'max-height' 'max-width' 'min-height' 'min-width'{{CSSLengthValue}} | {{CSSKeywordValue}}
'orphans'{{CSSNumberValue}} | {{CSSKeywordValue}}
'outline-color'ColorValue | {{CSSKeywordValue}}
'outline-style'{{CSSKeywordValue}}
'outline-width'{{CSSLengthValue}} | {{CSSKeywordValue}}
'overflow'{{CSSKeywordValue}}
'padding-top' 'padding-right' 'padding-bottom' 'padding-left'{{CSSLengthValue}} | {{CSSKeywordValue}}
'page-break-after' 'page-break-before'{{CSSKeywordValue}}
'page-break-inside'{{CSSKeywordValue}}
'pause-after' 'pause-before'TimeValue | PercentageValue | {{CSSKeywordValue}}
'perspective'{{CSSLengthValue}} | {{CSSKeywordValue}}
'perspective-origin'{{CSSPositionValue}} | {{CSSKeywordValue}}
'pitch-range'{{CSSNumberValue}} | {{CSSKeywordValue}}
'pitch'FrequencyValue | {{CSSKeywordValue}}
'play-during'PlayDuringValue | {{CSSKeywordValue}}
'position'{{CSSKeywordValue}}
'quotes'PairValue<StringValue> | {{CSSKeywordValue}}
'richness'{{CSSNumberValue}} | {{CSSKeywordValue}}
'right'{{CSSLengthValue}} | {{CSSKeywordValue}}
'speak-header'{{CSSKeywordValue}}
'speak-numeral'{{CSSKeywordValue}}
'speak-punctuation'{{CSSKeywordValue}}
'speak'{{CSSKeywordValue}}
'speech-rate'{{CSSNumberValue}} | {{CSSKeywordValue}}
'stress'{{CSSNumberValue}} | {{CSSKeywordValue}}
'table-layout'{{CSSKeywordValue}}
'text-align'{{CSSKeywordValue}}
'text-decoration'{{CSSKeywordValue}}
'text-indent'{{CSSLengthValue}} | {{CSSKeywordValue}}
'text-transform'{{CSSKeywordValue}}
'top'{{CSSLengthValue}} | {{CSSKeywordValue}}
'transform'{{CSSTransformValue}} | {{CSSKeywordValue}}
'transform-box'{{CSSKeywordValue}}
'transform-origin'TransformOriginValue | {{CSSKeywordValue}}
'transform-style'{{CSSKeywordValue}}
'unicode-bidi'{{CSSKeywordValue}}
'vertical-align'{{CSSLengthValue}} | {{CSSKeywordValue}}
'visibility'{{CSSKeywordValue}}
'voice-family'VoiceValue | {{CSSKeywordValue}}
'volume'{{CSSNumberValue}} | PercentageValue | {{CSSKeywordValue}}
'white-space'{{CSSKeywordValue}}
'widows'{{CSSNumberValue}} | {{CSSKeywordValue}}
'width'{{CSSLengthValue}} | {{CSSKeywordValue}}
'word-spacing'{{CSSLengthValue}} | {{CSSKeywordValue}}
'z-index'{{CSSNumberValue}} | {{CSSKeywordValue}}
- -Issue(159): Spec up ColorValue - -{{CSSStyleValue}} normalization {#stylevalue-normalization} -=========================================================== +
+ 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|. +
-This section describes how normalized {{CSSStyleValue}} objects are -constructed from CSS DOMString values. + : {{CSSMathInvert}} + :: +
+ 1. Let |values| be the result of [=creating a sum value=] + from this’s {{CSSMathInvert/value}} internal slot. -Strings are normalized by resulting CSSStyleValue subclass. To -determine which subclass to use, the CSS property that the string -is a value for is used to look up the table in -. The normalization -procedure defined by each of the CSSStyleValue subclasses listed for that property in -turn, falling back to the next subclass in the list if the current one -fails to normalize. + 2. If |values| is failure, + return failure. -If all listed subclasses fail, then the value is normalized as a raw -{{CSSStyleValue}}, with the cssText attribute -set to the input value. + 3. If the length of [=values=] is more than one, + return failure. -{{CSSTokenStreamValue}} normalization {#tokenstreamvalue-normalization} ------------------------------------------------------------------------ + 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=]. -Values which contain custom property references are tokenized then split into runs of -tokens separated by custom property references. + 4. Return |values|. +
-The token runs are serialized, while each custom property reference -is represented by a {{CSSVariableReferenceValue}}. + : {{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. -The string "calc(42px + var(--foo, 15em) + var(--bar, var(--far) + 15px))" -is converted into a {{CSSTokenStreamValue}} that contains a sequence with: -* the string "calc(42px + " -* a {{CSSVariableReferenceValue}} with: - * variable "--foo" - * fallback a - {{CSSTokenStreamValue}} with a single-valued sequence containing " 15em" -* the string " + " -* a {{CSSVariableReferenceValue}} with: - * variable "--bar" - * fallback a {{CSSTokenStreamValue}} - with a sequence containing: - * the string " " - * a {{CSSVariableReferenceValue}} with - * variable "--far" - * fallback undefined - * the string " + 15px" -* the string ")" - -
- -{{CSSKeywordValue}} normalization {#keywordvalue-normalization} ---------------------------------------------------------------- + 3. If not all of the [=sum value/unit maps=] among the [=list/items=] of |args| are identical, + return failure. -If the provided value can tokenize as an <>, then -a {{CSSKeywordValue}} is constructed with the provided value -as the keywordValue attribute. + 4. Return the [=list/item=] of |args| whose sole [=list/item=] has the smallest [=sum value/value=]. +
-Otherwise, normalization fails. + : {{CSSMathMax}} + :: +
+ 1. Let |args| be the result of [=creating a sum value=] + [=list/for each=] [=list/item=] in this’s {{CSSMathMax/values}} internal slot. -{{CSSNumberValue}} normalization {#numbervalue-normalization} -------------------------------------------------------------- + 2. If any [=list/item=] of |args| is failure, + or has a length greater than one, + return failure. -If the provided value can tokenize as a <>, then -a {{CSSNumberValue}} is constructed with the value -attribute set to the number parsed from the <>. + 3. If not all of the [=sum value/unit maps=] among the [=list/items=] of |args| are identical, + return failure. -Otherwise, if the provided value can be parsed as a calc expression with a -resolved type of <>, then the expression is solved and -a {{CSSNumberValue}} is constructed with the value -attribute set to the result. + 4. Return the [=list/item=] of |args| whose sole [=list/item=] has the largest [=sum value/value=]. +
+
+
-Otherwise, normalization fails. +
+ To create a type from a unit map |unit map|: -{{CSSLengthValue}} normalization {#lengthvalue-normalization} -------------------------------------------------------------- + 1. Let |types| be an initially empty [=list=]. -If the provided value matches the <> production or the <> -production then a {{CSSSimpleLength}} is constructed with the value -attribute set to the number part of the length, and the type -set to the unit. + 2. [=map/For each=] |unit| → |power| in |unit map|: -Otherwise, if the provided value can be parsed as a calc expression with a -resolved type of <> or <>, then -a {{CSSCalcLength}} is constructed with each unit that is mentioned in the -calc expression reflected as the matching attribute set to the appropriate -value. + 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|. -
-A length of calc(42px + 15% - 42px) will normalize to a -{{CSSCalcLength}} with both the px and -the percent attributes set (to 0 and 15 -respectively). Even though the two pixel lengths cancel each other, the fact -that pixels are mentioned in the expression means that they're represented -in the normalized object. + 3. Return the result of [=CSSNumericValue/multiplying=] all the [=list/items=] of |types|.
-Otherwise, normalization fails. +
+ The product of two unit maps |units1| and |units2| + is the result given by the following steps: -{{CSSAngleValue}} normalization {#anglevalue-normalization} ------------------------------------------------------------ + 1. Let |result| be a copy of |units1|. -If the provided value matches the <> production then a {{CSSAngleValue}} -is constructed using the CSSAngleValue(double value, CSSAngleUnit unit) -constructor, with value set to the number part of the angle, -and unit set to the unit. + 2. [=map/For each=] |unit| → |power| in |units2|: -Otherwise, normalization fails. + 1. If |result|[|unit|] [=map/exists=], + increment |result|[|unit|] by |power|. + 3. Otherwise, set |result|[|unit|] to |power|. -{{CSSTransformValue}} normalization {#transformvalue-normalization} -------------------------------------------------------------------- + 3. Return |result|. +
-If the provided value matches the <> production then a {{CSSTransformComponent}} -is constructed for each matching <> in the production, and a {{CSSTransformValue}} -is constructed around the resulting sequence. +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. +
-Otherwise, if the provided value is "none", an empty string, or a string containing only whitespace, -then a {{CSSTransformValue}} is constructed around an empty sequence. + + +### 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. +
-Otherwise, normalization fails. +
+ 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. +
+
-{{CSSPositionValue}} normalization {#positionvalue-normalization} ------------------------------------------------------------------ +
+ To apply the percent hint |hint| to a |type|, + perform the following steps: -If the provided value matches the <> production, then a {{CSSPositionValue}} is constructed -with x and y components determined via the following process. If this process, or -any sub-process referenced by this process fails, then normalization as a whole fails. - -1. Initialize both x and y to a {{CSSSimpleLength}} value representing 50%. -1. If the provided value is a single keyword, length, percentage, or calc expression, then follow - the procedure outlined in with value given - by the provided value and a horizontal bias. -1. Otherwise, if the provided value consists of a combination of two keywords, then: - - 1. follow the procedure outlined in with value given by the - first keyword and an auto bias. - 1. if bias is horizontal, set it to vertical. Otherwise, set it to horizontal. - 1. follow the procedure again with value given by the second keyword, using the existing bias. - -1. Otherwise, if the provided value consists of a combination of two keywords, lengths, percentages, and - calc expressions, then follow the procedure outlined in with - value given by the first part of the provided value and a horizontal bias, then follow the - procedure again with value given by the second part of the provided value and a vertical bias. -1. Otherwise: - - 1. if the provided value starts with a keyword followed by a length, percentage, or calc expression, then follow - the procedure outlined in with keyword set to the - keyword, length set to the length, percentage, or calc expression, and auto bias. - 1. otherwise, follow the procedure outlined in with value - set to the first component of the provided value and an auto bias. - 1. if bias is horizontal, set it to vertical. Otherwise, set it to horizontal. - 1. if the remainder of the provided value is a single keyword, length, percentage or calc expression, follow - the procedure outlined in - with value set to the keyword and the existing - bias. - 1. otherwise, if the remainder of the provided value consists of a keyword followed by a length, percentage or - calc expression, follow the procedure outlined in with - keyword set to the keyword, length set to the length, percentage, or calc expression, and - the existing bias. - 1. Otherwise, the process fails. - -

Determining x or y from a single value

- -The following process sets a value for either x or y, depending on an input value -and bias. The process also updates bias based on the value. - -1. If value is the keyword "left" and bias is not vertical, then set x to a - {{CSSSimpleLength}} value representing 0% and bias to horizontal and exit this process. -1. If value is the keyword "right" and bias is not vertical, then set x to a - {{CSSSimpleLength}} value representing 100% and bias to horizontal and exit this process. -1. If value is the keyword "top" and bias is not horizontal, then set y to a - {{CSSSimpleLength}} value representing 0% and bias to vertical and exit this process. -1. If value is the keyword "bottom" and bias is not horizontal, then set y to a - {{CSSSimpleLength}} value representing 100% and bias to vertical and exit this process. -1. If value matches the <> production, then set norm to the result of - normalizing the value according to . If bias is vertical, - set y to norm, otherwise set x to norm and bias to - horizontal. Exit this process. -1. If value is not the keyword "center", then this process fails. - -

Determining x or y from a keyword and a length

- -The following process sets a value for either x ory, depending on an input keyword, -length, and bias. The process also updates bias based on the keyword and -length. - -1. follow the procedure outlined in with value given by - keyword, using the provided bias -1. let adjustment be the result of normalizing length according to - . -1. If the keyword is "right" or "bottom", let adjustment be the result of subtracting - adjustment from a zero length. -1. amend x (if bias is horizontal) or y (if bias is vertical) by - adding adjustment to it. - -{{CSSResourceValue}} normalization {#resourcevalue-normalization} --------------------------------------------------------------- + 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|. +
-Resource references are normalized by determining whether the reference is invalid -(in which case state is set to ''error'') or -requires network data (in which case state -is set to ''loading''). If data is not required and the reference is valid then -state is set to ''loaded''. +
+ To multiply two types |type1| and |type2|, + perform the following steps: -If state is set to ''loading'' then the image -reference is reevaluated once the pending data becomes available, according to the -same rules referenced above. + 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=]. -Normalization does not fail for {{CSSResourceValue}} objects. + 2. If both |type1| and |type2| have non-null [=percent hints=] + with different values, + the types can't be multiplied. + Return failure. -Security Considerations {#security-considerations} -================================================== + 3. If |type1| has a non-null [=percent hint=] |hint| and |type2| doesn't, + [=apply the percent hint=] |hint| to |type2|. -There are no known security issues introduced by these features. + Vice versa if |type2| has a non-null [=percent hint=] and |type1| doesn't. -Privacy Considerations {#privacy-considerations} -================================================== + 4. Copy all of |type1|’s [=map/entries=] to |finalType|, + then [=map/for each=] |baseType| → |power| of |type2|: -There are no known privacy issues introduced by these features. + 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=]. -Appendix A: Computed {{CSSStyleValue}} objects {#computed-stylevalue-objects} -============================================================================= + 5. Return |finalType|. +
-This appendix describes the restrictions on {{CSSStyleValue}} objects that -appear as computed values (i.e. as a value stored on computed -{{StylePropertyMapReadOnly}} objects). +
+ To invert a type |type|, + perform the following steps: -Computed {{CSSTokenStreamValue}} objects {#computed-tokenstreamvalue-objects} ------------------------------------------------------------------------------ + 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|. +
-Custom property references are resolved as part of style computation. Accordingly, -computed {{CSSTokenStreamValue}} objects will not contain {{CSSVariableReferenceValue}} objects. -As a result, only a single {{DOMString}} will appear in the sequence contained by -computed {{CSSTokenStreamValue}} objects. +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 <>, <
+ + + +Introduction {#intro} +===================== + +The API exposed by this specification is designed to provide basic font metrics +for both in-document and out-of-document content. + +Note: In a future version of this spec support may be added for exposing +information about individual runs of text, including information about +directionality, script, and character properties. + +Measure API {#measure-api} +============================================ + +
+partial interface Document {
+    FontMetrics measureElement(Element element);
+    FontMetrics measureText(DOMString text, StylePropertyMapReadOnly styleMap);
+};
+
+ +Two methods are provided for measuring text, one for in-document measurements +and another for out-of-document measurements. Both return a {{FontMetrics}} +object. + +{{Document/measureElement()}} takes an {{Element}} and returns a {{FontMetrics}} +object. If the {{Element}} is not in the document or isn't rendered an empty +{{FontMetrics}} object will be returned. + +{{Document/measureText()}} takes a {{DOMString}} and a +{{StylePropertyMapReadOnly}}, returning a {{FontMetrics}} object. Unless a font +is specified as a part of the styleMap the user agents default will be used. + +Note: The only styles that apply to the {{Document/measureText()}} method are +those that are passed in as a part of the styleMap. Document styles do not apply. + +{{FontMetrics}} object {#fontmetrics-definition} +---------------------------- + +
+[Exposed=Window]
+interface FontMetrics {
+  readonly attribute double width;
+  readonly attribute FrozenArray<double> advances;
+
+  readonly attribute double boundingBoxLeft;
+  readonly attribute double boundingBoxRight;
+
+  readonly attribute double height;
+  readonly attribute double emHeightAscent;
+  readonly attribute double emHeightDescent;
+  readonly attribute double boundingBoxAscent;
+  readonly attribute double boundingBoxDescent;
+  readonly attribute double fontBoundingBoxAscent;
+  readonly attribute double fontBoundingBoxDescent;
+
+  readonly attribute Baseline dominantBaseline;
+  readonly attribute FrozenArray<Baseline> baselines;
+  readonly attribute FrozenArray<Font> fonts;
+};
+
+ +The {{FontMetrics}} object has the following attributes: + +{{FontMetrics/width}} +The advance width of the line box, in CSS pixels. + +{{FontMetrics/advances}} +List of advances for each codepoint in the given text relative to the preceding +codepoint, in CSS pixels. Where a glyph is composed of a sequence of codepoints +the advance for the all but the first codepoint in the sequence will be zero. + +{{FontMetrics/boundingBoxLeft}} +The distance parallel to the {{FontMetrics/dominantBaseline}} from the alignment +point given by the text-align property to the left side of the bounding +rectangle of the given text, in CSS pixels; positive numbers indicating a +distance going left from the given alignment point. + +Note: The sum of this value and {{FontMetrics/boundingBoxRight}} can be wider +than the {{FontMetrics/width}}, in particular with slanted fonts where +characters overhang their advance width. + +{{FontMetrics/boundingBoxRight}} +The distance parallel to the {{FontMetrics/dominantBaseline}} from the alignment +point given by the text-align property to the right side of the bounding +rectangle of the given text, in CSS pixels. Positive numbers indicating a +distance going right from the given alignment point. + +{{FontMetrics/height}} +The distance between the highest top and the lowest bottom of the em squares in +the line box, in CSS pixels. + +{{FontMetrics/emHeightAscent}} +The distance from the {{FontMetrics/dominantBaseline}} to the highest top of the +em squares in the line box, in CSS pixels. +Positive numbers indicating that the {{FontMetrics/dominantBaseline}} is below +the top of that em square (so this value will usually be positive). +Zero if the {{FontMetrics/dominantBaseline}} is the top of that em square. +Half the font size if the {{FontMetrics/dominantBaseline}} is the middle of that +em square. + +{{FontMetrics/emHeightDescent}} +The distance from the {{FontMetrics/dominantBaseline}} to the lowest bottom of +the em squares in the line box, in CSS pixels. +Positive numbers indicating that the {{FontMetrics/dominantBaseline}} is below +the bottom of that em square (so this value will usually be negative). +Zero if the {{FontMetrics/dominantBaseline}} is the bottom of that em square. + +{{FontMetrics/boundingBoxAscent}} +The distance from the {{FontMetrics/dominantBaseline}} to the top of the +bounding rectangle of the given text, in CSS pixels; positive numbers indicating +a distance going up from the {{FontMetrics/dominantBaseline}}. + +Note: This number can vary greatly based on the input text, even if the first +font specified covers all the characters in the input. + +{{FontMetrics/boundingBoxDescent}} +The distance from the {{FontMetrics/dominantBaseline}} to the bottom of the +bounding rectangle of the given text, in CSS pixels; positive numbers indicating +a distance going down from the {{FontMetrics/dominantBaseline}}. + +{{FontMetrics/fontBoundingBoxAscent}} +The distance from the {{FontMetrics/dominantBaseline}} to the top of the highest +bounding rectangle of all the fonts used to render the text, in CSS pixels; +positive numbers indicating a distance going up from the +{{FontMetrics/dominantBaseline}}. + +Note: This value and {{FontMetrics/fontBoundingBoxDescent}} are useful when +metrics independent of the actual text being measured are desired as the values +will be consistent regardless of the text as long as the same fonts are being +used. +The {{FontMetrics/boundingBoxAscent}} attribute (and its corresponding attribute +for the descent) are useful when metrics specific to the given text are desired. + +{{FontMetrics/fontBoundingBoxDescent}} +The distance from the {{FontMetrics/dominantBaseline}} to the bottom of the +lowest bounding rectangle of all the fonts used to render the text, in +CSS pixels; positive numbers indicating a distance going down from the +{{FontMetrics/dominantBaseline}}. + +{{FontMetrics/dominantBaseline}} +Reference to the dominant {{Baseline}} for the given text in the list of +{{FontMetrics/baselines}}. + +{{FontMetrics/baselines}} +List of all {{Baseline}}s for the given text. + + +{{Baseline}} object {#baseline-definition} +---------------------------- + +
+[Exposed=Window]
+interface Baseline {
+  readonly attribute DOMString name;
+  readonly attribute double value;
+};
+
+ +Each {{Baseline}} object represents a baseline of the measured text and has the +following attributes: + +{{Baseline/name}} +Name of the baseline in question. + +{{Baseline/value}} +Distance from the {{FontMetrics/dominantBaseline}}, in CSS pixels. +Positive numbers indicating a distance going down from the +{{FontMetrics/dominantBaseline}}. + + +{{Font}} object {#font-definition} +---------------------------- + +
+[Exposed=Window]
+interface Font {
+  readonly attribute DOMString name;
+  readonly attribute unsigned long glyphsRendered;
+};
+
+ +Each {{Font}} object represents a font that was used for at least one glyph in +the measured text. It contains the following fields: + +{{Font/name}} +Font family name. + +{{Font/glyphsRendered}} +Number of glyphs used from the specific font. If multiple fonts are required to +render the specified text this attribute will indicate how many glyphs where +used from each font. + +Note: Indicates the number of glyphs which may be lower than the number of +codepoints. diff --git a/scroll-customization-api/UseCases.md b/scroll-customization-api/UseCases.md index 128f3cb9..32e5da88 100644 --- a/scroll-customization-api/UseCases.md +++ b/scroll-customization-api/UseCases.md @@ -67,7 +67,7 @@ Disable scroll chaining - Eg. Facebook chat widget (or any position:fixed overlay window) - position: fixed container with an overflow:scroll div inside - When the scroller has scrollTop=0, user attempting to scroll up on top of the widget should NOT cause the document to scroll -- IE/Edge have [-ms-scroll-chaining](https://msdn.microsoft.com/en-us/library/windows/apps/hh466007.aspx) for this +- Similar to IE/Edge's [-ms-scroll-chaining](https://msdn.microsoft.com/en-us/library/windows/apps/hh466007.aspx) except that effects only the chaining that occurs during a scroll gesture. The use case here may want to disable chaining even at the start of a new scroll gesture. Custom scroll limit ----- diff --git a/w3c.json b/w3c.json new file mode 100644 index 00000000..64df0a8b --- /dev/null +++ b/w3c.json @@ -0,0 +1,8 @@ +{ + "group": 32061, + "contacts": [ + "svgeesus" + ], + "repo-type": "rec-track", + "policy": "open" +} \ No newline at end of file diff --git a/worklets/Overview.bs b/worklets/Overview.bs index 9d8d656f..5601ca4f 100644 --- a/worklets/Overview.bs +++ b/worklets/Overview.bs @@ -3,55 +3,59 @@ Title: Worklets Level 1 Status: ED Group: houdini ED: https://drafts.css-houdini.org/worklets/ -Previous Version: http://www.w3.org/TR/2016/WD-worklets-1-20160607/ +TR: http://www.w3.org/TR/worklets-1/ +Previous Version: https://www.w3.org/TR/2020/WD-worklets-1-20200908/ Shortname: worklets Level: 1 Abstract: This specification defines an API for running scripts in stages of the rendering pipeline independent of the main javascript execution environment. -Editor: Ian Kilpatrick, ikilpatrick@chromium.org +Editor: Ian Kilpatrick, ikilpatrick@chromium.org, w3cid 73001 +
+ + + +
-urlPrefix: http://heycam.github.io/webidl/#dfn-; type: dfn;
-    text: inherit
+urlPrefix: https://fetch.spec.whatwg.org/; type: dfn;
+    urlPrefix: #concept-;
+        text: fetch
 urlPrefix: https://html.spec.whatwg.org/multipage/browsers.html; type: dfn;
     text: effective script origin
     url: #origin-2; text: origin
+urlPrefix: http://heycam.github.io/webidl/; type: dfn;
+    urlPrefix: #es-;
+        text: invoking callback functions
 urlPrefix: https://html.spec.whatwg.org/multipage/workers.html; type: dfn;
-    text: web workers
     urlPrefix: #dom-workerglobalscope-;
         text: self
 urlPrefix: https://html.spec.whatwg.org/multipage/webappapis.html; type: dfn;
-    text: api base url
-    text: api url character encoding
-    text: browsing context
-    text: code entry-point
-    text: creation url
     text: document environment
-    text: entry settings object
-    text: environment settings object
-    text: event loop
-    text: fetch a module script tree
-    text: global object
-    text: https state
-    text: incumbent settings object
+    text: event loop processing model
     text: microtask queue
-    text: module script
-    text: realm execution context
-    text: responsible browsing context
-    text: responsible document
-    text: responsible event loop
-    text: run a module script
-    text: script execution environment
     text: task queues
-urlPrefix: https://html.spec.whatwg.org/multipage/infrastructure.html; type: dfn;
-    text: cors setting attribute
-    text: in parallel
-    text: javascript global environment
-    urlPrefix: #js-;
-        text: syntaxerror;
-    url: resolve-a-url; text: resolve;
-urlPrefix: https://www.w3.org/2001/tag/doc/promises-guide; type: dfn;
-    text: a new promise
+    text: discarded; url: a-browsing-context-is-discarded
+urlPrefix: https://w3c.github.io/webappsec-csp/#; type: dfn;
+    text: initialize a global object's CSP list; url: initialize-global-object-csp
 urlPrefix: http://www.ecma-international.org/ecma-262/6.0/#sec-; type: dfn;
     text: Construct
     text: InitializeHostDefinedRealm
@@ -88,7 +92,7 @@ Worklets are similar to web workers however they:
     parallelism.
  - Are not event API based. Instead classes are registered on the global scope, whose methods are to
     be invoked by the user agent.
- - Have a reduced API surface on the javascript global environment (global scope).
+ - Have a reduced API surface on the global scope.
  - Have a lifetime for the global scope which is defined by subsequent specifications or user
     agents. They aren't tied to the lifetime of the document.
 
@@ -98,28 +102,49 @@ expected to be shared between separate scripts. This is similar to the docume
 Code Idempotency {#code-idempotency}
 ------------------------------------
 
-This section is not normative.
-
-Multiple instances of {{WorkletGlobalScope}} can be created for each {{Worklet}} that they belong
-to. User agents may choose to do this in order to parallelize work over multiple threads, or to move
-work between threads as required.
+Some specifications which use worklets ([[css-paint-api-1]]), allow user agents to parallelize work
+over multiple threads, or to move work between threads as required.
 
-Additionally different user agents may invoke a method on a class in a different order to other user
-agents.
+In these specifications user agents might invoke methods on a class in a different order to other
+user agents.
 
 As a result of this, to prevent this compatibility risk between user agents, authors who register
-classes on the global scope should make their code idempotent. That is, a method or set of methods
-on a class should produce the same output given a particular input.
+classes on the global scope using these APIs, should make their code idempotent. That is, a method
+or set of methods on a class should produce the same output given a particular input.
+
+The following techniques are used in order to encourage authors to write code in an idempotent way:
 
-The following techniques should be used in order to encourage authors to write code in an idempotent
-way:
  - No reference to the global object, e.g. self on a {{DedicatedWorkerGlobalScope}}.
+
  - Code is loaded as a module script which resulting in the code being executed in strict
      mode code without a shared this. This prevents two different module scripts sharing
-     state be referencing shared objects on the global scope.
- - User agents may choose to always have at least two {{WorkletGlobalScope}}s per {{Worklet}} and
-    randomly assign a method or set of methods on a class to a particular global scope.
- - User agents may create and destroy {{WorkletGlobalScope}}s at any time.
+     state by referencing shared objects on the global scope.
+
+ - These specifications must require user agents to always have at least two {{WorkletGlobalScope}}s
+     per {{Worklet}} and randomly assign a method or set of methods on a class to a particular
+     global scope. These specifications can provide an opt-out under memory constraints.
+
+ - User agents can create and destroy {{WorkletGlobalScope}}s at any time for these specifications.
+
+Speculative Evaluation {#speculative-evaluation}
+------------------------------------------------
+
+Some specifications which use worklets ([[css-paint-api-1]]) may invoke methods on a class based on
+the state of the user agent. To increase the concurrency between threads, a user agent may invoke a
+method speculatively, based on potential future states.
+
+In these specifications user agents might invoke methods on a class at any time, and with any
+arguments, not just ones corresponding to the current state of the user agent. The results of such
+speculative evaluations are not displayed immediately, but may be cached for use if the user agent
+state matches the speculated state.  This may increase the concurrency between the user agent and
+worklet threads.
+
+As a result of this, to prevent this compatibility risk between user agents, authors who register
+classes on the global scope using these APIs, should make their code stateless. That is, the only
+effect of invoking a method should be its result, not any side-effects such as updating mutable
+state.
+
+The same techniques which encourage code idempotence also encourage authors to write stateless code.
 
 Infrastructure {#infrastructure}
 ================================
@@ -133,11 +158,25 @@ global execution context of a {{Worklet}}.
 
 [Exposed=Worklet]
 interface WorkletGlobalScope {
-    attribute Console console;
 };
 
-A {{WorkletGlobalScope}} has an associated environment settings object. +Each {{WorkletGlobalScope}} has an assocated owner +document. +It is initially null and set inside the create a WorkletGlobalScope algorithm. + +Whenever a {{Document}} object is discarded, each {{WorkletGlobalScope}} whose owner +document is that {{Document}} object, should clear its owner document. + +Each {{WorkletGlobalScope}} has an associated environment settings object. + +Each {{WorkletGlobalScope}} has an associated module map. It is a +module map, initially empty. + +Each {{WorkletGlobalScope}} has a worklet global scope execution environment. This +execution environment may be parallel (i.e. it may be on a separate thread, process, or other +equivalent construct), or it may live on the same thread or process as the {{Worklet}} object it +belongs to. Which thread or process it lives on is decided by the user agent. Note: The {{WorkletGlobalScope}} has a limited global scope when compared to a @@ -145,109 +184,145 @@ Note: {{WorkletGlobalScope}} with registerAClass methods which will allow authors to register classes for the user agent create and invoke methods on. +When asked to report an exception, do nothing instead, or optionally report the exception to +a developer console. + +Issue(whatwg/html#2611): HTML's report an exception needs updating to work with + non-EventTarget global objects. + ### The event loop ### {#the-event-loop} Each {{WorkletGlobalScope}} object has a distinct event loop. This event loop has no -associated browsing context, and only its microtask queue is used (all other task -queues are not used). The event loop is created by the create a +associated browsing context. The event loop is created by the create a WorkletGlobalScope algorithm. +The event loop is run on the worklet global scope execution environment defined above. + +It is expected that only tasks associated {{Worklet/addModule()}}, the user agent invoking author +defined callbacks, and microtasks will use this event loop. + +Note: + Even through the event loop processing model specifies that it loops continually, + practically implementations aren't expected to do this. The microtask queue is emptied + while invoking callback functions provided by the author. + ### Creating a WorkletGlobalScope ### {#creating-a-workletglobalscope} -When a user agent is to create a WorkletGlobalScope, for a given |worklet|, it -must run the following steps: +
+When a user agent is to create a WorkletGlobalScope, given |workletGlobalScopeType|, +|moduleResponsesMap|, and |outsideSettings|, it must run the following steps: - 1. Let |workletGlobalScopeType| be the |worklet|'s worklet global scope type. + 1. Let |agent| be the result of [=obtain a worklet agent|obtaining a worklet agent=] given + |outsideSettings|. This agent corresponds to the a worklet global scope execution + environment. Run the rest of these steps in that context. - 2. Call the JavaScript InitializeHostDefinedRealm abstract operation with the following - customizations: + 2. Let |realmExecutionContext| be the result of + [=create a new JavaScript realm|creating a new JavaScript realm=] given |agent| and + the following customizations: - For the global object, create a new |workletGlobalScopeType| object. Let |workletGlobalScope| be the created object. - - Let |realmExecutionContext| be the created JavaScript execution context. + 3. Let |insideSettings| be the result of set up a worklet environment settings object + given |realmExecutionContext|, and |outsideSettings|. + + 4. Set |workletGlobalScope|'s owner document to |outsideSettings|'s responsible + document. - - Do not obtain any source texts for scripts or modules. + 5. Invoke the initialize a global object's CSP list algorithm given |workletGlobalScope|. - 3. Let |settingsObject| be the result of set up a worklet environment settings object - with |realmExecutionContext|. + 6. For each |entry| in the given |moduleResponsesMap| (in insertion order), run the following + substeps: - 4. Associate the |settingsObject| with |workletGlobalScope|. + 1. Let |moduleURLRecord| be |entry|'s key. - 5. For each |resolvedModuleURL| in the given |worklet|'s worklet's resolved module URLs, - run the following substeps: - 1. Let |script| be the result of fetch a module script tree given - |resolvedModuleURL|, "anonymous" for the CORS setting attribute, and - |settingsObject|. + 2. Let |script| be the result of fetch a worklet script given |moduleURLRecord|, + |moduleResponsesMap|, |outsideSettings|, and |insideSettings| when + it asynchronously completes. - Note: Worklets follow web workers here in not allowing "use-credientials" for - fetching resources. + 3. Run a module script given |script|. - 2. Run a module script given |script|. + Note: Fetch a worklet script won't actually perform a network request as it will hit + the worklet's module responses map. It also won't have a parsing error as at this + point it should have successfully been parsed by another worklet global scope. I.e. + |script| should never be null here. + + 6. Run the responsible event loop specified by |insideSettings|. +
### Script settings for worklets ### {#script-settings-for-worklets} -When a user agent is to set up a worklet environment settings object, given a -|executionContext|, it must run the following steps: - 1. Let |inheritedResponsibleBrowsingContext| be the responsible browsing context - specified by the incumbent settings object. +
+When a user agent is to set up a worklet environment settings object, given a +|executionContext|, and |outsideSettings|, it must run the following steps: + 1. Let |origin| be a unique opaque origin. + + 2. Let |inheritedAPIBaseURL| be |outsideSettings|'s API base URL. - 2. Let |inheritedOrigin| be the origin specified by the incumbent settings object. + 3. Let |inheritedReferrerPolicy| be |outsideSettings|'s referrer policy. - 3. Let |inheritedAPIBaseURL| be the API base URL specified by the incumbent settings object. + 4. Let |inheritedEmbedderPolicy| be |outsideSettings|'s embedder policy. - 4. Let |workletEventLoop| be a newly created event loop. + 5. Let |realm| be the value of |executionContext|'s Realm component. - 5. Let |workletGlobalScope| be |executionContext|'s global object. + 6. Let |workletGlobalScope| be |realm|'s global object. - 6. Let |settingsObject| be a new environment settings object whose algorithms are - defined as follows: + 7. Let |settingsObject| be a new environment settings object whose algorithms are defined + as follows: - : The realm execution context + : The realm execution context :: Return |executionContext|. - : The global object - :: Return |workletGlobalScope|. + : The module map. + :: Return |workletGlobalScope|'s module map. - : The responsible browsing context - :: Return |inheritedResponsibleBrowsingContext|. + : The responsible document + :: Not applicable. - : The responsible event loop - :: Return |workletEventLoop|. + : The API URL character encoding + :: Return UTF-8. - : The responsible document - :: Not applicable (the responsible event loop is not a browsing context - event loop). + : The API base URL + :: Return |inheritedAPIBaseURL|. - : The API URL character encoding - :: Return UTF-8. + : The origin + :: Return |origin|. - : The API base URL - :: Return |inheritedAPIBaseURL|. + : The referrer policy + :: Return |inheritedReferrerPolicy|. - : The origin and effective script origin - :: Return |inheritedOrigin|. + : The embedder policy + :: Return |inheritedEmbedderPolicy|. - : The creation URL - :: Not applicable. + 8. Set |settingsObject|'s id to a new unique opaque string, + creation URL to |inheritedAPIBaseURL|, + top-level creation URL to null, + top-level origin to |outsideSettings|'s top-level origin, + target browsing context to null, and + active service worker to null. - : The HTTPS state - :: Return |workletGlobalScope|'s HTTPS state. + 9. Set |realm|'s \[[HostDefined]] field to |settingsObject|. - 7. Return |settingsObject|. + 10. Return |settingsObject|. +
Issue: Merge this with https://html.spec.whatwg.org/multipage/workers.html#set-up-a-worker-environment-settings-object Worklet {#worklet-section} -------------------------- -The {{Worklet}} object provides the capability to import module scripts into its associated +The {{Worklet}} object provides the capability to add module scripts into its associated {{WorkletGlobalScope}}s. The user agent can then create classes registered on the {{WorkletGlobalScope}}s and invoke their methods.
+[Exposed=Window]
 interface Worklet {
-    [NewObject] Promise<void> import(DOMString moduleURL);
+    [NewObject] Promise<undefined> addModule(USVString moduleURL, optional WorkletOptions options = {});
+};
+
+dictionary WorkletOptions {
+    RequestCredentials credentials = "same-origin";
 };
 
@@ -259,52 +334,200 @@ Note: As an example the worklet global scope type might be a {{PaintWorkl A {{Worklet}} has a list of the worklet's WorkletGlobalScopes. Initially this list is empty; it is populated when the user agent chooses to create its {{WorkletGlobalScope}}. -A {{Worklet}} has a list of the worklet's resolved module URLs. Initially this list is -empty; it is populated when module scripts resolved. +A {{Worklet}} has a worklet destination type. This is used for setting the destination requests from fetch a module worker script graph. -When the import(|moduleURL|) method is called on a {{Worklet}} object, -the user agent must run the following steps: +A {{Worklet}} has a module responses map. This is a ordered map of module URLs to values +that are a fetch responses. The map's entries are ordered based on their insertion order. +Access to this map should be thread-safe. + +The module responses map exists to ensure that {{WorkletGlobalScope}}s created at different +times contain the same set of script source text and have the same behaviour. The creation of +additional {{WorkletGlobalScope}}s should be transparent to the author. + +
+ Practically user agents aren't expected to implement the following algorithm using a + thread-safe map. Instead when {{Worklet/addModule()}} is called user agents can fetch the module + graph on the main thread, and send the fetched sources (the data contained in the module + responses map) to each thread which has a {{WorkletGlobalScope}}. + + If the user agent wishes to create a new {{WorkletGlobalScope}} it can simply sent the list of + all fetched sources from the main thread to the thread which owns the {{WorkletGlobalScope}}. +
+ +A pending tasks struct is a struct consisting of: + - A counter. +This is used by the algorithms below. + +
+When the addModule(|moduleURL|, |options|) method is called on a +{{Worklet}} object, the user agent must run the following steps: 1. Let |promise| be a new promise. - 2. Run the following steps in parallel: - 1. Let |resolvedModuleURL| be the result of resolving the |moduleURL| relative to the - API base URL specified by the entry settings object when the method was - invoked. + 2. Let |worklet| be this {{Worklet}}. + + 3. Let |outsideSettings| be the relevant settings object of this. + + 4. Let |moduleURLRecord| be the result of parsing the |moduleURL| argument relative to + |outsideSettings|. + + 5. If |moduleURLRecord| is failure, then reject promise with a "{{SyntaxError}}" + {{DOMException}} and return |promise|. + + 6. Return |promise|, and then continue running this algorithm in parallel. + + 7. Let |credentialOptions| be the {{WorkletOptions/credentials}} member of |options|. + + 8. Let |moduleResponsesMap| be |worklet|'s module responses map. - 2. If this fails, reject |promise| with a SyntaxError exception and abort these - steps. + 9. Let |workletGlobalScopeType| be |worklet|'s worklet global scope type. - 3. Add |resolvedModuleURL| to the list of worklet's resolved module URLs. + 10. Let |destination| be |worklet|'s worklet destination type. - 4. Ensure that there is at least one {{WorkletGlobalScope}} in the worklet's - WorkletGlobalScopes. If not create a WorkletGlobalScope given the current - {{Worklet}}. + 11. If the worklet's WorkletGlobalScopes is empty, run the following steps: - 5. For each {{WorkletGlobalScope}} in the worklet's WorkletGlobalScopes, run these - substeps: - 1. Let |settings| be the {{WorkletGlobalScope}}'s associated environment settings - object. + 1. Create a WorkletGlobalScope given |workletGlobalScopeType|, |moduleResponsesMap|, + and |outsideSettings|. - 2. Let |script| be the result of fetch a module script tree given - |resolvedModuleURL|, "anonymous" for the CORS setting attribute, and - |settings|. + 2. Add the {{WorkletGlobalScope}} to worklet's WorkletGlobalScopes. - Note: Worklets follow web workers here in not allowing "use-credientials" for - fetching resources. + Depending on the type of worklet the user agent may create additional + {{WorkletGlobalScope}}s at this time. - 3. Run a module script given |script|. + Note: Specifically the [[css-paint-api-1]] allows for multiple global scopes, while the + [[webaudio]] API does not. - 6. If all the steps above succeeded (in particular, if all of the scripts parsed - and loaded into the global scopes), resolve |promise|. -
Otherwise, reject |promise|. + Wait for this step to complete before continuing. - Note: Specifically, if a script fails to parse or fails to load over the network, it should - reject the promise; if the script throws an error while first evaluating the promise - should resolve as a classes may have been registered correctly. + 12. Let |pendingTaskStruct| be a new pending tasks struct with counter initialized to the length of worklet's + WorkletGlobalScopes. - 3. Return |promise|. + 13. For each |workletGlobalScope| in the worklet's WorkletGlobalScopes, queue a + task on the |workletGlobalScope| to fetch and invoke a worklet script given + |workletGlobalScope|, |moduleURLRecord|, |destination|, |moduleResponsesMap|, + |credentialOptions|, |outsideSettings|, |pendingTaskStruct|, and |promise|. -Issue(w3c/css-houdini-drafts#47): Need ability to load code into {{WorkletGlobalScope}} declaratively. + Note: The rejecting and resolving of the |promise| occurs within the fetch and invoke a + worklet script algorithm. +
+ +
+When the user agent is to fetch and invoke a worklet script given |workletGlobalScope|, +|moduleURLRecord|, |destination|, |moduleResponsesMap|, |credentialOptions|, |outsideSettings|, +|pendingTaskStruct|, and |promise|, the user agent must run the following steps: + + Note: This algorithm is to be run within the worklet global scope execution environment. + + 1. Let |insideSettings| be the |workletGlobalScope|'s associated environment settings + object. + + 2. Let |script| by the result of fetch a worklet script given |moduleURLRecord|, + |destination|, |moduleResponsesMap|, |credentialOptions|, |outsideSettings|, and + |insideSettings| when it asynchronously completes. + + 3. If |script| is null, then queue a task on |outsideSettings|'s responsible event + loop to run these steps: + + 1. If |pendingTaskStruct|'s counter is not -1, then + run these steps: + + 1. Set |pendingTaskStruct|'s counter to -1. + + 2. Reject |promise| with an "{{AbortError}}" {{DOMException}}. + + 4. If |script|'s error to rethrow is not null, then queue a task on + |outsideSettings|'s responsible event loop given |script|'s error to rethrow + to run these steps: + + 1. If |pendingTaskStruct|'s counter is not -1, then + run these steps: + + 1. Set |pendingTaskStruct|'s counter to -1. + + 2. Reject |promise| with error to rethrow. + + 5. Run a module script given |script|. + + 6. Queue a task on |outsideSettings|'s responsible event loop to run these steps: + + 1. If |pendingTaskStruct|'s counter is not -1, then + run these steps: + + 1. Decrement |pendingTaskStruct|'s counter by + 1. + + 2. If |pendingTaskStruct|'s counter is 0, then + resolve |promise|. +
+ +
+When the user agent is to fetch a worklet script given |moduleURLRecord|, |destination|, +|moduleResponsesMap|, |credentialOptions|, |outsideSettings|, and |insideSettings|, the user agent +must run the following steps: + +Note: This algorithm is to be run within the worklet global scope execution environment. + +1. Fetch a module worker script graph given |moduleURLRecord|, |outsideSettings|, + |destination|, |credentialOptions|, and |insideSettings|. + + To perform the fetch given |request|, perform the following steps: + + 1. Let |cache| be the |moduleResponsesMap|. + + 2. Let |url| be |request|'s url. + + 3. If |cache| contains an entry with key |url| whose value is "fetching", wait until that + entry's value changes, then proceed to the next step. + + 4. If |cache| contains an entry with key |url|, asynchronously complete this algorithm with + that entry's value, and abort these steps. + + 5. Create an entry in |cache| with key |url| and value "fetching". + + 6. Fetch |request|. + + 7. Let |response| be the result of fetch when it asynchronously completes. + + 8. Set the value of the entry in |cache| whose key is |url| to |response|, and + asynchronously complete this algorithm with |response|. + +2. Return the result of fetch a module worker script graph when it asynchronously completes. + +Note: Specifically, if a script fails to parse or fails to load over the network, it will reject the + promise. If the script throws an error while first evaluating the promise it will resolve as a + classes may have been registered correctly. + +
+ When an author adds code into a {{Worklet}} the code may run against multiple + {{WorkletGlobalScope}}s, for example: +
+        // script.js
+        console.log('Hello from a WorkletGlobalScope!');
+    
+ +
+        // main.js
+        await CSS.paintWorklet.addModule('script.js');
+    
+ + Behind the scenes the user agent may load the script.js + into 4 global scopes, in which case the debugging tools for the user agent would print: +
+        [paintWorklet#1] Hello from a WorkletGlobalScope!
+        [paintWorklet#4] Hello from a WorkletGlobalScope!
+        [paintWorklet#2] Hello from a WorkletGlobalScope!
+        [paintWorklet#3] Hello from a WorkletGlobalScope!
+    
+ + If the user agent decided to kill and restart a {{WorkletGlobalScope}} number 3 in this example, + it would print [paintWorklet#3] Hello from a + WorkletGlobalScope! again in the debugging tools when this occurs. +
+
+ +Issue(w3c/css-houdini-drafts#47): Need ability to load code into a {{WorkletGlobalScope}} + declaratively. Lifetime of the Worklet {#lifetime-of-the-worklet} -------------------------------------------------- @@ -330,34 +553,34 @@ Examples {#examples} For these examples we'll use a fake worklet on window. -
-partial interface Window {
-  [SameObject] readonly attribute Worklet fakeWorklet1;
-  [SameObject] readonly attribute Worklet fakeWorklet2;
-};
-
+
+
+    partial interface Window {
+      [SameObject] readonly attribute Worklet fakeWorklet1;
+      [SameObject] readonly attribute Worklet fakeWorklet2;
+    };
+    
-
-callback Function = any (any... arguments);
+    
+    [Global=(Worklet,FakeWorklet),Exposed=FakeWorklet]
+    interface FakeWorkletGlobalScope : WorkletGlobalScope {
+        void registerAnArbitaryClass(DOMString type, Function classConstructor);
+    };
+    
-[Global=(Worklet,FakeWorklet),Exposed=FakeWorklet] -interface FakeWorkletGlobalScope : WorkletGlobalScope { - void registerAnArbitaryClass(DOMString type, Function classConstructor); -}; -
+ Each {{FakeWorkletGlobalScope}} has a map of the registered class constructors map. -Each {{FakeWorkletGlobalScope}} has a map of the registered class constructors map. - -When the -registerAnArbitaryClass(|type|, |classConstructor|) method is called, the user agent will add -the |classConstructor| of |type| to the map of registered class constructors map. + When the + registerAnArbitaryClass(|type|, |classConstructor|) method is called, the user agent will add + the |classConstructor| of |type| to the map of registered class constructors map. +
Loading scripts into a worklet. {#example-single} -------------------------------------------------
-window.fakeWorklet1.import('script1.js');
-window.fakeWorklet1.import('script2.js');
+window.fakeWorklet1.addModule('script1.js');
+window.fakeWorklet1.addModule('script2.js');
 
 // Assuming no other calls to fakeWorklet1 valid script loading orderings are:
 // 1. 'script1.js', 'script2.js'
@@ -369,8 +592,8 @@ Loading scripts into multiple worklets. {#example-multiple}
 
 
 Promise.all([
-    window.fakeWorklet1.import('script1.js'),
-    window.fakeWorklet2.import('script2.js')
+    window.fakeWorklet1.addModule('script1.js'),
+    window.fakeWorklet2.addModule('script2.js')
 ]).then(function() {
     // Both scripts now have loaded code, can do a task which relies on this.
 });
@@ -405,4 +628,3 @@ could follow the following steps:
         Arguments=["true"]).
 
     5. Return |result|.
-