<div class="pedal-path-container">
<div class="pedal-path">
<img src="https://d1yei2z3i6k35z.cloudfront.net/1970629/677599f123309_dsfgvdsv.png">
<img src="https://d1yei2z3i6k35z.cloudfront.net/1970629/677599f123309_dsfgvdsv.png">
<img src="https://d1yei2z3i6k35z.cloudfront.net/1970629/677599f123309_dsfgvdsv.png">
<img src="https://d1yei2z3i6k35z.cloudfront.net/1970629/677599f123309_dsfgvdsv.png">
<img src="https://d1yei2z3i6k35z.cloudfront.net/1970629/677599f123309_dsfgvdsv.png">
</div>
<div class="road-overlay"></div>
</div>
<style>
.pedal-path-container {
width: 100%;
height: 35px;
overflow: hidden;
position: relative;
}
.pedal-path {
height: 100%;
position: absolute;
white-space: nowrap;
will-change: transform;
}
.pedal-path img {
height: 100%;
width: auto;
}
.road-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to right,
#140D3F 0%,
transparent 10%,
transparent 90%,
#140D3F 100%
);
pointer-events: none;
}
</style>
<script>
class PedalPathAnimation {
constructor() {
this.container = document.querySelector('.pedal-path-container');
this.path = document.querySelector('.pedal-path');
this.imgSrc = 'https://d1yei2z3i6k35z.cloudfront.net/1970629/677599f123309_dsfgvdsv.png';
this.wheelWidth = 0;
this.animationId = null;
this.position = 0;
this.lastTime = null;
this.speed = 0.3;
// Bind methods
this.init = this.init.bind(this);
this.startRide = this.startRide.bind(this);
this.stopRide = this.stopRide.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
this.handleResize = this.handleResize.bind(this);
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', this.init);
} else {
this.init();
}
}
async init() {
try {
await this.setupPedalPath();
this.addEventListeners();
} catch (error) {
console.error('Initialization error:', error);
}
}
async loadWheelImage() {
return new Promise((resolve, reject) => {
const wheel = new Image();
wheel.onload = () => resolve(wheel);
wheel.onerror = (error) => reject(error);
wheel.src = this.imgSrc;
});
}
async setupPedalPath() {
try {
const wheel = await this.loadWheelImage();
this.wheelWidth = wheel.width * (this.container.offsetHeight / wheel.height);
const roadWidth = this.container.offsetWidth;
const wheelCount = Math.ceil(roadWidth / this.wheelWidth) + 1;
// Clear existing content
this.path.innerHTML = '';
// Add new images
const fragment = document.createDocumentFragment();
for (let i = 0; i < wheelCount; i++) {
fragment.appendChild(wheel.cloneNode());
}
this.path.appendChild(fragment);
this.startRide();
} catch (error) {
console.error('Setup error:', error);
}
}
startRide() {
const animate = (timestamp) => {
if (this.lastTime === null) {
this.lastTime = timestamp;
}
const elapsed = timestamp - this.lastTime;
this.lastTime = timestamp;
this.position = (this.position - (this.speed * elapsed)) % this.wheelWidth;
this.path.style.transform = `translateX(${this.position}px)`;
this.animationId = requestAnimationFrame(animate);
};
this.animationId = requestAnimationFrame(animate);
}
stopRide() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
this.lastTime = null;
}
}
handleVisibilityChange() {
if (document.hidden) {
this.stopRide();
} else {
this.startRide();
}
}
handleResize() {
this.stopRide();
this.setupPedalPath();
}
addEventListeners() {
document.addEventListener('visibilitychange', this.handleVisibilityChange);
const resizeObserver = new ResizeObserver(this.handleResize);
resizeObserver.observe(this.container);
}
cleanup() {
this.stopRide();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
// ResizeObserver will be automatically disconnected when the element is removed
}
}
// Initialize the animation
const pedalAnimation = new PedalPathAnimation();
</script>
Preview:
downloadDownload PNG
downloadDownload JPEG
downloadDownload SVG
Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!
Click to optimize width for Twitter