@motionscript/grids 0.1.0-alpha.8 → 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.
- package/CHANGELOG.md +2 -0
- package/LICENSE +201 -201
- package/NOTICE +49 -49
- package/README.md +118 -118
- package/dist/browser/chunks/{chunk-J62D7EFZ.js → chunk-TYNFMZ3D.js} +1 -1
- package/dist/browser/chunks/chunk-TYNFMZ3D.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/kit.js +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kit/index.js.map +1 -1
- package/dist/kit/item-effects.js.map +1 -1
- package/dist/kit/item-grid.js.map +1 -1
- package/dist/kit/order.js.map +1 -1
- package/dist/kit/random.js.map +1 -1
- package/dist/kit/slots/index.js.map +1 -1
- package/dist/kit/slots/path-templates.js.map +1 -1
- package/dist/kit/slots/path.js.map +1 -1
- package/dist/kit/slots/radial.js.map +1 -1
- package/dist/kit/slots/rectangular.js.map +1 -1
- package/dist/kit/slots/slot.js.map +1 -1
- package/dist/kit/slots/sphere.js.map +1 -1
- package/dist/kit/stagger.js.map +1 -1
- package/dist/nodes.js.map +1 -1
- package/dist/path-grid/index.js.map +1 -1
- package/dist/path-grid/path-grid.js.map +1 -1
- package/dist/radial-grid/index.js.map +1 -1
- package/dist/radial-grid/radial-grid.js.map +1 -1
- package/dist/rectangular-grid/index.js.map +1 -1
- package/dist/rectangular-grid/rectangular-grid.js.map +1 -1
- package/dist/sphere-grid/index.js.map +1 -1
- package/dist/sphere-grid/sphere-grid.js.map +1 -1
- package/package.json +4 -4
- package/registry.json +32 -32
- package/src/engine.ts +12 -12
- package/src/index.ts +6 -6
- package/src/kit/index.ts +18 -18
- package/src/kit/item-effects.ts +114 -114
- package/src/kit/item-grid.ts +648 -648
- package/src/kit/order.ts +41 -41
- package/src/kit/random.ts +29 -29
- package/src/kit/slots/index.ts +39 -39
- package/src/kit/slots/path-templates.ts +120 -120
- package/src/kit/slots/path.ts +120 -120
- package/src/kit/slots/radial.ts +62 -62
- package/src/kit/slots/rectangular.ts +108 -108
- package/src/kit/slots/slot.ts +82 -82
- package/src/kit/slots/sphere.ts +94 -94
- package/src/kit/stagger.ts +96 -96
- package/src/nodes.ts +30 -30
- package/src/path-grid/index.ts +1 -1
- package/src/path-grid/path-grid.ts +186 -186
- package/src/radial-grid/index.ts +1 -1
- package/src/radial-grid/radial-grid.ts +81 -81
- package/src/rectangular-grid/index.ts +1 -1
- package/src/rectangular-grid/rectangular-grid.ts +145 -145
- package/src/sphere-grid/index.ts +1 -1
- package/src/sphere-grid/sphere-grid.ts +129 -129
- package/dist/browser/chunks/chunk-J62D7EFZ.js.map +0 -7
package/src/kit/slots/slot.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/kit/slots/sphere.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/kit/stagger.ts
CHANGED
|
@@ -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
|
+
]
|
package/src/path-grid/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./path-grid"
|
|
1
|
+
export * from "./path-grid"
|