Add a drag-and-resize animation
A drawn pointer grabs the frame near its top edge, drags it across the screen, lets go, then takes hold of one corner and pulls the frame to a new size. Moves ease in and overshoot slightly before settling, and the paths bow, so the motion reads as a hand rather than a tween. The resize pins the opposite corner and keeps the aspect ratio locked — a stretched video would read as broken, not as someone resizing — and clamps the width to the room left before the corner leaves the screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
This commit is contained in:
@@ -33,18 +33,24 @@ autoplay, so the first click is what unlocks audio. One click per session.
|
|||||||
whenever a length is still unknown.
|
whenever a length is still unknown.
|
||||||
- The queue is saved to `queue.json` on every change and restored at startup.
|
- The queue is saved to `queue.json` on every change and restored at startup.
|
||||||
- Settings live in `config.js`: overlap length, frame size, drift speed, corner
|
- Settings live in `config.js`: overlap length, frame size, drift speed, corner
|
||||||
screenshot interval and margin, port, and the selectable backgrounds and
|
screenshot interval and margin, the drag animation's size range, port, and
|
||||||
animations. Anything the browser needs must also be mirrored in the
|
the selectable backgrounds and animations. Anything the browser needs must
|
||||||
`/config.js` route in `server/index.js`.
|
also be mirrored in the `/config.js` route in `server/index.js`.
|
||||||
|
|
||||||
## Animations
|
## Animations
|
||||||
|
|
||||||
The video frame's movement changes with every track. Two animations ship today:
|
The video frame's movement changes with every track. Three animations ship
|
||||||
|
today:
|
||||||
|
|
||||||
- **bounce** — diagonal drift, reflecting off the screen edges.
|
- **bounce** — diagonal drift, reflecting off the screen edges.
|
||||||
- **corners** — the frame parks in a random corner and every 5 seconds a
|
- **corners** — the frame parks in a random corner and every 5 seconds a
|
||||||
screenshot of it appears in the next of the three other corners, going
|
screenshot of it appears in the next of the three other corners, going
|
||||||
clockwise. The frame never moves, so a screenshot can never cover it.
|
clockwise. The frame never moves, so a screenshot can never cover it.
|
||||||
|
- **drag** — a drawn mouse pointer travels to the frame, grabs it near its top
|
||||||
|
edge and drags it across the screen, lets go for a beat, then takes hold of
|
||||||
|
one corner and pulls the frame to a new size. The aspect ratio stays locked
|
||||||
|
and the frame stays on screen, so the resize reads as a window resize rather
|
||||||
|
than a stretch.
|
||||||
|
|
||||||
The queue page's **Animation** buttons pin the player to one of them, which is
|
The queue page's **Animation** buttons pin the player to one of them, which is
|
||||||
how you look at a single animation without waiting for the cycle to reach it.
|
how you look at a single animation without waiting for the cycle to reach it.
|
||||||
|
|||||||
@@ -23,11 +23,18 @@ export const CORNER_INTERVAL_SECONDS = 5;
|
|||||||
// Gap between the frame and the screen edge, as a fraction of the short side.
|
// Gap between the frame and the screen edge, as a fraction of the short side.
|
||||||
export const CORNER_MARGIN = 0.025;
|
export const CORNER_MARGIN = 0.025;
|
||||||
|
|
||||||
|
// The drag animation resizes the frame to somewhere between these multiples of
|
||||||
|
// the normal frame size. Above 1 the frame grows past FRAME_SCALE, which is
|
||||||
|
// fine — nothing else is on screen while this animation runs.
|
||||||
|
export const DRAG_MIN_SCALE = 0.55;
|
||||||
|
export const DRAG_MAX_SCALE = 1.6;
|
||||||
|
|
||||||
// The frame animations, keyed by the id sent over the socket. The names must
|
// The frame animations, keyed by the id sent over the socket. The names must
|
||||||
// match the registry in public/player/motion.js, which supplies the code.
|
// match the registry in public/player/motion.js, which supplies the code.
|
||||||
export const ANIMATIONS = {
|
export const ANIMATIONS = {
|
||||||
bounce: { label: 'Bounce' },
|
bounce: { label: 'Bounce' },
|
||||||
corners: { label: 'Corners' },
|
corners: { label: 'Corners' },
|
||||||
|
drag: { label: 'Drag & resize' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cycle a fresh random animation on every track change. Anything else pins the
|
// Cycle a fresh random animation on every track change. Anything else pins the
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
BOUNCE_SPEED,
|
BOUNCE_SPEED,
|
||||||
CORNER_INTERVAL_SECONDS,
|
CORNER_INTERVAL_SECONDS,
|
||||||
CORNER_MARGIN,
|
CORNER_MARGIN,
|
||||||
|
DRAG_MIN_SCALE,
|
||||||
|
DRAG_MAX_SCALE,
|
||||||
AUTO_ANIMATION,
|
AUTO_ANIMATION,
|
||||||
} from '/config.js';
|
} from '/config.js';
|
||||||
|
|
||||||
@@ -296,12 +298,394 @@ export function startCorners(context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- drag and resize ----------------------------------------------------
|
||||||
|
|
||||||
|
// Someone shoving a video window around a desktop: a pointer travels to the
|
||||||
|
// frame, grabs it, drags it somewhere else, lets go for a moment, then takes
|
||||||
|
// hold of one corner and pulls the frame to a new size. The pointer is drawn,
|
||||||
|
// so every move on screen has a visible cause.
|
||||||
|
|
||||||
|
const ARROW = `<svg viewBox="0 0 24 24" width="27" height="27" aria-hidden="true">
|
||||||
|
<path d="M5 2.5 L5 19.6 L9.5 15.4 L12.3 21.5 L15.2 20.1 L12.4 14.2 L18.6 14 Z"
|
||||||
|
fill="#fff" stroke="#14060f" stroke-width="1.3" stroke-linejoin="round" />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
// Where on the frame the pointer takes hold, as a fraction of its height —
|
||||||
|
// the top strip, where a window's title bar would be.
|
||||||
|
const GRAB_BAND = [0.06, 0.3];
|
||||||
|
|
||||||
|
// Seconds the pointer rests between grabs, and how far it drifts while resting.
|
||||||
|
const PAUSE = [0.5, 1.7];
|
||||||
|
const IDLE_DRIFT = 2.5;
|
||||||
|
|
||||||
|
function clamp(value, min, max) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function between(min, max) {
|
||||||
|
return min + Math.random() * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Smoothstep: no jerk at either end, which is what an arm does. */
|
||||||
|
function smooth(t) {
|
||||||
|
return t * t * (3 - 2 * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress along a hand-driven move: a gentle launch, then a slight overshoot
|
||||||
|
* that pulls back onto the target. `overshoot` 0 is a plain smooth ramp; the
|
||||||
|
* bigger it gets, the further past the target the move swings first.
|
||||||
|
*/
|
||||||
|
export function settle(t, overshoot = 0.6) {
|
||||||
|
const back = smooth(clamp(t, 0, 1)) - 1;
|
||||||
|
return 1 + (overshoot + 1) * back ** 3 + overshoot * back ** 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pointer position at `t` (0..1) on its way from `from` to `to`. `curve` bows
|
||||||
|
* the path sideways — a hand does not move in a straight line — and its sign
|
||||||
|
* decides which way the arc leans.
|
||||||
|
*/
|
||||||
|
export function pointerAt(from, to, t, { curve = 0, overshoot = 0.6 } = {}) {
|
||||||
|
const dx = to.x - from.x;
|
||||||
|
const dy = to.y - from.y;
|
||||||
|
const p = settle(t, overshoot);
|
||||||
|
const bow = Math.sin(Math.PI * clamp(t, 0, 1)) * curve;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: from.x + dx * p - dy * bow,
|
||||||
|
y: from.y + dy * p + dx * bow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rect you get by dragging `corner` (0 = top left, clockwise) to `pointer`
|
||||||
|
* while the opposite corner stays pinned, the way a window resize works.
|
||||||
|
*
|
||||||
|
* The aspect ratio is locked — a squashed video reads as broken rather than as
|
||||||
|
* someone resizing — so the axis the pointer pulled furthest wins, and the
|
||||||
|
* width is clamped into `min`..`max`.
|
||||||
|
*/
|
||||||
|
export function resizeRect(rect, corner, pointer, { min, max }) {
|
||||||
|
const anchor = anchorOf(rect, corner);
|
||||||
|
const pulled = Math.max(
|
||||||
|
Math.abs(pointer.x - anchor.x),
|
||||||
|
Math.abs(pointer.y - anchor.y) / ASPECT,
|
||||||
|
);
|
||||||
|
|
||||||
|
return rectFromWidth(rect, corner, clamp(pulled, min, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The corner that stays put while `corner` is dragged. */
|
||||||
|
function anchorOf(rect, corner) {
|
||||||
|
const right = corner === 1 || corner === 2;
|
||||||
|
const bottom = corner === 2 || corner === 3;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: right ? rect.x : rect.x + rect.width,
|
||||||
|
y: bottom ? rect.y : rect.y + rect.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Grow `rect` to `width` about its anchor, keeping the aspect ratio. */
|
||||||
|
function rectFromWidth(rect, corner, width) {
|
||||||
|
const anchor = anchorOf(rect, corner);
|
||||||
|
const right = corner === 1 || corner === 2;
|
||||||
|
const bottom = corner === 2 || corner === 3;
|
||||||
|
const height = width * ASPECT;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: right ? anchor.x : anchor.x - width,
|
||||||
|
y: bottom ? anchor.y : anchor.y - height,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Screen position of one corner of a rect, same indexing as resizeRect. */
|
||||||
|
function cornerOf(rect, corner) {
|
||||||
|
const right = corner === 1 || corner === 2;
|
||||||
|
const bottom = corner === 2 || corner === 3;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: right ? rect.x + rect.width : rect.x,
|
||||||
|
y: bottom ? rect.y + rect.height : rect.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(a, b) {
|
||||||
|
return Math.hypot(b.x - a.x, b.y - a.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Longer trips take longer, but not proportionally — Fitts, roughly. */
|
||||||
|
function reachTime(px) {
|
||||||
|
return clamp(0.28 + px / 1500, 0.28, 1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dragging is slower than moving an empty hand, and much slower than a flick. */
|
||||||
|
function dragTime(px) {
|
||||||
|
return clamp(0.55 + px / 700, 0.55, 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startDragResize({ element }) {
|
||||||
|
const pointer = document.createElement('div');
|
||||||
|
pointer.id = 'pointer';
|
||||||
|
pointer.innerHTML = ARROW;
|
||||||
|
document.body.appendChild(pointer);
|
||||||
|
|
||||||
|
let base = frameSize(window.innerWidth, window.innerHeight);
|
||||||
|
let rect = {
|
||||||
|
x: (window.innerWidth - base.width) / 2,
|
||||||
|
y: (window.innerHeight - base.height) / 2,
|
||||||
|
width: base.width,
|
||||||
|
height: base.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The pointer starts off screen and slides in for its first grab.
|
||||||
|
let at = { x: -40, y: window.innerHeight * 0.75 };
|
||||||
|
|
||||||
|
function margin() {
|
||||||
|
return Math.min(window.innerWidth, window.innerHeight) * CORNER_MARGIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Widths the frame may take: never wider than the screen allows. */
|
||||||
|
function widthLimits() {
|
||||||
|
const fit = Math.min(
|
||||||
|
window.innerWidth - 2 * margin(),
|
||||||
|
(window.innerHeight - 2 * margin()) / ASPECT,
|
||||||
|
);
|
||||||
|
const min = Math.min(base.width * DRAG_MIN_SCALE, fit);
|
||||||
|
return { min, max: clamp(base.width * DRAG_MAX_SCALE, min, fit) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
element.style.width = `${rect.width.toFixed(1)}px`;
|
||||||
|
element.style.height = `${rect.height.toFixed(1)}px`;
|
||||||
|
place(element, rect.x, rect.y);
|
||||||
|
place(pointer, at.x, at.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- the script ---
|
||||||
|
//
|
||||||
|
// One cycle is a fixed sequence of beats; each beat gets its endpoints when
|
||||||
|
// it starts, so a viewport resize part-way through never strands the pointer
|
||||||
|
// at coordinates that no longer exist.
|
||||||
|
|
||||||
|
let steps = [];
|
||||||
|
let index = 0;
|
||||||
|
let elapsed = 0;
|
||||||
|
|
||||||
|
// Filled in by the press beats and read by the drag beats that follow.
|
||||||
|
let held = { x: 0, y: 0 };
|
||||||
|
let corner = 0;
|
||||||
|
let anchored = rect;
|
||||||
|
|
||||||
|
function travel(to, options) {
|
||||||
|
const from = at;
|
||||||
|
const target = to();
|
||||||
|
return {
|
||||||
|
duration: options.time(distance(from, target)),
|
||||||
|
update(p) {
|
||||||
|
at = pointerAt(from, target, p, options);
|
||||||
|
options.moved?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Grab: the pointer lands on the frame's title-bar strip. */
|
||||||
|
function reachFrame() {
|
||||||
|
return travel(
|
||||||
|
() => ({
|
||||||
|
x: rect.x + between(0.2, 0.8) * rect.width,
|
||||||
|
y: rect.y + between(GRAB_BAND[0], GRAB_BAND[1]) * rect.height,
|
||||||
|
}),
|
||||||
|
// An empty hand shoots past the target and pulls back onto it.
|
||||||
|
{ time: reachTime, curve: between(-0.07, 0.07), overshoot: 1.6 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reachCorner() {
|
||||||
|
corner = Math.floor(Math.random() * CORNERS);
|
||||||
|
return travel(() => cornerOf(rect, corner), {
|
||||||
|
time: reachTime,
|
||||||
|
curve: between(-0.05, 0.05),
|
||||||
|
overshoot: 1.4,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function press() {
|
||||||
|
return {
|
||||||
|
duration: between(0.1, 0.22),
|
||||||
|
begin() {
|
||||||
|
pointer.classList.add('down');
|
||||||
|
element.classList.add('dragging');
|
||||||
|
held = { x: at.x - rect.x, y: at.y - rect.y };
|
||||||
|
anchored = { ...rect };
|
||||||
|
},
|
||||||
|
update() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function release() {
|
||||||
|
const from = at;
|
||||||
|
return {
|
||||||
|
duration: between(PAUSE[0], PAUSE[1]),
|
||||||
|
begin() {
|
||||||
|
pointer.classList.remove('down');
|
||||||
|
element.classList.remove('dragging');
|
||||||
|
},
|
||||||
|
// A resting hand is never quite still.
|
||||||
|
update(p) {
|
||||||
|
at = {
|
||||||
|
x: from.x + Math.sin(p * 7) * IDLE_DRIFT,
|
||||||
|
y: from.y + Math.sin(p * 5 + 1) * IDLE_DRIFT * 0.7,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drag: the frame hangs off the pointer at the offset it was grabbed by. */
|
||||||
|
function dragFrame() {
|
||||||
|
const gap = margin();
|
||||||
|
return travel(
|
||||||
|
() => {
|
||||||
|
const limitX = Math.max(gap, window.innerWidth - rect.width - gap);
|
||||||
|
const limitY = Math.max(gap, window.innerHeight - rect.height - gap);
|
||||||
|
return {
|
||||||
|
x: between(gap, limitX) + held.x,
|
||||||
|
y: between(gap, limitY) + held.y,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: dragTime,
|
||||||
|
curve: between(-0.04, 0.04),
|
||||||
|
// A loaded hand overshoots less than an empty one.
|
||||||
|
overshoot: 0.8,
|
||||||
|
moved() {
|
||||||
|
rect.x = at.x - held.x;
|
||||||
|
rect.y = at.y - held.y;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resize: the held corner follows the pointer, the opposite one stays put. */
|
||||||
|
function dragCorner() {
|
||||||
|
const limits = widthLimits();
|
||||||
|
const gap = margin();
|
||||||
|
|
||||||
|
return travel(
|
||||||
|
() => {
|
||||||
|
// How far the corner can travel before it leaves the screen — the same
|
||||||
|
// wall a real hand would run the window into. The anchor never moves,
|
||||||
|
// so the room is measured from there.
|
||||||
|
const anchor = anchorOf(anchored, corner);
|
||||||
|
const right = corner === 1 || corner === 2;
|
||||||
|
const bottom = corner === 2 || corner === 3;
|
||||||
|
const room = Math.min(
|
||||||
|
right ? window.innerWidth - gap - anchor.x : anchor.x - gap,
|
||||||
|
(bottom ? window.innerHeight - gap - anchor.y : anchor.y - gap) / ASPECT,
|
||||||
|
);
|
||||||
|
|
||||||
|
const width = clamp(
|
||||||
|
between(limits.min, limits.max),
|
||||||
|
limits.min,
|
||||||
|
Math.max(limits.min, room),
|
||||||
|
);
|
||||||
|
|
||||||
|
return cornerOf(rectFromWidth(anchored, corner, width), corner);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: dragTime,
|
||||||
|
curve: between(-0.03, 0.03),
|
||||||
|
// Least of all when pulling against a corner: the size lands and stays.
|
||||||
|
overshoot: 0.5,
|
||||||
|
moved() {
|
||||||
|
rect = resizeRect(anchored, corner, at, limits);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beats are built lazily: each one reads the frame where the previous one
|
||||||
|
// left it.
|
||||||
|
const CYCLE = [reachFrame, press, dragFrame, release, reachCorner, press, dragCorner, release];
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
const build = CYCLE[index % CYCLE.length];
|
||||||
|
index += 1;
|
||||||
|
const current = build();
|
||||||
|
current.begin?.();
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = step();
|
||||||
|
|
||||||
|
function measure() {
|
||||||
|
base = frameSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
|
const gap = margin();
|
||||||
|
const limits = widthLimits();
|
||||||
|
rect.width = clamp(rect.width, limits.min, limits.max);
|
||||||
|
rect.height = rect.width * ASPECT;
|
||||||
|
rect.x = clamp(rect.x, gap, Math.max(gap, window.innerWidth - rect.width - gap));
|
||||||
|
rect.y = clamp(rect.y, gap, Math.max(gap, window.innerHeight - rect.height - gap));
|
||||||
|
|
||||||
|
// Whatever the pointer was aiming at is stale now; start the next beat
|
||||||
|
// from where things actually are.
|
||||||
|
pointer.classList.remove('down');
|
||||||
|
element.classList.remove('dragging');
|
||||||
|
elapsed = 0;
|
||||||
|
current = step();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', measure);
|
||||||
|
|
||||||
|
let last = performance.now();
|
||||||
|
let handle = 0;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
function frame(now) {
|
||||||
|
if (stopped) return;
|
||||||
|
|
||||||
|
const dt = Math.min((now - last) / 1000, 0.05);
|
||||||
|
last = now;
|
||||||
|
elapsed += dt;
|
||||||
|
|
||||||
|
const p = current.duration > 0 ? Math.min(elapsed / current.duration, 1) : 1;
|
||||||
|
current.update(p);
|
||||||
|
draw();
|
||||||
|
|
||||||
|
if (p >= 1) {
|
||||||
|
elapsed = 0;
|
||||||
|
current = step();
|
||||||
|
}
|
||||||
|
|
||||||
|
handle = requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw();
|
||||||
|
handle = requestAnimationFrame(frame);
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
stopped = true;
|
||||||
|
cancelAnimationFrame(handle);
|
||||||
|
window.removeEventListener('resize', measure);
|
||||||
|
element.classList.remove('dragging');
|
||||||
|
pointer.remove();
|
||||||
|
// The next animation sizes the frame itself, but leaving a stretched frame
|
||||||
|
// up for even one paint would flash.
|
||||||
|
applyFrameSize(element);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --- registry -----------------------------------------------------------
|
// --- registry -----------------------------------------------------------
|
||||||
|
|
||||||
// Keyed by the names in config.js, which is what travels over the socket.
|
// Keyed by the names in config.js, which is what travels over the socket.
|
||||||
export const ANIMATIONS = {
|
export const ANIMATIONS = {
|
||||||
bounce: startBounce,
|
bounce: startBounce,
|
||||||
corners: startCorners,
|
corners: startCorners,
|
||||||
|
drag: startDragResize,
|
||||||
};
|
};
|
||||||
|
|
||||||
const NAMES = Object.keys(ANIMATIONS);
|
const NAMES = Object.keys(ANIMATIONS);
|
||||||
|
|||||||
@@ -94,6 +94,37 @@ body {
|
|||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lift the frame while the drag animation is holding it, the way a dragged
|
||||||
|
window separates from the desktop. */
|
||||||
|
#frameMover.dragging #frameSkin {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 4px rgba(255, 79, 195, 0.75),
|
||||||
|
0 40px 100px rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- the drawn pointer -------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Built and removed by the drag animation; nothing else on the page has one. */
|
||||||
|
#pointer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
will-change: transform;
|
||||||
|
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#pointer svg {
|
||||||
|
display: block;
|
||||||
|
transition: transform 90ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The click itself: the arrow dips, so a grab is visible even mid-pause. */
|
||||||
|
#pointer.down svg {
|
||||||
|
transform: scale(0.82);
|
||||||
|
}
|
||||||
|
|
||||||
.deck {
|
.deck {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
BOUNCE_SPEED,
|
BOUNCE_SPEED,
|
||||||
CORNER_INTERVAL_SECONDS,
|
CORNER_INTERVAL_SECONDS,
|
||||||
CORNER_MARGIN,
|
CORNER_MARGIN,
|
||||||
|
DRAG_MIN_SCALE,
|
||||||
|
DRAG_MAX_SCALE,
|
||||||
BACKGROUNDS,
|
BACKGROUNDS,
|
||||||
ANIMATIONS,
|
ANIMATIONS,
|
||||||
AUTO_ANIMATION,
|
AUTO_ANIMATION,
|
||||||
@@ -45,6 +47,8 @@ app.get('/config.js', (_req, res) => {
|
|||||||
`export const BOUNCE_SPEED = ${BOUNCE_SPEED};\n` +
|
`export const BOUNCE_SPEED = ${BOUNCE_SPEED};\n` +
|
||||||
`export const CORNER_INTERVAL_SECONDS = ${CORNER_INTERVAL_SECONDS};\n` +
|
`export const CORNER_INTERVAL_SECONDS = ${CORNER_INTERVAL_SECONDS};\n` +
|
||||||
`export const CORNER_MARGIN = ${CORNER_MARGIN};\n` +
|
`export const CORNER_MARGIN = ${CORNER_MARGIN};\n` +
|
||||||
|
`export const DRAG_MIN_SCALE = ${DRAG_MIN_SCALE};\n` +
|
||||||
|
`export const DRAG_MAX_SCALE = ${DRAG_MAX_SCALE};\n` +
|
||||||
`export const BACKGROUNDS = ${JSON.stringify(BACKGROUNDS)};\n` +
|
`export const BACKGROUNDS = ${JSON.stringify(BACKGROUNDS)};\n` +
|
||||||
`export const ANIMATIONS = ${JSON.stringify(ANIMATIONS)};\n` +
|
`export const ANIMATIONS = ${JSON.stringify(ANIMATIONS)};\n` +
|
||||||
`export const AUTO_ANIMATION = ${JSON.stringify(AUTO_ANIMATION)};\n`,
|
`export const AUTO_ANIMATION = ${JSON.stringify(AUTO_ANIMATION)};\n`,
|
||||||
|
|||||||
Reference in New Issue
Block a user