Typography
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Progress and possibility
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Heading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Block quote
Ordered list
- Item 1
- Item 2
- Item 3
Unordered list
- Item A
- Item B
- Item C
Bold text
Emphasis
Superscript
Subscript
Block Quote
- This is some text inside of a div block.
- This is some text inside of a div block.
- This is some text inside of a div block.
Color
Elements
Heading

CORE Architecture
A scalable battery system architecture that helps align module design, pack integration, and production execution.
Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

Heading
Supporting production-focused work, including module and pack manufacturing capabilities for programs moving toward repeatable, scalable output.
Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Operating Capacity
Production Lines
Years of Testing Data
Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Heading
Through a safety-first design philosophy and rigorous validation, Acculon bridges the gap between cutting-edge chemistries and industrial-grade durability. We build tougher batteries so you can go further.
Components
A Heading Goes Here
Through a safety-first design philosophy and rigorous validation, Acculon bridges the gap between cutting-edge chemistries and industrial-grade durability. We build tougher batteries so you can go further.

A Heading Goes Here
Through a safety-first design philosophy and rigorous validation, Acculon bridges the gap between cutting-edge chemistries and industrial-grade durability. We build tougher batteries so you can go further.

Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Talk with a Specialist
Share your runtime, load, redundancy, and site requirements. Our team will help identify the CORE configuration best aligned to your application.
Message Received!
Get Started Today
Tell us what challenge you are facing and we will provide a solution that meets your needs.
Heading
Through a safety-first design philosophy and rigorous validation, Acculon bridges the gap between cutting-edge chemistries and industrial-grade durability. We build tougher batteries so you can go further.
CORE Architecture
A scalable battery system architecture that helps align module design, pack integration, and production execution.
CORE Architecture
A scalable battery system architecture that helps align module design, pack integration, and production execution.
CORE Architecture
A scalable battery system architecture that helps align module design, pack integration, and production execution.
Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Operating Capacity
Production Lines
Years of Testing Data
Heading
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Heading
Through a safety-first design philosophy and rigorous validation, Acculon bridges the gap between cutting-edge chemistries and industrial-grade durability. We build tougher batteries so you can go further.

