Nico Pr

Nico Pr

<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);