@motionscript/grids 0.1.0-alpha.4 → 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 +79 -41
  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
package/src/nodes.ts CHANGED
@@ -7,9 +7,24 @@ import { SphereGrid } from "./sphere-grid"
7
7
  * Every node type this package publishes.
8
8
  *
9
9
  * A host registers these by handing the array to an engine —
10
- * `new Engine(platform, { nodes: NODES })` — which reads each class's `@node()`
11
- * key. Listing class **values** is also what survives bundling: this package
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
12
15
  * declares `sideEffects: false`, and a document names a node type by string, so
13
16
  * an import kept only for its side effect is exactly what gets shaken out.
14
17
  */
15
- export const NODES = [RectangularGrid, RadialGrid, PathGrid, SphereGrid]
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,213 +1,186 @@
1
1
  import {
2
- command,
3
- node,
4
- Path,
2
+ component2d,
5
3
  pathOps,
6
- PathShape,
7
- pathProperty,
8
- property,
9
- type Command,
10
- type CommandArgs,
11
- type NodeConfig,
4
+ Types,
5
+ type NodeOf,
6
+ type NodeView,
12
7
  type PathData,
13
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,
14
13
  } from "@motionscript/sdk"
15
14
 
16
15
  import {
17
- findNode2D,
18
- frameIn,
19
16
  ItemGrid,
20
17
  PATH_SPACINGS,
21
18
  PATH_TEMPLATES,
22
19
  staggerArgs,
23
20
  TRANSITION_STAGGER,
24
21
  templatePath,
25
- type ItemGridProps,
26
22
  type PathArrangement,
27
23
  type PathPlacement,
28
- type PathSpacing,
29
24
  type PathTemplate,
30
- type StaggerArgs,
31
25
  } from "../kit"
32
26
 
33
- /** A template, or `custom` for the grid's own {@link PathGrid.path}. */
27
+ /** A template, or `custom` for the grid's own `path`. */
34
28
  export const PATH_GRID_SHAPES = [...PATH_TEMPLATES, "custom"] as const
35
29
  export type PathGridShape = (typeof PATH_GRID_SHAPES)[number]
36
30
 
37
- export interface PathGridProps extends ItemGridProps {
38
- shape: PathGridShape
39
- path: PathData
40
- pathNode: string
41
- spacing: PathSpacing
42
- spacingDistance: number
43
- offset: number
44
- flowSpeed: number
45
- trimStart: number
46
- trimEnd: number
47
- alignToPath: boolean
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,
48
46
  }
49
47
 
48
+ type Own = Values<typeof own>
49
+
50
50
  const AT_ORIGIN: PathPlacement = { originX: 0, originY: 0, x: 0, y: 0, rotation: 0, scale: 1 }
51
51
 
52
52
  /** The size a path grid is placed at when nothing says otherwise, since a template is drawn to fill it. */
53
53
  const DEFAULT_SIZE = { width: 640, height: 400 }
54
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
+
55
73
  /**
56
74
  * Children along a path: one of the templates drawn to fill the node's box,
57
- * a path of the grid's own, or a `Path` node elsewhere in the scene, followed
75
+ * a path of the grid's own, or a path node elsewhere in the scene, followed
58
76
  * wherever it is.
59
77
  *
60
78
  * Items are spaced by distance along the curve, so they sit evenly on a tight
61
- * bend and a straight alike. Animating {@link offset} moves them along it — on
62
- * a closed path they come round again, which is a conveyor belt for free.
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.
63
81
  */
