-
-
Save Pitu/782dad9b4973352616665357f25ac6ed to your computer and use it in GitHub Desktop.
ScrollBuddy Implementation - https://focusfurnace.com/scroll_buddy.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="scrollBuddy" style="top: 0px;"> | |
<div class="head"></div> | |
<div class="body"></div> | |
<div class="left-arm" style="transform: rotate(90deg);"></div> | |
<div class="left-arm-lower" style="transform: translate(6.12323e-16px, 10px) rotate(92.8284deg);"></div> | |
<div class="right-arm" style="transform: rotate(90deg);"></div> | |
<div class="right-arm-lower" style="transform: translate(6.12323e-16px, 10px) rotate(87.1716deg);"></div> | |
<div class="left-leg-upper" style="transform: rotate(90deg);"></div> | |
<div class="left-leg-lower" style="transform: translate(7.34788e-16px, 12px) rotate(75.8579deg);"></div> | |
<div class="left-foot" style="transform: translate(2.44328px, 21.6969px) rotate(180deg);"></div> | |
<div class="right-leg-upper" style="transform: rotate(90deg);"></div> | |
<div class="right-leg-lower" style="transform: translate(7.34788e-16px, 12px) rotate(104.142deg);"></div> | |
<div class="right-foot" style="transform: translate(-2.44328px, 21.6969px) rotate(180deg);"></div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const buddy = document.getElementById('scrollBuddy'); | |
let lastScroll = 0; | |
let walkPhase = 0; | |
const walkSpeed = 0.0314; | |
// Check for reduced motion preferences | |
const prefersReducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); | |
const prefersReducedMotion = prefersReducedMotionQuery.matches || | |
navigator.platform.toLowerCase().includes('mac') && | |
window.matchMedia('(-apple-reduce-motion: reduce)').matches; | |
// Hide scroll buddy if reduced motion is preferred | |
if (prefersReducedMotion) { | |
buddy.style.display = 'none'; | |
} | |
// Cache DOM elements for better performance | |
const leftArm = buddy.querySelector('.left-arm'); | |
const rightArm = buddy.querySelector('.right-arm'); | |
const leftArmLower = buddy.querySelector('.left-arm-lower'); | |
const rightArmLower = buddy.querySelector('.right-arm-lower'); | |
const leftLegUpper = buddy.querySelector('.left-leg-upper'); | |
const rightLegUpper = buddy.querySelector('.right-leg-upper'); | |
const leftLegLower = buddy.querySelector('.left-leg-lower'); | |
const rightLegLower = buddy.querySelector('.right-leg-lower'); | |
const leftFoot = buddy.querySelector('.left-foot'); | |
const rightFoot = buddy.querySelector('.right-foot'); | |
// Set initial neutral pose if reduced motion is preferred | |
if (prefersReducedMotion) { | |
// Set all limbs to neutral standing position | |
leftArm.style.transform = rightArm.style.transform = 'rotate(90deg)'; | |
leftArmLower.style.transform = rightArmLower.style.transform = 'rotate(90deg)'; | |
leftLegUpper.style.transform = rightLegUpper.style.transform = 'rotate(90deg)'; | |
leftLegLower.style.transform = rightLegLower.style.transform = 'rotate(90deg)'; | |
leftFoot.style.transform = rightFoot.style.transform = 'rotate(180deg)'; | |
} | |
function updateArmMovement(phase) { | |
if (prefersReducedMotion) return; | |
// Upper arm movement | |
const upperArmAngle = Math.sin(phase) * 30; | |
leftArm.style.transform = `rotate(${90 + upperArmAngle}deg)`; | |
rightArm.style.transform = `rotate(${90 - upperArmAngle}deg)`; | |
// Lower arm movement (elbow bend) | |
// Only bend elbow when arm swings forward (positive angle for left, negative for right) | |
const leftLowerArmAngle = Math.sin(phase + Math.PI/4) * 20 * (upperArmAngle > 0 ? 1 : 0.2); | |
const rightLowerArmAngle = Math.sin(phase + Math.PI/4) * 20 * (upperArmAngle < 0 ? 1 : 0.2); | |
const leftElbowX = Math.cos((90 + upperArmAngle) * Math.PI/180) * 10; | |
const leftElbowY = Math.sin((90 + upperArmAngle) * Math.PI/180) * 10; | |
const rightElbowX = Math.cos((90 - upperArmAngle) * Math.PI/180) * 10; | |
const rightElbowY = Math.sin((90 - upperArmAngle) * Math.PI/180) * 10; | |
leftArmLower.style.transform = `translate(${leftElbowX}px, ${leftElbowY}px) rotate(${90 + upperArmAngle + leftLowerArmAngle}deg)`; | |
rightArmLower.style.transform = `translate(${rightElbowX}px, ${rightElbowY}px) rotate(${90 - upperArmAngle - rightLowerArmAngle}deg)`; | |
} | |
function updateLegMovement(phase) { | |
if (prefersReducedMotion) return; | |
// Upper leg movement | |
const upperLegAngle = Math.sin(phase) * 25; | |
leftLegUpper.style.transform = `rotate(${90 - upperLegAngle}deg)`; | |
rightLegUpper.style.transform = `rotate(${90 + upperLegAngle}deg)`; | |
// Lower leg movement (knee bend) | |
const lowerLegAngle = Math.sin(phase + Math.PI/4) * 20; | |
const leftKneeX = Math.cos((90 - upperLegAngle) * Math.PI/180) * 12; | |
const leftKneeY = Math.sin((90 - upperLegAngle) * Math.PI/180) * 12; | |
const rightKneeX = Math.cos((90 + upperLegAngle) * Math.PI/180) * 12; | |
const rightKneeY = Math.sin((90 + upperLegAngle) * Math.PI/180) * 12; | |
leftLegLower.style.transform = `translate(${leftKneeX}px, ${leftKneeY}px) rotate(${90 - upperLegAngle - lowerLegAngle}deg)`; | |
rightLegLower.style.transform = `translate(${rightKneeX}px, ${rightKneeY}px) rotate(${90 + upperLegAngle + lowerLegAngle}deg)`; | |
// Foot movement | |
const leftFootX = leftKneeX + Math.cos((90 - upperLegAngle - lowerLegAngle) * Math.PI/180) * 10; | |
const leftFootY = leftKneeY + Math.sin((90 - upperLegAngle - lowerLegAngle) * Math.PI/180) * 10; | |
const rightFootX = rightKneeX + Math.cos((90 + upperLegAngle + lowerLegAngle) * Math.PI/180) * 10; | |
const rightFootY = rightKneeY + Math.sin((90 + upperLegAngle + lowerLegAngle) * Math.PI/180) * 10; | |
const footAngle = Math.sin(phase) * 15; | |
leftFoot.style.transform = `translate(${leftFootX}px, ${leftFootY}px) rotate(${180 - footAngle}deg)`; | |
rightFoot.style.transform = `translate(${rightFootX}px, ${rightFootY}px) rotate(${180 + footAngle}deg)`; | |
} | |
function updateBuddyVerticalPosition(scrollPosition) { | |
const windowHeight = window.innerHeight; | |
const documentHeight = document.documentElement.scrollHeight; | |
const scrollPercent = scrollPosition / (documentHeight - windowHeight); | |
const buddyHeight = buddy.offsetHeight; | |
const maxTop = windowHeight - buddyHeight; | |
const newTop = scrollPercent * maxTop; | |
// Use requestAnimationFrame for smooth updates | |
requestAnimationFrame(() => { | |
buddy.style.top = `${newTop}px`; | |
}); | |
} | |
function updateBuddyPosition() { | |
const scrollPosition = window.scrollY; | |
const scrollDelta = scrollPosition - lastScroll; | |
// Only update walk phase if animations are enabled | |
if (!prefersReducedMotion) { | |
walkPhase += scrollDelta * walkSpeed; | |
updateArmMovement(walkPhase); | |
updateLegMovement(walkPhase); | |
} | |
// Always update vertical position | |
updateBuddyVerticalPosition(scrollPosition); | |
lastScroll = scrollPosition; | |
} | |
// Listen for changes to motion preference (both standard and macOS specific) | |
prefersReducedMotionQuery.addEventListener('change', (e) => { | |
location.reload(); | |
}); | |
window.matchMedia('(-apple-reduce-motion: reduce)').addEventListener('change', (e) => { | |
location.reload(); | |
}); | |
// Event listeners | |
window.addEventListener('scroll', updateBuddyPosition); | |
window.addEventListener('resize', updateBuddyPosition); | |
// Set initial position | |
updateBuddyPosition(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
height: auto; | |
margin: 0; | |
position: relative; | |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
line-height: 1.6; | |
color: #333; | |
} | |
.blog-content { | |
max-width: 800px; | |
margin: 40px auto; | |
padding: 0 20px; | |
} | |
.blog-title { | |
font-size: 2.5em; | |
margin-bottom: 0.5em; | |
color: #222; | |
} | |
.blog-meta { | |
color: #666; | |
margin-bottom: 2em; | |
font-size: 0.9em; | |
} | |
.blog-section { | |
margin-bottom: 2em; | |
} | |
.blog-section h2 { | |
color: #444; | |
margin: 1.5em 0 0.5em; | |
} | |
p { | |
margin-bottom: 1.2em; | |
} | |
#scrollBuddy { | |
position: fixed; | |
right: 10px; | |
top: 50%; | |
width: 30px; | |
height: 70px; | |
transition: top 0.1s ease-out; | |
transform: rotate(270deg); | |
z-index: 1000; | |
} | |
/* Stick figure parts */ | |
.head { | |
width: 10px; | |
height: 10px; | |
background: black; | |
border-radius: 50%; | |
position: absolute; | |
left: 8px; | |
top: 0; | |
} | |
.body { | |
width: 2px; | |
height: 20px; | |
background: black; | |
position: absolute; | |
left: 12px; | |
top: 10px; | |
} | |
.left-arm, | |
.right-arm { | |
width: 10px; | |
height: 2px; | |
background: black; | |
position: absolute; | |
transform: rotate(90deg); | |
} | |
.left-arm { | |
left: 13px; | |
top: 15px; | |
transform-origin: left center; | |
} | |
.right-arm { | |
left: 13px; | |
top: 15px; | |
transform-origin: left center; | |
} | |
.left-arm-lower, | |
.right-arm-lower { | |
left: 13px; | |
top: 15px; | |
width: 8px; | |
height: 2px; | |
background: black; | |
position: absolute; | |
transform-origin: left center; | |
} | |
/* Updated leg styles with upper and lower segments */ | |
.left-leg-upper, | |
.right-leg-upper { | |
width: 12px; | |
height: 2px; | |
background: black; | |
position: absolute; | |
transform-origin: top left; | |
} | |
.left-leg-upper { | |
left: 14px; | |
top: 30px; | |
} | |
.right-leg-upper { | |
left: 14px; | |
top: 30px; | |
} | |
.left-leg-lower, | |
.right-leg-lower { | |
left: 14px; | |
top: 30px; | |
width: 10px; | |
height: 2px; | |
background: black; | |
position: absolute; | |
transform-origin: top left; | |
} | |
.left-foot, | |
.right-foot { | |
left: 14px; | |
top: 30px; | |
width: 8px; | |
height: 2px; | |
background: black; | |
position: absolute; | |
transform-origin: left center; | |
} | |
@media (prefers-reduced-motion: reduce) { | |
#scrollBuddy { | |
display: none; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment