anycoder-ef214f41 / index.html
HI7RAI's picture
Upload folder using huggingface_hub
c0eddd3 verified
<!DOCTYPE html>
<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 ===