code constant
String
const code
Implementation
static const String code = r'''
function bindCarousels() {
document.querySelectorAll('.arcane-carousel').forEach(function(carousel) {
if (carousel.dataset.arcaneCarouselBound) return;
carousel.dataset.arcaneCarouselBound = 'true';
var track = carousel.querySelector('.arcane-carousel-track');
if (!track) return;
var isDragging = false;
var startX = 0;
var currentTranslateX = 0;
var dragStartTranslateX = 0;
var hasInteracted = false;
var resumeTimer = null;
var trackWidth = 0;
var animationDuration = parseInt(carousel.dataset.animationDuration) || 60;
var resumeDelay = parseInt(carousel.dataset.resumeDelay) || 5000;
// Velocity tracking for momentum
var lastX = 0;
var lastTime = 0;
var velocity = 0;
var momentumAnimationId = null;
function getTrackWidth() {
return track.scrollWidth / 2;
}
function parseTranslateX(transform) {
if (transform === 'none' || !transform) return 0;
var match = transform.match(/matrix\(([^)]+)\)/);
if (match) {
var values = match[1].split(',');
if (values.length >= 5) {
return parseFloat(values[4].trim()) || 0;
}
}
return 0;
}
function applyTransform() {
track.style.transform = 'translateX(' + currentTranslateX + 'px)';
}
function wrapPosition() {
if (trackWidth <= 0) return;
while (currentTranslateX > 0) {
currentTranslateX -= trackWidth;
dragStartTranslateX -= trackWidth;
}
while (currentTranslateX < -trackWidth) {
currentTranslateX += trackWidth;
dragStartTranslateX += trackWidth;
}
}
function stopMomentum() {
if (momentumAnimationId) {
cancelAnimationFrame(momentumAnimationId);
momentumAnimationId = null;
}
}
function startDrag(clientX) {
if (resumeTimer) {
clearTimeout(resumeTimer);
resumeTimer = null;
}
stopMomentum();
trackWidth = getTrackWidth();
if (!hasInteracted) {
var computedStyle = window.getComputedStyle(track);
currentTranslateX = parseTranslateX(computedStyle.transform);
hasInteracted = true;
}
isDragging = true;
startX = clientX;
lastX = clientX;
lastTime = performance.now();
velocity = 0;
dragStartTranslateX = currentTranslateX;
track.classList.add('dragging');
track.classList.remove('resuming');
applyTransform();
}
function updateDrag(clientX) {
if (!isDragging) return;
var now = performance.now();
var deltaTime = now - lastTime;
if (deltaTime > 0) {
// Calculate velocity (pixels per millisecond)
velocity = (clientX - lastX) / deltaTime;
}
lastX = clientX;
lastTime = now;
var deltaX = clientX - startX;
currentTranslateX = dragStartTranslateX + deltaX;
wrapPosition();
applyTransform();
}
function animateMomentum() {
// Apply friction to slow down
var friction = 0.95;
velocity *= friction;
// Stop when velocity is negligible
if (Math.abs(velocity) < 0.01) {
stopMomentum();
scheduleResume();
return;
}
// Move based on velocity (multiply by ~16ms for smooth 60fps)
currentTranslateX += velocity * 16;
wrapPosition();
applyTransform();
momentumAnimationId = requestAnimationFrame(animateMomentum);
}
function scheduleResume() {
if (resumeTimer) clearTimeout(resumeTimer);
resumeTimer = setTimeout(resumeAnimation, resumeDelay);
}
function endDrag() {
if (!isDragging) return;
isDragging = false;
// If there's significant velocity, start momentum animation
if (Math.abs(velocity) > 0.1) {
momentumAnimationId = requestAnimationFrame(animateMomentum);
} else {
scheduleResume();
}
}
// Track the current style element for cleanup
var currentStyleEl = null;
function resumeAnimation() {
if (!track) return;
trackWidth = getTrackWidth();
if (trackWidth <= 0) return;
// Normalize position to be within valid range
wrapPosition();
// Clean up previous dynamic style if exists
if (currentStyleEl && currentStyleEl.parentNode) {
currentStyleEl.parentNode.removeChild(currentStyleEl);
}
// Create a unique animation name for this carousel instance
var animName = 'scroll-carousel-resume-' + Date.now();
// Inject a keyframe animation that starts from the current position
currentStyleEl = document.createElement('style');
currentStyleEl.textContent = '@keyframes ' + animName + ' { from { transform: translateX(' + currentTranslateX + 'px); } to { transform: translateX(' + (currentTranslateX - trackWidth) + 'px); } }';
document.head.appendChild(currentStyleEl);
// Apply the animation in a single frame to avoid snap
// First set the animation, which will immediately start from currentTranslateX
track.style.animation = 'none';
track.offsetHeight; // Force reflow
track.style.transform = '';
track.style.animation = animName + ' ' + animationDuration + 's linear infinite';
track.classList.remove('dragging');
track.classList.remove('resuming');
hasInteracted = false;
}
// Mouse events
track.addEventListener('mousedown', function(e) {
e.preventDefault();
startDrag(e.clientX);
});
track.addEventListener('mousemove', function(e) {
if (isDragging) {
e.preventDefault();
updateDrag(e.clientX);
}
});
document.addEventListener('mouseup', function() {
endDrag();
});
// Touch events
track.addEventListener('touchstart', function(e) {
if (e.touches.length > 0) {
startDrag(e.touches[0].clientX);
}
}, { passive: true });
track.addEventListener('touchmove', function(e) {
if (isDragging && e.touches.length > 0) {
updateDrag(e.touches[0].clientX);
}
}, { passive: true });
document.addEventListener('touchend', function() {
endDrag();
});
document.addEventListener('touchcancel', function() {
endDrag();
});
});
}
// Initial binding
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindCarousels);
} else {
bindCarousels();
}
// Re-bind on dynamic content changes
var carouselObserver = new MutationObserver(function() {
bindCarousels();
});
carouselObserver.observe(document.body, { childList: true, subtree: true });
''';