64
- @node({
82
+ export const PathGrid = ItemGrid.define(component2d({
65
83
  key: "pathGrid",
66
- parentKey: "node",
67
- forkable: true,
68
- layout: {
69
- children: "freeform",
70
- acceptsChildren: true,
71
- defaultWidthMode: "fixed",
72
- defaultHeightMode: "fixed",
73
- defaultWidth: DEFAULT_SIZE.width,
74
- defaultHeight: DEFAULT_SIZE.height,
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
+ },
75
123
  },
76
- seed: { ...DEFAULT_SIZE },
77
- })
78
- export class PathGrid extends ItemGrid<PathGridProps> {
79
- @property({ default: "circle", kind: "enum", options: PATH_GRID_SHAPES }) declare shape: PathGridShape
80
- /** Used when {@link shape} is `custom`: an SVG path, y-down as SVG writes one, centred on its own bounds. */
81
- @pathProperty() declare path: PathData
82
- /** A path node to follow instead, by id — the items track it wherever it goes. */
83
- @property({ default: "", kind: "nodeRef", nodeTypes: ["path"], animatable: false }) declare pathNode: string
84
- @property({ default: "distribute", kind: "enum", options: PATH_SPACINGS }) declare spacing: PathSpacing
85
- @property({ default: 80, min: 0, step: 1 }) declare spacingDistance: number
86
- /** Share of the path the items are moved along — `1` is once round. */
87
- @property({ default: 0, step: 0.01 }) declare offset: number
88
- /** Share of the path per second the items travel on their own. */
89
- @property({ default: 0, step: 0.01, unit: "/s" }) declare flowSpeed: number
90
- @property({ default: 0, min: 0, max: 1, step: 0.01 }) declare trimStart: number
91
- @property({ default: 1, min: 0, max: 1, step: 0.01 }) declare trimEnd: number
92
- @property({ default: true }) declare alignToPath: boolean
124
+ ...ItemGrid.fields(arranger),
125
+ }), arranger)
93
126
 
94
- constructor(props?: NodeConfig<PathGrid, PathGridProps>) {
95
- super(props)
96
- }
97
-
98
- /** Move the items along the path by `loops` — `1` is once round, and they keep their spacing. */
99
- @command({ args: [{ key: "loops", step: 0.25, default: 1 }] })
100
- flow(args: CommandArgs<{ loops?: number }> & { duration: number }): Command<PathGridProps> {
101
- const loops = args.data?.loops ?? 1
102
- return this.to({ data: { offset: this.offset + loops }, duration: args.duration, easing: args.easing })
103
- }
104
-
105
- /** Squeeze the items into part of the path, between two shares of its length. */
106
- @command({
107
- args: [
108
- { key: "trimStart", min: 0, max: 1, step: 0.01, default: 0.25 },
109
- { key: "trimEnd", min: 0, max: 1, step: 0.01, default: 0.75 },
110
- ],
111
- })
112
- trimTo(args: CommandArgs<{ trimStart?: number; trimEnd?: number }> & { duration: number }): Command<PathGridProps> {
113
- return this.to({
114
- data: { trimStart: args.data?.trimStart ?? this.trimStart, trimEnd: args.data?.trimEnd ?? this.trimEnd },
115
- duration: args.duration,
116
- easing: args.easing,
117
- })
118
- }
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]>
119
130
 
120
- /**
121
- * Move the items onto another path — a template, or a `Path` node in the
122
- * scene. Each item travels from its place on the old path to its place on the
123
- * new one; the path itself is not morphed.
124
- */
125
- @command({
126
- args: [
127
- { key: "shape", kind: "enum", options: PATH_GRID_SHAPES, default: "heart" },
128
- { key: "pathNode", kind: "nodeRef", nodeTypes: ["path"] },
129
- ...staggerArgs(TRANSITION_STAGGER),
130
- ],
131
- })
132
- setPath(args: CommandArgs<{ shape?: PathGridShape; pathNode?: string } & StaggerArgs> & { duration: number }): Command<PathGridProps> {
133
- const pathNode = args.data?.pathNode ?? ""
134
- const changes: Partial<PathGridProps> = pathNode ? { pathNode } : { shape: args.data?.shape ?? this.shape, pathNode: "" }
135
- return this._transitionTo(changes, args)
136
- }
137
-
138
- protected override arrangementFor(box: Size2D): PathArrangement {
139
- const { path, placement } = this._pathIn(box)
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
140
  return {
141
- kind: "path",
142
- path,
143
- placement,
144
- spacing: this.spacing,
145
- spacingDistance: this.spacingDistance,
146
- offset: this.offset,
147
- flowSpeed: this.flowSpeed,
148
- trimStart: this.trimStart,
149
- trimEnd: this.trimEnd,
150
- alignToPath: this.alignToPath,
141
+ path: data,
142
+ placement: {
143
+ originX: (bounds.minX + bounds.maxX) / 2,
144
+ originY: (bounds.minY + bounds.maxY) / 2,
145
+ ...linked.frame,
146
+ },
151
147
  }
152
148
  }
