325 lines
8.7 KiB
TypeScript
325 lines
8.7 KiB
TypeScript
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<HTMLCanvasElement>(null);
|
|
const shaderRef = useRef<FakeShader | null>(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 (
|
|
<canvas
|
|
ref={canvasRef}
|
|
id="canvas"
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100vw',
|
|
height: '100vh',
|
|
imageRendering: 'pixelated',
|
|
touchAction: 'none',
|
|
pointerEvents: 'auto',
|
|
}}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseUp={handleMouseUp}
|
|
onTouchStart={handleTouchStart}
|
|
onTouchMove={handleTouchMove}
|
|
onTouchEnd={handleTouchEnd}
|
|
/>
|
|
);
|
|
}
|