import { Vector3 } from "three"; import BezierEasing from "bezier-easing"; import Stats from "stats.js"; const stats = new Stats(); stats.showPanel(0); document.body.appendChild(stats.dom); export const easeIn = BezierEasing(0.5, 0.16, 0.74, 0.38); export const easeOut = BezierEasing(0, 0, 0.58, 1); const enterDuration = 3000; const exitDuration = 5000; const margin = 55; class El { constructor(seed) { const { data, row, wall, key, index } = seed; const { imgWidth, blockWidth, imgHeight, sphere } = wall; const { ratio, y } = row; let x = index * blockWidth + blockWidth / 2 - ratio * blockWidth; const point = new Vector3(x, y, 0); const targetPoint = new Vector3(x, y, 0); const spherePoint = sphere ? sphere.point : null; let scale = 1; if (sphere && point.distanceTo(spherePoint) < sphere.displacement) { const direction = point.clone().sub(spherePoint); const displacementAmount = sphere.displacement - direction.length(); scale = ((1 - displacementAmount / sphere.displacement) / 1) * 0.3 + 0.7; direction.setLength(displacementAmount); direction.add(point); point.lerp(direction, 0.725); } if (point.distanceTo(targetPoint) > 0.01) { point.lerp(targetPoint, 0.27); } const top = point.y - imgHeight / 2; const left = point.x - imgWidth / 2; const { urlSmall } = data; const style = { width: `${imgWidth}px`, height: `${imgHeight}px`, top: `${top}px`, left: `${left}px`, backgroundImage: `url(${urlSmall})`, transform: `scale(${scale})`, opcaity: scale, zIndex: wall.rowNum - Math.floor(Math.abs(wall.rowNum / 2 - row.index)), }; Object.assign(this, { seed, data, style, key, point }); } update() {} } class Row { q = []; est; fst; t1; t2; constructor({ wall, index }) { const initY = wall.marginTop + index * wall.blockHeight + wall.blockHeight / 2; const offsetY = initY - wall.containerHeight - wall.blockHeight; Object.assign(this, { wall, minSize: wall.colNum + 2, index, ratio: Math.random(), initY, y: offsetY, offsetY, }); } get size() { return this.q.length; } init() { while (this.size < this.minSize) { this.push(); } } push() { const { wall } = this; const data = wall.getNext(); const key = wall.getKey(); const seed = { data, row: this, wall, key, index: this.q.length, }; const el = new El(seed); this.wall.elMap.set(key, el); this.q.push(el); } shift() { const el = this.q.shift(); this.wall.elMap.delete(el.key); } update() { this.q.forEach((el) => { this.wall.elMap.set(el.key, new El(el.seed)); }); } enter(now) { if (!this.est) { this.est = now; } else { this.y = this.offsetY + (this.wall.containerHeight + this.wall.blockHeight) * easeOut((now - this.est) / enterDuration); } this.update(); } exit(now) { if (!this.fst) { this.fst = now; } else { this.y = this.initY + this.wall.containerHeight * easeIn((now - this.fst) / exitDuration); } this.update(); } nextFrame(ratioSpeed) { this.ratio += ratioSpeed; if (this.ratio > 1) { this.shift(); this.push(); this.q.forEach((el) => { el.seed.index--; }); this.ratio -= 1; } this.update(); } } export default class Wall { #rafID = null; #lastTime; entering = false; exiting = false; rows = []; key = 0; time = 0; index = 0; maxDeltaTime = 1 / 30; constructor({ items, imgWidth, imgHeight, containerWidth, containerHeight, speed, onListChange, }) { const blockWidth = imgWidth + margin; const blockHeight = imgHeight + margin; const colNum = Math.floor(containerWidth / blockWidth) + 1; const rowNum = Math.floor(containerHeight / blockHeight); const elNum = items.length; const ratioSpeed = speed / (containerWidth / colNum); const marginTop = (containerHeight - rowNum * blockHeight) / 2; const elMap = new Map(); Object.assign(this, { items, colNum, rowNum, elNum, imgWidth, imgHeight, containerWidth, containerHeight, ratioSpeed, blockWidth, blockHeight, marginTop, elMap, onListChange, }); } getNext() { const { items } = this; const next = items[this.index]; this.index++; if (this.index === items.length) this.index = 0; return next; } async init() { const { rowNum } = this; for (let i = 0; i < rowNum; i++) { const row = new Row({ wall: this, index: i }); row.init(); this.rows.push(row); } this.#rafID = window.requestAnimationFrame(this.animate); this.isRunning = true; this.entering = true; await new Promise((resolve) => { setTimeout(() => { this.entering = false; resolve(); }, enterDuration); }); return this; } animate = () => { stats.begin(); if (!this.isRunning) return; const now = performance.now(); const dt = Math.min(this.maxDeltaTime, (now - this.#lastTime) / 1000); this.rows.forEach((row) => this.entering ? row.enter(now) : this.exiting ? row.exit(now) : row.nextFrame(this.ratioSpeed) ); this.getList(); this.time += dt; this.#lastTime = now; this.#rafID = window.requestAnimationFrame(this.animate); stats.end(); }; async dispose() { if (this.#rafID === null) return; this.exiting = true; await new Promise((resolve) => { setTimeout(() => { this.exiting = false; window.cancelAnimationFrame(this.#rafID); this.#rafID = null; this.isRunning = false; resolve(); }, exitDuration + 500); }); return this; } getKey() { this.key++; return this.key; } getList() { if (this.onListChange) this.onListChange(Array.from(this.elMap.entries())); } attachSphere(sphere) { this.sphere = sphere; } }