Two Fire Souls

Aryana Soul

CLICK HERE
🥰👇

SPIN ME
HOLD • DRAG • SPIN
GETTING ME READY
0%
←DRAG ME→
SLOW
FAST
LOADING...
(() => { /* ========================= SETTINGS ========================= */ const FRAME_COUNT = 100; const BASE = "https://res.cloudinary.com/u1wpvp5m/image/upload/"; /* Smaller number = more sensitive/faster drag. 4.2 works well for 100-frame sequence. */ const PIXELS_PER_FRAME = 4.2; /* Inertia settings */ const FRICTION = 0.945; const MAX_SPEED = 4.5; const STOP_SPEED = 0.015; /* ========================= ELEMENTS ========================= */ const stage = document.getElementById("tfsStage"); const canvas = document.getElementById("tfsCanvas"); const ctx = canvas.getContext( "2d", { alpha: false, desynchronized: true } ); const loader = document.getElementById("tfsLoader"); const percent = document.getElementById("tfsPercent"); const hint = document.getElementById("tfsHint"); const bar = document.getElementById("tfsBar"); const status = document.getElementById("tfsStatus"); /* ========================= STATE ========================= */ const frames = new Array(FRAME_COUNT); let loaded = 0; let ready = false; let position = 0; let renderedIndex = -1; let dragging = false; let lastX = 0; let lastPointerTime = 0; let velocity = 0; let targetVelocity = 0; let raf = null; let lastRenderTime = 0; /* ========================= URL ========================= */ function getURL(index) { const number = String(index + 1) .padStart(3, "0"); return ( BASE + "spin_" + number + ".webp" ); } /* ========================= FRAME HELPERS ========================= */ function wrap(value) { value %= FRAME_COUNT; if (value < 0) { value += FRAME_COUNT; } return value; } function frameIndex() { return Math.floor( wrap(position) ); } /* ========================= CANVAS ========================= */ function configureCanvas(img) { /* Internal canvas resolution matches our WebP files. */ canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; /* This prevents the canvas from initially collapsing. */ canvas.style.aspectRatio = img.naturalWidth + " / " + img.naturalHeight; } function draw(force = false) { if (!ready) return; const index = frameIndex(); if ( !force && index === renderedIndex ) { return; } const img = frames[index]; if (!img) return; ctx.drawImage( img, 0, 0, canvas.width, canvas.height ); renderedIndex = index; } /* ========================= SPEED BAR ========================= */ function updateBar() { const amount = Math.min( 100, Math.abs(velocity) / MAX_SPEED * 100 ); bar.style.width = amount + "%"; } /* ========================= PRELOAD ========================= */ function preload() { let failed = false; for ( let i = 0; i < FRAME_COUNT; i++ ) { const img = new Image(); /* Decode before interaction where supported. */ img.decoding = "async"; frames[i] = img; img.onload = async () => { try { if (img.decode) { await img.decode(); } } catch (_) {} loaded++; const p = Math.round( loaded / FRAME_COUNT * 100 ); percent.textContent = p + "%"; if ( loaded === FRAME_COUNT && !failed ) { configureCanvas( frames[0] ); ready = true; draw(true); status.textContent = "HOLD + DRAG"; loader.style.opacity = "0"; setTimeout(() => { loader.style.display = "none"; }, 260); } }; img.onerror = () => { failed = true; loader.innerHTML = "
IMAGE LOAD ERROR
"; status.textContent = "A frame could not load"; }; img.src = getURL(i); } } /* ========================= POINTER DOWN ========================= */ function pointerDown(e) { if (!ready) return; dragging = true; lastX = e.clientX; lastPointerTime = performance.now(); velocity = 0; targetVelocity = 0; hint.style.opacity = ".18"; try { stage.setPointerCapture( e.pointerId ); } catch (_) {} startLoop(); } /* ========================= POINTER MOVE ========================= */ function pointerMove(e) { if ( !dragging || !ready ) return; const now = performance.now(); const dx = e.clientX - lastX; const dt = Math.max( 8, now - lastPointerTime ); /* Finger directly controls sequence position. */ const frameMovement = dx / PIXELS_PER_FRAME; position += frameMovement; /* Calculate velocity for release/inertia. */ targetVelocity = frameMovement * (16.67 / dt); targetVelocity = Math.max( -MAX_SPEED, Math.min( MAX_SPEED, targetVelocity ) ); /* Smooth velocity measurement. */ velocity = velocity * 0.35 + targetVelocity * 0.65; lastX = e.clientX; lastPointerTime = now; draw(); updateBar(); } /* ========================= RELEASE ========================= */ function pointerUp() { if (!dragging) return; dragging = false; hint.style.opacity = "1"; startLoop(); } /* ========================= ANIMATION LOOP ========================= */ function startLoop() { if (raf !== null) return; lastRenderTime = performance.now(); raf = requestAnimationFrame( animate ); } function animate(now) { const dt = Math.min( 32, now - lastRenderTime ); lastRenderTime = now; const scale = dt / 16.67; if (!dragging) { position += velocity * scale; velocity *= Math.pow( FRICTION, scale ); if ( Math.abs(velocity) < STOP_SPEED ) { velocity = 0; } draw(); updateBar(); } if ( dragging || Math.abs(velocity) >= STOP_SPEED ) { raf = requestAnimationFrame( animate ); } else { raf = null; updateBar(); } } /* ========================= EVENTS ========================= */ stage.addEventListener( "pointerdown", pointerDown ); stage.addEventListener( "pointermove", pointerMove ); stage.addEventListener( "pointerup", pointerUp ); stage.addEventListener( "pointercancel", pointerUp ); /* Prevent image-style long press behavior around interactive area. */ stage.addEventListener( "contextmenu", e => e.preventDefault() ); /* ========================= START ========================= */ preload(); })();

Sensitive Content 🔞

This page contains content intended for adults (18+) only.