@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.
- package/CHANGELOG.md +4 -0
- package/LICENSE +201 -201
- package/NOTICE +49 -48
- 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/browser/manifest.json +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 +5 -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/order.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A grid's `order` lists items in slot order: slot `k` holds item `order[k]`.
|
|
3
|
-
* These keep it a permutation whatever it was written as — a stale entry past
|
|
4
|
-
* the count, a repeat, or nothing at all — so a child added or removed never
|
|
5
|
-
* leaves a slot empty or two items in one.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** `order` repaired into a permutation of `0..count-1`: its valid entries first, then the rest in item order. */
|
|
9
|
-
export function normalizeOrder(order: readonly number[] | undefined, count: number): number[] {
|
|
10
|
-
const out: number[] = []
|
|
11
|
-
const taken = new Array<boolean>(count).fill(false)
|
|
12
|
-
for (const entry of order ?? []) {
|
|
13
|
-
const item = Math.round(entry)
|
|
14
|
-
if (item >= 0 && item < count && !taken[item]) {
|
|
15
|
-
taken[item] = true
|
|
16
|
-
out.push(item)
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
for (let item = 0; item < count; item++) if (!taken[item]) out.push(item)
|
|
20
|
-
return out
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** For each item, the slot it sits in. */
|
|
24
|
-
export function slotOfItem(order: readonly number[]): number[] {
|
|
25
|
-
const slots = new Array<number>(order.length)
|
|
26
|
-
order.forEach((item, slot) => {
|
|
27
|
-
slots[item] = slot
|
|
28
|
-
})
|
|
29
|
-
return slots
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** An order written as `"2, 0, 1"` or as numbers. */
|
|
33
|
-
export function parseOrder(value: unknown): number[] {
|
|
34
|
-
if (Array.isArray(value)) return value.map(Number).filter(Number.isFinite)
|
|
35
|
-
if (typeof value !== "string") return []
|
|
36
|
-
return value
|
|
37
|
-
.split(/[\s,]+/)
|
|
38
|
-
.filter((part) => part !== "")
|
|
39
|
-
.map(Number)
|
|
40
|
-
.filter(Number.isFinite)
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* A grid's `order` lists items in slot order: slot `k` holds item `order[k]`.
|
|
3
|
+
* These keep it a permutation whatever it was written as — a stale entry past
|
|
4
|
+
* the count, a repeat, or nothing at all — so a child added or removed never
|
|
5
|
+
* leaves a slot empty or two items in one.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** `order` repaired into a permutation of `0..count-1`: its valid entries first, then the rest in item order. */
|
|
9
|
+
export function normalizeOrder(order: readonly number[] | undefined, count: number): number[] {
|
|
10
|
+
const out: number[] = []
|
|
11
|
+
const taken = new Array<boolean>(count).fill(false)
|
|
12
|
+
for (const entry of order ?? []) {
|
|
13
|
+
const item = Math.round(entry)
|
|
14
|
+
if (item >= 0 && item < count && !taken[item]) {
|
|
15
|
+
taken[item] = true
|
|
16
|
+
out.push(item)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (let item = 0; item < count; item++) if (!taken[item]) out.push(item)
|
|
20
|
+
return out
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** For each item, the slot it sits in. */
|
|
24
|
+
export function slotOfItem(order: readonly number[]): number[] {
|
|
25
|
+
const slots = new Array<number>(order.length)
|
|
26
|
+
order.forEach((item, slot) => {
|
|
27
|
+
slots[item] = slot
|
|
28
|
+
})
|
|
29
|
+
return slots
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** An order written as `"2, 0, 1"` or as numbers. */
|
|
33
|
+
export function parseOrder(value: unknown): number[] {
|
|
34
|
+
if (Array.isArray(value)) return value.map(Number).filter(Number.isFinite)
|
|
35
|
+
if (typeof value !== "string") return []
|
|
36
|
+
return value
|
|
37
|
+
.split(/[\s,]+/)
|
|
38
|
+
.filter((part) => part !== "")
|
|
39
|
+
.map(Number)
|
|
40
|
+
.filter(Number.isFinite)
|
|
41
|
+
}
|
package/src/kit/random.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
/** A seeded generator in `[0, 1)` — mulberry32, so a shuffle is the same on every frame and every machine. */
|
|
2
|
-
export function seededRandom(seed: number): () => number {
|
|
3
|
-
let a = Math.floor(seed) >>> 0
|
|
4
|
-
return () => {
|
|
5
|
-
a = (a + 0x6d2b79f5) >>> 0
|
|
6
|
-
let t = a
|
|
7
|
-
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
8
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
9
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** `values` in a seeded random order. */
|
|
14
|
-
export function shuffled<T>(values: readonly T[], seed: number): T[] {
|
|
15
|
-
const out = [...values]
|
|
16
|
-
const random = seededRandom(seed)
|
|
17
|
-
for (let i = out.length - 1; i > 0; i--) {
|
|
18
|
-
const j = Math.floor(random() * (i + 1))
|
|
19
|
-
;[out[i], out[j]] = [out[j], out[i]]
|
|
20
|
-
}
|
|
21
|
-
return out
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** A stable number for a string — a node's id, so its "random" stagger never changes between builds. */
|
|
25
|
-
export function hashSeed(text: string): number {
|
|
26
|
-
let h = 2166136261
|
|
27
|
-
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619)
|
|
28
|
-
return h >>> 0
|
|
29
|
-
}
|
|
1
|
+
/** A seeded generator in `[0, 1)` — mulberry32, so a shuffle is the same on every frame and every machine. */
|
|
2
|
+
export function seededRandom(seed: number): () => number {
|
|
3
|
+
let a = Math.floor(seed) >>> 0
|
|
4
|
+
return () => {
|
|
5
|
+
a = (a + 0x6d2b79f5) >>> 0
|
|
6
|
+
let t = a
|
|
7
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
8
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
9
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `values` in a seeded random order. */
|
|
14
|
+
export function shuffled<T>(values: readonly T[], seed: number): T[] {
|
|
15
|
+
const out = [...values]
|
|
16
|
+
const random = seededRandom(seed)
|
|
17
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
18
|
+
const j = Math.floor(random() * (i + 1))
|
|
19
|
+
;[out[i], out[j]] = [out[j], out[i]]
|
|
20
|
+
}
|
|
21
|
+
return out
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A stable number for a string — a node's id, so its "random" stagger never changes between builds. */
|
|
25
|
+
export function hashSeed(text: string): number {
|
|
26
|
+
let h = 2166136261
|
|
27
|
+
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619)
|
|
28
|
+
return h >>> 0
|
|
29
|
+
}
|
package/src/kit/slots/index.ts
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import { pathSlots, type PathArrangement } from "./path"
|
|
2
|
-
import { radialSlots, type RadialArrangement } from "./radial"
|
|
3
|
-
import { rectangularSlots, type RectangularArrangement } from "./rectangular"
|
|
4
|
-
import type { Slot, SlotInput } from "./slot"
|
|
5
|
-
import { sphereSlots, type SphereArrangement } from "./sphere"
|
|
6
|
-
|
|
7
|
-
export * from "./slot"
|
|
8
|
-
export * from "./rectangular"
|
|
9
|
-
export * from "./radial"
|
|
10
|
-
export * from "./path"
|
|
11
|
-
export * from "./path-templates"
|
|
12
|
-
export * from "./sphere"
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Everything an arrangement needs to place items, as plain data — so a
|
|
16
|
-
* transition can keep the one it is leaving, and a grid can borrow another's.
|
|
17
|
-
*/
|
|
18
|
-
export type Arrangement = RectangularArrangement | RadialArrangement | PathArrangement | SphereArrangement
|
|
19
|
-
|
|
20
|
-
export type ArrangementKind = Arrangement["kind"]
|
|
21
|
-
|
|
22
|
-
/** Where each of `input.count` items sits under `arrangement`, in slot order. */
|
|
23
|
-
export function slotsOf(arrangement: Arrangement, input: SlotInput): Slot[] {
|
|
24
|
-
switch (arrangement.kind) {
|
|
25
|
-
case "rectangular":
|
|
26
|
-
return rectangularSlots(arrangement, input)
|
|
27
|
-
case "radial":
|
|
28
|
-
return radialSlots(arrangement, input)
|
|
29
|
-
case "path":
|
|
30
|
-
return pathSlots(arrangement, input)
|
|
31
|
-
case "sphere":
|
|
32
|
-
return sphereSlots(arrangement, input)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The camera distance an arrangement is seen through, or `0` when it is flat. */
|
|
37
|
-
export function focalLengthOf(arrangement: Arrangement): number {
|
|
38
|
-
return arrangement.kind === "sphere" ? Math.max(1, arrangement.focalLength) : 0
|
|
39
|
-
}
|
|
1
|
+
import { pathSlots, type PathArrangement } from "./path"
|
|
2
|
+
import { radialSlots, type RadialArrangement } from "./radial"
|
|
3
|
+
import { rectangularSlots, type RectangularArrangement } from "./rectangular"
|
|
4
|
+
import type { Slot, SlotInput } from "./slot"
|
|
5
|
+
import { sphereSlots, type SphereArrangement } from "./sphere"
|
|
6
|
+
|
|
7
|
+
export * from "./slot"
|
|
8
|
+
export * from "./rectangular"
|
|
9
|
+
export * from "./radial"
|
|
10
|
+
export * from "./path"
|
|
11
|
+
export * from "./path-templates"
|
|
12
|
+
export * from "./sphere"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Everything an arrangement needs to place items, as plain data — so a
|
|
16
|
+
* transition can keep the one it is leaving, and a grid can borrow another's.
|
|
17
|
+
*/
|
|
18
|
+
export type Arrangement = RectangularArrangement | RadialArrangement | PathArrangement | SphereArrangement
|
|
19
|
+
|
|
20
|
+
export type ArrangementKind = Arrangement["kind"]
|
|
21
|
+
|
|
22
|
+
/** Where each of `input.count` items sits under `arrangement`, in slot order. */
|
|
23
|
+
export function slotsOf(arrangement: Arrangement, input: SlotInput): Slot[] {
|
|
24
|
+
switch (arrangement.kind) {
|
|
25
|
+
case "rectangular":
|
|
26
|
+
return rectangularSlots(arrangement, input)
|
|
27
|
+
case "radial":
|
|
28
|
+
return radialSlots(arrangement, input)
|
|
29
|
+
case "path":
|
|
30
|
+
return pathSlots(arrangement, input)
|
|
31
|
+
case "sphere":
|
|
32
|
+
return sphereSlots(arrangement, input)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The camera distance an arrangement is seen through, or `0` when it is flat. */
|
|
37
|
+
export function focalLengthOf(arrangement: Arrangement): number {
|
|
38
|
+
return arrangement.kind === "sphere" ? Math.max(1, arrangement.focalLength) : 0
|
|
39
|
+
}
|
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
import type { PathCommand, Size2D } from "@motionscript/sdk"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The shapes a path grid can follow without a path of its own, each drawn to
|
|
5
|
-
* fill the grid's box. Centred and y-up, like every path in the library; the
|
|
6
|
-
* closed ones run clockwise, so an item flowing along any of them travels the
|
|
7
|
-
* same way round.
|
|
8
|
-
*/
|
|
9
|
-
export const PATH_TEMPLATES = [
|
|
10
|
-
"circle",
|
|
11
|
-
"square",
|
|
12
|
-
"star",
|
|
13
|
-
"heart",
|
|
14
|
-
"infinity",
|
|
15
|
-
"wave",
|
|
16
|
-
"spiral",
|
|
17
|
-
"arc",
|
|
18
|
-
"line",
|
|
19
|
-
] as const
|
|
20
|
-
export type PathTemplate = (typeof PATH_TEMPLATES)[number]
|
|
21
|
-
|
|
22
|
-
/** What a template is drawn at when the grid has no box to fill. */
|
|
23
|
-
const FALLBACK = 400
|
|
24
|
-
|
|
25
|
-
const DRAWN = new Map<string, PathCommand[]>()
|
|
26
|
-
const DRAWN_CACHE = 64
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* `template` drawn to fill `box`. The same template at the same size is the
|
|
30
|
-
* same array, which is what lets the path's measurement be kept between frames.
|
|
31
|
-
*/
|
|
32
|
-
export function templatePath(template: PathTemplate, box: Size2D): PathCommand[] {
|
|
33
|
-
const key = `${template}:${Math.round(box.width * 100)}:${Math.round(box.height * 100)}`
|
|
34
|
-
const hit = DRAWN.get(key)
|
|
35
|
-
if (hit) return hit
|
|
36
|
-
const path = drawTemplate(template, box)
|
|
37
|
-
if (DRAWN.size >= DRAWN_CACHE) DRAWN.delete(DRAWN.keys().next().value!)
|
|
38
|
-
DRAWN.set(key, path)
|
|
39
|
-
return path
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function drawTemplate(template: PathTemplate, box: Size2D): PathCommand[] {
|
|
43
|
-
const w = box.width > 0 ? box.width : FALLBACK
|
|
44
|
-
const h = box.height > 0 ? box.height : FALLBACK
|
|
45
|
-
const rx = w / 2
|
|
46
|
-
const ry = h / 2
|
|
47
|
-
switch (template) {
|
|
48
|
-
case "square":
|
|
49
|
-
return closed([
|
|
50
|
-
[-rx, ry], [rx, ry], [rx, -ry], [-rx, -ry],
|
|
51
|
-
])
|
|
52
|
-
case "star": {
|
|
53
|
-
const points: [number, number][] = []
|
|
54
|
-
for (let i = 0; i < 10; i++) {
|
|
55
|
-
const angle = (i * Math.PI) / 5
|
|
56
|
-
const r = i % 2 === 0 ? 1 : 0.45
|
|
57
|
-
points.push([Math.sin(angle) * rx * r, Math.cos(angle) * ry * r])
|
|
58
|
-
}
|
|
59
|
-
return closed(points)
|
|
60
|
-
}
|
|
61
|
-
case "heart":
|
|
62
|
-
return closed(sample(96, (t) => {
|
|
63
|
-
// The classic heart curve, from its top notch, clockwise.
|
|
64
|
-
const a = t * 2 * Math.PI
|
|
65
|
-
const x = 16 * Math.sin(a) ** 3
|
|
66
|
-
const y = 13 * Math.cos(a) - 5 * Math.cos(2 * a) - 2 * Math.cos(3 * a) - Math.cos(4 * a)
|
|
67
|
-
return [(x / 17) * rx, ((y + 2.5) / 15.5) * ry]
|
|
68
|
-
}).slice(0, -1))
|
|
69
|
-
case "infinity":
|
|
70
|
-
return closed(sample(96, (t) => {
|
|
71
|
-
const a = t * 2 * Math.PI
|
|
72
|
-
const d = 1 + Math.sin(a) ** 2
|
|
73
|
-
return [(Math.cos(a) / d) * rx, ((Math.sin(a) * Math.cos(a)) / d) * ry * 2]
|
|
74
|
-
}).slice(0, -1))
|
|
75
|
-
case "wave":
|
|
76
|
-
return open(sample(96, (t) => [(-1 + 2 * t) * rx, Math.sin(t * 4 * Math.PI) * ry * 0.6]))
|
|
77
|
-
case "spiral":
|
|
78
|
-
return open(sample(160, (t) => {
|
|
79
|
-
const a = t * 3 * 2 * Math.PI
|
|
80
|
-
const r = 0.08 + 0.92 * t
|
|
81
|
-
return [Math.sin(a) * rx * r, Math.cos(a) * ry * r]
|
|
82
|
-
}))
|
|
83
|
-
case "arc":
|
|
84
|
-
return open(sample(64, (t) => {
|
|
85
|
-
const a = -Math.PI / 2 + t * Math.PI
|
|
86
|
-
return [Math.sin(a) * rx, -ry + Math.cos(a) * h]
|
|
87
|
-
}))
|
|
88
|
-
case "line":
|
|
89
|
-
return [{ type: "M", x: -rx, y: 0 }, { type: "L", x: rx, y: 0 }]
|
|
90
|
-
default:
|
|
91
|
-
return ellipse(rx, ry)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Four quarter-arcs from the top, clockwise. */
|
|
96
|
-
function ellipse(rx: number, ry: number): PathCommand[] {
|
|
97
|
-
const k = 0.5522847498
|
|
98
|
-
return [
|
|
99
|
-
{ type: "M", x: 0, y: ry },
|
|
100
|
-
{ type: "C", x1: k * rx, y1: ry, x2: rx, y2: k * ry, x: rx, y: 0 },
|
|
101
|
-
{ type: "C", x1: rx, y1: -k * ry, x2: k * rx, y2: -ry, x: 0, y: -ry },
|
|
102
|
-
{ type: "C", x1: -k * rx, y1: -ry, x2: -rx, y2: -k * ry, x: -rx, y: 0 },
|
|
103
|
-
{ type: "C", x1: -rx, y1: k * ry, x2: -k * rx, y2: ry, x: 0, y: ry },
|
|
104
|
-
{ type: "Z" },
|
|
105
|
-
]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function sample(steps: number, at: (t: number) => [number, number]): [number, number][] {
|
|
109
|
-
const points: [number, number][] = []
|
|
110
|
-
for (let i = 0; i <= steps; i++) points.push(at(i / steps))
|
|
111
|
-
return points
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function open(points: [number, number][]): PathCommand[] {
|
|
115
|
-
return points.map(([x, y], i) => (i === 0 ? { type: "M", x, y } : { type: "L", x, y }))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function closed(points: [number, number][]): PathCommand[] {
|
|
119
|
-
return [...open(points), { type: "Z" }]
|
|
120
|
-
}
|
|
1
|
+
import type { PathCommand, Size2D } from "@motionscript/sdk"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shapes a path grid can follow without a path of its own, each drawn to
|
|
5
|
+
* fill the grid's box. Centred and y-up, like every path in the library; the
|
|
6
|
+
* closed ones run clockwise, so an item flowing along any of them travels the
|
|
7
|
+
* same way round.
|
|
8
|
+
*/
|
|
9
|
+
export const PATH_TEMPLATES = [
|
|
10
|
+
"circle",
|
|
11
|
+
"square",
|
|
12
|
+
"star",
|
|
13
|
+
"heart",
|
|
14
|
+
"infinity",
|
|
15
|
+
"wave",
|
|
16
|
+
"spiral",
|
|
17
|
+
"arc",
|
|
18
|
+
"line",
|
|
19
|
+
] as const
|
|
20
|
+
export type PathTemplate = (typeof PATH_TEMPLATES)[number]
|
|
21
|
+
|
|
22
|
+
/** What a template is drawn at when the grid has no box to fill. */
|
|
23
|
+
const FALLBACK = 400
|
|
24
|
+
|
|
25
|
+
const DRAWN = new Map<string, PathCommand[]>()
|
|
26
|
+
const DRAWN_CACHE = 64
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `template` drawn to fill `box`. The same template at the same size is the
|
|
30
|
+
* same array, which is what lets the path's measurement be kept between frames.
|
|
31
|
+
*/
|
|
32
|
+
export function templatePath(template: PathTemplate, box: Size2D): PathCommand[] {
|
|
33
|
+
const key = `${template}:${Math.round(box.width * 100)}:${Math.round(box.height * 100)}`
|
|
34
|
+
const hit = DRAWN.get(key)
|
|
35
|
+
if (hit) return hit
|
|
36
|
+
const path = drawTemplate(template, box)
|
|
37
|
+
if (DRAWN.size >= DRAWN_CACHE) DRAWN.delete(DRAWN.keys().next().value!)
|
|
38
|
+
DRAWN.set(key, path)
|
|
39
|
+
return path
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function drawTemplate(template: PathTemplate, box: Size2D): PathCommand[] {
|
|
43
|
+
const w = box.width > 0 ? box.width : FALLBACK
|
|
44
|
+
const h = box.height > 0 ? box.height : FALLBACK
|
|
45
|
+
const rx = w / 2
|
|
46
|
+
const ry = h / 2
|
|
47
|
+
switch (template) {
|
|
48
|
+
case "square":
|
|
49
|
+
return closed([
|
|
50
|
+
[-rx, ry], [rx, ry], [rx, -ry], [-rx, -ry],
|
|
51
|
+
])
|
|
52
|
+
case "star": {
|
|
53
|
+
const points: [number, number][] = []
|
|
54
|
+
for (let i = 0; i < 10; i++) {
|
|
55
|
+
const angle = (i * Math.PI) / 5
|
|
56
|
+
const r = i % 2 === 0 ? 1 : 0.45
|
|
57
|
+
points.push([Math.sin(angle) * rx * r, Math.cos(angle) * ry * r])
|
|
58
|
+
}
|
|
59
|
+
return closed(points)
|
|
60
|
+
}
|
|
61
|
+
case "heart":
|
|
62
|
+
return closed(sample(96, (t) => {
|
|
63
|
+
// The classic heart curve, from its top notch, clockwise.
|
|
64
|
+
const a = t * 2 * Math.PI
|
|
65
|
+
const x = 16 * Math.sin(a) ** 3
|
|
66
|
+
const y = 13 * Math.cos(a) - 5 * Math.cos(2 * a) - 2 * Math.cos(3 * a) - Math.cos(4 * a)
|
|
67
|
+
return [(x / 17) * rx, ((y + 2.5) / 15.5) * ry]
|
|
68
|
+
}).slice(0, -1))
|
|
69
|
+
case "infinity":
|
|
70
|
+
return closed(sample(96, (t) => {
|
|
71
|
+
const a = t * 2 * Math.PI
|
|
72
|
+
const d = 1 + Math.sin(a) ** 2
|
|
73
|
+
return [(Math.cos(a) / d) * rx, ((Math.sin(a) * Math.cos(a)) / d) * ry * 2]
|
|
74
|
+
}).slice(0, -1))
|
|
75
|
+
case "wave":
|
|
76
|
+
return open(sample(96, (t) => [(-1 + 2 * t) * rx, Math.sin(t * 4 * Math.PI) * ry * 0.6]))
|
|
77
|
+
case "spiral":
|
|
78
|
+
return open(sample(160, (t) => {
|
|
79
|
+
const a = t * 3 * 2 * Math.PI
|
|
80
|
+
const r = 0.08 + 0.92 * t
|
|
81
|
+
return [Math.sin(a) * rx * r, Math.cos(a) * ry * r]
|
|
82
|
+
}))
|
|
83
|
+
case "arc":
|
|
84
|
+
return open(sample(64, (t) => {
|
|
85
|
+
const a = -Math.PI / 2 + t * Math.PI
|
|
86
|
+
return [Math.sin(a) * rx, -ry + Math.cos(a) * h]
|
|
87
|
+
}))
|
|
88
|
+
case "line":
|
|
89
|
+
return [{ type: "M", x: -rx, y: 0 }, { type: "L", x: rx, y: 0 }]
|
|
90
|
+
default:
|
|
91
|
+
return ellipse(rx, ry)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Four quarter-arcs from the top, clockwise. */
|
|
96
|
+
function ellipse(rx: number, ry: number): PathCommand[] {
|
|
97
|
+
const k = 0.5522847498
|
|
98
|
+
return [
|
|
99
|
+
{ type: "M", x: 0, y: ry },
|
|
100
|
+
{ type: "C", x1: k * rx, y1: ry, x2: rx, y2: k * ry, x: rx, y: 0 },
|
|
101
|
+
{ type: "C", x1: rx, y1: -k * ry, x2: k * rx, y2: -ry, x: 0, y: -ry },
|
|
102
|
+
{ type: "C", x1: -k * rx, y1: -ry, x2: -rx, y2: -k * ry, x: -rx, y: 0 },
|
|
103
|
+
{ type: "C", x1: -rx, y1: k * ry, x2: -k * rx, y2: ry, x: 0, y: ry },
|
|
104
|
+
{ type: "Z" },
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sample(steps: number, at: (t: number) => [number, number]): [number, number][] {
|
|
109
|
+
const points: [number, number][] = []
|
|
110
|
+
for (let i = 0; i <= steps; i++) points.push(at(i / steps))
|
|
111
|
+
return points
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function open(points: [number, number][]): PathCommand[] {
|
|
115
|
+
return points.map(([x, y], i) => (i === 0 ? { type: "M", x, y } : { type: "L", x, y }))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function closed(points: [number, number][]): PathCommand[] {
|
|
119
|
+
return [...open(points), { type: "Z" }]
|
|
120
|
+
}
|