Copy import imagesLoaded from 'imagesloaded';
interface RenderedStyle {
previous: number;
current: number;
ease: number;
fromValue?: number;
setValue: () => number;
}
interface ItemProps {
height: number;
top: number;
}
const MathUtils = {
map: (x: number, a: number, b: number, c: number, d: number) => ((x - a) * (d - c)) / (b - a) + c,
lerp: (a: number, b: number, n: number) => (1 - n) * a + n * b,
getRandomFloat: (min: number, max: number) => parseFloat((Math.random() * (max - min) + min).toFixed(2))
};
/**
* !class Item
*/
let docScroll = 0;
let lastScroll = 0;
/**
* class Item!
*/
/**
* !class SmoothScroll
*/
let scrollingSpeed = 0;
/**
* class SmoothScroll!
*/
let winsize: { width: number; height: number } = { width: 0, height: 0 }; // Initialize to prevent undefined access
const calcWinsize = () => winsize = { width: window.innerWidth, height: window.innerHeight };
calcWinsize();
// and recalculate on resize
window.addEventListener('resize', calcWinsize);
class Item {
// Step A1
DOM: {
el: HTMLElement;
image?: HTMLImageElement;
imageWrapper?: HTMLElement;
title?: HTMLElement | null; // Allow for null
deco?: HTMLElement | null; // Allow for null
};
// Step A2
renderedStyles: {
imageScale: RenderedStyle;
titleTranslationY: RenderedStyle;
decoTranslationY: RenderedStyle;
};
props!: ItemProps; // Definite assignment assertion
// Step A5
observer: IntersectionObserver;
// Step A6
isVisible = false;
// Step B2
insideViewport = false;
constructor(el: HTMLElement) {
this.DOM = {
el
};
this.DOM.image = this.DOM.el.querySelector(".content__item-img") as HTMLImageElement;
this.DOM.imageWrapper = this.DOM.image?.parentElement || undefined;
this.DOM.title = this.DOM.el.querySelector(".content__item-title");
this.DOM.deco = this.DOM.el.querySelector('.content__item-decobar');
this.renderedStyles = {
imageScale: {
previous: 0,
current: 0,
ease: 0.1,
setValue: () => {
const fromValue = 1;
const toValue = 1.5;
const val = MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, fromValue, toValue); // Chuyển đổi x từ khoảng [a, b] sang [c, d] theo tỷ lệ tương ứng.
return Math.max(Math.min(val, toValue), fromValue);
}
},
titleTranslationY: {
previous: 0,
current: 0,
ease: 0.1,
fromValue: MathUtils.getRandomFloat(30, 400),
setValue: () => {
const fromValue = this.renderedStyles.titleTranslationY.fromValue!;
const toValue = -1 * fromValue;
const val = MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, fromValue, toValue);
return fromValue < 0 ? Math.min(Math.max(val, fromValue), toValue) : Math.max(Math.min(val, fromValue), toValue);
}
},
decoTranslationY: {
previous: 0,
current: 0,
ease: 0.1,
setValue: () => {
const fromValue = -600;
const toValue = 600;
const val = MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, fromValue, toValue);
return Math.min(Math.max(val, fromValue), toValue);
}
}
}
// Step A3
this.getSize();
// Step A4
this.update();
this.observer = new IntersectionObserver((entries) => {
entries.forEach(
(entry) => (this.isVisible = entry.intersectionRatio > 0)
);
});
this.observer.observe(this.DOM.el);
this.initEvents();
}
getSize() {
const rect = this.DOM.el.getBoundingClientRect();
this.props = {
height: rect.height,
top: docScroll + rect.top,
};
}
update() {
for (const key in this.renderedStyles) {
const style = this.renderedStyles[key as keyof typeof this.renderedStyles];
style.current = style.previous = style.setValue();
}
this.layout();
}
resize() {
this.getSize();
this.update();
}
layout() {
if (this.DOM.title) {
const y = this.renderedStyles.titleTranslationY.previous;
this.DOM.title.style.transform = `translate3d(0,${y}px,0)`;
}
if (this.DOM.image) {
const scale = this.renderedStyles.imageScale.previous;
this.DOM.image.style.transform = `scale3d(${scale}, ${scale}, 1)`;
}
}
render() {
for (const key in this.renderedStyles) {
const style = this.renderedStyles[key as keyof typeof this.renderedStyles];
style.current = style.setValue();
style.previous = MathUtils.lerp(style.previous, style.current, style.ease);
}
this.layout();
}
initEvents() {
window.addEventListener("resize", () => this.resize());
}
}
class SmoothScroll {
DOM: {
body: HTMLElement;
main: HTMLElement;
scrollable: HTMLElement;
content: HTMLElement;
};
items: Item[];
renderedStyles: {
translationY: RenderedStyle;
skew: RenderedStyle;
};
constructor() {
this.DOM = {
body: document.querySelector("body")!,
main: document.querySelector("main")!,
scrollable: document.querySelector("div[data-scroll]")!,
content: document.querySelector(".content")!,
};
this.items = Array.from(this.DOM.content.querySelectorAll(".content__item")).map(
(item) => new Item(item as HTMLElement)
);
this.renderedStyles = {
translationY: {
previous: 0,
current: 0,
ease: 0.1,
setValue: () => docScroll,
},
skew: {
previous: 0,
current: 0,
ease: 0.1,
setValue: () => {
const toValue = 30;
const fromValue = 0;
const val = Math.max(Math.min(MathUtils.map(scrollingSpeed, 20, 100, fromValue, toValue), toValue), fromValue)
return this.renderedStyles.translationY.previous < docScroll ? val : -1 * val;
}
}
}
// Step B1
this.setSize();
this.update();
this.style();
this.initEvents();
requestAnimationFrame(() => this.render());
}
setSize() {
if (this.DOM.body && this.DOM.scrollable) { // Check if body and scrollable are not null
this.DOM.body.style.height = `${this.DOM.scrollable.scrollHeight}px`;
} else {
console.warn("Body or scrollable element is null! Cannot set height.");
}
}
style() {
Object.assign(this.DOM.main.style, {
position: "fixed",
width: "100%",
height: "100%",
top: "0",
left: "0",
overflow: "hidden",
});
}
update() {
for (const key in this.renderedStyles) {
const style = this.renderedStyles[key as keyof typeof this.renderedStyles];
style.current = style.previous = style.setValue();
}
this.layout();
}
layout() {
const y = -1 * this.renderedStyles.translationY.previous;
this.DOM.scrollable.style.transform = `translate3d(0, ${y}px, 0)`;
// this.DOM.scrollable.style.transform = `translate3d(0,${-1*this.renderedStyles.translationY.previous}px,0) skewY(${this.renderedStyles.skew.previous}deg)`;
}
initEvents() {
window.addEventListener("resize", () => this.setSize());
}
render() {
scrollingSpeed = Math.abs(docScroll - lastScroll);
lastScroll = docScroll;
for (const key in this.renderedStyles) {
const style = this.renderedStyles[key as keyof typeof this.renderedStyles];
style.current = style.setValue();
style.previous = MathUtils.lerp(style.previous, style.current, style.ease);
}
this.layout();
for (const item of this.items) {
if (item.isVisible) {
if (item.insideViewport) {
item.render();
} else {
item.insideViewport = true;
item.update();
}
} else {
item.insideViewport = false;
}
}
requestAnimationFrame(() => this.render());
}
}
export const getScrollPage = () => {
const getPageYScroll = () => {
docScroll = window.scrollY || document.documentElement.scrollTop;
};
window.addEventListener("scroll", getPageYScroll);
// ... (rest of Item, SmoothScroll classes and logic)
const preloadImages = (): Promise<void> => {
return new Promise((resolve) => {
imagesLoaded(
document.querySelectorAll(".content__item-img"),
{ background: true },
(instance) => {
// Your logic here, using the instance of ImagesLoaded
resolve(); // Call resolve after handling the loaded images
}
);
});
};
preloadImages().then(() => {
document.body.classList.remove("loading");
getPageYScroll();
lastScroll = docScroll;
new SmoothScroll(); // Khởi động hiệu ứng
});
}