react-x11 2.52.0 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1091 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
|
@@ -1,707 +0,0 @@
|
|
|
1
|
-
# Animation in styles: timelines, opacity, transforms, and who interpolates
|
|
2
|
-
|
|
3
|
-
_Design document, 2026-09-06. Written against master at c9ce902 (react-x11
|
|
4
|
-
2.6.1, ntk ^8.7.0, `@windowkit/appkit` ^0.4.0). File:line references will
|
|
5
|
-
drift with the code; the shape of the argument should not._
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 0. TL;DR
|
|
10
|
-
|
|
11
|
-
The style vocabulary already has two animation shapes — `transition` (a
|
|
12
|
-
change, over so many ms) and `animation` (a loop between two values) — and
|
|
13
|
-
one runtime that drives both: the window's frame clock, JS interpolation,
|
|
14
|
-
a damage claim per frame. That runtime is correct on every backend and is
|
|
15
|
-
the fallback everything below degrades to.
|
|
16
|
-
|
|
17
|
-
What an author cannot write today, in order of how often it comes up:
|
|
18
|
-
|
|
19
|
-
1. **A fade.** There is no `opacity`. The ecosystem page opens by saying so.
|
|
20
|
-
2. **A move that does not reflow.** There is no `translate`/`rotate`/`scale`;
|
|
21
|
-
the Switch thumb slides on `start` and pays a yoga pass per frame for it.
|
|
22
|
-
3. **A timeline that ends.** `animation` loops forever by definition; an
|
|
23
|
-
entrance, a shake, a pulse-three-times have no spelling.
|
|
24
|
-
4. **More than two stops**, an easing on a transition, a delay, a spring.
|
|
25
|
-
5. **Motion that survives a busy JS thread.** On macOS the retained layer
|
|
26
|
-
presenter can hand a transition to Core Animation and stop scheduling
|
|
27
|
-
frames for it; today it sets layer properties per frame like everything
|
|
28
|
-
else.
|
|
29
|
-
|
|
30
|
-
The proposal is one vocabulary, four additions, and one execution rule:
|
|
31
|
-
|
|
32
|
-
- **Easing** becomes cubic-bezier control points under the hood, with names
|
|
33
|
-
as aliases and springs as a second kind. This is the load-bearing change:
|
|
34
|
-
it is what lets a JS evaluator and Core Animation agree on the same
|
|
35
|
-
declaration (§3.1 has the numbers — mapping today's ease-out to CA's
|
|
36
|
-
_named_ ease-out is 22% off at the peak; to control points it is 0.3%).
|
|
37
|
-
- **`transition`** grows a per-property object form with `easing` and
|
|
38
|
-
`delay`, and `all` for the shorthand's slot.
|
|
39
|
-
- **`animation`** grows `keyframes`, `repeat` (default `Infinity`, so every
|
|
40
|
-
existing loop keeps its meaning), `delay`, and `key` to re-trigger. A
|
|
41
|
-
finite one rests at the declared value when it ends — the rule loops
|
|
42
|
-
already follow, which turns out to be CSS's `fill-mode: none` for free.
|
|
43
|
-
- **`opacity`, `translateX`, `translateY`, `rotate`, `scaleX`, `scaleY`** as
|
|
44
|
-
paint-only style properties on both backends. Free on a layer; an
|
|
45
|
-
offscreen composite on X11, bounded to the subtree, with fast paths for
|
|
46
|
-
the common cases (a leaf's opacity, a pure translate).
|
|
47
|
-
- **The presenter decides who interpolates.** The node model stays the one
|
|
48
|
-
source of truth for _what_ is animating and _when it ends_. A presenter
|
|
49
|
-
may take a property it can express on the node's own layer; anything it
|
|
50
|
-
declines runs on the JS loop exactly as now. The layer presenter takes
|
|
51
|
-
paint properties, opacity and transforms; layout properties stay on the
|
|
52
|
-
JS loop everywhere, because yoga has to run.
|
|
53
|
-
|
|
54
|
-
Five bridge verbs stand between the layer presenter and that offload
|
|
55
|
-
(§4.4). The first — control-point timing functions — is a prerequisite,
|
|
56
|
-
not an enhancement: without it every existing transition would change its
|
|
57
|
-
look the day it moved to the render server.
|
|
58
|
-
|
|
59
|
-
**Status, 2026-09-06.** Bridge items 1–8 shipped in `@windowkit/appkit`
|
|
60
|
-
0.5.0 (§4.4). Landed in react-x11 on top of it: the presenter seam of §4.1
|
|
61
|
-
and the PropBox rows of §4.2 (`animate`/`cancel` in
|
|
62
|
-
src/cocoa/presenter.js, the `offloaded` entries in src/nodes/animation.js), with additive
|
|
63
|
-
retargets for lengths and presentation-value retargets for colours, and
|
|
64
|
-
reduce motion on the Cocoa backend (src/desktopsettings.js's `fromMacOS`).
|
|
65
|
-
The vocabulary of §3 — easing, timelines, `opacity`, transforms — is still
|
|
66
|
-
to come; the control-point tables it needs exist (`EASING_CONTROL_POINTS`).
|
|
67
|
-
|
|
68
|
-
_Since then, `opacity` has landed (#601) — on Cocoa an opacity transition
|
|
69
|
-
or loop runs in the render server (#818) — and so has a loop's `delay`
|
|
70
|
-
(#605). Transform style properties, keyframes and finite timelines have
|
|
71
|
-
not._
|
|
72
|
-
|
|
73
|
-
## 1. What exists
|
|
74
|
-
|
|
75
|
-
### 1.1 The vocabulary
|
|
76
|
-
|
|
77
|
-
`transition: number | { [prop]: ms }` — a number covers every animatable
|
|
78
|
-
property, an object picks them (docs/styling.md "Transitions"). One fixed
|
|
79
|
-
ease-out cubic (`ease`, src/styles.js:1121). A transition starts from what
|
|
80
|
-
is on screen, so an interrupted one reverses from where it got to
|
|
81
|
-
(`_retarget`, src/nodes/animation.js), and it starts at `now()` rather than the
|
|
82
|
-
last frame's timestamp, so an idle window does not find it already over
|
|
83
|
-
(#80). Nothing before a node's first frame transitions — an inserted
|
|
84
|
-
element appears at its style (`_placed`, test/transition-mount.test.js).
|
|
85
|
-
Layout properties may transition and cost a layout pass per frame; enums,
|
|
86
|
-
`zIndex`, `boxShadow` and `backgroundImage` snap (`NOT_ANIMATABLE`,
|
|
87
|
-
src/styles.js:726).
|
|
88
|
-
|
|
89
|
-
`animation: { [prop]: { from?, to, duration, easing?, alternate? } }` — a
|
|
90
|
-
loop (#352). `from` defaults to the declared value, which is also where the
|
|
91
|
-
property rests whenever the loop is not running. Four named easings,
|
|
92
|
-
`linear` by default because a cycle that eases out stutters at the wrap.
|
|
93
|
-
Phase is a modulo of elapsed time (`animationValueAt`, src/styles.js:965).
|
|
94
|
-
An equal declaration keeps its phase across re-renders (`sameAnimation`).
|
|
95
|
-
It stops when the window is unmapped, minimized or obscured, when anything
|
|
96
|
-
above the node hides it, when the node unmounts or the style drops it, and
|
|
97
|
-
under reduced motion (`_loopsAllowed`, src/nodes/animation.js) — and every one
|
|
98
|
-
of those restarts it from the top when it goes away.
|
|
99
|
-
|
|
100
|
-
### 1.2 The runtime
|
|
101
|
-
|
|
102
|
-
One path for both shapes. `_retarget` puts an entry per property into
|
|
103
|
-
`node._anim` and the node into `root._animating`; `flush` calls
|
|
104
|
-
`_advanceAnimations(now())` (src/nodes/animation.js), which claims each
|
|
105
|
-
animating node where it stands, ticks it, and sets `needsPaint`. That claim
|
|
106
|
-
is where the node _was_; a tick on a layout property asks for a layout pass,
|
|
107
|
-
and the pass claims the old and new rect of every node it moves — the
|
|
108
|
-
animating one and whatever it pushed, in flow or not (`_assignAbs`,
|
|
109
|
-
src/nodes/layout.js) — so a layout animation repaints what it moves rather
|
|
110
|
-
than the window (#603). A box that moved and changed nothing else is one
|
|
111
|
-
claim at each end for its whole subtree, clipped to the boxes that clip it
|
|
112
|
-
(`_beginRigidMove`), and the nodes under it that yoga did not lay out again
|
|
113
|
-
are moved with it rather than read back out of yoga (`_followParent`). The
|
|
114
|
-
window keeps asking for frames while `_animating` is non-empty; the
|
|
115
|
-
animation _is_ the repaint loop. `interpolate` (src/styles.js:990) lerps
|
|
116
|
-
numbers, percentages of the same unit, and colours — premultiplied, so a
|
|
117
|
-
fade from `transparent` does not pass through grey.
|
|
118
|
-
|
|
119
|
-
`withFrameClock` in `react-x11/test` freezes the clock; the loop tests
|
|
120
|
-
(test/animation-loop.test.js) and the transition tests in test/style.test.js
|
|
121
|
-
drive it by hand.
|
|
122
|
-
|
|
123
|
-
### 1.3 Where the pixels come from
|
|
124
|
-
|
|
125
|
-
Three presentation paths, and the animation runtime is identical on all of
|
|
126
|
-
them:
|
|
127
|
-
|
|
128
|
-
| backend | a frame is | can anything interpolate for us? |
|
|
129
|
-
| -------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
130
|
-
| X11 (ntk / XRender) | damage rects → paint walk → XRender | no |
|
|
131
|
-
| Cocoa, `surface` presenter (**the default**) | the same paint walk into an IOSurface swapchain | no — one bitmap per window |
|
|
132
|
-
| Cocoa, `layers` presenter (opt-in) | dirty nodes → per-node visuals → one `CATransaction` of property diffs | **yes** — the render server animates layer properties |
|
|
133
|
-
|
|
134
|
-
The layer presenter (src/cocoa/presenter.js) keeps a visual per drawn node:
|
|
135
|
-
a **PropBox** for a plain `<box>` (frame, zPosition, hidden, masksToBounds,
|
|
136
|
-
cornerRadius, backgroundColor, borderWidth, borderColor — zero raster), a
|
|
137
|
-
**Raster** for everything else (the node's own paint replayed into a bitmap
|
|
138
|
-
that becomes the layer's `contents`), shape layers for `<svg>`, an overlay
|
|
139
|
-
for scrollbars, and a clip host whose `bounds.origin` is the scroll offset.
|
|
140
|
-
A JS-driven transition on this presenter works today by construction: each
|
|
141
|
-
tick invalidates the node, the frame re-diffs its properties and sends the
|
|
142
|
-
changed ones under `disableActions`. It is the same cost model as X11 with
|
|
143
|
-
a cheaper sink.
|
|
144
|
-
|
|
145
|
-
It is opt-in — `createRoot({ cocoa: { presenter: 'layers' } })` or
|
|
146
|
-
`REACT_X11_COCOA_PRESENTER=layers` — while docs/macos.md's measure-first
|
|
147
|
-
gate is open. Everything in §4 is about that presenter; on the surface
|
|
148
|
-
presenter there is nothing to hand an animation to.
|
|
149
|
-
|
|
150
|
-
### 1.4 What the bridge can already do
|
|
151
|
-
|
|
152
|
-
`@windowkit/appkit`'s verbs, as react-x11 reaches them through `native`
|
|
153
|
-
(src/cocoa/native.js):
|
|
154
|
-
|
|
155
|
-
- `addAnimation(layer, keyPath, opts, key)` — a `CABasicAnimation`:
|
|
156
|
-
`from`/`to` (numbers, `[x, y]` points, `[r, g, b, a]` colours), `duration`
|
|
157
|
-
in **seconds**, `repeat` (a count, or `Infinity`), `autoreverse`, `timing`
|
|
158
|
-
as one of four **named** curves (`linear`, `easeIn`, `easeOut`,
|
|
159
|
-
`easeInEaseOut`; anything else is CA's default), and `hold` for
|
|
160
|
-
`fillMode: forwards` + `removedOnCompletion: NO`.
|
|
161
|
-
- `removeAnimation(layer, key)`, `removeAllAnimations(layer)`.
|
|
162
|
-
- `txBegin({ duration, timing, disableActions })` / `txCommit()` — the
|
|
163
|
-
implicit-animation route: set a property inside a transaction with a
|
|
164
|
-
duration and CA animates it.
|
|
165
|
-
- `setLayerProps` accepts `opacity`, `anchorPoint`, `position`, `bounds`,
|
|
166
|
-
the shadow quartet, and `transform` as
|
|
167
|
-
`{ translateX, translateY, rotate (radians), scale | scaleX, scaleY }`,
|
|
168
|
-
composed translate → rotate → scale — CSS's order for the individual
|
|
169
|
-
transform properties, and the shape §3.4 proposes.
|
|
170
|
-
|
|
171
|
-
What it cannot do, each of which §4.4 turns into an issue: a timing function
|
|
172
|
-
from control points; a keyframe animation; a spring; a delay (`beginTime`);
|
|
173
|
-
an additive animation; a completion callback; reading a presentation value;
|
|
174
|
-
and the desktop's reduce-motion switch, which on Cocoa never reaches
|
|
175
|
-
`desktopSettings().animations` at all (src/desktopsettings.js reads
|
|
176
|
-
XSETTINGS and nothing else, so a mac reports `animations: true` whatever
|
|
177
|
-
the user set).
|
|
178
|
-
|
|
179
|
-
## 2. What cannot be said
|
|
180
|
-
|
|
181
|
-
Concrete, from the examples and the widget set:
|
|
182
|
-
|
|
183
|
-
- **Fade in a tooltip, a toast, a menu.** No `opacity`. The ecosystem page
|
|
184
|
-
tells people to animate a colour instead, and every entry on that page
|
|
185
|
-
exists partly because of this hole.
|
|
186
|
-
- **Hover lift.** `':hover': { scaleX: 1.02, scaleY: 1.02 }` — a paint-only
|
|
187
|
-
state change that today has no property to change. A state block cannot
|
|
188
|
-
touch layout, so growing the box is not an option either.
|
|
189
|
-
- **The Switch thumb.** `transition: { start: 120 }` is a layout pass per
|
|
190
|
-
frame on every backend and can never be offloaded, because `start` is
|
|
191
|
-
yoga's. As `translateX` it is a paint property: no layout on X11, a
|
|
192
|
-
`transform.translation.x` animation in the render server on layers.
|
|
193
|
-
- **A shake on a bad password.** Six stops in 300ms, once, and again on the
|
|
194
|
-
next attempt. Neither shape has a spelling for "once" or for "again".
|
|
195
|
-
- **An entrance.** Slide up and fade in on mount. Nothing may transition
|
|
196
|
-
before the first frame (correctly — the mount rule), and the loop shape
|
|
197
|
-
never ends.
|
|
198
|
-
- **A spinner.** `rotate` from 0 to 360, forever — the one loop that is
|
|
199
|
-
obviously a transform, and the one with no property to loop.
|
|
200
|
-
- **A staggered list.** Delay per row. No `delay`.
|
|
201
|
-
- **A transition with a curve.** `ease-in-out` on a panel slide; a spring on
|
|
202
|
-
a drag release so it carries the velocity. One fixed curve.
|
|
203
|
-
- **"It kept animating while the app was busy."** Any transition on macOS
|
|
204
|
-
stalls with the JS thread. docs/macos.md calls this the qualitative gap
|
|
205
|
-
Tier L exists for; it is not reachable until the presenter takes the
|
|
206
|
-
animation.
|
|
207
|
-
|
|
208
|
-
Out of scope here, so nobody expects them: **exit animations** (keeping a
|
|
209
|
-
node mounted after React removed it is a reconciler feature — a deferred
|
|
210
|
-
`removeChild` — and deserves its own design; `onAnimationEnd` below is the
|
|
211
|
-
seam it will need), and **window-level opacity** (a `<popup>` fade is
|
|
212
|
-
`_NET_WM_WINDOW_OPACITY` on X11 and `NSWindow.alphaValue` on Cocoa, a
|
|
213
|
-
different mechanism from a node's).
|
|
214
|
-
|
|
215
|
-
## 3. The vocabulary
|
|
216
|
-
|
|
217
|
-
### 3.1 Easing, shared
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
type Easing =
|
|
221
|
-
| 'linear'
|
|
222
|
-
| 'ease'
|
|
223
|
-
| 'ease-in'
|
|
224
|
-
| 'ease-out'
|
|
225
|
-
| 'ease-in-out'
|
|
226
|
-
| [x1: number, y1: number, x2: number, y2: number] // cubic-bezier
|
|
227
|
-
| {
|
|
228
|
-
spring: {
|
|
229
|
-
stiffness?: number;
|
|
230
|
-
damping?: number;
|
|
231
|
-
mass?: number;
|
|
232
|
-
velocity?: number;
|
|
233
|
-
};
|
|
234
|
-
};
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**The canonical form is the four control points.** Names are aliases
|
|
238
|
-
resolved once, in `styles.js`, to the beziers they already are:
|
|
239
|
-
|
|
240
|
-
| name | control points | today |
|
|
241
|
-
| ------------- | ---------------------- | --------------------------------------- |
|
|
242
|
-
| `linear` | `[0, 0, 1, 1]` | `t` |
|
|
243
|
-
| `ease-out` | `[0.33, 1, 0.68, 1]` | `1 - (1 - t)^3`, the transition default |
|
|
244
|
-
| `ease-in` | `[0.32, 0, 0.67, 0]` | `t^3` |
|
|
245
|
-
| `ease-in-out` | `[0.65, 0, 0.35, 1]` | the piecewise cubic |
|
|
246
|
-
| `ease` | `[0.25, 0.1, 0.25, 1]` | — (CSS's default, new) |
|
|
247
|
-
|
|
248
|
-
Why this and not the polynomials: **the two evaluators have to agree.** A
|
|
249
|
-
declaration that runs on the JS loop on X11 and in the render server on
|
|
250
|
-
layers must look the same, and CA's only timing primitive is a cubic
|
|
251
|
-
bezier. Sampled at a thousand points (scratch script, not committed), the
|
|
252
|
-
existing curves against their bezier twins differ by at most 0.003
|
|
253
|
-
(ease-out, ease-in) and 0.0095 (ease-in-out) — invisible. The existing
|
|
254
|
-
ease-out against CA's **named** `easeOut` (`[0, 0, 0.58, 1]`) differs by
|
|
255
|
-
**0.216** at t = 0.35 — a visibly different animation. So the bridge's four
|
|
256
|
-
named curves are not a mapping target; control points are, and the JS side
|
|
257
|
-
gets a bezier solver (Newton with a bisection fallback, the shape every
|
|
258
|
-
browser ships, ~40 lines, pure, pinned by a table test against CSS's
|
|
259
|
-
published samples).
|
|
260
|
-
|
|
261
|
-
**Springs** are the second kind, not a curve: no duration, a settling time
|
|
262
|
-
derived from the physics, and the reason they exist is interruption — a
|
|
263
|
-
retarget carries the current velocity, which is what makes a dragged thing
|
|
264
|
-
let go feel like it was let go. CA's names (`mass`, `stiffness`,
|
|
265
|
-
`damping`, `initialVelocity`) are physical units and are also popmotion's
|
|
266
|
-
and Framer's; use them. A spring is only ever between two values, so it is
|
|
267
|
-
an easing for `transition` and for a two-stop `animation`; a keyframe
|
|
268
|
-
timeline takes beziers per segment. The JS evaluator is the closed-form
|
|
269
|
-
damped oscillator, settling at a displacement threshold; CA's is
|
|
270
|
-
`CASpringAnimation` and reports `settlingDuration`, which is what the
|
|
271
|
-
completion timer uses. Springs are the last thing to land (§7); the
|
|
272
|
-
vocabulary is fixed now so nothing built before them has to move.
|
|
273
|
-
|
|
274
|
-
### 3.2 `transition`
|
|
275
|
-
|
|
276
|
-
```js
|
|
277
|
-
transition: 120 // as today: everything, ease-out
|
|
278
|
-
transition: { backgroundColor: 120, borderColor: 120 } // as today
|
|
279
|
-
transition: {
|
|
280
|
-
all: 120, // the shorthand's slot
|
|
281
|
-
start: { duration: 160, easing: 'ease-in-out', delay: 40 },
|
|
282
|
-
scaleX: { easing: { spring: { stiffness: 400, damping: 30 } } },
|
|
283
|
-
}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
A property's value is `ms | { duration?, easing?, delay? }`; `all` is the
|
|
287
|
-
default for every animatable property not named, and is not a style
|
|
288
|
-
property, so the object stays one grammar. `duration` may be omitted only
|
|
289
|
-
with a spring. The default easing stays `ease-out` — a change that ends
|
|
290
|
-
looks right slowing into its value — and the default delay is 0.
|
|
291
|
-
|
|
292
|
-
`transitionFor` returns `{ duration, easing, delay }` instead of a number.
|
|
293
|
-
(The `.d.ts` already declares that shape — `{ duration; delay? } | null`,
|
|
294
|
-
src/style.d.ts:93 — and the JavaScript returns a number: one of #120's
|
|
295
|
-
drifts, closed by this change rather than by editing the declaration.)
|
|
296
|
-
|
|
297
|
-
`delay` is a start time, nothing more: the transition still reads its
|
|
298
|
-
`from` from what is on screen _when it starts_, so a retarget during the
|
|
299
|
-
delay is a retarget of a transition that has not moved.
|
|
300
|
-
|
|
301
|
-
### 3.3 `animation`: a timeline, which may end
|
|
302
|
-
|
|
303
|
-
```js
|
|
304
|
-
animation: {
|
|
305
|
-
// today's loop, unchanged
|
|
306
|
-
backgroundColor: { to: '$accent', duration: 900, alternate: true },
|
|
307
|
-
// an entrance: from somewhere, to the declared value, once
|
|
308
|
-
opacity: { from: 0, duration: 200, repeat: 1 },
|
|
309
|
-
translateY: { from: 8, duration: 200, repeat: 1, easing: 'ease-out' },
|
|
310
|
-
// a shake, and again on the next attempt
|
|
311
|
-
translateX: { keyframes: [0, -6, 6, -6, 6, 0], duration: 300, repeat: 1, key: attempt },
|
|
312
|
-
// a spinner
|
|
313
|
-
rotate: { from: 0, to: 360, duration: 800 },
|
|
314
|
-
// a staggered pulse
|
|
315
|
-
scaleX: { to: 1.1, duration: 600, alternate: true, delay: index * 80 },
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
Per property, as now — CA needs one animation per key path too, and CSS's
|
|
320
|
-
one-`@keyframes`-many-properties shape would only be a way to write the
|
|
321
|
-
same duration several times. Options:
|
|
322
|
-
|
|
323
|
-
| key | |
|
|
324
|
-
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
325
|
-
| `from`, `to` | the two-stop form. **Exactly one may be omitted** and defaults to the declared value |
|
|
326
|
-
| `keyframes` | the many-stop form, instead of `from`/`to`: values evenly spaced, or `{ at: 0..1, value, easing? }` for placed stops and per-segment curves |
|
|
327
|
-
| `duration` | one pass, ms |
|
|
328
|
-
| `easing` | over the pass; `linear` default, as now |
|
|
329
|
-
| `delay` | before the first pass only |
|
|
330
|
-
| `repeat` | passes; **`Infinity` by default**, so a declaration written today means what it meant |
|
|
331
|
-
| `alternate` | as now |
|
|
332
|
-
| `key` | identity: a changed `key` restarts the timeline from the top, which is how a finite one is played again |
|
|
333
|
-
|
|
334
|
-
**Where it rests is the declared value — and that is the whole fill-mode
|
|
335
|
-
story.** Loops already have this rule: the property is at its declared
|
|
336
|
-
value before the first frame, off screen, under reduced motion. A finite
|
|
337
|
-
timeline ends and the property is at its declared value. So an entrance is
|
|
338
|
-
"declare the end state, arrive from somewhere else"; a shake ends where it
|
|
339
|
-
began because the author wrote `0` at both ends. There is no `fill:
|
|
340
|
-
'forwards'`, deliberately: a held last frame means the style lies about the
|
|
341
|
-
property from then on, and the author who wants that value can write it.
|
|
342
|
-
A finite timeline whose last stop is not the declared value snaps at the
|
|
343
|
-
end, visibly — which is the declaration being honest, not a bug.
|
|
344
|
-
|
|
345
|
-
**Start and restart.** A timeline starts on the style swap that introduces
|
|
346
|
-
it — a mount, or a later change — the same moment loops start today. An
|
|
347
|
-
equal declaration keeps its phase (`sameAnimation`, extended to
|
|
348
|
-
`keyframes`, `repeat`, `delay`, `key`); a changed one restarts. A finished
|
|
349
|
-
finite timeline stays finished across re-renders of the same declaration;
|
|
350
|
-
`key` is the one field whose only job is to differ. (`key` reads well in a
|
|
351
|
-
React codebase; `run` was the alternative.)
|
|
352
|
-
|
|
353
|
-
**Completion.** `onTransitionEnd({ property })` and `onAnimationEnd({
|
|
354
|
-
property })` as element props, per property, fired when the JS clock says
|
|
355
|
-
so — on every presenter, including one that offloaded the pixels, so a
|
|
356
|
-
component can chain or unmount without knowing where the animation ran.
|
|
357
|
-
Loops never fire it. An interrupted transition does not fire it either
|
|
358
|
-
(CSS agrees: `transitioncancel` is a different event, and nothing here
|
|
359
|
-
needs it yet).
|
|
360
|
-
|
|
361
|
-
**Reduced motion.** The existing split holds and is the simple rule:
|
|
362
|
-
**`animation` is motion for its own sake and never starts under reduced
|
|
363
|
-
motion, finite or not; `transition` is the answer to an input and always
|
|
364
|
-
runs.** A shake and an entrance are the kind of thing the switch exists
|
|
365
|
-
for, and the still frame is the declared value — the same design decision
|
|
366
|
-
`<ProgressBar indeterminate>` already makes for its loop.
|
|
367
|
-
|
|
368
|
-
### 3.4 `opacity` and the individual transforms
|
|
369
|
-
|
|
370
|
-
Six paint-only style properties, on both backends:
|
|
371
|
-
|
|
372
|
-
| property | value | notes |
|
|
373
|
-
| -------------------------- | ---------- | ---------------------------------------------------------- |
|
|
374
|
-
| `opacity` | 0..1 | **group** opacity of the subtree, CSS semantics |
|
|
375
|
-
| `translateX`, `translateY` | logical px | |
|
|
376
|
-
| `rotate` | degrees | the bridge takes radians; the presenter converts |
|
|
377
|
-
| `scaleX`, `scaleY` | factor | no `scale` — `<box scale>` is already the zoom (see below) |
|
|
378
|
-
|
|
379
|
-
Composed translate → rotate → scale about the box centre — CSS's order for
|
|
380
|
-
its individual transform properties and exactly what the bridge's
|
|
381
|
-
`transform` object does. A `transformOrigin` is the obvious later seam;
|
|
382
|
-
centre is CA's default anchor and the case every hover lift and spinner
|
|
383
|
-
wants.
|
|
384
|
-
|
|
385
|
-
Paint-only means: **state blocks may set them.** A `:hover` that scales a
|
|
386
|
-
card by 2% is a repaint of one node, no layout — the property that was
|
|
387
|
-
missing from the "hover is a repaint, not a render" story. It also means
|
|
388
|
-
layout is untouched: the box yoga computed is where the node _is_; the
|
|
389
|
-
transform is where it is _drawn_, and where the pointer finds it. Hit
|
|
390
|
-
testing maps the point through the inverse (the walk in `events.js` tests
|
|
391
|
-
node rects; a transformed node tests the inverse-mapped point against its
|
|
392
|
-
own rect and passes the mapped point to its children). `paintBounds()` is
|
|
393
|
-
the transformed box's bounding rect, which is what damage, culling and
|
|
394
|
-
`_subtreeBounds()` already consume. `opacity: 0` still hits, as in CSS;
|
|
395
|
-
`pointerEvents: 'none'` is the property for not hitting.
|
|
396
|
-
|
|
397
|
-
**Not the zoom.** `<box scale={2}>` multiplies every length in the subtree
|
|
398
|
-
and reshapes text at the size it will be drawn at (src/nodes/cascade.js,
|
|
399
|
-
docs/scale.md); it is layout, and it is expensive to change. `scaleX` is
|
|
400
|
-
paint: the rasterized subtree stretched. They are two different tools and
|
|
401
|
-
the transform is the one that animates cheaply; the docs say so in one
|
|
402
|
-
place so the names stop looking like a mistake.
|
|
403
|
-
|
|
404
|
-
Both animate (they are numbers), both may transition, both go in
|
|
405
|
-
timelines. That is the whole point of choosing individual properties over
|
|
406
|
-
a `transform: 'rotate(45deg) …'` string: the per-property machinery
|
|
407
|
-
interpolates them with no matrix decomposition, and each is one CA key
|
|
408
|
-
path (`opacity`, `transform.translation.x`, `transform.rotation.z`,
|
|
409
|
-
`transform.scale.x`).
|
|
410
|
-
|
|
411
|
-
### 3.5 Deliberately not proposed
|
|
412
|
-
|
|
413
|
-
- **CSS strings** (`'200ms ease-in-out'`, `'rotate(45deg)'`). The style
|
|
414
|
-
language here is numbers and objects; a second grammar inside strings is
|
|
415
|
-
a parser and a source of errors that arrive as a thing that does not move.
|
|
416
|
-
- **`fill-mode`.** §3.3.
|
|
417
|
-
- **A per-frame callback in the style.** `animation` describes the motion
|
|
418
|
-
and the renderer runs it — the offload in §4 depends on that being true.
|
|
419
|
-
The imperative seam for a 2D per-frame draw is still open
|
|
420
|
-
(docs/styling.md, last paragraph of "Loops"); a `<canvas>` with a value
|
|
421
|
-
from popmotion is the answer meanwhile, and this proposal makes that
|
|
422
|
-
cheaper rather than replacing it.
|
|
423
|
-
- **Exit animations.** §2.
|
|
424
|
-
|
|
425
|
-
## 4. Who interpolates
|
|
426
|
-
|
|
427
|
-
### 4.1 The rule
|
|
428
|
-
|
|
429
|
-
**The node model is the source of truth for what is animating and when it
|
|
430
|
-
ends. A presenter may take the pixels.** Concretely, `_retarget` and
|
|
431
|
-
`_updateLoops` keep doing everything they do — the `_anim` entry, the
|
|
432
|
-
start time, `sameAnimation`, the loop-stop rules — and then ask:
|
|
433
|
-
|
|
434
|
-
```js
|
|
435
|
-
presenter.animate?.(node, prop, entry); // → true: the presenter interpolates
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
A presenter that answers `true` owns the frames for that entry: the node
|
|
439
|
-
is **not** added to `root._animating` for it, no damage is claimed per
|
|
440
|
-
frame, and the window's frame clock stays idle. The model value goes out
|
|
441
|
-
as it does now — the resolved target, in the disabled-actions transaction
|
|
442
|
-
— and the presenter has attached the motion to the layer. Completion is a
|
|
443
|
-
timer at `delay + duration` (or the spring's settling time), which fires
|
|
444
|
-
`onTransitionEnd`/`onAnimationEnd` and, for a finite timeline, lets the
|
|
445
|
-
property rest. A presenter that answers `false`, has no `animate`, or is the
|
|
446
|
-
X11 painter, gets the JS loop exactly as today. **Every entry has the JS
|
|
447
|
-
loop as its fallback**, so correctness never depends on coverage — the
|
|
448
|
-
same rule the layer presenter already applies to painting (raster per node
|
|
449
|
-
when the property vocabulary runs out).
|
|
450
|
-
|
|
451
|
-
Retarget and stop go through the same seam: `presenter.cancel(node, prop)`
|
|
452
|
-
before a new `animate`, and from every loop-stop path.
|
|
453
|
-
|
|
454
|
-
### 4.2 What the layer presenter takes
|
|
455
|
-
|
|
456
|
-
A property qualifies when it maps to a property **of the node's own layer**
|
|
457
|
-
and changing it does not re-raster that layer:
|
|
458
|
-
|
|
459
|
-
| style property | key path | PropBox | Raster |
|
|
460
|
-
| --------------------------------------------- | -------------------------------- | ------- | -------------- |
|
|
461
|
-
| `backgroundColor`, `borderColor` | `backgroundColor`, `borderColor` | yes | no — re-raster |
|
|
462
|
-
| `borderWidth`, `borderRadius` | `borderWidth`, `cornerRadius` | yes | no |
|
|
463
|
-
| `opacity` | `opacity` | yes | **yes** |
|
|
464
|
-
| `translateX/Y`, `rotate`, `scaleX/Y` | `transform.*` | yes | **yes** |
|
|
465
|
-
| `boxShadow` components (once it interpolates) | `shadowOpacity/Radius/Offset` | later | — |
|
|
466
|
-
| `color` on `<text>` | — | — | no |
|
|
467
|
-
| any layout property | — | no | no |
|
|
468
|
-
|
|
469
|
-
The last row is the important one. **Layout properties stay on the JS loop
|
|
470
|
-
on every presenter**, because yoga has to run for the frame to be right —
|
|
471
|
-
a `width` that grows moves its siblings. The one tempting exception, an
|
|
472
|
-
absolutely positioned node's `left`/`top`/`start` as the layer's
|
|
473
|
-
`position`, is deferred: once `translateX` exists, the Switch thumb and
|
|
474
|
-
its kind have a paint property to slide on, which is cheaper on X11 too
|
|
475
|
-
and needs no exception. The `Raster` rows are what make transforms and
|
|
476
|
-
opacity worth more than they look: a paragraph, an image or a registered
|
|
477
|
-
element fades and moves in the render server with its bitmap untouched.
|
|
478
|
-
|
|
479
|
-
**Mechanics** (per property, inside the frame's transaction):
|
|
480
|
-
|
|
481
|
-
1. Model value out as today: `visual.set({ backgroundColor: to })` under
|
|
482
|
-
`disableActions`.
|
|
483
|
-
2. `addAnimation(layer, keyPath, { from, to, duration: ms / 1000, timing:
|
|
484
|
-
[x1, y1, x2, y2], delay }, key)`. CA renders `from` → `to` over the
|
|
485
|
-
model value and removes itself on completion, leaving the model showing
|
|
486
|
-
— the standard explicit-animation pattern, no `hold`.
|
|
487
|
-
3. Timer for completion.
|
|
488
|
-
|
|
489
|
-
**Loops** are `repeat: Infinity` with `autoreverse` for `alternate`, and
|
|
490
|
-
cost **zero JS frames** — the spinner, the pulse and the indeterminate bar
|
|
491
|
-
become a render-server timer. The loop-stop rules still run (they are the
|
|
492
|
-
one truth about whether a loop may run), and each one is a
|
|
493
|
-
`removeAnimation`; a resume is a fresh `addAnimation`, phase reset, as the
|
|
494
|
-
JS loop does now. CA stops compositing an occluded window on its own, so
|
|
495
|
-
the rule costs nothing there and keeps the two backends identical in what
|
|
496
|
-
they promise.
|
|
497
|
-
|
|
498
|
-
**Interruption** — the "reverse from where it got to" property, which
|
|
499
|
-
test/style.test.js pins for the JS loop and which has to hold here:
|
|
500
|
-
|
|
501
|
-
- Numeric key paths (`opacity`, `transform.*`, `borderWidth`,
|
|
502
|
-
`cornerRadius`): **additive** animations, CA's own idiom. The model goes
|
|
503
|
-
straight to the new target; the animation is `from: old − new, to: 0,
|
|
504
|
-
additive: true`. Several in flight sum, so a retarget mid-flight is
|
|
505
|
-
continuous and carries its shape — with no readback of anything. This
|
|
506
|
-
needs the bridge's `additive` flag (§4.4).
|
|
507
|
-
- Colours (CA cannot add colours): the JS side computes where the running
|
|
508
|
-
animation is from its own clock and the same bezier — within a frame of
|
|
509
|
-
CA's truth, since both evaluate the same curve — and starts the next one
|
|
510
|
-
there. `presentationValue(layer, keyPath)` (§4.4) makes it exact later;
|
|
511
|
-
it is not on the critical path.
|
|
512
|
-
|
|
513
|
-
**What the presenter needs from the node** is what `_syncPropBox` already
|
|
514
|
-
reads plus the animated entry; the table above is a static map from style
|
|
515
|
-
property to key path plus a check of `visual.isRaster`. It is a small
|
|
516
|
-
addition to src/cocoa/presenter.js, and the cocoa-mock presenter in the
|
|
517
|
-
tests records `addAnimation` calls the same way it records `setLayerProps`.
|
|
518
|
-
|
|
519
|
-
### 4.3 The surface presenter, and X11
|
|
520
|
-
|
|
521
|
-
X11 has nothing to offload: one bitmap per window, painted by the X11
|
|
522
|
-
walk, and the JS loop runs. The surface presenter started there too, and
|
|
523
|
-
now takes the same table as §4.2 by **layer promotion** (issue #483,
|
|
524
|
-
docs/macos.md §"Layer promotion"): a plain box with a transition or a loop
|
|
525
|
-
on an offloadable property gets a CALayer of its own above the window's
|
|
526
|
-
bitmap for as long as it animates, where nothing is painted over it, and
|
|
527
|
-
the same `LayerAnimations` books run it in the render server. The bitmap
|
|
528
|
-
keeps the frame for everything else. What this moved in the measure-first
|
|
529
|
-
gate's argument is recorded there, measured. `opacity` is in the table on
|
|
530
|
-
both presenters since #817, on a plain box: its layer is its group, since
|
|
531
|
-
Core Animation fades a layer and its sublayers as one, and promotion keeps
|
|
532
|
-
a box inside a faded one off a layer of its own, which would be outside
|
|
533
|
-
the group. The `Raster` column of §4.2 is not taken by either yet. An
|
|
534
|
-
element that draws its own scene hands over parts of it instead (#819,
|
|
535
|
-
`Node.sprites()`): each a layer under promotion's rules, its `opacity` and
|
|
536
|
-
`transform` animated as keyframes the element computed — a CSS transform
|
|
537
|
-
sampled into matrices, so that the element's interpolation, not Core
|
|
538
|
-
Animation's, is what the render server plays.
|
|
539
|
-
|
|
540
|
-
### 4.4 Bridge additions (`@windowkit/appkit`)
|
|
541
|
-
|
|
542
|
-
In the order the offload needs them:
|
|
543
|
-
|
|
544
|
-
1. **`timing: [x1, y1, x2, y2]`** on `addAnimation` and `txBegin` —
|
|
545
|
-
`CAMediaTimingFunction functionWithControlPoints:`. **Prerequisite**:
|
|
546
|
-
§3.1's numbers say the named curves would change every existing
|
|
547
|
-
transition's look.
|
|
548
|
-
2. **`additive: true`** — one line on the `CABasicAnimation`. What makes
|
|
549
|
-
retargeting continuous without readback.
|
|
550
|
-
3. **`delay`** — `beginTime = CACurrentMediaTime() + delay`, with
|
|
551
|
-
`fillMode: backwards` so the layer shows `from` during the delay.
|
|
552
|
-
4. **Keyframes** — `values`, `keyTimes`, `timingFunctions` (one bezier per
|
|
553
|
-
segment), `calculationMode: linear` (a `CAKeyframeAnimation`).
|
|
554
|
-
5. **Springs** — `CASpringAnimation` with `mass`/`stiffness`/`damping`/
|
|
555
|
-
`initialVelocity`, returning `settlingDuration` so JS can time completion.
|
|
556
|
-
6. **`presentationValue(layer, keyPath)`** — `presentationLayer` readback;
|
|
557
|
-
exactness for colour retargets.
|
|
558
|
-
7. **Completion callback** — the animation delegate's `animationDidStop:
|
|
559
|
-
finished:` forwarded through the event callback; makes
|
|
560
|
-
`onTransitionEnd` exact rather than timer-derived. Optional.
|
|
561
|
-
8. **Reduce motion** — `NSWorkspace.accessibilityDisplayShouldReduceMotion`
|
|
562
|
-
plus its notification, into `desktopSettings().animations` on the Cocoa
|
|
563
|
-
app. Independent of this design and already wrong without it.
|
|
564
|
-
|
|
565
|
-
Each is a verb, not policy, which is the shape the bridge's contract asks
|
|
566
|
-
for (docs/macos.md, "The channel is already node-granular").
|
|
567
|
-
|
|
568
|
-
Filed as [windowkit/appkit#29](https://github.com/windowkit/appkit/issues/29);
|
|
569
|
-
[windowkit/appkit#30](https://github.com/windowkit/appkit/pull/30) implements
|
|
570
|
-
items 1–7 plus `speed`/`timeOffset` (merged); item 8 is
|
|
571
|
-
[windowkit/appkit#31](https://github.com/windowkit/appkit/issues/31), implemented
|
|
572
|
-
by [windowkit/appkit#32](https://github.com/windowkit/appkit/pull/32). Two
|
|
573
|
-
things the bridge work found that the presenter has to know: CA interpolates
|
|
574
|
-
colours in the display's space, so a colour read back mid-flight is not the
|
|
575
|
-
component midpoint; and a paused animation sampled at exactly its duration
|
|
576
|
-
reads as its start, so an end is sampled a millisecond early.
|
|
577
|
-
|
|
578
|
-
## 5. What `opacity` and transforms cost on X11
|
|
579
|
-
|
|
580
|
-
Both are **group** operations on a subtree, and a group needs an offscreen:
|
|
581
|
-
per-op alpha on overlapping children double-blends where they overlap, and
|
|
582
|
-
per-op transforms would rotate each child about its own centre. So:
|
|
583
|
-
|
|
584
|
-
1. Paint the subtree into an ntk `Surface` the size of `paintBounds()` in
|
|
585
|
-
device pixels — the same surface family the paint cache and #433's
|
|
586
|
-
`CocoaSurface` use, behind `app.createSurface` on the Cocoa surface
|
|
587
|
-
backend and ntk's pixmap on X11.
|
|
588
|
-
2. Composite it back with `ctx.globalAlpha = opacity` and the transform set
|
|
589
|
-
on the context. ntk's `drawImage` already routes a transformed draw
|
|
590
|
-
through `SetPictureTransform` with a filter and scales the composite by
|
|
591
|
-
`globalAlpha` (node_modules/ntk/lib/renderingcontext_2d.js:2046,
|
|
592
|
-
:4599) — server-side, no readback. Rotation's filter quality and the
|
|
593
|
-
transformed bounding box are the two things to verify with a pixel test
|
|
594
|
-
rather than assume.
|
|
595
|
-
|
|
596
|
-
Per animated frame that is one pixmap the size of the subtree plus one
|
|
597
|
-
composite — bounded to the thing that moves, the way every claim here is.
|
|
598
|
-
Two fast paths cover most of what apps write and skip the offscreen
|
|
599
|
-
entirely:
|
|
600
|
-
|
|
601
|
-
- **A pure translate**: `ctx.translate` and paint the subtree through it.
|
|
602
|
-
Every child paints through the same context, clips included. That is
|
|
603
|
-
the shake, the slide, the entrance, the Switch thumb.
|
|
604
|
-
- **Opacity on a leaf** (a `<box>` with no children, or a `<text>`):
|
|
605
|
-
multiply the alpha into the fill, border and ink. One node, no group.
|
|
606
|
-
|
|
607
|
-
`opacity: 1` and identity transforms cost nothing, which keeps the
|
|
608
|
-
properties safe to leave in a style. The offscreen surface is per node,
|
|
609
|
-
kept between frames of one animation and dropped when it ends, and it is
|
|
610
|
-
the paint cache's budget that bounds it (src/paintcache.js's byte budget
|
|
611
|
-
is the pattern; the surface here is not content-keyed, it is a frame
|
|
612
|
-
scratch).
|
|
613
|
-
|
|
614
|
-
Damage: `_advanceAnimations` already claims the node before the tick, which
|
|
615
|
-
is the old bounding rect. A transform moves nothing in layout, so no pass
|
|
616
|
-
claims the new one for it: the tick claims that too, the pair a layout
|
|
617
|
-
transition gets from the layout pass's diff (`_assignAbs`).
|
|
618
|
-
|
|
619
|
-
## 6. Where the code goes
|
|
620
|
-
|
|
621
|
-
- **src/styles.js** — `resolveEasing` (names → control points → a bezier
|
|
622
|
-
function; the spring solver), `transitionFor` → `{ duration, easing,
|
|
623
|
-
delay }`, `parseAnimation` (`keyframes`, `repeat`, `delay`, `key`; the
|
|
624
|
-
validation messages name the option and the property as now),
|
|
625
|
-
`animationValueAt` over a keyframe list (segment lookup, per-segment
|
|
626
|
-
easing, `repeat` as a cap on the cycle count), `sameAnimation` over the
|
|
627
|
-
new fields, and the six new properties in `PAINT_PROPS` — which puts
|
|
628
|
-
them in state blocks and out of `NOT_ANIMATABLE` for free.
|
|
629
|
-
- **src/nodes/** — `_retarget` (easing and delay on the entry; the
|
|
630
|
-
presenter seam), `_tickAnimations` (finite end → rest at the target,
|
|
631
|
-
completion events), `_updateLoops` (cancel through the presenter),
|
|
632
|
-
`paint` (the group path and its two fast paths around
|
|
633
|
-
`_paintBackground`/`paintContent`/`_paintChildren`), `paintBounds()`
|
|
634
|
-
and `_subtreeBounds()` (transformed boxes), `_advanceAnimations` (the
|
|
635
|
-
claim for where a transform moved the box).
|
|
636
|
-
- **src/events.js** — the inverse-mapped hit test.
|
|
637
|
-
- **src/cocoa/presenter.js** — `animate`/`cancel`, the key-path table,
|
|
638
|
-
`opacity` and `transform` on both visual kinds; the mock presenter
|
|
639
|
-
records them.
|
|
640
|
-
- **src/types/style.d.ts, src/style.d.ts, test/types/api.tsx** — `Easing`,
|
|
641
|
-
`Transition`, `AnimationSpec`, the six properties, the two events.
|
|
642
|
-
- **docs/styling.md** ("Transitions", "Loops" → "Timelines"),
|
|
643
|
-
**docs/macos.md** (the "Animations and transforms" section becomes a
|
|
644
|
-
pointer here plus the measured result), **docs/ecosystem/animation.md**
|
|
645
|
-
(delete the "no opacity" constraint; the page shrinks), **docs/system.md**
|
|
646
|
-
(reduced motion covers finite timelines), **docs/elements.md** (`scale`
|
|
647
|
-
vs `scaleX`, the events).
|
|
648
|
-
- **Widgets and examples** — Switch on `translateX`; Tooltip fades;
|
|
649
|
-
ProgressBar unchanged; a shake in the configurator's password field;
|
|
650
|
-
the website demos that use `transition` still pass `check-demos`.
|
|
651
|
-
- **Tests** — bezier table vs CSS samples and vs the polynomials
|
|
652
|
-
(test/style.test.js); finite timelines, keyframes, `key` restart, rest at
|
|
653
|
-
the declared value, `delay` (test/animation-loop.test.js); the mock
|
|
654
|
-
presenter: a `backgroundColor` transition on a PropBox schedules zero
|
|
655
|
-
frames and one `addAnimation` with control points, `color` on a `<text>`
|
|
656
|
-
falls back to the loop, a loop stop removes the animation; pixels: group
|
|
657
|
-
opacity over overlapping children against a reference composite, a
|
|
658
|
-
rotate against a pre-rotated reference, the translate fast path
|
|
659
|
-
byte-identical to the offscreen path (the differential oracle
|
|
660
|
-
test/dirty-rect.test.js already has, applied here); the hit test through
|
|
661
|
-
a transform. And a **structural** row in `scripts/bench/presenters-gate.json`:
|
|
662
|
-
JS frames scheduled per 120ms hover transition on layers — 0 with the
|
|
663
|
-
offload, ~8 without — the same kind of number the gate already judges.
|
|
664
|
-
|
|
665
|
-
## 7. Sequencing
|
|
666
|
-
|
|
667
|
-
Each step ships on both backends and is useful alone; the offload is
|
|
668
|
-
additive on top and can proceed in parallel from step 1.
|
|
669
|
-
|
|
670
|
-
1. **Easing vocabulary and the `transition` object form** (`all`, per-property
|
|
671
|
-
`{ duration, easing, delay }`). Small; the bezier solver and its table
|
|
672
|
-
test are most of it.
|
|
673
|
-
2. **Timelines**: `keyframes`, `repeat`, `delay`, `key`, the completion
|
|
674
|
-
events, reduced motion over finite timelines.
|
|
675
|
-
3. **`opacity`**: the group path with the leaf fast path on X11 and the
|
|
676
|
-
surface presenter; `opacity` on both visuals on layers.
|
|
677
|
-
4. **Transforms**: translate first (fast path only, no offscreen — most of
|
|
678
|
-
the value for a fraction of the work), then rotate and scale over the
|
|
679
|
-
offscreen with the inverse hit test. Switch moves to `translateX`.
|
|
680
|
-
5. **Layer offload**: bridge items 1–3 (`windowkit/appkit` PRs), then the
|
|
681
|
-
presenter seam and its mock tests, the gate row, and a measured
|
|
682
|
-
before/after on a real display the way docs/macos.md "Measured" records
|
|
683
|
-
them. Items 4–7 follow as the timeline and spring features reach the
|
|
684
|
-
presenter.
|
|
685
|
-
6. **Springs**: the JS solver, bridge item 5, `easing: { spring }` on
|
|
686
|
-
transitions.
|
|
687
|
-
|
|
688
|
-
## 8. Open questions
|
|
689
|
-
|
|
690
|
-
- **`key` vs `run`** for the restart token (§3.3). Cheap to decide, hard to
|
|
691
|
-
rename later.
|
|
692
|
-
- **Should a finite timeline snap or ease into the declared value** when its
|
|
693
|
-
last stop differs? This document says snap, because it is honest and CSS
|
|
694
|
-
does the same; the alternative — an implicit last stop at the declared
|
|
695
|
-
value — hides a mismatch behind a nicer default.
|
|
696
|
-
- **Additive colours.** CA cannot; the clock-estimated `from` is within a
|
|
697
|
-
frame, and the readback verb closes it. Decide whether a frame's error
|
|
698
|
-
on a 120ms colour retarget is ever visible before building the verb.
|
|
699
|
-
- **Transforms and text selection, carets, scroll**: a transformed
|
|
700
|
-
`<textinput>` maps its pointer through the inverse like everything else,
|
|
701
|
-
but its caret and selection geometry are computed in layout space. The
|
|
702
|
-
first cut may reject transforms on nodes that own an editing surface, or
|
|
703
|
-
accept a documented gap; a real display will say which.
|
|
704
|
-
- **Whether `opacity` on X11 should take the offscreen at all** for a
|
|
705
|
-
subtree above a size, or fall back to per-node alpha with the
|
|
706
|
-
double-blend documented. The paint cache's item cap (`MAX_ITEM_PIXELS`)
|
|
707
|
-
is the precedent for "past this size, do the simple thing".
|