@cardstack/choreo 0.0.0 → 0.1.0-unstable.1
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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +55 -6
- package/addon-main.cjs +4 -0
- package/declarations/anchors.d.ts +26 -0
- package/declarations/anchors.d.ts.map +1 -0
- package/declarations/arming.d.ts +33 -0
- package/declarations/arming.d.ts.map +1 -0
- package/declarations/beacon.d.ts +9 -0
- package/declarations/beacon.d.ts.map +1 -0
- package/declarations/beacons.d.ts +23 -0
- package/declarations/beacons.d.ts.map +1 -0
- package/declarations/changeset.d.ts +46 -0
- package/declarations/changeset.d.ts.map +1 -0
- package/declarations/choreo.d.ts +261 -0
- package/declarations/choreo.d.ts.map +1 -0
- package/declarations/compile.d.ts +45 -0
- package/declarations/compile.d.ts.map +1 -0
- package/declarations/deliver.d.ts +44 -0
- package/declarations/deliver.d.ts.map +1 -0
- package/declarations/easings.d.ts +20 -0
- package/declarations/easings.d.ts.map +1 -0
- package/declarations/far.d.ts +33 -0
- package/declarations/far.d.ts.map +1 -0
- package/declarations/film/clip.d.ts +51 -0
- package/declarations/film/clip.d.ts.map +1 -0
- package/declarations/film/clips.d.ts +155 -0
- package/declarations/film/clips.d.ts.map +1 -0
- package/declarations/film/film.d.ts +1208 -0
- package/declarations/film/film.d.ts.map +1 -0
- package/declarations/film/graph/adjust.d.ts +112 -0
- package/declarations/film/graph/adjust.d.ts.map +1 -0
- package/declarations/film/graph/compile.d.ts +140 -0
- package/declarations/film/graph/compile.d.ts.map +1 -0
- package/declarations/film/graph/host.d.ts +67 -0
- package/declarations/film/graph/host.d.ts.map +1 -0
- package/declarations/film/graph/nodes.d.ts +287 -0
- package/declarations/film/graph/nodes.d.ts.map +1 -0
- package/declarations/film/index.d.ts +24 -0
- package/declarations/film/index.d.ts.map +1 -0
- package/declarations/film/joins.d.ts +143 -0
- package/declarations/film/joins.d.ts.map +1 -0
- package/declarations/film/math.d.ts +15 -0
- package/declarations/film/math.d.ts.map +1 -0
- package/declarations/film/overlays.d.ts +54 -0
- package/declarations/film/overlays.d.ts.map +1 -0
- package/declarations/film/picture.d.ts +104 -0
- package/declarations/film/picture.d.ts.map +1 -0
- package/declarations/film/plate.d.ts +37 -0
- package/declarations/film/plate.d.ts.map +1 -0
- package/declarations/film/player.d.ts +120 -0
- package/declarations/film/player.d.ts.map +1 -0
- package/declarations/film/rail.d.ts +40 -0
- package/declarations/film/rail.d.ts.map +1 -0
- package/declarations/film/schedule.d.ts +93 -0
- package/declarations/film/schedule.d.ts.map +1 -0
- package/declarations/film/seam.d.ts +38 -0
- package/declarations/film/seam.d.ts.map +1 -0
- package/declarations/film/titles.d.ts +37 -0
- package/declarations/film/titles.d.ts.map +1 -0
- package/declarations/film/types.d.ts +544 -0
- package/declarations/film/types.d.ts.map +1 -0
- package/declarations/film.d.ts +3 -0
- package/declarations/film.d.ts.map +1 -0
- package/declarations/gesture.d.ts +33 -0
- package/declarations/gesture.d.ts.map +1 -0
- package/declarations/index.d.ts +26 -0
- package/declarations/index.d.ts.map +1 -0
- package/declarations/measure.d.ts +29 -0
- package/declarations/measure.d.ts.map +1 -0
- package/declarations/path.d.ts +73 -0
- package/declarations/path.d.ts.map +1 -0
- package/declarations/registry.d.ts +46 -0
- package/declarations/registry.d.ts.map +1 -0
- package/declarations/run.d.ts +406 -0
- package/declarations/run.d.ts.map +1 -0
- package/declarations/space.d.ts +31 -0
- package/declarations/space.d.ts.map +1 -0
- package/declarations/steps.d.ts +479 -0
- package/declarations/steps.d.ts.map +1 -0
- package/declarations/test-support/index.d.ts +30 -0
- package/declarations/test-support/index.d.ts.map +1 -0
- package/declarations/types.d.ts +761 -0
- package/declarations/types.d.ts.map +1 -0
- package/dist/anchors.js +34 -0
- package/dist/anchors.js.map +1 -0
- package/dist/arming.js +120 -0
- package/dist/arming.js.map +1 -0
- package/dist/beacon.js +25 -0
- package/dist/beacon.js.map +1 -0
- package/dist/beacons.js +77 -0
- package/dist/beacons.js.map +1 -0
- package/dist/changeset.js +129 -0
- package/dist/changeset.js.map +1 -0
- package/dist/choreo.js +1026 -0
- package/dist/choreo.js.map +1 -0
- package/dist/compile.js +1403 -0
- package/dist/compile.js.map +1 -0
- package/dist/deliver.js +326 -0
- package/dist/deliver.js.map +1 -0
- package/dist/easings.js +39 -0
- package/dist/easings.js.map +1 -0
- package/dist/far.js +147 -0
- package/dist/far.js.map +1 -0
- package/dist/film/clip.js +100 -0
- package/dist/film/clip.js.map +1 -0
- package/dist/film/clips.js +147 -0
- package/dist/film/clips.js.map +1 -0
- package/dist/film/film.js +4424 -0
- package/dist/film/film.js.map +1 -0
- package/dist/film/graph/adjust.js +160 -0
- package/dist/film/graph/adjust.js.map +1 -0
- package/dist/film/graph/compile.js +225 -0
- package/dist/film/graph/compile.js.map +1 -0
- package/dist/film/graph/host.js +77 -0
- package/dist/film/graph/host.js.map +1 -0
- package/dist/film/graph/nodes.js +500 -0
- package/dist/film/graph/nodes.js.map +1 -0
- package/dist/film/index.js +18 -0
- package/dist/film/index.js.map +1 -0
- package/dist/film/joins.js +260 -0
- package/dist/film/joins.js.map +1 -0
- package/dist/film/math.js +49 -0
- package/dist/film/math.js.map +1 -0
- package/dist/film/overlays.js +66 -0
- package/dist/film/overlays.js.map +1 -0
- package/dist/film/picture.js +58 -0
- package/dist/film/picture.js.map +1 -0
- package/dist/film/plate.js +36 -0
- package/dist/film/plate.js.map +1 -0
- package/dist/film/player.js +79 -0
- package/dist/film/player.js.map +1 -0
- package/dist/film/rail.js +38 -0
- package/dist/film/rail.js.map +1 -0
- package/dist/film/schedule.js +238 -0
- package/dist/film/schedule.js.map +1 -0
- package/dist/film/seam.js +64 -0
- package/dist/film/seam.js.map +1 -0
- package/dist/film/titles.js +45 -0
- package/dist/film/titles.js.map +1 -0
- package/dist/film/types.js +2 -0
- package/dist/film/types.js.map +1 -0
- package/dist/film.js +18 -0
- package/dist/film.js.map +1 -0
- package/dist/gesture.js +111 -0
- package/dist/gesture.js.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/measure.js +99 -0
- package/dist/measure.js.map +1 -0
- package/dist/path.js +272 -0
- package/dist/path.js.map +1 -0
- package/dist/registry.js +55 -0
- package/dist/registry.js.map +1 -0
- package/dist/run.js +2423 -0
- package/dist/run.js.map +1 -0
- package/dist/space.js +57 -0
- package/dist/space.js.map +1 -0
- package/dist/steps.js +831 -0
- package/dist/steps.js.map +1 -0
- package/dist/test-support/index.js +150 -0
- package/dist/test-support/index.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +201 -6
package/dist/choreo.js
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import { registerDestructor } from '@ember/destroyable';
|
|
2
|
+
import Component from '@glimmer/component';
|
|
3
|
+
import { tracked } from '@glimmer/tracking';
|
|
4
|
+
import { consumeTag, VOLATILE_TAG } from '@glimmer/validator';
|
|
5
|
+
import { modifier } from 'ember-modifier';
|
|
6
|
+
import { registerBusyProbe, snapshotOnRender, motionSpeed, postRender, flushPendingMounts } from 'glimmer-motion';
|
|
7
|
+
import { measureBeacons } from './beacons.js';
|
|
8
|
+
import { Changeset } from './changeset.js';
|
|
9
|
+
import compile, { continuation, sameScore } from './compile.js';
|
|
10
|
+
import { leave, join, setPassScheduler, setBeforeMeasure } from './far.js';
|
|
11
|
+
import { trackGestures, GESTURE } from './gesture.js';
|
|
12
|
+
import { measure, boundsOf } from './measure.js';
|
|
13
|
+
import { setChoreoHost } from './registry.js';
|
|
14
|
+
import { execute } from './run.js';
|
|
15
|
+
import { collect, Wait, Tween, Tether, Spring, SlowZoom, Sequence, Scroll, Raise, Perform, Parallel, Pan, Move, Hold, Gate, Frame, Follow, Crossing, Camera3D, Camera, Attach, Aim } from './steps.js';
|
|
16
|
+
import { precompileTemplate } from '@ember/template-compilation';
|
|
17
|
+
import { setComponentTemplate } from '@ember/component';
|
|
18
|
+
import { g, i } from 'decorator-transforms/runtime';
|
|
19
|
+
|
|
20
|
+
const selector = type => Object.assign(role => role === undefined ? {
|
|
21
|
+
type
|
|
22
|
+
} : {
|
|
23
|
+
role,
|
|
24
|
+
type
|
|
25
|
+
}, {
|
|
26
|
+
type
|
|
27
|
+
});
|
|
28
|
+
/** what the region yields: the step components and the sprite queries */
|
|
29
|
+
|
|
30
|
+
function contextFor(region) {
|
|
31
|
+
return {
|
|
32
|
+
Aim,
|
|
33
|
+
Attach,
|
|
34
|
+
Camera,
|
|
35
|
+
Camera3D,
|
|
36
|
+
Crossing,
|
|
37
|
+
Follow,
|
|
38
|
+
Frame,
|
|
39
|
+
Gate,
|
|
40
|
+
Hold,
|
|
41
|
+
Move,
|
|
42
|
+
Pan,
|
|
43
|
+
Parallel,
|
|
44
|
+
Perform,
|
|
45
|
+
Raise,
|
|
46
|
+
Scroll,
|
|
47
|
+
Sequence,
|
|
48
|
+
SlowZoom,
|
|
49
|
+
Spring,
|
|
50
|
+
Tether,
|
|
51
|
+
Tween,
|
|
52
|
+
Wait,
|
|
53
|
+
advance: () => region.run?.advance(),
|
|
54
|
+
all: selector(),
|
|
55
|
+
beacon: beacon => ({
|
|
56
|
+
beacon
|
|
57
|
+
}),
|
|
58
|
+
gesture: GESTURE,
|
|
59
|
+
counterpart: selector('counterpart'),
|
|
60
|
+
id: id => ({
|
|
61
|
+
id
|
|
62
|
+
}),
|
|
63
|
+
inserted: selector('inserted'),
|
|
64
|
+
kept: selector('kept'),
|
|
65
|
+
moved: selector('moved'),
|
|
66
|
+
onstage: query => ({
|
|
67
|
+
...query,
|
|
68
|
+
onstage: true
|
|
69
|
+
}),
|
|
70
|
+
received: selector('received'),
|
|
71
|
+
removed: selector('removed'),
|
|
72
|
+
role: role => ({
|
|
73
|
+
role
|
|
74
|
+
}),
|
|
75
|
+
get camera() {
|
|
76
|
+
return region.cameraState;
|
|
77
|
+
},
|
|
78
|
+
get run() {
|
|
79
|
+
return region.run ?? null;
|
|
80
|
+
},
|
|
81
|
+
still: selector('still')
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let debugStyle;
|
|
85
|
+
/**
|
|
86
|
+
* Two rules every region shares. An orphaned skin flies with its
|
|
87
|
+
* backdrop-filters OFF: a backdrop-filter can never be cached — it
|
|
88
|
+
* re-samples and re-blurs whatever is behind it on every frame — and
|
|
89
|
+
* inside a scaling, fading leaver that is paid at full price for the
|
|
90
|
+
* whole flight (the camera demo's four glass panels, over its gradient
|
|
91
|
+
* washes, was the return trip's jank). It is also the wrong picture: a
|
|
92
|
+
* skin is a memory of the OLD scene, and its glass sampling the new
|
|
93
|
+
* scene behind it shows the wrong world. The panels keep their own
|
|
94
|
+
* translucent grounds, so they still read as glass in the crossfade.
|
|
95
|
+
*
|
|
96
|
+
* And an orphaned skin does not REPLAY ITS BIRTH: re-inserting a node
|
|
97
|
+
* restarts every CSS animation on it, so a leaver re-parented into the
|
|
98
|
+
* orphan layer re-ran its entrance choreography — rules drawing
|
|
99
|
+
* themselves, letters settling out of a blur — while it was supposed to
|
|
100
|
+
* be dying. A one-frame-old memory acting out its own arrival is the
|
|
101
|
+
* flash the Towers film chased for a night. The run's own exit motion
|
|
102
|
+
* is WAAPI and survives re-parenting untouched; only stylesheet
|
|
103
|
+
* animations restart, and on a memory they are all lies.
|
|
104
|
+
*/
|
|
105
|
+
let orphanStyle;
|
|
106
|
+
function ensureOrphanStyle() {
|
|
107
|
+
if (orphanStyle) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
orphanStyle = document.createElement('style');
|
|
111
|
+
orphanStyle.setAttribute('data-choreo-style', '');
|
|
112
|
+
orphanStyle.textContent = '[data-choreo-orphans] *,' + '[data-choreo-orphans] *::before,' + '[data-choreo-orphans] *::after' + '{backdrop-filter:none!important;' + '-webkit-backdrop-filter:none!important;' + 'animation:none!important;}';
|
|
113
|
+
document.head.appendChild(orphanStyle);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A fingerprint of the collected timeline tree, for telling an EDIT from
|
|
117
|
+
* noise while a run's own orphans are aloft. Mid-flight, compiled cues are
|
|
118
|
+
* value-laden (every re-measure shifts them), so sameScore cannot answer
|
|
119
|
+
* "did the author change the timeline?" — but the tree can: it is template
|
|
120
|
+
* state, identical under noise, different under any real edit. Functions
|
|
121
|
+
* (property sources) print by identity: a fresh function is a change we
|
|
122
|
+
* cannot disprove, and recompiling is the safe answer.
|
|
123
|
+
*/
|
|
124
|
+
const fnIds = new WeakMap();
|
|
125
|
+
let nextFnId = 0;
|
|
126
|
+
function treePrint(tree) {
|
|
127
|
+
return JSON.stringify(tree, (_key, value) => {
|
|
128
|
+
if (typeof value === 'function') {
|
|
129
|
+
let id = fnIds.get(value);
|
|
130
|
+
if (id === undefined) {
|
|
131
|
+
fnIds.set(value, id = ++nextFnId);
|
|
132
|
+
}
|
|
133
|
+
return `fn#${id}`;
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The color the page actually shows behind this region — the nearest
|
|
140
|
+
* ancestor (the region itself included) wearing a background with any
|
|
141
|
+
* alpha at all. What a crossing blends a semi-transparent skin against
|
|
142
|
+
* when it turns opacity into an actual color (§4.7). Undefined when the
|
|
143
|
+
* whole chain is transparent; the crossfade then stays a plain dissolve.
|
|
144
|
+
*/
|
|
145
|
+
function groundOf(root) {
|
|
146
|
+
let el = root;
|
|
147
|
+
while (el) {
|
|
148
|
+
const bg = getComputedStyle(el).backgroundColor;
|
|
149
|
+
if (bg && bg !== 'transparent' && !/rgba\([^)]*,\s*0\)$/.test(bg)) {
|
|
150
|
+
return bg;
|
|
151
|
+
}
|
|
152
|
+
el = el.parentElement;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The barrier books one post-render callback for every region taking part in a
|
|
158
|
+
* pass. `joinPass(this)` is where the region's PassHost contract (measurePass /
|
|
159
|
+
* finishPass) is type-checked — it is not on the class, because a `.gts` class
|
|
160
|
+
* header cannot carry two `implements` clauses through the build.
|
|
161
|
+
*/
|
|
162
|
+
setPassScheduler(run => postRender(run));
|
|
163
|
+
setBeforeMeasure(() => flushPendingMounts());
|
|
164
|
+
class Choreo extends Component {
|
|
165
|
+
element;
|
|
166
|
+
orphanLayer;
|
|
167
|
+
raisedLayer;
|
|
168
|
+
tetherLayer;
|
|
169
|
+
/** tracked mirror of the frame's resting state — see ChoreoContext.camera */
|
|
170
|
+
static {
|
|
171
|
+
g(this.prototype, "cameraState", [tracked], function () {
|
|
172
|
+
return {
|
|
173
|
+
x: 0,
|
|
174
|
+
y: 0,
|
|
175
|
+
zoom: 1
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
#cameraState = (i(this, "cameraState"), void 0);
|
|
180
|
+
/**
|
|
181
|
+
* The same state, untracked, for the pass pipeline itself. A pass runs
|
|
182
|
+
* inside computations that must not CONSUME the tracked mirror — a pass
|
|
183
|
+
* that read it would be invalidated by the very landing it causes, and
|
|
184
|
+
* the region would render forever.
|
|
185
|
+
*/ /** where a c.Camera3D left the shot, carried between runs — seeded by
|
|
186
|
+
* `@camera3dFrom` when the host knows its own opening pose */
|
|
187
|
+
resting3d = this.args.camera3dFrom ? {
|
|
188
|
+
...this.args.camera3dFrom
|
|
189
|
+
} : {
|
|
190
|
+
dolly: 1,
|
|
191
|
+
pitch: 0,
|
|
192
|
+
x: 0,
|
|
193
|
+
y: 0,
|
|
194
|
+
yaw: 0
|
|
195
|
+
};
|
|
196
|
+
restingCamera = {
|
|
197
|
+
x: 0,
|
|
198
|
+
y: 0,
|
|
199
|
+
zoom: 1
|
|
200
|
+
};
|
|
201
|
+
participants = new Set();
|
|
202
|
+
/** registered since the last pass */
|
|
203
|
+
arrived = new Set();
|
|
204
|
+
/** destroyed before the pass that removed them has been processed */
|
|
205
|
+
claimed = new Set();
|
|
206
|
+
/** removed participants this region is keeping alive in its orphan layer */
|
|
207
|
+
orphans = new Set();
|
|
208
|
+
/** participants whose Presence is letting them go: removed once, not on every pass while they leave */
|
|
209
|
+
leaving = new Set();
|
|
210
|
+
snapshots = new Map();
|
|
211
|
+
rootSnapshot;
|
|
212
|
+
passPending = false;
|
|
213
|
+
rendered = false;
|
|
214
|
+
run;
|
|
215
|
+
context = contextFor(this);
|
|
216
|
+
/** the last changeset this region built — read it from a property function or a test */
|
|
217
|
+
changeset;
|
|
218
|
+
constructor(owner, args) {
|
|
219
|
+
super(owner, args);
|
|
220
|
+
trackGestures();
|
|
221
|
+
// what `animationsSettled()` waits for: a pass announced but not yet run,
|
|
222
|
+
// or a timeline still playing. Orphans are deliberately not counted — a
|
|
223
|
+
// leaver stranded in the layer is a bug, and a probe that reported it
|
|
224
|
+
// would turn every one of those into a timeout instead of a failed
|
|
225
|
+
// assertion with a name on it.
|
|
226
|
+
const stopProbe = registerBusyProbe(() => {
|
|
227
|
+
if (this.passPending) {
|
|
228
|
+
return `<Choreo${this.args.id ? ` ${this.args.id}` : ''}> pass pending`;
|
|
229
|
+
}
|
|
230
|
+
// parked is a still and STANDING is an annotation that is simply on
|
|
231
|
+
// (a score of nothing but open steps never ends): both are settled,
|
|
232
|
+
// and a probe that called either one "in flight" would turn every
|
|
233
|
+
// wire the page leaves up into a suite-wide timeout
|
|
234
|
+
if (this.run && !this.run.isDone() && !this.run.parked && !this.run.standing) {
|
|
235
|
+
return `<Choreo${this.args.id ? ` ${this.args.id}` : ''}> run in flight`;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
});
|
|
239
|
+
registerDestructor(this, () => {
|
|
240
|
+
stopProbe();
|
|
241
|
+
leave(this);
|
|
242
|
+
this.run?.cancel();
|
|
243
|
+
this.run = undefined;
|
|
244
|
+
// a quiet region must not take its pause to the grave
|
|
245
|
+
this.unquiet();
|
|
246
|
+
for (const node of [...this.orphans, ...this.claimed]) {
|
|
247
|
+
this.drop(node);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/* ---- ChoreoHost ---- */ /** SPIKE (Lane): providers from outside the markup, appended to every collected tree */
|
|
252
|
+
contributors = new Set();
|
|
253
|
+
contribute(provider) {
|
|
254
|
+
this.contributors.add(provider);
|
|
255
|
+
return () => {
|
|
256
|
+
this.contributors.delete(provider);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/** the run an attachment drives: the one playing or holding, none between runs */
|
|
260
|
+
currentRun() {
|
|
261
|
+
return this.run ?? null;
|
|
262
|
+
}
|
|
263
|
+
/** the region's own steps in document order, then whatever was contributed from outside */
|
|
264
|
+
collectTree(root) {
|
|
265
|
+
const tree = collect(root);
|
|
266
|
+
for (const provider of this.contributors) {
|
|
267
|
+
tree.push(provider.node());
|
|
268
|
+
}
|
|
269
|
+
return tree;
|
|
270
|
+
}
|
|
271
|
+
register(node) {
|
|
272
|
+
this.participants.add(node);
|
|
273
|
+
this.arrived.add(node);
|
|
274
|
+
if (this.args.debug && node.element) {
|
|
275
|
+
node.element.style.outline = '1px dotted #16a34a';
|
|
276
|
+
// the seam lints (§5.3): inside a region the timeline is the only
|
|
277
|
+
// animation authority, and Presence double-retains participants
|
|
278
|
+
if (node.ownAnimation) {
|
|
279
|
+
console.warn(`choreo: participant '${node.id ?? node.role}' carries its own ` + 'animate/exit/initial — a second scheduler beside the timeline');
|
|
280
|
+
}
|
|
281
|
+
if (node.presenceManaged) {
|
|
282
|
+
console.warn(`choreo: participant '${node.id ?? node.role}' is inside a ` + '<Presence> — the region already retains its own leavers');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return () => {
|
|
286
|
+
// a destroyed participant may still be claimed (below); the sets that decide that survive until drop()
|
|
287
|
+
this.participants.delete(node);
|
|
288
|
+
this.arrived.delete(node);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
claim(node) {
|
|
292
|
+
if (this.orphans.has(node)) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (this.passPending) {
|
|
296
|
+
// its pass has not been processed yet: decide then
|
|
297
|
+
this.claimed.add(node);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
/* ---- the render pass ---- */ /* eslint-disable ember/no-side-effects -- this getter IS the render hook: boxel-motion's render detector,
|
|
303
|
+
re-evaluated on every render pass before the DOM is patched (React's getSnapshotBeforeUpdate slot) */
|
|
304
|
+
get renderDetector() {
|
|
305
|
+
consumeTag(VOLATILE_TAG);
|
|
306
|
+
// A region is a layout boundary as well: projecting participants
|
|
307
|
+
// (`layout` / `layoutId`) snapshot here too, so `{{motion layout=true}}`
|
|
308
|
+
// works inside a <Choreo> without a <LayoutGroup> wrapped around it.
|
|
309
|
+
snapshotOnRender();
|
|
310
|
+
if (!this.passPending) {
|
|
311
|
+
this.passPending = true;
|
|
312
|
+
this.snapshot();
|
|
313
|
+
// every region announces here, while Glimmer is still rendering, so the
|
|
314
|
+
// barrier knows the whole cast before anybody measures
|
|
315
|
+
join(this);
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
/* eslint-enable ember/no-side-effects */
|
|
320
|
+
snapshot() {
|
|
321
|
+
this.snapshots.clear();
|
|
322
|
+
this.rootSnapshot = this.element?.getBoundingClientRect();
|
|
323
|
+
for (const node of this.participants) {
|
|
324
|
+
const el = node.element;
|
|
325
|
+
if (el?.isConnected && !this.orphans.has(node)) {
|
|
326
|
+
this.snapshots.set(node, measure(el));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** phase 1: what changed this pass, measured — nothing is played yet */
|
|
331
|
+
measurePass() {
|
|
332
|
+
this.passPending = false;
|
|
333
|
+
const root = this.element;
|
|
334
|
+
if (!root) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
// THE FAST PATH: a volatile re-pass that cannot change the run. On a
|
|
338
|
+
// busy page every app render replays every region's pass, and the full
|
|
339
|
+
// pipeline is destructive even when its verdict is "keep" — release
|
|
340
|
+
// puts the world at rest (stopping the very animations it measures
|
|
341
|
+
// around), and standing them back up once per frame IS the jitter the
|
|
342
|
+
// keep exists to prevent. So a pass with no arrivals, no new leavers
|
|
343
|
+
// and the same timeline tree is declined before anything is touched:
|
|
344
|
+
// the run plays on, undisturbed.
|
|
345
|
+
if (this.fastKeep(root)) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
// `initial` was captured before the DOM changed, with any in-flight Move's
|
|
349
|
+
// values still on the elements — that is what was on screen. `final` must
|
|
350
|
+
// be the layout the stylesheet actually asks for, so put everything the
|
|
351
|
+
// last run touched back to rest before measuring anything.
|
|
352
|
+
this.run?.releaseForMeasure();
|
|
353
|
+
// @route: scroll is part of the move (§4.7) — the window is placed where
|
|
354
|
+
// the arriving scene wants it after the swap renders and BEFORE final
|
|
355
|
+
// bounds are measured, so every landing is measured where it will live
|
|
356
|
+
if (this.args.route && this.arrived.size && this.snapshots.size) {
|
|
357
|
+
const intent = this.args.scroll ?? 'top';
|
|
358
|
+
const y = intent === 'top' ? 0 : intent();
|
|
359
|
+
window.scrollTo(0, y);
|
|
360
|
+
}
|
|
361
|
+
const before = this.rootSnapshot ?? root.getBoundingClientRect();
|
|
362
|
+
const after = root.getBoundingClientRect();
|
|
363
|
+
const inserted = [];
|
|
364
|
+
const removed = [];
|
|
365
|
+
const kept = [];
|
|
366
|
+
const sprite = (node, type, initial, final) => ({
|
|
367
|
+
delta: initial && final ? {
|
|
368
|
+
height: final.page.height - initial.page.height,
|
|
369
|
+
width: final.page.width - initial.page.width,
|
|
370
|
+
x: final.page.x - initial.page.x,
|
|
371
|
+
y: final.page.y - initial.page.y
|
|
372
|
+
} : undefined,
|
|
373
|
+
element: node.element,
|
|
374
|
+
final,
|
|
375
|
+
id: node.id,
|
|
376
|
+
initial,
|
|
377
|
+
node,
|
|
378
|
+
role: node.role,
|
|
379
|
+
type
|
|
380
|
+
});
|
|
381
|
+
const candidates = new Set([...this.participants, ...this.claimed, ...this.orphans]);
|
|
382
|
+
for (const node of candidates) {
|
|
383
|
+
const el = node.element;
|
|
384
|
+
if (!el) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const snap = this.snapshots.get(node);
|
|
388
|
+
if (this.orphans.has(node)) {
|
|
389
|
+
// still in flight from an earlier run: addressable again as removed
|
|
390
|
+
const prior = this.orphanBounds.get(node);
|
|
391
|
+
removed.push(sprite(node, 'removed', prior));
|
|
392
|
+
} else if (this.arrived.has(node)) {
|
|
393
|
+
inserted.push(sprite(node, 'inserted', undefined, boundsOf(measure(el), after)));
|
|
394
|
+
} else if (!el.isConnected || this.claimed.has(node)) {
|
|
395
|
+
if (this.leaving.has(node)) {
|
|
396
|
+
// its Presence has let it go — we already played its exit
|
|
397
|
+
this.leaving.delete(node);
|
|
398
|
+
this.drop(node);
|
|
399
|
+
} else if (snap) {
|
|
400
|
+
removed.push(sprite(node, 'removed', boundsOf(snap, before)));
|
|
401
|
+
} else if (this.claimed.has(node)) {
|
|
402
|
+
this.drop(node);
|
|
403
|
+
}
|
|
404
|
+
} else if (!node.isPresent) {
|
|
405
|
+
if (!this.leaving.has(node) && snap) {
|
|
406
|
+
this.leaving.add(node);
|
|
407
|
+
removed.push(sprite(node, 'removed', boundsOf(snap, before)));
|
|
408
|
+
}
|
|
409
|
+
} else if (snap) {
|
|
410
|
+
this.leaving.delete(node);
|
|
411
|
+
kept.push(sprite(node, 'kept', boundsOf(snap, before), boundsOf(measure(el), after)));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// an inserted id that matches a removed one: the new element carries the old as its counterpart
|
|
415
|
+
for (const s of inserted) {
|
|
416
|
+
if (s.id === null) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const old = removed.find(r => r.id === s.id && !r.counterpart);
|
|
420
|
+
if (old) {
|
|
421
|
+
old.claimed = true;
|
|
422
|
+
s.counterpart = old;
|
|
423
|
+
s.initial = old.initial;
|
|
424
|
+
s.type = 'kept';
|
|
425
|
+
s.delta = s.initial && s.final && {
|
|
426
|
+
height: s.final.page.height - s.initial.page.height,
|
|
427
|
+
width: s.final.page.width - s.initial.page.width,
|
|
428
|
+
x: s.final.page.x - s.initial.page.x,
|
|
429
|
+
y: s.final.page.y - s.initial.page.y
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
this.arrived.clear();
|
|
434
|
+
this.snapshots.clear();
|
|
435
|
+
const claimed = [...this.claimed];
|
|
436
|
+
this.claimed.clear();
|
|
437
|
+
// the barrier now pairs any id inserted here against the same id removed in
|
|
438
|
+
// ANOTHER region, before finishPass compiles a timeline against the result
|
|
439
|
+
return {
|
|
440
|
+
claimed,
|
|
441
|
+
host: this,
|
|
442
|
+
inserted,
|
|
443
|
+
kept,
|
|
444
|
+
removed,
|
|
445
|
+
root
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/** phase 3: identities are settled — compile this region's timeline and play it */
|
|
449
|
+
finishPass(pass) {
|
|
450
|
+
const {
|
|
451
|
+
claimed,
|
|
452
|
+
inserted,
|
|
453
|
+
kept,
|
|
454
|
+
root
|
|
455
|
+
} = pass;
|
|
456
|
+
// a sprite whose identity was received by another region is not leaving:
|
|
457
|
+
// the element that carries it on is already being animated over there
|
|
458
|
+
const removed = pass.removed.filter(s => !s.sent);
|
|
459
|
+
for (const s of pass.removed) {
|
|
460
|
+
if (s.sent) {
|
|
461
|
+
this.finish(s);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const after = root.getBoundingClientRect();
|
|
465
|
+
// The zoom every box below was measured under (§6.3) — read from the
|
|
466
|
+
// PAINTED frame, never from the run's bookkeeping. While a camera cue
|
|
467
|
+
// flies on WAAPI the shadow value is one rAF behind the compositor,
|
|
468
|
+
// and inside a fast cue that one frame is the whole flight: fitted
|
|
469
|
+
// zooms cancel the mismatch, but translate targets keep it forever,
|
|
470
|
+
// and a chain of replacement passes compounds it (the reel's loop
|
|
471
|
+
// seams landed every shot hundreds of pixels off, differently per
|
|
472
|
+
// loop). getComputedStyle samples the same frozen timeline the rects
|
|
473
|
+
// above were measured under, so zoom and boxes are one measurement.
|
|
474
|
+
//
|
|
475
|
+
// It is the TOTAL scale between the page and this region's local
|
|
476
|
+
// space: the root's painted width over its LAYOUT width, not the
|
|
477
|
+
// region's own camera alone. Those two are the same number right up
|
|
478
|
+
// until something above the region is scaled, and then they are not.
|
|
479
|
+
// An ancestor `transform: scale(s)` multiplies every box this pass
|
|
480
|
+
// measures while the run still writes local pixels, so every flight
|
|
481
|
+
// is off by exactly s — a region inside a scale(1.5) card starts its
|
|
482
|
+
// flights a thousand pixels from where they belong. Reading the
|
|
483
|
+
// root's own rect subsumes the camera and everything above it in one
|
|
484
|
+
// measurement, taken in the same window as the boxes.
|
|
485
|
+
const layoutWidth = root.offsetWidth;
|
|
486
|
+
const shadow = this.run?.camera.zoom ?? this.restingCamera.zoom;
|
|
487
|
+
const painted = layoutWidth > 0 ? after.width / layoutWidth : 0;
|
|
488
|
+
// …snapped to the exact bookkeeping value whenever the two agree: the
|
|
489
|
+
// layout width is an integer, and letting that quantisation into the
|
|
490
|
+
// divide gives two passes of the same still landings a thousandth of
|
|
491
|
+
// a pixel apart. The painted value only OVERRULES the shadow when
|
|
492
|
+
// they genuinely diverge — which now means "something outside is
|
|
493
|
+
// scaling us" as well as "the camera is mid-cue".
|
|
494
|
+
const measureZoom = !painted || Math.abs(painted - shadow) < 0.004 ? shadow : painted;
|
|
495
|
+
const changeset = new Changeset(inserted.filter(s => s.type === 'inserted'), removed, [...kept, ...inserted.filter(s => s.type === 'kept')],
|
|
496
|
+
// measured in the same window as `final`, and re-measured every pass: a
|
|
497
|
+
// beacon can move without this region rendering at all
|
|
498
|
+
measureBeacons(after), measureZoom,
|
|
499
|
+
// the frame's own size in the same final layout, local pixels — the
|
|
500
|
+
// camera's centre reference, so aim terms are internally consistent
|
|
501
|
+
{
|
|
502
|
+
height: after.height / measureZoom,
|
|
503
|
+
width: after.width / measureZoom
|
|
504
|
+
}, groundOf(root));
|
|
505
|
+
this.changeset = changeset;
|
|
506
|
+
const firstRender = !this.rendered;
|
|
507
|
+
this.rendered = true;
|
|
508
|
+
// instant means instant on a crossing: no run, not a zero-length run
|
|
509
|
+
if (this.args.route && motionSpeed() === 0) {
|
|
510
|
+
for (const node of claimed) {
|
|
511
|
+
this.drop(node);
|
|
512
|
+
}
|
|
513
|
+
for (const s of removed) {
|
|
514
|
+
this.finish(s);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// A pass whose changeset is all-kept still runs its timeline (§3.1):
|
|
519
|
+
// the share badge, the hot wire — a Hold with a lifetime fired by an
|
|
520
|
+
// event that inserts, removes and moves nothing. Steps that select
|
|
521
|
+
// change (inserted / removed / moved) produce no cues on such a pass.
|
|
522
|
+
const tree = firstRender ? [] : this.collectTree(root);
|
|
523
|
+
const compiled = tree.length ? compile(tree, changeset) : {
|
|
524
|
+
cues: [],
|
|
525
|
+
gates: []
|
|
526
|
+
};
|
|
527
|
+
const cues = compiled.cues;
|
|
528
|
+
// CONTINUITY (§3.1): sprites the prior run is still driving through
|
|
529
|
+
// space, which this new score does not name, would be dropped to
|
|
530
|
+
// rest in ONE FRAME — the whole-bay snap every conditional timeline
|
|
531
|
+
// had to hand-patch with a `c.moved` twin. The changeset already
|
|
532
|
+
// reads a flying sprite as kept-and-moved (its initial is the painted
|
|
533
|
+
// box, by design), so the score is completed rather than overridden:
|
|
534
|
+
// each gets a continuation move from where it is painted to its rest.
|
|
535
|
+
// Before the empty-score bail on purpose — a timeline that un-renders
|
|
536
|
+
// mid-flight compiles NOTHING, and that bail neither cancels the run
|
|
537
|
+
// nor reasserts the world: the release itself was the snap. A sprite
|
|
538
|
+
// that merely REFLOWED is untouched — it was never driven.
|
|
539
|
+
if (this.run && !this.run.isDone()) {
|
|
540
|
+
const scored = new Set(cues.map(c => c.sprite.node));
|
|
541
|
+
for (const [node, transition] of this.run.midflight()) {
|
|
542
|
+
if (scored.has(node)) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const s = changeset.kept.find(k => k.node === node);
|
|
546
|
+
if (!s) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const cue = continuation(s, changeset, transition);
|
|
550
|
+
if (cue) {
|
|
551
|
+
cues.push(cue);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!cues.length) {
|
|
556
|
+
// nothing to play: let every leaver go now
|
|
557
|
+
for (const node of claimed) {
|
|
558
|
+
this.drop(node);
|
|
559
|
+
}
|
|
560
|
+
for (const s of removed) {
|
|
561
|
+
// §5.3, from the other side of the seam: on a pass this region
|
|
562
|
+
// declines to animate, a <Presence>-managed leaver still mid-exit
|
|
563
|
+
// belongs to its Presence — calling exitComplete here would cut
|
|
564
|
+
// that animation short. The region stands aside; when the exit
|
|
565
|
+
// ends, Presence unmounts the element and it leaves for real.
|
|
566
|
+
if (s.node.presenceManaged && !s.node.isPresent && s.element.isConnected && !this.orphans.has(s.node)) {
|
|
567
|
+
this.leaving.delete(s.node);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
this.finish(s);
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// An unrelated render replays every region's pass — the detector is
|
|
575
|
+
// volatile by design, so on a busy page (the gallery: a neighbouring
|
|
576
|
+
// demo writing tracked state per frame) a region re-passes on every
|
|
577
|
+
// app render. A finished run replays (§3.1's Hold re-fire). An
|
|
578
|
+
// all-still pass — no arrivals, leavers, or moved boxes — is noise
|
|
579
|
+
// (a parent writing `hot`, a neighbour ticking at 60fps) and must
|
|
580
|
+
// not restart the clock. Gates and path-moves make a freshly compiled
|
|
581
|
+
// cue list incomparable (zero-delta Moves drop; flights exist), so
|
|
582
|
+
// sameScore cannot be the only detector.
|
|
583
|
+
//
|
|
584
|
+
// A removed sprite that is one of this region's own ORPHANS is not a
|
|
585
|
+
// new leaver: it re-enters every changeset on purpose (addressable
|
|
586
|
+
// again), and while one is aloft a recompiled score is NOT
|
|
587
|
+
// authoritative — the pairing that produced the run's flights lived
|
|
588
|
+
// in the pass that removed and inserted together, and a later pass
|
|
589
|
+
// sees only the orphan half. Counting orphans as dirt meant a
|
|
590
|
+
// crossing on a busy page was cancelled into a leave-only rump by
|
|
591
|
+
// its own neighbours' renders, every frame.
|
|
592
|
+
const print = treePrint(tree);
|
|
593
|
+
const nothingNew = !inserted.length && pass.removed.every(s => this.orphans.has(s.node)) && (
|
|
594
|
+
// A sprite mid-flight reads as "moved" on every re-pass — its initial
|
|
595
|
+
// is the painted box, by design — so while this run's own orphans are
|
|
596
|
+
// aloft, kept-motion cannot justify a recompile either: the score it
|
|
597
|
+
// would produce is the degraded one above.
|
|
598
|
+
this.orphans.size > 0 || !changeset.sprites({
|
|
599
|
+
type: 'moved'
|
|
600
|
+
}).length) &&
|
|
601
|
+
// …but an EDITED timeline is never noise: a changed tree replays,
|
|
602
|
+
// exactly as it would with no orphans aloft (the build-order
|
|
603
|
+
// transport's delay nudge, made while a leaver is frozen mid-park)
|
|
604
|
+
print === this.scorePrint;
|
|
605
|
+
if (this.run && !this.run.isDone() && nothingNew) {
|
|
606
|
+
for (const node of claimed) {
|
|
607
|
+
this.drop(node);
|
|
608
|
+
}
|
|
609
|
+
// the measurement above put the run's world at rest; a keep must be
|
|
610
|
+
// invisible, so the run stands its picture back up (see reassert)
|
|
611
|
+
this.run.reassert();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// dirty but the compiled score is the one already in flight (tween-only
|
|
615
|
+
// measurement jitter): keep that run too.
|
|
616
|
+
//
|
|
617
|
+
// The TREE has to be the one in flight as well, not just the cues it
|
|
618
|
+
// compiled to. Identical cues are not proof of an identical timeline:
|
|
619
|
+
// a score that plays a slice of itself — a chapter skip that re-cuts
|
|
620
|
+
// to the head it is already inside — compiles to the same cue list at
|
|
621
|
+
// the same offsets, and keeping the run there hands the "new" film
|
|
622
|
+
// the old one's CLOCK. The score restarts on paper and carries on
|
|
623
|
+
// playing in fact: beats land early, some never land at all, and the
|
|
624
|
+
// picture runs ahead of the words that name it. An author who wants a
|
|
625
|
+
// re-execution says so by editing the tree (a fresh sequence name is
|
|
626
|
+
// the usual way, which is why the name is in the print).
|
|
627
|
+
if (this.run && !this.run.isDone() && !inserted.length && !pass.removed.length && !compiled.gates.length && print === this.scorePrint && sameScore(this.run.cues, cues)) {
|
|
628
|
+
for (const node of claimed) {
|
|
629
|
+
this.drop(node);
|
|
630
|
+
}
|
|
631
|
+
this.run.reassert();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// the removed participants the run names stay; the rest go now
|
|
635
|
+
const named = new Set(cues.map(c => c.sprite));
|
|
636
|
+
for (const s of [...kept, ...inserted]) {
|
|
637
|
+
if (s.counterpart && named.has(s)) {
|
|
638
|
+
named.add(s.counterpart);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const prior = this.run;
|
|
642
|
+
// Which camera the replacement starts from is decided by WHAT it is.
|
|
643
|
+
// The same score recompiled mid-run is a re-execution of the same
|
|
644
|
+
// timeline: it inherits the prior run's fold origin, so a seek's
|
|
645
|
+
// reconstruction folds the score's prefix once and only once —
|
|
646
|
+
// handing it the pose in force would fold every relative cue on top
|
|
647
|
+
// of the pose that cue already produced, and the world doubles
|
|
648
|
+
// (world-dock-test). A NEW score responds to the world as it stands:
|
|
649
|
+
// it starts from the pose actually applied, which mid-flight is the
|
|
650
|
+
// prior run's live shadow, not the last landed state.
|
|
651
|
+
const inherited = prior !== undefined && !prior.isDone() ? print === this.scorePrint ? {
|
|
652
|
+
aim: prior.initialAim,
|
|
653
|
+
camera: prior.initialCamera
|
|
654
|
+
} : {
|
|
655
|
+
aim: prior.cameraAim,
|
|
656
|
+
camera: prior.camera
|
|
657
|
+
} : {
|
|
658
|
+
aim: prior?.cameraAim ?? null,
|
|
659
|
+
camera: this.restingCamera
|
|
660
|
+
};
|
|
661
|
+
// the shot carries run to run for the same reason the frame does: a
|
|
662
|
+
// replacement pass must continue the move, not snap back to framing
|
|
663
|
+
const shot3d = prior !== undefined && !prior.isDone() ? prior.camera3d : this.resting3d;
|
|
664
|
+
prior?.cancel(new Set([...named].map(s => s.node)));
|
|
665
|
+
for (const s of removed) {
|
|
666
|
+
if (!named.has(s)) {
|
|
667
|
+
this.finish(s);
|
|
668
|
+
} else if (!s.element.isConnected || claimed.includes(s.node)) {
|
|
669
|
+
this.orphan(s, removed, s.claimed === true);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (this.args.debug) {
|
|
673
|
+
// the hierarchy lint (§6.3): a removed participant no step names will
|
|
674
|
+
// simply vanish — the uncanny valley the deck warns about
|
|
675
|
+
for (const s of removed) {
|
|
676
|
+
if (!named.has(s)) {
|
|
677
|
+
console.warn(`choreo: removed participant '${s.id ?? s.role}' is named by ` + 'no step — it vanishes without a frame');
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
this.log(changeset, cues);
|
|
681
|
+
}
|
|
682
|
+
this.scorePrint = print;
|
|
683
|
+
// taken here, with the world released and measured: the resting layout
|
|
684
|
+
// this run is about to fly away from (see fastKeep)
|
|
685
|
+
this.layoutPrint = this.layoutOf();
|
|
686
|
+
// @quiet: pause everything running RIGHT NOW — the run's own animations
|
|
687
|
+
// do not exist yet, so they are exempt by construction. Accumulated,
|
|
688
|
+
// not replaced: an interrupted run's replacement sweeps again, and
|
|
689
|
+
// everything resumes together when the run that survives settles.
|
|
690
|
+
if (this.args.quiet) {
|
|
691
|
+
this.quiet();
|
|
692
|
+
}
|
|
693
|
+
this.run = execute(compiled, {
|
|
694
|
+
camera: {
|
|
695
|
+
...inherited.camera
|
|
696
|
+
},
|
|
697
|
+
camera3d: {
|
|
698
|
+
...shot3d
|
|
699
|
+
},
|
|
700
|
+
cameraFrame: this.element,
|
|
701
|
+
// the aim point in force carries run to run, so a re-aim lerps from
|
|
702
|
+
// what is actually applied rather than assuming an unaimed frame —
|
|
703
|
+
// except a same-score re-execution, which carries the fold origin
|
|
704
|
+
cameraAim: inherited.aim,
|
|
705
|
+
// updated at step boundaries only — a still value app logic can
|
|
706
|
+
// read. Guarded by equality: the landing itself renders, the render
|
|
707
|
+
// is an all-kept pass, and the pass replays the camera step — an
|
|
708
|
+
// unguarded set would revalidate forever.
|
|
709
|
+
onCamera3D: state => {
|
|
710
|
+
this.resting3d = state;
|
|
711
|
+
this.args.onCamera3D?.(state);
|
|
712
|
+
},
|
|
713
|
+
onCamera: state => {
|
|
714
|
+
const prior = this.restingCamera;
|
|
715
|
+
this.restingCamera = state;
|
|
716
|
+
if (prior.zoom !== state.zoom || prior.x !== state.x || prior.y !== state.y) {
|
|
717
|
+
// the library's own host hook — the Ember adapter installs the
|
|
718
|
+
// runloop's afterRender behind it, and this file stays host-blind
|
|
719
|
+
postRender(() => this.cameraState = state);
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
raisedLayer: this.raisedLayer,
|
|
723
|
+
tetherLayer: this.tetherLayer,
|
|
724
|
+
// ember-animated's continuity, in the terms motion-dom offers: it sums a
|
|
725
|
+
// corrective curve onto the one it interrupted, which transfers velocity
|
|
726
|
+
// implicitly; a spring takes a velocity outright, so the run that is
|
|
727
|
+
// being replaced hands over how fast everything was going.
|
|
728
|
+
inherit: prior?.velocities,
|
|
729
|
+
onPerform: this.args.onPerform,
|
|
730
|
+
onPerformReset: this.args.onPerformReset,
|
|
731
|
+
onSpriteDone: s => this.finish(s),
|
|
732
|
+
removed: removed.filter(s => named.has(s)),
|
|
733
|
+
// nothing in this region is staying: no participant kept, none
|
|
734
|
+
// arriving, and every one it can see on its way out. A region in that
|
|
735
|
+
// state is not watching a departure, it is inside one — see `park`.
|
|
736
|
+
doomed: !kept.length && !inserted.length && removed.length > 0
|
|
737
|
+
});
|
|
738
|
+
if (this.args.quiet) {
|
|
739
|
+
const run = this.run;
|
|
740
|
+
void run.finished.then(() => {
|
|
741
|
+
// only the run still in charge hands the page back — a cancelled
|
|
742
|
+
// one was replaced, and its replacement holds the pause
|
|
743
|
+
if (this.run === run) {
|
|
744
|
+
this.unquiet();
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/* ---- orphans: a removed participant kept alive, locked where it was ---- */
|
|
750
|
+
orphanBounds = new Map();
|
|
751
|
+
/** the fingerprint of the tree the current run compiled from (see treePrint) */
|
|
752
|
+
scorePrint;
|
|
753
|
+
/** the layout every participant stood in when the run compiled (see layoutPrint) */
|
|
754
|
+
layoutPrint;
|
|
755
|
+
/**
|
|
756
|
+
* Where the stylesheet puts each participant, as a string.
|
|
757
|
+
*
|
|
758
|
+
* `offsetLeft/Top/Width/Height` and NOT a bounding rect, because the
|
|
759
|
+
* difference is the whole point: a rect includes the transform a flight
|
|
760
|
+
* is writing, so a moving element reads as different on every frame,
|
|
761
|
+
* while offsets are the box layout asked for — steady under a transform,
|
|
762
|
+
* and different the moment something actually reflows. Integers, too,
|
|
763
|
+
* which quietly forgives the sub-pixel noise a busy page generates.
|
|
764
|
+
*
|
|
765
|
+
* A Move that animates real width/height (`@size={{true}}`) does change
|
|
766
|
+
* these, so such a run never takes the fast path. That is the honest
|
|
767
|
+
* answer rather than a missed optimisation: a run writing layout cannot
|
|
768
|
+
* be told apart from a page writing layout without measuring.
|
|
769
|
+
*
|
|
770
|
+
* Offsets are accumulated up the offset chain to the region, not read
|
|
771
|
+
* at the participant alone, because what a render moves may be an
|
|
772
|
+
* ANCESTOR that is no participant of anything — Escort's seat, one
|
|
773
|
+
* positioned wrapper carrying three participants. Read flat, the
|
|
774
|
+
* fingerprint was blind to exactly the reflow a retarget makes, the
|
|
775
|
+
* pass was declined, and the old run played on against the moved
|
|
776
|
+
* layout: card, badge and shadow teleported a whole bay, together. The
|
|
777
|
+
* walk stops at the region's edge so the page above it stays out of the
|
|
778
|
+
* print — a busy neighbour reflowing above the region must not cancel
|
|
779
|
+
* its runs.
|
|
780
|
+
*/
|
|
781
|
+
layoutOf() {
|
|
782
|
+
const root = this.element;
|
|
783
|
+
const parts = [];
|
|
784
|
+
for (const node of this.participants) {
|
|
785
|
+
const el = node.element;
|
|
786
|
+
if (!el || !el.isConnected) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
let x = 0;
|
|
790
|
+
let y = 0;
|
|
791
|
+
let walk = el;
|
|
792
|
+
while (walk && walk !== root && root?.contains(walk)) {
|
|
793
|
+
x += walk.offsetLeft;
|
|
794
|
+
y += walk.offsetTop;
|
|
795
|
+
const parent = walk.offsetParent;
|
|
796
|
+
walk = parent instanceof HTMLElement ? parent : null;
|
|
797
|
+
}
|
|
798
|
+
parts.push(`${node.id ?? node.role ?? ''}@${x},${y},` + `${el.offsetWidth},${el.offsetHeight}`);
|
|
799
|
+
}
|
|
800
|
+
return parts.join('|');
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Can this pass be declined outright? Only while a run is in flight, and
|
|
804
|
+
* only when nothing the pass could discover would change it: no arrivals,
|
|
805
|
+
* no retentions pending, every participant either present or already a
|
|
806
|
+
* known leaver, the collected timeline tree fingerprints identical to the
|
|
807
|
+
* one the run compiled from (an EDIT always replays), and every
|
|
808
|
+
* participant standing in the same layout box it compiled in (a REFLOW
|
|
809
|
+
* always replays — it is what a Move is FOR). Anything else takes the
|
|
810
|
+
* full pipeline.
|
|
811
|
+
*/
|
|
812
|
+
fastKeep(root) {
|
|
813
|
+
if (!this.run || this.run.isDone() || this.claimed.size || this.arrived.size) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
// A HELD RUN CANNOT BE MEASURED AROUND.
|
|
817
|
+
//
|
|
818
|
+
// Everything below this line decides "nothing changed" by looking at the
|
|
819
|
+
// page — and while a run is holding, what the page reports is what the
|
|
820
|
+
// RUN wrote. A Move keeps inline width and height on its sprites for as
|
|
821
|
+
// long as it owns them, and `layoutOf` reads exactly those. So a run
|
|
822
|
+
// parked at the pose it compiled TO reports a layout identical to the
|
|
823
|
+
// one recorded when it compiled, and a genuine change to the resting
|
|
824
|
+
// layout arrives on a pass that declines itself: the score is never
|
|
825
|
+
// recompiled and the new pose never lands.
|
|
826
|
+
//
|
|
827
|
+
// That is not hypothetical. It is what a host scrubbing `run.time` hits
|
|
828
|
+
// every time it swaps to a neighbouring segment — the swap happens at a
|
|
829
|
+
// waypoint, and a waypoint is the pose the outgoing run is parked at by
|
|
830
|
+
// definition.
|
|
831
|
+
//
|
|
832
|
+
// Measuring the resting layout instead would mean releasing the run, and
|
|
833
|
+
// releasing is the destructive thing this path exists to avoid. So the
|
|
834
|
+
// rule is simply: the fast path is for a run flying under its own clock.
|
|
835
|
+
// A run whose clock somebody else is holding takes the full pipeline —
|
|
836
|
+
// which costs nothing in practice, because a host that owns a clock is
|
|
837
|
+
// not the busy-gallery case this path was written for. It scrubs; it
|
|
838
|
+
// does not re-render.
|
|
839
|
+
if (this.run.paused) {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
for (const node of this.participants) {
|
|
843
|
+
const el = node.element;
|
|
844
|
+
if (!el || !el.isConnected) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (!node.isPresent && !this.leaving.has(node) && !this.orphans.has(node)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return treePrint(this.collectTree(root)) === this.scorePrint && this.layoutOf() === this.layoutPrint;
|
|
852
|
+
}
|
|
853
|
+
/* ---- @quiet: the rest of the page, paused for the span of a run ---- */ /** what quiet() paused, accumulated across replaced runs, resumed as one */
|
|
854
|
+
quieted = [];
|
|
855
|
+
quiet() {
|
|
856
|
+
for (const animation of document.getAnimations()) {
|
|
857
|
+
if (animation.playState === 'running') {
|
|
858
|
+
animation.pause();
|
|
859
|
+
this.quieted.push(animation);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
unquiet() {
|
|
864
|
+
for (const animation of this.quieted.splice(0)) {
|
|
865
|
+
try {
|
|
866
|
+
animation.play();
|
|
867
|
+
} catch {
|
|
868
|
+
// an animation whose element left with the old scene: nothing to resume
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
orphan(s, removed, extract = false) {
|
|
873
|
+
// Only the topmost removed element is moved; what it contains comes with
|
|
874
|
+
// it — EXCEPT a claimed skin. Its whole job is to cross inside the flight,
|
|
875
|
+
// and riding a fading ancestor instead is the View Transitions nesting
|
|
876
|
+
// rule (never name a container of named things) sneaking back in. A
|
|
877
|
+
// claimed sprite is lifted out of the doomed subtree; both end up locked
|
|
878
|
+
// to the same page coordinates in the same layer, so the lift is
|
|
879
|
+
// invisible — all it leaves behind is a hole in the ancestor, which is
|
|
880
|
+
// exactly where the eye expects one while the skin is elsewhere.
|
|
881
|
+
const inside = removed.some(o => o !== s && o.element !== s.element && o.element.contains(s.element));
|
|
882
|
+
if (inside && !extract || !s.initial) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const layer = this.orphanLayer;
|
|
886
|
+
const root = this.element;
|
|
887
|
+
if (!layer || !root) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
// Page measurements carry every transform above the region — its own
|
|
891
|
+
// camera, and whatever is carrying the whole region this frame (a
|
|
892
|
+
// page crossing scaling the stage). The lock is written as LOCAL
|
|
893
|
+
// pixels inside the equally-transformed orphan layer, so the scale
|
|
894
|
+
// must come back out or it is applied twice and the leaver jumps the
|
|
895
|
+
// moment it is lifted — the raise learned this same rule.
|
|
896
|
+
const now = root.getBoundingClientRect();
|
|
897
|
+
const sx = root.offsetWidth ? now.width / root.offsetWidth : 1;
|
|
898
|
+
const sy = root.offsetHeight ? now.height / root.offsetHeight : 1;
|
|
899
|
+
if (inside) {
|
|
900
|
+
// hold the seat: the ancestor is itself a leaver, and without this its
|
|
901
|
+
// remaining children reflow into the gap mid-fade. The placeholder
|
|
902
|
+
// lives only inside that doomed subtree, so it leaves with it.
|
|
903
|
+
const seat = document.createElement('div');
|
|
904
|
+
seat.style.width = `${s.initial.page.width / sx}px`;
|
|
905
|
+
seat.style.height = `${s.initial.page.height / sy}px`;
|
|
906
|
+
s.element.parentElement?.insertBefore(seat, s.element);
|
|
907
|
+
}
|
|
908
|
+
// locked where it was ON THE PAGE: the region itself may have moved in the same pass
|
|
909
|
+
const {
|
|
910
|
+
page
|
|
911
|
+
} = s.initial;
|
|
912
|
+
const el = s.element;
|
|
913
|
+
el.style.position = 'absolute';
|
|
914
|
+
el.style.left = `${(page.x - now.left) / sx}px`;
|
|
915
|
+
el.style.top = `${(page.y - now.top) / sy}px`;
|
|
916
|
+
el.style.width = `${page.width / sx}px`;
|
|
917
|
+
el.style.height = `${page.height / sy}px`;
|
|
918
|
+
el.style.boxSizing = 'border-box';
|
|
919
|
+
el.style.margin = '0';
|
|
920
|
+
el.style.pointerEvents = 'none';
|
|
921
|
+
// one texture, transformed — not a subtree re-rastered per frame
|
|
922
|
+
el.style.willChange = 'transform, opacity';
|
|
923
|
+
layer.appendChild(el);
|
|
924
|
+
this.orphans.add(s.node);
|
|
925
|
+
this.orphanBounds.set(s.node, s.initial);
|
|
926
|
+
}
|
|
927
|
+
/** a removed sprite's row has ended */
|
|
928
|
+
finish(s) {
|
|
929
|
+
if (this.orphans.has(s.node) || this.participants.has(s.node)) {
|
|
930
|
+
if (!s.element.isConnected || this.orphans.has(s.node)) {
|
|
931
|
+
this.drop(s.node);
|
|
932
|
+
} else if (!s.node.isPresent) {
|
|
933
|
+
s.node.exitComplete();
|
|
934
|
+
}
|
|
935
|
+
} else {
|
|
936
|
+
this.drop(s.node);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
drop(node) {
|
|
940
|
+
const el = node.element;
|
|
941
|
+
if (el && this.orphans.has(node) && el.parentElement === this.orphanLayer) {
|
|
942
|
+
el.remove();
|
|
943
|
+
}
|
|
944
|
+
this.orphans.delete(node);
|
|
945
|
+
this.orphanBounds.delete(node);
|
|
946
|
+
this.claimed.delete(node);
|
|
947
|
+
this.leaving.delete(node);
|
|
948
|
+
this.participants.delete(node);
|
|
949
|
+
node.release();
|
|
950
|
+
}
|
|
951
|
+
/* ---- debug ---- */
|
|
952
|
+
log(changeset, cues) {
|
|
953
|
+
const id = this.args.id ?? '';
|
|
954
|
+
const row = s => ({
|
|
955
|
+
choreo: id,
|
|
956
|
+
final: s.final ? JSON.stringify(s.final.context) : null,
|
|
957
|
+
id: s.id,
|
|
958
|
+
initial: s.initial ? JSON.stringify(s.initial.context) : null,
|
|
959
|
+
role: s.role,
|
|
960
|
+
type: s.type
|
|
961
|
+
});
|
|
962
|
+
console.table(changeset.all.map(row));
|
|
963
|
+
console.table(cues.map(c => ({
|
|
964
|
+
choreo: id,
|
|
965
|
+
duration: Math.round(c.duration),
|
|
966
|
+
sprite: c.sprite.id ?? c.sprite.role,
|
|
967
|
+
start: Math.round(c.start),
|
|
968
|
+
step: c.kind,
|
|
969
|
+
values: JSON.stringify(c.target ?? c.hold?.values ?? null)
|
|
970
|
+
})));
|
|
971
|
+
}
|
|
972
|
+
/* ---- template ---- */
|
|
973
|
+
host = modifier(el => {
|
|
974
|
+
this.element = el;
|
|
975
|
+
setChoreoHost(el, this);
|
|
976
|
+
ensureOrphanStyle();
|
|
977
|
+
if (getComputedStyle(el).position === 'static') {
|
|
978
|
+
el.style.position = 'relative';
|
|
979
|
+
}
|
|
980
|
+
if (this.args.debug) {
|
|
981
|
+
el.style.outline = '1px dashed #2563eb';
|
|
982
|
+
if (!debugStyle) {
|
|
983
|
+
debugStyle = document.createElement('style');
|
|
984
|
+
debugStyle.textContent = '[data-choreo-orphans] > * { outline: 1px dotted #dc2626 !important; }';
|
|
985
|
+
document.head.appendChild(debugStyle);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return () => {
|
|
989
|
+
setChoreoHost(el, undefined);
|
|
990
|
+
this.element = undefined;
|
|
991
|
+
};
|
|
992
|
+
});
|
|
993
|
+
layer = modifier(el => {
|
|
994
|
+
this.orphanLayer = el;
|
|
995
|
+
return () => {
|
|
996
|
+
this.orphanLayer = undefined;
|
|
997
|
+
};
|
|
998
|
+
});
|
|
999
|
+
raised = modifier(el => {
|
|
1000
|
+
this.raisedLayer = el;
|
|
1001
|
+
return () => {
|
|
1002
|
+
this.raisedLayer = undefined;
|
|
1003
|
+
};
|
|
1004
|
+
});
|
|
1005
|
+
tethers = modifier(el => {
|
|
1006
|
+
this.tetherLayer = el;
|
|
1007
|
+
return () => {
|
|
1008
|
+
this.tetherLayer = undefined;
|
|
1009
|
+
};
|
|
1010
|
+
});
|
|
1011
|
+
/**
|
|
1012
|
+
* The host is the scene: participants are its DIRECT children, so the
|
|
1013
|
+
* author's own grid/flex layout on the region element applies to them —
|
|
1014
|
+
* a wrapper here would silently unhook every gap and track. c.Camera
|
|
1015
|
+
* therefore drives the host element itself (§6.3), and the overlay
|
|
1016
|
+
* layers ride the frame with the content they annotate.
|
|
1017
|
+
*/
|
|
1018
|
+
static {
|
|
1019
|
+
setComponentTemplate(precompileTemplate("{{this.renderDetector}}\n<div data-choreo={{if @id @id \"\"}} data-motion-host {{this.host}} ...attributes>\n <div data-choreo-orphans style=\"position:absolute;inset:0;pointer-events:none;overflow:visible\" {{this.layer}}></div>\n {{!-- the elevated layer: where c.Raise promotes the living \u2014 above every\n stacking context and clip in the region (\xA76.3) --}}\n <div data-choreo-raised style=\"position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:2147483000\" {{this.raised}}></div>\n {{!-- the wires: geometry drawn between sprites, every frame (\xA76.1) --}}\n <svg data-choreo-tethers style=\"position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible\" {{this.tethers}}></svg>\n {{yield this.context}}\n</div>", {
|
|
1020
|
+
strictMode: true
|
|
1021
|
+
}), this);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export { Choreo, Choreo as default };
|
|
1026
|
+
//# sourceMappingURL=choreo.js.map
|