@motionscript/grids 0.0.0-stage → 0.1.0-alpha.10
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 +87 -0
- package/LICENSE +201 -0
- package/NOTICE +49 -0
- package/README.md +117 -2
- package/dist/browser/chunks/chunk-TYNFMZ3D.js +2 -0
- package/dist/browser/chunks/chunk-TYNFMZ3D.js.map +7 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/kit.js +2 -0
- package/dist/browser/kit.js.map +7 -0
- package/dist/browser/manifest.json +13 -0
- package/dist/engine.d.ts +13 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +13 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/index.d.ts +9 -0
- package/dist/kit/index.d.ts.map +1 -0
- package/dist/kit/index.js +10 -0
- package/dist/kit/index.js.map +1 -0
- package/dist/kit/item-effects.d.ts +37 -0
- package/dist/kit/item-effects.d.ts.map +1 -0
- package/dist/kit/item-effects.js +76 -0
- package/dist/kit/item-effects.js.map +1 -0
- package/dist/kit/item-grid.d.ts +240 -0
- package/dist/kit/item-grid.d.ts.map +1 -0
- package/dist/kit/item-grid.js +492 -0
- package/dist/kit/item-grid.js.map +1 -0
- package/dist/kit/order.d.ts +13 -0
- package/dist/kit/order.d.ts.map +1 -0
- package/dist/kit/order.js +43 -0
- package/dist/kit/order.js.map +1 -0
- package/dist/kit/random.d.ts +7 -0
- package/dist/kit/random.d.ts.map +1 -0
- package/dist/kit/random.js +29 -0
- package/dist/kit/random.js.map +1 -0
- package/dist/kit/slots/index.d.ts +22 -0
- package/dist/kit/slots/index.d.ts.map +1 -0
- package/dist/kit/slots/index.js +28 -0
- package/dist/kit/slots/index.js.map +1 -0
- package/dist/kit/slots/path-templates.d.ts +15 -0
- package/dist/kit/slots/path-templates.d.ts.map +1 -0
- package/dist/kit/slots/path-templates.js +113 -0
- package/dist/kit/slots/path-templates.js.map +1 -0
- package/dist/kit/slots/path.d.ts +45 -0
- package/dist/kit/slots/path.d.ts.map +1 -0
- package/dist/kit/slots/path.js +87 -0
- package/dist/kit/slots/path.js.map +1 -0
- package/dist/kit/slots/radial.d.ts +30 -0
- package/dist/kit/slots/radial.d.ts.map +1 -0
- package/dist/kit/slots/radial.js +45 -0
- package/dist/kit/slots/radial.js.map +1 -0
- package/dist/kit/slots/rectangular.d.ts +42 -0
- package/dist/kit/slots/rectangular.d.ts.map +1 -0
- package/dist/kit/slots/rectangular.js +79 -0
- package/dist/kit/slots/rectangular.js.map +1 -0
- package/dist/kit/slots/slot.d.ts +62 -0
- package/dist/kit/slots/slot.d.ts.map +1 -0
- package/dist/kit/slots/slot.js +41 -0
- package/dist/kit/slots/slot.js.map +1 -0
- package/dist/kit/slots/sphere.d.ts +36 -0
- package/dist/kit/slots/sphere.d.ts.map +1 -0
- package/dist/kit/slots/sphere.js +68 -0
- package/dist/kit/slots/sphere.js.map +1 -0
- package/dist/kit/stagger.d.ts +24 -0
- package/dist/kit/stagger.d.ts.map +1 -0
- package/dist/kit/stagger.js +90 -0
- package/dist/kit/stagger.js.map +1 -0
- package/dist/nodes.d.ts +20 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +11 -0
- package/dist/nodes.js.map +1 -0
- package/dist/path-grid/index.d.ts +2 -0
- package/dist/path-grid/index.d.ts.map +1 -0
- package/dist/path-grid/index.js +2 -0
- package/dist/path-grid/index.js.map +1 -0
- package/dist/path-grid/path-grid.d.ts +196 -0
- package/dist/path-grid/path-grid.d.ts.map +1 -0
- package/dist/path-grid/path-grid.js +149 -0
- package/dist/path-grid/path-grid.js.map +1 -0
- package/dist/radial-grid/index.d.ts +2 -0
- package/dist/radial-grid/index.d.ts.map +1 -0
- package/dist/radial-grid/index.js +2 -0
- package/dist/radial-grid/index.js.map +1 -0
- package/dist/radial-grid/radial-grid.d.ts +177 -0
- package/dist/radial-grid/radial-grid.d.ts.map +1 -0
- package/dist/radial-grid/radial-grid.js +63 -0
- package/dist/radial-grid/radial-grid.js.map +1 -0
- package/dist/rectangular-grid/index.d.ts +2 -0
- package/dist/rectangular-grid/index.d.ts.map +1 -0
- package/dist/rectangular-grid/index.js +2 -0
- package/dist/rectangular-grid/index.js.map +1 -0
- package/dist/rectangular-grid/rectangular-grid.d.ts +212 -0
- package/dist/rectangular-grid/rectangular-grid.d.ts.map +1 -0
- package/dist/rectangular-grid/rectangular-grid.js +118 -0
- package/dist/rectangular-grid/rectangular-grid.js.map +1 -0
- package/dist/sphere-grid/index.d.ts +2 -0
- package/dist/sphere-grid/index.d.ts.map +1 -0
- package/dist/sphere-grid/index.js +2 -0
- package/dist/sphere-grid/index.js.map +1 -0
- package/dist/sphere-grid/sphere-grid.d.ts +200 -0
- package/dist/sphere-grid/sphere-grid.d.ts.map +1 -0
- package/dist/sphere-grid/sphere-grid.js +102 -0
- package/dist/sphere-grid/sphere-grid.js.map +1 -0
- package/package.json +72 -3
- package/registry.json +32 -0
- package/src/engine.ts +12 -0
- package/src/index.ts +6 -0
- package/src/kit/index.ts +18 -0
- package/src/kit/item-effects.ts +114 -0
- package/src/kit/item-grid.ts +648 -0
- package/src/kit/order.ts +41 -0
- package/src/kit/random.ts +29 -0
- package/src/kit/slots/index.ts +39 -0
- package/src/kit/slots/path-templates.ts +120 -0
- package/src/kit/slots/path.ts +120 -0
- package/src/kit/slots/radial.ts +62 -0
- package/src/kit/slots/rectangular.ts +108 -0
- package/src/kit/slots/slot.ts +82 -0
- package/src/kit/slots/sphere.ts +94 -0
- package/src/kit/stagger.ts +96 -0
- package/src/nodes.ts +30 -0
- package/src/path-grid/index.ts +1 -0
- package/src/path-grid/path-grid.ts +186 -0
- package/src/radial-grid/index.ts +1 -0
- package/src/radial-grid/radial-grid.ts +81 -0
- package/src/rectangular-grid/index.ts +1 -0
- package/src/rectangular-grid/rectangular-grid.ts +145 -0
- package/src/sphere-grid/index.ts +1 -0
- package/src/sphere-grid/sphere-grid.ts +129 -0
|
@@ -0,0 +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
|
+
}
|
package/src/nodes.ts
ADDED
|
@@ -0,0 +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
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./path-grid"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
component2d,
|
|
3
|
+
pathOps,
|
|
4
|
+
Types,
|
|
5
|
+
type NodeOf,
|
|
6
|
+
type NodeView,
|
|
7
|
+
type PathData,
|
|
8
|
+
type Size2D,
|
|
9
|
+
type Values,
|
|
10
|
+
// Named so the emitted declaration reaches these through the sdk, not core.
|
|
11
|
+
type Node2D as _Node2D,
|
|
12
|
+
type Node2DProps as _Node2DProps,
|
|
13
|
+
} from "@motionscript/sdk"
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
ItemGrid,
|
|
17
|
+
PATH_SPACINGS,
|
|
18
|
+
PATH_TEMPLATES,
|
|
19
|
+
staggerArgs,
|
|
20
|
+
TRANSITION_STAGGER,
|
|
21
|
+
templatePath,
|
|
22
|
+
type PathArrangement,
|
|
23
|
+
type PathPlacement,
|
|
24
|
+
type PathTemplate,
|
|
25
|
+
} from "../kit"
|
|
26
|
+
|
|
27
|
+
/** A template, or `custom` for the grid's own `path`. */
|
|
28
|
+
export const PATH_GRID_SHAPES = [...PATH_TEMPLATES, "custom"] as const
|
|
29
|
+
export type PathGridShape = (typeof PATH_GRID_SHAPES)[number]
|
|
30
|
+
|
|
31
|
+
const own = {
|
|
32
|
+
shape: Types.oneOf(PATH_GRID_SHAPES, { default: "circle" }),
|
|
33
|
+
/** Used when `shape` is `custom`: an SVG path, y-down as SVG writes one, centred on its own bounds. */
|
|
34
|
+
path: Types.path,
|
|
35
|
+
/** A path node to follow instead, by id — the items track it wherever it goes. */
|
|
36
|
+
pathNode: ItemGrid.nodeRef(["path"]),
|
|
37
|
+
spacing: Types.oneOf(PATH_SPACINGS, { default: "distribute" }),
|
|
38
|
+
spacingDistance: Types.number(80, { min: 0, step: 1 }),
|
|
39
|
+
/** Share of the path the items are moved along — `1` is once round. */
|
|
40
|
+
offset: Types.number(0, { step: 0.01 }),
|
|
41
|
+
/** Share of the path per second the items travel on their own. */
|
|
42
|
+
flowSpeed: Types.number(0, { step: 0.01, unit: "/s" }),
|
|
43
|
+
trimStart: Types.number(0, { min: 0, max: 1, step: 0.01 }),
|
|
44
|
+
trimEnd: Types.number(1, { min: 0, max: 1, step: 0.01 }),
|
|
45
|
+
alignToPath: true,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Own = Values<typeof own>
|
|
49
|
+
|
|
50
|
+
const AT_ORIGIN: PathPlacement = { originX: 0, originY: 0, x: 0, y: 0, rotation: 0, scale: 1 }
|
|
51
|
+
|
|
52
|
+
/** The size a path grid is placed at when nothing says otherwise, since a template is drawn to fill it. */
|
|
53
|
+
const DEFAULT_SIZE = { width: 640, height: 400 }
|
|
54
|
+
|
|
55
|
+
const arranger: ItemGrid.Arranger<Own> = {
|
|
56
|
+
arrangement: (s, box): PathArrangement => {
|
|
57
|
+
const { path, placement } = pathIn(s, box)
|
|
58
|
+
return {
|
|
59
|
+
kind: "path",
|
|
60
|
+
path,
|
|
61
|
+
placement,
|
|
62
|
+
spacing: s.spacing,
|
|
63
|
+
spacingDistance: s.spacingDistance,
|
|
64
|
+
offset: s.offset,
|
|
65
|
+
flowSpeed: s.flowSpeed,
|
|
66
|
+
trimStart: s.trimStart,
|
|
67
|
+
trimEnd: s.trimEnd,
|
|
68
|
+
alignToPath: s.alignToPath,
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Children along a path: one of the templates drawn to fill the node's box,
|
|
75
|
+
* a path of the grid's own, or a path node elsewhere in the scene, followed
|
|
76
|
+
* wherever it is.
|
|
77
|
+
*
|
|
78
|
+
* Items are spaced by distance along the curve, so they sit evenly on a tight
|
|
79
|
+
* bend and a straight alike. Animating `offset` moves them along it — on a
|
|
80
|
+
* closed path they come round again, which is a conveyor belt for free.
|
|
81
|
+
*/
|
|
82
|
+
export const PathGrid = ItemGrid.define(component2d({
|
|
83
|
+
key: "pathGrid",
|
|
84
|
+
state: { ...ItemGrid.state, ...own },
|
|
85
|
+
// A template is drawn to fill the box, so a path grid needs one of its own
|
|
86
|
+
// rather than hugging items whose places depend on it.
|
|
87
|
+
defaults: { ...DEFAULT_SIZE },
|
|
88
|
+
commands: {
|
|
89
|
+
...ItemGrid.commands,
|
|
90
|
+
/** Move the items along the path by `loops` — `1` is once round, and they keep their spacing. */
|
|
91
|
+
flow: {
|
|
92
|
+
args: { loops: Types.number(1, { step: 0.25 }) },
|
|
93
|
+
run: (timeline, s) => timeline.tween({ offset: s.offset + (timeline.args.loops ?? 1) }),
|
|
94
|
+
},
|
|
95
|
+
/** Squeeze the items into part of the path, between two shares of its length. */
|
|
96
|
+
trimTo: {
|
|
97
|
+
args: {
|
|
98
|
+
trimStart: ItemGrid.hinted(0.25, { min: 0, max: 1, step: 0.01 }),
|
|
99
|
+
trimEnd: ItemGrid.hinted(0.75, { min: 0, max: 1, step: 0.01 }),
|
|
100
|
+
},
|
|
101
|
+
run: (timeline, s) => timeline.tween({
|
|
102
|
+
trimStart: timeline.args.trimStart ?? s.trimStart,
|
|
103
|
+
trimEnd: timeline.args.trimEnd ?? s.trimEnd,
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* Move the items onto another path — a template, or a path node in the
|
|
108
|
+
* scene. Each item travels from its place on the old path to its place on
|
|
109
|
+
* the new one; the path itself is not morphed.
|
|
110
|
+
*/
|
|
111
|
+
setPath: {
|
|
112
|
+
args: {
|
|
113
|
+
shape: ItemGrid.hinted("heart", { kind: "enum", options: PATH_GRID_SHAPES }),
|
|
114
|
+
pathNode: ItemGrid.nodeRefArg(["path"]),
|
|
115
|
+
...staggerArgs(TRANSITION_STAGGER),
|
|
116
|
+
},
|
|
117
|
+
run: (timeline, s, children) => {
|
|
118
|
+
const pathNode = timeline.args.pathNode ?? ""
|
|
119
|
+
const changes = pathNode ? { pathNode } : { shape: timeline.args.shape ?? s.shape, pathNode: "" }
|
|
120
|
+
return ItemGrid.transitionTo(timeline as never, s, children, changes)
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
...ItemGrid.fields(arranger),
|
|
125
|
+
}), arranger)
|
|
126
|
+
|
|
127
|
+
export type PathGrid = NodeOf<typeof PathGrid>
|
|
128
|
+
/** What a call or a document row may set. */
|
|
129
|
+
export type PathGridProps = NonNullable<Parameters<typeof PathGrid>[0]>
|
|
130
|
+
|
|
131
|
+
/** The path the items follow, and where its coordinates land in this grid's frame. */
|
|
132
|
+
function pathIn(s: Own, box: Size2D): { path: PathData; placement: PathPlacement } {
|
|
133
|
+
const linked = s.pathNode
|
|
134
|
+
// A document `path` draws its data in its own unit box, at its own position.
|
|
135
|
+
if (linked?.key === "path") return { path: unitPath(linked), placement: { ...AT_ORIGIN, ...linked.frame } }
|
|
136
|
+
// A face-value path draws its data centred on the data's own bounds, at its own position.
|
|
137
|
+
const data = linked?.key === "facePath" ? dataOf(linked) : undefined
|
|
138
|
+
const bounds = data !== undefined ? pathOps.bounds(data) : null
|
|
139
|
+
if (linked && data !== undefined && bounds) {
|
|
140
|
+
return {
|
|
141
|
+
path: data,
|
|
142
|
+
placement: {
|
|
143
|
+
originX: (bounds.minX + bounds.maxX) / 2,
|
|
144
|
+
originY: (bounds.minY + bounds.maxY) / 2,
|
|
145
|
+
...linked.frame,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (s.shape === "custom") {
|
|
150
|
+
const own = pathOps.bounds(s.path)
|
|
151
|
+
if (own) {
|
|
152
|
+
return {
|
|
153
|
+
path: s.path,
|
|
154
|
+
placement: {
|
|
155
|
+
...AT_ORIGIN,
|
|
156
|
+
originX: (own.minX + own.maxX) / 2,
|
|
157
|
+
originY: (own.minY + own.maxY) / 2,
|
|
158
|
+
flipY: true,
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const template: PathTemplate = s.shape === "custom" ? "circle" : s.shape
|
|
164
|
+
return { path: templatePath(template, box), placement: AT_ORIGIN }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function dataOf(view: NodeView): PathData {
|
|
168
|
+
return (view.state as { readonly data: PathData }).data
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const unitPaths = new WeakMap<NodeView, { data: PathData; width: number; height: number; path: PathData }>()
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A path node's geometry as it draws it: stored in its unit box, y-down, and
|
|
175
|
+
* scaled to its laid-out size — kept while neither changes, so the path's
|
|
176
|
+
* measurement is kept with it.
|
|
177
|
+
*/
|
|
178
|
+
function unitPath(view: NodeView): PathData {
|
|
179
|
+
const data = dataOf(view)
|
|
180
|
+
const { width, height } = view.size
|
|
181
|
+
const cached = unitPaths.get(view)
|
|
182
|
+
if (cached && cached.data === data && cached.width === width && cached.height === height) return cached.path
|
|
183
|
+
const path = pathOps.anchorCommands(pathOps.anchors(data), (p) => ({ x: p.x * width, y: -p.y * height }))
|
|
184
|
+
unitPaths.set(view, { data, width, height, path })
|
|
185
|
+
return path
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./radial-grid"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
component2d,
|
|
3
|
+
Types,
|
|
4
|
+
type NodeOf,
|
|
5
|
+
type Values,
|
|
6
|
+
// Named so the emitted declaration reaches these through the sdk, not core.
|
|
7
|
+
type Node2D as _Node2D,
|
|
8
|
+
type Node2DProps as _Node2DProps,
|
|
9
|
+
} from "@motionscript/sdk"
|
|
10
|
+
|
|
11
|
+
import { ItemGrid, normalizeOrder, RADIAL_DIRECTIONS, radialTurnToTop, type RadialArrangement } from "../kit"
|
|
12
|
+
|
|
13
|
+
const own = {
|
|
14
|
+
radius: Types.number(200, { min: 0, step: 1 }),
|
|
15
|
+
startAngle: Types.number(0, { step: 1, unit: "°" }),
|
|
16
|
+
sweep: Types.number(360, { min: -360, max: 360, step: 1, unit: "°" }),
|
|
17
|
+
direction: Types.oneOf(RADIAL_DIRECTIONS, { default: "clockwise" }),
|
|
18
|
+
rotateItems: false,
|
|
19
|
+
spinSpeed: Types.number(0, { step: 1, unit: "°/s" }),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Own = Values<typeof own>
|
|
23
|
+
|
|
24
|
+
function arrangement(s: Own): RadialArrangement {
|
|
25
|
+
return {
|
|
26
|
+
kind: "radial",
|
|
27
|
+
radius: s.radius,
|
|
28
|
+
startAngle: s.startAngle,
|
|
29
|
+
sweep: s.sweep,
|
|
30
|
+
direction: s.direction,
|
|
31
|
+
rotateItems: s.rotateItems,
|
|
32
|
+
spinSpeed: s.spinSpeed,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const arranger: ItemGrid.Arranger<Own> = { arrangement }
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Children round a circle, or along an arc of one.
|
|
40
|
+
*
|
|
41
|
+
* A full circle spaces the items evenly with none on top of another; an arc
|
|
42
|
+
* puts one at each end. Angles run clockwise from twelve o'clock, so `turnTo`
|
|
43
|
+
* can bring any item to the top like a dial.
|
|
44
|
+
*/
|
|
45
|
+
export const RadialGrid = ItemGrid.define(component2d({
|
|
46
|
+
key: "radialGrid",
|
|
47
|
+
state: { ...ItemGrid.state, ...own },
|
|
48
|
+
defaults: ItemGrid.defaults,
|
|
49
|
+
commands: {
|
|
50
|
+
...ItemGrid.commands,
|
|
51
|
+
/** Orbit the items round the centre by `degrees` — `360` is one full turn. */
|
|
52
|
+
spin: {
|
|
53
|
+
args: { degrees: Types.number(360, { step: 15, unit: "°" }) },
|
|
54
|
+
run: (timeline, s) => timeline.tween({ startAngle: s.startAngle + (timeline.args.degrees ?? 360) }),
|
|
55
|
+
},
|
|
56
|
+
/** Turn the ring the shorter way round until an item (by its place in the layers) sits at the top. */
|
|
57
|
+
turnTo: {
|
|
58
|
+
args: { item: Types.number(0, { min: 0, step: 1 }) },
|
|
59
|
+
run: (timeline, s, children) => {
|
|
60
|
+
const count = children.length
|
|
61
|
+
const slot = normalizeOrder(s.order, count).indexOf(Math.round(timeline.args.item ?? 0))
|
|
62
|
+
const turn = slot < 0 ? 0 : radialTurnToTop(arrangement(s), count, slot)
|
|
63
|
+
return timeline.tween({ startAngle: s.startAngle + turn })
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
radiusTo: {
|
|
67
|
+
args: { radius: ItemGrid.hinted(280, { min: 0 }) },
|
|
68
|
+
run: (timeline, s) => timeline.tween({ radius: timeline.args.radius ?? s.radius }),
|
|
69
|
+
},
|
|
70
|
+
/** Open or close the arc like a fan — from `0` to `360` is a reveal of its own. */
|
|
71
|
+
sweepTo: {
|
|
72
|
+
args: { sweep: ItemGrid.hinted(360, { min: -360, max: 360, unit: "°" }) },
|
|
73
|
+
run: (timeline, s) => timeline.tween({ sweep: timeline.args.sweep ?? s.sweep }),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
...ItemGrid.fields(arranger),
|
|
77
|
+
}), arranger)
|
|
78
|
+
|
|
79
|
+
export type RadialGrid = NodeOf<typeof RadialGrid>
|
|
80
|
+
/** What a call or a document row may set. */
|
|
81
|
+
export type RadialGridProps = NonNullable<Parameters<typeof RadialGrid>[0]>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./rectangular-grid"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
component2d,
|
|
3
|
+
easeInOut,
|
|
4
|
+
Types,
|
|
5
|
+
type NodeOf,
|
|
6
|
+
type Values,
|
|
7
|
+
// Named so the emitted declaration reaches these through the sdk, not core.
|
|
8
|
+
type Node2D as _Node2D,
|
|
9
|
+
type Node2DProps as _Node2DProps,
|
|
10
|
+
} from "@motionscript/sdk"
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
CELL_MODES,
|
|
14
|
+
FILL_DIRECTIONS,
|
|
15
|
+
ItemGrid,
|
|
16
|
+
LINE_ALIGNS,
|
|
17
|
+
rectangularCell,
|
|
18
|
+
staggerArgs,
|
|
19
|
+
TRANSITION_STAGGER,
|
|
20
|
+
type RectangularArrangement,
|
|
21
|
+
} from "../kit"
|
|
22
|
+
|
|
23
|
+
const own = {
|
|
24
|
+
columns: Types.number(3, { min: 1, step: 1 }),
|
|
25
|
+
fillDirection: Types.oneOf(FILL_DIRECTIONS, { default: "row" }),
|
|
26
|
+
cellMode: Types.oneOf(CELL_MODES, { default: "fit" }),
|
|
27
|
+
cellWidth: Types.number(120, { min: 0, step: 1 }),
|
|
28
|
+
cellHeight: Types.number(120, { min: 0, step: 1 }),
|
|
29
|
+
columnGap: Types.number(24, { step: 1 }),
|
|
30
|
+
rowGap: Types.number(24, { step: 1 }),
|
|
31
|
+
lastLineAlign: Types.oneOf(LINE_ALIGNS, { default: "start" }),
|
|
32
|
+
rowOffset: Types.number(0, { min: -1, max: 1, step: 0.05 }),
|
|
33
|
+
columnOffset: Types.number(0, { min: -1, max: 1, step: 0.05 }),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Own = Values<typeof own>
|
|
37
|
+
|
|
38
|
+
function arrangement(s: Own): RectangularArrangement {
|
|
39
|
+
return {
|
|
40
|
+
kind: "rectangular",
|
|
41
|
+
columns: s.columns,
|
|
42
|
+
fillDirection: s.fillDirection,
|
|
43
|
+
cellMode: s.cellMode,
|
|
44
|
+
cellWidth: s.cellWidth,
|
|
45
|
+
cellHeight: s.cellHeight,
|
|
46
|
+
columnGap: s.columnGap,
|
|
47
|
+
rowGap: s.rowGap,
|
|
48
|
+
lastLineAlign: s.lastLineAlign,
|
|
49
|
+
rowOffset: s.rowOffset,
|
|
50
|
+
columnOffset: s.columnOffset,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const arranger: ItemGrid.Arranger<Own> = {
|
|
55
|
+
arrangement,
|
|
56
|
+
// A child set to fill takes its cell, where the cell is not simply the largest child.
|
|
57
|
+
itemSpace: (s, count, space) => (s.cellMode === "fit" ? space : rectangularCell(arrangement(s), count, [], space)),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The focus args of a row or a column: the grow a row leaves out is the shared focus's. */
|
|
61
|
+
const lineFocus = {
|
|
62
|
+
dim: Types.number(0.7, { min: 0, max: 1, step: 0.05 }),
|
|
63
|
+
grow: ItemGrid.hinted(0.1, { min: 0, max: 1, step: 0.05 }),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Children in rows and columns — the grid the other three are variations on.
|
|
68
|
+
*
|
|
69
|
+
* Cells are as big as the largest child (`fit`), a size of their own
|
|
70
|
+
* (`fixed`), or an equal share of the node's box (`fill`), which is also the
|
|
71
|
+
* room a child set to fill grows into. Shift every other row by half a cell
|
|
72
|
+
* for a brick bond or a hex pack.
|
|
73
|
+
*/
|
|
74
|
+
export const RectangularGrid = ItemGrid.define(component2d({
|
|
75
|
+
key: "rectangularGrid",
|
|
76
|
+
state: { ...ItemGrid.state, ...own },
|
|
77
|
+
defaults: ItemGrid.defaults,
|
|
78
|
+
commands: {
|
|
79
|
+
...ItemGrid.commands,
|
|
80
|
+
/** Re-flow the items into `columns` columns, each moving to its new cell. */
|
|
81
|
+
columnsTo: {
|
|
82
|
+
args: { columns: ItemGrid.hinted(4, { min: 1, step: 1 }), ...staggerArgs(TRANSITION_STAGGER) },
|
|
83
|
+
run: (timeline, s, children) => {
|
|
84
|
+
const columns = Math.max(1, Math.round(timeline.args.columns ?? s.columns))
|
|
85
|
+
return ItemGrid.transitionTo(timeline as never, s, children, { columns })
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
/** Animate the spacing between columns and rows. */
|
|
89
|
+
gapTo: {
|
|
90
|
+
args: { columnGap: ItemGrid.hinted(48), rowGap: ItemGrid.hinted(48) },
|
|
91
|
+
run: (timeline) => {
|
|
92
|
+
const data: { columnGap?: number; rowGap?: number } = {}
|
|
93
|
+
if (timeline.args.columnGap !== undefined) data.columnGap = timeline.args.columnGap
|
|
94
|
+
if (timeline.args.rowGap !== undefined) data.rowGap = timeline.args.rowGap
|
|
95
|
+
return timeline.tween(data)
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* Animate the cell size, from whatever the cells measure now. The grid
|
|
100
|
+
* switches to `fixed` cells as it starts, since a size of its own is what
|
|
101
|
+
* `fixed` means.
|
|
102
|
+
*/
|
|
103
|
+
cellSizeTo: {
|
|
104
|
+
args: { cellWidth: ItemGrid.hinted(160, { min: 0 }), cellHeight: ItemGrid.hinted(160, { min: 0 }) },
|
|
105
|
+
easing: easeInOut("cubic"),
|
|
106
|
+
run: (timeline, s, children) => {
|
|
107
|
+
const sizes = children.map((child) => child.size)
|
|
108
|
+
const box = ItemGrid.contentBox(timeline.node.size, s.padding)
|
|
109
|
+
const from = rectangularCell(arrangement(s), sizes.length, sizes, box)
|
|
110
|
+
const width = timeline.args.cellWidth ?? from.width
|
|
111
|
+
const height = timeline.args.cellHeight ?? from.height
|
|
112
|
+
return timeline.animate((t) => ({
|
|
113
|
+
cellMode: "fixed",
|
|
114
|
+
cellWidth: from.width + (width - from.width) * t,
|
|
115
|
+
cellHeight: from.height + (height - from.height) * t,
|
|
116
|
+
}))
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
/** Bring one row (from 0 at the top) forward and push the rest back. */
|
|
120
|
+
focusRow: {
|
|
121
|
+
args: { row: Types.number(0, { min: 0, step: 1 }), ...lineFocus },
|
|
122
|
+
run: (timeline, s, children) => {
|
|
123
|
+
const slots = ItemGrid.itemSlots(timeline as never, s, children)
|
|
124
|
+
const row = Math.round(timeline.args.row ?? 0)
|
|
125
|
+
const inRow = (i: number) => slots[i]?.meta.kind === "rectangular" && slots[i].meta.row === row
|
|
126
|
+
return ItemGrid.focusOn(timeline as never, s, inRow, slots.length, timeline.args.dim ?? 0.7, timeline.args.grow ?? 0.15)
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
/** Bring one column (from 0 at the left) forward and push the rest back. */
|
|
130
|
+
focusColumn: {
|
|
131
|
+
args: { column: Types.number(0, { min: 0, step: 1 }), ...lineFocus },
|
|
132
|
+
run: (timeline, s, children) => {
|
|
133
|
+
const slots = ItemGrid.itemSlots(timeline as never, s, children)
|
|
134
|
+
const column = Math.round(timeline.args.column ?? 0)
|
|
135
|
+
const inColumn = (i: number) => slots[i]?.meta.kind === "rectangular" && slots[i].meta.column === column
|
|
136
|
+
return ItemGrid.focusOn(timeline as never, s, inColumn, slots.length, timeline.args.dim ?? 0.7, timeline.args.grow ?? 0.15)
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
...ItemGrid.fields(arranger),
|
|
141
|
+
}), arranger)
|
|
142
|
+
|
|
143
|
+
export type RectangularGrid = NodeOf<typeof RectangularGrid>
|
|
144
|
+
/** What a call or a document row may set. */
|
|
145
|
+
export type RectangularGridProps = NonNullable<Parameters<typeof RectangularGrid>[0]>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./sphere-grid"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
component2d,
|
|
3
|
+
Types,
|
|
4
|
+
type NodeOf,
|
|
5
|
+
type Values,
|
|
6
|
+
// Named so the emitted declaration reaches these through the sdk, not core.
|
|
7
|
+
type Node2D as _Node2D,
|
|
8
|
+
type Node2DProps as _Node2DProps,
|
|
9
|
+
} from "@motionscript/sdk"
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ItemGrid,
|
|
13
|
+
normalizeOrder,
|
|
14
|
+
SPHERE_AXES,
|
|
15
|
+
SPHERE_FACINGS,
|
|
16
|
+
sphereFaceForward,
|
|
17
|
+
wrapDegrees,
|
|
18
|
+
type SphereArrangement,
|
|
19
|
+
type SphereAxis,
|
|
20
|
+
} from "../kit"
|
|
21
|
+
|
|
22
|
+
const own = {
|
|
23
|
+
radius: Types.number(220, { min: 0, step: 1 }),
|
|
24
|
+
yaw: Types.number(0, { step: 1, unit: "°" }),
|
|
25
|
+
pitch: Types.number(0, { step: 1, unit: "°" }),
|
|
26
|
+
roll: Types.number(0, { step: 1, unit: "°" }),
|
|
27
|
+
facing: Types.oneOf(SPHERE_FACINGS, { default: "outward" }),
|
|
28
|
+
focalLength: Types.number(900, { min: 100, step: 10 }),
|
|
29
|
+
depthFade: Types.number(0.5, { min: 0, max: 1, step: 0.05 }),
|
|
30
|
+
cullBackfaces: false,
|
|
31
|
+
spinSpeed: Types.number(0, { step: 1, unit: "°/s" }),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Own = Values<typeof own>
|
|
35
|
+
|
|
36
|
+
function arrangement(s: Own): SphereArrangement {
|
|
37
|
+
return {
|
|
38
|
+
kind: "sphere",
|
|
39
|
+
radius: s.radius,
|
|
40
|
+
yaw: s.yaw,
|
|
41
|
+
pitch: s.pitch,
|
|
42
|
+
roll: s.roll,
|
|
43
|
+
facing: s.facing,
|
|
44
|
+
focalLength: s.focalLength,
|
|
45
|
+
depthFade: s.depthFade,
|
|
46
|
+
cullBackfaces: s.cullBackfaces,
|
|
47
|
+
spinSpeed: s.spinSpeed,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const arranger: ItemGrid.Arranger<Own> = { arrangement }
|
|
52
|
+
|
|
53
|
+
const AXIS_PROP: Record<SphereAxis, "yaw" | "pitch" | "roll"> = { x: "pitch", y: "yaw", z: "roll" }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Children spread over a sphere — a Fibonacci lattice, as even as any number
|
|
57
|
+
* of points gets — and seen through a camera of the grid's own, so the far
|
|
58
|
+
* side is smaller, drawn first, and hidden where a face turns away.
|
|
59
|
+
*
|
|
60
|
+
* `outward` lays each item flat on the surface; `billboard` keeps each one
|
|
61
|
+
* facing the viewer, like labels on a globe. `turnTo` brings any item round
|
|
62
|
+
* to the front.
|
|
63
|
+
*/
|
|
64
|
+
export const SphereGrid = ItemGrid.define(component2d({
|
|
65
|
+
key: "sphereGrid",
|
|
66
|
+
state: { ...ItemGrid.state, ...own },
|
|
67
|
+
defaults: ItemGrid.defaults,
|
|
68
|
+
commands: {
|
|
69
|
+
...ItemGrid.commands,
|
|
70
|
+
/** Turn the sphere about an axis by `degrees` — `y` spins it like a globe. */
|
|
71
|
+
spin: {
|
|
72
|
+
args: {
|
|
73
|
+
axis: Types.oneOf(SPHERE_AXES, { default: "y" }),
|
|
74
|
+
degrees: Types.number(360, { step: 15, unit: "°" }),
|
|
75
|
+
},
|
|
76
|
+
run: (timeline, s) => {
|
|
77
|
+
const key = AXIS_PROP[timeline.args.axis ?? "y"] ?? "yaw"
|
|
78
|
+
return timeline.tween({ [key]: s[key] + (timeline.args.degrees ?? 360) })
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
/** Turn the sphere to a set orientation. */
|
|
82
|
+
orientTo: {
|
|
83
|
+
args: {
|
|
84
|
+
yaw: ItemGrid.hinted(45, { unit: "°" }),
|
|
85
|
+
pitch: ItemGrid.hinted(20, { unit: "°" }),
|
|
86
|
+
roll: ItemGrid.hinted(0, { unit: "°" }),
|
|
87
|
+
},
|
|
88
|
+
run: (timeline, s) => timeline.tween({
|
|
89
|
+
yaw: timeline.args.yaw ?? s.yaw,
|
|
90
|
+
pitch: timeline.args.pitch ?? s.pitch,
|
|
91
|
+
roll: timeline.args.roll ?? s.roll,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Turn the sphere the shorter way until an item (by its place in the layers)
|
|
96
|
+
* faces the viewer. It turns from the orientation the sphere is given: the
|
|
97
|
+
* yaw its `spinSpeed` adds by the time the command runs is not counted, as
|
|
98
|
+
* commands are built against the tree standing at time 0.
|
|
99
|
+
*/
|
|
100
|
+
turnTo: {
|
|
101
|
+
args: { item: Types.number(0, { min: 0, step: 1 }) },
|
|
102
|
+
run: (timeline, s, children) => {
|
|
103
|
+
const count = children.length
|
|
104
|
+
const slot = normalizeOrder(s.order, count).indexOf(Math.round(timeline.args.item ?? 0))
|
|
105
|
+
if (slot < 0) return timeline.tween({})
|
|
106
|
+
const facing = sphereFaceForward(count, slot)
|
|
107
|
+
return timeline.tween({
|
|
108
|
+
yaw: s.yaw + wrapDegrees(facing.yaw - s.yaw),
|
|
109
|
+
pitch: s.pitch + wrapDegrees(facing.pitch - s.pitch),
|
|
110
|
+
roll: 0,
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
radiusTo: {
|
|
115
|
+
args: { radius: ItemGrid.hinted(300, { min: 0 }) },
|
|
116
|
+
run: (timeline, s) => timeline.tween({ radius: timeline.args.radius ?? s.radius }),
|
|
117
|
+
},
|
|
118
|
+
/** Dolly the camera: a shorter focal length exaggerates the depth, a longer one flattens it. */
|
|
119
|
+
focalLengthTo: {
|
|
120
|
+
args: { focalLength: ItemGrid.hinted(600, { min: 100 }) },
|
|
121
|
+
run: (timeline, s) => timeline.tween({ focalLength: timeline.args.focalLength ?? s.focalLength }),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
...ItemGrid.fields(arranger),
|
|
125
|
+
}), arranger)
|
|
126
|
+
|
|
127
|
+
export type SphereGrid = NodeOf<typeof SphereGrid>
|
|
128
|
+
/** What a call or a document row may set. */
|
|
129
|
+
export type SphereGridProps = NonNullable<Parameters<typeof SphereGrid>[0]>
|