@six-socks-studio/image-sequence-engine
Version:
A performant scroll-based image sequence animation engine
3 lines (2 loc) • 4.32 kB
JavaScript
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).ImageSequenceEngine={})}(this,(function(t){"use strict";t.ImageSequenceEngine=class{constructor(t={}){if(this.options={container:null,imageUrls:[],initialLoadComplete:!1,onError:t=>console.error(t),...t},!this.options.container)throw new Error("Container element is required");if(!Array.isArray(this.options.imageUrls)||0===this.options.imageUrls.length)throw new Error("Image URLs array is required and must not be empty");this.totalFrames=this.options.imageUrls.length,this.images=new Map,this.loadingPromises=new Map,this.currentFrame=0,this.isScrollLocked=!0,this.progress=0,this.init()}init(){this.canvas=document.createElement("canvas"),this.ctx=this.canvas.getContext("2d"),this.options.container.appendChild(this.canvas),this.setupCanvas(),this.startInitialLoading()}setupCanvas(){const t=()=>{const t=this.options.container.getBoundingClientRect(),i=window.devicePixelRatio||1;this.canvas.width=t.width*i,this.canvas.height=t.height*i,this.canvas.style.width=`${t.width}px`,this.canvas.style.height=`${t.height}px`,this.ctx.scale(i,i)};t(),window.addEventListener("resize",t)}async startInitialLoading(){try{await this.loadImage(this.options.imageUrls[0]);const t=[0].concat(Array.from({length:Math.floor(this.totalFrames/8)},((t,i)=>8*(i+1))));await this.loadFrames(t),this.unlockScroll(),this.startSecondaryLoading()}catch(t){this.options.onError(t)}}loadImage(t){if(!t)return Promise.reject(new Error("Invalid image URL"));if(this.loadingPromises.has(t))return this.loadingPromises.get(t);if(this.images.has(t))return Promise.resolve(this.images.get(t));const i=new Promise(((i,e)=>{const s=new Image;s.onload=()=>{this.images.set(t,s),this.loadingPromises.delete(t),this.emit("imageLoaded",{url:t,loaded:this.images.size,total:this.totalFrames}),i(s)},s.onerror=()=>{this.loadingPromises.delete(t);const i=new Error(`Failed to load image: ${t}`);this.options.onError(i),e(i)},s.src=t}));return this.loadingPromises.set(t,i),i}setupCanvas(){const t=()=>{const t=this.options.container.getBoundingClientRect(),i=window.devicePixelRatio||1;this.canvas.width=t.width*i,this.canvas.height=t.height*i,this.canvas.style.width=`${t.width}px`,this.canvas.style.height=`${t.height}px`,this.ctx.scale(i,i)};t(),window.addEventListener("resize",t)}async startSecondaryLoading(){const t=Array.from({length:Math.floor(this.totalFrames/4)},((t,i)=>4*(i+1))).filter((t=>!this.images.has(this.options.imageUrls[t])));this.loadFrames(t).then((()=>{const t=Array.from({length:this.totalFrames},((t,i)=>i)).filter((t=>!this.images.has(this.options.imageUrls[t])));this.loadFrames(t)}))}unlockScroll(){this.isScrollLocked=!1,this.setupScrollListener(),this.emit("readyToScroll")}setupScrollListener(){const t=()=>{const t=this.options.container.getBoundingClientRect(),i=this.options.container.scrollHeight-window.innerHeight,e=window.scrollY-t.top;this.progress=Math.max(0,Math.min(1,e/i)),this.render()};new IntersectionObserver((i=>{i.forEach((i=>{i.isIntersecting?window.addEventListener("scroll",t):window.removeEventListener("scroll",t)}))})).observe(this.options.container)}async loadFrames(t){const i=t.map((t=>{const i=this.options.imageUrls[t];return this.loadImage(i)}));await Promise.all(i),this.emit("batchLoaded",{loaded:this.images.size,total:this.totalFrames})}render(){if(this.isScrollLocked)return;const t=Math.floor(this.progress*(this.totalFrames-1)),i=this.options.imageUrls[t],e=this.images.get(i);e&&t!==this.currentFrame&&(this.currentFrame=t,this.drawImage(e))}drawImage(t){const i=this.canvas,e=this.ctx,s=this.options.container.getBoundingClientRect();e.clearRect(0,0,i.width,i.height);const o=s.width/s.height,n=t.width/t.height;let a=s.width,r=s.height;o>n?r=a/n:a=r*n;const h=(s.width-a)/2,l=(s.height-r)/2;e.drawImage(t,h,l,a,r)}emit(t,i){const e=new CustomEvent(`imageSequence:${t}`,{detail:i});this.options.container.dispatchEvent(e)}getLoadingProgress(){return{loaded:this.images.size,total:this.totalFrames,percentage:this.images.size/this.totalFrames*100}}destroy(){this.canvas.remove(),this.images.clear(),this.loadingPromises.clear()}}}));
//# sourceMappingURL=image-sequence-engine.umd.min.js.map