@motionscript/grids 0.1.0-alpha.7 → 0.1.0-alpha.9

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 (63) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/LICENSE +201 -201
  3. package/NOTICE +49 -48
  4. package/README.md +118 -118
  5. package/dist/browser/chunks/{chunk-J62D7EFZ.js → chunk-TYNFMZ3D.js} +1 -1
  6. package/dist/browser/chunks/chunk-TYNFMZ3D.js.map +7 -0
  7. package/dist/browser/index.js +1 -1
  8. package/dist/browser/index.js.map +1 -1
  9. package/dist/browser/kit.js +1 -1
  10. package/dist/browser/manifest.json +1 -1
  11. package/dist/engine.d.ts +1 -1
  12. package/dist/engine.js +1 -1
  13. package/dist/engine.js.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/kit/index.js.map +1 -1
  16. package/dist/kit/item-effects.js.map +1 -1
  17. package/dist/kit/item-grid.js.map +1 -1
  18. package/dist/kit/order.js.map +1 -1
  19. package/dist/kit/random.js.map +1 -1
  20. package/dist/kit/slots/index.js.map +1 -1
  21. package/dist/kit/slots/path-templates.js.map +1 -1
  22. package/dist/kit/slots/path.js.map +1 -1
  23. package/dist/kit/slots/radial.js.map +1 -1
  24. package/dist/kit/slots/rectangular.js.map +1 -1
  25. package/dist/kit/slots/slot.js.map +1 -1
  26. package/dist/kit/slots/sphere.js.map +1 -1
  27. package/dist/kit/stagger.js.map +1 -1
  28. package/dist/nodes.js.map +1 -1
  29. package/dist/path-grid/index.js.map +1 -1
  30. package/dist/path-grid/path-grid.js.map +1 -1
  31. package/dist/radial-grid/index.js.map +1 -1
  32. package/dist/radial-grid/radial-grid.js.map +1 -1
  33. package/dist/rectangular-grid/index.js.map +1 -1
  34. package/dist/rectangular-grid/rectangular-grid.js.map +1 -1
  35. package/dist/sphere-grid/index.js.map +1 -1
  36. package/dist/sphere-grid/sphere-grid.js.map +1 -1
  37. package/package.json +5 -4
  38. package/registry.json +32 -32
  39. package/src/engine.ts +12 -12
  40. package/src/index.ts +6 -6
  41. package/src/kit/index.ts +18 -18
  42. package/src/kit/item-effects.ts +114 -114
  43. package/src/kit/item-grid.ts +648 -648
  44. package/src/kit/order.ts +41 -41
  45. package/src/kit/random.ts +29 -29
  46. package/src/kit/slots/index.ts +39 -39
  47. package/src/kit/slots/path-templates.ts +120 -120
  48. package/src/kit/slots/path.ts +120 -120
  49. package/src/kit/slots/radial.ts +62 -62
  50. package/src/kit/slots/rectangular.ts +108 -108
  51. package/src/kit/slots/slot.ts +82 -82
  52. package/src/kit/slots/sphere.ts +94 -94
  53. package/src/kit/stagger.ts +96 -96
  54. package/src/nodes.ts +30 -30
  55. package/src/path-grid/index.ts +1 -1
  56. package/src/path-grid/path-grid.ts +186 -186
  57. package/src/radial-grid/index.ts +1 -1
  58. package/src/radial-grid/radial-grid.ts +81 -81
  59. package/src/rectangular-grid/index.ts +1 -1
  60. package/src/rectangular-grid/rectangular-grid.ts +145 -145
  61. package/src/sphere-grid/index.ts +1 -1
  62. package/src/sphere-grid/sphere-grid.ts +129 -129
  63. package/dist/browser/chunks/chunk-J62D7EFZ.js.map +0 -7
