257 lines
9.1 KiB
JavaScript
257 lines
9.1 KiB
JavaScript
// cursor.js — Venom/Spider Cursor · Space Edition (white + neon)
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
if (window.matchMedia("(pointer: coarse)").matches) return;
|
|
|
|
/* ── CONFIG ── */
|
|
const CONFIG = {
|
|
tentacleCount: 10,
|
|
triggerDist: 8,
|
|
maxLength: 300,
|
|
connectionDist: 150,
|
|
prediction: 3.5,
|
|
|
|
// Idle: cold silver-white
|
|
idleStroke: { r: 210, g: 228, b: 255 },
|
|
idleGlow: { r: 160, g: 200, b: 255 },
|
|
|
|
// Hover: neon orange (matches site accent)
|
|
hoverStroke: { r: 255, g: 102, b: 0 },
|
|
hoverGlow: { r: 255, g: 80, b: 0 },
|
|
};
|
|
|
|
/* ── toggle ── */
|
|
const toggleBtn = document.getElementById('cursorToggle');
|
|
const body = document.body;
|
|
|
|
let isCursorDisabled = localStorage.getItem('venomCursorDisabled') !== 'false';
|
|
|
|
function updateCursorState() {
|
|
if (isCursorDisabled) {
|
|
body.classList.add('system-cursor');
|
|
document.documentElement.style.cursor = '';
|
|
if (toggleBtn) {
|
|
toggleBtn.classList.remove('active');
|
|
const icon = toggleBtn.querySelector('.cursor-icon');
|
|
if (icon && icon.tagName === 'IMG') {
|
|
icon.src = 'images/icons/spider.png';
|
|
icon.alt = 'Spider Cursor';
|
|
}
|
|
}
|
|
} else {
|
|
body.classList.remove('system-cursor');
|
|
document.documentElement.style.cursor = 'none';
|
|
if (toggleBtn) {
|
|
toggleBtn.classList.add('active');
|
|
const icon = toggleBtn.querySelector('.cursor-icon');
|
|
if (icon && icon.tagName === 'IMG') {
|
|
icon.src = 'images/additional/cursor.png';
|
|
icon.alt = 'Custom Cursor';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
updateCursorState();
|
|
|
|
if (toggleBtn) {
|
|
toggleBtn.addEventListener('click', () => {
|
|
isCursorDisabled = !isCursorDisabled;
|
|
localStorage.setItem('venomCursorDisabled', isCursorDisabled);
|
|
updateCursorState();
|
|
});
|
|
}
|
|
|
|
/* ── canvas ── */
|
|
const canvas = document.createElement('canvas');
|
|
canvas.id = 'venom-cursor';
|
|
document.body.appendChild(canvas);
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
/* ── state ── */
|
|
const mouse = { x: 0, y: 0 };
|
|
const oldMouse = { x: 0, y: 0 };
|
|
let isHover = false;
|
|
let colorT = 0; // 0 = idle, 1 = hover (lerped)
|
|
|
|
const tentacles = [];
|
|
let rotation = 0; // reticle ring slow spin
|
|
|
|
/* ── hover selector ── */
|
|
const HOVER_SEL = [
|
|
'a', 'button', '[role="button"]', 'select', 'label',
|
|
'.nav-link', '.btn-primary', '.btn-ghost', '.nav-demo-btn',
|
|
'.price-cta', '.prob-card', '.module-card', '.how-step',
|
|
'.dropdown-item', '.mobile-nav-link', '.mobile-nav-cta',
|
|
'[onclick]', '.clickable'
|
|
].join(',');
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
mouse.x = e.clientX;
|
|
mouse.y = e.clientY;
|
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
isHover = !!el && (el.matches(HOVER_SEL) || !!el.closest(HOVER_SEL));
|
|
}, { passive: true });
|
|
|
|
/* ── colour helper ── */
|
|
function lerpColor(a, b, t) {
|
|
return {
|
|
r: a.r + (b.r - a.r) * t | 0,
|
|
g: a.g + (b.g - a.g) * t | 0,
|
|
b: a.b + (b.b - a.b) * t | 0,
|
|
};
|
|
}
|
|
|
|
/* ── Tentacle ── */
|
|
class Tentacle {
|
|
constructor(targetX, targetY) {
|
|
this.anchor = { x: targetX, y: targetY };
|
|
this.dead = false;
|
|
this.dist = 0;
|
|
this.age = 0;
|
|
}
|
|
|
|
update() {
|
|
const dx = mouse.x - this.anchor.x;
|
|
const dy = mouse.y - this.anchor.y;
|
|
this.dist = Math.sqrt(dx * dx + dy * dy);
|
|
this.age++;
|
|
if (this.dist > CONFIG.maxLength) this.dead = true;
|
|
}
|
|
|
|
draw(stroke, glow) {
|
|
if (this.dead) return;
|
|
|
|
const tension = Math.min(this.dist / CONFIG.maxLength, 1);
|
|
|
|
// Fade in over first 10 frames
|
|
const fadeIn = Math.min(this.age / 10, 1);
|
|
const lineAlpha = fadeIn * (1 - tension * 0.85) * 0.70;
|
|
const dotAlpha = fadeIn * (1 - tension) * 0.90;
|
|
const lw = Math.max(0.2, 1.1 * (1 - tension * 0.80));
|
|
|
|
// Glow pass (wide, soft)
|
|
ctx.save();
|
|
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},${lineAlpha * 0.55})`;
|
|
ctx.shadowBlur = 6;
|
|
ctx.beginPath();
|
|
ctx.moveTo(mouse.x, mouse.y);
|
|
ctx.lineTo(this.anchor.x, this.anchor.y);
|
|
ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${lineAlpha})`;
|
|
ctx.lineWidth = lw;
|
|
ctx.lineCap = 'round';
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
// Anchor dot
|
|
ctx.beginPath();
|
|
ctx.arc(this.anchor.x, this.anchor.y, 1.8 * (1 - tension), 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${dotAlpha})`;
|
|
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},${dotAlpha * 0.6})`;
|
|
ctx.shadowBlur = 4;
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
}
|
|
|
|
/* ── render ── */
|
|
function render() {
|
|
if (isCursorDisabled) {
|
|
requestAnimationFrame(render);
|
|
return;
|
|
}
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Lerp color T
|
|
colorT += (isHover ? 1 : 0 - colorT) * 0.12;
|
|
|
|
const stroke = lerpColor(CONFIG.idleStroke, CONFIG.hoverStroke, colorT);
|
|
const glow = lerpColor(CONFIG.idleGlow, CONFIG.hoverGlow, colorT);
|
|
|
|
/* spawn tentacle on movement */
|
|
const moved = Math.hypot(mouse.x - oldMouse.x, mouse.y - oldMouse.y);
|
|
if (moved > CONFIG.triggerDist) {
|
|
const vx = mouse.x - oldMouse.x;
|
|
const vy = mouse.y - oldMouse.y;
|
|
const tx = mouse.x + vx * CONFIG.prediction + (Math.random() - 0.5) * 60;
|
|
const ty = mouse.y + vy * CONFIG.prediction + (Math.random() - 0.5) * 60;
|
|
tentacles.push(new Tentacle(tx, ty));
|
|
oldMouse.x = mouse.x;
|
|
oldMouse.y = mouse.y;
|
|
}
|
|
if (tentacles.length > CONFIG.tentacleCount) tentacles.shift();
|
|
|
|
/* update + draw tentacles */
|
|
for (let i = tentacles.length - 1; i >= 0; i--) {
|
|
tentacles[i].update();
|
|
if (tentacles[i].dead) tentacles.splice(i, 1);
|
|
else tentacles[i].draw(stroke, glow);
|
|
}
|
|
|
|
/* web connections between close anchor pairs */
|
|
for (let i = 0; i < tentacles.length; i++) {
|
|
for (let j = i + 1; j < tentacles.length; j++) {
|
|
const a = tentacles[i].anchor;
|
|
const b = tentacles[j].anchor;
|
|
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
|
if (d < CONFIG.connectionDist) {
|
|
const alpha = (1 - d / CONFIG.connectionDist) * 0.28;
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.x, a.y);
|
|
ctx.lineTo(b.x, b.y);
|
|
ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${alpha})`;
|
|
ctx.lineWidth = 0.5;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cursor shape — segmented reticle ring */
|
|
rotation += isHover ? 0.040 : 0.016;
|
|
|
|
const ringR = isHover ? 9 : 7;
|
|
const gapHalf = 0.28; // gap half-angle in radians at each cardinal point
|
|
const segments = [
|
|
[rotation + gapHalf, rotation + Math.PI * 0.5 - gapHalf],
|
|
[rotation + Math.PI * 0.5 + gapHalf, rotation + Math.PI - gapHalf],
|
|
[rotation + Math.PI + gapHalf, rotation + Math.PI * 1.5 - gapHalf],
|
|
[rotation + Math.PI * 1.5 + gapHalf, rotation + Math.PI * 2.0 - gapHalf],
|
|
];
|
|
|
|
ctx.save();
|
|
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},0.75)`;
|
|
ctx.shadowBlur = isHover ? 14 : 8;
|
|
ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},0.90)`;
|
|
ctx.lineWidth = isHover ? 1.4 : 1.1;
|
|
ctx.lineCap = 'round';
|
|
for (const [start, end] of segments) {
|
|
ctx.beginPath();
|
|
ctx.arc(mouse.x, mouse.y, ringR, start, end);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
|
|
// Center dot — exact cursor tip
|
|
ctx.save();
|
|
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},0.90)`;
|
|
ctx.shadowBlur = isHover ? 10 : 6;
|
|
ctx.beginPath();
|
|
ctx.arc(mouse.x, mouse.y, isHover ? 2.2 : 1.6, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},1)`;
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
render();
|
|
});
|