153
-
154
- // A template is drawn to fill the box, so a path grid needs one of its own
155
- // rather than hugging items whose places depend on it.
156
- protected override applyDefaultSize(props?: { readonly width?: unknown; readonly height?: unknown }): void {
157
- if (!props || props.width === undefined) this.applyProp("width", DEFAULT_SIZE.width)
158
- if (!props || props.height === undefined) this.applyProp("height", DEFAULT_SIZE.height)
159
- }
160
-
161
- private _linked: { data: PathData; width: number; height: number; path: PathData } | null = null
162
-
163
- /** The path the items follow, and where its coordinates land in this grid's frame. */
164
- private _pathIn(box: Size2D): { path: PathData; placement: PathPlacement } {
165
- const linked = this.pathNode ? findNode2D(this, this.pathNode) : null
166
- if (linked instanceof PathShape) {
167
- return { path: this._unitPath(linked), placement: { ...AT_ORIGIN, ...frameIn(this, linked) } }
168
- }
169
- if (linked instanceof Path && pathOps.bounds(linked.data)) {
170
- // A `Path` draws its data centred on the data's own bounds, at its own position.
171
- const bounds = pathOps.bounds(linked.data)!
172
- const frame = frameIn(this, linked)
149
+ if (s.shape === "custom") {
150
+ const own = pathOps.bounds(s.path)
151
+ if (own) {
173
152
  return {
174
- path: linked.data,
153
+ path: s.path,
175
154
  placement: {
176
- originX: (bounds.minX + bounds.maxX) / 2,
177
- originY: (bounds.minY + bounds.maxY) / 2,
178
- ...frame,
155
+ ...AT_ORIGIN,
156
+ originX: (own.minX + own.maxX) / 2,
157
+ originY: (own.minY + own.maxY) / 2,
158
+ flipY: true,
179
159
  },
180
160
  }
181
161
  }
182
- if (this.shape === "custom") {
183
- const bounds = pathOps.bounds(this.path)
184
- if (bounds) {
185
- return {
186
- path: this.path,
187
- placement: {
188
- ...AT_ORIGIN,
189
- originX: (bounds.minX + bounds.maxX) / 2,
190
- originY: (bounds.minY + bounds.maxY) / 2,
191
- flipY: true,
192
- },
193
- }
194
- }
195
- }
196
- const template: PathTemplate = this.shape === "custom" ? "circle" : this.shape
197
- return { path: templatePath(template, box), placement: AT_ORIGIN }
198
162
  }
163
+ const template: PathTemplate = s.shape === "custom" ? "circle" : s.shape
164
+ return { path: templatePath(template, box), placement: AT_ORIGIN }
165
+ }
199
166
 
200
- /**
201
- * A path node's geometry as it draws it: stored in its unit box, y-down, and
202
- * scaled to its laid-out size — kept while neither changes, so the path's
203
- * measurement is kept with it.
204
- */
205
- private _unitPath(shape: PathShape): PathData {
206
- const { width, height } = shape.layoutBounds
207
- const cached = this._linked
208
- if (cached && cached.data === shape.data && cached.width === width && cached.height === height) return cached.path
209
- const path = pathOps.anchorCommands(pathOps.anchors(shape.data), (p) => ({ x: p.x * width, y: -p.y * height }))
210
- this._linked = { data: shape.data, width, height, path }
211
- return path
212
- }
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
213
186
  }
@@ -1,90 +1,81 @@
1
- import { command, node, property, type Command, type CommandArgs, type NodeConfig } from "@motionscript/sdk"
2
-
3
1
  import {
4
- ItemGrid,
5
- normalizeOrder,
6
- RADIAL_DIRECTIONS,
7
- radialTurnToTop,
8
- type ItemGridProps,
9
- type RadialArrangement,
10
- type RadialDirection,
11
- } from "../kit"
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>
12
23
 
13
- export interface RadialGridProps extends ItemGridProps {
14
- radius: number
15
- startAngle: number
16
- sweep: number
17
- direction: RadialDirection
18
- rotateItems: boolean
19
- spinSpeed: number
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
+ }
20
34
  }
