Spaces:
Running
Running
| <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> |