import { Vector3 } from "three"; export const fps = 60; export const mspf = 1000 / fps; 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; if (sphere && point.distanceTo(spherePoint) < sphere.displacement) { const direction = point.clone().sub(spherePoint); const displacementAmount = sphere.displacement - direction.length(); direction.setLength(displacementAmount); direction.add(point); point.lerp(direction, 1); // ✨ magic number } // and move them back to their original position if (point.distanceTo(targetPoint) > 0.01) { point.lerp(targetPoint, 0.27); // ✨ magic number } const top = point.y - imgHeight / 2; const left = point.x - imgWidth / 2; const { url } = data; const style = { position: "absolute", width: `${imgWidth}px`, height: `${imgHeight}px`, top: `${top}px`, left: `${left}px`, backgroundSize: "cover", backgroundImage: `url(${url})`, opacity: 1, zIndex: row.size - Math.floor(Math.abs(row.size / 2 - row.index)), }; Object.assign(this, { seed, data, style, key, point }); } update() {} } class Row { constructor({ wall, index }) { Object.assign(this, { wall, q: [], minSize: wall.colNum + 1, index, ratio: Math.random(), y: index * wall.blockHeight + wall.blockHeight / 2, }); } 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); } nextFrame(ratioSpeed) { this.ratio += ratioSpeed; if (this.ratio > 1) { this.shift(); this.push(); this.q.forEach((el) => { el.seed.index--; }); this.ratio -= 1; } this.q.forEach((el) => { this.wall.elMap.set(el.key, new El(el.seed)); }); } } export default class Wall { #rafID; #lastTime; constructor({ items, imgWidth, imgHeight, containerWidth, containerHeight, speed, onListChange, }) { const colNum = Math.floor(containerWidth / imgWidth); const rowNum = Math.floor(containerHeight / imgHeight); const elNum = items.length; const ratioSpeed = speed / (containerWidth / colNum); const blockWidth = containerWidth / (colNum - 1); const blockHeight = containerHeight / (rowNum - 1); const elMap = new Map(); Object.assign(this, { time: 0, index: 0, items, colNum, rowNum, elNum, imgWidth, imgHeight, ratioSpeed, rows: [], key: 0, blockWidth, blockHeight, elMap, onListChange, maxDeltaTime: 1 / 30, }); } getNext() { const { index, elNum, items } = this; if (index === elNum) { this.index = 1; return items[0]; } else { this.index++; return items[index]; } } 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; return this; } animate = () => { if (!this.isRunning) return; window.requestAnimationFrame(this.animate); const now = performance.now(); const dt = Math.min(this.maxDeltaTime, (now - this.#lastTime) / 1000); this.rows.forEach((row) => row.nextFrame(this.ratioSpeed)); this.getList(); this.time += dt; this.#lastTime = now; }; dispose() { if (this.#rafID === null) return; window.cancelAnimationFrame(this.#rafID); this.#rafID = null; this.isRunning = false; return this; } getKey() { this.key++; return this.key; } getList() { if (this.onListChange) this.onListChange(Array.from(this.elMap.entries())); } attachSphere(sphere) { this.sphere = sphere; } }