const thing = document.getElementById('cursor-thing'); let updateTimerId; let lastUpdate = 0; // targeting let mouseX = 0; let mouseY = 0; let scrollX = document.scrollingElement.scrollLeft; let scrollY = document.scrollingElement.scrollTop; const getTargetX = () => mouseX + scrollX; const getTargetY = () => mouseY + scrollY; // physics let xPos = 0; let yPos = 0; let angle = 0; let velocity = 0; const maxVel = 600; let accel = 0; const maxAccel = 1500; const accelCurve = (x) => maxAccel*(x/200)**3; // maxAccel when >150px distance // more physics const slideFric = .9; // coefficient of friction const turnSpeedCoeff = .08; // coefficient of turning // other stuff let brake = false; const brakeDist = 70; const duckSize = 50; // animation let alt = false; let lastAlt = 0; function update() { // timing thisUpdate = Date.now(); deltaT = (thisUpdate - lastUpdate)/1000.0; lastUpdate = thisUpdate; // update trajectory targetX = getTargetX(); targetY = getTargetY(); xDelta = targetX - xPos; yDelta = targetY - yPos; // find target angle let targetAngle = Math.atan(yDelta/xDelta); if (isNaN(targetAngle)) targetAngle = 0; if (xDelta < 0) targetAngle = Math.PI*.5 - (targetAngle*-1); else targetAngle = -((targetAngle*-1) - Math.PI*1.5); // find shortest rotation delta let aAngle = angle; let aTargetAngle = targetAngle; if (aTargetAngle > aAngle && aTargetAngle - aAngle > Math.PI) { aAngle += Math.PI; aTargetAngle -= Math.PI; } else if (aAngle > aTargetAngle && aAngle - aTargetAngle > Math.PI) { aAngle -= Math.PI; aTargetAngle += Math.PI; } rDelta = aTargetAngle - aAngle; // console.log(`myAngle: ${angle * (180 / Math.PI)} target angle: ${targetAngle * (180 / Math.PI)}`) // console.log(`deelta: ${(rDelta) * (180 / Math.PI)} `); // apply delta angle += rDelta * turnSpeedCoeff; // counter overflow if (angle > 2*Math.PI) angle -= 2*Math.PI; else if (angle < 0) angle += 2*Math.PI; // stop when close let dist = Math.sqrt((xPos-targetX)**2 + (yPos-targetY)**2); brake = dist < brakeDist; if (!brake) { accel = accelCurve(dist); if (accel > maxAccel) accel = maxAccel; else if (accel < -maxAccel) accel = -maxAccel; } else accel = 0; // physics velocity += accel * deltaT; if (brake) { // basic friction velocity *= slideFric; } // vel limit if (velocity > maxVel) velocity = maxVel; else if (velocity < -maxVel) velocity = -maxVel // translate vector to 2D let hAngle = angle % Math.PI; let right = angle > Math.PI; let xVel = Math.sin(hAngle) * velocity; let yVel = Math.cos(hAngle) * velocity; if (right) { yVel *= -1; } else { xVel *= -1; } // apply velocity xPos += xVel * deltaT; yPos += yVel * deltaT; // bounce duck at edge of screen if (xPos > document.scrollingElement.scrollWidth - duckSize) { xPos = document.scrollingElement.scrollWidth - duckSize; angle = 2*Math.PI - angle; } else if (xPos < 0) { xPos = 0; angle = 2*Math.PI - angle; } if (yPos > document.scrollingElement.scrollHeight - duckSize) { yPos = document.scrollingElement.scrollHeight - duckSize; angle = Math.PI - angle; } else if (yPos < 0) { yPos = 0; angle = Math.PI - angle; } // limit angle again if (angle > 2*Math.PI) angle -= 2*Math.PI; else if (angle < 0) angle += 2*Math.PI; // console.log(`accel: ${accel}`); // console.log(`velocity: ${velocity} (${xVel}, ${yVel})`) let visualAngle = angle + (rDelta/2); visualAngle = targetAngle; thing.style.left = xPos + 'px'; thing.style.top = yPos + 'px'; // thing.style.rotate = visualAngle + 'rad'; // if (brake) { // thing.style['background-color'] = '#00ff00'; // } else { // thing.style['background-color'] = '#ff0000'; // } let tAngle = visualAngle + Math.PI/4; if (tAngle < Math.PI/2 || tAngle > Math.PI * 2) { // front-facing thing.style['background-position-x'] = (2*duckSize) + 'px'; } else if (tAngle < Math.PI) { // left-facing thing.style['background-position-x'] = '0px'; } else if (tAngle < Math.PI*3/2) { // right-facing thing.style['background-position-x'] = (3*duckSize) + 'px'; } else { // back-facing thing.style['background-position-x'] = duckSize + 'px'; } if (thisUpdate - lastAlt > 70) { lastAlt = thisUpdate; alt = !alt; } if (velocity > 50) { thing.style['background-position-y'] = (duckSize * (alt + 1)) + 'px'; } else { thing.style['background-position-y'] = '0px'; } } // scroll and mouse are separate so the cursor is followed when scrolled document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; }); document.addEventListener('scroll', e => { el = e.target.scrollingElement; scrollX = el.scrollLeft; scrollY = el.scrollTop; }); // kick-start duck let duck_start = false; function click_duck() { if (duck_start) return; duck_start = true; xPos = thing.offsetLeft; yPos = thing.offsetTop; thing.style.position = 'absolute'; thing.style.cursor = 'auto'; lastUpdate = Date.now(); updateTimerId = setInterval(update, 10); }