Skip to content

Instantly share code, notes, and snippets.

@ijurko
Created September 12, 2024 09:27
Show Gist options
  • Save ijurko/5b3cb6dd2fc69199bdfc7eda76b19a86 to your computer and use it in GitHub Desktop.
Save ijurko/5b3cb6dd2fc69199bdfc7eda76b19a86 to your computer and use it in GitHub Desktop.
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export default class ImageSequence {
constructor() {
this.DOM = {
chapterNavigation: ".js-chapter-navigation-wrapper",
sequence: ".js-image-sequence",
sequenceWrapper: ".js-image-sequence-wrapper",
canvasWrapper: ".js-image-sequence-canvas-wrapper",
step: ".js-sequence-step",
};
}
init() {
this.sequenceWrapper = document.querySelector(this.DOM.sequenceWrapper);
this.chapterNavigation = document.querySelector(this.DOM.chapterNavigation);
if (!this.sequenceWrapper) {
return;
}
if (this.chapterNavigation) {
this.chapterNavigationToggle();
}
this.sequence = document.querySelector(this.DOM.sequence);
this.canvasWrapper = document.querySelector(this.DOM.canvasWrapper);
gsap.set("html", {
"--canvas-height": `${this.canvasWrapper.clientHeight}px`,
});
// set scroll position to top of the document
window.scrollTo(0, 0);
if ("scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual";
}
this.loaded = false;
this.frameIndex = 0;
this.imagesArray = [];
this.frameCount = 360;
let device = "mobile";
if (window.matchMedia("(min-width: 800px)").matches) {
this.frameCount = 360;
device = "desktop";
}
for (let i = 0; i <= this.frameCount; i++) {
this.imagesArray.push({ url: `${window.sequence}/${device}/${String(i).padStart(4, "0")}_result.avif?v=1` });
}
if (this.imagesArray && this.imagesArray.length > 0) {
this.steps = gsap.utils.toArray(this.DOM.step);
this.stepsLength = this.steps.length;
this.segmentsLoaded = 0;
// don't squish img when resized
window.addEventListener("resize", () => this.resize());
this.canvasLoad();
this.sequenceController();
}
}
chapterNavigationToggle() {
ScrollTrigger.create({
ignoreMobileResize: true,
trigger: this.sequenceWrapper,
start: "top top+=1px",
end: "bottom bottom",
onEnter: () => {
gsap.to(this.chapterNavigation, {
autoAlpha: 0,
});
},
onEnterBack: () => {
gsap.to(this.chapterNavigation, {
autoAlpha: 0,
});
},
onLeave: () => {
gsap.to(this.chapterNavigation, {
autoAlpha: 1,
});
},
});
}
canvasLoad() {
this.context = this.sequence.getContext("2d");
this.context.imageSmoothingEnabled = true;
// for retina screens
this.retinaScale();
// num of images
this.frameCount = this.imagesArray.length;
this.framesLoaded = 0;
// initial image load
this.img = new Image();
this.img.src = this.currentFrame(0);
this.sequence.width = this.canvasWrapper.offsetWidth;
this.sequence.height = this.canvasWrapper.offsetHeight;
this.img.onload = () => {
this.drawImage(this.img);
};
// num of images in single chunk - for preload sequence
this.singleChunk = Math.floor(this.frameCount / (this.stepsLength + 1));
this.images = [];
this.preloadImages();
}
preloadImages() {
if (this.segmentsLoaded < this.stepsLength + 1) {
for (let i = this.singleChunk * this.segmentsLoaded; i < this.singleChunk * (this.segmentsLoaded + 1); i++) {
const img = new Image();
img.src = this.currentFrame(i);
const imageProps = [img, i];
this.images.push(imageProps);
img.onload = () => {
this.framesLoaded += 1;
if (this.framesLoaded > 0) {
this.progressController();
}
};
}
this.segmentsLoaded++;
setTimeout(() => {
this.preloadImages();
}, 500);
}
}
/**
*
* @param {number} index
* @returns {string}
*/
currentFrame(index) {
return `${this.imagesArray[index].url}`;
}
/**
*
* @param {HTMLImageElement} img
*/
drawImage(img) {
if (img != null) {
this.context.drawImage(img, 0, 0, this.sequence.width, this.sequence.height);
}
}
/**
*
* @param {number} index
*/
updateImage(index) {
if (this.images[index] != null) {
this.drawImage(this.images[index][0]);
}
}
sequenceController() {
let scrollDirection = 1;
this.steps.forEach((step, i) => {
const inc = 1 / (this.stepsLength - 1);
this.scrollInteractions(inc, scrollDirection, i, step);
});
}
/**
*
* @param {number} inc
* @param {number} scrollDirection
* @param {number} i
* @param {HTMLElement} step
*/
scrollInteractions(inc, scrollDirection, i, step) {
let trigger = step;
// if the step is pinned inside ScrollTrigger
if (step.parentNode.classList.contains("pin-spacer")) {
trigger = step.parentNode;
}
let starting;
let ending = "bottom bottom";
if (i === 0) {
starting = "top top";
} else {
starting = "top bottom";
}
const stepsDivider = this.stepsLength - 1;
ScrollTrigger.create({
ignoreMobileResize: true,
trigger: trigger,
start: starting,
end: ending,
onUpdate: (self) => {
let progress = 0;
if (this.stepsLength > 0) {
progress = (i - 1) / stepsDivider + self.progress * inc;
}
this.frameIndex = Math.floor(progress * this.frameCount);
this.updateImage(this.frameIndex);
},
});
}
progressController() {
const frameCount = this.frameCount / this.stepsLength - 1;
const progress = Math.floor((100 / frameCount) * this.framesLoaded);
if (progress < 100) {
// console.log(progress);
} else if (progress >= 100 && !this.loaded) {
console.log("Images for first section are loaded!");
this.loaded = true;
gsap.to(this.sequenceWrapper, {
autoAlpha: 1,
});
}
}
resize() {
this.sequence.width = this.canvasWrapper.clientWidth;
this.sequence.height = this.canvasWrapper.clientHeight;
this.retinaScale();
this.updateImage(this.frameIndex);
}
retinaScale() {
// for retina screens
if (window.devicePixelRatio !== 1) {
const width = this.canvasWrapper.clientWidth;
const height = this.canvasWrapper.clientHeight;
// scale the canvas by window.devicePixelRatio
this.sequence.setAttribute("width", `${width}`);
this.sequence.setAttribute("height", `${height}`);
// use css to bring it back to regular size
this.sequence.setAttribute("style", `width: ${width}px; height: ${height}px;`);
// set the scale of the context
// this.sequence.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment