<div id="grad"></div>
#grad {
width: 100%;
height: 100%;
touch-action: none;
cursor: grab;
}
#grad:active {
cursor: grabbing;
}
#grad::before {
content: "";
z-index: -1;
position: fixed;
width: 100vmax;
height: 100vmax;
background: repeating-linear-gradient(
var(--angle, -45deg),
#ee7752 calc(0 * var(--len) + var(--off)),
#e73c7e calc(0.25 * var(--len) + var(--off)),
#23a6d5 calc(0.5 * var(--len) + var(--off)),
#23d5ab calc(0.75 * var(--len) + var(--off)),
#ee7752 calc(1 * var(--len) + var(--off))
);
}
const gradient = document.getElementById("grad");
const points = new Map();
const state = {
scale: 2,
angle: -45,
offset: 0,
prevDist: -1,
prevAngle: -1,
lastPos: { x: 0, y: 0 },
gesture: false,
speed: 10,
lastTime: 0,
};
const CONFIG = {
GRADIENT_LENGTH: 100,
MIN_SCALE: 0.05,
MAX_SCALE: 16,
WHEEL_SENSITIVITY: 0.0015,
PAN_SENSITIVITY: 0.8,
};
const getGeom = (degs) => {
const rads = (degs * Math.PI) / 180;
const vect = {
x: Math.sin(rads),
y: -Math.cos(rads),
};
const ww = window.innerWidth,
wh = window.innerHeight,
mx = Math.max(ww, wh);
let minp = Infinity,
maxp = -Infinity;
[
{ x: 0, y: 0 },
{ x: mx, y: 0 },
{ x: 0, y: mx },
{ x: mx, y: mx },
].forEach((corn) => {
const proj = corn.x * vect.x + corn.y * vect.y;
minp = Math.min(minp, proj);
maxp = Math.max(maxp, proj);
});
const projLength = Math.max(1, maxp - minp);
const projCenter =
(((ww / 2) * vect.x + (wh / 2) * vect.y - minp) /
projLength) *
100;
return { vect, projLength, projCenter };
};
const applyScale = (factor) => {
const newScale = Math.max(
CONFIG.MIN_SCALE,
Math.min(state.scale * factor, CONFIG.MAX_SCALE)
);
const ratio = newScale / state.scale;
const center = getGeom(state.angle).projCenter;
state.offset = ratio * state.offset + (1 - ratio) * center;
state.scale = newScale;
};
const onPointerDown = (evt) => {
evt.preventDefault();
const ex = evt.clientX;
const ey = evt.clientY;
const pt = evt.pointerId;
points.set(pt, {
x: ex,
y: ey,
});
state.lastPos = { x: ex, y: ey };
state.gesture = true;
gradient.setPointerCapture?.(pt);
};
const onPointerMove = (evt) => {
if (!points.has(evt.pointerId)) return;
evt.preventDefault();
const ex = evt.clientX;
const ey = evt.clientY;
points.set(evt.pointerId, {
x: ex,
y: ey,
});
if (points.size === 1) {
const moveDelta = {
x: ex - state.lastPos.x,
y: ey - state.lastPos.y,
};
state.lastPos = { x: ex, y: ey };
const geom = getGeom(state.angle);
const panOff =
moveDelta.x * geom.vect.x +
moveDelta.y * geom.vect.y;
state.offset +=
(panOff / geom.projLength) *
100 *
CONFIG.PAN_SENSITIVITY;
} else if (points.size === 2) {
const [p1, p2] = [...points.values()];
const dist = Math.hypot(p1.x - p2.x, p1.y - p2.y);
const angle =
(Math.atan2(p1.y - p2.y, p1.x - p2.x) * 180) /
Math.PI;
if (state.prevDist > 0) {
applyScale(dist / state.prevDist);
let angleDelta = angle - state.prevAngle;
angleDelta -= Math.round(angleDelta / 360) * 360;
const anteRot = getGeom(state.angle).projCenter;
state.angle += angleDelta;
const postRot = getGeom(state.angle).projCenter;
state.offset += postRot - anteRot;
}
state.prevDist = dist;
state.prevAngle = angle;
}
};
const onPointerUp = (evt) => {
const pt = evt.pointerId;
points.delete(pt);
if (points.size < 2) {
state.prevDist = -1;
state.prevAngle = -1;
}
state.gesture = points.size > 0;
if (points.size === 1) {
const pointer = points.values().next().value;
state.lastPos = { x: pointer.x, y: pointer.y };
}
gradient.releasePointerCapture?.(pt);
};
const onWheel = (evt) => {
evt.preventDefault();
applyScale(
Math.exp(-evt.deltaY * CONFIG.WHEEL_SENSITIVITY)
);
};
const animate = (ts) => {
const deltaTime = state.lastTime
? (ts - state.lastTime) / 1000
: 0;
state.lastTime = ts;
const targetSpeed = state.gesture ? 0 : 10;
state.speed += (targetSpeed - state.speed) * 0.05;
if (Math.abs(state.speed) > 0.01)
state.offset -= state.speed * deltaTime;
const cssLen = CONFIG.GRADIENT_LENGTH * state.scale;
const cssOff = ((state.offset % cssLen) + cssLen) % cssLen;
gradient.style.setProperty("--off", `${cssOff}%`);
gradient.style.setProperty("--angle", `${state.angle}deg`);
gradient.style.setProperty("--len", `${cssLen}%`);
requestAnimationFrame(animate);
};
gradient.addEventListener("touchstart", evt => evt.preventDefault());
gradient.addEventListener("pointerdown", onPointerDown);
gradient.addEventListener("pointermove", onPointerMove);
["pointerup", "pointercancel", "pointerleave"].forEach(
(eventName) =>
gradient.addEventListener(eventName, onPointerUp)
);
gradient.addEventListener("wheel", onWheel, { passive: false });
requestAnimationFrame(animate);