mirror of
https://github.com/SamEyeBam/animate.git
synced 2025-09-28 06:55:25 +00:00
So much play
This commit is contained in:
19431
docs/react/package-lock.json
generated
Normal file
19431
docs/react/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
docs/react/package.json
Normal file
41
docs/react/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "animate-react",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^9.92.7",
|
||||
"@react-three/fiber": "^8.15.12",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"three": "^0.159.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-scripts": "5.0.1"
|
||||
}
|
||||
}
|
18
docs/react/public/index.html
Normal file
18
docs/react/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Animate - Ported to React and Three.js"
|
||||
/>
|
||||
<title>Animate - React</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
54
docs/react/src/App.js
Normal file
54
docs/react/src/App.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import AnimationScene from './components/AnimationScene';
|
||||
import useAnimationStore from './store/animationStore';
|
||||
|
||||
function App() {
|
||||
const [showToolbar, setShowToolbar] = useState(true);
|
||||
const { selectedAnimation, setSelectedAnimation, animations } = useAnimationStore();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'p' || e.key === 'P') {
|
||||
setShowToolbar(prev => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<Canvas
|
||||
style={{ background: '#000', width: '100%', height: '100vh', position: 'absolute' }}
|
||||
camera={{ position: [0, 0, 5], fov: 75 }}
|
||||
dpr={[1, 2]}
|
||||
>
|
||||
<ambientLight intensity={0.5} />
|
||||
<AnimationScene />
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
|
||||
<button
|
||||
className="toggle-button"
|
||||
onClick={() => setShowToolbar(prev => !prev)}
|
||||
>
|
||||
{showToolbar ? 'Hide' : 'Show'} Controls
|
||||
</button>
|
||||
|
||||
<Toolbar
|
||||
isVisible={showToolbar}
|
||||
selectedAnimation={selectedAnimation}
|
||||
onAnimationChange={setSelectedAnimation}
|
||||
animations={animations}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
132
docs/react/src/components/AnimationScene.js
Normal file
132
docs/react/src/components/AnimationScene.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import useAnimationStore from '../store/animationStore';
|
||||
import { PolyTwistColourWidth } from './animations/PolyTwistColourWidth';
|
||||
import { FloralPhyllo } from './animations/FloralPhyllo';
|
||||
import { RaysInShape } from './animations/RaysInShape';
|
||||
|
||||
const AnimationScene = () => {
|
||||
const { selectedAnimation, elapsedTime, rotation, updateAnimation, animations, paused } = useAnimationStore();
|
||||
const lastTimeRef = useRef(0);
|
||||
const sceneRef = useRef();
|
||||
const objectsToDisposeRef = useRef([]);
|
||||
const { viewport, size } = useThree();
|
||||
|
||||
// Helper function to properly dispose of Three.js objects
|
||||
const disposeObjects = () => {
|
||||
if (objectsToDisposeRef.current.length > 0) {
|
||||
objectsToDisposeRef.current.forEach(object => {
|
||||
// Dispose of geometries
|
||||
if (object.geometry) {
|
||||
object.geometry.dispose();
|
||||
}
|
||||
|
||||
// Dispose of materials (could be an array or a single material)
|
||||
if (object.material) {
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach(material => material.dispose());
|
||||
} else {
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear the disposal list
|
||||
objectsToDisposeRef.current = [];
|
||||
}
|
||||
};
|
||||
|
||||
// Clear scene when animation changes
|
||||
useEffect(() => {
|
||||
if (sceneRef.current) {
|
||||
// Dispose of all existing objects first
|
||||
disposeObjects();
|
||||
|
||||
while (sceneRef.current.children.length > 0) {
|
||||
const child = sceneRef.current.children[0];
|
||||
sceneRef.current.remove(child);
|
||||
}
|
||||
}
|
||||
}, [selectedAnimation]);
|
||||
|
||||
// Cleanup on component unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
disposeObjects();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update animation on each frame
|
||||
useFrame((state, delta) => {
|
||||
if (!paused) {
|
||||
updateAnimation(delta);
|
||||
}
|
||||
|
||||
// Get the current animation parameters
|
||||
const animationParams = animations[selectedAnimation].parameters;
|
||||
|
||||
// Get elapsed time in seconds
|
||||
const currentTime = elapsedTime;
|
||||
const deltaTime = currentTime - lastTimeRef.current;
|
||||
lastTimeRef.current = currentTime;
|
||||
|
||||
// Clear the scene for a new frame
|
||||
if (sceneRef.current) {
|
||||
// Dispose of previous objects first to prevent memory leaks
|
||||
disposeObjects();
|
||||
|
||||
while (sceneRef.current.children.length > 0) {
|
||||
const child = sceneRef.current.children[0];
|
||||
sceneRef.current.remove(child);
|
||||
}
|
||||
|
||||
// Array to collect objects created in this frame for later disposal
|
||||
const newObjects = [];
|
||||
|
||||
// Render the selected animation
|
||||
switch (selectedAnimation) {
|
||||
case 'PolyTwistColourWidth':
|
||||
PolyTwistColourWidth({
|
||||
scene: sceneRef.current,
|
||||
rotation,
|
||||
params: animationParams,
|
||||
viewport,
|
||||
objectsToDispose: newObjects
|
||||
});
|
||||
break;
|
||||
case 'FloralPhyllo':
|
||||
FloralPhyllo({
|
||||
scene: sceneRef.current,
|
||||
rotation,
|
||||
params: animationParams,
|
||||
viewport,
|
||||
objectsToDispose: newObjects
|
||||
});
|
||||
break;
|
||||
case 'RaysInShape':
|
||||
RaysInShape({
|
||||
scene: sceneRef.current,
|
||||
elapsedTime: currentTime,
|
||||
deltaTime,
|
||||
params: animationParams,
|
||||
viewport,
|
||||
objectsToDispose: newObjects
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// Default: render nothing
|
||||
break;
|
||||
}
|
||||
|
||||
// Store objects for disposal in the next frame
|
||||
objectsToDisposeRef.current = newObjects;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={sceneRef} />
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimationScene;
|
67
docs/react/src/components/ControlFilter.js
Normal file
67
docs/react/src/components/ControlFilter.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import useAnimationStore from '../store/animationStore';
|
||||
|
||||
const ControlFilter = ({ filter, property, index }) => {
|
||||
const { updateFilter, removeFilter } = useAnimationStore();
|
||||
|
||||
const handleFilterChange = (filterProperty, value) => {
|
||||
updateFilter(property, index, filterProperty, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="filter">
|
||||
<div className="filter-header">
|
||||
Filter {index + 1}: {filter.min} to {filter.max} @ {filter.frequency.toFixed(2)}Hz
|
||||
</div>
|
||||
|
||||
<div className="control-row">
|
||||
<label>Min:</label>
|
||||
<input
|
||||
type="range"
|
||||
className="range-control"
|
||||
value={filter.min}
|
||||
min={-200}
|
||||
max={filter.max}
|
||||
onChange={(e) => handleFilterChange('min', parseFloat(e.target.value))}
|
||||
/>
|
||||
<span className="value">{Math.round(filter.min)}</span>
|
||||
</div>
|
||||
|
||||
<div className="control-row">
|
||||
<label>Max:</label>
|
||||
<input
|
||||
type="range"
|
||||
className="range-control"
|
||||
value={filter.max}
|
||||
min={filter.min}
|
||||
max={200}
|
||||
onChange={(e) => handleFilterChange('max', parseFloat(e.target.value))}
|
||||
/>
|
||||
<span className="value">{Math.round(filter.max)}</span>
|
||||
</div>
|
||||
|
||||
<div className="control-row">
|
||||
<label>Frequency:</label>
|
||||
<input
|
||||
type="range"
|
||||
className="range-control"
|
||||
value={filter.frequency * 100}
|
||||
min={1}
|
||||
max={200}
|
||||
onChange={(e) => handleFilterChange('frequency', parseFloat(e.target.value) / 100)}
|
||||
/>
|
||||
<span className="value">{filter.frequency.toFixed(2)}Hz</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="button button-reset"
|
||||
style={{ marginTop: '5px', fontSize: '11px', padding: '3px 8px' }}
|
||||
onClick={() => removeFilter(property, index)}
|
||||
>
|
||||
Remove Filter
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlFilter;
|
177
docs/react/src/components/Toolbar.js
Normal file
177
docs/react/src/components/Toolbar.js
Normal file
@@ -0,0 +1,177 @@
|
||||
import React from 'react';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import useAnimationStore from '../store/animationStore';
|
||||
import ControlFilter from './ControlFilter';
|
||||
|
||||
const Toolbar = ({ isVisible }) => {
|
||||
const {
|
||||
selectedAnimation,
|
||||
setSelectedAnimation,
|
||||
animations,
|
||||
updateParameter,
|
||||
addFilter,
|
||||
paused,
|
||||
togglePause,
|
||||
resetAnimation,
|
||||
speedMultiplier,
|
||||
} = useAnimationStore();
|
||||
|
||||
const currentAnimation = animations[selectedAnimation];
|
||||
|
||||
const handleAnimationChange = (e) => {
|
||||
setSelectedAnimation(e.target.value);
|
||||
};
|
||||
|
||||
const handleParameterChange = (property, value) => {
|
||||
updateParameter(property, value);
|
||||
};
|
||||
|
||||
const renderControl = (control) => {
|
||||
const value = currentAnimation.parameters[control.property];
|
||||
|
||||
switch (control.type) {
|
||||
case 'range':
|
||||
return (
|
||||
<div className="control-row" key={control.property}>
|
||||
<label htmlFor={`control-${control.property}`}>{control.label || control.property}:</label>
|
||||
<input
|
||||
id={`control-${control.property}`}
|
||||
type="range"
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
step={1}
|
||||
value={value}
|
||||
onChange={(e) => handleParameterChange(control.property, parseFloat(e.target.value))}
|
||||
className="range-control"
|
||||
/>
|
||||
<span className="value">{Math.round(value * 100) / 100}</span>
|
||||
|
||||
<button
|
||||
className="add-filter-button"
|
||||
onClick={() => addFilter(control.property)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'color':
|
||||
return (
|
||||
<div className="control-container" key={control.property}>
|
||||
<label htmlFor={`control-${control.property}`}>{control.label || control.property}:</label>
|
||||
<div style={{ margin: '10px 0' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '20px',
|
||||
background: value,
|
||||
marginBottom: '5px',
|
||||
border: '1px solid #444'
|
||||
}}
|
||||
/>
|
||||
<HexColorPicker
|
||||
color={value}
|
||||
onChange={(color) => handleParameterChange(control.property, color)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<div className="control-row" key={control.property}>
|
||||
<label htmlFor={`control-${control.property}`}>{control.label || control.property}:</label>
|
||||
<input
|
||||
id={`control-${control.property}`}
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={(e) => handleParameterChange(control.property, e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderFilters = (property) => {
|
||||
const filters = currentAnimation.filters[property];
|
||||
if (!filters || filters.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="filter-container">
|
||||
<div className="filter-header">Filters</div>
|
||||
{filters.map((filter, index) => (
|
||||
<ControlFilter
|
||||
key={`${property}-filter-${index}`}
|
||||
filter={filter}
|
||||
property={property}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`toolbar ${isVisible ? '' : 'hidden'}`}>
|
||||
<h2>Animation Controls</h2>
|
||||
|
||||
<div className="control-container">
|
||||
<label htmlFor="animation-selector">Animation:</label>
|
||||
<select
|
||||
id="animation-selector"
|
||||
value={selectedAnimation}
|
||||
onChange={handleAnimationChange}
|
||||
className="shape-selector"
|
||||
>
|
||||
{Object.keys(animations).map(animName => (
|
||||
<option key={animName} value={animName}>{animName}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="control-container">
|
||||
<button
|
||||
className="button"
|
||||
onClick={togglePause}
|
||||
>
|
||||
{paused ? 'Play' : 'Pause'}
|
||||
</button>
|
||||
<button
|
||||
className="button button-reset"
|
||||
onClick={resetAnimation}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="control-row">
|
||||
<label htmlFor="speed-multiplier">Speed:</label>
|
||||
<input
|
||||
id="speed-multiplier"
|
||||
type="range"
|
||||
min={1}
|
||||
max={500}
|
||||
value={speedMultiplier}
|
||||
onChange={(e) => useAnimationStore.setState({ speedMultiplier: parseInt(e.target.value) })}
|
||||
className="range-control"
|
||||
/>
|
||||
<span className="value">{speedMultiplier}</span>
|
||||
</div>
|
||||
|
||||
<hr style={{ margin: '20px 0' }} />
|
||||
|
||||
<h3>Parameters</h3>
|
||||
{currentAnimation.config.map(control => (
|
||||
<React.Fragment key={control.property}>
|
||||
{renderControl(control)}
|
||||
{renderFilters(control.property)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolbar;
|
96
docs/react/src/components/animations/FloralPhyllo.js
Normal file
96
docs/react/src/components/animations/FloralPhyllo.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export const FloralPhyllo = ({ scene, rotation, params, viewport, objectsToDispose = [] }) => {
|
||||
const {
|
||||
width,
|
||||
depth,
|
||||
start,
|
||||
colour1,
|
||||
colour2
|
||||
} = params;
|
||||
|
||||
// The effective rotation with start offset
|
||||
const effectiveRotation = rotation + start;
|
||||
|
||||
// Generate points in phyllotaxis pattern
|
||||
for (let n = 1; n <= depth; n++) {
|
||||
// Calculate position using phyllotaxis formula
|
||||
// Use the golden angle (137.5 degrees) for natural-looking pattern
|
||||
const a = n * 0.1 + effectiveRotation / 100;
|
||||
const r = Math.sqrt(n) * (width / 25);
|
||||
const x = r * Math.cos(a);
|
||||
const y = r * Math.sin(a);
|
||||
|
||||
// Calculate color using gradient based on position in the sequence
|
||||
const colorFraction = n / depth;
|
||||
const color = lerpColor(colour1, colour2, colorFraction);
|
||||
|
||||
// Create a petal/eye shape at this position
|
||||
createPetal(scene, n/2 + 10, x, y, 0, color, objectsToDispose);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to create a petal/eye shape
|
||||
function createPetal(scene, size, x, y, z, color, objectsToDispose) {
|
||||
// Create a custom shape for the petal/eye
|
||||
const shape = new THREE.Shape();
|
||||
|
||||
// Define control points for the petal shape
|
||||
const halfSize = size / 2;
|
||||
|
||||
// Starting point
|
||||
shape.moveTo(-halfSize, 0);
|
||||
|
||||
// Top curve
|
||||
shape.quadraticCurveTo(0, halfSize, halfSize, 0);
|
||||
|
||||
// Bottom curve
|
||||
shape.quadraticCurveTo(0, -halfSize, -halfSize, 0);
|
||||
|
||||
// Create geometry from shape
|
||||
const geometry = new THREE.ShapeGeometry(shape);
|
||||
|
||||
// Create material with specified color
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: new THREE.Color(color),
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
// Create mesh and position it
|
||||
const petal = new THREE.Mesh(geometry, material);
|
||||
petal.position.set(x, y, z);
|
||||
|
||||
// Calculate angle based on position relative to origin
|
||||
const angle = Math.atan2(y, x);
|
||||
petal.rotation.z = angle; // Rotate to face outward
|
||||
|
||||
// Add stroke outline
|
||||
const edgesGeometry = new THREE.EdgesGeometry(geometry);
|
||||
const edgesMaterial = new THREE.LineBasicMaterial({
|
||||
color: new THREE.Color(0x000000),
|
||||
linewidth: 1
|
||||
});
|
||||
const edges = new THREE.LineSegments(edgesGeometry, edgesMaterial);
|
||||
|
||||
petal.add(edges);
|
||||
scene.add(petal);
|
||||
|
||||
// Add to objects to dispose list
|
||||
if (objectsToDispose) {
|
||||
objectsToDispose.push(petal);
|
||||
// Also track the edges for disposal
|
||||
objectsToDispose.push(edges);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert hex color string to THREE.js color and interpolate
|
||||
function lerpColor(colorA, colorB, t) {
|
||||
const a = new THREE.Color(colorA);
|
||||
const b = new THREE.Color(colorB);
|
||||
|
||||
return new THREE.Color(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t
|
||||
).getHex();
|
||||
}
|
96
docs/react/src/components/animations/PolyTwistColourWidth.js
Normal file
96
docs/react/src/components/animations/PolyTwistColourWidth.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export const PolyTwistColourWidth = ({ scene, rotation, params, viewport, objectsToDispose = [] }) => {
|
||||
const {
|
||||
sides,
|
||||
width,
|
||||
lineWidth,
|
||||
depth,
|
||||
rotation: rotationParam,
|
||||
colour1,
|
||||
colour2
|
||||
} = params;
|
||||
|
||||
// Calculate the inner angle of the polygon
|
||||
const innerAngle = 180 - ((sides - 2) * 180) / sides;
|
||||
const scopeAngle = rotation - (innerAngle * Math.floor(rotation / innerAngle));
|
||||
|
||||
// Calculate the twist angle
|
||||
let outAngle = 0;
|
||||
if (scopeAngle < innerAngle / 2) {
|
||||
outAngle = innerAngle / (2 * Math.cos((2 * Math.PI * scopeAngle) / (3 * innerAngle))) - innerAngle / 2;
|
||||
} else {
|
||||
outAngle = -innerAngle / (2 * Math.cos(((2 * Math.PI) / 3) - ((2 * Math.PI * scopeAngle) / (3 * innerAngle)))) + (innerAngle * 3) / 2;
|
||||
}
|
||||
|
||||
// Calculate width multiplier
|
||||
const minWidth = Math.sin(toRadians(innerAngle / 2)) * (0.5 / Math.tan(toRadians(innerAngle / 2))) * 2;
|
||||
const widthMultiplier = minWidth / Math.sin(Math.PI / 180 * (90 + innerAngle / 2 - outAngle + innerAngle * Math.floor(outAngle / innerAngle)));
|
||||
|
||||
// Draw each polygon with increasing size and color transition
|
||||
for (let i = 0; i < depth; i++) {
|
||||
const fraction = i / depth;
|
||||
const color = lerpColor(colour1, colour2, fraction);
|
||||
|
||||
// Create a polygon shape
|
||||
drawPolygon(
|
||||
scene,
|
||||
sides,
|
||||
width * Math.pow(widthMultiplier, i),
|
||||
outAngle * i + rotationParam,
|
||||
color,
|
||||
lineWidth,
|
||||
objectsToDispose
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
function drawPolygon(scene, sides, width, rotation, color, lineWidth, objectsToDispose) {
|
||||
const points = [];
|
||||
|
||||
// Create points for the polygon
|
||||
for (let i = 0; i <= sides; i++) {
|
||||
const angle = (i * 2 * Math.PI) / sides + (rotation * Math.PI) / 180;
|
||||
points.push(new THREE.Vector3(
|
||||
width * Math.cos(angle),
|
||||
width * Math.sin(angle),
|
||||
0
|
||||
));
|
||||
}
|
||||
|
||||
// Create geometry from points
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
|
||||
// Create material with specified color
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: new THREE.Color("#" + color),
|
||||
linewidth: lineWidth // Note: linewidth may not work as expected in WebGL
|
||||
});
|
||||
|
||||
// Create and add the line
|
||||
const line = new THREE.Line(geometry, material);
|
||||
scene.add(line);
|
||||
|
||||
// Add to objects to dispose list
|
||||
if (objectsToDispose) {
|
||||
objectsToDispose.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert hex color string to THREE.js color
|
||||
function lerpColor(colorA, colorB, t) {
|
||||
const a = new THREE.Color(colorA);
|
||||
const b = new THREE.Color(colorB);
|
||||
|
||||
return new THREE.Color(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t
|
||||
).getHexString();
|
||||
}
|
||||
|
||||
// Convert degrees to radians
|
||||
function toRadians(degrees) {
|
||||
return degrees * Math.PI / 180;
|
||||
}
|
280
docs/react/src/components/animations/RaysInShape.js
Normal file
280
docs/react/src/components/animations/RaysInShape.js
Normal file
@@ -0,0 +1,280 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export const RaysInShape = ({ scene, elapsedTime, deltaTime, params, viewport, objectsToDispose = [] }) => {
|
||||
const {
|
||||
rays,
|
||||
speed,
|
||||
doesWave,
|
||||
speedVertRate,
|
||||
speedHorrRate,
|
||||
speedVert,
|
||||
speedHorr,
|
||||
boxSize,
|
||||
trailLength,
|
||||
lineWidth,
|
||||
fade,
|
||||
colourFree,
|
||||
colourContained,
|
||||
boxVisible
|
||||
} = params;
|
||||
|
||||
// Define box boundaries
|
||||
const halfBoxSize = boxSize / 200;
|
||||
const boxLeft = -halfBoxSize;
|
||||
const boxRight = halfBoxSize;
|
||||
const boxTop = -halfBoxSize;
|
||||
const boxBottom = halfBoxSize;
|
||||
|
||||
// Draw the box if visible
|
||||
if (boxVisible) {
|
||||
const boxGeometry = new THREE.BufferGeometry();
|
||||
const boxVertices = new Float32Array([
|
||||
boxLeft, boxTop, 0,
|
||||
boxRight, boxTop, 0,
|
||||
boxRight, boxBottom, 0,
|
||||
boxLeft, boxBottom, 0,
|
||||
boxLeft, boxTop, 0
|
||||
]);
|
||||
boxGeometry.setAttribute('position', new THREE.BufferAttribute(boxVertices, 3));
|
||||
const boxMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
|
||||
const box = new THREE.Line(boxGeometry, boxMaterial);
|
||||
scene.add(box);
|
||||
|
||||
// Track box for disposal
|
||||
if (objectsToDispose) {
|
||||
objectsToDispose.push(box);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate current wave values if waves are enabled
|
||||
let currentSpeedVert = speedVert;
|
||||
let currentSpeedHorr = speedHorr;
|
||||
|
||||
if (doesWave) {
|
||||
const vertRate = speedVertRate / 1000;
|
||||
const horrRate = speedHorrRate / 1000;
|
||||
currentSpeedVert = Math.sin(elapsedTime * vertRate) * 85 + 100;
|
||||
currentSpeedHorr = Math.sin(elapsedTime * horrRate) * 85 + 100;
|
||||
}
|
||||
|
||||
// Generate rays
|
||||
const actualRayCount = Math.min(rays, 100); // Limit for performance
|
||||
for (let i = 0; i < actualRayCount; i++) {
|
||||
const angle = (i * 360) / actualRayCount;
|
||||
|
||||
// Create ray trajectory
|
||||
const positions = generateRayPositions(
|
||||
0, 0, 0, // Start at center
|
||||
angle,
|
||||
speed / 10000,
|
||||
currentSpeedVert / 100,
|
||||
currentSpeedHorr / 100,
|
||||
boxLeft,
|
||||
boxRight,
|
||||
boxTop,
|
||||
boxBottom,
|
||||
elapsedTime
|
||||
);
|
||||
|
||||
// Draw the ray trail
|
||||
drawRayTrail(
|
||||
scene,
|
||||
positions,
|
||||
Math.min(trailLength, 30), // Limit trail length for performance
|
||||
lineWidth / 10,
|
||||
colourContained,
|
||||
fade,
|
||||
objectsToDispose
|
||||
);
|
||||
|
||||
// Draw center-bound rays from collision points
|
||||
positions.forEach(pos => {
|
||||
if (pos.collision) {
|
||||
const angleToCenter = Math.atan2(-pos.y, -pos.x) * 180 / Math.PI;
|
||||
|
||||
const centerTrail = generateCenterRayPositions(
|
||||
pos.x, pos.y, 0,
|
||||
angleToCenter,
|
||||
speed / 8000,
|
||||
Math.min(trailLength / 2, 15) // Limit trail length for performance
|
||||
);
|
||||
|
||||
drawRayTrail(
|
||||
scene,
|
||||
centerTrail,
|
||||
Math.min(trailLength / 2, 15),
|
||||
lineWidth / 10,
|
||||
colourFree,
|
||||
fade,
|
||||
objectsToDispose
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Generate positions for a ray
|
||||
function generateRayPositions(
|
||||
startX, startY, startZ,
|
||||
angle,
|
||||
speed,
|
||||
speedVert,
|
||||
speedHorr,
|
||||
boxLeft,
|
||||
boxRight,
|
||||
boxTop,
|
||||
boxBottom,
|
||||
elapsedTime
|
||||
) {
|
||||
const positions = [];
|
||||
let x = startX;
|
||||
let y = startY;
|
||||
let z = startZ;
|
||||
let currentAngle = angle;
|
||||
let collisionCount = 0;
|
||||
const maxCollisions = 5; // Limit collisions for performance
|
||||
|
||||
// Calculate initial direction
|
||||
const radians = (currentAngle * Math.PI) / 180;
|
||||
let dirX = Math.cos(radians);
|
||||
let dirY = Math.sin(radians);
|
||||
|
||||
// Add starting position
|
||||
positions.push({ x, y, z, angle: currentAngle });
|
||||
|
||||
// Move ray until collision or max collisions reached
|
||||
while (collisionCount < maxCollisions) {
|
||||
// Calculate new position
|
||||
const dx = speedHorr * speed * dirX;
|
||||
const dy = speedVert * speed * dirY;
|
||||
|
||||
x += dx;
|
||||
y += dy;
|
||||
|
||||
// Check for collisions
|
||||
let collision = null;
|
||||
|
||||
// Check horizontal boundaries
|
||||
if (x < boxLeft) {
|
||||
x = boxLeft;
|
||||
currentAngle = 180 - currentAngle;
|
||||
collision = 'left';
|
||||
} else if (x > boxRight) {
|
||||
x = boxRight;
|
||||
currentAngle = 180 - currentAngle;
|
||||
collision = 'right';
|
||||
}
|
||||
|
||||
// Check vertical boundaries
|
||||
if (y < boxTop) {
|
||||
y = boxTop;
|
||||
currentAngle = 360 - currentAngle;
|
||||
collision = 'top';
|
||||
} else if (y > boxBottom) {
|
||||
y = boxBottom;
|
||||
currentAngle = 360 - currentAngle;
|
||||
collision = 'bottom';
|
||||
}
|
||||
|
||||
// Normalize angle
|
||||
currentAngle = ((currentAngle % 360) + 360) % 360;
|
||||
|
||||
// Add position to array
|
||||
positions.push({
|
||||
x, y, z,
|
||||
angle: currentAngle,
|
||||
collision
|
||||
});
|
||||
|
||||
// If collision occurred, update direction and increment counter
|
||||
if (collision) {
|
||||
const newRadians = (currentAngle * Math.PI) / 180;
|
||||
dirX = Math.cos(newRadians);
|
||||
dirY = Math.sin(newRadians);
|
||||
collisionCount++;
|
||||
}
|
||||
|
||||
// Break if we're outside the box
|
||||
if (x < boxLeft * 2 || x > boxRight * 2 || y < boxTop * 2 || y > boxBottom * 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
// Generate center-bound ray positions
|
||||
function generateCenterRayPositions(startX, startY, startZ, angle, speed, trailLength) {
|
||||
const positions = [];
|
||||
let x = startX;
|
||||
let y = startY;
|
||||
let z = startZ;
|
||||
|
||||
// Calculate direction toward center
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
const dirX = Math.cos(radians);
|
||||
const dirY = Math.sin(radians);
|
||||
|
||||
// Add starting position
|
||||
positions.push({ x, y, z, angle });
|
||||
|
||||
// Generate trail points
|
||||
for (let i = 0; i < trailLength; i++) {
|
||||
// Move toward center
|
||||
x += dirX * speed;
|
||||
y += dirY * speed;
|
||||
|
||||
// Add to positions
|
||||
positions.push({ x, y, z, angle });
|
||||
|
||||
// If very close to center, stop
|
||||
if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
// Draw a ray trail
|
||||
function drawRayTrail(scene, positions, maxPoints, lineWidth, color, fade, objectsToDispose) {
|
||||
// Use only a portion of positions for performance
|
||||
const usePositions = positions.slice(0, maxPoints);
|
||||
|
||||
// Create line segments for the ray
|
||||
for (let i = 1; i < usePositions.length; i++) {
|
||||
const prev = usePositions[i - 1];
|
||||
const curr = usePositions[i];
|
||||
|
||||
// Calculate opacity based on position in trail if fade is enabled
|
||||
let opacity = 1;
|
||||
if (fade) {
|
||||
opacity = 1 - (i / usePositions.length);
|
||||
}
|
||||
|
||||
// Create geometry for line segment
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const vertices = new Float32Array([
|
||||
prev.x, prev.y, prev.z,
|
||||
curr.x, curr.y, curr.z
|
||||
]);
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
||||
|
||||
// Create material with appropriate color
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: curr.collision ? 0xffff00 : new THREE.Color(color),
|
||||
transparent: fade,
|
||||
opacity: opacity,
|
||||
linewidth: lineWidth
|
||||
});
|
||||
|
||||
// Create and add the line
|
||||
const line = new THREE.Line(geometry, material);
|
||||
scene.add(line);
|
||||
|
||||
// Track line for disposal
|
||||
if (objectsToDispose) {
|
||||
objectsToDispose.push(line);
|
||||
}
|
||||
}
|
||||
}
|
173
docs/react/src/index.css
Normal file
173
docs/react/src/index.css
Normal file
@@ -0,0 +1,173 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: black;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.App {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100vh !important;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
background-color: rgba(32, 32, 32, 0.8);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
color: #e0e0e0;
|
||||
font-family: Roboto, system-ui;
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.toolbar.hidden {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.control-container {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.shape-selector {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 20px;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #e1ecf4;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #7aa7c7;
|
||||
box-sizing: border-box;
|
||||
color: #1f3f55;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
margin: 5px 2px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #b3d3ea;
|
||||
}
|
||||
|
||||
.button-reset {
|
||||
background-color: #f4e1e1;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 320px;
|
||||
z-index: 11;
|
||||
padding: 10px;
|
||||
background-color: rgba(32, 32, 32, 0.8);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-left: 20px;
|
||||
padding: 10px;
|
||||
border-left: 1px solid #444;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.range-control {
|
||||
width: 100%;
|
||||
background: #444;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.range-control::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background: #e1ecf4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-filter-button {
|
||||
margin-top: 5px;
|
||||
padding: 3px 8px;
|
||||
background-color: #444;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.add-filter-button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.control-row label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.control-row input[type="range"] {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.control-row .value {
|
||||
flex: 0 0 40px;
|
||||
text-align: right;
|
||||
}
|
12
docs/react/src/index.js
Normal file
12
docs/react/src/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
295
docs/react/src/store/animationStore.js
Normal file
295
docs/react/src/store/animationStore.js
Normal file
@@ -0,0 +1,295 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
// Animation definitions with their configurations
|
||||
const animationConfigs = {
|
||||
PolyTwistColourWidth: [
|
||||
{ type: "range", min: 3, max: 10, defaultValue: 5, property: "sides", label: "Sides" },
|
||||
{ type: "range", min: 400, max: 2000, defaultValue: 400, property: "width", label: "Width" },
|
||||
{ type: "range", min: 2, max: 5, defaultValue: 5, property: "lineWidth", label: "Line Width" },
|
||||
{ type: "range", min: 1, max: 100, defaultValue: 50, property: "depth", label: "Depth" },
|
||||
{ type: "range", min: -180, max: 180, defaultValue: -90, property: "rotation", label: "Rotation" },
|
||||
{ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier", label: "Speed" },
|
||||
{ type: "color", defaultValue: "#4287f5", property: "colour1", label: "Color 1" },
|
||||
{ type: "color", defaultValue: "#42f57b", property: "colour2", label: "Color 2" },
|
||||
],
|
||||
FloralPhyllo: [
|
||||
{ type: "range", min: 1, max: 600, defaultValue: 300, property: "width", label: "Width" },
|
||||
{ type: "range", min: 1, max: 300, defaultValue: 150, property: "depth", label: "Depth" },
|
||||
{ type: "range", min: 0, max: 3141, defaultValue: 0, property: "start", label: "Start" },
|
||||
{ type: "color", defaultValue: "#4287f5", property: "colour1", label: "Color 1" },
|
||||
{ type: "color", defaultValue: "#FC0362", property: "colour2", label: "Color 2" },
|
||||
],
|
||||
RaysInShape: [
|
||||
{ type: "range", min: 50, max: 1000, defaultValue: 500, property: "rays", label: "Rays" },
|
||||
{ type: "range", min: 1, max: 30, defaultValue: 2, property: "speed", label: "Speed" },
|
||||
{ type: "checkbox", defaultValue: true, property: "doesWave", label: "Wave Effect" },
|
||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedVertRate", label: "Vertical Rate" },
|
||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedHorrRate", label: "Horizontal Rate" },
|
||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedVert", label: "Vertical Speed" },
|
||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedHorr", label: "Horizontal Speed" },
|
||||
{ type: "range", min: 10, max: 2000, defaultValue: 800, property: "boxSize", label: "Box Size" },
|
||||
{ type: "range", min: 1, max: 80, defaultValue: 5, property: "trailLength", label: "Trail Length" },
|
||||
{ type: "range", min: 1, max: 500, defaultValue: 5, property: "lineWidth", label: "Line Width" },
|
||||
{ type: "checkbox", defaultValue: false, property: "fade", label: "Fade Effect" },
|
||||
{ type: "color", defaultValue: "#43dbad", property: "colourFree", label: "Free Color" },
|
||||
{ type: "color", defaultValue: "#f05c79", property: "colourContained", label: "Contained Color" },
|
||||
{ type: "checkbox", defaultValue: false, property: "boxVisible", label: "Show Box" },
|
||||
]
|
||||
};
|
||||
|
||||
// Generate default parameters for each animation
|
||||
const generateDefaultParameters = (config) => {
|
||||
const defaults = {};
|
||||
config.forEach(item => {
|
||||
defaults[item.property] = item.defaultValue;
|
||||
});
|
||||
return defaults;
|
||||
};
|
||||
|
||||
// Helper function to create an animation state object
|
||||
const createAnimationState = () => {
|
||||
const animations = {};
|
||||
|
||||
Object.keys(animationConfigs).forEach(animName => {
|
||||
animations[animName] = {
|
||||
config: animationConfigs[animName],
|
||||
parameters: generateDefaultParameters(animationConfigs[animName]),
|
||||
baseParameters: generateDefaultParameters(animationConfigs[animName]), // Store original values
|
||||
filters: {} // Will store filters applied to parameters
|
||||
};
|
||||
});
|
||||
|
||||
return animations;
|
||||
};
|
||||
|
||||
// Create the store
|
||||
const useAnimationStore = create((set, get) => ({
|
||||
// Store all animation configurations and their parameters
|
||||
animations: createAnimationState(),
|
||||
|
||||
// Currently selected animation
|
||||
selectedAnimation: 'PolyTwistColourWidth',
|
||||
|
||||
// Animation control
|
||||
paused: false,
|
||||
elapsedTime: 0,
|
||||
rotation: 0,
|
||||
speedMultiplier: 100,
|
||||
|
||||
// Set the selected animation
|
||||
setSelectedAnimation: (animationName) => {
|
||||
set({ selectedAnimation: animationName });
|
||||
},
|
||||
|
||||
// Toggle pause state
|
||||
togglePause: () => {
|
||||
set(state => ({ paused: !state.paused }));
|
||||
},
|
||||
|
||||
// Reset the animation
|
||||
resetAnimation: () => {
|
||||
set({ rotation: 0, elapsedTime: 0 });
|
||||
},
|
||||
|
||||
// Update a parameter for the current animation
|
||||
updateParameter: (property, value) => {
|
||||
set(state => {
|
||||
const animName = state.selectedAnimation;
|
||||
return {
|
||||
animations: {
|
||||
...state.animations,
|
||||
[animName]: {
|
||||
...state.animations[animName],
|
||||
parameters: {
|
||||
...state.animations[animName].parameters,
|
||||
[property]: value
|
||||
},
|
||||
baseParameters: {
|
||||
...state.animations[animName].baseParameters,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Add a filter to a parameter
|
||||
addFilter: (property, filterType = 'sine') => {
|
||||
set(state => {
|
||||
const animName = state.selectedAnimation;
|
||||
const config = state.animations[animName].config.find(c => c.property === property);
|
||||
|
||||
if (!config) return state;
|
||||
|
||||
// Calculate default min and max based on the base parameter value
|
||||
const baseValue = state.animations[animName].baseParameters[property];
|
||||
const range = config.max - config.min;
|
||||
const offset = range * 0.2; // 20% of range
|
||||
|
||||
// Create default filter with min/max values
|
||||
const newFilter = {
|
||||
type: filterType,
|
||||
min: baseValue - offset,
|
||||
max: baseValue + offset,
|
||||
frequency: 0.3, // Hz - cycles per second
|
||||
phase: Math.random() * Math.PI * 2, // Random phase offset between 0 and 2π
|
||||
enabled: true
|
||||
};
|
||||
|
||||
const currentFilters = state.animations[animName].filters[property] || [];
|
||||
|
||||
return {
|
||||
animations: {
|
||||
...state.animations,
|
||||
[animName]: {
|
||||
...state.animations[animName],
|
||||
filters: {
|
||||
...state.animations[animName].filters,
|
||||
[property]: [...currentFilters, newFilter]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Update a filter
|
||||
updateFilter: (property, filterIndex, filterProperty, value) => {
|
||||
set(state => {
|
||||
const animName = state.selectedAnimation;
|
||||
const filters = state.animations[animName].filters[property] || [];
|
||||
|
||||
if (filterIndex >= filters.length) return state;
|
||||
|
||||
const updatedFilters = [...filters];
|
||||
updatedFilters[filterIndex] = {
|
||||
...updatedFilters[filterIndex],
|
||||
[filterProperty]: value
|
||||
};
|
||||
|
||||
return {
|
||||
animations: {
|
||||
...state.animations,
|
||||
[animName]: {
|
||||
...state.animations[animName],
|
||||
filters: {
|
||||
...state.animations[animName].filters,
|
||||
[property]: updatedFilters
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Remove a filter
|
||||
removeFilter: (property, filterIndex) => {
|
||||
set(state => {
|
||||
const animName = state.selectedAnimation;
|
||||
const filters = state.animations[animName].filters[property] || [];
|
||||
|
||||
if (filterIndex >= filters.length) return state;
|
||||
|
||||
const updatedFilters = filters.filter((_, i) => i !== filterIndex);
|
||||
|
||||
return {
|
||||
animations: {
|
||||
...state.animations,
|
||||
[animName]: {
|
||||
...state.animations[animName],
|
||||
filters: {
|
||||
...state.animations[animName].filters,
|
||||
[property]: updatedFilters
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Update the animation state on each frame
|
||||
updateAnimation: (deltaTime) => {
|
||||
set(state => {
|
||||
if (state.paused) return state;
|
||||
|
||||
const newElapsedTime = state.elapsedTime + deltaTime;
|
||||
const newRotation = state.rotation + (deltaTime * state.speedMultiplier / 100);
|
||||
|
||||
// Apply filters to parameters
|
||||
const animName = state.selectedAnimation;
|
||||
const animation = state.animations[animName];
|
||||
const updatedParameters = { ...animation.parameters };
|
||||
|
||||
// Process each parameter that has filters
|
||||
Object.entries(animation.filters).forEach(([property, filters]) => {
|
||||
if (!filters || filters.length === 0) return;
|
||||
|
||||
// Start with the base parameter value (unmodified by filters)
|
||||
const baseValue = animation.baseParameters[property];
|
||||
|
||||
// Find the config for this property to get min/max bounds
|
||||
const config = animation.config.find(c => c.property === property);
|
||||
const propMin = config?.min;
|
||||
const propMax = config?.max;
|
||||
|
||||
// Calculate the combined effect of all filters
|
||||
let totalModification = 0;
|
||||
|
||||
// Apply each filter to build up the modifications
|
||||
filters.forEach(filter => {
|
||||
if (!filter.enabled) return;
|
||||
|
||||
if (filter.type === 'sine') {
|
||||
// Calculate sine wave value based on time and filter properties
|
||||
const frequency = filter.frequency || 0.3; // Hz
|
||||
const phase = filter.phase || 0;
|
||||
const min = filter.min || baseValue - 10;
|
||||
const max = filter.max || baseValue + 10;
|
||||
|
||||
// Calculate center and amplitude from min/max
|
||||
const center = (min + max) / 2;
|
||||
const amplitude = (max - min) / 2;
|
||||
|
||||
// Create sinusoidal oscillation between min and max values
|
||||
// const sineValue = center + Math.sin(newElapsedTime * 2 * Math.PI * frequency + phase) * amplitude;
|
||||
const sineValue = min + amplitude + Math.sin(newElapsedTime * (1 / frequency)) * amplitude;
|
||||
|
||||
// Instead of direct modification, calculate the difference from base
|
||||
const modification = sineValue;
|
||||
|
||||
console.log(`Filter: ${property}, Sine Value: ${sineValue}, Modification: ${modification}`);
|
||||
// Add this filter's contribution to the total
|
||||
totalModification += modification;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Apply total modification to base value
|
||||
// let filteredValue = baseValue + totalModification;
|
||||
let filteredValue = totalModification;
|
||||
|
||||
// Constrain to the property's min/max if available
|
||||
if (propMin !== undefined && filteredValue < propMin) filteredValue = propMin;
|
||||
if (propMax !== undefined && filteredValue > propMax) filteredValue = propMax;
|
||||
|
||||
// Update the parameter with the filtered value
|
||||
updatedParameters[property] = filteredValue;
|
||||
});
|
||||
|
||||
return {
|
||||
elapsedTime: newElapsedTime,
|
||||
rotation: newRotation,
|
||||
animations: {
|
||||
...state.animations,
|
||||
[animName]: {
|
||||
...animation,
|
||||
parameters: updatedParameters
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
export default useAnimationStore;
|
Reference in New Issue
Block a user