@motionscript/grids 0.1.0-alpha.5 → 0.1.0-alpha.6

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +47 -15
  3. package/dist/browser/chunks/chunk-J62D7EFZ.js +2 -0
  4. package/dist/browser/chunks/chunk-J62D7EFZ.js.map +7 -0
  5. package/dist/browser/index.js +1 -1
  6. package/dist/browser/index.js.map +3 -3
  7. package/dist/browser/kit.js +1 -1
  8. package/dist/browser/manifest.json +1 -1
  9. package/dist/engine.d.ts +4 -4
  10. package/dist/engine.js +4 -4
  11. package/dist/engine.js.map +1 -1
  12. package/dist/kit/index.d.ts +3 -1
  13. package/dist/kit/index.d.ts.map +1 -1
  14. package/dist/kit/index.js +4 -1
  15. package/dist/kit/index.js.map +1 -1
  16. package/dist/kit/item-grid.d.ts +203 -198
  17. package/dist/kit/item-grid.d.ts.map +1 -1
  18. package/dist/kit/item-grid.js +413 -440
  19. package/dist/kit/item-grid.js.map +1 -1
  20. package/dist/nodes.d.ts +8 -3
  21. package/dist/nodes.d.ts.map +1 -1
  22. package/dist/nodes.js +6 -10
  23. package/dist/nodes.js.map +1 -1
  24. package/dist/path-grid/path-grid.d.ts +108 -73
  25. package/dist/path-grid/path-grid.d.ts.map +1 -1
  26. package/dist/path-grid/path-grid.js +129 -172
  27. package/dist/path-grid/path-grid.js.map +1 -1
  28. package/dist/radial-grid/radial-grid.d.ts +97 -45
  29. package/dist/radial-grid/radial-grid.d.ts.map +1 -1
  30. package/dist/radial-grid/radial-grid.js +55 -87
  31. package/dist/radial-grid/radial-grid.js.map +1 -1
  32. package/dist/rectangular-grid/rectangular-grid.d.ts +114 -70
  33. package/dist/rectangular-grid/rectangular-grid.d.ts.map +1 -1
  34. package/dist/rectangular-grid/rectangular-grid.js +107 -145
  35. package/dist/rectangular-grid/rectangular-grid.js.map +1 -1
  36. package/dist/sphere-grid/sphere-grid.d.ts +109 -60
  37. package/dist/sphere-grid/sphere-grid.d.ts.map +1 -1
  38. package/dist/sphere-grid/sphere-grid.js +90 -137
  39. package/dist/sphere-grid/sphere-grid.js.map +1 -1
  40. package/package.json +4 -4
  41. package/src/engine.ts +4 -4
  42. package/src/kit/index.ts +13 -1
  43. package/src/kit/item-grid.ts +505 -442
  44. package/src/nodes.ts +18 -3
  45. package/src/path-grid/path-grid.ts +137 -164
  46. package/src/radial-grid/radial-grid.ts +69 -78
  47. package/src/rectangular-grid/rectangular-grid.ts +118 -133
  48. package/src/sphere-grid/sphere-grid.ts +103 -117
  49. package/dist/browser/chunks/chunk-XDUDKWM7.js +0 -2
  50. package/dist/browser/chunks/chunk-XDUDKWM7.js.map +0 -7
@@ -1,24 +1,9 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { command, easeIn, easeInOut, easeOut, findNode2D, flowOps, frameIn, insetsOps, Node2D, property, sizeInputOps, } from "@motionscript/sdk";
8
- // Once this kit's own; kept on its barrel for the grids that import them from it.
9
- export { findNode2D, frameIn } from "@motionscript/sdk";
1
+ import { commandsFor, easeIn, easeInOut, easeOut, flowOps, insetsOps, Types, } from "@motionscript/sdk";
10
2
  import { applyItemEffects, REVEAL_STYLES, WAVE_STYLES, } from "./item-effects.js";
11
3
  import { normalizeOrder, parseOrder, slotOfItem } from "./order.js";
12
4
  import { hashSeed, shuffled } from "./random.js";
13
5
  import { blendSlot, focalLengthOf, slotsOf, spreadSlot } from "./slots/index.js";
14
6
  import { STAGGER_ORDERS, staggerProgress, staggerRanks } from "./stagger.js";
15
- /** `@command` descriptors for {@link StaggerArgs}, `stagger` defaulting to `fallback`. */
16
- export function staggerArgs(fallback) {
17
- return [
18
- { key: "staggerOrder", kind: "enum", options: STAGGER_ORDERS, default: "index" },
19
- { key: "stagger", min: 0, max: 0.95, step: 0.05, default: fallback },
20
- ];
21
- }
22
7
  /**
23
8
  * Every grid's document key — what a reference to "another grid" may name. A
24
9
  * `nodeRef` descriptor carries it as `nodeTypes`, for a host's picker to offer
@@ -27,460 +12,448 @@ export function staggerArgs(fallback) {
27
12
  export const ITEM_GRID_TYPES = ["rectangularGrid", "radialGrid", "pathGrid", "sphereGrid"];
28
13
  /** The stagger a move to new slots takes when its command states none. */
29
14
  export const TRANSITION_STAGGER = 0.3;