// required script in before closing </body> tag
<script>
function initCascadingSlider() {
const duration = 0.65;
const ease = 'power3.inOut';
const breakpoints = [
{ maxWidth: 479, activeWidth: 0.78, siblingWidth: 0.08 },
{ maxWidth: 767, activeWidth: 0.70, siblingWidth: 0.10 },
{ maxWidth: 991, activeWidth: 0.60, siblingWidth: 0.10 },
{ maxWidth: Infinity, activeWidth: 0.60, siblingWidth: 0.13 },
];
const wrappers = document.querySelectorAll('[data-cascading-slider-wrap]');
wrappers.forEach(setupInstance);
function setupInstance(wrapper) {
const viewport = wrapper.querySelector('[data-cascading-viewport]');
const prevButton = wrapper.querySelector('[data-cascading-slider-prev]');
const nextButton = wrapper.querySelector('[data-cascading-slider-next]');
const slides = Array.from(viewport.querySelectorAll('[data-cascading-slide]'));
let totalSlides = slides.length;
if (totalSlides === 0) return;
if (totalSlides < 9) {
const originalSlides = slides.slice();
while (slides.length < 9) {
originalSlides.forEach(function(original) {
const clone = original.cloneNode(true);
clone.setAttribute('data-clone', '');
viewport.appendChild(clone);
slides.push(clone);
});
}
totalSlides = slides.length;
}
let activeIndex = 0;
let isAnimating = false;
let slideWidth = 0;
let slotCenters = {};
let slotWidths = {};
function readGap() {
const raw = getComputedStyle(viewport).getPropertyValue('--gap').trim();
if (!raw) return 0;
const temp = document.createElement('div');
temp.style.width = raw;
temp.style.position = 'absolute';
temp.style.visibility = 'hidden';
viewport.appendChild(temp);
const px = temp.offsetWidth;
viewport.removeChild(temp);
return px;
}
function getSettings() {
const windowWidth = window.innerWidth;
for (let i = 0; i < breakpoints.length; i++) {
if (windowWidth <= breakpoints[i].maxWidth) return breakpoints[i];
}
return breakpoints[breakpoints.length - 1];
}
function getOffset(slideIndex, fromIndex) {
if (fromIndex === undefined) fromIndex = activeIndex;
let distance = slideIndex - fromIndex;
const half = totalSlides / 2;
if (distance > half) distance -= totalSlides;
if (distance < -half) distance += totalSlides;
return distance;
}
function measure() {
const settings = getSettings();
const viewportWidth = viewport.offsetWidth;
const gap = readGap();
const activeSlideWidth = viewportWidth * settings.activeWidth;
const siblingSlideWidth = viewportWidth * settings.siblingWidth;
const farSlideWidth = Math.max(0, (viewportWidth - activeSlideWidth - 2 * siblingSlideWidth - 4 * gap) / 2);
slideWidth = activeSlideWidth;
const visibleSlots = [
{ slot: -2, width: farSlideWidth },
{ slot: -1, width: siblingSlideWidth },
{ slot: 0, width: activeSlideWidth },
{ slot: 1, width: siblingSlideWidth },
{ slot: 2, width: farSlideWidth },
];
let x = 0;
visibleSlots.forEach(function(def, i) {
slotCenters[String(def.slot)] = x + def.width / 2;
slotWidths[String(def.slot)] = def.width;
if (i < visibleSlots.length - 1) x += def.width + gap;
});
slotCenters['-3'] = slotCenters['-2'] - farSlideWidth / 2 - gap - farSlideWidth / 2;
slotWidths['-3'] = farSlideWidth;
slotCenters['3'] = slotCenters['2'] + farSlideWidth / 2 + gap + farSlideWidth / 2;
slotWidths['3'] = farSlideWidth;
slides.forEach(function(slide) {
slide.style.width = slideWidth + 'px';
});
}
function getSlideProps(offset) {
const clamped = Math.max(-3, Math.min(3, offset));
const slotWidth = slotWidths[String(clamped)];
const clipAmount = Math.max(0, (slideWidth - slotWidth) / 2);
const translateX = slotCenters[String(clamped)] - slideWidth / 2;
return {
x: translateX,
'--clip': clipAmount,
zIndex: 10 - Math.abs(clamped),
};
}
function layout(animate, previousIndex) {
slides.forEach(function(slide, index) {
const offset = getOffset(index);
if (offset < -3 || offset > 3) {
if (animate && previousIndex !== undefined) {
const previousOffset = getOffset(index, previousIndex);
if (previousOffset >= -2 && previousOffset <= 2) {
const exitSlot = previousOffset < 0 ? -3 : 3;
gsap.to(slide, Object.assign({}, getSlideProps(exitSlot), {
duration: duration,
ease: ease,
overwrite: true,
}));
return;
}
}
const parkSlot = offset < 0 ? -3 : 3;
gsap.set(slide, getSlideProps(parkSlot));
return;
}
const props = getSlideProps(offset);
slide.setAttribute('data-status', offset === 0 ? 'active' : 'inactive');
if (animate) {
gsap.to(slide, Object.assign({}, props, {
duration: duration,
ease: ease,
overwrite: true,
}));
} else {
gsap.set(slide, props);
}
});
}
function goTo(targetIndex) {
const normalizedTarget = ((targetIndex % totalSlides) + totalSlides) % totalSlides;
if (isAnimating || normalizedTarget === activeIndex) return;
isAnimating = true;
const previousIndex = activeIndex;
const travelDirection = getOffset(normalizedTarget, previousIndex) > 0 ? 1 : -1;
slides.forEach(function(slide, index) {
const currentOffset = getOffset(index, previousIndex);
const nextOffset = getOffset(index, normalizedTarget);
const wasInRange = currentOffset >= -3 && currentOffset <= 3;
const willBeVisible = nextOffset >= -2 && nextOffset <= 2;
if (!wasInRange && willBeVisible) {
const entrySlot = travelDirection > 0 ? 3 : -3;
gsap.set(slide, getSlideProps(entrySlot));
}
const wasInvisible = Math.abs(currentOffset) >= 3;
const willBeStaging = Math.abs(nextOffset) === 3;
const crossesSides = currentOffset * nextOffset < 0;
if (wasInvisible && willBeStaging && crossesSides) {
gsap.set(slide, getSlideProps(nextOffset > 0 ? 3 : -3));
}
});
activeIndex = normalizedTarget;
layout(true, previousIndex);
gsap.delayedCall(duration + 0.05, function() { isAnimating = false; });
}
if (prevButton) prevButton.addEventListener('click', function() { goTo(activeIndex - 1); });
if (nextButton) nextButton.addEventListener('click', function() { goTo(activeIndex + 1); });
slides.forEach(function(slide, index) {
slide.addEventListener('click', function() {
if (index !== activeIndex) goTo(index);
});
});
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowLeft') goTo(activeIndex - 1);
if (event.key === 'ArrowRight') goTo(activeIndex + 1);
});
let resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
measure();
layout(false);
}, 100);
});
measure();
layout(false);
}
}
initCascadingSlider();
</script>// required script in <head> tag
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
// required script in before closing </body> tag
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
// required script in before closing </body> tag
<script>
function initSliderComponents() {
if (typeof Swiper === "undefined" || typeof gsap === "undefined") {
setTimeout(initSliderComponents, 50);
return;
}
document.querySelectorAll(".slider_component").forEach((component) => {
const swiperEl = component.querySelector(".swiper");
const wrapper = component.querySelector(".swiper-wrapper");
if (!swiperEl || !wrapper) return;
if (swiperEl.swiper) {
swiperEl.swiper.destroy(true, true);
}
wrapper.querySelectorAll("[data-slider-clone='true']").forEach((slide) => {
slide.remove();
});
const originalSlides = Array.from(wrapper.children).filter((slide) => {
return slide.classList.contains("swiper-slide");
});
if (originalSlides.length < 2) return;
const cloneSets = 3;
const slideCount = originalSlides.length;
const middleStart = slideCount * cloneSets;
let activeDotIndex = null;
originalSlides.forEach((slide, index) => {
slide.dataset.sliderIndex = index;
});
for (let i = 0; i < cloneSets; i++) {
originalSlides.forEach((slide, index) => {
const clone = slide.cloneNode(true);
clone.dataset.sliderClone = "true";
clone.dataset.sliderIndex = index;
wrapper.appendChild(clone);
});
}
for (let i = 0; i < cloneSets; i++) {
[...originalSlides].reverse().forEach((slide, reverseIndex) => {
const index = slideCount - 1 - reverseIndex;
const clone = slide.cloneNode(true);
clone.dataset.sliderClone = "true";
clone.dataset.sliderIndex = index;
wrapper.insertBefore(clone, wrapper.firstChild);
});
}
component.querySelectorAll(".slide-dot-matrix").forEach((svg) => {
const dots = Array.from(svg.querySelectorAll("circle"));
const sortedDots = [...dots].sort((a, b) => {
return getDotSortValue(a) - getDotSortValue(b);
});
svg.dotMatrixDots = dots;
svg.dotMatrixSortedDots = sortedDots;
svg.dotMatrixTimeline = null;
gsap.set(svg, {
autoAlpha: 0
});
gsap.set(dots, {
autoAlpha: 0
});
});
const swiper = new Swiper(swiperEl, {
slidesPerView: 3.5,
slidesPerGroup: 1,
spaceBetween: 2,
speed: 900,
initialSlide: middleStart,
centeredSlides: true,
slideActiveClass: "is-active",
loop: false,
rewind: false,
allowTouchMove: true,
simulateTouch: true,
followFinger: true,
grabCursor: true,
slideToClickedSlide: true,
keyboard: {
enabled: true,
onlyInViewport: true
},
observer: true,
observeParents: true,
resizeObserver: true,
breakpoints: {
0: {
slidesPerView: 1.5
},
768: {
slidesPerView: 2.25
},
992: {
slidesPerView: 3.5
}
},
on: {
init(swiper) {
swiper.slideTo(middleStart, 0, false);
syncDotMatrices(swiper, true);
},
slideChangeTransitionStart(swiper) {
syncDotMatrices(swiper, true);
},
slideChangeTransitionEnd(swiper) {
resetSliderToMiddle(swiper);
syncDotMatrices(swiper, false);
},
resize(swiper) {
swiper.update();
resetSliderToMiddle(swiper);
syncDotMatrices(swiper, false);
}
}
});
requestAnimationFrame(() => {
swiper.update();
swiper.slideTo(middleStart, 0, false);
syncDotMatrices(swiper, false);
});
function syncDotMatrices(swiper, shouldAnimate) {
const activeSlide = swiper.slides[swiper.activeIndex];
if (!activeSlide) return;
const nextDotIndex = activeSlide.dataset.sliderIndex;
if (shouldAnimate && nextDotIndex === activeDotIndex) return;
activeDotIndex = nextDotIndex;
swiper.slides.forEach((slide) => {
const matrix = slide.querySelector(".slide-dot-matrix");
if (!matrix) return;
if (slide === activeSlide) {
showDotMatrix(matrix, shouldAnimate);
} else {
hideDotMatrix(matrix);
}
});
}
function showDotMatrix(matrix, shouldAnimate) {
const dots = matrix.dotMatrixDots || [];
const sortedDots = matrix.dotMatrixSortedDots || [];
if (matrix.dotMatrixTimeline) {
matrix.dotMatrixTimeline.kill();
}
gsap.killTweensOf(matrix);
gsap.killTweensOf(dots);
if (!shouldAnimate) {
gsap.set(matrix, {
autoAlpha: 1
});
gsap.set(dots, {
autoAlpha: 1
});
return;
}
matrix.dotMatrixTimeline = gsap.timeline();
matrix.dotMatrixTimeline.set(matrix, {
autoAlpha: 1
});
matrix.dotMatrixTimeline.fromTo(
sortedDots,
{
autoAlpha: 0
},
{
autoAlpha: 1,
duration: 0.28,
ease: "power1.out",
stagger: {
each: 0.004,
from: "start"
}
}
);
}
function hideDotMatrix(matrix) {
const dots = matrix.dotMatrixDots || [];
if (matrix.dotMatrixTimeline) {
matrix.dotMatrixTimeline.kill();
}
gsap.killTweensOf(matrix);
gsap.killTweensOf(dots);
gsap.set(matrix, {
autoAlpha: 0
});
gsap.set(dots, {
autoAlpha: 0
});
}
function resetSliderToMiddle(swiper) {
const activeSlide = swiper.slides[swiper.activeIndex];
if (!activeSlide) return;
const realIndex = Number(activeSlide.dataset.sliderIndex);
const targetIndex = middleStart + realIndex;
if (swiper.activeIndex < middleStart || swiper.activeIndex >= middleStart + slideCount) {
swiper.slideTo(targetIndex, 0, false);
}
}
function getDotSortValue(dot) {
const x = Number(dot.getAttribute("cx"));
const y = Number(dot.getAttribute("cy"));
return (x + y) * 100 + y;
}
});
}
initSliderComponents();
</script>Cell Strategy
Cell selection sets the technical foundation for the full system. Acculon uses testing, modeling, and application requirements to narrow the chemistry and performance choices that matter.
Module Architecture
At the module level, CORE architecture translates cell decisions into a repeatable building block with thermal isolation, cell-to-cell fusing, and adaptable configuration logic.
Pack Integration
Pack design brings power, energy, voltage, packaging, and operating environment into one manufacturable form factor that can move beyond prototype thinking.
System Intelligence
BMS controls, telemetry, SOC/SOH insight, and certification pathways complete the system story, turning the battery into a connected product platform.
// required script in before closing </body> tag
function initVideoScroll() {
var ROOT_SELECTOR = "[data-video-scroll-init]";
var ELEMENT_SELECTOR = "[data-video-scroll-element]";
var MOBILE_MAX = 767;
var roots = Array.prototype.slice
.call(document.querySelectorAll(ROOT_SELECTOR))
.filter(function (root) {
return !!root.querySelector(ELEMENT_SELECTOR);
});
roots.forEach(initInstance);
function initInstance(player, playerIndex) {
destroyPreviousInstance(player);
var slot =
player.querySelector(".technology-slot") ||
player.querySelector("[data-scroll-element-slot]") ||
player.querySelector("[data-video-scroll-slot]") ||
player;
var container =
(player.classList && player.classList.contains("technology-container") && player) ||
player.querySelector(".technology-container") ||
(slot && slot.closest(".technology-container")) ||
player;
var scrollElements = Array.prototype.slice.call(
slot.querySelectorAll(ELEMENT_SELECTOR)
);
if (!scrollElements.length) return;
var mediaShell =
player.querySelector(".technology-video [data-player-src]") ||
player.querySelector("[data-video-scroll-media]") ||
player.querySelector("[data-player-src]") ||
player;
var video =
mediaShell.querySelector("video") ||
player.querySelector(".technology-video video") ||
player.querySelector("video");
if (!video) {
console.warn("[VideoScroll] No video found inside:", player);
return;
}
var src =
mediaShell.getAttribute("data-player-src") ||
video.getAttribute("data-player-src") ||
video.getAttribute("data-src") ||
video.getAttribute("src") ||
getChildSource(mediaShell) ||
getChildSource(player);
if (!src) {
console.warn("[VideoScroll] No video source found:", player);
return;
}
var SCRUB = parseFloat(player.getAttribute("data-video-scroll-scrub"));
if (isNaN(SCRUB)) SCRUB = 0.35;
var SEEK_FPS = parseFloat(player.getAttribute("data-video-scroll-seek-fps"));
if (isNaN(SEEK_FPS)) SEEK_FPS = 16;
var SEEK_THROTTLE_MS = 1000 / SEEK_FPS;
var SEEK_THRESHOLD = parseFloat(player.getAttribute("data-video-scroll-threshold"));
if (isNaN(SEEK_THRESHOLD)) SEEK_THRESHOLD = 1 / 45;
var FAST_SEEK_DISTANCE = parseFloat(
player.getAttribute("data-video-scroll-fast-seek-distance")
);
if (isNaN(FAST_SEEK_DISTANCE)) FAST_SEEK_DISTANCE = 1.25;
var END_PADDING = parseFloat(player.getAttribute("data-video-scroll-end-padding"));
if (isNaN(END_PADDING)) END_PADDING = 0.05;
var PRELOAD_MARGIN =
player.getAttribute("data-video-scroll-preload-margin") || "150% 0px";
var DESKTOP_START =
player.getAttribute("data-video-scroll-desktop-start") ||
player.getAttribute("data-video-scroll-global-start") ||
"top top";
var DESKTOP_END =
player.getAttribute("data-video-scroll-desktop-end") ||
player.getAttribute("data-video-scroll-global-end") ||
"bottom bottom";
var MOBILE_START =
player.getAttribute("data-video-scroll-mobile-start") ||
"top top";
var MOBILE_END =
player.getAttribute("data-video-scroll-mobile-end") ||
"bottom bottom";
var isHls = /\.m3u8(\?|$)/i.test(src);
var isSafariNative = !!video.canPlayType("application/vnd.apple.mpegurl");
var canUseHlsJs = !!(window.Hls && window.Hls.isSupported()) && !isSafariNative;
var hls = null;
var activeProgressTrigger = null;
var setupComplete = false;
var duration = 0;
var hasMetadata = false;
var isAttached = false;
var isSeeking = false;
var rafId = 0;
var lastSeekAt = 0;
var lastAppliedTime = NaN;
var pendingTime = null;
var currentProgress = 0;
var seekTimeout = null;
var preloadObserver = null;
player._videoScrollCleanup = [];
setStatus("idle");
setActivated(false);
prepareVideoElement();
waitForDeps(function () {
setupResponsiveTriggers();
setupMediaPreload();
syncToCurrentScroll(true);
window.addEventListener("load", handleLateSync, { once: true });
window.addEventListener("pageshow", handleLateSync);
window.addEventListener("orientationchange", handleLateSync);
var debouncedLateSync = debounce(handleLateSync, 120);
window.addEventListener("resize", debouncedLateSync);
player._videoScrollCleanup.push(function () {
window.removeEventListener("pageshow", handleLateSync);
window.removeEventListener("orientationchange", handleLateSync);
window.removeEventListener("resize", debouncedLateSync);
});
});
function prepareVideoElement() {
video.muted = true;
video.defaultMuted = true;
video.setAttribute("muted", "");
video.setAttribute("playsinline", "");
video.setAttribute("webkit-playsinline", "");
video.playsInline = true;
video.preload = "auto";
video.autoplay = false;
video.loop = false;
video.controls = false;
if ("disableRemotePlayback" in video) video.disableRemotePlayback = true;
video.addEventListener("loadedmetadata", handleMetadata);
video.addEventListener("durationchange", handleMetadata);
video.addEventListener("seeking", function () {
isSeeking = true;
});
video.addEventListener("seeked", function () {
isSeeking = false;
clearSeekTimeout();
if (pendingTime != null) scheduleSeek(true);
});
video.addEventListener("canplay", function () {
if (hasMetadata) {
setActivated(true);
setStatus(isInScrubRange(currentProgress) ? "scrubbing" : "ready");
}
});
video.addEventListener("error", function () {
setStatus("error");
});
}
function setupMediaPreload() {
if (!("IntersectionObserver" in window)) {
attachMediaOnce();
return;
}
preloadObserver = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
attachMediaOnce();
if (preloadObserver) preloadObserver.disconnect();
preloadObserver = null;
}
});
}, { root: null, rootMargin: PRELOAD_MARGIN, threshold: 0 }
);
preloadObserver.observe(player);
player._videoScrollCleanup.push(function () {
if (preloadObserver) preloadObserver.disconnect();
preloadObserver = null;
});
if (isElementNearViewport(player, 1.5)) attachMediaOnce();
}
function attachMediaOnce() {
if (isAttached) return;
isAttached = true;
setStatus("loading");
try {
video.pause();
video.removeAttribute("src");
video.load();
} catch (_) {}
if (isHls && canUseHlsJs) {
hls = new Hls({
autoStartLoad: true,
startFragPrefetch: true,
lowLatencyMode: false,
enableWorker: true,
capLevelToPlayerSize: true,
maxBufferLength: 24,
maxMaxBufferLength: 60,
backBufferLength: 12,
maxBufferSize: 80 * 1000 * 1000,
abrEwmaDefaultEstimate: 3500000
});
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
hls.loadSource(src);
});
hls.on(Hls.Events.MANIFEST_PARSED, function (_event, data) {
var cap = chooseLevelCap(data && data.levels ? data.levels : []);
if (cap > -1) hls.autoLevelCapping = cap;
syncToCurrentScroll(true);
});
hls.on(Hls.Events.ERROR, function (_event, data) {
if (data && data.fatal) {
console.warn("[VideoScroll] HLS fatal error:", data);
setStatus("error");
}
});
player._hls = hls;
} else {
video.src = src;
video.load();
}
}
function handleMetadata() {
if (!video.duration || isNaN(video.duration) || video.duration === Infinity) return;
duration = video.duration;
hasMetadata = true;
syncToCurrentScroll(true);
warmDecoder();
setActivated(true);
setStatus(isInScrubRange(currentProgress) ? "scrubbing" : "ready");
}
function setupResponsiveTriggers() {
if (setupComplete) return;
setupComplete = true;
gsap.registerPlugin(ScrollTrigger);
var mm = gsap.matchMedia();
player._mm = mm;
mm.add("(min-width: " + (MOBILE_MAX + 1) + "px)", function () {
var firstElement = scrollElements[0];
var lastElement = scrollElements[scrollElements.length - 1];
var trigger = ScrollTrigger.create({
trigger: slot,
startTrigger: firstElement,
start: DESKTOP_START,
endTrigger: lastElement,
end: DESKTOP_END,
invalidateOnRefresh: true,
onUpdate: function (self) {
updateProgress(self.progress, false);
},
onRefresh: function (self) {
updateProgress(self.progress, true);
},
onEnter: function () {
setStatus("scrubbing");
},
onEnterBack: function () {
setStatus("scrubbing");
},
onLeave: function () {
updateProgress(1, true);
setStatus("ready");
},
onLeaveBack: function () {
updateProgress(0, true);
setStatus("ready");
}
});
activeProgressTrigger = trigger;
player._videoScrollTriggers = [trigger];
requestSyncs();
return function cleanup() {
try {
trigger.kill();
} catch (_) {}
if (activeProgressTrigger === trigger) activeProgressTrigger = null;
};
});
mm.add("(max-width: " + MOBILE_MAX + "px)", function () {
gsap.set(slot, { willChange: "transform" });
var slotTween = gsap.to(slot, {
xPercent: -100,
x: function () {
return window.innerWidth - 4;
},
ease: "none",
scrollTrigger: {
trigger: container,
start: MOBILE_START,
end: MOBILE_END,
scrub: true,
invalidateOnRefresh: true
}
});
var trigger = ScrollTrigger.create({
trigger: container,
start: MOBILE_START,
end: MOBILE_END,
invalidateOnRefresh: true,
onUpdate: function (self) {
updateProgress(self.progress, false);
},
onRefresh: function (self) {
updateProgress(self.progress, true);
},
onEnter: function () {
setStatus("scrubbing");
},
onEnterBack: function () {
setStatus("scrubbing");
},
onLeave: function () {
updateProgress(1, true);
setStatus("ready");
},
onLeaveBack: function () {
updateProgress(0, true);
setStatus("ready");
}
});
activeProgressTrigger = trigger;
player._videoScrollTriggers = [trigger];
requestSyncs();
return function cleanup() {
try {
if (slotTween.scrollTrigger) slotTween.scrollTrigger.kill();
slotTween.kill();
} catch (_) {}
try {
trigger.kill();
} catch (_) {}
if (activeProgressTrigger === trigger) activeProgressTrigger = null;
gsap.set(slot, { clearProps: "transform,willChange" });
};
});
ScrollTrigger.addEventListener("refresh", syncAfterRefresh);
player._videoScrollCleanup.push(function () {
ScrollTrigger.removeEventListener("refresh", syncAfterRefresh);
});
ScrollTrigger.refresh();
}
function updateProgress(progress, force) {
currentProgress = clamp(progress, 0, 1);
if (!hasMetadata) {
pendingTime = null;
return;
}
var next = progressToTime(currentProgress);
requestSeek(next, force);
if (isInScrubRange(currentProgress)) {
setStatus("scrubbing");
}
}
function progressToTime(progress) {
if (!duration) return 0;
var count = Math.max(scrollElements.length, 1);
var p = clamp(progress, 0, 1);
if (p <= 0.001) return 0.001;
if (p >= 0.999) return getEndTime();
var rawChunk = p * count;
var index = Math.min(count - 1, Math.floor(rawChunk));
var local = rawChunk - index;
var chunkStart = (duration * index) / count;
var chunkEnd = (duration * (index + 1)) / count;
return clamp(chunkStart + (chunkEnd - chunkStart) * local, 0.001, getEndTime());
}
function requestSeek(time, force) {
pendingTime = clamp(time, 0.001, getEndTime());
scheduleSeek(!!force);
}
function scheduleSeek(force) {
if (rafId) return;
rafId = requestAnimationFrame(function () {
rafId = 0;
flushSeek(!!force);
});
}
function flushSeek(force) {
if (pendingTime == null || !hasMetadata) return;
var now = performance.now();
if (!force && now - lastSeekAt < SEEK_THROTTLE_MS) {
scheduleSeek(false);
return;
}
if (!force && isSeeking) {
scheduleSeek(false);
return;
}
var next = pendingTime;
var current = isFinite(video.currentTime) ? video.currentTime : 0;
if (!force && isFinite(lastAppliedTime)) {
if (Math.abs(next - lastAppliedTime) < SEEK_THRESHOLD) {
pendingTime = null;
return;
}
}
pendingTime = null;
lastSeekAt = now;
lastAppliedTime = next;
try {
var distance = Math.abs(next - current);
if (
distance >= FAST_SEEK_DISTANCE &&
typeof video.fastSeek === "function"
) {
video.fastSeek(next);
} else {
video.currentTime = next;
}
isSeeking = true;
clearSeekTimeout();
seekTimeout = setTimeout(function () {
isSeeking = false;
seekTimeout = null;
if (pendingTime != null) scheduleSeek(true);
}, 350);
} catch (_) {
isSeeking = false;
}
}
function syncToCurrentScroll(force) {
if (activeProgressTrigger) {
updateProgress(activeProgressTrigger.progress || 0, !!force);
return;
}
var rect = container.getBoundingClientRect();
var viewport = window.innerHeight || document.documentElement.clientHeight;
var total = rect.height - viewport;
if (total <= 0) {
updateProgress(rect.top <= 0 ? 1 : 0, !!force);
return;
}
var progress = clamp((0 - rect.top) / total, 0, 1);
updateProgress(progress, !!force);
}
function syncAfterRefresh() {
syncToCurrentScroll(true);
}
function handleLateSync() {
requestSyncs();
}
function requestSyncs() {
syncToCurrentScroll(true);
requestAnimationFrame(function () {
syncToCurrentScroll(true);
});
setTimeout(function () {
syncToCurrentScroll(true);
}, 80);
setTimeout(function () {
syncToCurrentScroll(true);
}, 250);
}
function warmDecoder() {
if (!hasMetadata) return;
var playPromise;
try {
playPromise = video.play();
} catch (_) {
return;
}
var pauseAndSync = function () {
try {
video.pause();
} catch (_) {}
syncToCurrentScroll(true);
};
if (playPromise && typeof playPromise.then === "function") {
playPromise.then(pauseAndSync).catch(pauseAndSync);
} else {
pauseAndSync();
}
}
function chooseLevelCap(levels) {
if (!levels || !levels.length) return -1;
var rect = video.getBoundingClientRect();
var targetWidth =
(rect.width || video.clientWidth || video.width || 1280) *
Math.min(window.devicePixelRatio || 1, 2);
var targetHeight =
(rect.height || video.clientHeight || video.height || 720) *
Math.min(window.devicePixelRatio || 1, 2);
var best = levels.length - 1;
for (var i = 0; i < levels.length; i++) {
var level = levels[i];
if (!level) continue;
if (
(!level.width || level.width >= targetWidth) &&
(!level.height || level.height >= targetHeight)
) {
best = i;
break;
}
}
return best;
}
function getEndTime() {
if (!duration) return 0.001;
return Math.max(0.001, duration - END_PADDING);
}
function setStatus(status) {
player.setAttribute("data-player-status", status);
if (mediaShell && mediaShell.setAttribute) {
mediaShell.setAttribute("data-player-status", status);
}
}
function setActivated(value) {
player.setAttribute("data-player-activated", value ? "true" : "false");
if (mediaShell && mediaShell.setAttribute) {
mediaShell.setAttribute("data-player-activated", value ? "true" : "false");
}
}
function isInScrubRange(progress) {
return progress > 0.001 && progress < 0.999;
}
function clearSeekTimeout() {
if (seekTimeout) {
clearTimeout(seekTimeout);
seekTimeout = null;
}
}
}
function destroyPreviousInstance(player) {
if (player._videoScrollCleanup) {
player._videoScrollCleanup.forEach(function (cleanup) {
try {
cleanup();
} catch (_) {}
});
}
if (player._mm) {
try {
player._mm.revert();
} catch (_) {}
player._mm = null;
}
if (player._videoScrollTriggers) {
player._videoScrollTriggers.forEach(function (trigger) {
try {
trigger.kill();
} catch (_) {}
});
player._videoScrollTriggers = null;
}
if (player._hls) {
try {
player._hls.destroy();
} catch (_) {}
player._hls = null;
}
player._videoScrollCleanup = [];
}
function getChildSource(root) {
var el =
root.querySelector("[data-player-src]") ||
root.querySelector("source[src]");
if (!el) return "";
return el.getAttribute("data-player-src") || el.getAttribute("src") || "";
}
function waitForDeps(callback) {
var attempts = 0;
(function check() {
attempts++;
if (window.gsap && window.ScrollTrigger) {
callback();
return;
}
if (attempts >= 80) {
console.warn("[VideoScroll] GSAP or ScrollTrigger not found.");
return;
}
setTimeout(check, 50);
})();
}
function isElementNearViewport(el, multiplier) {
var rect = el.getBoundingClientRect();
var vh = window.innerHeight || document.documentElement.clientHeight;
var margin = vh * multiplier;
return rect.bottom >= -margin && rect.top <= vh + margin;
}
function debounce(fn, wait) {
var timeout = null;
return function () {
clearTimeout(timeout);
timeout = setTimeout(fn, wait);
};
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
}
initVideoScroll();



