interactive-bg 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +202 -2
- package/dist/core/canvas2d-renderer.cjs +119 -0
- package/dist/core/canvas2d-renderer.cjs.map +1 -0
- package/dist/core/canvas2d-renderer.d.cts +7 -0
- package/dist/core/canvas2d-renderer.d.ts +7 -0
- package/dist/core/canvas2d-renderer.js +119 -0
- package/dist/core/canvas2d-renderer.js.map +1 -0
- package/dist/core/color.cjs +46 -0
- package/dist/core/color.cjs.map +1 -0
- package/dist/core/color.d.cts +6 -0
- package/dist/core/color.d.ts +6 -0
- package/dist/core/color.js +46 -0
- package/dist/core/color.js.map +1 -0
- package/dist/core/engine.cjs +367 -0
- package/dist/core/engine.cjs.map +1 -0
- package/dist/core/engine.d.cts +69 -0
- package/dist/core/engine.d.ts +69 -0
- package/dist/core/engine.js +366 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/style.cjs +21 -0
- package/dist/core/style.cjs.map +1 -0
- package/dist/core/style.d.cts +85 -0
- package/dist/core/style.d.ts +85 -0
- package/dist/core/style.js +21 -0
- package/dist/core/style.js.map +1 -0
- package/dist/core/surface.cjs +25 -0
- package/dist/core/surface.cjs.map +1 -0
- package/dist/core/surface.d.cts +42 -0
- package/dist/core/surface.d.ts +42 -0
- package/dist/core/surface.js +24 -0
- package/dist/core/surface.js.map +1 -0
- package/dist/core/webgl-renderer.cjs +137 -0
- package/dist/core/webgl-renderer.cjs.map +1 -0
- package/dist/core/webgl-renderer.d.cts +8 -0
- package/dist/core/webgl-renderer.d.ts +8 -0
- package/dist/core/webgl-renderer.js +137 -0
- package/dist/core/webgl-renderer.js.map +1 -0
- package/dist/core/webgl.cjs +62 -0
- package/dist/core/webgl.cjs.map +1 -0
- package/dist/core/webgl.d.cts +15 -0
- package/dist/core/webgl.d.ts +15 -0
- package/dist/core/webgl.js +60 -0
- package/dist/core/webgl.js.map +1 -0
- package/dist/dots/canvas2d.cjs +14 -0
- package/dist/dots/canvas2d.cjs.map +1 -0
- package/dist/dots/canvas2d.d.cts +5 -0
- package/dist/dots/canvas2d.d.ts +5 -0
- package/dist/dots/canvas2d.js +13 -0
- package/dist/dots/canvas2d.js.map +1 -0
- package/dist/dots/index.cjs +16 -0
- package/dist/dots/index.cjs.map +1 -0
- package/dist/dots/index.d.cts +5 -0
- package/dist/dots/index.d.ts +5 -0
- package/dist/dots/index.js +15 -0
- package/dist/dots/index.js.map +1 -0
- package/dist/dots/webgl.cjs +14 -0
- package/dist/dots/webgl.cjs.map +1 -0
- package/dist/dots/webgl.d.cts +5 -0
- package/dist/dots/webgl.d.ts +5 -0
- package/dist/dots/webgl.js +13 -0
- package/dist/dots/webgl.js.map +1 -0
- package/dist/index.cjs +24 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +11 -0
- package/dist/index.umd.js +211 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/plus/canvas2d.cjs +14 -0
- package/dist/plus/canvas2d.cjs.map +1 -0
- package/dist/plus/canvas2d.d.cts +5 -0
- package/dist/plus/canvas2d.d.ts +5 -0
- package/dist/plus/canvas2d.js +13 -0
- package/dist/plus/canvas2d.js.map +1 -0
- package/dist/plus/index.cjs +16 -0
- package/dist/plus/index.cjs.map +1 -0
- package/dist/plus/index.d.cts +5 -0
- package/dist/plus/index.d.ts +5 -0
- package/dist/plus/index.js +15 -0
- package/dist/plus/index.js.map +1 -0
- package/dist/plus/webgl.cjs +14 -0
- package/dist/plus/webgl.cjs.map +1 -0
- package/dist/plus/webgl.d.cts +5 -0
- package/dist/plus/webgl.d.ts +5 -0
- package/dist/plus/webgl.js +13 -0
- package/dist/plus/webgl.js.map +1 -0
- package/dist/shared/buffer.cjs +20 -0
- package/dist/shared/buffer.cjs.map +1 -0
- package/dist/shared/buffer.d.cts +11 -0
- package/dist/shared/buffer.d.ts +11 -0
- package/dist/shared/buffer.js +19 -0
- package/dist/shared/buffer.js.map +1 -0
- package/dist/shared/grid-webgl.cjs +63 -0
- package/dist/shared/grid-webgl.cjs.map +1 -0
- package/dist/shared/grid-webgl.d.cts +10 -0
- package/dist/shared/grid-webgl.d.ts +10 -0
- package/dist/shared/grid-webgl.js +61 -0
- package/dist/shared/grid-webgl.js.map +1 -0
- package/dist/shared/grid.cjs +133 -0
- package/dist/shared/grid.cjs.map +1 -0
- package/dist/shared/grid.d.cts +72 -0
- package/dist/shared/grid.d.ts +72 -0
- package/dist/shared/grid.js +123 -0
- package/dist/shared/grid.js.map +1 -0
- package/dist/shared/noise.cjs +166 -0
- package/dist/shared/noise.cjs.map +1 -0
- package/dist/shared/noise.d.cts +4 -0
- package/dist/shared/noise.d.ts +4 -0
- package/dist/shared/noise.js +166 -0
- package/dist/shared/noise.js.map +1 -0
- package/dist/styles/dots/canvas2d.cjs +99 -0
- package/dist/styles/dots/canvas2d.cjs.map +1 -0
- package/dist/styles/dots/canvas2d.d.cts +7 -0
- package/dist/styles/dots/canvas2d.d.ts +7 -0
- package/dist/styles/dots/canvas2d.js +99 -0
- package/dist/styles/dots/canvas2d.js.map +1 -0
- package/dist/styles/dots/definition.cjs +15 -0
- package/dist/styles/dots/definition.cjs.map +1 -0
- package/dist/styles/dots/definition.d.cts +7 -0
- package/dist/styles/dots/definition.d.ts +7 -0
- package/dist/styles/dots/definition.js +16 -0
- package/dist/styles/dots/definition.js.map +1 -0
- package/dist/styles/dots/motion.cjs +6 -0
- package/dist/styles/dots/motion.cjs.map +1 -0
- package/dist/styles/dots/motion.d.cts +2 -0
- package/dist/styles/dots/motion.d.ts +2 -0
- package/dist/styles/dots/motion.js +6 -0
- package/dist/styles/dots/motion.js.map +1 -0
- package/dist/styles/dots/options.cjs +33 -0
- package/dist/styles/dots/options.cjs.map +1 -0
- package/dist/styles/dots/options.d.cts +33 -0
- package/dist/styles/dots/options.d.ts +33 -0
- package/dist/styles/dots/options.js +32 -0
- package/dist/styles/dots/options.js.map +1 -0
- package/dist/styles/dots/webgl.cjs +113 -0
- package/dist/styles/dots/webgl.cjs.map +1 -0
- package/dist/styles/dots/webgl.d.cts +4 -0
- package/dist/styles/dots/webgl.d.ts +4 -0
- package/dist/styles/dots/webgl.js +113 -0
- package/dist/styles/dots/webgl.js.map +1 -0
- package/dist/styles/plus/canvas2d.cjs +88 -0
- package/dist/styles/plus/canvas2d.cjs.map +1 -0
- package/dist/styles/plus/canvas2d.d.cts +8 -0
- package/dist/styles/plus/canvas2d.d.ts +8 -0
- package/dist/styles/plus/canvas2d.js +88 -0
- package/dist/styles/plus/canvas2d.js.map +1 -0
- package/dist/styles/plus/definition.cjs +17 -0
- package/dist/styles/plus/definition.cjs.map +1 -0
- package/dist/styles/plus/definition.d.cts +9 -0
- package/dist/styles/plus/definition.d.ts +9 -0
- package/dist/styles/plus/definition.js +18 -0
- package/dist/styles/plus/definition.js.map +1 -0
- package/dist/styles/plus/motion.cjs +18 -0
- package/dist/styles/plus/motion.cjs.map +1 -0
- package/dist/styles/plus/motion.d.cts +5 -0
- package/dist/styles/plus/motion.d.ts +5 -0
- package/dist/styles/plus/motion.js +18 -0
- package/dist/styles/plus/motion.js.map +1 -0
- package/dist/styles/plus/options.cjs +45 -0
- package/dist/styles/plus/options.cjs.map +1 -0
- package/dist/styles/plus/options.d.cts +45 -0
- package/dist/styles/plus/options.d.ts +45 -0
- package/dist/styles/plus/options.js +43 -0
- package/dist/styles/plus/options.js.map +1 -0
- package/dist/styles/plus/turns.cjs +102 -0
- package/dist/styles/plus/turns.cjs.map +1 -0
- package/dist/styles/plus/turns.d.cts +17 -0
- package/dist/styles/plus/turns.d.ts +17 -0
- package/dist/styles/plus/turns.js +102 -0
- package/dist/styles/plus/turns.js.map +1 -0
- package/dist/styles/plus/webgl.cjs +121 -0
- package/dist/styles/plus/webgl.cjs.map +1 -0
- package/dist/styles/plus/webgl.d.cts +8 -0
- package/dist/styles/plus/webgl.d.ts +8 -0
- package/dist/styles/plus/webgl.js +121 -0
- package/dist/styles/plus/webgl.js.map +1 -0
- package/dist/styles/topo/canvas2d.cjs +77 -0
- package/dist/styles/topo/canvas2d.cjs.map +1 -0
- package/dist/styles/topo/canvas2d.d.cts +4 -0
- package/dist/styles/topo/canvas2d.d.ts +4 -0
- package/dist/styles/topo/canvas2d.js +77 -0
- package/dist/styles/topo/canvas2d.js.map +1 -0
- package/dist/styles/topo/contours.cjs +83 -0
- package/dist/styles/topo/contours.cjs.map +1 -0
- package/dist/styles/topo/contours.d.cts +9 -0
- package/dist/styles/topo/contours.d.ts +9 -0
- package/dist/styles/topo/contours.js +83 -0
- package/dist/styles/topo/contours.js.map +1 -0
- package/dist/styles/topo/definition.cjs +13 -0
- package/dist/styles/topo/definition.cjs.map +1 -0
- package/dist/styles/topo/definition.d.cts +7 -0
- package/dist/styles/topo/definition.d.ts +7 -0
- package/dist/styles/topo/definition.js +14 -0
- package/dist/styles/topo/definition.js.map +1 -0
- package/dist/styles/topo/field.cjs +118 -0
- package/dist/styles/topo/field.cjs.map +1 -0
- package/dist/styles/topo/field.d.cts +26 -0
- package/dist/styles/topo/field.d.ts +26 -0
- package/dist/styles/topo/field.js +118 -0
- package/dist/styles/topo/field.js.map +1 -0
- package/dist/styles/topo/options.cjs +39 -0
- package/dist/styles/topo/options.cjs.map +1 -0
- package/dist/styles/topo/options.d.cts +52 -0
- package/dist/styles/topo/options.d.ts +52 -0
- package/dist/styles/topo/options.js +38 -0
- package/dist/styles/topo/options.js.map +1 -0
- package/dist/styles/topo/terrain.cjs +22 -0
- package/dist/styles/topo/terrain.cjs.map +1 -0
- package/dist/styles/topo/terrain.d.cts +11 -0
- package/dist/styles/topo/terrain.d.ts +11 -0
- package/dist/styles/topo/terrain.js +22 -0
- package/dist/styles/topo/terrain.js.map +1 -0
- package/dist/styles/topo/webgl.cjs +150 -0
- package/dist/styles/topo/webgl.cjs.map +1 -0
- package/dist/styles/topo/webgl.d.cts +7 -0
- package/dist/styles/topo/webgl.d.ts +7 -0
- package/dist/styles/topo/webgl.js +150 -0
- package/dist/styles/topo/webgl.js.map +1 -0
- package/dist/topo/canvas2d.cjs +14 -0
- package/dist/topo/canvas2d.cjs.map +1 -0
- package/dist/topo/canvas2d.d.cts +5 -0
- package/dist/topo/canvas2d.d.ts +5 -0
- package/dist/topo/canvas2d.js +13 -0
- package/dist/topo/canvas2d.js.map +1 -0
- package/dist/topo/index.cjs +16 -0
- package/dist/topo/index.cjs.map +1 -0
- package/dist/topo/index.d.cts +5 -0
- package/dist/topo/index.d.ts +5 -0
- package/dist/topo/index.js +15 -0
- package/dist/topo/index.js.map +1 -0
- package/dist/topo/webgl.cjs +14 -0
- package/dist/topo/webgl.cjs.map +1 -0
- package/dist/topo/webgl.d.cts +5 -0
- package/dist/topo/webgl.d.ts +5 -0
- package/dist/topo/webgl.js +13 -0
- package/dist/topo/webgl.js.map +1 -0
- package/package.json +192 -4
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createSurface } from "./surface.js";
|
|
2
|
+
// src/core/engine.ts
|
|
3
|
+
var engineDefaults = Object.freeze({
|
|
4
|
+
renderer: "auto",
|
|
5
|
+
backgroundColor: null,
|
|
6
|
+
fadeOutTop: 0,
|
|
7
|
+
fadeOutRight: 0,
|
|
8
|
+
fadeOutBottom: 0,
|
|
9
|
+
fadeOutLeft: 0,
|
|
10
|
+
interactive: true,
|
|
11
|
+
maxDpr: 1.5,
|
|
12
|
+
maxFps: 60,
|
|
13
|
+
idleFps: 30,
|
|
14
|
+
respectReducedMotion: true
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Mount an interactive background.
|
|
18
|
+
*
|
|
19
|
+
* - `target` `null`/omitted: a fixed, full-viewport canvas is placed behind the page.
|
|
20
|
+
* - `target` is a `<canvas>`: it is drawn into directly and sized by your CSS.
|
|
21
|
+
* - `target` is any other element (or a selector): a canvas is appended that
|
|
22
|
+
* fills it. The element gets `position: relative` if it was `static`.
|
|
23
|
+
*
|
|
24
|
+
* @example createBackground('#hero', topo({ levels: 14 }), { maxFps: 30 })
|
|
25
|
+
*/
|
|
26
|
+
function createBackground(target, style, engineOptions = {}) {
|
|
27
|
+
const host = typeof target === "string" ? document.querySelector(target) : target;
|
|
28
|
+
if (typeof target === "string" && !host) throw new Error(`interactive-bg: no element matches "${target}"`);
|
|
29
|
+
const definition = style.definition;
|
|
30
|
+
let options = { ...style.options };
|
|
31
|
+
let engine = {
|
|
32
|
+
...engineDefaults,
|
|
33
|
+
...engineOptions
|
|
34
|
+
};
|
|
35
|
+
let backgroundColor = resolveColor(engine.backgroundColor);
|
|
36
|
+
let fade = resolveFade(engine);
|
|
37
|
+
const ownsCanvas = !(host instanceof HTMLCanvasElement);
|
|
38
|
+
const hostPosition = ownsCanvas && host ? host.style.position : null;
|
|
39
|
+
let canvas = ownsCanvas ? mountCanvas(host ?? null) : host;
|
|
40
|
+
const surface = createSurface(() => canvas, definition.renderers, options, engine.renderer, () => {
|
|
41
|
+
needsDraw = true;
|
|
42
|
+
wake();
|
|
43
|
+
}, ownsCanvas ? () => {
|
|
44
|
+
const fresh = mountCanvas(host ?? null);
|
|
45
|
+
canvas.remove();
|
|
46
|
+
canvas = fresh;
|
|
47
|
+
} : void 0) ?? blankSurface(definition);
|
|
48
|
+
let width = 0;
|
|
49
|
+
let height = 0;
|
|
50
|
+
let dpr = 1;
|
|
51
|
+
let pointerX = 0;
|
|
52
|
+
let pointerY = 0;
|
|
53
|
+
let targetX = 0;
|
|
54
|
+
let targetY = 0;
|
|
55
|
+
let influence = 0;
|
|
56
|
+
let targetInfluence = 0;
|
|
57
|
+
/** Clicks since the last rendered frame, and the array handed to the style for the current one (swapped each frame). */
|
|
58
|
+
let clicks = [];
|
|
59
|
+
let frameClicks = [];
|
|
60
|
+
const pointer = {
|
|
61
|
+
x: 0,
|
|
62
|
+
y: 0,
|
|
63
|
+
influence: 0
|
|
64
|
+
};
|
|
65
|
+
const frameState = {
|
|
66
|
+
width: 0,
|
|
67
|
+
height: 0,
|
|
68
|
+
dpr: 1,
|
|
69
|
+
dt: 0,
|
|
70
|
+
reducedMotion: false,
|
|
71
|
+
pointer,
|
|
72
|
+
clicks: frameClicks
|
|
73
|
+
};
|
|
74
|
+
/** The style reported a short-lived effect still playing in its last frame. */
|
|
75
|
+
let styleBusy = false;
|
|
76
|
+
let lastFrame = 0;
|
|
77
|
+
let rafId = 0;
|
|
78
|
+
let timerId = 0;
|
|
79
|
+
let paused = false;
|
|
80
|
+
let visible = true;
|
|
81
|
+
let intersecting = true;
|
|
82
|
+
let destroyed = false;
|
|
83
|
+
let needsDraw = true;
|
|
84
|
+
const reducedMotionQuery = window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
85
|
+
let reducedMotion = reducedMotionQuery?.matches ?? false;
|
|
86
|
+
function holdStill() {
|
|
87
|
+
return engine.respectReducedMotion && reducedMotion;
|
|
88
|
+
}
|
|
89
|
+
/** Is the style's own ambient animation running (as opposed to only reacting to the pointer)? */
|
|
90
|
+
function animating() {
|
|
91
|
+
return surface.name !== null && definition.animates(options) && !holdStill();
|
|
92
|
+
}
|
|
93
|
+
/** Does something need the full frame rate: the pointer's effect easing, or a style effect playing? */
|
|
94
|
+
function busy() {
|
|
95
|
+
return styleBusy || clicks.length > 0 || pointerEasing();
|
|
96
|
+
}
|
|
97
|
+
/** Frame rate for the ambient animation; `0` means uncapped for the engine options. */
|
|
98
|
+
function effectiveIdleFps() {
|
|
99
|
+
const configured = engine.maxFps <= 0 ? engine.idleFps : engine.idleFps <= 0 ? engine.maxFps : Math.min(engine.maxFps, engine.idleFps);
|
|
100
|
+
const needed = definition.idleFps?.(options) ?? 0;
|
|
101
|
+
if (needed <= 0) return configured;
|
|
102
|
+
return configured <= 0 ? needed : Math.min(configured, needed);
|
|
103
|
+
}
|
|
104
|
+
function frameInterval(fps) {
|
|
105
|
+
return fps > 0 ? 1e3 / fps : 0;
|
|
106
|
+
}
|
|
107
|
+
function canRun() {
|
|
108
|
+
return !destroyed && !paused && visible;
|
|
109
|
+
}
|
|
110
|
+
/** Is the pointer's effect still catching up with the pointer? */
|
|
111
|
+
function pointerEasing() {
|
|
112
|
+
if (influence !== targetInfluence) return true;
|
|
113
|
+
return influence > 0 && (Math.abs(targetX - pointerX) > .5 || Math.abs(targetY - pointerY) > .5);
|
|
114
|
+
}
|
|
115
|
+
function resize() {
|
|
116
|
+
const rect = canvas.getBoundingClientRect();
|
|
117
|
+
const nextWidth = Math.max(1, Math.round(rect.width));
|
|
118
|
+
const nextHeight = Math.max(1, Math.round(rect.height));
|
|
119
|
+
const nextDpr = Math.min(window.devicePixelRatio || 1, Math.max(.5, engine.maxDpr));
|
|
120
|
+
if (nextWidth === width && nextHeight === height && nextDpr === dpr) return;
|
|
121
|
+
width = nextWidth;
|
|
122
|
+
height = nextHeight;
|
|
123
|
+
dpr = nextDpr;
|
|
124
|
+
canvas.width = Math.round(width * dpr);
|
|
125
|
+
canvas.height = Math.round(height * dpr);
|
|
126
|
+
needsDraw = true;
|
|
127
|
+
}
|
|
128
|
+
function frame(now) {
|
|
129
|
+
rafId = 0;
|
|
130
|
+
if (!canRun()) return;
|
|
131
|
+
const minInterval = frameInterval(busy() ? engine.maxFps : effectiveIdleFps());
|
|
132
|
+
if (lastFrame && now - lastFrame < minInterval * .85) {
|
|
133
|
+
schedule(minInterval - (now - lastFrame));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const dt = lastFrame ? Math.min((now - lastFrame) / 1e3, .1) : 0;
|
|
137
|
+
lastFrame = now;
|
|
138
|
+
const wasBusy = busy();
|
|
139
|
+
const ease = 1 - Math.exp(-dt * 8);
|
|
140
|
+
pointerX += (targetX - pointerX) * ease;
|
|
141
|
+
pointerY += (targetY - pointerY) * ease;
|
|
142
|
+
influence += (targetInfluence - influence) * ease;
|
|
143
|
+
if (Math.abs(targetInfluence - influence) < .001) influence = targetInfluence;
|
|
144
|
+
const moving = animating();
|
|
145
|
+
if (moving || wasBusy || needsDraw) {
|
|
146
|
+
needsDraw = false;
|
|
147
|
+
[frameClicks, clicks] = [clicks, frameClicks];
|
|
148
|
+
clicks.length = 0;
|
|
149
|
+
pointer.x = pointerX;
|
|
150
|
+
pointer.y = pointerY;
|
|
151
|
+
pointer.influence = influence;
|
|
152
|
+
frameState.width = width;
|
|
153
|
+
frameState.height = height;
|
|
154
|
+
frameState.dpr = dpr;
|
|
155
|
+
frameState.dt = dt;
|
|
156
|
+
frameState.reducedMotion = holdStill();
|
|
157
|
+
frameState.clicks = frameClicks;
|
|
158
|
+
styleBusy = surface.render(frameState, backgroundColor, fade);
|
|
159
|
+
}
|
|
160
|
+
if (busy()) schedule();
|
|
161
|
+
else if (moving) schedule(frameInterval(effectiveIdleFps()));
|
|
162
|
+
else lastFrame = 0;
|
|
163
|
+
}
|
|
164
|
+
/** Request a frame `delay` ms from now. */
|
|
165
|
+
function schedule(delay = 0) {
|
|
166
|
+
if (rafId || timerId || !canRun()) return;
|
|
167
|
+
if (delay > 8) timerId = window.setTimeout(onTimer, delay * .8);
|
|
168
|
+
else rafId = requestAnimationFrame(frame);
|
|
169
|
+
}
|
|
170
|
+
function onTimer() {
|
|
171
|
+
timerId = 0;
|
|
172
|
+
schedule();
|
|
173
|
+
}
|
|
174
|
+
/** Render as soon as possible, cutting short any idle sleep. */
|
|
175
|
+
function wake() {
|
|
176
|
+
if (timerId) clearTimeout(timerId);
|
|
177
|
+
timerId = 0;
|
|
178
|
+
schedule();
|
|
179
|
+
}
|
|
180
|
+
function cancel() {
|
|
181
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
182
|
+
if (timerId) clearTimeout(timerId);
|
|
183
|
+
rafId = timerId = 0;
|
|
184
|
+
}
|
|
185
|
+
function onPointerMove(event) {
|
|
186
|
+
if (!engine.interactive || !canRun()) return;
|
|
187
|
+
const rect = canvas.getBoundingClientRect();
|
|
188
|
+
const x = event.clientX - rect.left;
|
|
189
|
+
const y = event.clientY - rect.top;
|
|
190
|
+
const inside = x >= 0 && y >= 0 && x <= rect.width && y <= rect.height;
|
|
191
|
+
if (inside && targetInfluence === 0) {
|
|
192
|
+
pointerX = x;
|
|
193
|
+
pointerY = y;
|
|
194
|
+
}
|
|
195
|
+
targetX = x;
|
|
196
|
+
targetY = y;
|
|
197
|
+
targetInfluence = inside ? 1 : 0;
|
|
198
|
+
if (inside || influence > 0) wake();
|
|
199
|
+
}
|
|
200
|
+
function onPointerDown(event) {
|
|
201
|
+
if (!engine.interactive || !canRun()) return;
|
|
202
|
+
const rect = canvas.getBoundingClientRect();
|
|
203
|
+
const x = event.clientX - rect.left;
|
|
204
|
+
const y = event.clientY - rect.top;
|
|
205
|
+
if (x < 0 || y < 0 || x > rect.width || y > rect.height) return;
|
|
206
|
+
clicks.push({
|
|
207
|
+
x,
|
|
208
|
+
y
|
|
209
|
+
});
|
|
210
|
+
wake();
|
|
211
|
+
}
|
|
212
|
+
function onPointerLeave() {
|
|
213
|
+
targetInfluence = 0;
|
|
214
|
+
wake();
|
|
215
|
+
}
|
|
216
|
+
function onVisibilityChange() {
|
|
217
|
+
visible = document.visibilityState === "visible" && intersecting;
|
|
218
|
+
lastFrame = 0;
|
|
219
|
+
if (visible) wake();
|
|
220
|
+
else cancel();
|
|
221
|
+
}
|
|
222
|
+
function onReducedMotionChange(event) {
|
|
223
|
+
reducedMotion = event.matches;
|
|
224
|
+
needsDraw = true;
|
|
225
|
+
wake();
|
|
226
|
+
}
|
|
227
|
+
let dprQuery = null;
|
|
228
|
+
function watchDpr() {
|
|
229
|
+
dprQuery?.removeEventListener?.("change", onDprChange);
|
|
230
|
+
dprQuery = window.matchMedia?.(`(resolution: ${window.devicePixelRatio || 1}dppx)`) ?? null;
|
|
231
|
+
dprQuery?.addEventListener?.("change", onDprChange);
|
|
232
|
+
}
|
|
233
|
+
function onDprChange() {
|
|
234
|
+
resize();
|
|
235
|
+
wake();
|
|
236
|
+
watchDpr();
|
|
237
|
+
}
|
|
238
|
+
watchDpr();
|
|
239
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
240
|
+
resize();
|
|
241
|
+
wake();
|
|
242
|
+
});
|
|
243
|
+
resizeObserver.observe(canvas);
|
|
244
|
+
const intersectionObserver = typeof IntersectionObserver === "function" ? new IntersectionObserver((entries) => {
|
|
245
|
+
intersecting = entries[entries.length - 1].isIntersecting;
|
|
246
|
+
onVisibilityChange();
|
|
247
|
+
}) : null;
|
|
248
|
+
intersectionObserver?.observe(canvas);
|
|
249
|
+
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
|
250
|
+
window.addEventListener("pointerdown", onPointerDown, { passive: true });
|
|
251
|
+
document.documentElement.addEventListener("pointerleave", onPointerLeave);
|
|
252
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
253
|
+
reducedMotionQuery?.addEventListener?.("change", onReducedMotionChange);
|
|
254
|
+
resize();
|
|
255
|
+
schedule();
|
|
256
|
+
return {
|
|
257
|
+
canvas,
|
|
258
|
+
style: definition.name,
|
|
259
|
+
renderer: surface.name,
|
|
260
|
+
get options() {
|
|
261
|
+
return options;
|
|
262
|
+
},
|
|
263
|
+
get engineOptions() {
|
|
264
|
+
return engine;
|
|
265
|
+
},
|
|
266
|
+
setOptions(next) {
|
|
267
|
+
options = {
|
|
268
|
+
...options,
|
|
269
|
+
...next
|
|
270
|
+
};
|
|
271
|
+
surface.update(options);
|
|
272
|
+
needsDraw = true;
|
|
273
|
+
wake();
|
|
274
|
+
},
|
|
275
|
+
setEngineOptions(next) {
|
|
276
|
+
const previous = engine;
|
|
277
|
+
engine = {
|
|
278
|
+
...engine,
|
|
279
|
+
...next
|
|
280
|
+
};
|
|
281
|
+
backgroundColor = resolveColor(engine.backgroundColor);
|
|
282
|
+
fade = resolveFade(engine);
|
|
283
|
+
if (engine.maxDpr !== previous.maxDpr) resize();
|
|
284
|
+
if (!engine.interactive) {
|
|
285
|
+
influence = targetInfluence = 0;
|
|
286
|
+
clicks.length = 0;
|
|
287
|
+
}
|
|
288
|
+
needsDraw = true;
|
|
289
|
+
wake();
|
|
290
|
+
},
|
|
291
|
+
pause() {
|
|
292
|
+
paused = true;
|
|
293
|
+
cancel();
|
|
294
|
+
},
|
|
295
|
+
resume() {
|
|
296
|
+
paused = false;
|
|
297
|
+
lastFrame = 0;
|
|
298
|
+
wake();
|
|
299
|
+
},
|
|
300
|
+
destroy() {
|
|
301
|
+
if (destroyed) return;
|
|
302
|
+
destroyed = true;
|
|
303
|
+
cancel();
|
|
304
|
+
resizeObserver.disconnect();
|
|
305
|
+
intersectionObserver?.disconnect();
|
|
306
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
307
|
+
window.removeEventListener("pointerdown", onPointerDown);
|
|
308
|
+
document.documentElement.removeEventListener("pointerleave", onPointerLeave);
|
|
309
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
310
|
+
reducedMotionQuery?.removeEventListener?.("change", onReducedMotionChange);
|
|
311
|
+
dprQuery?.removeEventListener?.("change", onDprChange);
|
|
312
|
+
if (!ownsCanvas) surface.clear();
|
|
313
|
+
surface.destroy();
|
|
314
|
+
if (ownsCanvas) {
|
|
315
|
+
if (surface.name === "webgl") canvas.getContext("webgl2")?.getExtension("WEBGL_lose_context")?.loseContext();
|
|
316
|
+
canvas.remove();
|
|
317
|
+
if (host && hostPosition !== null) host.style.position = hostPosition;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/** Stands in when none of the style's renderers works here: draws nothing, and says so once. */
|
|
323
|
+
function blankSurface(definition) {
|
|
324
|
+
const needs = definition.renderers.map((r) => r.name === "webgl" ? "WebGL2" : "Canvas 2D").join(" or ");
|
|
325
|
+
console.warn(`interactive-bg: style "${definition.name}" needs ${needs}, which this browser doesn't provide`);
|
|
326
|
+
return {
|
|
327
|
+
name: null,
|
|
328
|
+
update() {},
|
|
329
|
+
render: () => false,
|
|
330
|
+
clear() {},
|
|
331
|
+
destroy() {}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function resolveFade(engine) {
|
|
335
|
+
return {
|
|
336
|
+
top: Math.max(0, engine.fadeOutTop),
|
|
337
|
+
right: Math.max(0, engine.fadeOutRight),
|
|
338
|
+
bottom: Math.max(0, engine.fadeOutBottom),
|
|
339
|
+
left: Math.max(0, engine.fadeOutLeft)
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function resolveColor(color) {
|
|
343
|
+
return color && color !== "transparent" ? color : null;
|
|
344
|
+
}
|
|
345
|
+
/** Create the background canvas inside `host`, or fixed behind the page when there is no host. */
|
|
346
|
+
function mountCanvas(host) {
|
|
347
|
+
const canvas = document.createElement("canvas");
|
|
348
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
349
|
+
Object.assign(canvas.style, {
|
|
350
|
+
position: host ? "absolute" : "fixed",
|
|
351
|
+
inset: "0",
|
|
352
|
+
width: "100%",
|
|
353
|
+
height: "100%",
|
|
354
|
+
display: "block",
|
|
355
|
+
pointerEvents: "none",
|
|
356
|
+
zIndex: host ? "0" : "-1"
|
|
357
|
+
});
|
|
358
|
+
if (host) {
|
|
359
|
+
if (getComputedStyle(host).position === "static") host.style.position = "relative";
|
|
360
|
+
host.prepend(canvas);
|
|
361
|
+
} else document.body.prepend(canvas);
|
|
362
|
+
return canvas;
|
|
363
|
+
}
|
|
364
|
+
export { createBackground, engineDefaults };
|
|
365
|
+
|
|
366
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","names":[],"sources":["../../src/core/engine.ts"],"sourcesContent":["import type { Frame, Point, RendererName, Style, StyleDefinition } from './style';\nimport { createSurface, type Fade, type Surface } from './surface';\n\n/** Options shared by every style: how and when the background is rendered. */\nexport interface EngineOptions {\n /**\n * Rendering backend, chosen when the background is created (changing it later has no effect).\n * `'auto'` uses the style's renderers in their own order (WebGL2 first for the built-in styles)\n * and falls back to the next when the browser lacks one. Naming one tries it first.\n */\n renderer: 'auto' | RendererName;\n /** Fill colour painted behind the style. `null` or `'transparent'` keeps the canvas transparent. */\n backgroundColor: string | null;\n /**\n * Fade the pattern out towards the top edge over this many CSS pixels: from its own opacity\n * this far in from the edge to fully transparent at the edge. The background colour isn't\n * faded. `0` = no fade. Fades on several sides multiply where they overlap.\n */\n fadeOutTop: number;\n /** Like `fadeOutTop`, towards the right edge. */\n fadeOutRight: number;\n /** Like `fadeOutTop`, towards the bottom edge. */\n fadeOutBottom: number;\n /** Like `fadeOutTop`, towards the left edge. */\n fadeOutLeft: number;\n /** React to the pointer and to clicks / taps. */\n interactive: boolean;\n /**\n * Upper limit for the canvas pixel density. Canvas memory grows with its square:\n * a 1920×1080 canvas takes 8 MB at 1, 18 MB at 1.5 and 32 MB at 2.\n */\n maxDpr: number;\n /** Frame-rate cap while the pointer is moving the background. `0` = uncapped. */\n maxFps: number;\n /** Frame-rate cap while only the style's own animation runs. Lower saves CPU and battery. `0` = same as `maxFps`. */\n idleFps: number;\n /** Hold the style's own animation still when the user prefers reduced motion. */\n respectReducedMotion: boolean;\n}\n\nexport const engineDefaults: Readonly<EngineOptions> = Object.freeze({\n renderer: 'auto',\n backgroundColor: null,\n fadeOutTop: 0,\n fadeOutRight: 0,\n fadeOutBottom: 0,\n fadeOutLeft: 0,\n interactive: true,\n maxDpr: 1.5,\n maxFps: 60,\n idleFps: 30,\n respectReducedMotion: true,\n});\n\nexport interface Background<O extends object> {\n readonly canvas: HTMLCanvasElement;\n /** Name of the style, e.g. `'topo'`. */\n readonly style: string;\n /**\n * The rendering backend actually in use, or `null` when none of the style's renderers works in\n * this browser (e.g. a WebGL-only style without WebGL2): the background then stays blank.\n */\n readonly renderer: RendererName | null;\n readonly options: Readonly<O>;\n readonly engineOptions: Readonly<EngineOptions>;\n /** Update any of the style's options on the fly. */\n setOptions(options: Partial<O>): void;\n /** Update any engine options on the fly (except `renderer`). */\n setEngineOptions(options: Partial<EngineOptions>): void;\n pause(): void;\n resume(): void;\n /** Stop rendering, remove listeners and (if it was created by us) the canvas. */\n destroy(): void;\n}\n\n/**\n * Mount an interactive background.\n *\n * - `target` `null`/omitted: a fixed, full-viewport canvas is placed behind the page.\n * - `target` is a `<canvas>`: it is drawn into directly and sized by your CSS.\n * - `target` is any other element (or a selector): a canvas is appended that\n * fills it. The element gets `position: relative` if it was `static`.\n *\n * @example createBackground('#hero', topo({ levels: 14 }), { maxFps: 30 })\n */\nexport function createBackground<O extends object>(\n target: HTMLElement | string | null | undefined,\n style: Style<O>,\n engineOptions: Partial<EngineOptions> = {},\n): Background<O> {\n const host = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target;\n if (typeof target === 'string' && !host) {\n throw new Error(`interactive-bg: no element matches \"${target}\"`);\n }\n\n const definition = style.definition;\n let options: O = { ...style.options };\n let engine: EngineOptions = { ...engineDefaults, ...engineOptions };\n let backgroundColor = resolveColor(engine.backgroundColor);\n let fade = resolveFade(engine);\n\n const ownsCanvas = !(host instanceof HTMLCanvasElement);\n // We may switch the host to `position: relative`; remember what to restore on destroy().\n const hostPosition = ownsCanvas && host ? host.style.position : null;\n let canvas = ownsCanvas ? mountCanvas(host ?? null) : (host as HTMLCanvasElement);\n const surface =\n createSurface(\n () => canvas,\n definition.renderers,\n options,\n engine.renderer,\n () => {\n needsDraw = true;\n wake();\n },\n // A renderer that failed may have claimed the canvas's context; the next one needs a fresh canvas.\n ownsCanvas\n ? () => {\n const fresh = mountCanvas(host ?? null);\n canvas.remove();\n canvas = fresh;\n }\n : undefined,\n ) ?? blankSurface(definition);\n\n // Canvas size in CSS pixels, and the pixel density it is rendered at.\n let width = 0;\n let height = 0;\n let dpr = 1;\n\n // Pointer state: `target*` is where the pointer is, the rest eases toward it.\n let pointerX = 0;\n let pointerY = 0;\n let targetX = 0;\n let targetY = 0;\n let influence = 0;\n let targetInfluence = 0;\n /** Clicks since the last rendered frame, and the array handed to the style for the current one (swapped each frame). */\n let clicks: Point[] = [];\n let frameClicks: Point[] = [];\n // Reused for every frame: styles only read them during render().\n const pointer = { x: 0, y: 0, influence: 0 };\n const frameState: Frame = { width: 0, height: 0, dpr: 1, dt: 0, reducedMotion: false, pointer, clicks: frameClicks };\n /** The style reported a short-lived effect still playing in its last frame. */\n let styleBusy = false;\n\n let lastFrame = 0;\n let rafId = 0;\n let timerId = 0;\n let paused = false;\n let visible = true;\n let intersecting = true;\n let destroyed = false;\n let needsDraw = true;\n\n const reducedMotionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');\n let reducedMotion = reducedMotionQuery?.matches ?? false;\n\n function holdStill(): boolean {\n return engine.respectReducedMotion && reducedMotion;\n }\n\n /** Is the style's own ambient animation running (as opposed to only reacting to the pointer)? */\n function animating(): boolean {\n return surface.name !== null && definition.animates(options) && !holdStill();\n }\n\n /** Does something need the full frame rate: the pointer's effect easing, or a style effect playing? */\n function busy(): boolean {\n return styleBusy || clicks.length > 0 || pointerEasing();\n }\n\n /** Frame rate for the ambient animation; `0` means uncapped for the engine options. */\n function effectiveIdleFps(): number {\n const configured =\n engine.maxFps <= 0\n ? engine.idleFps\n : engine.idleFps <= 0\n ? engine.maxFps\n : Math.min(engine.maxFps, engine.idleFps);\n const needed = definition.idleFps?.(options) ?? 0;\n if (needed <= 0) return configured;\n return configured <= 0 ? needed : Math.min(configured, needed);\n }\n\n function frameInterval(fps: number): number {\n return fps > 0 ? 1000 / fps : 0;\n }\n\n function canRun(): boolean {\n return !destroyed && !paused && visible;\n }\n\n /** Is the pointer's effect still catching up with the pointer? */\n function pointerEasing(): boolean {\n if (influence !== targetInfluence) return true;\n return influence > 0 && (Math.abs(targetX - pointerX) > 0.5 || Math.abs(targetY - pointerY) > 0.5);\n }\n\n function resize(): void {\n const rect = canvas.getBoundingClientRect();\n const nextWidth = Math.max(1, Math.round(rect.width));\n const nextHeight = Math.max(1, Math.round(rect.height));\n const nextDpr = Math.min(window.devicePixelRatio || 1, Math.max(0.5, engine.maxDpr));\n if (nextWidth === width && nextHeight === height && nextDpr === dpr) return;\n width = nextWidth;\n height = nextHeight;\n dpr = nextDpr;\n canvas.width = Math.round(width * dpr);\n canvas.height = Math.round(height * dpr);\n needsDraw = true;\n }\n\n function frame(now: number): void {\n rafId = 0;\n if (!canRun()) return;\n\n // Full frame rate only while reacting to the pointer or playing an effect.\n const minInterval = frameInterval(busy() ? engine.maxFps : effectiveIdleFps());\n // 15% tolerance so a 60 fps cap doesn't drop frames on 60 Hz displays.\n if (lastFrame && now - lastFrame < minInterval * 0.85) {\n schedule(minInterval - (now - lastFrame));\n return;\n }\n const dt = lastFrame ? Math.min((now - lastFrame) / 1000, 0.1) : 0;\n lastFrame = now;\n\n const wasBusy = busy();\n // Frame-rate independent easing.\n const ease = 1 - Math.exp(-dt * 8);\n pointerX += (targetX - pointerX) * ease;\n pointerY += (targetY - pointerY) * ease;\n influence += (targetInfluence - influence) * ease;\n if (Math.abs(targetInfluence - influence) < 0.001) influence = targetInfluence;\n\n const moving = animating();\n if (moving || wasBusy || needsDraw) {\n needsDraw = false;\n [frameClicks, clicks] = [clicks, frameClicks];\n clicks.length = 0;\n pointer.x = pointerX;\n pointer.y = pointerY;\n pointer.influence = influence;\n frameState.width = width;\n frameState.height = height;\n frameState.dpr = dpr;\n frameState.dt = dt;\n frameState.reducedMotion = holdStill();\n frameState.clicks = frameClicks;\n styleBusy = surface.render(frameState, backgroundColor, fade);\n }\n\n // Nothing left to animate: stop until a pointer move, click, resize or option change wakes us.\n if (busy()) schedule();\n else if (moving) schedule(frameInterval(effectiveIdleFps()));\n else lastFrame = 0;\n }\n\n /** Request a frame `delay` ms from now. */\n function schedule(delay = 0): void {\n if (rafId || timerId || !canRun()) return;\n // requestAnimationFrame alone wakes up on every display refresh (up to 120–165 Hz),\n // so sleep through longer gaps with a timer and only then wait for the next frame.\n // The timer wakes slightly early so the next frame lands within frame()'s tolerance.\n if (delay > 8) {\n timerId = window.setTimeout(onTimer, delay * 0.8);\n } else {\n rafId = requestAnimationFrame(frame);\n }\n }\n\n function onTimer(): void {\n timerId = 0;\n schedule();\n }\n\n /** Render as soon as possible, cutting short any idle sleep. */\n function wake(): void {\n if (timerId) clearTimeout(timerId);\n timerId = 0;\n schedule();\n }\n\n function cancel(): void {\n if (rafId) cancelAnimationFrame(rafId);\n if (timerId) clearTimeout(timerId);\n rafId = timerId = 0;\n }\n\n function onPointerMove(event: PointerEvent): void {\n if (!engine.interactive || !canRun()) return;\n const rect = canvas.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n const inside = x >= 0 && y >= 0 && x <= rect.width && y <= rect.height;\n if (inside && targetInfluence === 0) {\n // Entering: start where the pointer is instead of sliding in.\n pointerX = x;\n pointerY = y;\n }\n targetX = x;\n targetY = y;\n targetInfluence = inside ? 1 : 0;\n if (inside || influence > 0) wake();\n }\n\n function onPointerDown(event: PointerEvent): void {\n if (!engine.interactive || !canRun()) return;\n const rect = canvas.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n if (x < 0 || y < 0 || x > rect.width || y > rect.height) return;\n clicks.push({ x, y });\n wake();\n }\n\n function onPointerLeave(): void {\n targetInfluence = 0;\n wake();\n }\n\n function onVisibilityChange(): void {\n visible = document.visibilityState === 'visible' && intersecting;\n lastFrame = 0;\n if (visible) wake();\n else cancel();\n }\n\n function onReducedMotionChange(event: MediaQueryListEvent): void {\n reducedMotion = event.matches;\n needsDraw = true;\n wake();\n }\n\n // ResizeObserver doesn't fire when only the pixel density changes (e.g. the window moves to\n // another screen), so watch the current density with a media query, re-armed after each change.\n let dprQuery: MediaQueryList | null = null;\n function watchDpr(): void {\n dprQuery?.removeEventListener?.('change', onDprChange);\n dprQuery = window.matchMedia?.(`(resolution: ${window.devicePixelRatio || 1}dppx)`) ?? null;\n dprQuery?.addEventListener?.('change', onDprChange);\n }\n function onDprChange(): void {\n resize();\n wake();\n watchDpr();\n }\n watchDpr();\n\n const resizeObserver = new ResizeObserver(() => {\n resize();\n wake();\n });\n resizeObserver.observe(canvas);\n\n const intersectionObserver =\n typeof IntersectionObserver === 'function'\n ? new IntersectionObserver((entries) => {\n intersecting = entries[entries.length - 1].isIntersecting;\n onVisibilityChange();\n })\n : null;\n intersectionObserver?.observe(canvas);\n\n window.addEventListener('pointermove', onPointerMove, { passive: true });\n window.addEventListener('pointerdown', onPointerDown, { passive: true });\n document.documentElement.addEventListener('pointerleave', onPointerLeave);\n document.addEventListener('visibilitychange', onVisibilityChange);\n reducedMotionQuery?.addEventListener?.('change', onReducedMotionChange);\n\n resize();\n schedule();\n\n return {\n canvas,\n style: definition.name,\n renderer: surface.name,\n get options() {\n return options;\n },\n get engineOptions() {\n return engine;\n },\n setOptions(next) {\n options = { ...options, ...next };\n surface.update(options);\n needsDraw = true;\n wake();\n },\n setEngineOptions(next) {\n const previous = engine;\n engine = { ...engine, ...next };\n backgroundColor = resolveColor(engine.backgroundColor);\n fade = resolveFade(engine);\n if (engine.maxDpr !== previous.maxDpr) resize();\n if (!engine.interactive) {\n influence = targetInfluence = 0;\n clicks.length = 0;\n }\n needsDraw = true;\n wake();\n },\n pause() {\n paused = true;\n cancel();\n },\n resume() {\n paused = false;\n lastFrame = 0;\n wake();\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n cancel();\n resizeObserver.disconnect();\n intersectionObserver?.disconnect();\n window.removeEventListener('pointermove', onPointerMove);\n window.removeEventListener('pointerdown', onPointerDown);\n document.documentElement.removeEventListener('pointerleave', onPointerLeave);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n reducedMotionQuery?.removeEventListener?.('change', onReducedMotionChange);\n dprQuery?.removeEventListener?.('change', onDprChange);\n if (!ownsCanvas) surface.clear();\n surface.destroy();\n if (ownsCanvas) {\n // Free the GPU context now rather than at garbage collection: browsers cap live WebGL contexts.\n if (surface.name === 'webgl') canvas.getContext('webgl2')?.getExtension('WEBGL_lose_context')?.loseContext();\n canvas.remove();\n if (host && hostPosition !== null) host.style.position = hostPosition;\n }\n },\n };\n}\n\n/** Stands in when none of the style's renderers works here: draws nothing, and says so once. */\nfunction blankSurface<O extends object>(definition: StyleDefinition<O>): Omit<Surface<O>, 'name'> & { name: null } {\n const needs = definition.renderers.map((r) => (r.name === 'webgl' ? 'WebGL2' : 'Canvas 2D')).join(' or ');\n console.warn(`interactive-bg: style \"${definition.name}\" needs ${needs}, which this browser doesn't provide`);\n return { name: null, update() {}, render: () => false, clear() {}, destroy() {} };\n}\n\nfunction resolveFade(engine: EngineOptions): Fade {\n return {\n top: Math.max(0, engine.fadeOutTop),\n right: Math.max(0, engine.fadeOutRight),\n bottom: Math.max(0, engine.fadeOutBottom),\n left: Math.max(0, engine.fadeOutLeft),\n };\n}\n\nfunction resolveColor(color: string | null): string | null {\n return color && color !== 'transparent' ? color : null;\n}\n\n/** Create the background canvas inside `host`, or fixed behind the page when there is no host. */\nfunction mountCanvas(host: HTMLElement | null): HTMLCanvasElement {\n const canvas = document.createElement('canvas');\n canvas.setAttribute('aria-hidden', 'true');\n Object.assign(canvas.style, {\n position: host ? 'absolute' : 'fixed',\n inset: '0',\n width: '100%',\n height: '100%',\n display: 'block',\n pointerEvents: 'none',\n zIndex: host ? '0' : '-1',\n });\n if (host) {\n if (getComputedStyle(host).position === 'static') host.style.position = 'relative';\n host.prepend(canvas);\n } else {\n document.body.prepend(canvas);\n }\n return canvas;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/core/style.ts
|
|
2
|
+
/**
|
|
3
|
+
* Turn a style definition into the function users call, e.g. `topo({ levels: 14 })`.
|
|
4
|
+
*/
|
|
5
|
+
function defineStyle(definition) {
|
|
6
|
+
if (definition.renderers.length === 0) throw new Error(`interactive-bg: style "${definition.name}" has no renderer`);
|
|
7
|
+
const factory = (options = {}) => ({
|
|
8
|
+
definition,
|
|
9
|
+
options: {
|
|
10
|
+
...definition.defaults,
|
|
11
|
+
...options
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
return Object.assign(factory, {
|
|
15
|
+
styleName: definition.name,
|
|
16
|
+
defaults: definition.defaults
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
exports.defineStyle = defineStyle;
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=style.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.cjs","names":[],"sources":["../../src/core/style.ts"],"sourcesContent":["import type { Renderer } from './surface';\n\nexport type RendererName = 'webgl' | 'canvas2d';\n\n/** A position in CSS pixels relative to the canvas. */\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** The pointer as a style sees it. */\nexport interface Pointer extends Point {\n /** Eases from 0 to 1 while the pointer is over the canvas, and back when it leaves. Always 0 when not interactive. */\n influence: number;\n}\n\n/** What a style gets for each frame. The engine reuses this object: read it during `render()`, don't keep it. */\nexport interface Frame {\n /** Canvas size in CSS pixels. */\n width: number;\n height: number;\n /** Pixel density the canvas is rendered at. */\n dpr: number;\n /** Seconds since the previous frame (`0` on the first frame after a pause). */\n dt: number;\n /**\n * The user prefers reduced motion (and `respectReducedMotion` is on): hold the\n * style's own ambient animation still. Reactions to the pointer and clicks may still play.\n */\n reducedMotion: boolean;\n pointer: Pointer;\n /** Clicks / taps on the canvas since the previous frame. Empty when not interactive. */\n clicks: readonly Point[];\n}\n\n/**\n * Draws one style into a canvas. The engine has already cleared the canvas, and\n * takes care of the background colour and the edge fades, so just draw the pattern:\n * - Canvas 2D: the transform is set so you draw in CSS pixels.\n * - WebGL: the viewport covers the canvas, and blending is set up for premultiplied alpha\n * (`ONE, ONE_MINUS_SRC_ALPHA`), so output premultiplied colour.\n */\nexport interface StyleRenderer<O> {\n /** The style's options changed. */\n update(options: O): void;\n /**\n * Draw one frame. Return `true` while a short-lived effect (e.g. a click ripple)\n * is still playing: the engine then keeps rendering at the full frame rate.\n */\n render(frame: Frame): boolean | void;\n /** Free GPU resources and the like. */\n destroy?(): void;\n}\n\nexport interface StyleDefinition<O extends object> {\n /** Unique, lowercase name, e.g. `'topo'`. */\n name: string;\n defaults: Readonly<O>;\n /**\n * Whether the background moves on its own with these options. When it doesn't,\n * the engine only renders on pointer movement and option changes.\n */\n animates(options: O): boolean;\n /**\n * The lowest frame rate at which the ambient animation still looks smooth with\n * these options. The engine renders the ambient animation at the lower of this\n * and its `idleFps`. Omit to always use `idleFps`.\n */\n idleFps?(options: O): number;\n /**\n * How the style can be drawn, made with `webglRenderer()` and `canvas2dRenderer()`, in order of\n * preference: the engine uses the first one the browser supports. At least one.\n */\n renderers: readonly Renderer<O>[];\n}\n\n/** Everything about a style except how it's drawn: shared by its WebGL-only, Canvas 2D-only and auto builds. */\nexport type StyleBase<O extends object> = Omit<StyleDefinition<O>, 'renderers'>;\n\n/** A style with its options filled in, ready for `createBackground()`. */\nexport interface Style<O extends object> {\n readonly definition: StyleDefinition<O>;\n readonly options: Readonly<O>;\n}\n\nexport interface StyleFactory<O extends object> {\n (options?: Partial<O>): Style<O>;\n readonly styleName: string;\n readonly defaults: Readonly<O>;\n}\n\n/**\n * Turn a style definition into the function users call, e.g. `topo({ levels: 14 })`.\n */\nexport function defineStyle<O extends object>(definition: StyleDefinition<O>): StyleFactory<O> {\n if (definition.renderers.length === 0) {\n throw new Error(`interactive-bg: style \"${definition.name}\" has no renderer`);\n }\n const factory = (options: Partial<O> = {}): Style<O> => ({\n definition,\n options: { ...definition.defaults, ...options },\n });\n return Object.assign(factory, { styleName: definition.name, defaults: definition.defaults });\n}\n"],"mappings":";;;;AA8FA,SAAgB,YAA8B,YAAiD;CAC7F,IAAI,WAAW,UAAU,WAAW,GAClC,MAAM,IAAI,MAAM,0BAA0B,WAAW,KAAK,kBAAkB;CAE9E,MAAM,WAAW,UAAsB,CAAC,OAAiB;EACvD;EACA,SAAS;GAAE,GAAG,WAAW;GAAU,GAAG;EAAQ;CAChD;CACA,OAAO,OAAO,OAAO,SAAS;EAAE,WAAW,WAAW;EAAM,UAAU,WAAW;CAAS,CAAC;AAC7F"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Renderer } from './surface.cjs';
|
|
2
|
+
export type RendererName = 'webgl' | 'canvas2d';
|
|
3
|
+
/** A position in CSS pixels relative to the canvas. */
|
|
4
|
+
export interface Point {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
/** The pointer as a style sees it. */
|
|
9
|
+
export interface Pointer extends Point {
|
|
10
|
+
/** Eases from 0 to 1 while the pointer is over the canvas, and back when it leaves. Always 0 when not interactive. */
|
|
11
|
+
influence: number;
|
|
12
|
+
}
|
|
13
|
+
/** What a style gets for each frame. The engine reuses this object: read it during `render()`, don't keep it. */
|
|
14
|
+
export interface Frame {
|
|
15
|
+
/** Canvas size in CSS pixels. */
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
/** Pixel density the canvas is rendered at. */
|
|
19
|
+
dpr: number;
|
|
20
|
+
/** Seconds since the previous frame (`0` on the first frame after a pause). */
|
|
21
|
+
dt: number;
|
|
22
|
+
/**
|
|
23
|
+
* The user prefers reduced motion (and `respectReducedMotion` is on): hold the
|
|
24
|
+
* style's own ambient animation still. Reactions to the pointer and clicks may still play.
|
|
25
|
+
*/
|
|
26
|
+
reducedMotion: boolean;
|
|
27
|
+
pointer: Pointer;
|
|
28
|
+
/** Clicks / taps on the canvas since the previous frame. Empty when not interactive. */
|
|
29
|
+
clicks: readonly Point[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Draws one style into a canvas. The engine has already cleared the canvas, and
|
|
33
|
+
* takes care of the background colour and the edge fades, so just draw the pattern:
|
|
34
|
+
* - Canvas 2D: the transform is set so you draw in CSS pixels.
|
|
35
|
+
* - WebGL: the viewport covers the canvas, and blending is set up for premultiplied alpha
|
|
36
|
+
* (`ONE, ONE_MINUS_SRC_ALPHA`), so output premultiplied colour.
|
|
37
|
+
*/
|
|
38
|
+
export interface StyleRenderer<O> {
|
|
39
|
+
/** The style's options changed. */
|
|
40
|
+
update(options: O): void;
|
|
41
|
+
/**
|
|
42
|
+
* Draw one frame. Return `true` while a short-lived effect (e.g. a click ripple)
|
|
43
|
+
* is still playing: the engine then keeps rendering at the full frame rate.
|
|
44
|
+
*/
|
|
45
|
+
render(frame: Frame): boolean | void;
|
|
46
|
+
/** Free GPU resources and the like. */
|
|
47
|
+
destroy?(): void;
|
|
48
|
+
}
|
|
49
|
+
export interface StyleDefinition<O extends object> {
|
|
50
|
+
/** Unique, lowercase name, e.g. `'topo'`. */
|
|
51
|
+
name: string;
|
|
52
|
+
defaults: Readonly<O>;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the background moves on its own with these options. When it doesn't,
|
|
55
|
+
* the engine only renders on pointer movement and option changes.
|
|
56
|
+
*/
|
|
57
|
+
animates(options: O): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The lowest frame rate at which the ambient animation still looks smooth with
|
|
60
|
+
* these options. The engine renders the ambient animation at the lower of this
|
|
61
|
+
* and its `idleFps`. Omit to always use `idleFps`.
|
|
62
|
+
*/
|
|
63
|
+
idleFps?(options: O): number;
|
|
64
|
+
/**
|
|
65
|
+
* How the style can be drawn, made with `webglRenderer()` and `canvas2dRenderer()`, in order of
|
|
66
|
+
* preference: the engine uses the first one the browser supports. At least one.
|
|
67
|
+
*/
|
|
68
|
+
renderers: readonly Renderer<O>[];
|
|
69
|
+
}
|
|
70
|
+
/** Everything about a style except how it's drawn: shared by its WebGL-only, Canvas 2D-only and auto builds. */
|
|
71
|
+
export type StyleBase<O extends object> = Omit<StyleDefinition<O>, 'renderers'>;
|
|
72
|
+
/** A style with its options filled in, ready for `createBackground()`. */
|
|
73
|
+
export interface Style<O extends object> {
|
|
74
|
+
readonly definition: StyleDefinition<O>;
|
|
75
|
+
readonly options: Readonly<O>;
|
|
76
|
+
}
|
|
77
|
+
export interface StyleFactory<O extends object> {
|
|
78
|
+
(options?: Partial<O>): Style<O>;
|
|
79
|
+
readonly styleName: string;
|
|
80
|
+
readonly defaults: Readonly<O>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Turn a style definition into the function users call, e.g. `topo({ levels: 14 })`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function defineStyle<O extends object>(definition: StyleDefinition<O>): StyleFactory<O>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Renderer } from './surface.js';
|
|
2
|
+
export type RendererName = 'webgl' | 'canvas2d';
|
|
3
|
+
/** A position in CSS pixels relative to the canvas. */
|
|
4
|
+
export interface Point {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
/** The pointer as a style sees it. */
|
|
9
|
+
export interface Pointer extends Point {
|
|
10
|
+
/** Eases from 0 to 1 while the pointer is over the canvas, and back when it leaves. Always 0 when not interactive. */
|
|
11
|
+
influence: number;
|
|
12
|
+
}
|
|
13
|
+
/** What a style gets for each frame. The engine reuses this object: read it during `render()`, don't keep it. */
|
|
14
|
+
export interface Frame {
|
|
15
|
+
/** Canvas size in CSS pixels. */
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
/** Pixel density the canvas is rendered at. */
|
|
19
|
+
dpr: number;
|
|
20
|
+
/** Seconds since the previous frame (`0` on the first frame after a pause). */
|
|
21
|
+
dt: number;
|
|
22
|
+
/**
|
|
23
|
+
* The user prefers reduced motion (and `respectReducedMotion` is on): hold the
|
|
24
|
+
* style's own ambient animation still. Reactions to the pointer and clicks may still play.
|
|
25
|
+
*/
|
|
26
|
+
reducedMotion: boolean;
|
|
27
|
+
pointer: Pointer;
|
|
28
|
+
/** Clicks / taps on the canvas since the previous frame. Empty when not interactive. */
|
|
29
|
+
clicks: readonly Point[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Draws one style into a canvas. The engine has already cleared the canvas, and
|
|
33
|
+
* takes care of the background colour and the edge fades, so just draw the pattern:
|
|
34
|
+
* - Canvas 2D: the transform is set so you draw in CSS pixels.
|
|
35
|
+
* - WebGL: the viewport covers the canvas, and blending is set up for premultiplied alpha
|
|
36
|
+
* (`ONE, ONE_MINUS_SRC_ALPHA`), so output premultiplied colour.
|
|
37
|
+
*/
|
|
38
|
+
export interface StyleRenderer<O> {
|
|
39
|
+
/** The style's options changed. */
|
|
40
|
+
update(options: O): void;
|
|
41
|
+
/**
|
|
42
|
+
* Draw one frame. Return `true` while a short-lived effect (e.g. a click ripple)
|
|
43
|
+
* is still playing: the engine then keeps rendering at the full frame rate.
|
|
44
|
+
*/
|
|
45
|
+
render(frame: Frame): boolean | void;
|
|
46
|
+
/** Free GPU resources and the like. */
|
|
47
|
+
destroy?(): void;
|
|
48
|
+
}
|
|
49
|
+
export interface StyleDefinition<O extends object> {
|
|
50
|
+
/** Unique, lowercase name, e.g. `'topo'`. */
|
|
51
|
+
name: string;
|
|
52
|
+
defaults: Readonly<O>;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the background moves on its own with these options. When it doesn't,
|
|
55
|
+
* the engine only renders on pointer movement and option changes.
|
|
56
|
+
*/
|
|
57
|
+
animates(options: O): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The lowest frame rate at which the ambient animation still looks smooth with
|
|
60
|
+
* these options. The engine renders the ambient animation at the lower of this
|
|
61
|
+
* and its `idleFps`. Omit to always use `idleFps`.
|
|
62
|
+
*/
|
|
63
|
+
idleFps?(options: O): number;
|
|
64
|
+
/**
|
|
65
|
+
* How the style can be drawn, made with `webglRenderer()` and `canvas2dRenderer()`, in order of
|
|
66
|
+
* preference: the engine uses the first one the browser supports. At least one.
|
|
67
|
+
*/
|
|
68
|
+
renderers: readonly Renderer<O>[];
|
|
69
|
+
}
|
|
70
|
+
/** Everything about a style except how it's drawn: shared by its WebGL-only, Canvas 2D-only and auto builds. */
|
|
71
|
+
export type StyleBase<O extends object> = Omit<StyleDefinition<O>, 'renderers'>;
|
|
72
|
+
/** A style with its options filled in, ready for `createBackground()`. */
|
|
73
|
+
export interface Style<O extends object> {
|
|
74
|
+
readonly definition: StyleDefinition<O>;
|
|
75
|
+
readonly options: Readonly<O>;
|
|
76
|
+
}
|
|
77
|
+
export interface StyleFactory<O extends object> {
|
|
78
|
+
(options?: Partial<O>): Style<O>;
|
|
79
|
+
readonly styleName: string;
|
|
80
|
+
readonly defaults: Readonly<O>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Turn a style definition into the function users call, e.g. `topo({ levels: 14 })`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function defineStyle<O extends object>(definition: StyleDefinition<O>): StyleFactory<O>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/core/style.ts
|
|
2
|
+
/**
|
|
3
|
+
* Turn a style definition into the function users call, e.g. `topo({ levels: 14 })`.
|
|
4
|
+
*/
|
|
5
|
+
function defineStyle(definition) {
|
|
6
|
+
if (definition.renderers.length === 0) throw new Error(`interactive-bg: style "${definition.name}" has no renderer`);
|
|
7
|
+
const factory = (options = {}) => ({
|
|
8
|
+
definition,
|
|
9
|
+
options: {
|
|
10
|
+
...definition.defaults,
|
|
11
|
+
...options
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
return Object.assign(factory, {
|
|
15
|
+
styleName: definition.name,
|
|
16
|
+
defaults: definition.defaults
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export { defineStyle };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=style.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../src/core/style.ts"],"sourcesContent":["import type { Renderer } from './surface';\n\nexport type RendererName = 'webgl' | 'canvas2d';\n\n/** A position in CSS pixels relative to the canvas. */\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** The pointer as a style sees it. */\nexport interface Pointer extends Point {\n /** Eases from 0 to 1 while the pointer is over the canvas, and back when it leaves. Always 0 when not interactive. */\n influence: number;\n}\n\n/** What a style gets for each frame. The engine reuses this object: read it during `render()`, don't keep it. */\nexport interface Frame {\n /** Canvas size in CSS pixels. */\n width: number;\n height: number;\n /** Pixel density the canvas is rendered at. */\n dpr: number;\n /** Seconds since the previous frame (`0` on the first frame after a pause). */\n dt: number;\n /**\n * The user prefers reduced motion (and `respectReducedMotion` is on): hold the\n * style's own ambient animation still. Reactions to the pointer and clicks may still play.\n */\n reducedMotion: boolean;\n pointer: Pointer;\n /** Clicks / taps on the canvas since the previous frame. Empty when not interactive. */\n clicks: readonly Point[];\n}\n\n/**\n * Draws one style into a canvas. The engine has already cleared the canvas, and\n * takes care of the background colour and the edge fades, so just draw the pattern:\n * - Canvas 2D: the transform is set so you draw in CSS pixels.\n * - WebGL: the viewport covers the canvas, and blending is set up for premultiplied alpha\n * (`ONE, ONE_MINUS_SRC_ALPHA`), so output premultiplied colour.\n */\nexport interface StyleRenderer<O> {\n /** The style's options changed. */\n update(options: O): void;\n /**\n * Draw one frame. Return `true` while a short-lived effect (e.g. a click ripple)\n * is still playing: the engine then keeps rendering at the full frame rate.\n */\n render(frame: Frame): boolean | void;\n /** Free GPU resources and the like. */\n destroy?(): void;\n}\n\nexport interface StyleDefinition<O extends object> {\n /** Unique, lowercase name, e.g. `'topo'`. */\n name: string;\n defaults: Readonly<O>;\n /**\n * Whether the background moves on its own with these options. When it doesn't,\n * the engine only renders on pointer movement and option changes.\n */\n animates(options: O): boolean;\n /**\n * The lowest frame rate at which the ambient animation still looks smooth with\n * these options. The engine renders the ambient animation at the lower of this\n * and its `idleFps`. Omit to always use `idleFps`.\n */\n idleFps?(options: O): number;\n /**\n * How the style can be drawn, made with `webglRenderer()` and `canvas2dRenderer()`, in order of\n * preference: the engine uses the first one the browser supports. At least one.\n */\n renderers: readonly Renderer<O>[];\n}\n\n/** Everything about a style except how it's drawn: shared by its WebGL-only, Canvas 2D-only and auto builds. */\nexport type StyleBase<O extends object> = Omit<StyleDefinition<O>, 'renderers'>;\n\n/** A style with its options filled in, ready for `createBackground()`. */\nexport interface Style<O extends object> {\n readonly definition: StyleDefinition<O>;\n readonly options: Readonly<O>;\n}\n\nexport interface StyleFactory<O extends object> {\n (options?: Partial<O>): Style<O>;\n readonly styleName: string;\n readonly defaults: Readonly<O>;\n}\n\n/**\n * Turn a style definition into the function users call, e.g. `topo({ levels: 14 })`.\n */\nexport function defineStyle<O extends object>(definition: StyleDefinition<O>): StyleFactory<O> {\n if (definition.renderers.length === 0) {\n throw new Error(`interactive-bg: style \"${definition.name}\" has no renderer`);\n }\n const factory = (options: Partial<O> = {}): Style<O> => ({\n definition,\n options: { ...definition.defaults, ...options },\n });\n return Object.assign(factory, { styleName: definition.name, defaults: definition.defaults });\n}\n"],"mappings":";;;;AA8FA,SAAgB,YAA8B,YAAiD;CAC7F,IAAI,WAAW,UAAU,WAAW,GAClC,MAAM,IAAI,MAAM,0BAA0B,WAAW,KAAK,kBAAkB;CAE9E,MAAM,WAAW,UAAsB,CAAC,OAAiB;EACvD;EACA,SAAS;GAAE,GAAG,WAAW;GAAU,GAAG;EAAQ;CAChD;CACA,OAAO,OAAO,OAAO,SAAS;EAAE,WAAW,WAAW;EAAM,UAAU,WAAW;CAAS,CAAC;AAC7F"}
|