15
+ /** A reference to another node, a host's picker offering only `types`. Its fields read a view of it. */
16
+ export function nodeRef(types) {
17
+ const described = { kind: "nodeRef", nodeTypes: types, animatable: false };
18
+ return Types.custom("", described);
19
+ }
20
+ /** The stagger args every per-item command takes, `stagger` defaulting to `fallback`. */
21
+ export function staggerArgs(fallback) {
22
+ return {
23
+ staggerOrder: Types.oneOf(STAGGER_ORDERS, { default: "index" }),
24
+ stagger: Types.number(fallback, { min: 0, max: 0.95, step: 0.05 }),
25
+ };
26
+ }
27
+ /**
28
+ * A command arg a host fills in with `hint`, which a row may leave out: the
29
+ * command then keeps what the grid has. A run arg's default is both what a
30
+ * host offers and what an omitted arg means, so the hint rides on the arg's
31
+ * description alone, and an omitted arg reads `undefined`.
32
+ */
33
+ export function hinted(hint, options = {}) {
34
+ const described = { ...options, default: hint };
35
+ return Types.custom(undefined, described);
36
+ }
37
+ /** A command arg naming another node, a host's picker offering only `types`; left out, it reads `undefined`. */
38
+ export function nodeRefArg(types) {
39
+ const described = { kind: "nodeRef", nodeTypes: types };
40
+ return Types.custom(undefined, described);
41
+ }
42
+ /** The state every grid shares. */
43
+ export const state = {
44
+ /** Items in slot order: slot `k` holds child `order[k]`. Empty is the children's own order. */
45
+ order: Types.custom([], { kind: "order", animatable: false }),
46
+ /** `0` gathers every item at the centre, `1` is the arrangement, past `1` pushes them apart. */
47
+ spread: Types.number(1, { min: 0, max: 3, step: 0.05 }),
48
+ /** Another grid whose arrangement this one takes on — what `morphInto` animates. */
49
+ arrangeLike: nodeRef(ITEM_GRID_TYPES),
50
+ // What the per-item commands write. State rather than settings: a command
51
+ // owns each while it runs, and a document has no reason to name them. Unset
52
+ // until a command writes one, so a fresh node's row does not state them.
53
+ revealState: Types.custom(undefined, { kind: "state", animatable: false }),
54
+ waveState: Types.custom(undefined, { kind: "state", animatable: false }),
55
+ focusState: Types.custom(undefined, { kind: "state", animatable: false }),
56
+ transition: Types.custom(undefined, { kind: "state", animatable: false }),
57
+ /** The grid a transition's leaving arrangement was borrowed from, followed while the items travel. */
58
+ transitionSource: Types.custom("", { kind: "nodeRef", private: true }),
59
+ };
60
+ /** A grid's size is what its items come to, whether it was built with children or they arrive after it. */
61
+ export const defaults = { width: "hug", height: "hug" };
62
+ const GRIDS = new Map();
63
+ /**
64
+ * `component` as a grid: registers its `arranger`, which another grid borrowing
65
+ * its arrangement or morphing into it reads it by, and which {@link isGrid} knows it by.
66
+ */
67
+ export function define(component, arranger) {
68
+ GRIDS.set(component.key, { component, arranger: arranger });
69
+ return component;
70
+ }
71
+ /** Whether `value` is a node of one of the grids {@link define} has seen. */
72
+ export function isGrid(value) {
73
+ for (const { component } of GRIDS.values())
74
+ if (value instanceof component.node)
75
+ return true;
76
+ return false;
77
+ }
78
+ /** The grid's measure, layout and render, for `arranger`'s arrangement: spread them into the component. */
79
+ export function fields(arranger) {
80
+ return {
81
+ measure: (measurer, s, children) => measure(measurer, s, children, arranger),
82
+ layout: (box, s, children) => layout(box, s, children, arranger),
83
+ render: (ctx, s) => render(ctx, s, arranger),
84
+ };
85
+ }
30
86
  const NO_CONTENT = { width: 0, height: 0 };
31
87
  const CENTRE = { x: 0, y: 0 };
88
+ /** What `measure` found, for `layout` to reuse in the same pass. Keyed by the grid's state, one object per node. */
89
+ const measured = new WeakMap();
90
+ export function measure(measurer, s, children, arranger) {
91
+ const constraints = { maxWidth: measurer.max.width, maxHeight: measurer.max.height };
92
+ const padding = s.padding;
93
+ if (children.length === 0) {
94
+ measured.delete(s);
95
+ return flowOps.size(s, constraints, padding, NO_CONTENT);
96
+ }
97
+ const space = flowOps.contentBox(s, constraints, padding);
98
+ const sizes = measureItems(children, space, s, arranger);
99
+ measured.set(s, { space, sizes });
100
+ const grid = { state: s, arranger, children, isSelf: sharesChildren(children) };
101
+ const target = targetSlots(grid, sizes.length, sizes, space, measurer.time.total);
102
+ return flowOps.size(s, constraints, padding, extentOf(target.slots, sizes, focalOf(s, target.arrangement)));
103
+ }
104
+ export function layout(box, s, children, arranger) {
105
+ const kept = measured.get(s);
106
+ measured.delete(s);
107
+ if (children.length === 0)
108
+ return [];
109
+ const inner = insetsOps.insetBounds(box.width, box.height, s.padding);
110
+ const sizes = kept && kept.sizes.length === children.length && sameSize(kept.space, inner)
111
+ ? kept.sizes
112
+ : measureItems(children, inner, s, arranger);
113
+ const slots = resolve({ state: s, arranger, children, isSelf: sharesChildren(children) }, sizes, inner, box.time.total);
114
+ return slots.map((slot, i) => placement(inner, slot, sizes[i]));
115
+ }
32
116
  /**
33
- * The base of every grid: a container that places each of its children in a
34
- * slot of an arrangement — a rectangular grid, a ring, a path, a sphere.
35
- *
36
- * ## Children only
37
- *
38
- * The items are the children, in layer order. A child's slot is a parent
39
- * transform on it, so its own `x`/`y`/`rotation`/`scale` — and any command on
40
- * them — still work, relative to the slot. A group is one item; a grid of
41
- * grids is a grid of items that happen to be grids.
42
- *
43
- * ## One pass
44
- *
45
- * Every frame: measure the children, ask the arrangement for its slots (a pure
46
- * function of the props, the sizes and the clock), blend them from where a
47
- * transition is leaving, and layer on the per-item effects — reveal, wave,
48
- * focus. What is left is each child's box and {@link ChildPlacement}, which the
49
- * renderer, picking and `global` all read, so a selection outline lands on
50
- * the pixels even on a sphere.
51
- *
52
- * ## Commands
53
- *
54
- * Every command is a pure function of time, like the rest of the library. The
55
- * per-item ones stagger *inside* their duration — a one-second reveal takes one
56
- * second for four children or four hundred — and write their per-item state
57
- * into props ({@link revealState} and its siblings), which is what keeps them
58
- * seekable. The ones that change the arrangement itself (a reorder, a morph, a
59
- * column count) keep the arrangement they are leaving as data and blend each
60
- * item from its old slot to its new one.
117
+ * A grid whose arrangement has depth is seen through a camera of its own,
118
+ * which paints its items far to near — unless it already sits in a space (an
119
+ * orbit camera, a 3D stage), where its items join that one and its camera.
61
120
  */
