fancy-ui-react 0.2.1 → 0.3.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/README.md +4 -1
- package/dist/components/button/Button.js +5 -1
- package/dist/components/datamosh-transition/DatamoshTransition.d.ts +76 -0
- package/dist/components/datamosh-transition/DatamoshTransition.js +368 -0
- package/dist/components/datamosh-transition/datamosh-transition-core.d.ts +180 -0
- package/dist/components/datamosh-transition/datamosh-transition-core.js +370 -0
- package/dist/components/datamosh-transition/index.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/styles.css +75 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ React counterpart of [`fancy-ui-svelte`](https://www.npmjs.com/package/fancy-ui-
|
|
|
4
4
|
same components, same visual contract, ported from the Svelte 5 reference
|
|
5
5
|
implementation that lives at the root of this repo.
|
|
6
6
|
|
|
7
|
-
Status: at parity. All
|
|
7
|
+
Status: at parity. All 145 components of `fancy-ui-svelte` are ported and
|
|
8
8
|
exported from the package root, alongside the `sound` family; the cameleon
|
|
9
9
|
skin engine (FancyProvider + primitives + skins) ships on the
|
|
10
10
|
`fancy-ui-react/cameleon` subpath. The Svelte package remains the reference;
|
|
@@ -240,6 +240,9 @@ Deliberate, small, and documented — everything else is a faithful transpose:
|
|
|
240
240
|
- **confetti**: No behavioural divergence in `ConfettiButton`: no className, no rest-prop spread, no `type` attribute — the Svelte source declares only `options`, `children` and `sound` on a bare `<button>`.
|
|
241
241
|
- **confetti**: `Confetti` gains one extra file over the Svelte tree (`context.ts`), mirroring the `button-group` precedent; one file per Svelte file is otherwise preserved.
|
|
242
242
|
- **confetti**: `fire()` reaches consumers through the ref channel, not a component instance. Svelte declares `export function fire(opts)` (callable via `bind:this`); React has no instance, so `Confetti` is a `forwardRef<ConfettiHandle, ConfettiProps>` whose ref carries `{ fire }` rather than the DOM node. This is the one forwardRef in the port not backed by a `ref = $bindable` declaration (PORTING.md C-4); the alternative was deleting a public method. `ConfettiHandle` is exported from `Confetti.tsx` but deliberately NOT from `index.ts`, since the mandated export list is exactly Confetti/ConfettiButton + Props types.
|
|
243
|
+
- **datamosh-transition**: `cover()`, `reveal()` and `play()` reach consumers through the ref channel, not a component instance. Svelte declares them with `export function` (callable via `bind:this`) and also binds the overlay element through `ref = $bindable(null)`; React has no instance, so `DatamoshTransition` is a `forwardRef<DatamoshTransitionHandle, DatamoshTransitionProps>` whose ref carries `{ cover, reveal, play, element }` — the three methods plus the overlay element that the Svelte `ref` published.
|
|
244
|
+
- **datamosh-transition**: The read-only bindable `phase` becomes an `onPhaseChange(phase)` callback, called on every change (`covering`, `covered`, `revealing`, `idle`); `data-state` on the overlay carries the same value on both sides.
|
|
245
|
+
- **datamosh-transition**: SSR markup: React serialises the overlay's inline style as `style="z-index:9999"` (no space after the colon) where Svelte emits `z-index: 9999`. Same declaration, different byte string.
|
|
243
246
|
- **direction-aware-hover**: fade transitions on the overlay and caption are reproduced via usePresence + a local fade300 TransitionSpec (WAAPI mechanism per internals-api §5) instead of a framework transition directive — same 300ms linear opacity curve
|
|
244
247
|
- **direction-aware-hover**: isMobile lives in a ref rather than state — it is only read in event handlers and never drives markup, so resize events no longer trigger re-renders (no observable difference)
|
|
245
248
|
- **direction-aware-hover**: non-null assertion added on event.touches[0] to satisfy the React tsconfig's indexed-access checking (no runtime change)
|
|
@@ -42,7 +42,11 @@ const Button = forwardRef(
|
|
|
42
42
|
const classes = cn(
|
|
43
43
|
"ft-btn",
|
|
44
44
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium",
|
|
45
|
-
|
|
45
|
+
// No `transition-colors` here: the colour channel is declared in
|
|
46
|
+
// `button.css` instead, alongside the press, so one rule owns the
|
|
47
|
+
// whole transition list. The utility would only add a second,
|
|
48
|
+
// competing declaration over the same three properties.
|
|
49
|
+
"cursor-pointer",
|
|
46
50
|
"focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ft-btn-accent)]/35",
|
|
47
51
|
// `data-disabled` covers the anchor branch, which has no native `:disabled`
|
|
48
52
|
// pseudo-class to hang the same dimmed treatment off. It tracks `disabled`
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { type DatamoshEffect, type DatamoshPaletteName, type DatamoshSweep } from "./datamosh-transition-core.js";
|
|
3
|
+
export type DatamoshTransitionPhase = "idle" | "covering" | "covered" | "revealing";
|
|
4
|
+
/**
|
|
5
|
+
* What the forwarded ref carries. The Svelte source exposes `cover()`,
|
|
6
|
+
* `reveal()` and `play()` on the component instance and binds the overlay
|
|
7
|
+
* element through `ref`; React has no instance, so both travel through one
|
|
8
|
+
* handle: the three methods plus `element`.
|
|
9
|
+
*/
|
|
10
|
+
export interface DatamoshTransitionHandle {
|
|
11
|
+
/** Cover the page. Resolves once every column is down. */
|
|
12
|
+
cover: () => Promise<void>;
|
|
13
|
+
/** Clear the page. Resolves once every column has fallen away. */
|
|
14
|
+
reveal: () => Promise<void>;
|
|
15
|
+
/** Cover, then reveal — a one-shot flash. */
|
|
16
|
+
play: () => Promise<void>;
|
|
17
|
+
/** The overlay element. */
|
|
18
|
+
readonly element: HTMLDivElement | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* DatamoshTransition — a page-transition overlay that looks like a corrupted
|
|
22
|
+
* video decode.
|
|
23
|
+
*
|
|
24
|
+
* A fixed grid of columns (narrow on the left, wide on the right) fills with
|
|
25
|
+
* flat blocks of saturated colour. Inside each column a stack of tiles falls
|
|
26
|
+
* along one sigmoid: slivers at the edges, one tile snapping open through
|
|
27
|
+
* the middle. `cover()` drops the columns over the page like a curtain,
|
|
28
|
+
* right to left; `reveal()` lets them fall away. Nothing moves sideways.
|
|
29
|
+
*
|
|
30
|
+
* Mount once (root layout) and drive it from your router's navigation hook.
|
|
31
|
+
*/
|
|
32
|
+
export interface DatamoshTransitionProps extends Omit<HTMLAttributes<HTMLDivElement>, "color"> {
|
|
33
|
+
/** Additional classes on the overlay */
|
|
34
|
+
className?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Palette name or colour list (hex or rgb()). The lightest and darkest
|
|
37
|
+
* colours recur most often.
|
|
38
|
+
*/
|
|
39
|
+
colors?: string[] | DatamoshPaletteName;
|
|
40
|
+
/** Shape of the covering band: curtain, rise, split or interlace */
|
|
41
|
+
variant?: DatamoshEffect;
|
|
42
|
+
/** Order in which the columns move */
|
|
43
|
+
sweep?: DatamoshSweep;
|
|
44
|
+
/**
|
|
45
|
+
* Picture to decode: tiles take their colours from it instead of the
|
|
46
|
+
* palette. A URL (same-origin or CORS-enabled), an image or a canvas.
|
|
47
|
+
*/
|
|
48
|
+
source?: string | HTMLImageElement | HTMLCanvasElement;
|
|
49
|
+
/** Seed for the colour order — same seed, same strip */
|
|
50
|
+
seed?: number;
|
|
51
|
+
/** Number of columns */
|
|
52
|
+
columns?: number;
|
|
53
|
+
/** Column edge exponent: 1 = uniform grid, higher = wider columns to the right */
|
|
54
|
+
power?: number;
|
|
55
|
+
/** Tiles per column stack (forced odd) */
|
|
56
|
+
tiles?: number;
|
|
57
|
+
/** Time for one column to cover, ms */
|
|
58
|
+
coverDuration?: number;
|
|
59
|
+
/** Time for one column to clear, ms */
|
|
60
|
+
revealDuration?: number;
|
|
61
|
+
/** Delay between neighbouring columns, ms (rightmost first) */
|
|
62
|
+
stagger?: number;
|
|
63
|
+
/** Speed of the falling tiles, 1 = default */
|
|
64
|
+
speed?: number;
|
|
65
|
+
/** Fill the positioned parent instead of the viewport */
|
|
66
|
+
contained?: boolean;
|
|
67
|
+
/** Stacking order of the overlay */
|
|
68
|
+
zIndex?: number;
|
|
69
|
+
/** Called with every phase change (the Svelte `bind:phase`) */
|
|
70
|
+
onPhaseChange?: (phase: DatamoshTransitionPhase) => void;
|
|
71
|
+
/** Called once the page is fully covered */
|
|
72
|
+
oncovered?: () => void;
|
|
73
|
+
/** Called once the page is fully revealed */
|
|
74
|
+
onrevealed?: () => void;
|
|
75
|
+
}
|
|
76
|
+
export declare const DatamoshTransition: import("react").ForwardRefExoticComponent<DatamoshTransitionProps & import("react").RefAttributes<DatamoshTransitionHandle>>;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useMemo, useState, useRef, useCallback, useImperativeHandle, useEffect } from "react";
|
|
4
|
+
import { cn } from "../../utils.js";
|
|
5
|
+
import { useLiveRef } from "../../internals/dom/use-live-ref.js";
|
|
6
|
+
import { useReducedMotion } from "../../internals/motion/media-query.js";
|
|
7
|
+
import { resolvePalette, buildStrip, columnDelays, paletteRoles, createCoverage, DEFAULT_POWER, oddTiles, DEFAULT_COLUMNS, DEFAULT_TILES, coverFit, sampleSource, columnEdges, settleCoverage, CYCLE, FALL_STAGGER, columnBand, easeEdge, columnFlow, columnTiles, sourceSlot, stripIndex, stepCoverage, BLEED } from "./datamosh-transition-core.js";
|
|
8
|
+
function currentDpr() {
|
|
9
|
+
return Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
|
|
10
|
+
}
|
|
11
|
+
const DatamoshTransition = forwardRef(
|
|
12
|
+
function DatamoshTransition2({
|
|
13
|
+
className,
|
|
14
|
+
colors = "broadcast",
|
|
15
|
+
variant = "curtain",
|
|
16
|
+
sweep = "right",
|
|
17
|
+
source,
|
|
18
|
+
seed = 1,
|
|
19
|
+
columns = DEFAULT_COLUMNS,
|
|
20
|
+
power = DEFAULT_POWER,
|
|
21
|
+
tiles = DEFAULT_TILES,
|
|
22
|
+
coverDuration = 380,
|
|
23
|
+
revealDuration = 480,
|
|
24
|
+
stagger = 28,
|
|
25
|
+
speed = 1,
|
|
26
|
+
contained = false,
|
|
27
|
+
zIndex = 9999,
|
|
28
|
+
onPhaseChange,
|
|
29
|
+
oncovered,
|
|
30
|
+
onrevealed,
|
|
31
|
+
style,
|
|
32
|
+
...restProps
|
|
33
|
+
}, ref) {
|
|
34
|
+
const reduced = useReducedMotion();
|
|
35
|
+
const palette = useMemo(() => resolvePalette(colors), [colors]);
|
|
36
|
+
const columnsC = Math.max(1, Math.round(Number.isFinite(columns) ? columns : DEFAULT_COLUMNS));
|
|
37
|
+
const powerC = Number.isFinite(power) && power > 0 ? power : DEFAULT_POWER;
|
|
38
|
+
const tilesC = oddTiles(tiles);
|
|
39
|
+
const speedC = Number.isFinite(speed) && speed > 0 ? speed : 1;
|
|
40
|
+
const strip = useMemo(() => buildStrip(seed, palette), [seed, palette]);
|
|
41
|
+
const delays = useMemo(
|
|
42
|
+
() => columnDelays(
|
|
43
|
+
columnsC,
|
|
44
|
+
sweep,
|
|
45
|
+
Math.max(0, Number.isFinite(stagger) ? stagger : 0) / 1e3,
|
|
46
|
+
seed
|
|
47
|
+
),
|
|
48
|
+
[columnsC, sweep, stagger, seed]
|
|
49
|
+
);
|
|
50
|
+
const backing = palette[paletteRoles(palette).darks[0] ?? 0] ?? "#14101f";
|
|
51
|
+
const [phase, setPhaseState] = useState("idle");
|
|
52
|
+
const hostRef = useRef(null);
|
|
53
|
+
const canvasRef = useRef(null);
|
|
54
|
+
const live = useLiveRef({
|
|
55
|
+
reduced,
|
|
56
|
+
palette,
|
|
57
|
+
columnsC,
|
|
58
|
+
powerC,
|
|
59
|
+
tilesC,
|
|
60
|
+
speedC,
|
|
61
|
+
strip,
|
|
62
|
+
delays,
|
|
63
|
+
backing,
|
|
64
|
+
variant,
|
|
65
|
+
coverDuration,
|
|
66
|
+
revealDuration,
|
|
67
|
+
onPhaseChange,
|
|
68
|
+
oncovered,
|
|
69
|
+
onrevealed
|
|
70
|
+
});
|
|
71
|
+
const s = useRef({
|
|
72
|
+
ctx: null,
|
|
73
|
+
w: 0,
|
|
74
|
+
h: 0,
|
|
75
|
+
edges: [],
|
|
76
|
+
cov: createCoverage(DEFAULT_COLUMNS),
|
|
77
|
+
direction: "reveal",
|
|
78
|
+
phase: "idle",
|
|
79
|
+
phaseTime: 0,
|
|
80
|
+
elapsed: 0,
|
|
81
|
+
rafId: null,
|
|
82
|
+
lastT: 0,
|
|
83
|
+
pending: [],
|
|
84
|
+
sourceImage: null,
|
|
85
|
+
sourceSize: { w: 0, h: 0 },
|
|
86
|
+
sourceTable: null
|
|
87
|
+
}).current;
|
|
88
|
+
const engine = useMemo(() => {
|
|
89
|
+
function setPhase(next) {
|
|
90
|
+
if (s.phase === next) return;
|
|
91
|
+
s.phase = next;
|
|
92
|
+
setPhaseState(next);
|
|
93
|
+
live.current.onPhaseChange?.(next);
|
|
94
|
+
}
|
|
95
|
+
function settlePending() {
|
|
96
|
+
const list = s.pending;
|
|
97
|
+
s.pending = [];
|
|
98
|
+
for (const resolve of list) resolve();
|
|
99
|
+
}
|
|
100
|
+
function clear() {
|
|
101
|
+
s.ctx?.clearRect(0, 0, s.w, s.h);
|
|
102
|
+
}
|
|
103
|
+
function draw() {
|
|
104
|
+
const c = s.ctx;
|
|
105
|
+
const p = live.current;
|
|
106
|
+
if (!c || !s.edges.length) return;
|
|
107
|
+
const { w, h } = s;
|
|
108
|
+
c.clearRect(0, 0, w, h);
|
|
109
|
+
const bleed = Math.round(BLEED * h);
|
|
110
|
+
const len = p.palette.length;
|
|
111
|
+
const t = p.reduced ? CYCLE * 0.45 + p.columnsC * FALL_STAGGER : s.elapsed;
|
|
112
|
+
for (let i = 0; i < p.columnsC; i++) {
|
|
113
|
+
const x0 = s.edges[i];
|
|
114
|
+
const cw = s.edges[i + 1] - x0;
|
|
115
|
+
if (cw <= 0) continue;
|
|
116
|
+
const band = columnBand(
|
|
117
|
+
p.variant,
|
|
118
|
+
i,
|
|
119
|
+
easeEdge(s.cov.top[i] ?? 0),
|
|
120
|
+
easeEdge(s.cov.bot[i] ?? 0)
|
|
121
|
+
);
|
|
122
|
+
const bandTop = Math.round(band.y0 * h);
|
|
123
|
+
const bandBot = Math.round(band.y1 * h);
|
|
124
|
+
if (bandBot <= bandTop) continue;
|
|
125
|
+
c.fillStyle = p.backing;
|
|
126
|
+
c.fillRect(x0, bandTop, cw, bandBot - bandTop);
|
|
127
|
+
const flow = columnFlow(t, i, p.columnsC, p.speedC);
|
|
128
|
+
for (const tile of columnTiles(flow, p.tilesC, h, bleed)) {
|
|
129
|
+
const top = band.mirror ? h - tile.bot : tile.top;
|
|
130
|
+
const bot = band.mirror ? h - tile.top : tile.bot;
|
|
131
|
+
const y0 = Math.max(bandTop, top);
|
|
132
|
+
const y1 = Math.min(bandBot, bot);
|
|
133
|
+
if (y1 <= y0) continue;
|
|
134
|
+
c.fillStyle = s.sourceTable?.[i]?.[sourceSlot(tile.id, i, p.tilesC)] ?? p.palette[stripIndex(p.strip, tile.id, i) % len];
|
|
135
|
+
c.fillRect(x0, y0, cw, y1 - y0);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function stopLoop() {
|
|
140
|
+
if (s.rafId !== null && typeof cancelAnimationFrame === "function")
|
|
141
|
+
cancelAnimationFrame(s.rafId);
|
|
142
|
+
s.rafId = null;
|
|
143
|
+
}
|
|
144
|
+
function requestFrame() {
|
|
145
|
+
if (s.rafId !== null || !s.ctx || typeof requestAnimationFrame !== "function") return;
|
|
146
|
+
s.lastT = 0;
|
|
147
|
+
s.rafId = requestAnimationFrame(tick);
|
|
148
|
+
}
|
|
149
|
+
function finish(dir) {
|
|
150
|
+
settleCoverage(s.cov, dir);
|
|
151
|
+
setPhase(dir === "cover" ? "covered" : "idle");
|
|
152
|
+
if (dir === "reveal") {
|
|
153
|
+
stopLoop();
|
|
154
|
+
clear();
|
|
155
|
+
} else if (!live.current.reduced) {
|
|
156
|
+
requestFrame();
|
|
157
|
+
} else {
|
|
158
|
+
draw();
|
|
159
|
+
}
|
|
160
|
+
settlePending();
|
|
161
|
+
if (dir === "cover") live.current.oncovered?.();
|
|
162
|
+
else live.current.onrevealed?.();
|
|
163
|
+
}
|
|
164
|
+
function tick(now) {
|
|
165
|
+
s.rafId = null;
|
|
166
|
+
if (!s.ctx) return;
|
|
167
|
+
const dt = s.lastT ? Math.min(0.05, Math.max(0, (now - s.lastT) / 1e3)) : 0;
|
|
168
|
+
s.lastT = now;
|
|
169
|
+
s.elapsed += dt;
|
|
170
|
+
if (s.phase === "covering" || s.phase === "revealing") {
|
|
171
|
+
s.phaseTime += dt;
|
|
172
|
+
const p = live.current;
|
|
173
|
+
const duration = (s.direction === "cover" ? p.coverDuration : p.revealDuration) / 1e3;
|
|
174
|
+
const done = stepCoverage(s.cov, s.direction, s.phaseTime, dt, duration, p.delays);
|
|
175
|
+
if (done) {
|
|
176
|
+
finish(s.direction);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
draw();
|
|
181
|
+
if (s.phase !== "idle") s.rafId = requestAnimationFrame(tick);
|
|
182
|
+
}
|
|
183
|
+
function run(dir) {
|
|
184
|
+
settlePending();
|
|
185
|
+
const promise = new Promise((resolve) => s.pending.push(resolve));
|
|
186
|
+
if (dir === "reveal" && s.phase === "idle" || dir === "cover" && s.phase === "covered") {
|
|
187
|
+
settlePending();
|
|
188
|
+
return promise;
|
|
189
|
+
}
|
|
190
|
+
s.direction = dir;
|
|
191
|
+
s.phaseTime = 0;
|
|
192
|
+
setPhase(dir === "cover" ? "covering" : "revealing");
|
|
193
|
+
const instant = !s.ctx || live.current.reduced || typeof requestAnimationFrame !== "function" || typeof document !== "undefined" && document.hidden;
|
|
194
|
+
if (instant) finish(dir);
|
|
195
|
+
else requestFrame();
|
|
196
|
+
return promise;
|
|
197
|
+
}
|
|
198
|
+
function buildSourceTable() {
|
|
199
|
+
s.sourceTable = null;
|
|
200
|
+
if (!s.sourceImage || !s.edges.length || typeof document === "undefined") return;
|
|
201
|
+
const pw = 96;
|
|
202
|
+
const ph = Math.max(8, Math.round(pw * s.h / Math.max(1, s.w)));
|
|
203
|
+
try {
|
|
204
|
+
const scratch = document.createElement("canvas");
|
|
205
|
+
scratch.width = pw;
|
|
206
|
+
scratch.height = ph;
|
|
207
|
+
const sctx = scratch.getContext("2d", { willReadFrequently: true });
|
|
208
|
+
if (!sctx) return;
|
|
209
|
+
const fit = coverFit(s.sourceSize.w, s.sourceSize.h, pw, ph);
|
|
210
|
+
sctx.drawImage(s.sourceImage, fit.sx, fit.sy, fit.sw, fit.sh, 0, 0, pw, ph);
|
|
211
|
+
const { data } = sctx.getImageData(0, 0, pw, ph);
|
|
212
|
+
const norm = s.edges.map((e) => e / Math.max(1, s.w));
|
|
213
|
+
s.sourceTable = sampleSource(data, pw, ph, norm, live.current.tilesC);
|
|
214
|
+
} catch {
|
|
215
|
+
s.sourceTable = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function measure() {
|
|
219
|
+
const host = hostRef.current;
|
|
220
|
+
const canvas = canvasRef.current;
|
|
221
|
+
if (!host || !canvas || !s.ctx) return;
|
|
222
|
+
const p = live.current;
|
|
223
|
+
const dpr = currentDpr();
|
|
224
|
+
s.w = Math.max(1, Math.round(host.clientWidth * dpr));
|
|
225
|
+
s.h = Math.max(1, Math.round(host.clientHeight * dpr));
|
|
226
|
+
canvas.width = s.w;
|
|
227
|
+
canvas.height = s.h;
|
|
228
|
+
s.ctx.imageSmoothingEnabled = false;
|
|
229
|
+
s.edges = columnEdges(s.w, p.columnsC, p.powerC);
|
|
230
|
+
buildSourceTable();
|
|
231
|
+
if (s.cov.top.length !== p.columnsC) {
|
|
232
|
+
const covered = s.phase === "covered" || s.phase === "covering";
|
|
233
|
+
s.cov = createCoverage(p.columnsC, covered);
|
|
234
|
+
}
|
|
235
|
+
if (s.phase !== "idle") draw();
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
run,
|
|
239
|
+
draw,
|
|
240
|
+
finish,
|
|
241
|
+
measure,
|
|
242
|
+
stopLoop,
|
|
243
|
+
requestFrame,
|
|
244
|
+
settlePending,
|
|
245
|
+
buildSourceTable
|
|
246
|
+
};
|
|
247
|
+
}, [live, s]);
|
|
248
|
+
const cover = useCallback(() => engine.run("cover"), [engine]);
|
|
249
|
+
const reveal = useCallback(() => engine.run("reveal"), [engine]);
|
|
250
|
+
const play = useCallback(async () => {
|
|
251
|
+
await engine.run("cover");
|
|
252
|
+
await engine.run("reveal");
|
|
253
|
+
}, [engine]);
|
|
254
|
+
useImperativeHandle(
|
|
255
|
+
ref,
|
|
256
|
+
() => ({
|
|
257
|
+
cover,
|
|
258
|
+
reveal,
|
|
259
|
+
play,
|
|
260
|
+
get element() {
|
|
261
|
+
return hostRef.current;
|
|
262
|
+
}
|
|
263
|
+
}),
|
|
264
|
+
[cover, reveal, play]
|
|
265
|
+
);
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
const host = hostRef.current;
|
|
268
|
+
const canvas = canvasRef.current;
|
|
269
|
+
if (!host || !canvas) return;
|
|
270
|
+
s.ctx = canvas.getContext("2d");
|
|
271
|
+
if (!s.ctx) return;
|
|
272
|
+
engine.measure();
|
|
273
|
+
let ro;
|
|
274
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
275
|
+
ro = new ResizeObserver(() => engine.measure());
|
|
276
|
+
ro.observe(host);
|
|
277
|
+
}
|
|
278
|
+
const onVisibility = () => {
|
|
279
|
+
if (!document.hidden) {
|
|
280
|
+
if (s.phase !== "idle") engine.requestFrame();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
engine.stopLoop();
|
|
284
|
+
if (s.phase === "covering") engine.finish("cover");
|
|
285
|
+
else if (s.phase === "revealing") engine.finish("reveal");
|
|
286
|
+
};
|
|
287
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
288
|
+
return () => {
|
|
289
|
+
engine.stopLoop();
|
|
290
|
+
ro?.disconnect();
|
|
291
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
292
|
+
engine.settlePending();
|
|
293
|
+
s.ctx = null;
|
|
294
|
+
};
|
|
295
|
+
}, [engine, s]);
|
|
296
|
+
const gridInit = useRef(false);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!gridInit.current) {
|
|
299
|
+
gridInit.current = true;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
engine.measure();
|
|
303
|
+
}, [columnsC, powerC, engine]);
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!reduced) return;
|
|
306
|
+
if (s.phase === "covering") engine.finish("cover");
|
|
307
|
+
else if (s.phase === "revealing") engine.finish("reveal");
|
|
308
|
+
else if (s.phase === "covered") {
|
|
309
|
+
engine.stopLoop();
|
|
310
|
+
engine.draw();
|
|
311
|
+
}
|
|
312
|
+
}, [reduced, engine, s]);
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
s.sourceImage = null;
|
|
315
|
+
s.sourceTable = null;
|
|
316
|
+
const src = source;
|
|
317
|
+
if (!src || typeof window === "undefined") return;
|
|
318
|
+
let cancelled = false;
|
|
319
|
+
const ready = (img2, iw, ih) => {
|
|
320
|
+
if (cancelled || iw <= 0 || ih <= 0) return;
|
|
321
|
+
s.sourceImage = img2;
|
|
322
|
+
s.sourceSize = { w: iw, h: ih };
|
|
323
|
+
engine.buildSourceTable();
|
|
324
|
+
if (s.phase !== "idle") engine.draw();
|
|
325
|
+
};
|
|
326
|
+
if (typeof src !== "string") {
|
|
327
|
+
if (src instanceof HTMLCanvasElement) ready(src, src.width, src.height);
|
|
328
|
+
else if (src.complete && src.naturalWidth) ready(src, src.naturalWidth, src.naturalHeight);
|
|
329
|
+
else
|
|
330
|
+
src.addEventListener("load", () => ready(src, src.naturalWidth, src.naturalHeight), {
|
|
331
|
+
once: true
|
|
332
|
+
});
|
|
333
|
+
return () => {
|
|
334
|
+
cancelled = true;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const img = new Image();
|
|
338
|
+
img.decoding = "async";
|
|
339
|
+
if (!src.startsWith("data:")) img.crossOrigin = "anonymous";
|
|
340
|
+
img.onload = () => ready(img, img.naturalWidth || 300, img.naturalHeight || 150);
|
|
341
|
+
img.src = src;
|
|
342
|
+
return () => {
|
|
343
|
+
cancelled = true;
|
|
344
|
+
img.onload = null;
|
|
345
|
+
};
|
|
346
|
+
}, [source, tilesC, engine, s]);
|
|
347
|
+
return /* @__PURE__ */ jsx(
|
|
348
|
+
"div",
|
|
349
|
+
{
|
|
350
|
+
ref: hostRef,
|
|
351
|
+
className: cn(
|
|
352
|
+
"datamosh-transition inset-0",
|
|
353
|
+
contained ? "absolute" : "fixed",
|
|
354
|
+
phase === "idle" ? "pointer-events-none invisible" : "pointer-events-auto",
|
|
355
|
+
className
|
|
356
|
+
),
|
|
357
|
+
style: { zIndex, ...style },
|
|
358
|
+
"data-state": phase,
|
|
359
|
+
"aria-hidden": "true",
|
|
360
|
+
...restProps,
|
|
361
|
+
children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "block h-full w-full" })
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
export {
|
|
367
|
+
DatamoshTransition
|
|
368
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DatamoshTransition core — pure math, no DOM.
|
|
3
|
+
*
|
|
4
|
+
* The look: flat blocks of saturated colour in a FIXED column grid, each column
|
|
5
|
+
* a stack of tiles falling along one sigmoid curve — slivers at the top and
|
|
6
|
+
* bottom, one fat tile snapping open through the middle. Nothing ever moves
|
|
7
|
+
* horizontally; only the vertical tiling animates.
|
|
8
|
+
*
|
|
9
|
+
* On top of that sits the transition itself: every column owns a covered band
|
|
10
|
+
* [top, bot] (0..1 of the height). Cover drops the band's bottom edge like a
|
|
11
|
+
* curtain, reveal lets its top edge fall away, columns staggered right to left.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_COLORS: readonly ["#ffffff", "#3566ff", "#ffcf00", "#192aff", "#d62036", "#282142", "#ec4978", "#ff9900", "#14101f", "#00dd33", "#00ef82"];
|
|
14
|
+
/** Column edges sit at W·(i/N)^power: narrow left, wide right — reads as receding perspective. */
|
|
15
|
+
export declare const DEFAULT_COLUMNS = 11;
|
|
16
|
+
export declare const DEFAULT_POWER = 1.65;
|
|
17
|
+
/** Odd so exactly one tile sits dead centre and takes the whole bulge. */
|
|
18
|
+
export declare const DEFAULT_TILES = 15;
|
|
19
|
+
/** Seconds per tile step at speed 1. */
|
|
20
|
+
export declare const CYCLE = 0.2;
|
|
21
|
+
/** Seconds between neighbouring columns starting to fall (right column first). */
|
|
22
|
+
export declare const FALL_STAGGER = 0.006;
|
|
23
|
+
/** Sigmoid exponent — steepness at the centre is tile height. */
|
|
24
|
+
export declare const STRETCH = 8.5;
|
|
25
|
+
/** Exponential arrival rate inside a tile step (the "spring"). */
|
|
26
|
+
export declare const SPRING_K = 1.6;
|
|
27
|
+
/** Each tile overhangs the one below by this fraction of the height. */
|
|
28
|
+
export declare const BLEED = 0.012;
|
|
29
|
+
/** Phase offset per column, in tiles — the zig-zag. Negative: stacks ride higher to the right. */
|
|
30
|
+
export declare const COL_PHASE = -0.4;
|
|
31
|
+
/** Slope of the curve past both ends; clamping would pile off-screen tiles onto y = 0. */
|
|
32
|
+
export declare const TAIL_SLOPE = 0.05;
|
|
33
|
+
/** Length of the cyclic colour strip every column reads. */
|
|
34
|
+
export declare const STRIP_LENGTH = 61;
|
|
35
|
+
/** Small, fast, seedable PRNG (32-bit state). Same seed → same sequence. */
|
|
36
|
+
export declare function mulberry32(seed: number): () => number;
|
|
37
|
+
/** Relative luminance of a hex (#rgb / #rrggbb) or rgb() colour; null when unparseable. */
|
|
38
|
+
export declare function luminance(color: string): number | null;
|
|
39
|
+
export interface PaletteRoles {
|
|
40
|
+
/** The lightest colour — recurs most often (~a quarter of the strip). */
|
|
41
|
+
light: number;
|
|
42
|
+
/** The darkest one or two colours — the other contrast anchor. */
|
|
43
|
+
darks: number[];
|
|
44
|
+
/** Everything else. */
|
|
45
|
+
hues: number[];
|
|
46
|
+
}
|
|
47
|
+
/** Split a palette into contrast anchors and mid-tone hues by luminance. */
|
|
48
|
+
export declare function paletteRoles(colors: readonly string[]): PaletteRoles;
|
|
49
|
+
/**
|
|
50
|
+
* The cyclic colour strip, as palette indices. Light and dark anchors recur
|
|
51
|
+
* often on purpose: a fixed palette stride walks past them and leaves flat
|
|
52
|
+
* mid-tone corduroy. No two neighbours repeat, including across the wrap.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildStrip(seed: number, colors: readonly string[], len?: number): number[];
|
|
55
|
+
/** Colour for a tile, keyed by its IDENTITY (never its on-screen slot) — colours stay put. */
|
|
56
|
+
export declare function stripIndex(strip: readonly number[], id: number, column: number): number;
|
|
57
|
+
/** Column edges in device px: W·(i/N)^power, rounded. Length = columns + 1. */
|
|
58
|
+
export declare function columnEdges(width: number, columns: number, power: number): number[];
|
|
59
|
+
/** Force a usable odd tile count (≥ 3). */
|
|
60
|
+
export declare function oddTiles(tiles: number): number;
|
|
61
|
+
/**
|
|
62
|
+
* Where tile boundary `k` sits, 0..1 of the height. Sigmoid across [0, tiles],
|
|
63
|
+
* extended LINEARLY (shallow slope) past both ends rather than clamped.
|
|
64
|
+
*/
|
|
65
|
+
export declare function tileEdge(k: number, tiles: number, stretch?: number): number;
|
|
66
|
+
/**
|
|
67
|
+
* Re-time the fraction of one tile step: exponential arrival mirrored at the
|
|
68
|
+
* halfway point. Rushes in, hangs at the centre (fattest), releases. Monotonic,
|
|
69
|
+
* exact at 0 and 1, so the stack never drifts off schedule.
|
|
70
|
+
*/
|
|
71
|
+
export declare function springStep(p: number): number;
|
|
72
|
+
/**
|
|
73
|
+
* Unbounded phase of column `i` at `elapsed` seconds. Never wrapped: tile
|
|
74
|
+
* identity is derived from it, so a wrap would teleport every boundary.
|
|
75
|
+
*/
|
|
76
|
+
export declare function columnFlow(elapsed: number, column: number, columns: number, speed?: number): number;
|
|
77
|
+
export interface TileRect {
|
|
78
|
+
id: number;
|
|
79
|
+
/** Device px, unclamped. */
|
|
80
|
+
top: number;
|
|
81
|
+
/** Device px, bleed included, unclamped. */
|
|
82
|
+
bot: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The tiles of one column at `flow`, ordered BOTTOM-UP (paint order): each
|
|
86
|
+
* tile overhangs the one below it by `bleed` px, so painting upward laps the
|
|
87
|
+
* overhang over the lower tile instead of hiding it under it.
|
|
88
|
+
*/
|
|
89
|
+
export declare function columnTiles(flow: number, tiles: number, height: number, bleed: number): TileRect[];
|
|
90
|
+
export type Direction = "cover" | "reveal";
|
|
91
|
+
export interface Coverage {
|
|
92
|
+
/** Covered band per column, linear 0..1 (eased only when drawn). */
|
|
93
|
+
top: Float32Array;
|
|
94
|
+
bot: Float32Array;
|
|
95
|
+
}
|
|
96
|
+
export declare function createCoverage(columns: number, covered?: boolean): Coverage;
|
|
97
|
+
/** Order in which the columns move. */
|
|
98
|
+
export type DatamoshSweep = "right" | "left" | "center" | "edges" | "random";
|
|
99
|
+
/**
|
|
100
|
+
* Delay before each column moves, in seconds. `right` starts at the rightmost
|
|
101
|
+
* column, `center` fans out from the middle, `edges` closes in from both
|
|
102
|
+
* sides, `random` is a seeded shuffle.
|
|
103
|
+
*/
|
|
104
|
+
export declare function columnDelays(columns: number, sweep: DatamoshSweep, stagger: number, seed?: number): number[];
|
|
105
|
+
/**
|
|
106
|
+
* Advance every column one step toward `direction`. Cover drops the band's
|
|
107
|
+
* bottom edge to 1 (and pulls a half-fallen top back up); reveal lets the top
|
|
108
|
+
* edge fall onto the bottom one. Monotonic per column, so an interruption just
|
|
109
|
+
* reverses from wherever each column is. Returns true once every column is done.
|
|
110
|
+
*/
|
|
111
|
+
export declare function stepCoverage(cov: Coverage, direction: Direction, phaseTime: number, dt: number, duration: number, delays: ArrayLike<number>): boolean;
|
|
112
|
+
/** Snap every column to the end of `direction`. */
|
|
113
|
+
export declare function settleCoverage(cov: Coverage, direction: Direction): void;
|
|
114
|
+
/** Ease for a band edge: cubic in-out, exact at 0 and 1. */
|
|
115
|
+
export declare function easeEdge(a: number): number;
|
|
116
|
+
/**
|
|
117
|
+
* Shape of the covered band. `curtain` drops from the top and falls away,
|
|
118
|
+
* `rise` climbs from the bottom and lifts off, `split` opens from the centre
|
|
119
|
+
* line and squeezes shut onto it, `interlace` alternates curtain and rise
|
|
120
|
+
* column by column. Everything stays vertical.
|
|
121
|
+
*/
|
|
122
|
+
export type DatamoshEffect = "curtain" | "rise" | "split" | "interlace";
|
|
123
|
+
export interface Band {
|
|
124
|
+
/** 0..1 of the height. */
|
|
125
|
+
y0: number;
|
|
126
|
+
y1: number;
|
|
127
|
+
/** Draw the tile stack upside down (tiles rise instead of fall). */
|
|
128
|
+
mirror: boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Map a column's curtain-space band `[top, bot]` (already eased) onto the
|
|
132
|
+
* screen for `effect`. Curtain space is what `stepCoverage` animates; every
|
|
133
|
+
* effect is a pure re-mapping of it, so interruptions behave the same.
|
|
134
|
+
*/
|
|
135
|
+
export declare function columnBand(effect: DatamoshEffect, column: number, top: number, bot: number): Band;
|
|
136
|
+
/** Ready-made palettes. Each keeps one light anchor and a couple of near-blacks. */
|
|
137
|
+
export declare const DATAMOSH_PALETTES: {
|
|
138
|
+
readonly broadcast: readonly ["#ffffff", "#3566ff", "#ffcf00", "#192aff", "#d62036", "#282142", "#ec4978", "#ff9900", "#14101f", "#00dd33", "#00ef82"];
|
|
139
|
+
readonly thermal: readonly ["#fff4d6", "#ffd23f", "#ff9f1c", "#ff5a1f", "#e0282e", "#b0125b", "#6a0f63", "#2a0a3d", "#0d0614"];
|
|
140
|
+
readonly sunset: readonly ["#fff6d8", "#fbc56b", "#f59a3a", "#e97a1d", "#c85a14", "#8a4a22", "#5a3b25", "#2d1c16", "#140b08"];
|
|
141
|
+
readonly mono: readonly ["#ffffff", "#e5e5e5", "#b5b5b5", "#8a8a8a", "#5c5c5c", "#2e2e2e", "#141414", "#050505"];
|
|
142
|
+
readonly acid: readonly ["#f4ffe8", "#e4ff1a", "#00ff9c", "#00e0ff", "#3d5afe", "#8c3dff", "#ff2bd6", "#1b0b36", "#070707"];
|
|
143
|
+
};
|
|
144
|
+
export type DatamoshPaletteName = keyof typeof DATAMOSH_PALETTES;
|
|
145
|
+
/** Resolve a palette name or colour list; falls back to the default for anything unusable. */
|
|
146
|
+
export declare function resolvePalette(colors: readonly string[] | DatamoshPaletteName | undefined): string[];
|
|
147
|
+
/** Largest brightness gain the `boost` auto-levels may apply. */
|
|
148
|
+
export declare const MAX_GAIN = 1.6;
|
|
149
|
+
/** Share of the picture's height the slots sample from, centred. */
|
|
150
|
+
export declare const SOURCE_SPAN = 0.6;
|
|
151
|
+
/**
|
|
152
|
+
* Height (0..1) a slot samples the picture at: spread linearly over the middle
|
|
153
|
+
* `SOURCE_SPAN` of the picture. Not along the fall's sigmoid, which packs
|
|
154
|
+
* almost every slot against the edges, and not over the full height, where
|
|
155
|
+
* frames and borders (the darkest, dullest parts of most pictures) sit.
|
|
156
|
+
*/
|
|
157
|
+
export declare function sourceSlotY(slot: number, tiles: number): number;
|
|
158
|
+
/**
|
|
159
|
+
* Which rest slot a tile's colour is sampled from. Keyed by IDENTITY, like the
|
|
160
|
+
* colour strip: a tile keeps its piece of the picture as it falls, so the
|
|
161
|
+
* image gets dragged down in chunks instead of sliding under the grid.
|
|
162
|
+
*/
|
|
163
|
+
export declare function sourceSlot(id: number, column: number, tiles: number): number;
|
|
164
|
+
/** Source rect that fits an image over a box like `object-fit: cover`. */
|
|
165
|
+
export declare function coverFit(imgW: number, imgH: number, boxW: number, boxH: number): {
|
|
166
|
+
sx: number;
|
|
167
|
+
sy: number;
|
|
168
|
+
sw: number;
|
|
169
|
+
sh: number;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Colour table `[column][slot]` sampled from an RGBA buffer that already maps
|
|
173
|
+
* onto the overlay box. Each cell averages a 3×3 neighbourhood at the column's
|
|
174
|
+
* centre and the slot's height. `edges` are column edges normalised to 0..1.
|
|
175
|
+
*
|
|
176
|
+
* With `boost` (default), the table is auto-levelled (darkest cell → black,
|
|
177
|
+
* brightest → white) and saturated, so a moody picture still decodes into the
|
|
178
|
+
* flat, saturated blocks the effect is about.
|
|
179
|
+
*/
|
|
180
|
+
export declare function sampleSource(pixels: ArrayLike<number>, pw: number, ph: number, edges: readonly number[], tiles: number, boost?: boolean): string[][];
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DEFAULT_COLORS = [
|
|
3
|
+
"#ffffff",
|
|
4
|
+
"#3566ff",
|
|
5
|
+
"#ffcf00",
|
|
6
|
+
"#192aff",
|
|
7
|
+
"#d62036",
|
|
8
|
+
"#282142",
|
|
9
|
+
"#ec4978",
|
|
10
|
+
"#ff9900",
|
|
11
|
+
"#14101f",
|
|
12
|
+
"#00dd33",
|
|
13
|
+
"#00ef82"
|
|
14
|
+
];
|
|
15
|
+
const DEFAULT_COLUMNS = 11;
|
|
16
|
+
const DEFAULT_POWER = 1.65;
|
|
17
|
+
const DEFAULT_TILES = 15;
|
|
18
|
+
const CYCLE = 0.2;
|
|
19
|
+
const FALL_STAGGER = 6e-3;
|
|
20
|
+
const STRETCH = 8.5;
|
|
21
|
+
const BLEED = 0.012;
|
|
22
|
+
const COL_PHASE = -0.4;
|
|
23
|
+
const TAIL_SLOPE = 0.05;
|
|
24
|
+
const STRIP_LENGTH = 61;
|
|
25
|
+
function mulberry32(seed) {
|
|
26
|
+
let a = seed >>> 0;
|
|
27
|
+
return () => {
|
|
28
|
+
a = a + 1831565813 >>> 0;
|
|
29
|
+
let t = a;
|
|
30
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
31
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
32
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function luminance(color) {
|
|
36
|
+
const s = color.trim().toLowerCase();
|
|
37
|
+
let rgb = null;
|
|
38
|
+
const hex = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
|
|
39
|
+
if (hex) {
|
|
40
|
+
const h = hex[1];
|
|
41
|
+
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
42
|
+
rgb = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
43
|
+
} else {
|
|
44
|
+
const m = s.match(/^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
45
|
+
if (m) rgb = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
46
|
+
}
|
|
47
|
+
if (!rgb) return null;
|
|
48
|
+
const [r, g, b] = rgb.map((v) => {
|
|
49
|
+
const c = Math.min(255, v) / 255;
|
|
50
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
51
|
+
});
|
|
52
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
53
|
+
}
|
|
54
|
+
function paletteRoles(colors) {
|
|
55
|
+
const ranked = colors.map((c, i) => ({ i, l: luminance(c) ?? 0.5 })).sort((a, b) => a.l - b.l || a.i - b.i);
|
|
56
|
+
const light = ranked[ranked.length - 1].i;
|
|
57
|
+
const darkCount = colors.length >= 5 ? 2 : 1;
|
|
58
|
+
const darks = ranked.slice(0, darkCount).map((r) => r.i);
|
|
59
|
+
const hues = ranked.slice(darkCount, -1).map((r) => r.i);
|
|
60
|
+
return { light, darks, hues };
|
|
61
|
+
}
|
|
62
|
+
function pickDifferent(from, avoid, len) {
|
|
63
|
+
for (let i = 1; i <= len; i++) {
|
|
64
|
+
const c = (from + i) % len;
|
|
65
|
+
if (!avoid.includes(c)) return c;
|
|
66
|
+
}
|
|
67
|
+
return from;
|
|
68
|
+
}
|
|
69
|
+
function buildStrip(seed, colors, len = STRIP_LENGTH) {
|
|
70
|
+
const n = colors.length;
|
|
71
|
+
if (n < 2) return Array.from({ length: len }, () => 0);
|
|
72
|
+
const { light, darks, hues } = paletteRoles(colors);
|
|
73
|
+
const pool = hues.length ? hues : darks;
|
|
74
|
+
const rand = mulberry32(seed);
|
|
75
|
+
const strip = [];
|
|
76
|
+
let lastHue = -1;
|
|
77
|
+
for (let k = 0; k < len; k++) {
|
|
78
|
+
const r = rand();
|
|
79
|
+
let next;
|
|
80
|
+
if (r < 0.26) {
|
|
81
|
+
next = light;
|
|
82
|
+
} else if (r < 0.42) {
|
|
83
|
+
next = darks[Math.floor(rand() * darks.length)];
|
|
84
|
+
} else {
|
|
85
|
+
let h = pool[Math.floor(rand() * pool.length)];
|
|
86
|
+
if (h === lastHue && pool.length > 1) h = pool[(pool.indexOf(h) + 1) % pool.length];
|
|
87
|
+
lastHue = h;
|
|
88
|
+
next = h;
|
|
89
|
+
}
|
|
90
|
+
if (k > 0 && next === strip[k - 1]) next = pickDifferent(next, [strip[k - 1]], n);
|
|
91
|
+
strip.push(next);
|
|
92
|
+
}
|
|
93
|
+
if (len > 2 && strip[len - 1] === strip[0]) {
|
|
94
|
+
strip[len - 1] = pickDifferent(strip[len - 1], [strip[len - 2], strip[0]], n);
|
|
95
|
+
}
|
|
96
|
+
return strip;
|
|
97
|
+
}
|
|
98
|
+
function stripIndex(strip, id, column) {
|
|
99
|
+
const len = strip.length;
|
|
100
|
+
const s = id - column;
|
|
101
|
+
return strip[(s % len + len) % len];
|
|
102
|
+
}
|
|
103
|
+
function columnEdges(width, columns, power) {
|
|
104
|
+
const edges = [];
|
|
105
|
+
for (let i = 0; i <= columns; i++) {
|
|
106
|
+
edges.push(Math.round(width * Math.pow(i / columns, power)));
|
|
107
|
+
}
|
|
108
|
+
return edges;
|
|
109
|
+
}
|
|
110
|
+
function oddTiles(tiles) {
|
|
111
|
+
const t = Math.max(3, Math.round(Number.isFinite(tiles) ? tiles : DEFAULT_TILES));
|
|
112
|
+
return t % 2 === 0 ? t + 1 : t;
|
|
113
|
+
}
|
|
114
|
+
function tileEdge(k, tiles, stretch = STRETCH) {
|
|
115
|
+
const u = k / tiles;
|
|
116
|
+
if (u < 0) return u * TAIL_SLOPE;
|
|
117
|
+
if (u > 1) return 1 + (u - 1) * TAIL_SLOPE;
|
|
118
|
+
const a = Math.pow(u, stretch);
|
|
119
|
+
return a / (a + Math.pow(1 - u, stretch));
|
|
120
|
+
}
|
|
121
|
+
const SPRING_NORM = 1 - Math.exp(-1.6);
|
|
122
|
+
function springStep(p) {
|
|
123
|
+
const half = (t) => (1 - Math.exp(-1.6 * t)) / SPRING_NORM;
|
|
124
|
+
return p < 0.5 ? 0.5 * half(2 * p) : 1 - 0.5 * half(2 * (1 - p));
|
|
125
|
+
}
|
|
126
|
+
function columnFlow(elapsed, column, columns, speed = 1) {
|
|
127
|
+
const delay = (columns - 1 - column) * FALL_STAGGER;
|
|
128
|
+
const t = elapsed - delay;
|
|
129
|
+
const raw = t <= 0 ? 0 : t * speed / CYCLE;
|
|
130
|
+
const linear = raw + column * COL_PHASE;
|
|
131
|
+
const step = Math.floor(linear);
|
|
132
|
+
return step + springStep(linear - step);
|
|
133
|
+
}
|
|
134
|
+
function columnTiles(flow, tiles, height, bleed) {
|
|
135
|
+
const out = [];
|
|
136
|
+
const base = -Math.floor(flow);
|
|
137
|
+
for (let n = tiles + 2; n >= -2; n--) {
|
|
138
|
+
const id = base + n;
|
|
139
|
+
const k = id + flow;
|
|
140
|
+
const top = Math.round(tileEdge(k, tiles) * height);
|
|
141
|
+
const bot = Math.round(tileEdge(k + 1, tiles) * height) + bleed;
|
|
142
|
+
if (bot <= top) continue;
|
|
143
|
+
out.push({ id, top, bot });
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
function createCoverage(columns, covered = false) {
|
|
148
|
+
const top = new Float32Array(columns);
|
|
149
|
+
const bot = new Float32Array(columns);
|
|
150
|
+
if (covered) bot.fill(1);
|
|
151
|
+
return { top, bot };
|
|
152
|
+
}
|
|
153
|
+
function columnDelays(columns, sweep, stagger, seed = 1) {
|
|
154
|
+
const mid = (columns - 1) / 2;
|
|
155
|
+
const rank = (i) => {
|
|
156
|
+
switch (sweep) {
|
|
157
|
+
case "left":
|
|
158
|
+
return i;
|
|
159
|
+
case "center":
|
|
160
|
+
return Math.abs(i - mid);
|
|
161
|
+
case "edges":
|
|
162
|
+
return mid - Math.abs(i - mid);
|
|
163
|
+
case "random":
|
|
164
|
+
return 0;
|
|
165
|
+
default:
|
|
166
|
+
return columns - 1 - i;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (sweep === "random") {
|
|
170
|
+
const order = Array.from({ length: columns }, (_, i) => i);
|
|
171
|
+
const rand = mulberry32(seed ^ 24301);
|
|
172
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
173
|
+
const j = Math.floor(rand() * (i + 1));
|
|
174
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
175
|
+
}
|
|
176
|
+
const delays = new Array(columns);
|
|
177
|
+
order.forEach((col, k) => delays[col] = k * stagger);
|
|
178
|
+
return delays;
|
|
179
|
+
}
|
|
180
|
+
return Array.from({ length: columns }, (_, i) => rank(i) * stagger);
|
|
181
|
+
}
|
|
182
|
+
function stepCoverage(cov, direction, phaseTime, dt, duration, delays) {
|
|
183
|
+
const n = cov.top.length;
|
|
184
|
+
const rate = duration > 0 ? dt / duration : Infinity;
|
|
185
|
+
let done = true;
|
|
186
|
+
for (let i = 0; i < n; i++) {
|
|
187
|
+
const started = phaseTime >= (delays[i] ?? 0);
|
|
188
|
+
if (direction === "cover") {
|
|
189
|
+
if (cov.bot[i] - cov.top[i] <= 1e-6 && cov.bot[i] < 1) {
|
|
190
|
+
cov.top[i] = 0;
|
|
191
|
+
cov.bot[i] = 0;
|
|
192
|
+
}
|
|
193
|
+
if (started) {
|
|
194
|
+
cov.bot[i] = Math.min(1, cov.bot[i] + rate);
|
|
195
|
+
cov.top[i] = Math.max(0, cov.top[i] - rate);
|
|
196
|
+
}
|
|
197
|
+
if (cov.bot[i] < 1 || cov.top[i] > 0) done = false;
|
|
198
|
+
} else {
|
|
199
|
+
if (started) cov.top[i] = Math.min(cov.bot[i], cov.top[i] + rate);
|
|
200
|
+
if (cov.top[i] < cov.bot[i]) done = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return done;
|
|
204
|
+
}
|
|
205
|
+
function settleCoverage(cov, direction) {
|
|
206
|
+
if (direction === "cover") {
|
|
207
|
+
cov.top.fill(0);
|
|
208
|
+
cov.bot.fill(1);
|
|
209
|
+
} else {
|
|
210
|
+
cov.top.fill(0);
|
|
211
|
+
cov.bot.fill(0);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function easeEdge(a) {
|
|
215
|
+
const x = Math.min(1, Math.max(0, a));
|
|
216
|
+
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
|
217
|
+
}
|
|
218
|
+
function columnBand(effect, column, top, bot) {
|
|
219
|
+
const rise = effect === "rise" || effect === "interlace" && column % 2 === 1;
|
|
220
|
+
if (effect === "split") {
|
|
221
|
+
const half = Math.max(0, bot - top) / 2;
|
|
222
|
+
return { y0: 0.5 - half, y1: 0.5 + half, mirror: false };
|
|
223
|
+
}
|
|
224
|
+
if (rise) return { y0: 1 - bot, y1: 1 - top, mirror: true };
|
|
225
|
+
return { y0: top, y1: bot, mirror: false };
|
|
226
|
+
}
|
|
227
|
+
const DATAMOSH_PALETTES = {
|
|
228
|
+
broadcast: DEFAULT_COLORS,
|
|
229
|
+
thermal: [
|
|
230
|
+
"#fff4d6",
|
|
231
|
+
"#ffd23f",
|
|
232
|
+
"#ff9f1c",
|
|
233
|
+
"#ff5a1f",
|
|
234
|
+
"#e0282e",
|
|
235
|
+
"#b0125b",
|
|
236
|
+
"#6a0f63",
|
|
237
|
+
"#2a0a3d",
|
|
238
|
+
"#0d0614"
|
|
239
|
+
],
|
|
240
|
+
sunset: [
|
|
241
|
+
"#fff6d8",
|
|
242
|
+
"#fbc56b",
|
|
243
|
+
"#f59a3a",
|
|
244
|
+
"#e97a1d",
|
|
245
|
+
"#c85a14",
|
|
246
|
+
"#8a4a22",
|
|
247
|
+
"#5a3b25",
|
|
248
|
+
"#2d1c16",
|
|
249
|
+
"#140b08"
|
|
250
|
+
],
|
|
251
|
+
mono: ["#ffffff", "#e5e5e5", "#b5b5b5", "#8a8a8a", "#5c5c5c", "#2e2e2e", "#141414", "#050505"],
|
|
252
|
+
acid: [
|
|
253
|
+
"#f4ffe8",
|
|
254
|
+
"#e4ff1a",
|
|
255
|
+
"#00ff9c",
|
|
256
|
+
"#00e0ff",
|
|
257
|
+
"#3d5afe",
|
|
258
|
+
"#8c3dff",
|
|
259
|
+
"#ff2bd6",
|
|
260
|
+
"#1b0b36",
|
|
261
|
+
"#070707"
|
|
262
|
+
]
|
|
263
|
+
};
|
|
264
|
+
function resolvePalette(colors) {
|
|
265
|
+
if (typeof colors === "string") return [...DATAMOSH_PALETTES[colors] ?? DEFAULT_COLORS];
|
|
266
|
+
return colors && colors.length >= 2 ? [...colors] : [...DEFAULT_COLORS];
|
|
267
|
+
}
|
|
268
|
+
const MAX_GAIN = 1.6;
|
|
269
|
+
const SOURCE_SPAN = 0.6;
|
|
270
|
+
function sourceSlotY(slot, tiles) {
|
|
271
|
+
return 0.5 + ((slot + 0.5) / tiles - 0.5) * SOURCE_SPAN;
|
|
272
|
+
}
|
|
273
|
+
function sourceSlot(id, column, tiles) {
|
|
274
|
+
const s = id - column;
|
|
275
|
+
return (s % tiles + tiles) % tiles;
|
|
276
|
+
}
|
|
277
|
+
function coverFit(imgW, imgH, boxW, boxH) {
|
|
278
|
+
if (imgW <= 0 || imgH <= 0 || boxW <= 0 || boxH <= 0) return { sx: 0, sy: 0, sw: imgW, sh: imgH };
|
|
279
|
+
const scale = Math.max(boxW / imgW, boxH / imgH);
|
|
280
|
+
const sw = boxW / scale;
|
|
281
|
+
const sh = boxH / scale;
|
|
282
|
+
return { sx: (imgW - sw) / 2, sy: (imgH - sh) / 2, sw, sh };
|
|
283
|
+
}
|
|
284
|
+
function sampleSource(pixels, pw, ph, edges, tiles, boost = true) {
|
|
285
|
+
const at = (x, y) => {
|
|
286
|
+
const cx = Math.min(pw - 1, Math.max(0, x));
|
|
287
|
+
const cy = Math.min(ph - 1, Math.max(0, y));
|
|
288
|
+
return (cy * pw + cx) * 4;
|
|
289
|
+
};
|
|
290
|
+
const cells = [];
|
|
291
|
+
let lo = Infinity;
|
|
292
|
+
let hi = -Infinity;
|
|
293
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
294
|
+
const x = Math.round((edges[i] + edges[i + 1]) / 2 * (pw - 1));
|
|
295
|
+
const row = [];
|
|
296
|
+
for (let s = 0; s < tiles; s++) {
|
|
297
|
+
const y = Math.round(sourceSlotY(s, tiles) * (ph - 1));
|
|
298
|
+
let r = 0;
|
|
299
|
+
let g = 0;
|
|
300
|
+
let b = 0;
|
|
301
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
302
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
303
|
+
const k = at(x + dx, y + dy);
|
|
304
|
+
r += pixels[k];
|
|
305
|
+
g += pixels[k + 1];
|
|
306
|
+
b += pixels[k + 2];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const cell = [r / 9, g / 9, b / 9];
|
|
310
|
+
const l = 0.2126 * cell[0] + 0.7152 * cell[1] + 0.0722 * cell[2];
|
|
311
|
+
lo = Math.min(lo, l);
|
|
312
|
+
hi = Math.max(hi, l);
|
|
313
|
+
row.push(cell);
|
|
314
|
+
}
|
|
315
|
+
cells.push(row);
|
|
316
|
+
}
|
|
317
|
+
const span = hi - lo;
|
|
318
|
+
const level = boost && span > 8 ? Math.min(MAX_GAIN, 255 / (hi - lo * 0.5)) : 1;
|
|
319
|
+
const offset = boost && span > 8 ? lo * 0.5 : 0;
|
|
320
|
+
const saturation = boost ? 1.35 : 1;
|
|
321
|
+
const clamp = (v) => Math.round(Math.min(255, Math.max(0, v)));
|
|
322
|
+
return cells.map(
|
|
323
|
+
(row) => row.map(([r, g, b]) => {
|
|
324
|
+
let [nr, ng, nb] = [(r - offset) * level, (g - offset) * level, (b - offset) * level];
|
|
325
|
+
const l = 0.2126 * nr + 0.7152 * ng + 0.0722 * nb;
|
|
326
|
+
nr = l + (nr - l) * saturation;
|
|
327
|
+
ng = l + (ng - l) * saturation;
|
|
328
|
+
nb = l + (nb - l) * saturation;
|
|
329
|
+
return `rgb(${clamp(nr)}, ${clamp(ng)}, ${clamp(nb)})`;
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
BLEED,
|
|
335
|
+
COL_PHASE,
|
|
336
|
+
CYCLE,
|
|
337
|
+
DATAMOSH_PALETTES,
|
|
338
|
+
DEFAULT_COLORS,
|
|
339
|
+
DEFAULT_COLUMNS,
|
|
340
|
+
DEFAULT_POWER,
|
|
341
|
+
DEFAULT_TILES,
|
|
342
|
+
FALL_STAGGER,
|
|
343
|
+
MAX_GAIN,
|
|
344
|
+
SOURCE_SPAN,
|
|
345
|
+
STRETCH,
|
|
346
|
+
STRIP_LENGTH,
|
|
347
|
+
TAIL_SLOPE,
|
|
348
|
+
buildStrip,
|
|
349
|
+
columnBand,
|
|
350
|
+
columnDelays,
|
|
351
|
+
columnEdges,
|
|
352
|
+
columnFlow,
|
|
353
|
+
columnTiles,
|
|
354
|
+
coverFit,
|
|
355
|
+
createCoverage,
|
|
356
|
+
easeEdge,
|
|
357
|
+
luminance,
|
|
358
|
+
mulberry32,
|
|
359
|
+
oddTiles,
|
|
360
|
+
paletteRoles,
|
|
361
|
+
resolvePalette,
|
|
362
|
+
sampleSource,
|
|
363
|
+
settleCoverage,
|
|
364
|
+
sourceSlot,
|
|
365
|
+
sourceSlotY,
|
|
366
|
+
springStep,
|
|
367
|
+
stepCoverage,
|
|
368
|
+
stripIndex,
|
|
369
|
+
tileEdge
|
|
370
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { DatamoshTransition } from "./DatamoshTransition.js";
|
|
2
|
+
export type { DatamoshTransitionProps, DatamoshTransitionPhase, DatamoshTransitionHandle, } from "./DatamoshTransition.js";
|
|
3
|
+
export { DATAMOSH_PALETTES, type DatamoshEffect, type DatamoshSweep, type DatamoshPaletteName, } from "./datamosh-transition-core.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export * from "./components/marquee/index.js";
|
|
|
85
85
|
export * from "./components/matrix-rain/index.js";
|
|
86
86
|
export * from "./components/meteors/index.js";
|
|
87
87
|
export * from "./components/mosaic-glow/index.js";
|
|
88
|
+
export * from "./components/datamosh-transition/index.js";
|
|
88
89
|
export * from "./components/navbar/index.js";
|
|
89
90
|
export * from "./components/navigation-menu/index.js";
|
|
90
91
|
export * from "./components/neon-border/index.js";
|
package/dist/index.js
CHANGED
|
@@ -134,6 +134,8 @@ import { ReviewCard } from "./components/marquee/ReviewCard.js";
|
|
|
134
134
|
import { MatrixRain } from "./components/matrix-rain/MatrixRain.js";
|
|
135
135
|
import { Meteors } from "./components/meteors/Meteors.js";
|
|
136
136
|
import { MosaicGlow } from "./components/mosaic-glow/MosaicGlow.js";
|
|
137
|
+
import { DatamoshTransition } from "./components/datamosh-transition/DatamoshTransition.js";
|
|
138
|
+
import { DATAMOSH_PALETTES } from "./components/datamosh-transition/datamosh-transition-core.js";
|
|
137
139
|
import { Navbar } from "./components/navbar/Navbar.js";
|
|
138
140
|
import { NavbarLink } from "./components/navbar/NavbarLink.js";
|
|
139
141
|
import { NavigationMenu } from "./components/navigation-menu/NavigationMenu.js";
|
|
@@ -302,9 +304,11 @@ export {
|
|
|
302
304
|
ContextMenuTrigger,
|
|
303
305
|
ContextRing,
|
|
304
306
|
CopyButton,
|
|
307
|
+
DATAMOSH_PALETTES,
|
|
305
308
|
DEFAULT_SOUND_FEEDBACK_ON,
|
|
306
309
|
DEFAULT_SOUND_PREFERENCES,
|
|
307
310
|
DROPDOWN_MENU_KEY,
|
|
311
|
+
DatamoshTransition,
|
|
308
312
|
DatePicker,
|
|
309
313
|
Dialog,
|
|
310
314
|
DimSiblings,
|
package/dist/styles.css
CHANGED
|
@@ -313,7 +313,81 @@
|
|
|
313
313
|
* scales the panel about its own centre without touching the centring.
|
|
314
314
|
*/
|
|
315
315
|
.ft-btn {
|
|
316
|
-
--ft-btn-accent: var(
|
|
316
|
+
--ft-btn-accent: var(
|
|
317
|
+
--ft-accent,
|
|
318
|
+
light-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))
|
|
319
|
+
);
|
|
320
|
+
/* 150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) =
|
|
321
|
+
tokens.EASINGS.inout — the reversible-state pair. It runs the colour
|
|
322
|
+
channel, and the release half of the press: letting go settles, it does
|
|
323
|
+
not snap back. */
|
|
324
|
+
--ft-btn-motion: var(--ft-duration-fast, 150ms) var(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));
|
|
325
|
+
/* The down-stroke only. 150ms = tokens.DURATIONS.fast,
|
|
326
|
+
cubic-bezier(0.16, 1, 0.3, 1) = tokens.EASINGS.out — the arrival curve,
|
|
327
|
+
which spends most of the scale in the first frames, so the press bites
|
|
328
|
+
instead of easing in. Pressing is an arrival; releasing is what resolves
|
|
329
|
+
either way. */
|
|
330
|
+
--ft-btn-press: var(--ft-duration-fast, 150ms) var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
|
|
331
|
+
/*
|
|
332
|
+
* Replaces the `transition-colors` utility removed from the class string,
|
|
333
|
+
* at exactly the values that utility already resolved to. Colour is a state
|
|
334
|
+
* change, not motion, so it stays OUTSIDE the reduced-motion query — gating
|
|
335
|
+
* it would only make a theme flip look broken for the people who asked for
|
|
336
|
+
* less movement.
|
|
337
|
+
*
|
|
338
|
+
* `box-shadow` is deliberately absent, and must stay absent: this button's
|
|
339
|
+
* focus ring is `focus-visible:ring-*`, which compiles to a `box-shadow`,
|
|
340
|
+
* and a focus ring must never animate.
|
|
341
|
+
*/
|
|
342
|
+
transition:
|
|
343
|
+
color var(--ft-btn-motion),
|
|
344
|
+
background-color var(--ft-btn-motion),
|
|
345
|
+
border-color var(--ft-btn-motion);
|
|
346
|
+
/* Kills the ~300ms tap delay without blocking scroll. A press that answers
|
|
347
|
+
a third of a second late is not press feedback. */
|
|
348
|
+
touch-action: manipulation;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/*
|
|
352
|
+
* The press. Pressable's contract inlined as a plain rule on the native
|
|
353
|
+
* control rather than wrapping a `<button>` in a `<Pressable>` div — same
|
|
354
|
+
* `0.97`, same clock, one element instead of two.
|
|
355
|
+
*
|
|
356
|
+
* Only the property list is re-declared in here: `transform` joins the colour
|
|
357
|
+
* channel under `no-preference` and nowhere else, so with motion reduced the
|
|
358
|
+
* colours still cross and the button simply does not move. The resting state
|
|
359
|
+
* (no `transform` at all) is the ungated fallback.
|
|
360
|
+
*
|
|
361
|
+
* The two strokes are timed apart. The rule below carries the release curve,
|
|
362
|
+
* because that is the transition the button runs on its way back to rest;
|
|
363
|
+
* `:active` overrides `transform` with the arrival curve for the way down.
|
|
364
|
+
*/
|
|
365
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
366
|
+
.ft-btn {
|
|
367
|
+
transition:
|
|
368
|
+
color var(--ft-btn-motion),
|
|
369
|
+
background-color var(--ft-btn-motion),
|
|
370
|
+
border-color var(--ft-btn-motion),
|
|
371
|
+
transform var(--ft-btn-motion);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* Both inert states are excluded so a press that does nothing does not
|
|
375
|
+
pretend to. `[data-disabled]` is the anchor branch, which has no native
|
|
376
|
+
`:disabled` to hang this off; `[aria-busy]` is the loading one, on both
|
|
377
|
+
branches. A native disabled `<button>` never matches `:active` anyway —
|
|
378
|
+
the attribute selectors are what make the anchor behave like it. */
|
|
379
|
+
.ft-btn:not([data-disabled="true"]):not([aria-busy="true"]):active {
|
|
380
|
+
transform: scale(0.97);
|
|
381
|
+
/* The whole list is re-declared rather than just
|
|
382
|
+
`transition-timing-function`, which has no per-property form here: a
|
|
383
|
+
single value would retime the colour channel too. Only `transform`
|
|
384
|
+
differs from the rule above. */
|
|
385
|
+
transition:
|
|
386
|
+
color var(--ft-btn-motion),
|
|
387
|
+
background-color var(--ft-btn-motion),
|
|
388
|
+
border-color var(--ft-btn-motion),
|
|
389
|
+
transform var(--ft-btn-press);
|
|
390
|
+
}
|
|
317
391
|
}
|
|
318
392
|
|
|
319
393
|
.ft-btn--accent {
|
package/package.json
CHANGED