@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,145 +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]>
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]>
@@ -1 +1 @@
1
- export * from "./sphere-grid"
1
+ export * from "./sphere-grid"
@@ -1,129 +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]>
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]>