@@ -1,82 +1,82 @@
1
- import type { Size2D } from "@motionscript/sdk"
2
-
3
- /**
4
- * One place in an arrangement, in the grid's own frame: centred on it, y-up,
5
- * `depth` toward the viewer.
6
- *
7
- * Pure data, and the whole of what an arrangement produces — which is what lets
8
- * a morph blend any two of them item by item, whatever shapes they came from.
9
- */
10
- export interface Slot {
11
- x: number
12
- y: number
13
- depth: number
14
- /** Degrees clockwise. */
15
- rotation: number
16
- /** Tilt, degrees, as a node's own `rotationX`/`rotationY`. */
17
- rotationX: number
18
- rotationY: number
19
- scale: number
20
- opacity: number
21
- meta: SlotMeta
22
- }
23
-
24
- /** What kind of place a slot is — the orderings a stagger can follow. */
25
- export type SlotMeta =
26
- | { kind: "rectangular"; row: number; column: number }
27
- /** Degrees clockwise from the arrangement's first slot. */
28
- | { kind: "radial"; angle: number }
29
- /** `0..1` along the part of the path in use. */
30
- | { kind: "path"; t: number }
31
- /** `depth` is `-1` at the back of the sphere and `1` at the front. */
32
- | { kind: "sphere"; lat: number; lon: number; depth: number }
33
-
34
- /** What an arrangement is laid out for. */
35
- export interface SlotInput {
36
- count: number
37
- /** Each item's measured size, in item order. */
38
- sizes: readonly Size2D[]
39
- /** The grid's content box. */
40
- box: Size2D
41
- /** Clip seconds, for arrangements that move on their own. */
42
- time: number
43
- }
44
-
45
- export function slot(x: number, y: number, meta: SlotMeta): Slot {
46
- return { x, y, depth: 0, rotation: 0, rotationX: 0, rotationY: 0, scale: 1, opacity: 1, meta }
47
- }
48
-
49
- /** `a` toward `b`. The meta switches halfway, since an ordering cannot be half one thing. */
50
- export function blendSlot(a: Slot, b: Slot, t: number): Slot {
51
- if (t <= 0) return a
52
- if (t >= 1) return b
53
- const mix = (p: number, q: number) => p + (q - p) * t
54
- return {
55
- x: mix(a.x, b.x),
56
- y: mix(a.y, b.y),
57
- depth: mix(a.depth, b.depth),
58
- rotation: mix(a.rotation, b.rotation),
59
- rotationX: mix(a.rotationX, b.rotationX),
60
- rotationY: mix(a.rotationY, b.rotationY),
61
- scale: mix(a.scale, b.scale),
62
- opacity: mix(a.opacity, b.opacity),
63
- meta: t < 0.5 ? a.meta : b.meta,
64
- }
65
- }
66
-
67
- /** Push `s` away from (or pull it toward) the grid's centre. */
68
- export function spreadSlot(s: Slot, spread: number): Slot {
69
- if (spread === 1) return s
70
- return { ...s, x: s.x * spread, y: s.y * spread, depth: s.depth * spread }
71
- }
72
-
73
- /** Largest width and height among `sizes`. */
74
- export function largest(sizes: readonly Size2D[]): Size2D {
75
- let width = 0
76
- let height = 0
77
- for (const size of sizes) {
78
- if (size.width > width) width = size.width
79
- if (size.height > height) height = size.height
80
- }
81
- return { width, height }
82
- }
1
+ import type { Size2D } from "@motionscript/sdk"
2
+
3
+ /**
4
+ * One place in an arrangement, in the grid's own frame: centred on it, y-up,
5
+ * `depth` toward the viewer.
6
+ *
7
+ * Pure data, and the whole of what an arrangement produces — which is what lets
8
+ * a morph blend any two of them item by item, whatever shapes they came from.
9
+ */
10
+ export interface Slot {
11
+ x: number
12
+ y: number
13
+ depth: number
14
+ /** Degrees clockwise. */
15
+ rotation: number
16
+ /** Tilt, degrees, as a node's own `rotationX`/`rotationY`. */
17
+ rotationX: number
18
+ rotationY: number
19
+ scale: number
20
+ opacity: number
21
+ meta: SlotMeta
22
+ }
23
+
24
+ /** What kind of place a slot is — the orderings a stagger can follow. */
25
+ export type SlotMeta =
26
+ | { kind: "rectangular"; row: number; column: number }
27
+ /** Degrees clockwise from the arrangement's first slot. */
28
+ | { kind: "radial"; angle: number }
29
+ /** `0..1` along the part of the path in use. */
30
+ | { kind: "path"; t: number }
31
+ /** `depth` is `-1` at the back of the sphere and `1` at the front. */
32
+ | { kind: "sphere"; lat: number; lon: number; depth: number }
33
+
34
+ /** What an arrangement is laid out for. */
35
+ export interface SlotInput {
36
+ count: number
37
+ /** Each item's measured size, in item order. */
38
+ sizes: readonly Size2D[]
39
+ /** The grid's content box. */
40
+ box: Size2D
41
+ /** Clip seconds, for arrangements that move on their own. */
42
+ time: number
43
+ }
44
+
45
+ export function slot(x: number, y: number, meta: SlotMeta): Slot {
46
+ return { x, y, depth: 0, rotation: 0, rotationX: 0, rotationY: 0, scale: 1, opacity: 1, meta }
47
+ }
48
+
49
+ /** `a` toward `b`. The meta switches halfway, since an ordering cannot be half one thing. */
50
+ export function blendSlot(a: Slot, b: Slot, t: number): Slot {
51
+ if (t <= 0) return a
52
+ if (t >= 1) return b
53
+ const mix = (p: number, q: number) => p + (q - p) * t
54
+ return {
55
+ x: mix(a.x, b.x),
56
+ y: mix(a.y, b.y),
57
+ depth: mix(a.depth, b.depth),
58
+ rotation: mix(a.rotation, b.rotation),
59
+ rotationX: mix(a.rotationX, b.rotationX),
60
+ rotationY: mix(a.rotationY, b.rotationY),
61
+ scale: mix(a.scale, b.scale),
62
+ opacity: mix(a.opacity, b.opacity),
63
+ meta: t < 0.5 ? a.meta : b.meta,
64
+ }
65
+ }
66
+
67
+ /** Push `s` away from (or pull it toward) the grid's centre. */
68
+ export function spreadSlot(s: Slot, spread: number): Slot {
69
+ if (spread === 1) return s
70
+ return { ...s, x: s.x * spread, y: s.y * spread, depth: s.depth * spread }
71
+ }
72
+
73
+ /** Largest width and height among `sizes`. */
74
+ export function largest(sizes: readonly Size2D[]): Size2D {
75
+ let width = 0
76
+ let height = 0
77
+ for (const size of sizes) {
78
+ if (size.width > width) width = size.width
79
+ if (size.height > height) height = size.height
80
+ }
81
+ return { width, height }
82
+ }
@@ -1,94 +1,94 @@
1
- import { slot, type Slot, type SlotInput } from "./slot"
2
-
3
- /** `outward` lays each item on the sphere's surface; `billboard` keeps every item facing the viewer. */
4
- export const SPHERE_FACINGS = ["outward", "billboard"] as const
5
- export type SphereFacing = (typeof SPHERE_FACINGS)[number]
6
-
7
- export const SPHERE_AXES = ["x", "y", "z"] as const
8
- export type SphereAxis = (typeof SPHERE_AXES)[number]
9
-
10
- export interface SphereArrangement {
11
- kind: "sphere"
12
- radius: number
13
- /** Turn about the vertical axis, degrees — positive carries the front to the right. */
14
- yaw: number
15
- /** Turn about the horizontal axis, degrees — positive brings the top toward the viewer. */
16
- pitch: number
17
- /** Turn about the view axis, degrees clockwise. */
18
- roll: number
19
- facing: SphereFacing
20
- /** The camera's distance from the sphere's centre plane, px — smaller is more dramatic. */
21
- focalLength: number
22
- /** How much the back half fades, `0..1`. */
23
- depthFade: number
24
- /** Hide the items whose faces point away from the viewer. */
25
- cullBackfaces: boolean
26
- /** Degrees per second of yaw the sphere turns on its own. */
27
- spinSpeed: number
28
- }
29
-
30
- const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5))
31
- const DEG = 180 / Math.PI
32
-
33
- /** Point `k` of `count` on the unit sphere — a Fibonacci lattice, as even as `count` points get. */
34
- export function fibonacciPoint(k: number, count: number): [number, number, number] {
35
- if (count <= 1) return [0, 0, 1]
36
- const y = 1 - (2 * (k + 0.5)) / count
37
- const r = Math.sqrt(Math.max(0, 1 - y * y))
38
- const theta = k * GOLDEN_ANGLE
39
- return [Math.cos(theta) * r, y, Math.sin(theta) * r]
40
- }
41
-
42
- /** `p` turned by yaw, then pitch, then roll — y-up, z toward the viewer. */
43
- export function orient(p: readonly [number, number, number], yaw: number, pitch: number, roll: number): [number, number, number] {
44
- const [x0, y0, z0] = p
45
- const ya = yaw / DEG, pa = pitch / DEG, ra = roll / DEG
46
- const x1 = x0 * Math.cos(ya) + z0 * Math.sin(ya)
47
- const z1 = -x0 * Math.sin(ya) + z0 * Math.cos(ya)
48
- const y2 = y0 * Math.cos(pa) - z1 * Math.sin(pa)
49
- const z2 = y0 * Math.sin(pa) + z1 * Math.cos(pa)
50
- return [x1 * Math.cos(ra) + y2 * Math.sin(ra), -x1 * Math.sin(ra) + y2 * Math.cos(ra), z2]
51
- }
52
-
53
- export function sphereSlots(arrangement: SphereArrangement, input: SlotInput): Slot[] {
54
- const { count } = input
55
- const radius = arrangement.radius
56
- const yaw = arrangement.yaw + arrangement.spinSpeed * input.time
57
- const outward = arrangement.facing === "outward"
58
- const slots: Slot[] = []
59
- for (let k = 0; k < count; k++) {
60
- const q = fibonacciPoint(k, count)
61
- const [nx, ny, nz] = orient(q, yaw, arrangement.pitch, arrangement.roll)
62
- const s = slot(nx * radius, ny * radius, {
63
- kind: "sphere",
64
- lat: Math.asin(Math.max(-1, Math.min(1, q[1]))) * DEG,
65
- lon: Math.atan2(q[2], q[0]) * DEG,
66
- depth: nz,
67
- })
68
- s.depth = nz * radius
69
- if (outward) {
70
- // The tilt whose `Rx · Ry` turns a face's normal onto the surface normal.
71
- s.rotationY = Math.asin(Math.max(-1, Math.min(1, nx))) * DEG
72
- s.rotationX = Math.atan2(ny, nz) * DEG
73
- }
74
- s.opacity = 1 - arrangement.depthFade * (1 - (nz + 1) / 2)
75
- if (outward && arrangement.cullBackfaces && facesAway(s, [nx, ny, nz], arrangement.focalLength)) s.opacity = 0
76
- slots.push(s)
77
- }
78
- return slots
79
- }
80
-
81
- /** Whether an outward face at `s` shows its back to a viewer `focal` px in front of the centre. */
82
- function facesAway(s: Slot, normal: readonly [number, number, number], focal: number): boolean {
83
- if (focal <= 0) return normal[2] < 0
84
- return normal[0] * -s.x + normal[1] * -s.y + normal[2] * (focal - s.depth) < 0
85
- }
86
-
87
- /** The yaw and pitch that bring point `index` of `count` round to face the viewer. */
88
- export function sphereFaceForward(count: number, index: number): { yaw: number; pitch: number } {
89
- const [x, y, z] = fibonacciPoint(index, count)
90
- return {
91
- yaw: -Math.atan2(x, z) * DEG,
92
- pitch: Math.atan2(y, Math.hypot(x, z)) * DEG,
93
- }
94
- }
1
+ import { slot, type Slot, type SlotInput } from "./slot"
2
+
3
+ /** `outward` lays each item on the sphere's surface; `billboard` keeps every item facing the viewer. */
4
+ export const SPHERE_FACINGS = ["outward", "billboard"] as const
5
+ export type SphereFacing = (typeof SPHERE_FACINGS)[number]
6
+
7
+ export const SPHERE_AXES = ["x", "y", "z"] as const
8
+ export type SphereAxis = (typeof SPHERE_AXES)[number]
9
+
10
+ export interface SphereArrangement {
11
+ kind: "sphere"
12
+ radius: number
13
+ /** Turn about the vertical axis, degrees — positive carries the front to the right. */
14
+ yaw: number
15
+ /** Turn about the horizontal axis, degrees — positive brings the top toward the viewer. */
16
+ pitch: number
17
+ /** Turn about the view axis, degrees clockwise. */
18
+ roll: number
19
+ facing: SphereFacing
20
+ /** The camera's distance from the sphere's centre plane, px — smaller is more dramatic. */
21
+ focalLength: number
22
+ /** How much the back half fades, `0..1`. */
23
+ depthFade: number
24
+ /** Hide the items whose faces point away from the viewer. */
25
+ cullBackfaces: boolean
26
+ /** Degrees per second of yaw the sphere turns on its own. */
27
+ spinSpeed: number
28
+ }
29
+
30
+ const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5))
31
+ const DEG = 180 / Math.PI
32
+
33
+ /** Point `k` of `count` on the unit sphere — a Fibonacci lattice, as even as `count` points get. */
34
+ export function fibonacciPoint(k: number, count: number): [number, number, number] {
35
+ if (count <= 1) return [0, 0, 1]
36
+ const y = 1 - (2 * (k + 0.5)) / count
37
+ const r = Math.sqrt(Math.max(0, 1 - y * y))
38
+ const theta = k * GOLDEN_ANGLE
39
+ return [Math.cos(theta) * r, y, Math.sin(theta) * r]
40
+ }
41
+
42
+ /** `p` turned by yaw, then pitch, then roll — y-up, z toward the viewer. */
43
+ export function orient(p: readonly [number, number, number], yaw: number, pitch: number, roll: number): [number, number, number] {
44
+ const [x0, y0, z0] = p
45
+ const ya = yaw / DEG, pa = pitch / DEG, ra = roll / DEG
46
+ const x1 = x0 * Math.cos(ya) + z0 * Math.sin(ya)
47
+ const z1 = -x0 * Math.sin(ya) + z0 * Math.cos(ya)
48
+ const y2 = y0 * Math.cos(pa) - z1 * Math.sin(pa)
49
+ const z2 = y0 * Math.sin(pa) + z1 * Math.cos(pa)
50
+ return [x1 * Math.cos(ra) + y2 * Math.sin(ra), -x1 * Math.sin(ra) + y2 * Math.cos(ra), z2]
51
+ }
52
+
53
+ export function sphereSlots(arrangement: SphereArrangement, input: SlotInput): Slot[] {
54
+ const { count } = input
55
+ const radius = arrangement.radius
56
+ const yaw = arrangement.yaw + arrangement.spinSpeed * input.time
57
+ const outward = arrangement.facing === "outward"
58
+ const slots: Slot[] = []
59
+ for (let k = 0; k < count; k++) {
60
+ const q = fibonacciPoint(k, count)
61
+ const [nx, ny, nz] = orient(q, yaw, arrangement.pitch, arrangement.roll)
62
+ const s = slot(nx * radius, ny * radius, {
63
+ kind: "sphere",
64
+ lat: Math.asin(Math.max(-1, Math.min(1, q[1]))) * DEG,
65
+ lon: Math.atan2(q[2], q[0]) * DEG,
66
+ depth: nz,
67
+ })
68
+ s.depth = nz * radius
69
+ if (outward) {
70
+ // The tilt whose `Rx · Ry` turns a face's normal onto the surface normal.
71
+ s.rotationY = Math.asin(Math.max(-1, Math.min(1, nx))) * DEG
72
+ s.rotationX = Math.atan2(ny, nz) * DEG
73
+ }
74
+ s.opacity = 1 - arrangement.depthFade * (1 - (nz + 1) / 2)
75
+ if (outward && arrangement.cullBackfaces && facesAway(s, [nx, ny, nz], arrangement.focalLength)) s.opacity = 0
76
+ slots.push(s)
77
+ }
78
+ return slots
79
+ }
80
+
81
+ /** Whether an outward face at `s` shows its back to a viewer `focal` px in front of the centre. */
82
+ function facesAway(s: Slot, normal: readonly [number, number, number], focal: number): boolean {
83
+ if (focal <= 0) return normal[2] < 0
84
+ return normal[0] * -s.x + normal[1] * -s.y + normal[2] * (focal - s.depth) < 0
85
+ }
86
+
87
+ /** The yaw and pitch that bring point `index` of `count` round to face the viewer. */
88
+ export function sphereFaceForward(count: number, index: number): { yaw: number; pitch: number } {
89
+ const [x, y, z] = fibonacciPoint(index, count)
90
+ return {
91
+ yaw: -Math.atan2(x, z) * DEG,
92
+ pitch: Math.atan2(y, Math.hypot(x, z)) * DEG,
93
+ }
94
+ }
@@ -1,96 +1,96 @@
1
- import { shuffled } from "./random"
2
- import type { Slot } from "./slots"
3
-
4
- /**
5
- * The order a per-item effect sweeps through a grid's items. The first five
6
- * work on any grid; the rest follow one arrangement's own geometry — rows and
7
- * columns of a rectangular grid (or top-to-bottom and left-to-right of any
8
- * other), the way round a ring, the way along a path, front to back of a sphere
9
- * — and fall back to item order where the grid has no such thing.
10
- */
11
- export const STAGGER_ORDERS = [
12
- "index",
13
- "reverse",
14
- "random",
15
- "centerOut",
16
- "edgesIn",
17
- "row",
18
- "column",
19
- "angle",
20
- "path",
21
- "depth",
22
- ] as const
23
- export type StaggerOrder = (typeof STAGGER_ORDERS)[number]
24
-
25
- /** The most of a command's length the offsets may take; the rest is each item's own motion. */
26
- const MAX_STAGGER = 0.95
27
-
28
- /**
29
- * Each item's start, `0..1`, as a share of the stagger: `0` goes first, `1`
30
- * last, and items that tie go together — a ring revealed `centerOut` from its
31
- * centre appears all at once, which is what that means.
32
- */
33
- export function staggerRanks(order: StaggerOrder, slots: readonly Slot[], seed: number): number[] {
34
- const n = slots.length
35
- const keys = keysOf(order, slots, seed)
36
- let min = Infinity
37
- let max = -Infinity
38
- for (const key of keys) {
39
- if (key < min) min = key
40
- if (key > max) max = key
41
- }
42
- const range = max - min
43
- if (!(range > 1e-9)) return new Array<number>(n).fill(0)
44
- return keys.map((key) => (key - min) / range)
45
- }
46
-
47
- /**
48
- * Item progress at command time `t`: each item runs over the same length of
49
- * time, starting `rank · stagger` in. The stagger lives inside the command's
50
- * duration, so a one-second reveal takes one second for four items or four
51
- * hundred.
52
- */
53
- export function staggerProgress(rank: number, t: number, stagger: number): number {
54
- const s = Math.max(0, Math.min(MAX_STAGGER, stagger))
55
- const p = s === 0 ? t : (t - rank * s) / (1 - s)
56
- return p < 0 ? 0 : p > 1 ? 1 : p
57
- }
58
-
59
- function keysOf(order: StaggerOrder, slots: readonly Slot[], seed: number): number[] {
60
- switch (order) {
61
- case "reverse":
62
- return slots.map((_, i) => -i)
63
- case "random": {
64
- const sequence = shuffled(slots.map((_, i) => i), seed)
65
- const keys = new Array<number>(slots.length)
66
- sequence.forEach((item, rank) => {
67
- keys[item] = rank
68
- })
69
- return keys
70
- }
71
- case "centerOut":
72
- return slots.map((s) => Math.round(Math.hypot(s.x, s.y, s.depth) * 1000) / 1000)
73
- case "edgesIn":
74
- return slots.map((s) => -Math.round(Math.hypot(s.x, s.y, s.depth) * 1000) / 1000)
75
- case "row":
76
- return slots.map((s) => (s.meta.kind === "rectangular" ? s.meta.row : -Math.round(s.y)))
77
- case "column":
78
- return slots.map((s) => (s.meta.kind === "rectangular" ? s.meta.column : Math.round(s.x)))
79
- case "angle":
80
- return slots.map((s, i) =>
81
- s.meta.kind === "radial" ? s.meta.angle : s.meta.kind === "rectangular" || s.meta.kind === "sphere" ? clockAngle(s) : i
82
- )
83
- case "path":
84
- return slots.map((s, i) => (s.meta.kind === "path" ? s.meta.t : i))
85
- case "depth":
86
- return slots.map((s, i) => (s.meta.kind === "sphere" ? -s.meta.depth : i))
87
- default:
88
- return slots.map((_, i) => i)
89
- }
90
- }
91
-
92
- /** Degrees clockwise from twelve o'clock to where `s` sits. */
93
- function clockAngle(s: Slot): number {
94
- const angle = (Math.atan2(s.x, s.y) * 180) / Math.PI
95
- return angle < 0 ? angle + 360 : angle
96
- }
1
+ import { shuffled } from "./random"
2
+ import type { Slot } from "./slots"
3
+
4
+ /**
5
+ * The order a per-item effect sweeps through a grid's items. The first five
6
+ * work on any grid; the rest follow one arrangement's own geometry — rows and
7
+ * columns of a rectangular grid (or top-to-bottom and left-to-right of any
8
+ * other), the way round a ring, the way along a path, front to back of a sphere
9
+ * — and fall back to item order where the grid has no such thing.
10
+ */
11
+ export const STAGGER_ORDERS = [
12
+ "index",
13
+ "reverse",
14
+ "random",
15
+ "centerOut",
16
+ "edgesIn",
17
+ "row",
18
+ "column",
19
+ "angle",
20
+ "path",
21
+ "depth",
22
+ ] as const
23
+ export type StaggerOrder = (typeof STAGGER_ORDERS)[number]
24
+
25
+ /** The most of a command's length the offsets may take; the rest is each item's own motion. */
26
+ const MAX_STAGGER = 0.95
27
+
28
+ /**
29
+ * Each item's start, `0..1`, as a share of the stagger: `0` goes first, `1`
30
+ * last, and items that tie go together — a ring revealed `centerOut` from its
31
+ * centre appears all at once, which is what that means.
32
+ */
33
+ export function staggerRanks(order: StaggerOrder, slots: readonly Slot[], seed: number): number[] {
34
+ const n = slots.length
35
+ const keys = keysOf(order, slots, seed)
36
+ let min = Infinity
37
+ let max = -Infinity
38
+ for (const key of keys) {
39
+ if (key < min) min = key
40
+ if (key > max) max = key
41
+ }
42
+ const range = max - min
43
+ if (!(range > 1e-9)) return new Array<number>(n).fill(0)
44
+ return keys.map((key) => (key - min) / range)
45
+ }
46
+
47
+ /**
48
+ * Item progress at command time `t`: each item runs over the same length of
49
+ * time, starting `rank · stagger` in. The stagger lives inside the command's
50
+ * duration, so a one-second reveal takes one second for four items or four
51
+ * hundred.
52
+ */
53
+ export function staggerProgress(rank: number, t: number, stagger: number): number {
54
+ const s = Math.max(0, Math.min(MAX_STAGGER, stagger))
55
+ const p = s === 0 ? t : (t - rank * s) / (1 - s)
56
+ return p < 0 ? 0 : p > 1 ? 1 : p
57
+ }
58
+
59
+ function keysOf(order: StaggerOrder, slots: readonly Slot[], seed: number): number[] {
60
+ switch (order) {
61
+ case "reverse":
62
+ return slots.map((_, i) => -i)
63
+ case "random": {
64
+ const sequence = shuffled(slots.map((_, i) => i), seed)
65
+ const keys = new Array<number>(slots.length)
66
+ sequence.forEach((item, rank) => {
67
+ keys[item] = rank
68
+ })
69
+ return keys
70
+ }
71
+ case "centerOut":
72
+ return slots.map((s) => Math.round(Math.hypot(s.x, s.y, s.depth) * 1000) / 1000)
73
+ case "edgesIn":
74
+ return slots.map((s) => -Math.round(Math.hypot(s.x, s.y, s.depth) * 1000) / 1000)
75
+ case "row":
76
+ return slots.map((s) => (s.meta.kind === "rectangular" ? s.meta.row : -Math.round(s.y)))
77
+ case "column":
78
+ return slots.map((s) => (s.meta.kind === "rectangular" ? s.meta.column : Math.round(s.x)))
79
+ case "angle":
80
+ return slots.map((s, i) =>
81
+ s.meta.kind === "radial" ? s.meta.angle : s.meta.kind === "rectangular" || s.meta.kind === "sphere" ? clockAngle(s) : i
82
+ )
83
+ case "path":
84
+ return slots.map((s, i) => (s.meta.kind === "path" ? s.meta.t : i))
85
+ case "depth":
86
+ return slots.map((s, i) => (s.meta.kind === "sphere" ? -s.meta.depth : i))
87
+ default:
88
+ return slots.map((_, i) => i)
89
+ }
90
+ }
91
+
92
+ /** Degrees clockwise from twelve o'clock to where `s` sits. */
93
+ function clockAngle(s: Slot): number {
94
+ const angle = (Math.atan2(s.x, s.y) * 180) / Math.PI
95
+ return angle < 0 ? angle + 360 : angle
96
+ }
package/src/nodes.ts CHANGED
@@ -1,30 +1,30 @@
1
- import { PathGrid } from "./path-grid"
2
- import { RadialGrid } from "./radial-grid"
3
- import { RectangularGrid } from "./rectangular-grid"
4
- import { SphereGrid } from "./sphere-grid"
5
-
6
- /**
7
- * Every node type this package publishes.
8
- *
9
- * A host registers these by handing the array to an engine —
10
- * `new Engine(platform, { nodes: NODES })` — which reads each component's key.
11
- * Defining a component only *declares* that key: nothing is registered by
12
- * importing a module, so a registry holds exactly what its owner asked for.
13
- *
14
- * Listing the component **values** is also what survives bundling: this package
15
- * declares `sideEffects: false`, and a document names a node type by string, so
16
- * an import kept only for its side effect is exactly what gets shaken out.
17
- */
18
- // Typed by name, so the declaration names each component rather than spelling out its type through core.
19
- type KitNode =
20
- | typeof RectangularGrid
21
- | typeof RadialGrid
22
- | typeof PathGrid
23
- | typeof SphereGrid
24
-
25
- export const NODES: KitNode[] = [
26
- RectangularGrid,
27
- RadialGrid,
28
- PathGrid,
29
- SphereGrid,
30
- ]
1
+ import { PathGrid } from "./path-grid"
2
+ import { RadialGrid } from "./radial-grid"
3
+ import { RectangularGrid } from "./rectangular-grid"
4
+ import { SphereGrid } from "./sphere-grid"
5
+
6
+ /**
7
+ * Every node type this package publishes.
8
+ *
9
+ * A host registers these by handing the array to an engine —
10
+ * `new Engine(platform, { nodes: NODES })` — which reads each component's key.
11
+ * Defining a component only *declares* that key: nothing is registered by
12
+ * importing a module, so a registry holds exactly what its owner asked for.
13
+ *
14
+ * Listing the component **values** is also what survives bundling: this package
15
+ * declares `sideEffects: false`, and a document names a node type by string, so
16
+ * an import kept only for its side effect is exactly what gets shaken out.
17
+ */
18
+ // Typed by name, so the declaration names each component rather than spelling out its type through core.
19
+ type KitNode =
20
+ | typeof RectangularGrid
21
+ | typeof RadialGrid
22
+ | typeof PathGrid
23
+ | typeof SphereGrid
24
+
25
+ export const NODES: KitNode[] = [
26
+ RectangularGrid,
27
+ RadialGrid,
28
+ PathGrid,
29
+ SphereGrid,
30
+ ]
@@ -1 +1 @@
1
- export * from "./path-grid"
1
+ export * from "./path-grid"