import { useRef, useEffect } from 'react'; import { useStore } from '@nanostores/react'; import { $appSettings } from '../stores/appSettings'; import { $shader } from '../stores/shader'; import { uiState, showPerformanceWarning } from '../stores/ui'; import { $input, updateMousePosition, updateTouchPosition, updateDeviceMotion, } from '../stores/input'; import { FakeShader } from '../FakeShader'; import { UI_HEIGHTS } from '../utils/constants'; export function ShaderCanvas() { const canvasRef = useRef(null); const shaderRef = useRef(null); const settings = useStore($appSettings); const shader = useStore($shader); const ui = useStore(uiState); const input = useStore($input); // Mouse tracking state const mouseState = useRef({ lastX: 0, lastY: 0, startTime: Date.now(), }); // Touch gesture state const touchState = useRef({ initialPinchDistance: 0, initialPinchAngle: 0, }); useEffect(() => { if (!canvasRef.current) return; // Initialize shader shaderRef.current = new FakeShader(canvasRef.current, shader.code); // Set up canvas size setupCanvas(); // Clean up on unmount return () => { if (shaderRef.current) { shaderRef.current.destroy(); } }; }, []); // Update shader when code changes useEffect(() => { if (shaderRef.current) { shaderRef.current.setCode(shader.code); } }, [shader.code]); // Update shader settings useEffect(() => { if (shaderRef.current) { shaderRef.current.setRenderMode(settings.renderMode); shaderRef.current.setValueMode(settings.valueMode ?? 'integer'); shaderRef.current.setHueShift(settings.hueShift ?? 0); shaderRef.current.setTimeSpeed(settings.timeSpeed ?? 1.0); shaderRef.current.setBPM(settings.currentBPM ?? 120); shaderRef.current.setTargetFPS(settings.fps); } }, [settings.renderMode, settings.valueMode, settings.hueShift, settings.timeSpeed, settings.currentBPM, settings.fps]); // Handle canvas resize when resolution or UI visibility changes useEffect(() => { setupCanvas(); }, [settings.resolution, ui.uiVisible]); // Handle animation useEffect(() => { if (shaderRef.current) { shaderRef.current.startAnimation(); } }, [shader.code]); // Update input data to shader useEffect(() => { if (shaderRef.current) { shaderRef.current.setMousePosition( input.mouseX, input.mouseY, input.mousePressed, input.mouseVX, input.mouseVY, input.mouseClickTime ); shaderRef.current.setTouchPosition( input.touchCount, input.touch0X, input.touch0Y, input.touch1X, input.touch1Y, input.pinchScale, input.pinchRotation ); shaderRef.current.setDeviceMotion( input.accelX, input.accelY, input.accelZ, input.gyroX, input.gyroY, input.gyroZ ); shaderRef.current.setAudioData( input.audioLevel, input.bassLevel, input.midLevel, input.trebleLevel ); } }, [input]); const setupCanvas = () => { if (!canvasRef.current) return; const width = window.innerWidth; const height = ui.uiVisible ? window.innerHeight - UI_HEIGHTS.TOTAL_UI_HEIGHT : window.innerHeight; const scale = settings.resolution; // Set canvas internal size with resolution scaling canvasRef.current.width = Math.floor(width / scale); canvasRef.current.height = Math.floor(height / scale); console.log( `Canvas setup: ${canvasRef.current.width}x${canvasRef.current.height} (scale: ${scale}x), UI visible: ${ui.uiVisible}` ); }; const handleMouseMove = (e: React.MouseEvent) => { const lastX = mouseState.current.lastX; const lastY = mouseState.current.lastY; const x = e.clientX / window.innerWidth; const y = 1.0 - e.clientY / window.innerHeight; // Invert Y to match shader coordinates const vx = x - lastX; const vy = y - lastY; mouseState.current.lastX = x; mouseState.current.lastY = y; updateMousePosition(x, y, input.mousePressed, vx, vy, input.mouseClickTime); }; const handleMouseDown = () => { const clickTime = Date.now(); updateMousePosition( input.mouseX, input.mouseY, true, input.mouseVX, input.mouseVY, clickTime ); }; const handleMouseUp = () => { updateMousePosition( input.mouseX, input.mouseY, false, input.mouseVX, input.mouseVY, input.mouseClickTime ); }; const handleTouchStart = (e: React.TouchEvent) => { // Only prevent default on canvas area for shader interaction e.preventDefault(); updateTouchPositions(e.touches); initializePinchGesture(e.touches); }; const handleTouchMove = (e: React.TouchEvent) => { e.preventDefault(); updateTouchPositions(e.touches); updatePinchGesture(e.touches); }; const handleTouchEnd = (e: React.TouchEvent) => { e.preventDefault(); const touchCount = e.touches.length; if (touchCount === 0) { updateTouchPosition(0, 0, 0, 0, 0, 1, 0); } else { updateTouchPositions(e.touches); } }; const updateTouchPositions = (touches: React.TouchList | TouchList) => { let touch0X = 0, touch0Y = 0, touch1X = 0, touch1Y = 0; if (touches.length > 0) { touch0X = touches[0].clientX / window.innerWidth; touch0Y = 1.0 - touches[0].clientY / window.innerHeight; } if (touches.length > 1) { touch1X = touches[1].clientX / window.innerWidth; touch1Y = 1.0 - touches[1].clientY / window.innerHeight; } updateTouchPosition( touches.length, touch0X, touch0Y, touch1X, touch1Y, input.pinchScale, input.pinchRotation ); }; const initializePinchGesture = (touches: React.TouchList | TouchList) => { if (touches.length === 2) { const dx = touches[1].clientX - touches[0].clientX; const dy = touches[1].clientY - touches[0].clientY; touchState.current.initialPinchDistance = Math.sqrt(dx * dx + dy * dy); touchState.current.initialPinchAngle = Math.atan2(dy, dx); } }; const updatePinchGesture = (touches: React.TouchList | TouchList) => { if (touches.length === 2 && touchState.current.initialPinchDistance > 0) { const dx = touches[1].clientX - touches[0].clientX; const dy = touches[1].clientY - touches[0].clientY; const distance = Math.sqrt(dx * dx + dy * dy); const angle = Math.atan2(dy, dx); const pinchScale = distance / touchState.current.initialPinchDistance; const pinchRotation = angle - touchState.current.initialPinchAngle; updateTouchPosition( touches.length, input.touch0X, input.touch0Y, input.touch1X, input.touch1Y, pinchScale, pinchRotation ); } }; // Set up device motion listener useEffect(() => { const handleDeviceMotion = (e: DeviceMotionEvent) => { if (e.acceleration && e.rotationRate) { updateDeviceMotion( e.acceleration.x || 0, e.acceleration.y || 0, e.acceleration.z || 0, e.rotationRate.alpha || 0, e.rotationRate.beta || 0, e.rotationRate.gamma || 0 ); } }; if (window.DeviceMotionEvent) { window.addEventListener('devicemotion', handleDeviceMotion); return () => window.removeEventListener('devicemotion', handleDeviceMotion); } }, []); // Set up performance warning listener useEffect(() => { const handlePerformanceWarning = (e: MessageEvent) => { if (e.data === 'performance-warning') { showPerformanceWarning(); } }; window.addEventListener('message', handlePerformanceWarning); return () => window.removeEventListener('message', handlePerformanceWarning); }, []); // Set up window resize listener useEffect(() => { const handleResize = () => setupCanvas(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [settings.resolution, ui.uiVisible]); return ( ); }