When OPTIKKA—a inventive orchestration platform remodeling conventional design workflows into clever, extensible techniques—got here to us at Zajno, we rapidly outlined a core visible metaphor: a dynamic, visually wealthy file system that expands as you scroll. All through design and improvement, we explored a number of iterations to make sure the web site’s central animation was not solely hanging but additionally seamless and constant throughout all gadgets.
On this article, we’ll clarify why we moved away from utilizing HTML5 video for scroll-synchronized animation and supply an in depth information on creating related animations utilizing body sequences.
The Preliminary Strategy: HTML5 Video
Why It Appeared Promising
Our first concept was to make use of HTML5 video for the scroll-triggered animation, paired with GSAP’s ScrollTrigger plugin for scroll monitoring. The method had clear benefits:
// Preliminary method with video component
export default class VideoScene extends Part {
non-public video: HTMLVideoElement;
non-public scrollTrigger: ScrollTrigger;
setupVideoScroll() {
this.scrollTrigger = ScrollTrigger.create({
set off: '.video-container',
begin: 'high high',
finish: 'backside backside',
scrub: true,
onUpdate: (self) => {
// Synchronize video time with scroll progress
const length = this.video.length;
this.video.currentTime = self.progress * length;
},
});
}
}
- Simplicity: Browsers help video playback natively.
- Compactness: One video file as a substitute of tons of of photographs.
- Compression: Video codecs effectively scale back file dimension.
In actuality, this method had vital drawbacks:
- Stuttering and lag, particularly on cellular gadgets.
- Autoplay restrictions in lots of browsers.
- Lack of visible constancy as a consequence of compression.
These points motivated a shift towards a extra controllable and dependable answer.
Transition to Body Sequences
What Is a Body Sequence?
A body sequence consists of particular person photographs performed quickly to create the phantasm of movement—very like a movie at 24 frames per second. This technique permits exact management over animation timing and high quality.
Extracting Frames from Video
We used FFmpeg to transform movies into particular person frames after which into optimized internet codecs:
- Take the supply video.
- Cut up it into particular person PNG frames.
- Convert PNGs into WebP to scale back file dimension.
// Extract frames as PNG sequence
console.log('🎬 Extracting PNG frames...');
await execPromise(`ffmpeg -i "video/${videoFile}" -vf "fps=30" "png/frame_percent03d.png"`);
// Convert PNG sequence to WebP
console.log('🔄 Changing to WebP sequence...');
await execPromise(`ffmpeg -i "png/frame_percent03d.png" -c:v libwebp -quality 80 "webp/frame_percent03d.webp"`);
console.log('✅ Processing full!');
Gadget-Particular Sequences
To optimize efficiency throughout gadgets, we created at the least two units of sequences for various facet ratios:
- Desktop: Increased body rely for smoother animation.
- Cell: Decrease body rely for quicker loading and effectivity.
// New picture sequence primarily based structure
export default summary class Scene extends Part {
non-public _canvas: HTMLCanvasElement;
non-public _ctx: CanvasRenderingContext2D;
non-public _frameImages: Map = new Map();
non-public _currentFrame: { contents: quantity } = { contents: 1 };
// Gadget-specific body configuration
non-public static readonly totalFrames: Document = {
[BreakpointType.Desktop]: 1182,
[BreakpointType.Tablet]: 880,
[BreakpointType.Mobile]: 880,
};
// Offset for video finish primarily based on gadget kind
non-public static readonly offsetVideoEnd: Document = {
[BreakpointType.Desktop]: 1500,
[BreakpointType.Tablet]: 1500,
[BreakpointType.Mobile]: 1800,
};
}
We additionally carried out dynamic path decision to load the proper picture sequence relying on the consumer’s gadget kind.
// Dynamic path primarily based on present breakpoint
img.src = `/${this._currentBreakpointType.toLowerCase()}/frame_${paddedNumber}.webp`;
Clever Body Loading System
The Problem
Loading 1,000+ photographs with out blocking the UI or consuming extreme bandwidth is difficult. Customers count on instantaneous animation, however heavy picture sequences can decelerate the positioning.
Stepwise Loading Answer
We carried out a staged loading system:
- Instant begin: Load the primary 10 frames immediately.
- First-frame show: Customers see animation instantly.
- Background loading: Remaining frames load seamlessly within the background.
await this.preloadFrames(1, countPreloadFrames);
this.renderFrame(1);
this.loadFramesToHash();
Parallel Background Loading
Utilizing a ParallelQueue system, we:
- Load remaining frames effectively with out blocking the UI.
- Begin from an outlined countPreloadFrames to keep away from redundancy.
- Cache every loaded body routinely for efficiency.
// Background loading of all frames utilizing parallel queue
non-public loadFramesToHash() {
const queue = new ParallelQueue();
for (let i = countPreloadFrames; i <= totalFrames[this._currentBreakpointType]; i++) {
queue.enqueue(async () => {
const img = await this.loadFrame(i);
this._frameImages.set(i, img);
});
}
queue.begin();
}
Rendering with Canvas
Why Canvas
Rendering frames in an HTML component supplied a number of advantages:
- Instantaneous rendering: Frames load into reminiscence for instant show.
- No DOM reflow: Avoids repainting the web page.
- Optimized animation: Works easily with requestAnimationFrame.
// Canvas rendering with correct scaling and positioning
non-public renderFrame(frameNumber: quantity) {
const img = this._frameImages.get(frameNumber);
if (img && this._ctx) {
// Clear earlier body
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.peak);
// Deal with excessive DPI shows
const pixelRatio = window.devicePixelRatio || 1;
const canvasRatio = this._canvas.width / this._canvas.peak;
const imageRatio = img.width / img.peak;
// Calculate dimensions for object-fit: cowl habits
let drawWidth = this._canvas.width;
let drawHeight = this._canvas.peak;
let offsetX = 0;
let offsetY = 0;
if (canvasRatio > imageRatio) {
// Canvas is wider than picture
drawWidth = this._canvas.width;
drawHeight = this._canvas.width / imageRatio;
} else {
// Canvas is taller than picture
drawHeight = this._canvas.peak;
drawWidth = this._canvas.peak * imageRatio;
offsetX = (this._canvas.width - drawWidth) / 2;
}
// Draw picture with correct scaling for prime DPI
this._ctx.drawImage(img, offsetX, offsetY, drawWidth / pixelRatio, drawHeight / pixelRatio);
}
}
Limitations of
Components
Whereas doable, utilizing for body sequences presents points:
- Restricted management over scaling.
- Synchronization issues throughout fast body modifications.
- Flickering and inconsistent cross-browser rendering.
// Auto-playing loop animation on the high of the web page
non-public async playLoop() {
if (!this.isLooping) return;
const startTime = Date.now();
const animate = () => {
if (!this.isLooping) return;
// Calculate present progress inside loop length
const elapsed = (Date.now() - startTime) % (this.loopDuration * 1000);
const progress = elapsed / (this.loopDuration * 1000);
// Map progress to border quantity
const body = Math.spherical(this.loopStartFrame + progress * this.framesPerLoop);
if (body !== this._currentFrame.contents) {
this._currentFrame.contents = body;
this.renderFrame(this._currentFrame.contents);
}
requestAnimationFrame(animate);
};
// Preload loop frames earlier than beginning animation
await this.preloadFrames(this.loopStartFrame, this.loopEndFrame);
animate();
}
Loop Animation at Web page Begin
Canvas additionally allowed us to implement looping animations at the beginning of the web page with seamless transitions to scroll-triggered frames utilizing GSAP.
// Easy transition between loop and scroll-based animation
// Background loading of all frames utilizing parallel queue
non-public handleScrollTransition(scrollProgress: quantity) {
if (this.isLooping && scrollProgress > 0) {
// Transition from loop to scroll-based animation
this.isLooping = false;
gsap.to(this._currentFrame, {
length: this.transitionDuration,
contents: this.framesPerLoop - this.transitionStartScrollOffset,
ease: 'power2.inOut',
onComplete: () => (this.isLooping = false),
});
} else if (!this.isLooping && scrollProgress === 0) {
// Transition again to loop animation
this.preloadFrames(this.loopStartFrame, this.loopEndFrame);
this.isLooping = true;
this.playLoop();
}
}
Efficiency Optimizations
Dynamic Preloading Primarily based on Scroll Course
We enhanced smoothness by preloading frames dynamically in line with scroll motion:
- Scroll down: Preload 5 frames forward.
- Scroll up: Preload 5 frames behind.
- Optimized vary: Solely load obligatory frames.
- Synchronized rendering: Preloading occurs in sync with the present body show.
// Sensible preloading primarily based on scroll route
_containerSequenceUpdate = async (self: ScrollTrigger) => {
const currentScroll = window.scrollY;
const isScrollingUp = currentScroll < this.lastScrollPosition;
this.lastScrollPosition = currentScroll;
// Calculate adjusted progress with finish offset
const totalHeight = doc.documentElement.scrollHeight - window.innerHeight;
const adjustedProgress = Math.min(1, currentScroll / (totalHeight - offsetVideoEnd[this._currentBreakpointType]));
// Deal with transition between states
this.handleScrollTransition(self.progress);
if (!this.isLooping) {
const body = Math.spherical(adjustedProgress * totalFrames[this._currentBreakpointType]);
if (body !== this._currentFrame.contents) {
this._currentFrame.contents = body;
// Preload frames in scroll route
const preloadAmount = 5;
await this.preloadFrames(
body + (isScrollingUp ? -preloadAmount : 1),
body + (isScrollingUp ? -1 : preloadAmount)
);
this.renderFrame(body);
}
}
};
Outcomes of the Transition
Advantages
- Steady efficiency throughout gadgets.
- Predictable reminiscence utilization.
- No playback stuttering.
- Cross-platform consistency.
- Autoplay flexibility.
- Exact management over every body.
Technical Commerce-offs
- Elevated bandwidth as a consequence of a number of requests.
- Bigger total information dimension.
- Increased implementation complexity with caching and preloading logic.
Conclusion
Switching from video to border sequences for OPTIKKA demonstrated the significance of selecting the best expertise for the duty. Regardless of added complexity, the brand new method supplied:
- Dependable efficiency throughout gadgets.
- Constant, clean animation.
- Fantastic-grained management for numerous situations.
Typically, a extra technically complicated answer is justified if it delivers a greater consumer expertise.









