So much play
This commit is contained in:
parent
bb736b5448
commit
938227cfa2
Binary file not shown.
After ![]() (image error) Size: 816 KiB |
|
@ -38,7 +38,7 @@ canvas {
|
|||
}
|
||||
}
|
||||
|
||||
#custom {
|
||||
#shape-controls {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
|
@ -50,7 +50,9 @@ canvas {
|
|||
/* background-color: rgba(32, 32, 32, 0.616); */
|
||||
}
|
||||
|
||||
#custom p{
|
||||
|
||||
|
||||
#shape-controls p{
|
||||
color: #e0e0e0;
|
||||
font-family: Roboto, system-ui;
|
||||
}
|
||||
|
@ -59,6 +61,20 @@ canvas {
|
|||
font-family: Roboto, system-ui;
|
||||
}
|
||||
|
||||
.control-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.filter-div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0px 0px 0px 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<!-- ahhh -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<head>
|
||||
<title>Document</title>
|
||||
<title>mathfunk</title>
|
||||
<link rel="stylesheet" href="./css/styles.css">
|
||||
</head>
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
|||
<br>
|
||||
<select id="shape-selector">
|
||||
<option value="RaysInShape">Rays</option>
|
||||
<option value="Countdown">Countdown</option>
|
||||
<option value="NewWave">NewWave</option>
|
||||
<option value="EyePrototype">EyePrototype</option>
|
||||
<option value="Nodal_expanding">Nodal_expanding</option>
|
||||
|
@ -26,7 +27,7 @@
|
|||
<option value="Phyllotaxis">Phyllotaxis</option>
|
||||
<option value="SquareTwist_angle">SquareTwist_angle</option>
|
||||
</select>
|
||||
<div id="custom"></div>
|
||||
<div id="shape-controls"></div>
|
||||
<br>
|
||||
<p>Controls:</p>
|
||||
<p>
|
||||
|
|
|
@ -92,6 +92,10 @@ async function fetchConfig(className) {
|
|||
{ type: "range", min: -180, max: 180, defaultValue: 18, property: "rotate2" },
|
||||
{ type: "range", min: 0, max: 400, defaultValue: 160, property: "width2" },
|
||||
],
|
||||
Countdown: [
|
||||
{ type: "range", min: 300, max: 600, defaultValue: 342, property: "width" },
|
||||
{ type: "range", min: 100, max: 1000, defaultValue: 100, property: "limiter" },
|
||||
],
|
||||
NewWave: [
|
||||
{ type: "range", min: 300, max: 600, defaultValue: 342, property: "width" },
|
||||
{ type: "range", min: 2, max: 40, defaultValue: 4, property: "sides" },
|
||||
|
@ -146,7 +150,7 @@ async function fetchConfig(className) {
|
|||
|
||||
|
||||
function addControl(item, instance) {
|
||||
let parentDiv = document.getElementById("custom");
|
||||
let parentDiv = document.getElementById("shape-controls");
|
||||
|
||||
let title = document.createElement("p");
|
||||
title.innerText = item.property + ": " + item.defaultValue;
|
||||
|
@ -203,6 +207,9 @@ function addControl(item, instance) {
|
|||
control.innerText = item.text;
|
||||
control.className = "header";
|
||||
control.id = "elHeader" + item.text.replace(/\s+/g, '');
|
||||
// Headers are handled differently - add directly to parent
|
||||
parentDiv.appendChild(control);
|
||||
return { element: control, listener: eventListener };
|
||||
}
|
||||
else if (item.type === "color") {
|
||||
control = document.createElement("input");
|
||||
|
@ -224,7 +231,6 @@ function addControl(item, instance) {
|
|||
control.checked = item.defaultValue;
|
||||
instance[item.property] = item.defaultValue;
|
||||
control.id = "el" + item.property;
|
||||
// control.height = "20px";
|
||||
control.addEventListener("change", (event) => {
|
||||
const newValue = event.target.checked;
|
||||
instance[item.property] = newValue;
|
||||
|
@ -234,15 +240,128 @@ function addControl(item, instance) {
|
|||
|
||||
if (item.type != "header") {
|
||||
control.className = "control";
|
||||
// control.id = "el" + item.property;
|
||||
}
|
||||
|
||||
if (item.type != "button" && item.type != "header") {
|
||||
parentDiv.appendChild(title);
|
||||
}
|
||||
parentDiv.appendChild(control);
|
||||
// Create container div for the control
|
||||
let containerDiv = document.createElement("div");
|
||||
containerDiv.className = "control-container";
|
||||
|
||||
return { element: control, listener: eventListener };
|
||||
// Add title and control to container
|
||||
if (item.type != "button") {
|
||||
containerDiv.appendChild(title);
|
||||
}
|
||||
containerDiv.appendChild(control);
|
||||
|
||||
|
||||
let filtersDiv = document.createElement("div");
|
||||
|
||||
// if (item.type === "range") {
|
||||
// let addFilterButton = document.createElement("button");
|
||||
// addFilterButton.innerText = "Add Filter";
|
||||
// addFilterButton.className = "add-filter-button";
|
||||
// addFilterButton.addEventListener("click", () => {
|
||||
// const filterDiv = createFilter(item);
|
||||
// filtersDiv.appendChild(filterDiv);
|
||||
|
||||
// });
|
||||
// filtersDiv.appendChild(addFilterButton);
|
||||
// }
|
||||
|
||||
// Add filters div at the bottom
|
||||
filtersDiv.className = "control-filters";
|
||||
filtersDiv.id = "filters-" + item.property;
|
||||
containerDiv.appendChild(filtersDiv);
|
||||
|
||||
// Add the complete container to parent
|
||||
parentDiv.appendChild(containerDiv);
|
||||
|
||||
return { element: control, listener: eventListener, filtersDiv: filtersDiv };
|
||||
}
|
||||
|
||||
function createFilter(item) {
|
||||
const filterDiv = document.createElement("div");
|
||||
filterDiv.className = "filter-div";
|
||||
filterDiv.innerText = "sin filter"; // Placeholder text
|
||||
|
||||
|
||||
let minTitle = document.createElement("p");
|
||||
minTitle.innerText = "Min:" + item.defaultValue;
|
||||
filterDiv.appendChild(minTitle);
|
||||
|
||||
let sinMin = document.createElement("input");
|
||||
sinMin.type = "range";
|
||||
sinMin.id = "el-filter-" + item.property;
|
||||
sinMin.min = -item.max;//item.min;
|
||||
sinMin.max = item.max;
|
||||
sinMin.value = item.defaultValue;
|
||||
|
||||
eventListener = (event) => {
|
||||
const newValue = parseInt(event.target.value, 10);
|
||||
// instance[item.property] = newValue;
|
||||
minTitle.innerText = "Min: " + newValue;
|
||||
|
||||
if (item.callback) {
|
||||
item.callback(instance, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
sinMin.addEventListener("input", eventListener);
|
||||
filterDiv.appendChild(sinMin);
|
||||
|
||||
let maxTitle = document.createElement("p");
|
||||
maxTitle.innerText = "Max:" + item.defaultValue;
|
||||
filterDiv.appendChild(maxTitle);
|
||||
|
||||
|
||||
let sinMax = document.createElement("input");
|
||||
sinMax.type = "range";
|
||||
sinMax.id = "el-filter-" + item.property;
|
||||
sinMax.min = item.min;
|
||||
sinMax.max = item.max;
|
||||
sinMax.value = item.defaultValue;
|
||||
|
||||
eventListener = (event) => {
|
||||
const newValue = parseInt(event.target.value, 10);
|
||||
// instance[item.property] = newValue;
|
||||
maxTitle.innerText = "Max: " + newValue;
|
||||
|
||||
if (item.callback) {
|
||||
item.callback(instance, newValue);
|
||||
}
|
||||
};
|
||||
sinMax.addEventListener("input", eventListener);
|
||||
filterDiv.appendChild(sinMax);
|
||||
|
||||
let rate = createFilterSlider("Rate", item, filterDiv);
|
||||
|
||||
return { filterDiv, min: sinMin, max: sinMax, rate: rate};
|
||||
}
|
||||
|
||||
function createFilterSlider(name, item, filterDiv) {
|
||||
let minTitle = document.createElement("p");
|
||||
minTitle.innerText = name + ":" + item.defaultValue;
|
||||
filterDiv.appendChild(minTitle);
|
||||
|
||||
let sinMin = document.createElement("input");
|
||||
sinMin.type = "range";
|
||||
sinMin.id = "el-filter-" + item.property;
|
||||
sinMin.min = item.min;
|
||||
sinMin.max = item.max;
|
||||
sinMin.value = item.defaultValue;
|
||||
|
||||
eventListener = (event) => {
|
||||
const newValue = parseInt(event.target.value, 10);
|
||||
// instance[item.property] = newValue;
|
||||
minTitle.innerText = name + ": " + newValue;
|
||||
|
||||
if (item.callback) {
|
||||
item.callback(instance, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
sinMin.addEventListener("input", eventListener);
|
||||
filterDiv.appendChild(sinMin);
|
||||
return sinMin;
|
||||
}
|
||||
|
||||
function updateControlInput(value, controlName) {// Find and update the slider element
|
||||
|
|
|
@ -21,6 +21,7 @@ let drawObj = null;
|
|||
function createInstance(className, args) {
|
||||
const classMap = {
|
||||
NewWave: NewWave,
|
||||
Countdown: Countdown,
|
||||
RaysInShape: RaysInShape,
|
||||
PolyTwistColourWidth: PolyTwistColourWidth,
|
||||
FloralPhyllo: FloralPhyllo,
|
||||
|
@ -88,9 +89,9 @@ function render(timestamp) {
|
|||
|
||||
}
|
||||
|
||||
ctx.font = "48px serif";
|
||||
ctx.fillStyle = "white"
|
||||
ctx.fillText(Math.floor(elapsedTime) + "ms", centerX - 100, centerY + 400);
|
||||
// ctx.font = "48px serif";
|
||||
// ctx.fillStyle = "white"
|
||||
// ctx.fillText(Math.floor(elapsedTime) + "ms", centerX - 100, centerY + 400);
|
||||
// drawCenter(300)
|
||||
|
||||
requestAnimationFrame(render);
|
||||
|
|
|
@ -6,8 +6,39 @@ class BaseShape {
|
|||
|
||||
initialise(config) {
|
||||
for (let item of config) {
|
||||
const { element, listener } = addControl(item, this);
|
||||
this.controls.push({ element, listener });
|
||||
const { element, listener, filtersDiv } = addControl(item, this);
|
||||
this.controls.push({ element, listener, });
|
||||
|
||||
if (item.type === "range" && item.property !== "rays") {
|
||||
// Initialize rangeFilter array for this control
|
||||
const controlIndex = this.controls.length - 1;
|
||||
this.controls[controlIndex].rangeFilters = [];
|
||||
|
||||
let addFilterButton = document.createElement("button");
|
||||
addFilterButton.innerText = "Add Filter";
|
||||
addFilterButton.className = "add-filter-button";
|
||||
|
||||
// Store the control index in the click handler closure
|
||||
addFilterButton.addEventListener("click", () => {
|
||||
const { filterDiv, eventListener, min, max, rate } = createFilter(item);
|
||||
filtersDiv.appendChild(filterDiv);
|
||||
|
||||
// Use the stored control index
|
||||
if (this.controls[controlIndex] && this.controls[controlIndex].rangeFilters) {
|
||||
this.controls[controlIndex].rangeFilters.push({
|
||||
element: filterDiv,
|
||||
listener: eventListener,
|
||||
min: min,
|
||||
max: max,
|
||||
rate: rate,
|
||||
});
|
||||
} else {
|
||||
console.error("Control or rangeFilters not found for index:", controlIndex);
|
||||
}
|
||||
});
|
||||
|
||||
filtersDiv.appendChild(addFilterButton);
|
||||
}
|
||||
}
|
||||
|
||||
const { element, listener } = addControl({ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier" }, this);
|
||||
|
@ -24,17 +55,57 @@ class BaseShape {
|
|||
else {
|
||||
console.log("Element or listener not found for removal:", element, listener);
|
||||
}
|
||||
if (element && element.parentElement) {
|
||||
element.parentElement.removeChild(element);
|
||||
const titleElement = document.getElementById("elText" + element.id.slice(2));
|
||||
if (titleElement) {
|
||||
titleElement.parentElement.removeChild(titleElement);
|
||||
|
||||
// Find and remove the container div instead of individual elements
|
||||
if (element && element.id) {
|
||||
// Handle header elements which don't have container
|
||||
if (element.className === "header") {
|
||||
if (element.parentElement) {
|
||||
element.parentElement.removeChild(element);
|
||||
}
|
||||
} else {
|
||||
// For regular controls, find and remove the container
|
||||
const containerDiv = element.closest(".control-container");
|
||||
if (containerDiv && containerDiv.parentElement) {
|
||||
containerDiv.parentElement.removeChild(containerDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.controls = [];
|
||||
}
|
||||
|
||||
updateFilters(elapsed) {
|
||||
for (let i = 0; i < this.controls.length; i++) {
|
||||
const control = this.controls[i];
|
||||
|
||||
if (control.rangeFilters?.length > 0) {
|
||||
let newValue = 0;
|
||||
for (let j = 0; j < control.rangeFilters.length; j++) {
|
||||
const filter = control.rangeFilters[j];
|
||||
// const value = parseFloat(filter.element.value);
|
||||
const min = parseFloat(filter.min.value);
|
||||
const max = parseFloat(filter.max.value);
|
||||
const rate = parseFloat(filter.rate.value);
|
||||
|
||||
const halfRange = (max - min) / 2;
|
||||
const filterValue = min + halfRange + Math.sin(elapsed * (1 / rate)) * halfRange; // Calculate the new value based on the range
|
||||
|
||||
if (filterValue >= min && filterValue <= max) {
|
||||
// console.log(newValue, min, max)
|
||||
newValue += filterValue;
|
||||
console.log("New Value:", newValue, filterValue, min, max);
|
||||
}
|
||||
}
|
||||
|
||||
control.element.value = newValue;
|
||||
const event = new Event('input', { bubbles: true });
|
||||
control.element.dispatchEvent(event);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
throw new Error("Draw function not implemented");
|
||||
}
|
||||
|
@ -653,6 +724,7 @@ class NewWave extends BaseShape {
|
|||
|
||||
draw(rotation) {
|
||||
rotation *= this.speedMultiplier / 400
|
||||
this.updateFilters(rotation);
|
||||
ctx.lineWidth = this.lineWidth
|
||||
for (let j = 0; j < this.sides; j++) {
|
||||
const radRotation = rad(360 / this.sides * j)
|
||||
|
@ -679,6 +751,60 @@ class NewWave extends BaseShape {
|
|||
}
|
||||
}
|
||||
|
||||
class Countdown extends BaseShape {
|
||||
constructor() {
|
||||
super();
|
||||
this.width;
|
||||
this.sides;
|
||||
}
|
||||
|
||||
secondsUntilDate(targetDate) {
|
||||
const now = new Date();
|
||||
const target = new Date(targetDate);
|
||||
const difference = target.getTime() - now.getTime();
|
||||
return Math.round(difference / 1000);
|
||||
}
|
||||
|
||||
drawProgressBar(progress, barWidth) {
|
||||
const colourBackground = "#0c2f69";
|
||||
const colourProgress = "#4287f5";
|
||||
// const barWidth = 400;
|
||||
const barHeight = 60;
|
||||
const barX = centerX - barWidth / 2;
|
||||
const barY = centerY + 350 - barHeight / 2;
|
||||
|
||||
ctx.fillStyle = colourBackground;
|
||||
ctx.beginPath();
|
||||
ctx.rect(barX, barY, barWidth, 60)
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = colourProgress;
|
||||
ctx.beginPath();
|
||||
ctx.rect(barX, barY, (barWidth/100)*progress, 60)
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
draw(elapsedTime) {
|
||||
// elapsedTime *= this.speedMultiplier / 400
|
||||
|
||||
ctx.font = "48px serif";
|
||||
ctx.fillStyle = "white"
|
||||
const futureDate = '2025-05-31T08:20:00';
|
||||
const seconds = this.secondsUntilDate(futureDate);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const percentRounded = (((elapsedTime / 1000) / seconds) * 100 ).toFixed(8);
|
||||
ctx.fillText(seconds + " Seconds", centerX - 100, centerY);
|
||||
ctx.fillText(minutes + " Minues", centerX - 100, centerY + 100);
|
||||
ctx.fillText(hours + " Hours", centerX - 100, centerY + 200);
|
||||
ctx.fillText(percentRounded + "% Closer", centerX - 100, centerY + 300);
|
||||
|
||||
// ctx.fillText(percentRounded + "% Closer", centerX - 100, centerY + 300);
|
||||
// this.drawProgressBar(percentRounded,400);
|
||||
this.drawProgressBar(percentRounded,1000);
|
||||
}
|
||||
}
|
||||
|
||||
class RaysInShape extends BaseShape {
|
||||
constructor(rays, speed, doesWave, speedVertRate, speedHorrRate, speedVert, speedHorr, boxSize, trailLength = 50, lineWidth, fade, colourFree, colourContained, boxVisible,) {
|
||||
super();
|
||||
|
@ -702,9 +828,39 @@ class RaysInShape extends BaseShape {
|
|||
|
||||
initialise(config) { //is overide
|
||||
for (let item of config) {
|
||||
const { element, listener } = addControl(item, this);
|
||||
this.controls.push({ element, listener });
|
||||
const { element, listener, filtersDiv } = addControl(item, this);
|
||||
this.controls.push({ element, listener, });
|
||||
|
||||
if (item.type === "range" && item.property !== "rays") {
|
||||
// Initialize rangeFilter array for this control
|
||||
const controlIndex = this.controls.length - 1;
|
||||
this.controls[controlIndex].rangeFilters = [];
|
||||
|
||||
let addFilterButton = document.createElement("button");
|
||||
addFilterButton.innerText = "Add Filter";
|
||||
addFilterButton.className = "add-filter-button";
|
||||
|
||||
// Store the control index in the click handler closure
|
||||
addFilterButton.addEventListener("click", () => {
|
||||
const { filterDiv, eventListener, min, max, rate } = createFilter(item);
|
||||
filtersDiv.appendChild(filterDiv);
|
||||
|
||||
// Use the stored control index
|
||||
if (this.controls[controlIndex] && this.controls[controlIndex].rangeFilters) {
|
||||
this.controls[controlIndex].rangeFilters.push({
|
||||
element: filterDiv,
|
||||
listener: eventListener,
|
||||
min: min,
|
||||
max: max,
|
||||
rate: rate,
|
||||
});
|
||||
} else {
|
||||
console.error("Control or rangeFilters not found for index:", controlIndex);
|
||||
}
|
||||
});
|
||||
|
||||
filtersDiv.appendChild(addFilterButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Add controls for speed multiplier and trail length
|
||||
|
@ -841,9 +997,13 @@ class RaysInShape extends BaseShape {
|
|||
this.prepareRayObjects(); // Reinitialize rayObjects with the new number of rays
|
||||
}
|
||||
|
||||
|
||||
|
||||
draw(elapsed, deltaTime) {
|
||||
deltaTime *= this.speedMultiplier / 100;
|
||||
|
||||
this.updateFilters(elapsed);
|
||||
|
||||
if (this.doesWave) {
|
||||
const vertRate = this.speedVertRate / 100;
|
||||
const horrRate = this.speedHorrRate / 100;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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;
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -0,0 +1,12 @@
|
|||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
|
@ -0,0 +1,33 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.0.7",
|
||||
"@react-three/fiber": "^9.1.2",
|
||||
"leva": "^0.10.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"three": "^0.176.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"vite": "^6.3.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After (image error) Size: 1.5 KiB |
|
@ -0,0 +1,21 @@
|
|||
/* Remove default App.css styles if they interfere */
|
||||
/* You might want to keep some or none depending on desired layout */
|
||||
|
||||
/* Example: Remove padding and max-width from #root if set here */
|
||||
/* #root {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
} */
|
||||
|
||||
/* Remove logo styles etc. */
|
||||
/* .logo { ... } */
|
||||
/* .logo:hover { ... } */
|
||||
/* ... etc ... */
|
||||
|
||||
/* @keyframes logo-spin { ... } */
|
||||
|
||||
/* @media (prefers-reduced-motion: no-preference) { ... } */
|
||||
|
||||
/* .card { ... } */
|
||||
|
||||
/* .read-the-docs { ... } */
|
|
@ -0,0 +1,59 @@
|
|||
import { Canvas } from '@react-three/fiber'
|
||||
import { OrbitControls } from '@react-three/drei'
|
||||
import { useControls, Leva } from 'leva' // Import leva
|
||||
import PhyllotaxisSystem from './PhyllotaxisSystem' // Import Phyllotaxis system
|
||||
// import PolyTwistSystem from './PolyTwistSystem' // Import PolyTwist system
|
||||
import './App.css'
|
||||
|
||||
// Define animation options
|
||||
const animationOptions = {
|
||||
Phyllotaxis: 'Phyllotaxis',
|
||||
PolyTwist: 'PolyTwist',
|
||||
// Add more animation names here as you implement them
|
||||
}
|
||||
|
||||
function App() {
|
||||
// Leva controls for animation selection and global settings
|
||||
const { selectedAnimation, speedMultiplier } = useControls({
|
||||
selectedAnimation: {
|
||||
options: animationOptions,
|
||||
value: animationOptions.Phyllotaxis, // Default selection
|
||||
label: 'Animation',
|
||||
},
|
||||
speedMultiplier: {
|
||||
value: 1.0, // Global speed multiplier
|
||||
min: 0.01,
|
||||
max: 5.0,
|
||||
step: 0.01,
|
||||
label: 'Global Speed',
|
||||
},
|
||||
})
|
||||
|
||||
// Function to render the selected animation component
|
||||
const renderSelectedAnimation = () => {
|
||||
switch (selectedAnimation) {
|
||||
case animationOptions.Phyllotaxis:
|
||||
return <PhyllotaxisSystem speedMultiplier={speedMultiplier} />
|
||||
// case animationOptions.PolyTwist:
|
||||
// return <PolyTwistSystem speedMultiplier={speedMultiplier} />
|
||||
// Add cases for other animations
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Leva panel will be added here automatically */}
|
||||
<Leva /> {/* Add the Leva panel, initially collapsed */}
|
||||
<Canvas camera={{ position: [0, 0, 500], fov: 75 }}> {/* Adjusted camera Z */}
|
||||
<ambientLight intensity={0.8} /> {/* Increased ambient light */}
|
||||
<pointLight position={[100, 100, 100]} intensity={1.5} /> {/* Adjusted point light */}
|
||||
{renderSelectedAnimation()} {/* Render the chosen animation */}
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
|
@ -0,0 +1,130 @@
|
|||
import React, { useRef, useMemo } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { useControls, folder } from 'leva'; // Import leva controls
|
||||
|
||||
// Default values matching original config
|
||||
const DEFAULTS = {
|
||||
width: 24,
|
||||
nMax: 300,
|
||||
color1: '#2D81FC',
|
||||
color2: '#FC0362',
|
||||
pointSize: 5.0, // Added point size control
|
||||
};
|
||||
|
||||
function PhyllotaxisSystem({ speedMultiplier }) { // Accept global speed multiplier
|
||||
const pointsRef = useRef();
|
||||
const positionBufferRef = useRef();
|
||||
const colorBufferRef = useRef();
|
||||
|
||||
// Leva controls specific to Phyllotaxis
|
||||
const { width, nMax, color1, color2, pointSize } = useControls('Phyllotaxis', {
|
||||
// Group controls in a folder
|
||||
width: { value: DEFAULTS.width, min: 1, max: 50, step: 1, label: 'Width Factor' },
|
||||
nMax: { value: DEFAULTS.nMax, min: 10, max: 5000, step: 10, label: 'Particle Count' },
|
||||
pointSize: { value: DEFAULTS.pointSize, min: 0.1, max: 20, step: 0.1, label: 'Point Size' },
|
||||
Colors: folder({ // Sub-folder for colors
|
||||
color1: { value: DEFAULTS.color1, label: 'Color 1' },
|
||||
color2: { value: DEFAULTS.color2, label: 'Color 2' },
|
||||
})
|
||||
});
|
||||
|
||||
// Memoize particle data generation, recalculate if controls change
|
||||
const particles = useMemo(() => {
|
||||
const positions = new Float32Array(nMax * 3);
|
||||
const colors = new Float32Array(nMax * 3);
|
||||
const tempColor = new THREE.Color();
|
||||
const col1 = new THREE.Color(color1);
|
||||
const col2 = new THREE.Color(color2);
|
||||
|
||||
for (let i = 0; i < nMax; i++) {
|
||||
const n = i;
|
||||
const radius = width * Math.sqrt(n);
|
||||
const angle = 0; // Initial angle
|
||||
|
||||
positions[i * 3] = radius * Math.cos(angle);
|
||||
positions[i * 3 + 1] = radius * Math.sin(angle);
|
||||
positions[i * 3 + 2] = 0;
|
||||
|
||||
tempColor.copy(col1).lerp(col2, n / nMax);
|
||||
colors[i * 3] = tempColor.r;
|
||||
colors[i * 3 + 1] = tempColor.g;
|
||||
colors[i * 3 + 2] = tempColor.b;
|
||||
}
|
||||
return { positions, colors };
|
||||
// Dependencies: recalculate when these control values change
|
||||
}, [nMax, width, color1, color2]);
|
||||
|
||||
// Update positions in the animation loop
|
||||
useFrame((state) => {
|
||||
if (!positionBufferRef.current || !colorBufferRef.current) return;
|
||||
|
||||
// Use combined speed (global * local if needed, here just global)
|
||||
const time = state.clock.elapsedTime * speedMultiplier * 0.3; // Adjusted speed factor
|
||||
const positions = positionBufferRef.current.array;
|
||||
|
||||
for (let i = 0; i < nMax; i++) {
|
||||
const n = i;
|
||||
const radius = width * Math.sqrt(n);
|
||||
const angle = n * time; // Angle driven by time and index
|
||||
|
||||
positions[i * 3] = radius * Math.cos(angle);
|
||||
positions[i * 3 + 1] = radius * Math.sin(angle);
|
||||
// z remains 0
|
||||
}
|
||||
positionBufferRef.current.needsUpdate = true;
|
||||
|
||||
// Update point size if material exists
|
||||
if (pointsRef.current && pointsRef.current.material) {
|
||||
pointsRef.current.material.size = pointSize;
|
||||
}
|
||||
});
|
||||
|
||||
// Effect to update buffers when particles data changes (due to control changes)
|
||||
React.useEffect(() => {
|
||||
if (positionBufferRef.current) {
|
||||
positionBufferRef.current.array = particles.positions;
|
||||
positionBufferRef.current.needsUpdate = true;
|
||||
}
|
||||
if (colorBufferRef.current) {
|
||||
colorBufferRef.current.array = particles.colors;
|
||||
colorBufferRef.current.needsUpdate = true;
|
||||
}
|
||||
}, [particles]);
|
||||
|
||||
|
||||
return (
|
||||
<points ref={pointsRef}>
|
||||
<bufferGeometry>
|
||||
{/* Use keys to force re-creation if nMax changes drastically */}
|
||||
<bufferAttribute
|
||||
key={`pos-${nMax}`}
|
||||
ref={positionBufferRef}
|
||||
attach="attributes-position"
|
||||
count={particles.positions.length / 3} // Use actual length
|
||||
array={particles.positions}
|
||||
itemSize={3}
|
||||
usage={THREE.DynamicDrawUsage} // Mark as dynamic
|
||||
/>
|
||||
<bufferAttribute
|
||||
key={`col-${nMax}`}
|
||||
ref={colorBufferRef}
|
||||
attach="attributes-color"
|
||||
count={particles.colors.length / 3} // Use actual length
|
||||
array={particles.colors}
|
||||
itemSize={3}
|
||||
usage={THREE.DynamicDrawUsage} // Mark as dynamic
|
||||
/>
|
||||
</bufferGeometry>
|
||||
<pointsMaterial
|
||||
size={pointSize}
|
||||
vertexColors={true}
|
||||
sizeAttenuation={true}
|
||||
depthWrite={false} // Often good for particles
|
||||
blending={THREE.AdditiveBlending} // Example blending mode
|
||||
/>
|
||||
</points>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhyllotaxisSystem;
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After (image error) Size: 4.0 KiB |
|
@ -0,0 +1,44 @@
|
|||
// filepath: c:\Users\samkl\Documents\GitHub\animate\docs\react\src\filters.js
|
||||
import * as THREE from 'three'; // Might be needed for future filters
|
||||
|
||||
// Additive filter application function
|
||||
export function applyFilters(baseValue, filters = [], time) {
|
||||
let offset = 0;
|
||||
filters.forEach(filter => {
|
||||
switch (filter.type) {
|
||||
case 'sin':
|
||||
// Ensure defaults if properties are missing
|
||||
offset += Math.sin(time * (filter.frequency ?? 1)) * (filter.amplitude ?? 0);
|
||||
break;
|
||||
// --- Add Noise Filter ---
|
||||
case 'noise':
|
||||
// Simple pseudo-random noise using time and an offset
|
||||
// Use a combination of sin functions for a smoother noise
|
||||
const noiseVal = (Math.sin(time * (filter.frequency ?? 1) * 1.3 + filter.id * 10) + Math.sin(time * (filter.frequency ?? 1) * 2.7 + filter.id * 5)) / 2;
|
||||
offset += noiseVal * (filter.amplitude ?? 0);
|
||||
break;
|
||||
// --- End Noise Filter ---
|
||||
default:
|
||||
console.warn(`Unknown filter type: ${filter.type}`);
|
||||
}
|
||||
});
|
||||
return baseValue + offset;
|
||||
}
|
||||
|
||||
// Function to get default parameters for a filter type
|
||||
export function getDefaultFilterParams(type) {
|
||||
switch (type) {
|
||||
case 'sin':
|
||||
return { amplitude: 10, frequency: 1 };
|
||||
case 'noise': // Default for noise
|
||||
return { amplitude: 5, frequency: 1 }; // Added frequency for noise control
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Available filter types (for dropdowns)
|
||||
export const FILTER_TYPES = {
|
||||
SIN: 'sin',
|
||||
NOISE: 'noise',
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #000000;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* Remove display flex and place-items */
|
||||
/* display: flex; */
|
||||
/* place-items: center; */
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
overflow: hidden; /* Prevent scrollbars from body */
|
||||
}
|
||||
|
||||
/* Make root and canvas full screen */
|
||||
#root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* Remove text-align center if you don't want leva centered initially */
|
||||
/* text-align: center; */
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block; /* Ensure canvas takes up block space */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react' // Use React instead of StrictMode for now
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<App />
|
||||
)
|
|
@ -0,0 +1,138 @@
|
|||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { button, buttonGroup, folder, monitor } from 'leva';
|
||||
import { applyFilters, getDefaultFilterParams, FILTER_TYPES } from './filters';
|
||||
|
||||
/**
|
||||
* Custom hook to manage a numeric control value with additive filters in Leva.
|
||||
*
|
||||
* @param {string} key - A unique key for this control (used in Leva schema).
|
||||
* @param {number} initialValue - The initial base value.
|
||||
* @param {object} options - Leva control options (min, max, step, label, etc.).
|
||||
* @returns {[Function, object]} A tuple containing:
|
||||
* - A function `getFilteredValue(time)` that returns the value with filters applied for the given time.
|
||||
* - A Leva schema fragment for this control and its filters.
|
||||
*/
|
||||
export function useFilteredControl(key, initialValue, options = {}) {
|
||||
const [baseValue, setBaseValue] = useState(initialValue);
|
||||
const [filters, setFilters] = useState([]);
|
||||
|
||||
const label = options.label || key;
|
||||
|
||||
// --- Filter Management Functions ---
|
||||
const addFilter = useCallback((type) => {
|
||||
const newFilter = {
|
||||
id: Date.now() + Math.random(),
|
||||
type: type,
|
||||
...getDefaultFilterParams(type),
|
||||
};
|
||||
setFilters((prev) => [...prev, newFilter]);
|
||||
}, []);
|
||||
|
||||
const removeFilter = useCallback((id) => {
|
||||
setFilters((prev) => prev.filter((f) => f.id !== id));
|
||||
}, []);
|
||||
|
||||
const updateFilter = useCallback((id, newParams) => {
|
||||
setFilters((prev) =>
|
||||
prev.map((f) => (f.id === id ? { ...f, ...newParams } : f))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
setFilters([]);
|
||||
}, []);
|
||||
|
||||
// --- Leva Schema Generation ---
|
||||
const schema = useMemo(() => {
|
||||
const controlSchema = {};
|
||||
|
||||
// 1. Base Value Slider
|
||||
controlSchema[key] = {
|
||||
...options,
|
||||
value: baseValue,
|
||||
label: `Base ${label}`,
|
||||
onChange: setBaseValue,
|
||||
};
|
||||
|
||||
// 2. Filters Folder
|
||||
const filterControls = {};
|
||||
// Prefix keys with the control key to ensure uniqueness
|
||||
filterControls[`${key}_Add Filter`] = buttonGroup({
|
||||
// Dynamically create buttons for available filter types
|
||||
...Object.entries(FILTER_TYPES).reduce((acc, [name, type]) => {
|
||||
acc[name] = () => addFilter(type);
|
||||
return acc;
|
||||
}, {}),
|
||||
});
|
||||
filterControls[`${key}_Applied`] = monitor(filters.length, { graph: false });
|
||||
filterControls[`${key}_Clear`] = button(clearFilters, {
|
||||
label: 'Clear All',
|
||||
disabled: filters.length === 0
|
||||
});
|
||||
|
||||
// 3. Individual Filter Controls
|
||||
filters.forEach((filter, index) => {
|
||||
const filterKey = `${key}_filter_${filter.id}`;
|
||||
const filterFolderContent = {};
|
||||
|
||||
filterFolderContent[`type_${filter.id}`] = {
|
||||
value: filter.type,
|
||||
options: Object.values(FILTER_TYPES),
|
||||
label: 'Type',
|
||||
onChange: (newType) => updateFilter(filter.id, { type: newType, ...getDefaultFilterParams(newType) }),
|
||||
};
|
||||
|
||||
// Common Amplitude Control (adjust range based on param if needed)
|
||||
filterFolderContent[`amp_${filter.id}`] = {
|
||||
value: filter.amplitude,
|
||||
min: 0,
|
||||
max: options.max ? options.max * 0.5 : 50, // Example: Max amplitude is half of base max
|
||||
step: (options.max ? options.max * 0.5 : 50) / 100, // Example step
|
||||
label: 'Amplitude',
|
||||
onChange: (v) => updateFilter(filter.id, { amplitude: v }),
|
||||
};
|
||||
|
||||
// Common Frequency Control
|
||||
filterFolderContent[`freq_${filter.id}`] = {
|
||||
value: filter.frequency,
|
||||
min: 0.1,
|
||||
max: 10,
|
||||
step: 0.1,
|
||||
label: 'Frequency',
|
||||
onChange: (v) => updateFilter(filter.id, { frequency: v }),
|
||||
};
|
||||
|
||||
filterFolderContent[`remove_${filter.id}`] = button(() => removeFilter(filter.id), { label: 'Remove' });
|
||||
|
||||
// Add the folder for this specific filter
|
||||
filterControls[filterKey] = folder(filterFolderContent, {
|
||||
collapsed: true,
|
||||
label: `${label} Filter ${index + 1} (${filter.type})`,
|
||||
});
|
||||
});
|
||||
|
||||
// Add the main filters folder under the base control's key
|
||||
controlSchema[`${label} Filters`] = folder(filterControls);
|
||||
|
||||
return controlSchema;
|
||||
|
||||
}, [key, label, options, baseValue, filters, addFilter, removeFilter, updateFilter, clearFilters]);
|
||||
|
||||
// --- Filtered Value Calculation ---
|
||||
const getFilteredValue = useCallback((time) => {
|
||||
// Ensure baseValue is treated as a number
|
||||
const numericBaseValue = Number(baseValue);
|
||||
if (isNaN(numericBaseValue)) {
|
||||
console.warn(`Base value for ${key} is not a number:`, baseValue);
|
||||
return 0; // Or initialValue
|
||||
}
|
||||
const filtered = applyFilters(numericBaseValue, filters, time);
|
||||
// Optional: Clamp the filtered value to the original min/max if desired
|
||||
if (options.min !== undefined && options.max !== undefined) {
|
||||
return Math.max(options.min, Math.min(filtered, options.max));
|
||||
}
|
||||
return filtered;
|
||||
}, [baseValue, filters, key, options.min, options.max]);
|
||||
|
||||
return [getFilteredValue, schema];
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
Loading…
Reference in New Issue