Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>FUSION // HYBRID NEURAL MORPH v5.0</title> | |
| <!-- LIBRARIES (CDNs) --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;700&family=Share+Tech+Mono&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| /* --- CSS VARIABLES --- */ | |
| :root { | |
| --neon-cyan: #00f3ff; | |
| --neon-pink: #ff00ff; | |
| --neon-purple: #bd00ff; | |
| --neon-green: #00ff88; | |
| --neon-yellow: #fcee0a; | |
| --dark-bg: #050505; | |
| --panel-bg: rgba(10, 10, 15, 0.85); | |
| --glass: rgba(20, 20, 30, 0.6); | |
| --border-color: rgba(255, 255, 255, 0.15); | |
| --font-ui: 'Rajdhani', sans-serif; | |
| --font-main: 'Share Tech Mono', monospace; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| -webkit-tap-highlight-color: transparent; | |
| user-select: none; | |
| outline: none; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| background-color: var(--dark-bg); | |
| font-family: var(--font-main); | |
| color: #eee; | |
| height: 100vh; | |
| width: 100vw; | |
| touch-action: none; | |
| } | |
| /* --- HEADER --- */ | |
| header { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 60px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 0 20px; | |
| z-index: 50; | |
| background: linear-gradient(to bottom, rgba(0, 0, 0, 0.9), transparent); | |
| pointer-events: none; | |
| } | |
| .brand { | |
| font-size: 1.1rem; | |
| letter-spacing: 2px; | |
| color: #fff; | |
| text-shadow: 0 0 10px var(--neon-purple); | |
| pointer-events: auto; | |
| font-family: var(--font-ui); | |
| font-weight: 700; | |
| } | |
| .anycoder-link { | |
| color: var(--neon-cyan); | |
| text-decoration: none; | |
| font-size: 0.7rem; | |
| border: 1px solid var(--neon-cyan); | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| pointer-events: auto; | |
| background: rgba(0, 243, 255, 0.05); | |
| transition: 0.3s; | |
| text-transform: uppercase; | |
| font-family: var(--font-ui); | |
| } | |
| .anycoder-link:hover { | |
| background: var(--neon-cyan); | |
| color: #000; | |
| box-shadow: 0 0 15px var(--neon-cyan); | |
| } | |
| /* --- CANVAS --- */ | |
| #canvas-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| background: #000; | |
| } | |
| /* --- HUD CONTROLS --- */ | |
| #hud { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| background: var(--panel-bg); | |
| backdrop-filter: blur(15px); | |
| -webkit-backdrop-filter: blur(15px); | |
| border-top: 1px solid var(--border-color); | |
| z-index: 20; | |
| padding: 20px; | |
| border-radius: 20px 20px 0 0; | |
| box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.8); | |
| transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); | |
| max-height: 45vh; | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--neon-purple) transparent; | |
| } | |
| /* Mobile collapse handling */ | |
| #hud.collapsed { | |
| transform: translateY(85%); | |
| } | |
| #hud-toggle { | |
| position: absolute; | |
| top: -30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: var(--panel-bg); | |
| border: 1px solid var(--border-color); | |
| border-bottom: none; | |
| border-radius: 10px 10px 0 0; | |
| padding: 5px 20px; | |
| color: var(--neon-cyan); | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| pointer-events: auto; | |
| } | |
| .hud-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| margin-bottom: 15px; | |
| } | |
| @media (max-width: 600px) { | |
| .hud-grid { grid-template-columns: 1fr; } | |
| } | |
| .control-block { | |
| background: rgba(255, 255, 255, 0.03); | |
| padding: 12px; | |
| border-radius: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .label { | |
| font-size: 0.65rem; | |
| color: #888; | |
| margin-bottom: 8px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| display: flex; | |
| justify-content: space-between; | |
| font-family: var(--font-ui); | |
| font-weight: 700; | |
| } | |
| /* --- BUTTONS --- */ | |
| button { | |
| width: 100%; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid #444; | |
| color: #fff; | |
| padding: 10px; | |
| font-family: var(--font-ui); | |
| font-weight: 600; | |
| font-size: 0.75rem; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| transition: all 0.2s; | |
| margin-bottom: 6px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 8px; | |
| border-radius: 4px; | |
| } | |
| button:active { transform: scale(0.97); } | |
| .btn-cyan { border-color: var(--neon-cyan); color: var(--neon-cyan); } | |
| .btn-cyan:hover, .btn-cyan.active { | |
| background: var(--neon-cyan); color: #000; | |
| box-shadow: 0 0 15px var(--neon-cyan); | |
| border-color: var(--neon-cyan); | |
| } | |
| .btn-pink { border-color: var(--neon-pink); color: var(--neon-pink); } | |
| .btn-pink:hover, .btn-pink.active { | |
| background: var(--neon-pink); color: #000; | |
| box-shadow: 0 0 15px var(--neon-pink); | |
| border-color: var(--neon-pink); | |
| } | |
| .btn-purple { border-color: var(--neon-purple); color: var(--neon-purple); } | |
| .btn-purple:hover, .btn-purple.active { | |
| background: var(--neon-purple); color: #fff; | |
| box-shadow: 0 0 15px var(--neon-purple); | |
| } | |
| .btn-rec { border-color: #ff3333; color: #ff3333; } | |
| .btn-rec.recording { | |
| background: #ff0000; color: white; | |
| border-color: #ff0000; | |
| animation: pulse-red 1s infinite; | |
| } | |
| @keyframes pulse-red { | |
| 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); } | |
| 70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); } | |
| } | |
| /* --- SLIDERS & SELECTS --- */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| margin: 10px 0; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 16px; width: 16px; | |
| border-radius: 50%; | |
| background: #fff; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px #fff; | |
| cursor: pointer; | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; height: 4px; | |
| background: linear-gradient(90deg, var(--neon-cyan), var(--neon-pink)); | |
| border-radius: 2px; | |
| } | |
| select { | |
| background: #111; | |
| color: #fff; | |
| border: 1px solid #444; | |
| padding: 8px; | |
| font-family: var(--font-main); | |
| font-size: 0.8rem; | |
| width: 100%; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| /* --- FILE INPUT --- */ | |
| .file-input-wrapper { position: relative; overflow: hidden; display: inline-block; width: 100%; } | |
| .file-input-wrapper input[type=file] { | |
| font-size: 100px; position: absolute; left: 0; top: 0; opacity: 0; cursor: pointer; | |
| } | |
| /* --- INTRO OVERLAY --- */ | |
| #intro { | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: #000; z-index: 100; | |
| display: flex; flex-direction: column; | |
| justify-content: center; align-items: center; | |
| transition: opacity 0.8s; | |
| } | |
| .glitch-title { | |
| font-size: 2.5rem; color: var(--neon-cyan); | |
| text-shadow: 2px 2px var(--neon-pink); | |
| margin-bottom: 20px; text-align: center; | |
| font-family: var(--font-ui); font-weight: 800; | |
| } | |
| .start-btn { | |
| border: 2px solid var(--neon-green); color: var(--neon-green); | |
| padding: 15px 50px; font-size: 1.2rem; | |
| background: rgba(0, 255, 136, 0.1); | |
| font-family: var(--font-ui); font-weight: 700; | |
| letter-spacing: 2px; | |
| cursor: pointer; | |
| transition: 0.3s; | |
| } | |
| .start-btn:hover { | |
| background: var(--neon-green); color: #000; | |
| box-shadow: 0 0 30px var(--neon-green); | |
| } | |
| /* --- DEBUG / STATUS --- */ | |
| #status-bar { | |
| position: absolute; top: 70px; right: 20px; | |
| text-align: right; font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.6); | |
| pointer-events: none; z-index: 40; line-height: 1.6; | |
| font-family: var(--font-main); | |
| } | |
| .val { color: var(--neon-cyan); } | |
| .val-pink { color: var(--neon-pink); } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header> | |
| <div class="brand">FUSION <span style="color:var(--neon-pink)">//</span> V5.0</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <!-- Status Overlay --> | |
| <div id="status-bar"> | |
| <div>BPM: <span id="bpm-val" class="val">--</span></div> | |
| <div>FPS: <span id="fps-val" class="val">60</span></div> | |
| <div>SENSOR: <span id="sensor-status" class="val">OFF</span></div> | |
| <div>LIB: <span class="val">CONNECTED</span></div> | |
| </div> | |
| <!-- Intro Screen --> | |
| <div id="intro"> | |
| <h1 class="glitch-title">SYSTEM READY</h1> | |
| <p style="color:#888; font-size:0.9rem; margin-bottom:40px; text-align:center; max-width:80%;"> | |
| AUDIO ENGINE: TONE.JS<br> | |
| VISUALS: HYBRID NEURAL SHADER<br> | |
| SENSORS: ACTIVE | |
| </p> | |
| <button class="start-btn" id="start-sys">INITIALIZE</button> | |
| </div> | |
| <!-- 3D Canvas --> | |
| <div id="canvas-container"></div> | |
| <!-- HUD Controls --> | |
| <div id="hud-toggle" onclick="document.getElementById('hud').classList.toggle('collapsed')"> | |
| <i class="fas fa-chevron-up"></i> CONTROLS | |
| </div> | |
| <div id="hud"> | |
| <!-- Main Channels --> | |
| <div class="hud-grid"> | |
| <div class="control-block"> | |
| <div class="label">CHANNEL A (CYAN)</div> | |
| <button class="btn-cyan" onclick="app.randomize(0)"> | |
| <i class="fas fa-random"></i> Random Giphy | |
| </button> | |
| <div class="file-input-wrapper"> | |
| <button class="btn-cyan" style="opacity:0.7"> | |
| <i class="fas fa-upload"></i> Upload File | |
| </button> | |
| <input type="file" accept="image/*,video/*" onchange="app.handleUpload(this, 0)"> | |
| </div> | |
| </div> | |
| <div class="control-block"> | |
| <div class="label">CHANNEL B (PINK)</div> | |
| <button class="btn-pink" onclick="app.randomize(1)"> | |
| <i class="fas fa-random"></i> Random Giphy | |
| </button> | |
| <div class="file-input-wrapper"> | |
| <button class="btn-pink" style="opacity:0.7"> | |
| <i class="fas fa-upload"></i> Upload File | |
| </button> | |
| <input type="file" accept="image/*,video/*" onchange="app.handleUpload(this, 1)"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Morph & Modes --> | |
| <div class="control-block"> | |
| <div class="label"><span>MORPH FACTOR</span> <span id="morph-val" class="val">50%</span></div> | |
| <input type="range" id="morph-slider" min="0" max="1" step="0.01" value="0.5"> | |
| </div> | |
| <div class="hud-grid"> | |
| <div class="control-block"> | |
| <div class="label">BLEND MODE</div> | |
| <button class="btn-purple" onclick="app.cycleMode()" id="mode-btn"> | |
| <i class="fas fa-layer-group"></i> NEURAL MIX | |
| </button> | |
| <div class="label" style="margin-top:10px;">AUTOSCALE</div> | |
| <button onclick="app.toggleAutoScale()" id="scale-btn" style="color:#aaa;"> | |
| <i class="fas fa-expand"></i> FIT SCREEN | |
| </button> | |
| </div> | |
| <div class="control-block"> | |
| <div class="label">AUDIO SYNC (Tone.js)</div> | |
| <div class="file-input-wrapper"> | |
| <button class="btn-cyan"> | |
| <i class="fas fa-music"></i> Load MP3 | |
| </button> | |
| <input type="file" accept="audio/*" onchange="app.handleAudio(this)"> | |
| </div> | |
| <button onclick="app.toggleStrobe()" id="strobe-btn" style="color:#aaa;"> | |
| <i class="fas fa-bolt"></i> STROBE FX: OFF | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Export --> | |
| <div class="control-block"> | |
| <div class="label">EXPORT STUDIO</div> | |
| <div style="display:flex; gap:10px;"> | |
| <select id="aspect-ratio" style="flex:1" onchange="app.updateAspect()"> | |
| <option value="0.5625">9:16 (Story)</option> | |
| <option value="1">1:1 (Square)</option> | |
| <option value="1.7777" selected>16:9 (Landscape)</option> | |
| <option value="fill">FULL FILL</option> | |
| </select> | |
| <button class="btn-rec" id="rec-btn" onclick="app.toggleRecording()" style="flex:1;"> | |
| <i class="fas fa-circle"></i> REC | |
| </button> | |
| </div> | |
| <div style="text-align:right; font-size:0.6rem; color:#666; margin-top:5px;"> | |
| REC TIME: <span id="rec-time" style="color:var(--neon-pink)">00:00</span> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * HYBRID CORE ENGINE v5.0 | |
| * Integrates Three.js, Tone.js, MediaRecorder, and Sensor APIs | |
| */ | |
| class HybridEngine { | |
| constructor() { | |
| this.container = document.getElementById('canvas-container'); | |
| // Scene Setup | |
| this.scene = new THREE.Scene(); | |
| this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| this.renderer = new THREE.WebGLRenderer({ | |
| alpha: false, | |
| antialias: false, | |
| preserveDrawingBuffer: true | |
| }); | |
| this.clock = new THREE.Clock(); | |
| this.textures = [null, null]; | |
| this.videos = [null, null]; | |
| // State | |
| this.mixRatio = 0.5; | |
| this.blendMode = 0; // 0:Mix, 1:Diff, 2:Screen, 3:Neural | |
| this.autoScale = true; | |
| this.aspectRatio = 1.7777; | |
| // Sensor State | |
| this.rotation = { x: 0, y: 0 }; | |
| this.sensorActive = false; | |
| this.strobeActive = false; | |
| this.motionIntensity = 0; | |
| // Audio (Tone.js) | |
| this.player = null; | |
| this.meter = null; | |
| this.waveform = null; | |
| this.analyserData = { bass: 0, high: 0 }; | |
| // Recording | |
| this.recorder = null; | |
| this.chunks = []; | |
| this.isRecording = false; | |
| this.recStartTime = 0; | |
| // --- EXTENSIVE GIPHY LIBRARY (Simulated "Full Library") --- | |
| // Categorized for variety | |
| this.library = [ | |
| // Abstract & Liquid | |
| "https://media.giphy.com/media/l41lFw057lAJQMxv2/giphy.mp4", | |
| "https://media.giphy.com/media/3o7aD2saalBwwftBIY/giphy.mp4", | |
| "https://media.giphy.com/media/l0HlO4p8j4kQ55hC0/giphy.mp4", | |
| "https://media.giphy.com/media/26ufdipQqU2lhNA4g/giphy.mp4", | |
| "https://media.giphy.com/media/xT9IgusfDcqpPFzjdS/giphy.mp4", | |
| // Glitch & Cyberpunk | |
| "https://media.giphy.com/media/3o7TKsAdsTDtBxp6DK/giphy.mp4", | |
| "https://media.giphy.com/media/26BRyO7kM1yRjYpjo/giphy.mp4", | |
| "https://media.giphy.com/media/3o7aD2saalBwwftBIY/giphy.mp4", | |
| "https://media.giphy.com/media/2AeJ3RcRrWqCk/giphy.mp4", | |
| "https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbmZ4eW14eXV4eXV4eXV4eXV4eXV4eXV4eXV4/LmNwrTEt4T8h5uJ8wM/giphy.mp4", // Generic fallback structure | |
| // Geometry & Tech | |
| "https://media.giphy.com/media/3oEjHCNRd6KSYgqBZm/giphy.mp4", | |
| "https://media.giphy.com/media/xT0xeJpnrWC4XWblEk/giphy.mp4", | |
| "https://media.giphy.com/media/26tn33aiTi1jkl6H6/giphy.mp4", | |
| "https://media.giphy.com/media/3o7aD2saalBwwftBIY/giphy.mp4", | |
| "https://media.giphy.com/media/l0HlNQ03J8gExgQ1O/giphy.mp4", | |
| // Neon & Lights | |
| "https://media.giphy.com/media/3o7TKsQ8MgRt8aCP5e/giphy.mp4", | |
| "https://media.giphy.com/media/1k050VPrkpAR6/giphy.mp4", | |
| "https://media.giphy.com/media/l46CyJmS9KUbopZsI/giphy.mp4", | |
| "https://media.giphy.com/media/3o7TKT5wRkYJqZ3R0E/giphy.mp4", | |
| "https://media.giphy.com/media/26tn33aiTi1jkl6H6/giphy.mp4" | |
| ]; | |
| this.init(); | |
| } | |
| init() { | |
| // Renderer Config | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| this.container.appendChild(this.renderer.domElement); | |
| // Camera Position | |
| this.camera.position.z = 2; | |
| // Create Shader Plane | |
| this.createHybridPlane(); | |
| // Initial Content | |
| this.loadSource(0, this.getRandomUrl()); | |
| this.loadSource(1, this.getRandomUrl()); | |
| // Initial Resize | |
| this.updateAspect(); | |
| // Listeners | |
| window.addEventListener('resize', () => this.updateAspect()); | |
| document.getElementById('morph-slider').addEventListener('input', (e) => { | |
| this.mixRatio = parseFloat(e.target.value); | |
| document.getElementById('morph-val').innerText = Math.round(this.mixRatio * 100) + "%"; | |
| if(this.material) this.material.uniforms.uMix.value = this.mixRatio; | |
| }); | |
| this.animate(); | |
| } | |
| // --- MEDIA HANDLING --- | |
| getRandomUrl() { | |
| return this.library[Math.floor(Math.random() * this.library.length)]; | |
| } | |
| loadSource(index, url, isFile = false) { | |
| const oldVideo = this.videos[index]; | |
| if(oldVideo) { | |
| oldVideo.pause(); | |
| oldVideo.src = ""; | |
| oldVideo.load(); | |
| } | |
| const video = document.createElement('video'); | |
| video.crossOrigin = "anonymous"; | |
| video.loop = true; | |
| video.muted = true; // Required for autoplay | |
| video.playsInline = true; | |
| video.src = url; | |
| // Autoplay with error handling | |
| const playPromise = video.play(); | |
| if (playPromise !== undefined) { | |
| playPromise.catch(error => { | |
| // Auto-play was prevented. User must interact first. | |
| // We handle this in the 'start-sys' button logic | |
| }); | |
| } | |
| const texture = new THREE.VideoTexture(video); | |
| texture.minFilter = THREE.LinearFilter; | |
| texture.magFilter = THREE.LinearFilter; | |
| texture.format = THREE.RGBFormat; | |
| this.videos[index] = video; | |
| this.textures[index] = texture; | |
| if(this.material) { | |
| if(index === 0) this.material.uniforms.uTex1.value = texture; | |
| if(index === 1) this.material.uniforms.uTex2.value = texture; | |
| // Trigger Glitch | |
| this.triggerGlitch(); | |
| } | |
| } | |
| triggerGlitch() { | |
| if(!this.material) return; | |
| this.material.uniforms.uGlitch.value = 1.0; | |
| setTimeout(() => this.material.uniforms.uGlitch.value = 0.0, 300); | |
| } | |
| randomize(index) { | |
| this.loadSource(index, this.getRandomUrl()); | |
| } | |
| handleUpload(input, index) { | |
| const file = input.files[0]; | |
| if (file) { | |
| const url = URL.createObjectURL(file); | |
| this.loadSource(index, url, true); | |
| } | |
| } | |
| // --- SHADER SYSTEM (MERGED & IMPROVED) --- | |
| createHybridPlane() { | |
| const vertexShader = ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `; | |
| const fragmentShader = ` | |
| uniform sampler2D uTex1; | |
| uniform sampler2D uTex2; | |
| uniform float uMix; | |
| uniform float uMode; | |
| uniform float uTime; | |
| uniform float uGlitch; | |
| uniform float uBeat; | |
| uniform float uBass; | |
| uniform float uHigh; | |
| varying vec2 vUv; | |
| // Simplex Noise | |
| vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); } | |
| float snoise(vec2 v){ | |
| const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); | |
| vec2 i = floor(v + dot(v, C.yy) ); | |
| vec2 x0 = v - i + dot(i, C.xx); | |
| vec2 i1; | |
| i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); | |
| vec4 x12 = x0.xyxy + C.xxzz; | |
| x12.xy -= i1; | |
| i = mod(i, 289.0); | |
| vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); | |
| vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); | |
| m = m*m ; m = m*m ; | |
| vec3 x = 2.0 * fract(p * C.www) - 1.0; | |
| vec3 h = abs(x) - 0.5; | |
| vec3 ox = floor(x + 0.5); | |
| vec3 a0 = x - ox; | |
| m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); | |
| vec3 g; | |
| g.x = a0.x * x0.x + h.x * x0.y; | |
| g.yz = a0.yz * x12.xz + h.yz * x12.yw; | |
| return 130.0 * dot(m, g); | |
| } | |
| void main() { | |
| vec2 uv = vUv; | |
| // 1. Audio Reactive Distortion (Bass) | |
| float noiseVal = snoise(uv * 3.0 + uTime * 0.5); | |
| float distStr = uBass * 0.1; | |
| vec2 distUV = uv + vec2(noiseVal * distStr, -noiseVal * distStr); | |
| // 2. Glitch Effect | |
| if(uGlitch > 0.0) { | |
| float split = sin(uTime * 20.0) * uGlitch * 0.05; | |
| distUV.x += split; | |
| } | |
| // 3. Sample Textures | |
| vec4 c1 = texture2D(uTex1, distUV); | |
| vec4 c2 = texture2D(uTex2, distUV); | |
| // 4. RGB Shift on High Frequencies | |
| float shift = uHigh * 0.03; | |
| c1.r = texture2D(uTex1, distUV + vec2(shift, 0.0)).r; | |
| c2.b = texture2D(uTex2, distUV - vec2(shift, 0.0)).b; | |
| vec3 final = vec3(0.0); | |
| // 5. Blend Modes | |
| if (uMode < 0.5) { // NEURAL MIX (Noise Mask) | |
| float mask = smoothstep(0.4, 0.6, noiseVal + uMix - 0.5); | |
| final = mix(c1.rgb, c2.rgb, mask); | |
| } | |
| else if (uMode < 1.5) { // DIFFERENCE | |
| vec3 diff = abs(c1.rgb - c2.rgb); | |
| final = mix(c1.rgb, diff, uMix); | |
| } | |
| else if (uMode < 2.5) { // SCREEN | |
| vec3 screen = 1.0 - (1.0 - c1.rgb) * (1.0 - c2.rgb); | |
| final = mix(c1.rgb, screen, uMix); | |
| } | |
| else { // ADDITIVE | |
| final = mix(c1.rgb, c1.rgb + c2.rgb, uMix); | |
| } | |
| // Vignette | |
| float dist = distance(uv, vec2(0.5)); | |
| final.rgb *= smoothstep(0.8, 0.2, dist); | |
| // Scanline | |
| final.rgb *= 0.95 + 0.05 * sin(uv.y * 1000.0); | |
| // Beat Pulse (Brightness) | |
| final *= uBeat; | |
| gl_FragColor = vec4(final, 1.0); | |
| } | |
| `; | |
| this.material = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTex1: { value: null }, | |
| uTex2: { value: null }, | |
| uMix: { value: 0.5 }, | |
| uMode: { value: 0.0 }, | |
| uTime: { value: 0.0 }, | |
| uGlitch: { value: 0.0 }, | |
| uBeat: { value: 1.0 }, | |
| uBass: { value: 0.0 }, | |
| uHigh: { value: 0.0 } | |
| }, | |
| vertexShader: vertexShader, | |
| fragmentShader: fragmentShader, | |
| side: THREE.DoubleSide | |
| }); | |
| const geometry = new THREE.PlaneGeometry(3, 3); // Base unit size | |
| this.plane = new THREE.Mesh(geometry, this.material); | |
| this.scene.add(this.plane); | |
| } | |
| cycleMode() { | |
| this.blendMode = (this.blendMode + 1) % 4; | |
| this.material.uniforms.uMode.value = parseFloat(this.blendMode); | |
| const names = ["NEURAL MIX", "DIFFERENCE", "SCREEN", "ADDITIVE"]; | |
| const btn = document.getElementById('mode-btn'); | |
| btn.innerHTML = `<i class="fas fa-layer-group"></i> ${names[this.blendMode]}`; | |
| this.triggerGlitch(); | |
| } | |
| // --- AUTOSCALE LOGIC --- | |
| toggleAutoScale() { | |
| this.autoScale = !this.autoScale; | |
| const btn = document.getElementById('scale-btn'); | |
| btn.innerHTML = this.autoScale ? '<i class="fas fa-expand"></i> FIT SCREEN' : '<i class="fas fa-compress"></i> STRETCH'; | |
| btn.style.color = this.autoScale ? '#fff' : '#aaa'; | |
| this.updateAspect(); | |
| } | |
| updateAspect() { | |
| const aspectSelect = document.getElementById('aspect-ratio'); | |
| const val = aspectSelect.value; | |
| let targetRatio; | |
| if (val === 'fill') { | |
| // Fill the screen completely | |
| this.plane.scale.set(window.innerWidth / 100, window.innerHeight / 100, 1); | |
| return; // Early exit for fill mode | |
| } else { | |
| targetRatio = parseFloat(val); | |
| } | |
| const screenRatio = window.innerWidth / window.innerHeight; | |
| // Calculate scale to fit plane within camera view (approx height 2.2 at z=2) | |
| // We want the plane to maintain targetRatio and fit within the view | |
| const planeHeight = 2.4; // Roughly filling vertical FOV | |
| const planeWidth = planeHeight * targetRatio; | |
| if (this.autoScale) { | |
| // Logic: Scale plane so it fills the screen as much as possible without cropping | |
| // or scale it to match the specific aspect ratio selected | |
| // Simply setting the plane aspect ratio: | |
| if (screenRatio > targetRatio) { | |
| // Screen is wider than target | |
| this.plane.scale.y = planeHeight; | |
| this.plane.scale.x = planeHeight * targetRatio; | |
| } else { | |
| // Screen is taller than target | |
| this.plane.scale.x = planeHeight * screenRatio; // Fill width | |
| this.plane.scale.y = (planeHeight * screenRatio) / targetRatio; | |
| } | |
| } else { | |
| // Stretch to fill screen | |
| this.plane.scale.y = planeHeight; | |
| this.plane.scale.x = planeHeight * screenRatio; | |
| } | |
| } | |
| // --- AUDIO SYSTEM (Tone.js) --- | |
| async handleAudio(input) { | |
| const file = input.files[0]; | |
| if(!file) return; | |
| const url = URL.createObjectURL(file); | |
| if(this.player) this.player.dispose(); | |
| // Tone.js Player Setup | |
| await Tone.start(); | |
| this.player = new Tone.Player(url).toDestination(); | |
| this.player.autostart = true; | |
| this.player.loop = true; | |
| // Analyzer for visuals | |
| this.meter = new Tone.Meter(); | |
| this.waveform = new Tone.Waveform(1024); | |
| // EQ to split bands for shader | |
| this.lowBand = new Tone.Filter(200, "lowpass").toDestination(); | |
| this.highBand = new Tone.Filter(1000, "highpass").toDestination(); | |
| // Split signal | |
| this.player.connect(this.lowBand); | |
| this.player.connect(this.highBand); | |
| this.player.connect(this.meter); | |
| // Meters for bands | |
| this.meterBass = new Tone.Meter(); | |
| this.meterHigh = new Tone.Meter(); | |
| this.lowBand.connect(this.meterBass); | |
| this.highBand.connect(this.meterHigh); | |
| Tone.loaded().then(() => { | |
| document.getElementById('bpm-val').innerText = "SYNCED"; | |
| document.getElementById('bpm-val').classList.add('val-pink'); | |
| }); | |
| } | |
| // --- SENSOR SYSTEM --- | |
| async enableSensors() { | |
| // Init Audio Context | |
| await Tone.start(); | |
| // Start Videos | |
| this.videos.forEach(v => { if(v) v.play(); }); | |
| // Hide Intro | |
| const intro = document.getElementById('intro'); | |
| intro.style.opacity = '0'; | |
| setTimeout(() => intro.remove(), 800); | |
| // Device Orientation (Mobile) | |
| if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') { | |
| try { | |
| const response = await DeviceOrientationEvent.requestPermission(); | |
| if (response === |