62
- export class ItemGrid extends Node2D {
121
+ export function render(ctx, s, arranger) {
122
+ // With no items there is nothing to see through a camera.
123
+ const focal = s.children.length === 0 ? 0 : focalOf(s, arrangementOf({ state: s, arranger, children: [], isSelf: () => false }, contentBox(ctx.size, s.padding)));
124
+ if (focal > 0 && !ctx.inSpace)
125
+ ctx.space(cameraAt(focal));
126
+ else
127
+ ctx.children();
128
+ }
129
+ /** A node's content box: its laid-out size inside its padding. */
130
+ export function contentBoxOf(grid) {
131
+ const b = grid.layoutBounds;
132
+ return contentBox({ width: b.width, height: b.height }, grid.effectivePadding());
133
+ }
134
+ /**
135
+ * Each item's slot in its arrangement as it stands, for a command being built,
136
+ * at the clock commands are built against (the tree stands at time 0 while a
137
+ * timeline compiles): what its stagger is ranked by.
138
+ */
139
+ export function itemSlots(timeline, s, children) {
140
+ const grid = commandGrid(timeline, s, children);
141
+ const sizes = children.map((child) => child.size);
142
+ return targetSlots(grid, children.length, sizes, contentBox(timeline.node.size, s.padding), 0).slots;
143
+ }
144
+ /** Each item's start in a stagger. A random order is seeded by the node's id, so it never changes between builds. */
145
+ export function ranks(timeline, s, children, order) {
146
+ return staggerRanks(order ?? "index", itemSlots(timeline, s, children), hashSeed(timeline.node.id));
147
+ }
148
+ /**
149
+ * Write `changes` to the arrangement and move each item from its slot in the
150
+ * one being left to its slot in the new one, staggered.
151
+ */
152
+ export function transitionTo(timeline, s, children, changes, stagger = TRANSITION_STAGGER) {
153
+ const from = snapshot(timeline, s, children);
154
+ const ease = timeline.easing ?? easeInOut("cubic");
155
+ const order = ranks(timeline, s, children, timeline.args.staggerOrder);
156
+ const spread = timeline.args.stagger ?? stagger;
157
+ return timeline.animate((t) => {
158
+ if (t >= 1)
159
+ return { ...changes, transition: null, transitionSource: "" };
160
+ const mix = order.map((rank) => ease(staggerProgress(rank, t, spread)));
161
+ return { ...changes, transition: { from, mix }, transitionSource: from.source };
162
+ }, { eased: false });
163
+ }
164
+ /** Emphasise the items `isFocus` picks and push the rest back. */
165
+ export function focusOn(timeline, s, isFocus, count, dim, grow) {
166
+ const from = s.focusState;
167
+ const target = Array.from({ length: count }, (_, i) => (isFocus(i) ? 1 : -1));
168
+ const ease = timeline.easing ?? easeInOut("cubic");
169
+ return timeline.animate((t) => {
170
+ const e = ease(t);
171
+ return {
172
+ focusState: {
173
+ dim: lerp(from?.dim ?? dim, dim, e),
174
+ grow: lerp(from?.grow ?? grow, grow, e),
175
+ values: target.map((v, i) => lerp(from?.values[i] ?? 0, v, e)),
176
+ },
177
+ };
178
+ }, { eased: false });
179
+ }
180
+ // What the commands are typed against: the shared state, and the padding every node has.
181
+ const GRID_STATE = { ...state, padding: Types.insets };
182
+ export const commands = commandsFor(GRID_STATE, {
63
183
  /**
64
184
  * Bring the items in, one after another. Every item starts hidden, so place
65
- * it where the grid enters — or after a {@link conceal}.
185
+ * it where the grid enters — or after a `conceal`.
66
186
  */
67
- reveal(args) {
68
- const style = args.data?.style ?? "scale";
69
- const ease = args.easing ?? (style === "scale" || style === "flip" ? easeOut("back") : easeOut("cubic"));
70
- const ranks = this._ranks(args.data?.staggerOrder);
71
- const stagger = args.data?.stagger ?? 0.5;
72
- return this.command((t) => {
73
- if (t >= 1)
74
- return { revealState: null };
75
- return { revealState: { style, values: ranks.map((rank) => ease(staggerProgress(rank, t, stagger))) } };
76
- }, args.duration);
77
- }
78
- /** Take the items out, one after another — {@link reveal} in reverse. They stay hidden after. */
79
- conceal(args) {
80
- const style = args.data?.style ?? "scale";
81
- const ease = args.easing ?? (style === "scale" || style === "flip" ? easeIn("back") : easeInOut("cubic"));
82
- const ranks = this._ranks(args.data?.staggerOrder);
83
- const stagger = args.data?.stagger ?? 0.5;
84
- const from = this.revealState?.values;
85
- return this.command((t) => ({
86
- revealState: {
87
- style,
88
- values: ranks.map((rank, i) => (from?.[i] ?? 1) * (1 - ease(staggerProgress(rank, t, stagger)))),
89
- },
90
- }), args.duration);
91
- }
92
- /** Send a ripple through the items that swells each in turn and settles back. */
93
- wave(args) {
94
- const style = args.data?.style ?? "scale";
95
- const amount = args.data?.amount ?? 0.3;
96
- const ease = args.easing ?? easeInOut("sine");
97
- const ranks = this._ranks(args.data?.staggerOrder);
98
- const stagger = args.data?.stagger ?? 0.7;
99
- return this.command((t) => {
100
- if (t >= 1)
101
- return { waveState: null };
102
- return {
103
- waveState: {
187
+ reveal: {
188
+ args: { style: Types.oneOf(REVEAL_STYLES, { default: "scale" }), ...staggerArgs(0.5) },
189
+ run: (timeline, s, children) => {
190
+ const style = timeline.args.style ?? "scale";
191
+ const ease = timeline.easing ?? (style === "scale" || style === "flip" ? easeOut("back") : easeOut("cubic"));
192
+ const order = ranks(timeline, s, children, timeline.args.staggerOrder);
193
+ const stagger = timeline.args.stagger ?? 0.5;
194
+ return timeline.animate((t) => {
195
+ if (t >= 1)
196
+ return { revealState: null };
197
+ return { revealState: { style, values: order.map((rank) => ease(staggerProgress(rank, t, stagger))) } };
198
+ }, { eased: false });
199
+ },
200
+ },
201
+ /** Take the items out, one after another — `reveal` in reverse. They stay hidden after. */
202
+ conceal: {
203
+ args: { style: Types.oneOf(REVEAL_STYLES, { default: "scale" }), ...staggerArgs(0.5) },
204
+ run: (timeline, s, children) => {
205
+ const style = timeline.args.style ?? "scale";
206
+ const ease = timeline.easing ?? (style === "scale" || style === "flip" ? easeIn("back") : easeInOut("cubic"));
207
+ const order = ranks(timeline, s, children, timeline.args.staggerOrder);
208
+ const stagger = timeline.args.stagger ?? 0.5;
209
+ const from = s.revealState?.values;
210
+ return timeline.animate((t) => ({
211
+ revealState: {
104
212
  style,
105
- amount,
106
- values: ranks.map((rank) => Math.sin(Math.PI * ease(staggerProgress(rank, t, stagger)))),
213
+ values: order.map((rank, i) => (from?.[i] ?? 1) * (1 - ease(staggerProgress(rank, t, stagger)))),
107
214
  },
108
- };
109
- }, args.duration);
110
- }
215
+ }), { eased: false });
216
+ },
217
+ },
218
+ /** Send a ripple through the items that swells each in turn and settles back. */
219
+ wave: {
220
+ args: {
221
+ style: Types.oneOf(WAVE_STYLES, { default: "scale" }),
222
+ amount: Types.number(0.3, { min: 0, max: 2, step: 0.05 }),
223
+ ...staggerArgs(0.7),
224
+ },
225
+ run: (timeline, s, children) => {
226
+ const style = timeline.args.style ?? "scale";
227
+ const amount = timeline.args.amount ?? 0.3;
228
+ const ease = timeline.easing ?? easeInOut("sine");
229
+ const order = ranks(timeline, s, children, timeline.args.staggerOrder);
230
+ const stagger = timeline.args.stagger ?? 0.7;
231
+ return timeline.animate((t) => {
232
+ if (t >= 1)
233
+ return { waveState: null };
234
+ return { waveState: { style, amount, values: order.map((rank) => Math.sin(Math.PI * ease(staggerProgress(rank, t, stagger)))) } };
235
+ }, { eased: false });
236
+ },
237
+ },
111
238
  /** Bring one item (by its place in the layers, from 0) forward and push the rest back. */
112
- focus(args) {
113
- const n = this._itemCount();
114
- const item = Math.round(args.data?.item ?? 0);
115
- return this._focusOn((i) => i === item, n, args);
116
- }
239
+ focus: {
240
+ args: {
241
+ item: Types.number(0, { min: 0, step: 1 }),
242
+ dim: Types.number(0.7, { min: 0, max: 1, step: 0.05 }),
243
+ grow: Types.number(0.15, { min: 0, max: 1, step: 0.05 }),
244
+ },
245
+ run: (timeline, s, children) => {
246
+ const item = Math.round(timeline.args.item ?? 0);
247
+ return focusOn(timeline, s, (i) => i === item, children.length, timeline.args.dim ?? 0.7, timeline.args.grow ?? 0.15);
248
+ },
249
+ },
117
250
  /** Bring every item back to even. */
118
- unfocus(args) {
119
- const from = this.focusState;
120
- const ease = args.easing ?? easeInOut("cubic");
121
- return this.command((t) => {
251
+ unfocus: (timeline, s) => {
252
+ const from = s.focusState;
253
+ const ease = timeline.easing ?? easeInOut("cubic");
254
+ return timeline.animate((t) => {
122
255
  if (t >= 1 || !from)
123
256
  return { focusState: null };
124
257
  const k = 1 - ease(t);
125
258
  return { focusState: { ...from, values: from.values.map((v) => v * k) } };
126
- }, args.duration);
127
- }
259
+ }, { eased: false });
260
+ },
128
261
  /** Gather the items toward the centre (`0`), back to the arrangement (`1`), or push them apart. */
129
- spreadTo(args) {
130
- return this.to({ data: { spread: args.data?.spread ?? 1 }, duration: args.duration, easing: args.easing });
131
- }
262
+ spreadTo: {
263
+ args: { spread: Types.number(1, { min: 0, max: 3, step: 0.05 }) },
264
+ run: (timeline) => timeline.tween({ spread: timeline.args.spread ?? 1 }),
265
+ },
132
266
  /** Send every item to a random slot. The same `seed` always deals the same way. */
133
- shuffle(args) {
134
- const current = normalizeOrder(this.order, this._itemCount());
135
- let next = shuffled(current, args.data?.seed ?? 1);
136
- // A shuffle that deals everything back where it was would look like nothing happened.
137
- if (next.length > 1 && next.every((item, k) => item === current[k]))
138
- next = [...next.slice(1), next[0]];
139
- return this._transitionTo({ order: next }, args);
140
- }
267
+ shuffle: {
268
+ args: { seed: Types.number(1, { min: 0, step: 1 }), ...staggerArgs(TRANSITION_STAGGER) },
269
+ run: (timeline, s, children) => {
270
+ const current = normalizeOrder(s.order, children.length);
271
+ let next = shuffled(current, timeline.args.seed ?? 1);
272
+ // A shuffle that deals everything back where it was would look like nothing happened.
273
+ if (next.length > 1 && next.every((item, k) => item === current[k]))
274
+ next = [...next.slice(1), next[0]];
275
+ return transitionTo(timeline, s, children, { order: next });
276
+ },
277
+ },
141
278
  /** Two items trade places, by their place in the layers. */
142
- swap(args) {
143
- const order = normalizeOrder(this.order, this._itemCount());
144
- const a = order.indexOf(Math.round(args.data?.first ?? 0));
145
- const b = order.indexOf(Math.round(args.data?.second ?? 1));
146
- if (a >= 0 && b >= 0)
147
- [order[a], order[b]] = [order[b], order[a]];
148
- return this._transitionTo({ order }, args, 0);
149
- }
279
+ swap: {
280
+ args: {
281
+ first: Types.number(0, { min: 0, step: 1 }),
282
+ second: Types.number(1, { min: 0, step: 1 }),
283
+ ...staggerArgs(0),
284
+ },
285
+ run: (timeline, s, children) => {
286
+ const order = normalizeOrder(s.order, children.length);
287
+ const a = order.indexOf(Math.round(timeline.args.first ?? 0));
288
+ const b = order.indexOf(Math.round(timeline.args.second ?? 1));
289
+ if (a >= 0 && b >= 0)
290
+ [order[a], order[b]] = [order[b], order[a]];
291
+ return transitionTo(timeline, s, children, { order }, 0);
292
+ },
293
+ },
150
294
  /** Move the items into a new order — a list of layer indices, first slot first, like `"2, 0, 1"`. */
151
- reorder(args) {
152
- const order = normalizeOrder(parseOrder(args.data?.order), this._itemCount());
153
- return this._transitionTo({ order }, args);
154
- }
295
+ reorder: {
296
+ args: { order: Types.text(""), ...staggerArgs(TRANSITION_STAGGER) },
297
+ run: (timeline, s, children) => {
298
+ const order = normalizeOrder(parseOrder(timeline.args.order), children.length);
299
+ return transitionTo(timeline, s, children, { order });
300
+ },
301
+ },
155
302
  /**
156
303
  * Move every item into another grid's arrangement — a grid into a ring, a ring
157
304
  * onto a sphere — and keep following it. An empty `target` morphs back to
158
305
  * this grid's own.
159
306
  */
160
- morphInto(args) {
161
- return this._transitionTo({ arrangeLike: args.data?.target ?? "" }, args);
162
- }
163
- // A grid's size is what its items come to. The base's rule — hug only when
164
- // built with children — would have a grid from a document, whose children
165
- // arrive after it, fill its parent instead.
166
- applyDefaultSize(props) {
167
- if (!props || props.width === undefined)
168
- this.applyProp("width", "hug", { tween: sizeInputOps.lerp });
169
- if (!props || props.height === undefined)
170
- this.applyProp("height", "hug", { tween: sizeInputOps.lerp });
171
- }
172
- /** The room each item is measured in — the whole content box, unless an arrangement has cells. */
173
- itemSpace(_count, space, _sizes) {
174
- return space;
175
- }
176
- measure(constraints, scope) {
177
- this.lastMeasure = { constraints, measurer: scope };
178
- const padding = this.effectivePadding();
179
- const items = this.flowChildren();
180
- if (items.length === 0) {
181
- this._measured = null;
182
- return flowOps.size(this, constraints, padding, NO_CONTENT);
183
- }
184
- const space = flowOps.contentBox(this, constraints, padding);
185
- const sizes = this._measureItems(items, space, scope);
186
- this._measured = { space, sizes };
187
- return flowOps.size(this, constraints, padding, extentOf(this._targetSlots(items.length, sizes, space).slots, sizes, this._focal));
188
- }
189
- layoutChildren(rect, scope) {
190
- this._placements.clear();
191
- const items = this.flowChildren();
192
- if (items.length === 0) {
193
- this._slots = [];
194
- this._sizes = [];
195
- this._focal = 0;
196
- return;
197
- }
198
- const inner = insetsOps.insetBounds(rect.width, rect.height, this.effectivePadding());
199
- const measured = this._measured;
200
- this._measured = null;
201
- const sizes = measured && measured.sizes.length === items.length && sameSize(measured.space, inner)
202
- ? measured.sizes
203
- : this._measureItems(items, inner, scope);
204
- const slots = this._resolve(sizes, inner);
205
- this._slots = slots;
206
- this._sizes = sizes;
207
- for (let i = 0; i < items.length; i++) {
208
- const s = slots[i];
209
- items[i].layout({ x: inner.x + s.x, y: inner.y - s.y, width: sizes[i].width, height: sizes[i].height }, scope);
210
- if (s.rotation !== 0 || s.scale !== 1 || s.rotationX !== 0 || s.rotationY !== 0 || s.depth !== 0 || s.opacity !== 1) {
211
- this._placements.set(items[i], {
212
- rotation: s.rotation,
213
- scale: s.scale,
214
- rotationX: s.rotationX,
215
- rotationY: s.rotationY,
216
- depth: s.depth,
217
- opacity: s.opacity,
218
- });
219
- }
307
+ morphInto: {
308
+ args: { target: nodeRefArg(ITEM_GRID_TYPES), ...staggerArgs(TRANSITION_STAGGER) },
309
+ run: (timeline, s, children) => transitionTo(timeline, s, children, { arrangeLike: timeline.args.target ?? "" }),
310
+ },
311
+ });
312
+ function arrangerOf(key) {
313
+ const grid = GRIDS.get(key);
314
+ if (!grid)
315
+ throw new Error(`"${key}" is no grid: pass its component through ItemGrid.define.`);
316
+ return grid.arranger;
317
+ }
318
+ /** A grid `view` names, when it is one this kit can arrange and not the reader itself. */
319
+ function gridIn(view, grid) {
320
+ if (!view)
321
+ return null;
322
+ const found = GRIDS.get(view.key);
323
+ if (!found || grid.isSelf(view))
324
+ return null;
325
+ return { view, arranger: found.arranger };
326
+ }
327
+ /** In layout, a view is this grid when it holds the same children: a child has one parent. */
328
+ function sharesChildren(children) {
329
+ return (view) => {
330
+ if (children.length === 0)
331
+ return false;
332
+ const theirs = view.state.children;
333
+ return theirs?.[0] === children[0];
334
+ };
335
+ }
336
+ function commandGrid(timeline, s, children) {
337
+ return { state: s, arranger: arrangerOf(timeline.node.key), children, isSelf: (view) => view.id === timeline.node.id };
338
+ }
339
+ function snapshot(timeline, s, children) {
340
+ const grid = commandGrid(timeline, s, children);
341
+ const source = gridIn(s.arrangeLike, grid);
342
+ return {
343
+ arrangement: source
344
+ ? source.arranger.arrangement(source.view.state, viewBox(source.view))
345
+ : grid.arranger.arrangement(s, contentBox(timeline.node.size, s.padding)),
346
+ source: source?.view.id ?? "",
347
+ order: normalizeOrder(s.order, children.length),
348
+ spread: s.spread,
349
+ };
350
+ }
351
+ function measureItems(children, space, s, arranger) {
352
+ const sizes = children.map((child) => child.measure(space));
353
+ const cell = arranger.itemSpace ? arranger.itemSpace(s, children.length, space) : space;
354
+ if (cell === space)
355
+ return sizes;
356
+ return children.map((child) => child.measure(cell));
357
+ }
358
+ /** The arrangement the grid is heading for: the one it borrows, or its own. */
359
+ function arrangementOf(grid, box) {
360
+ const source = gridIn(grid.state.arrangeLike, grid);
361
+ return source ? source.arranger.arrangement(source.view.state, viewBox(source.view)) : grid.arranger.arrangement(grid.state, box);
362
+ }
363
+ /** Where each item sits in the arrangement it is heading for, before any transition or effect. */
364
+ function targetSlots(grid, count, sizes, box, time) {
365
+ const s = grid.state;
366
+ const source = gridIn(s.arrangeLike, grid);
367
+ const sourceBox = source ? viewBox(source.view) : box;
368
+ const arrangement = source ? source.arranger.arrangement(source.view.state, sourceBox) : grid.arranger.arrangement(s, box);
369
+ let slots = slotsOf(arrangement, { count, sizes, box: sourceBox, time });
370
+ if (source)
371
+ slots = inFrame(source.view.frame, slots);
372
+ const slotOf = slotOfItem(normalizeOrder(s.order, count));
373
+ return { slots: slotOf.map((k) => spreadSlot(slots[k], s.spread)), arrangement };
374
+ }
375
+ /** Each item's final slot this frame: the target, blended from any transition, with its effects on. */
376
+ function resolve(grid, sizes, box, time) {
377
+ const s = grid.state;
378
+ const count = sizes.length;
379
+ const slots = targetSlots(grid, count, sizes, box, time).slots;
380
+ const transition = s.transition;
381
+ if (transition) {
382
+ const { from } = transition;
383
+ const named = from.source ? s.transitionSource : null;
384
+ const source = named && named.id === from.source && GRIDS.has(named.key) ? named : null;
385
+ const self = source !== null && grid.isSelf(source);
386
+ const fromBox = source && !self ? viewBox(source) : box;
387
+ let leaving = slotsOf(from.arrangement, { count, sizes, box: fromBox, time });
388
+ if (source && !self)
389
+ leaving = inFrame(source.frame, leaving);
390
+ const slotOf = slotOfItem(normalizeOrder(from.order, count));
391
+ for (let i = 0; i < count; i++) {
392
+ slots[i] = blendSlot(spreadSlot(leaving[slotOf[i]], from.spread), slots[i], transition.mix[i] ?? 1);
220
393
  }
221
394
  }
222
- _placementOf(child) {
223
- return this._placements.get(child) ?? null;
224
- }
225
- /**
226
- * A grid whose arrangement has depth is seen through a camera of its own,
227
- * which paints its items far to near — unless it already sits in a space (an
228
- * orbit camera, a 3D stage), where its items join that one and its camera.
229
- */
230
- _spaceCamera() {
231
- const focal = this._focal;
232
- if (focal <= 0 || this._inSpace())
233
- return null;
234
- return {
235
- perspective: focal,
236
- orbit: 0,
237
- elevation: 0,
238
- dolly: 0,
239
- lookAt: CENTRE,
240
- zoom: 1,
241
- heading: 0,
242
- viewportX: 0,
243
- viewportY: 0,
244
- };
245
- }
246
- _placements = new Map();
247
- /** What {@link measure} found, for {@link layoutChildren} to reuse in the same pass. */
248
- _measured = null;
249
- /** Each item's slot from the last layout — what a command ranks its stagger by. */
250
- _slots = [];
251
- /** Each item's size from the last layout. */
252
- _sizes = [];
253
- /** The last layout's camera distance, `0` while flat. */
254
- _focal = 0;
255
- _borrowCache = null;
256
- /** How many items a command built now will act on. */
257
- _itemCount() {
258
- return this.flowChildren().length;
259
- }
260
- /** Each item's slot as it stands, for a command being built — the last layout's where it still fits. */
261
- _itemSlots() {
262
- const n = this._itemCount();
263
- if (this._slots.length === n)
264
- return this._slots;
265
- const sizes = new Array(n).fill({ width: 100, height: 100 });
266
- return this._targetSlots(n, sizes, contentBoxOf(this)).slots;
267
- }
268
- /** The item sizes the last layout found. */
269
- _itemSizes() {
270
- return this._sizes.length === this._itemCount() ? this._sizes : [];
395
+ const reveal = s.revealState;
396
+ const wave = s.waveState;
397
+ const focus = s.focusState;
398
+ if (reveal || wave || focus) {
399
+ for (let i = 0; i < count; i++)
400
+ applyItemEffects(slots[i], i, sizes[i], reveal, wave, focus);
271
401
  }
272
- /** Each item's start in a stagger. A random order is seeded by the node's id, so it never changes between builds. */
273
- _ranks(order) {
274
- return staggerRanks(order ?? "index", this._itemSlots(), hashSeed(this.id));
275
- }
276
- /**
277
- * Write `changes` to the arrangement and move each item from its slot in the
278
- * one being left to its slot in the new one, staggered.
279
- */
280
- _transitionTo(changes, args, stagger = TRANSITION_STAGGER) {
281
- const from = this._snapshot();
282
- const ease = args.easing ?? easeInOut("cubic");
283
- const ranks = this._ranks(args.data?.staggerOrder);
284
- const spread = args.data?.stagger ?? stagger;
285
- return this.command((t) => {
286
- if (t >= 1)
287
- return { ...changes, transition: null };
288
- const mix = ranks.map((rank) => ease(staggerProgress(rank, t, spread)));
289
- return { ...changes, transition: { from, mix } };
290
- }, args.duration);
291
- }
292
- /** Emphasise the items `isFocus` picks and push the rest back. */
293
- _focusOn(isFocus, count, args) {
294
- const from = this.focusState;
295
- const dim = args.data?.dim ?? 0.7;
296
- const grow = args.data?.grow ?? 0.15;
297
- const target = Array.from({ length: count }, (_, i) => (isFocus(i) ? 1 : -1));
298
- const ease = args.easing ?? easeInOut("cubic");
299
- return this.command((t) => {
300
- const e = ease(t);
301
- return {
302
- focusState: {
303
- dim: lerp(from?.dim ?? dim, dim, e),
304
- grow: lerp(from?.grow ?? grow, grow, e),
305
- values: target.map((v, i) => lerp(from?.values[i] ?? 0, v, e)),
306
- },
307
- };
308
- }, args.duration);
309
- }
310
- _snapshot() {
311
- const source = this._borrowed();
402
+ return slots;
403
+ }
404
+ /** The camera distance the grid is seen through: the target's, or, while a morph runs, the one it is leaving. */
405
+ function focalOf(s, target) {
406
+ // A morph into or out of depth keeps the camera for its whole length.
407
+ return focalLengthOf(target) || (s.transition ? focalLengthOf(s.transition.from.arrangement) : 0);
408
+ }
409
+ function cameraAt(focal) {
410
+ return {
411
+ perspective: focal,
412
+ orbit: 0,
413
+ elevation: 0,
414
+ dolly: 0,
415
+ lookAt: CENTRE,
416
+ zoom: 1,
417
+ heading: 0,
418
+ viewportX: 0,
419
+ viewportY: 0,
420
+ };
421
+ }
422
+ /** A slot as a child's placement: its box centred on the slot, and the slot's transform when it has one. */
423
+ function placement(inner, s, size) {
424
+ const box = { x: inner.x + s.x, y: s.y - inner.y, width: size.width, height: size.height };
425
+ if (s.rotation === 0 && s.scale === 1 && s.rotationX === 0 && s.rotationY === 0 && s.depth === 0 && s.opacity === 1)
426
+ return box;
427
+ return {
428
+ ...box,
429
+ rotation: s.rotation,
430
+ scale: s.scale,
431
+ rotationX: s.rotationX,
432
+ rotationY: s.rotationY,
433
+ depth: s.depth,
434
+ opacity: s.opacity,
435
+ };
436
+ }
437
+ /** `slots`, laid out in another grid's frame, moved into this grid's. */
438
+ function inFrame(frame, slots) {
439
+ return slots.map((s) => {
440
+ const p = rotateClockwise(s.x * frame.scale, s.y * frame.scale, frame.rotation);
312
441
  return {
313
- arrangement: source ? source.arrangementFor(contentBoxOf(source)) : this.arrangementFor(contentBoxOf(this)),
314
- source: source?.id ?? "",
315
- order: normalizeOrder(this.order, this._itemCount()),
316
- spread: this.spread,
442
+ ...s,
443
+ x: frame.x + p.x,
444
+ y: frame.y + p.y,
445
+ depth: s.depth * frame.scale,
446
+ rotation: s.rotation + frame.rotation,
447
+ scale: s.scale * frame.scale,
317
448
  };
318
- }
319
- _measureItems(items, space, scope) {
320
- const measured = flowOps.measure({}, items, space, scope).sizes;
321
- const cell = this.itemSpace(items.length, space, measured);
322
- if (cell === space)
323
- return [...measured];
324
- return [...flowOps.measure({}, items, cell, scope).sizes];
325
- }
326
- /** Where each item sits in the arrangement it is heading for, before any transition or effect. */
327
- _targetSlots(count, sizes, box) {
328
- const source = this._borrowed();
329
- const arrangement = source ? source.arrangementFor(contentBoxOf(source)) : this.arrangementFor(box);
330
- let slots = slotsOf(arrangement, { count, sizes, box: source ? contentBoxOf(source) : box, time: this.time.total });
331
- if (source)
332
- slots = this._fromGrid(source, slots);
333
- const slotOf = slotOfItem(normalizeOrder(this.order, count));
334
- return { slots: slotOf.map((k) => spreadSlot(slots[k], this.spread)), arrangement };
335
- }
336
- /** Each item's final slot this frame: the target, blended from any transition, with its effects on. */
337
- _resolve(sizes, box) {
338
- const count = sizes.length;
339
- const target = this._targetSlots(count, sizes, box);
340
- const slots = target.slots;
341
- let focal = focalLengthOf(target.arrangement);
342
- const transition = this.transition;
343
- if (transition) {
344
- const { from } = transition;
345
- const source = from.source ? this._gridById(from.source) : null;
346
- const fromBox = source ? contentBoxOf(source) : box;
347
- let leaving = slotsOf(from.arrangement, { count, sizes, box: fromBox, time: this.time.total });
348
- if (source && source !== this)
349
- leaving = this._fromGrid(source, leaving);
350
- const slotOf = slotOfItem(normalizeOrder(from.order, count));
351
- for (let i = 0; i < count; i++) {
352
- slots[i] = blendSlot(spreadSlot(leaving[slotOf[i]], from.spread), slots[i], transition.mix[i] ?? 1);
353
- }
354
- // A morph into or out of depth keeps the camera for its whole length.
355
- focal = focal || focalLengthOf(from.arrangement);
356
- }
357
- this._focal = focal;
358
- const reveal = this.revealState;
359
- const wave = this.waveState;
360
- const focus = this.focusState;
361
- if (reveal || wave || focus) {
362
- for (let i = 0; i < count; i++)
363
- applyItemEffects(slots[i], i, sizes[i], reveal, wave, focus);
364
- }
365
- return slots;
366
- }
367
- /** Whether an ancestor places this grid in a space, as `spaceFrameOf` walks it. */
368
- _inSpace() {
369
- for (let n = this.parent; n; n = n.parent) {
370
- if (n._spaceCamera())
371
- return true;
372
- if (!n._rendersChildrenInSpace())
373
- return false;
374
- }
375
- return false;
376
- }
377
- /** The grid {@link arrangeLike} names, when it names another grid in this tree. */
378
- _borrowed() {
379
- const id = this.arrangeLike;
380
- if (!id || id === this.id)
381
- return null;
382
- return this._gridById(id);
383
- }
384
- _gridById(id) {
385
- if (id === this.id)
386
- return this;
387
- const cached = this._borrowCache;
388
- if (cached && cached.id === id && cached.grid.mounted)
389
- return cached.grid;
390
- const found = findNode2D(this, id);
391
- const grid = found instanceof ItemGrid ? found : null;
392
- this._borrowCache = grid ? { id, grid } : null;
393
- return grid;
394
- }
395
- /** `slots`, laid out in `source`'s frame, moved into this grid's. */
396
- _fromGrid(source, slots) {
397
- const frame = frameIn(this, source);
398
- return slots.map((s) => {
399
- const p = rotateClockwise(s.x * frame.scale, s.y * frame.scale, frame.rotation);
400
- return {
401
- ...s,
402
- x: frame.x + p.x,
403
- y: frame.y + p.y,
404
- depth: s.depth * frame.scale,
405
- rotation: s.rotation + frame.rotation,
406
- scale: s.scale * frame.scale,
407
- };
408
- });
409
- }
449
+ });
410
450
  }
411
- __decorate([
412
- property({ default: [], kind: "order", animatable: false })
413
- ], ItemGrid.prototype, "order", void 0);
414
- __decorate([
415
- property({ default: 1, min: 0, max: 3, step: 0.05 })
416
- ], ItemGrid.prototype, "spread", void 0);
417
- __decorate([
418
- property({ default: "", kind: "nodeRef", nodeTypes: ITEM_GRID_TYPES, animatable: false })
419
- ], ItemGrid.prototype, "arrangeLike", void 0);
420
- __decorate([
421
- property({ default: undefined, kind: "state", animatable: false })
422
- ], ItemGrid.prototype, "revealState", void 0);
423
- __decorate([
424
- property({ default: undefined, kind: "state", animatable: false })
425
- ], ItemGrid.prototype, "waveState", void 0);
426
- __decorate([
427
- property({ default: undefined, kind: "state", animatable: false })
428
- ], ItemGrid.prototype, "focusState", void 0);
429
- __decorate([
430
- property({ default: undefined, kind: "state", animatable: false })
431
- ], ItemGrid.prototype, "transition", void 0);
432
- __decorate([
433
- command({ args: [{ key: "style", kind: "enum", options: REVEAL_STYLES, default: "scale" }, ...staggerArgs(0.5)] })
434
- ], ItemGrid.prototype, "reveal", null);
435
- __decorate([
436
- command({ args: [{ key: "style", kind: "enum", options: REVEAL_STYLES, default: "scale" }, ...staggerArgs(0.5)] })
437
- ], ItemGrid.prototype, "conceal", null);
438
- __decorate([
439
- command({
440
- args: [
441
- { key: "style", kind: "enum", options: WAVE_STYLES, default: "scale" },
442
- { key: "amount", min: 0, max: 2, step: 0.05, default: 0.3 },
443
- ...staggerArgs(0.7),
444
- ],
445
- })
446
- ], ItemGrid.prototype, "wave", null);
447
- __decorate([
448
- command({
449
- args: [
450
- { key: "item", min: 0, step: 1, default: 0 },
451
- { key: "dim", min: 0, max: 1, step: 0.05, default: 0.7 },
452
- { key: "grow", min: 0, max: 1, step: 0.05, default: 0.15 },
453
- ],
454
- })
455
- ], ItemGrid.prototype, "focus", null);
456
- __decorate([
457
- command()
458
- ], ItemGrid.prototype, "unfocus", null);
459
- __decorate([
460
- command({ args: [{ key: "spread", min: 0, max: 3, step: 0.05, default: 1 }] })
461
- ], ItemGrid.prototype, "spreadTo", null);
462
- __decorate([
463
- command({ args: [{ key: "seed", min: 0, step: 1, default: 1 }, ...staggerArgs(TRANSITION_STAGGER)] })
464
- ], ItemGrid.prototype, "shuffle", null);
465
- __decorate([
466
- command({
467
- args: [
468
- { key: "first", min: 0, step: 1, default: 0 },
469
- { key: "second", min: 0, step: 1, default: 1 },
470
- ...staggerArgs(0),
471
- ],
472
- })
473
- ], ItemGrid.prototype, "swap", null);
474
- __decorate([
475
- command({ args: [{ key: "order", kind: "text", default: "" }, ...staggerArgs(TRANSITION_STAGGER)] })
476
- ], ItemGrid.prototype, "reorder", null);
477
- __decorate([
478
- command({ args: [{ key: "target", kind: "nodeRef", nodeTypes: ITEM_GRID_TYPES }, ...staggerArgs(TRANSITION_STAGGER)] })
479
- ], ItemGrid.prototype, "morphInto", null);
480
- /** A node's content box: its laid-out size inside its padding. */
481
- export function contentBoxOf(grid) {
482
- const b = grid.layoutBounds;
483
- const inner = insetsOps.insetBounds(b.width, b.height, grid.effectivePadding());
451
+ function viewBox(view) {
452
+ return contentBox(view.size, view.state.padding);
453
+ }
454
+ /** A box of `size` inside `padding`: what a command reads as its grid's content box from `timeline.node.size`. */
455
+ export function contentBox(size, padding) {
456
+ const inner = insetsOps.insetBounds(size.width, size.height, padding);
484
457
  return { width: inner.width, height: inner.height };
485
458
  }
486
459
  /** The box, centred on the grid, that holds every item where its slot puts it. */