switching
This commit is contained in:
327
src/components/ShaderCanvas.tsx
Normal file
327
src/components/ShaderCanvas.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
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.setTargetFPS(settings.fps);
|
||||
}
|
||||
}, [settings.renderMode, settings.valueMode, settings.fps]);
|
||||
|
||||
// Handle canvas resize when resolution or UI visibility changes
|
||||
useEffect(() => {
|
||||
setupCanvas();
|
||||
}, [settings.resolution, ui.uiVisible]);
|
||||
|
||||
// Handle animation
|
||||
useEffect(() => {
|
||||
if (shaderRef.current) {
|
||||
const hasTime = shader.code.includes('t');
|
||||
if (hasTime) {
|
||||
shaderRef.current.startAnimation();
|
||||
} else {
|
||||
shaderRef.current.stopAnimation();
|
||||
shaderRef.current.render(false);
|
||||
}
|
||||
}
|
||||
}, [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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user