21
35
 
36
+ const arranger: ItemGrid.Arranger<Own> = { arrangement }
37
+
22
38
  /**
23
39
  * Children round a circle, or along an arc of one.
24
40
  *
25
41
  * A full circle spaces the items evenly with none on top of another; an arc
26
- * puts one at each end. Angles run clockwise from twelve o'clock, so
27
- * {@link turnTo} can bring any item to the top like a dial.
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.
28
44
  */
29
- @node({
45
+ export const RadialGrid = ItemGrid.define(component2d({
30
46
  key: "radialGrid",
31
- parentKey: "node",
32
- forkable: true,
33
- layout: {
34
- children: "freeform",
35
- acceptsChildren: true,
36
- defaultWidthMode: "hug",
37
- defaultHeightMode: "hug",
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
+ },
38
75
  },
39
- })
40
- export class RadialGrid extends ItemGrid<RadialGridProps> {
41
- @property({ default: 200, min: 0, step: 1 }) declare radius: number
42
- @property({ default: 0, step: 1, unit: "°" }) declare startAngle: number
43
- @property({ default: 360, min: -360, max: 360, step: 1, unit: "°" }) declare sweep: number
44
- @property({ default: "clockwise", kind: "enum", options: RADIAL_DIRECTIONS }) declare direction: RadialDirection
45
- @property({ default: false }) declare rotateItems: boolean
46
- @property({ default: 0, step: 1, unit: "°/s" }) declare spinSpeed: number
76
+ ...ItemGrid.fields(arranger),
77
+ }), arranger)
47
78
 
48
- constructor(props?: NodeConfig<RadialGrid, RadialGridProps>) {
49
- super(props)
50
- }
51
-
52
- /** Orbit the items round the centre by `degrees` — `360` is one full turn. */
53
- @command({ args: [{ key: "degrees", step: 15, unit: "°", default: 360 }] })
54
- spin(args: CommandArgs<{ degrees?: number }> & { duration: number }): Command<RadialGridProps> {
55
- const degrees = args.data?.degrees ?? 360
56
- return this.to({ data: { startAngle: this.startAngle + degrees }, duration: args.duration, easing: args.easing })
57
- }
58
-
59
- /** Turn the ring the shorter way round until an item (by its place in the layers) sits at the top. */
60
- @command({ args: [{ key: "item", min: 0, step: 1, default: 0 }] })
61
- turnTo(args: CommandArgs<{ item?: number }> & { duration: number }): Command<RadialGridProps> {
62
- const count = this._itemCount()
63
- const slot = normalizeOrder(this.order, count).indexOf(Math.round(args.data?.item ?? 0))
64
- const turn = slot < 0 ? 0 : radialTurnToTop(this.arrangementFor(), count, slot)
65
- return this.to({ data: { startAngle: this.startAngle + turn }, duration: args.duration, easing: args.easing })
66
- }
67
-
68
- @command({ args: [{ key: "radius", min: 0, default: 280 }] })
69
- radiusTo(args: CommandArgs<{ radius: number }> & { duration: number }): Command<RadialGridProps> {
70
- return this.to({ data: { radius: args.data?.radius ?? this.radius }, duration: args.duration, easing: args.easing })
71
- }
72
-
73
- /** Open or close the arc like a fan — from `0` to `360` is a reveal of its own. */
74
- @command({ args: [{ key: "sweep", min: -360, max: 360, unit: "°", default: 360 }] })
75
- sweepTo(args: CommandArgs<{ sweep: number }> & { duration: number }): Command<RadialGridProps> {
76
- return this.to({ data: { sweep: args.data?.sweep ?? this.sweep }, duration: args.duration, easing: args.easing })
77
- }
78
-
79
- protected override arrangementFor(): RadialArrangement {
80
- return {
81
- kind: "radial",
82
- radius: this.radius,
83
- startAngle: this.startAngle,
84
- sweep: this.sweep,
85
- direction: this.direction,
86
- rotateItems: this.rotateItems,
87
- spinSpeed: this.spinSpeed,
88
- }
89
- }
90
- }
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]>