import {gsap} from "gsap"; import {ThreeWorld} from "./core/ThreeWorld.js"; import {PhysicsWorld} from "./core/PhysicsWorld.js"; import {VideoController} from "./controllers/VideoController.js"; import {StateController} from "./controllers/StateController.js"; import {UIController} from "./controllers/UIController.js"; import {SequenceController} from "./controllers/SequenceController.js"; import {InteractionController} from "./controllers/InteractionController.js"; import {PixelVoxelMesh} from "./view/PixelVoxelMesh.js"; import {debounce} from "./utils/common.js"; import {calculateGridMetrics} from "./utils/gridMath.js"; import {STATES, DEFAULT_PARAMS, INTERNAL_PARAMS} from "./config/constants.js"; import videoSource from "../video/output1.mp4"; class PixelVoxelDrop { constructor() { this.container = document.querySelector(".content"); this.params = {...DEFAULT_PARAMS}; this.settings = {...INTERNAL_PARAMS}; this.gridInfo = null; this.rebuildWorld = debounce(this.rebuildWorld.bind(this), 300); this.app = new ThreeWorld(this.container, this.rebuildWorld); this.physics = new PhysicsWorld(); this.videoController = new VideoController(this.container, videoSource, this.settings.playbackRate); this.sequence = new SequenceController(this); this.interaction = new InteractionController(this); this.uiController = null; this.stateController = null; this.pixelVoxelMesh = null; this.init(); } async init() { await this.physics.init(); await this.videoController.load(); this.onVideoReadyForBuildWorld(); this.setupGUI(); this.animate(); } onVideoReadyForBuildWorld() { if (this.buildScene()) { this.videoController .play() .then(() => { this.videoController.pause(); this.videoController.seek(0); this.app.adjustCamera(this.gridInfo, this.settings.fitMargin, this.settings.gridOffsetY); this.fadeoutLoadingScreen(); this.stateController.changeState(STATES.DEFAULT); }) .catch((e) => console.warn("Autoplay blocked", e)); } } rebuildWorld() { if (this.stateController.state === STATES.ANIMATING || this.stateController.state === STATES.REVERSING) return; if (this.buildScene()) { this.app.adjustCamera(this.gridInfo, this.settings.fitMargin, this.settings.gridOffsetY); this.stateController.changeState(STATES.DEFAULT); } } buildScene() { if (!this.videoController.width) return false; this._cleanupBeforeBuild(); this.gridInfo = calculateGridMetrics( this.videoController.width, this.videoController.height, this.app.camera, this.settings.cameraDistance, this.params, this.settings.fitMargin ); this.settings.gridOffsetY = this.gridInfo.height; this.settings.gravityY = -(this.gridInfo.height * 5.0); this.params.gridCount = this.gridInfo.count.toFixed(0); this.physics.setGravity(this.settings.gravityY); const {randomDepths} = this.physics.createGridBodies(this.gridInfo, this.params, this.settings); this._setupVoxelMesh(randomDepths); return true; } _cleanupBeforeBuild() { this.sequence.reset(); if (this.pixelVoxelMesh) { this.app.scene.remove(this.pixelVoxelMesh.mesh); this.pixelVoxelMesh.dispose(); this.pixelVoxelMesh = null; } this.physics.clearBodies(); } _setupVoxelMesh(randomDepths) { this.pixelVoxelMesh = new PixelVoxelMesh(this.gridInfo, this.params, this.videoController.texture); this.pixelVoxelMesh.setRandomDepthAttribute(randomDepths); this.physics.syncMesh(this.pixelVoxelMesh); this.app.scene.add(this.pixelVoxelMesh.mesh); } setupGUI() { this.stateController = new StateController(STATES.LOADING, { onStateChange: (newState) => this.uiController?.updateState(newState, this.params.manualMode), }); this.uiController = new UIController({ params: this.params, callbacks: { onRebuild: () => this.rebuildWorld(), onUpdateUniform: (key, val) => this.pixelVoxelMesh.setUniform(key, val), onUpdatePhysics: () => { const {dancingLevel, isDancing} = this.params; const levelNorm = dancingLevel / 10.0; this.settings.bodyDamping = isDancing ? 0.8 : 2.0; this.settings.bodyRestitution = isDancing ? 1.0 + 0.6 * levelNorm : 0.01; this.physics.updateAllPhysicsParams(this.settings.bodyDamping, this.settings.bodyRestitution); }, onAction: () => this.handleMainAction(), onManualChange: () => this.uiController.updateState(this.stateController.state, this.params.manualMode), }, }); this.uiController.callbacks.onUpdatePhysics(); } animate() { requestAnimationFrame(this.animate.bind(this)); const state = this.stateController.state; this._updatePhysicsAndLogic(state); this._syncVisuals(state); if (this.videoController?.isReady) { this.app.render(); } } _updatePhysicsAndLogic(state) { if (this.pixelVoxelMesh && state === STATES.ANIMATING) { this.sequence.update(); } if (state !== STATES.REVERSING) { this.physics.step(); } } _syncVisuals(state) { if (!this.pixelVoxelMesh || state === STATES.DEFAULT) { return; } const isReversing = state === STATES.REVERSING; this.physics.syncMesh(this.pixelVoxelMesh, isReversing); } handleMainAction() { const state = this.stateController.state; if (state === STATES.COLLAPSED) { this.sequence.startReverse(); return; } if (state === STATES.DEFAULT && !this.params.manualMode) { const u = Math.random(); const v = Math.random(); this.sequence.startCollapse(u, v); } } fadeoutLoadingScreen() { const overlay = document.querySelector("#loading-overlay"); gsap.to(overlay, { delay: 1.5, autoAlpha: 0, duration: 1.0, ease: "power4.inOut", onStart: () => { this.videoController.play(); }, onComplete: () => { overlay.classList.add("loaded"); overlay.style.display = "none"; }, }); } } window.addEventListener("load", () => { new PixelVoxelDrop(); });