math-animation1 / index.html
arirajuns's picture
Add 2 files
d6aa7b4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Math Animation Generator</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ccapture.js/1.1.0/CCapture.all.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
body {
font-family: 'Space Mono', monospace;
background-color: #0f172a;
color: #e2e8f0;
overflow-x: hidden;
}
.canvas-container {
position: relative;
width: 100%;
height: 70vh;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.control-panel {
background-color: #1e293b;
border-radius: 1rem;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
}
.recording-indicator {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.gradient-text {
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.tab-button {
transition: all 0.3s ease;
}
.tab-button.active {
background-color: #3b82f6;
color: white;
}
input[type="range"] {
-webkit-appearance: none;
height: 6px;
background: #334155;
border-radius: 3px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #3b82f6;
border-radius: 50%;
cursor: pointer;
}
.color-picker {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
cursor: pointer;
background-color: transparent;
}
.color-picker::-webkit-color-swatch {
border-radius: 50%;
border: 2px solid #334155;
}
.color-picker::-moz-color-swatch {
border-radius: 50%;
border: 2px solid #334155;
}
</style>
</head>
<body class="min-h-screen p-4 md:p-8">
<div class="max-w-7xl mx-auto">
<header class="mb-8 text-center">
<h1 class="text-4xl md:text-5xl font-bold mb-2 gradient-text">Math Animation Generator</h1>
<p class="text-lg text-slate-400">Create beautiful mathematical animations and export them as MP4</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
<div class="canvas-container">
<canvas id="animationCanvas"></canvas>
<div id="recordingOverlay" class="absolute top-4 right-4 hidden items-center bg-red-600 text-white px-3 py-1 rounded-full">
<span class="recording-indicator mr-2"></span>
<span id="recordingTimer">00:60</span>
</div>
</div>
</div>
<div class="control-panel p-6">
<div class="flex space-x-2 mb-6">
<button class="tab-button active px-4 py-2 rounded-md" data-tab="presets">Presets</button>
<button class="tab-button px-4 py-2 rounded-md" data-tab="parameters">Parameters</button>
<button class="tab-button px-4 py-2 rounded-md" data-tab="export">Export</button>
</div>
<div id="presetsTab" class="tab-content">
<h3 class="text-xl font-semibold mb-4">Animation Types</h3>
<div class="grid grid-cols-2 gap-4 mb-6">
<button class="animation-preset bg-slate-700 hover:bg-slate-600 p-4 rounded-lg transition" data-preset="lissajous">
<div class="text-blue-400 font-bold">Lissajous Curves</div>
<div class="text-sm text-slate-400">Parametric equations</div>
</button>
<button class="animation-preset bg-slate-700 hover:bg-slate-600 p-4 rounded-lg transition" data-preset="fractal">
<div class="text-purple-400 font-bold">Mandelbrot Set</div>
<div class="text-sm text-slate-400">Fractal visualization</div>
</button>
<button class="animation-preset bg-slate-700 hover:bg-slate-600 p-4 rounded-lg transition" data-preset="particles">
<div class="text-green-400 font-bold">Particle System</div>
<div class="text-sm text-slate-400">Mathematical attractors</div>
</button>
<button class="animation-preset bg-slate-700 hover:bg-slate-600 p-4 rounded-lg transition" data-preset="fourier">
<div class="text-yellow-400 font-bold">Fourier Series</div>
<div class="text-sm text-slate-400">Wave decomposition</div>
</button>
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Background Color</label>
<div class="flex items-center">
<input type="color" class="color-picker" id="bgColor" value="#0f172a">
<span class="ml-2 text-sm" id="bgColorValue">#0f172a</span>
</div>
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Foreground Color</label>
<div class="flex items-center">
<input type="color" class="color-picker" id="fgColor" value="#3b82f6">
<span class="ml-2 text-sm" id="fgColorValue">#3b82f6</span>
</div>
</div>
</div>
<div id="parametersTab" class="tab-content hidden">
<h3 class="text-xl font-semibold mb-4">Animation Parameters</h3>
<div id="lissajousParams" class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Frequency X</label>
<input type="range" min="1" max="10" step="0.1" value="3" class="w-full" id="freqX">
<div class="flex justify-between text-xs text-slate-400">
<span>1</span>
<span id="freqXValue">3</span>
<span>10</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Frequency Y</label>
<input type="range" min="1" max="10" step="0.1" value="2" class="w-full" id="freqY">
<div class="flex justify-between text-xs text-slate-400">
<span>1</span>
<span id="freqYValue">2</span>
<span>10</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Phase Shift</label>
<input type="range" min="0" max="6.28" step="0.01" value="0" class="w-full" id="phaseShift">
<div class="flex justify-between text-xs text-slate-400">
<span>0</span>
<span id="phaseShiftValue">0</span>
<span>6.28</span>
</div>
</div>
</div>
<div id="fractalParams" class="space-y-4 hidden">
<div>
<label class="block text-sm font-medium mb-1">Iterations</label>
<input type="range" min="10" max="200" step="1" value="100" class="w-full" id="iterations">
<div class="flex justify-between text-xs text-slate-400">
<span>10</span>
<span id="iterationsValue">100</span>
<span>200</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Zoom</label>
<input type="range" min="0.1" max="5" step="0.1" value="1" class="w-full" id="zoom">
<div class="flex justify-between text-xs text-slate-400">
<span>0.1</span>
<span id="zoomValue">1</span>
<span>5</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Offset X</label>
<input type="range" min="-2" max="2" step="0.01" value="0" class="w-full" id="offsetX">
<div class="flex justify-between text-xs text-slate-400">
<span>-2</span>
<span id="offsetXValue">0</span>
<span>2</span>
</div>
</div>
</div>
<div id="particlesParams" class="space-y-4 hidden">
<div>
<label class="block text-sm font-medium mb-1">Particle Count</label>
<input type="range" min="10" max="500" step="10" value="100" class="w-full" id="particleCount">
<div class="flex justify-between text-xs text-slate-400">
<span>10</span>
<span id="particleCountValue">100</span>
<span>500</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Attractor Strength</label>
<input type="range" min="0.1" max="2" step="0.1" value="0.5" class="w-full" id="attractorStrength">
<div class="flex justify-between text-xs text-slate-400">
<span>0.1</span>
<span id="attractorStrengthValue">0.5</span>
<span>2</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Particle Size</label>
<input type="range" min="1" max="10" step="0.5" value="3" class="w-full" id="particleSize">
<div class="flex justify-between text-xs text-slate-400">
<span>1</span>
<span id="particleSizeValue">3</span>
<span>10</span>
</div>
</div>
</div>
<div id="fourierParams" class="space-y-4 hidden">
<div>
<label class="block text-sm font-medium mb-1">Harmonics</label>
<input type="range" min="1" max="20" step="1" value="5" class="w-full" id="harmonics">
<div class="flex justify-between text-xs text-slate-400">
<span>1</span>
<span id="harmonicsValue">5</span>
<span>20</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Wave Type</label>
<select class="w-full bg-slate-700 border border-slate-600 rounded-md px-3 py-2 text-sm" id="waveType">
<option value="sine">Sine</option>
<option value="square">Square</option>
<option value="sawtooth">Sawtooth</option>
<option value="triangle">Triangle</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Animation Speed</label>
<input type="range" min="0.1" max="2" step="0.1" value="1" class="w-full" id="fourierSpeed">
<div class="flex justify-between text-xs text-slate-400">
<span>0.1</span>
<span id="fourierSpeedValue">1</span>
<span>2</span>
</div>
</div>
</div>
</div>
<div id="exportTab" class="tab-content hidden">
<h3 class="text-xl font-semibold mb-4">Export Settings</h3>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Duration (seconds)</label>
<input type="number" min="1" max="120" value="60" class="w-full bg-slate-700 border border-slate-600 rounded-md px-3 py-2" id="exportDuration">
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Resolution</label>
<select class="w-full bg-slate-700 border border-slate-600 rounded-md px-3 py-2 text-sm" id="exportResolution">
<option value="720">720p (HD)</option>
<option value="1080" selected>1080p (Full HD)</option>
<option value="1440">1440p (QHD)</option>
<option value="2160">2160p (4K)</option>
</select>
</div>
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Frame Rate</label>
<select class="w-full bg-slate-700 border border-slate-600 rounded-md px-3 py-2 text-sm" id="exportFramerate">
<option value="24">24 FPS (Cinematic)</option>
<option value="30" selected>30 FPS (Standard)</option>
<option value="60">60 FPS (Smooth)</option>
</select>
</div>
<button id="startExport" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-md transition">
Export Animation as MP4
</button>
<div id="exportProgress" class="mt-4 hidden">
<div class="flex justify-between text-sm mb-1">
<span>Export Progress</span>
<span id="exportPercent">0%</span>
</div>
<div class="w-full bg-slate-700 rounded-full h-2.5">
<div id="exportProgressBar" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
<footer class="mt-12 text-center text-sm text-slate-500">
<p>Math Animation Generator | Created with Anime.js and CCapture.js</p>
</footer>
</div>
<script>
// DOM Elements
const canvas = document.getElementById('animationCanvas');
const ctx = canvas.getContext('2d');
const recordingOverlay = document.getElementById('recordingOverlay');
const recordingTimer = document.getElementById('recordingTimer');
// Animation state
let currentAnimation = 'lissajous';
let animationRunning = false;
let animationId = null;
let particles = [];
let bgColor = '#0f172a';
let fgColor = '#3b82f6';
// Export state
let capturer = null;
let exportStartTime = 0;
let exportDuration = 60;
let isExporting = false;
// Initialize canvas size
function initCanvas() {
const container = document.querySelector('.canvas-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
// Animation presets
const animations = {
lissajous: {
freqX: 3,
freqY: 2,
phaseShift: 0,
points: [],
history: [],
maxHistory: 100
},
fractal: {
iterations: 100,
zoom: 1,
offsetX: 0,
offsetY: 0
},
particles: {
count: 100,
size: 3,
attractorStrength: 0.5,
particles: []
},
fourier: {
harmonics: 5,
waveType: 'sine',
speed: 1,
circles: [],
path: [],
maxPath: 500
}
};
// Initialize animation
function initAnimation() {
stopAnimation();
// Clear canvas
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Initialize based on current animation type
switch(currentAnimation) {
case 'lissajous':
animations.lissajous.points = [];
animations.lissajous.history = [];
break;
case 'fractal':
// Nothing to initialize
break;
case 'particles':
initParticles();
break;
case 'fourier':
initFourier();
break;
}
startAnimation();
}
// Start animation loop
function startAnimation() {
if (animationRunning) return;
animationRunning = true;
let lastTime = performance.now();
function animate(currentTime) {
if (!animationRunning) return;
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// Clear canvas
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Update and draw based on current animation
switch(currentAnimation) {
case 'lissajous':
updateLissajous(deltaTime);
break;
case 'fractal':
drawFractal();
break;
case 'particles':
updateParticles(deltaTime);
break;
case 'fourier':
updateFourier(deltaTime);
break;
}
// If exporting, capture frame
if (isExporting) {
capturer.capture(canvas);
const elapsed = (currentTime - exportStartTime) / 1000;
const remaining = Math.max(0, exportDuration - elapsed);
// Update timer
const minutes = Math.floor(remaining / 60);
const seconds = Math.floor(remaining % 60);
recordingTimer.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
// Update progress
const progress = Math.min(100, (elapsed / exportDuration) * 100);
document.getElementById('exportProgressBar').style.width = `${progress}%`;
document.getElementById('exportPercent').textContent = `${Math.round(progress)}%`;
// Check if export is complete
if (elapsed >= exportDuration) {
stopExport();
}
}
animationId = requestAnimationFrame(animate);
}
animate(lastTime);
}
// Stop animation loop
function stopAnimation() {
animationRunning = false;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// Lissajous curve animation
function updateLissajous(deltaTime) {
const { freqX, freqY, phaseShift, maxHistory } = animations.lissajous;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(canvas.width, canvas.height) * 0.4;
// Update time for animation
animations.lissajous.time = (animations.lissajous.time || 0) + deltaTime;
// Calculate current point
const t = animations.lissajous.time;
const x = centerX + Math.sin(t * freqX + phaseShift) * radius;
const y = centerY + Math.sin(t * freqY) * radius;
// Add to history
animations.lissajous.history.push({ x, y });
if (animations.lissajous.history.length > maxHistory) {
animations.lissajous.history.shift();
}
// Draw the curve
ctx.strokeStyle = fgColor;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < animations.lissajous.history.length; i++) {
const point = animations.lissajous.history[i];
if (i === 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
}
ctx.stroke();
// Draw current point
ctx.fillStyle = fgColor;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
}
// Mandelbrot fractal animation
function drawFractal() {
const { iterations, zoom, offsetX, offsetY } = animations.fractal;
const width = canvas.width;
const height = canvas.height;
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
// Convert pixel coordinates to complex plane coordinates
const zx = (x - width / 2) / (0.25 * zoom * width) + offsetX;
const zy = (y - height / 2) / (0.25 * zoom * height) + offsetY;
let cX = zx;
let cY = zy;
let iter = 0;
// Mandelbrot iteration
while (iter < iterations) {
const x2 = cX * cX;
const y2 = cY * cY;
if (x2 + y2 > 4) break;
const temp = x2 - y2 + zx;
cY = 2 * cX * cY + zy;
cX = temp;
iter++;
}
// Color based on iteration count
const idx = (x + y * width) * 4;
if (iter === iterations) {
// Inside the set - black
data[idx] = 0;
data[idx + 1] = 0;
data[idx + 2] = 0;
} else {
// Outside the set - color based on iterations
const norm = iter / iterations;
const r = Math.floor(norm * 255);
const g = Math.floor(norm * 120);
const b = Math.floor(norm * 255);
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
}
data[idx + 3] = 255; // Alpha
}
}
ctx.putImageData(imageData, 0, 0);
}
// Particle system initialization
function initParticles() {
const { count } = animations.particles;
particles = [];
for (let i = 0; i < count; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
size: animations.particles.size
});
}
}
// Particle system update
function updateParticles(deltaTime) {
const { attractorStrength } = animations.particles;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Update particles
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
// Calculate direction to center
const dx = centerX - p.x;
const dy = centerY - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Apply attractor force
if (dist > 0) {
const force = attractorStrength / dist;
p.vx += dx * force * deltaTime;
p.vy += dy * force * deltaTime;
}
// Apply velocity
p.x += p.vx;
p.y += p.vy;
// Bounce off edges
if (p.x < 0 || p.x > canvas.width) p.vx *= -0.8;
if (p.y < 0 || p.y > canvas.height) p.vy *= -0.8;
// Keep within bounds
p.x = Math.max(0, Math.min(canvas.width, p.x));
p.y = Math.max(0, Math.min(canvas.height, p.y));
}
// Draw particles
ctx.fillStyle = fgColor;
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
}
// Fourier series initialization
function initFourier() {
animations.fourier.circles = [];
animations.fourier.path = [];
// Create circles for each harmonic
for (let i = 0; i < animations.fourier.harmonics; i++) {
animations.fourier.circles.push({
radius: 50 / (i * 2 + 1),
frequency: (i * 2 + 1) * animations.fourier.speed,
phase: 0
});
}
}
// Fourier series update
function updateFourier(deltaTime) {
const { circles, path, maxPath, waveType } = animations.fourier;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Update time
animations.fourier.time = (animations.fourier.time || 0) + deltaTime;
let x = centerX;
let y = centerY;
// Draw and update each circle
ctx.strokeStyle = fgColor;
ctx.lineWidth = 1;
ctx.fillStyle = 'transparent';
for (let i = 0; i < circles.length; i++) {
const circle = circles[i];
// Calculate position on this circle
circle.phase = animations.fourier.time * circle.frequency;
const prevX = x;
const prevY = y;
// For different wave types
let amplitude = 1;
if (waveType === 'square') {
amplitude = (i % 2 === 0) ? 1 : -1;
} else if (waveType === 'sawtooth') {
amplitude = (i % 2 === 0) ? 1/(i+1) : -1/(i+1);
} else if (waveType === 'triangle') {
amplitude = (i % 2 === 0) ? 1/((i+1)*(i+1)) : -1/((i+1)*(i+1));
}
x += Math.cos(circle.phase) * circle.radius * amplitude;
y += Math.sin(circle.phase) * circle.radius * amplitude;
// Draw circle
ctx.beginPath();
ctx.arc(prevX, prevY, circle.radius, 0, Math.PI * 2);
ctx.stroke();
// Draw line to next circle
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(x, y);
ctx.stroke();
}
// Add current point to path
path.push({ x, y });
if (path.length > maxPath) {
path.shift();
}
// Draw the path
ctx.strokeStyle = fgColor;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < path.length; i++) {
if (i === 0) {
ctx.moveTo(path[i].x, path[i].y);
} else {
ctx.lineTo(path[i].x, path[i].y);
}
}
ctx.stroke();
// Draw current point
ctx.fillStyle = fgColor;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
}
// Start export process
function startExport() {
if (isExporting) return;
exportDuration = parseInt(document.getElementById('exportDuration').value);
const resolution = parseInt(document.getElementById('exportResolution').value);
const framerate = parseInt(document.getElementById('exportFramerate').value);
// Calculate dimensions while maintaining aspect ratio
const aspectRatio = canvas.width / canvas.height;
let exportWidth, exportHeight;
if (aspectRatio > 1) {
exportWidth = resolution;
exportHeight = Math.round(resolution / aspectRatio);
} else {
exportHeight = resolution;
exportWidth = Math.round(resolution * aspectRatio);
}
// Create capturer
capturer = new CCapture({
format: 'ffmpegserver',
framerate: framerate,
verbose: true,
display: true,
timeLimit: exportDuration,
frameLimit: 0,
autoSaveTime: 0,
name: `math_animation_${currentAnimation}`,
extension: '.mp4',
codec: 'libx264',
quality: 100,
workersPath: ''
});
// Start capturing
capturer.start();
isExporting = true;
exportStartTime = performance.now();
// Show recording UI
recordingOverlay.classList.remove('hidden');
// Update progress UI
document.getElementById('exportProgress').classList.remove('hidden');
document.getElementById('exportProgressBar').style.width = '0%';
document.getElementById('exportPercent').textContent = '0%';
// Disable export button
document.getElementById('startExport').disabled = true;
document.getElementById('startExport').classList.add('opacity-50');
}
// Stop export process
function stopExport() {
if (!isExporting) return;
capturer.stop();
capturer.save();
isExporting = false;
// Hide recording UI
recordingOverlay.classList.add('hidden');
// Re-enable export button
document.getElementById('startExport').disabled = false;
document.getElementById('startExport').classList.remove('opacity-50');
}
// Event listeners for UI controls
function setupEventListeners() {
// Window resize
window.addEventListener('resize', () => {
initCanvas();
initAnimation();
});
// Tab switching
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
const tab = button.dataset.tab;
// Update active tab button
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
button.classList.add('active');
// Show corresponding tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`${tab}Tab`).classList.remove('hidden');
});
});
// Animation preset buttons
document.querySelectorAll('.animation-preset').forEach(button => {
button.addEventListener('click', () => {
currentAnimation = button.dataset.preset;
initAnimation();
// Update active preset button
document.querySelectorAll('.animation-preset').forEach(btn => {
btn.classList.remove('bg-blue-600');
btn.classList.add('bg-slate-700');
});
button.classList.remove('bg-slate-700');
button.classList.add('bg-blue-600');
});
});
// Color pickers
document.getElementById('bgColor').addEventListener('input', (e) => {
bgColor = e.target.value;
document.getElementById('bgColorValue').textContent = bgColor;
initAnimation();
});
document.getElementById('fgColor').addEventListener('input', (e) => {
fgColor = e.target.value;
document.getElementById('fgColorValue').textContent = fgColor;
initAnimation();
});
// Lissajous controls
document.getElementById('freqX').addEventListener('input', (e) => {
animations.lissajous.freqX = parseFloat(e.target.value);
document.getElementById('freqXValue').textContent = animations.lissajous.freqX;
});
document.getElementById('freqY').addEventListener('input', (e) => {
animations.lissajous.freqY = parseFloat(e.target.value);
document.getElementById('freqYValue').textContent = animations.lissajous.freqY;
});
document.getElementById('phaseShift').addEventListener('input', (e) => {
animations.lissajous.phaseShift = parseFloat(e.target.value);
document.getElementById('phaseShiftValue').textContent = animations.lissajous.phaseShift.toFixed(2);
});
// Fractal controls
document.getElementById('iterations').addEventListener('input', (e) => {
animations.fractal.iterations = parseInt(e.target.value);
document.getElementById('iterationsValue').textContent = animations.fractal.iterations;
});
document.getElementById('zoom').addEventListener('input', (e) => {
animations.fractal.zoom = parseFloat(e.target.value);
document.getElementById('zoomValue').textContent = animations.fractal.zoom.toFixed(1);
});
document.getElementById('offsetX').addEventListener('input', (e) => {
animations.fractal.offsetX = parseFloat(e.target.value);
document.getElementById('offsetXValue').textContent = animations.fractal.offsetX.toFixed(2);
});
// Particle controls
document.getElementById('particleCount').addEventListener('input', (e) => {
animations.particles.count = parseInt(e.target.value);
document.getElementById('particleCountValue').textContent = animations.particles.count;
initParticles();
});
document.getElementById('attractorStrength').addEventListener('input', (e) => {
animations.particles.attractorStrength = parseFloat(e.target.value);
document.getElementById('attractorStrengthValue').textContent = animations.particles.attractorStrength.toFixed(1);
});
document.getElementById('particleSize').addEventListener('input', (e) => {
animations.particles.size = parseFloat(e.target.value);
document.getElementById('particleSizeValue').textContent = animations.particles.size.toFixed(1);
// Update existing particles
particles.forEach(p => {
p.size = animations.particles.size;
});
});
// Fourier controls
document.getElementById('harmonics').addEventListener('input', (e) => {
animations.fourier.harmonics = parseInt(e.target.value);
document.getElementById('harmonicsValue').textContent = animations.fourier.harmonics;
initFourier();
});
document.getElementById('waveType').addEventListener('change', (e) => {
animations.fourier.waveType = e.target.value;
initFourier();
});
document.getElementById('fourierSpeed').addEventListener('input', (e) => {
animations.fourier.speed = parseFloat(e.target.value);
document.getElementById('fourierSpeedValue').textContent = animations.fourier.speed.toFixed(1);
// Update circle frequencies
animations.fourier.circles.forEach((circle, i) => {
circle.frequency = (i * 2 + 1) * animations.fourier.speed;
});
});
// Export button
document.getElementById('startExport').addEventListener('click', startExport);
}
// Initialize everything
function init() {
initCanvas();
setupEventListeners();
initAnimation();
// Set initial active preset
document.querySelector('.animation-preset[data-preset="lissajous"]').click();
}
// Start the app
window.addEventListener('load', init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=arirajuns/math-animation1" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>