mirror of
https://github.com/SamEyeBam/animate.git
synced 2026-02-04 09:20:25 +00:00
V1.1
Giant refactor. added layers. ui overhaul. added save/load and we now got presets
This commit is contained in:
163
docs/js/core/AnimationEngine.js
Normal file
163
docs/js/core/AnimationEngine.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* AnimationEngine - Manages the canvas, render loop, and timing
|
||||
*/
|
||||
class AnimationEngine {
|
||||
constructor(canvasId) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
// Sizing
|
||||
this.resizeCanvas();
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
|
||||
// Timing
|
||||
this.targetFps = 60;
|
||||
this.frameDuration = 1000 / this.targetFps;
|
||||
this.lastTimestamp = 0;
|
||||
this.elapsedTime = 0;
|
||||
this.rotation = 0;
|
||||
this.degPerSec = 10;
|
||||
|
||||
// State
|
||||
this.paused = false;
|
||||
this.isRunning = false;
|
||||
|
||||
// Scene reference (set via setScene)
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the scene to render
|
||||
* @param {Scene} scene - The scene instance
|
||||
*/
|
||||
setScene(scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize canvas to fill window
|
||||
*/
|
||||
resizeCanvas() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
this.centerX = this.canvas.width / 2;
|
||||
this.centerY = this.canvas.height / 2;
|
||||
|
||||
// Update global references for backward compatibility
|
||||
if (typeof centerX !== 'undefined') {
|
||||
centerX = this.centerX;
|
||||
centerY = this.centerY;
|
||||
}
|
||||
if (typeof ctx !== 'undefined') {
|
||||
ctx = this.ctx;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the canvas
|
||||
*/
|
||||
clear() {
|
||||
this.ctx.fillStyle = 'black';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main render loop
|
||||
*/
|
||||
render(timestamp) {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
if (!this.lastTimestamp) {
|
||||
this.lastTimestamp = timestamp;
|
||||
}
|
||||
|
||||
const deltaTime = timestamp - this.lastTimestamp;
|
||||
this.lastTimestamp = timestamp;
|
||||
|
||||
let adjustedDeltaTime = 0;
|
||||
|
||||
if (!this.paused) {
|
||||
this.rotation += this.degPerSec / this.targetFps;
|
||||
this.elapsedTime += deltaTime;
|
||||
adjustedDeltaTime = deltaTime / 100;
|
||||
}
|
||||
|
||||
const adjustedElapsed = this.elapsedTime / 1000;
|
||||
|
||||
// Clear and draw
|
||||
this.clear();
|
||||
if (this.scene) {
|
||||
this.scene.draw(adjustedElapsed, adjustedDeltaTime);
|
||||
}
|
||||
|
||||
requestAnimationFrame((ts) => this.render(ts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the animation loop
|
||||
*/
|
||||
start() {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
this.lastTimestamp = 0;
|
||||
requestAnimationFrame((ts) => this.render(ts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the animation loop
|
||||
*/
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle pause state
|
||||
*/
|
||||
togglePause() {
|
||||
this.paused = !this.paused;
|
||||
return this.paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset rotation/timing
|
||||
*/
|
||||
reset() {
|
||||
this.rotation = 0;
|
||||
this.elapsedTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step forward one frame
|
||||
*/
|
||||
stepForward() {
|
||||
this.rotation += this.degPerSec / this.targetFps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step backward one frame
|
||||
*/
|
||||
stepBackward() {
|
||||
this.rotation -= this.degPerSec / this.targetFps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set degrees per second
|
||||
*/
|
||||
setSpeed(degPerSec) {
|
||||
this.degPerSec = parseFloat(degPerSec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canvas context (for shapes that need direct access)
|
||||
*/
|
||||
getContext() {
|
||||
return this.ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get center coordinates
|
||||
*/
|
||||
getCenter() {
|
||||
return { x: this.centerX, y: this.centerY };
|
||||
}
|
||||
}
|
||||
124
docs/js/core/BaseShape.js
Normal file
124
docs/js/core/BaseShape.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* BaseShape - Base class for all animated shapes
|
||||
*
|
||||
* To create a new shape:
|
||||
* 1. Create a new file in /js/shapes/
|
||||
* 2. Extend BaseShape
|
||||
* 3. Define static `config` array with control definitions
|
||||
* 4. Implement constructor with matching parameters
|
||||
* 5. Implement `draw(elapsed, deltaTime)` method
|
||||
* 6. Register with shapeRegistry at the end of the file
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* class MyShape extends BaseShape {
|
||||
* static config = [
|
||||
* { type: 'range', property: 'size', min: 10, max: 500, defaultValue: 100 },
|
||||
* { type: 'color', property: 'color', defaultValue: '#ff0000' }
|
||||
* ];
|
||||
*
|
||||
* constructor(size, color) {
|
||||
* super();
|
||||
* this.size = size;
|
||||
* this.color = color;
|
||||
* }
|
||||
*
|
||||
* draw(elapsed, deltaTime) {
|
||||
* this.updateFilters(elapsed);
|
||||
* // Drawing code using this.size and this.color
|
||||
* }
|
||||
* }
|
||||
* shapeRegistry.register('MyShape', MyShape);
|
||||
* ```
|
||||
*
|
||||
* Control types:
|
||||
* - range: { type, property, min, max, defaultValue, callback? }
|
||||
* - color: { type, property, defaultValue }
|
||||
* - checkbox: { type, property, defaultValue }
|
||||
* - dropdown: { type, property, defaultValue, options: [{value, label}] }
|
||||
* - button: { type, label, method }
|
||||
* - header: { type, text }
|
||||
*/
|
||||
class BaseShape {
|
||||
constructor() {
|
||||
this.controls = [];
|
||||
this.controlManager = null;
|
||||
this.speedMultiplier = 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize controls using the ControlManager
|
||||
* Called automatically when a shape is created
|
||||
* @param {ControlManager} controlManager - The control manager instance
|
||||
*/
|
||||
initializeControls(controlManager) {
|
||||
this.controlManager = controlManager;
|
||||
|
||||
// Get config from static property
|
||||
const config = this.constructor.config || [];
|
||||
|
||||
for (const item of config) {
|
||||
const controlData = controlManager.addControl(item, this);
|
||||
if (controlData) {
|
||||
this.controls.push(controlData);
|
||||
}
|
||||
}
|
||||
|
||||
// Add speed multiplier control (common to all shapes)
|
||||
const speedControl = controlManager.addControl({
|
||||
type: 'range',
|
||||
min: 1,
|
||||
max: 1000,
|
||||
defaultValue: this.speedMultiplier,
|
||||
property: 'speedMultiplier'
|
||||
}, this);
|
||||
if (speedControl) {
|
||||
this.controls.push(speedControl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up controls when shape is removed
|
||||
*/
|
||||
remove() {
|
||||
if (this.controlManager) {
|
||||
for (const control of this.controls) {
|
||||
this.controlManager.removeControl(control);
|
||||
}
|
||||
}
|
||||
this.controls = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any active filters on controls
|
||||
* Call this at the start of your draw() method
|
||||
* @param {number} elapsed - Total elapsed time in seconds
|
||||
*/
|
||||
updateFilters(elapsed) {
|
||||
if (!this.controlManager) return;
|
||||
|
||||
for (const control of this.controls) {
|
||||
if (control.filters && control.filters.length > 0) {
|
||||
const newValue = this.controlManager.filterManager.computeFilteredValue(
|
||||
control.filters,
|
||||
elapsed
|
||||
);
|
||||
|
||||
if (newValue !== null && control.element) {
|
||||
control.element.value = newValue;
|
||||
const event = new Event('input', { bubbles: true });
|
||||
control.element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main draw method - override in subclasses
|
||||
* @param {number} elapsed - Total elapsed time in seconds
|
||||
* @param {number} deltaTime - Time since last frame
|
||||
*/
|
||||
draw(elapsed, deltaTime) {
|
||||
throw new Error('draw() method not implemented');
|
||||
}
|
||||
}
|
||||
267
docs/js/core/Scene.js
Normal file
267
docs/js/core/Scene.js
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Scene - Manages multiple shapes on a single canvas
|
||||
*
|
||||
* Features:
|
||||
* - Multiple shapes rendered in layer order
|
||||
* - Per-shape visibility and pause controls
|
||||
* - Collapsible control panels per shape
|
||||
* - Add/remove shapes dynamically
|
||||
*/
|
||||
class Scene {
|
||||
constructor() {
|
||||
this.layers = []; // Array of { id, shape, visible, paused, collapsed }
|
||||
this.nextLayerId = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shape to the scene
|
||||
* @param {string} shapeName - Name of the shape class
|
||||
* @param {object} initialValues - Initial property values
|
||||
* @returns {number} Layer ID
|
||||
*/
|
||||
addShape(shapeName, initialValues = {}) {
|
||||
const shape = shapeRegistry.createInstance(shapeName, initialValues);
|
||||
|
||||
if (!shape) {
|
||||
throw new Error(`Failed to create shape: ${shapeName}`);
|
||||
}
|
||||
|
||||
const layer = {
|
||||
id: this.nextLayerId++,
|
||||
name: shapeName,
|
||||
shape: shape,
|
||||
visible: true,
|
||||
paused: false,
|
||||
collapsed: false
|
||||
};
|
||||
|
||||
this.layers.push(layer);
|
||||
return layer.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a shape from the scene
|
||||
* @param {number} layerId - Layer ID to remove
|
||||
*/
|
||||
removeShape(layerId) {
|
||||
const index = this.layers.findIndex(l => l.id === layerId);
|
||||
if (index !== -1) {
|
||||
const layer = this.layers[index];
|
||||
// Clean up controls
|
||||
if (layer.shape.remove) {
|
||||
layer.shape.remove();
|
||||
}
|
||||
this.layers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a layer by ID
|
||||
* @param {number} layerId
|
||||
* @returns {object|null}
|
||||
*/
|
||||
getLayer(layerId) {
|
||||
return this.layers.find(l => l.id === layerId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle layer visibility
|
||||
* @param {number} layerId
|
||||
*/
|
||||
toggleVisibility(layerId) {
|
||||
const layer = this.getLayer(layerId);
|
||||
if (layer) {
|
||||
layer.visible = !layer.visible;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle layer pause state
|
||||
* @param {number} layerId
|
||||
*/
|
||||
togglePause(layerId) {
|
||||
const layer = this.getLayer(layerId);
|
||||
if (layer) {
|
||||
layer.paused = !layer.paused;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle layer control panel collapsed state
|
||||
* @param {number} layerId
|
||||
*/
|
||||
toggleCollapsed(layerId) {
|
||||
const layer = this.getLayer(layerId);
|
||||
if (layer) {
|
||||
layer.collapsed = !layer.collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a layer up in the render order
|
||||
* @param {number} layerId
|
||||
*/
|
||||
moveUp(layerId) {
|
||||
const index = this.layers.findIndex(l => l.id === layerId);
|
||||
if (index > 0) {
|
||||
[this.layers[index - 1], this.layers[index]] = [this.layers[index], this.layers[index - 1]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a layer down in the render order
|
||||
* @param {number} layerId
|
||||
*/
|
||||
moveDown(layerId) {
|
||||
const index = this.layers.findIndex(l => l.id === layerId);
|
||||
if (index < this.layers.length - 1) {
|
||||
[this.layers[index], this.layers[index + 1]] = [this.layers[index + 1], this.layers[index]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw all visible shapes
|
||||
* @param {number} elapsed - Total elapsed time
|
||||
* @param {number} deltaTime - Time since last frame
|
||||
*/
|
||||
draw(elapsed, deltaTime) {
|
||||
for (const layer of this.layers) {
|
||||
if (layer.visible && !layer.paused) {
|
||||
layer.shape.draw(elapsed, deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all layers
|
||||
* @returns {array}
|
||||
*/
|
||||
getLayers() {
|
||||
return this.layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all layers
|
||||
*/
|
||||
clear() {
|
||||
for (const layer of this.layers) {
|
||||
if (layer.shape.remove) {
|
||||
layer.shape.remove();
|
||||
}
|
||||
}
|
||||
this.layers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scene to JSON
|
||||
* @returns {object} Scene data
|
||||
*/
|
||||
exportToJSON() {
|
||||
return {
|
||||
version: 1,
|
||||
layers: this.layers.map(layer => {
|
||||
const shape = layer.shape;
|
||||
const ShapeClass = shape.constructor;
|
||||
const config = ShapeClass.config || [];
|
||||
|
||||
// Extract current values from shape
|
||||
const values = {};
|
||||
config.forEach(item => {
|
||||
if (item.property && shape.hasOwnProperty(item.property)) {
|
||||
values[item.property] = shape[item.property];
|
||||
}
|
||||
});
|
||||
|
||||
// Also save speedMultiplier
|
||||
values.speedMultiplier = shape.speedMultiplier;
|
||||
|
||||
// Extract filter settings and control bounds from shape.controls
|
||||
const filters = {};
|
||||
const controlBounds = {};
|
||||
if (shape.controls) {
|
||||
for (const control of shape.controls) {
|
||||
const property = control.config?.property;
|
||||
if (!property) continue;
|
||||
|
||||
// Save filters
|
||||
if (control.filters && control.filters.length > 0) {
|
||||
filters[property] = control.filters.map(f => ({
|
||||
type: f.type,
|
||||
params: { ...f.params }
|
||||
}));
|
||||
}
|
||||
|
||||
// Save custom min/max bounds if different from original
|
||||
if (control.configState) {
|
||||
const { min, max, originalMin, originalMax } = control.configState;
|
||||
if (min !== originalMin || max !== originalMax) {
|
||||
controlBounds[property] = { min, max };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: layer.name,
|
||||
visible: layer.visible,
|
||||
paused: layer.paused,
|
||||
collapsed: layer.collapsed,
|
||||
values: values,
|
||||
filters: filters,
|
||||
controlBounds: controlBounds
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import scene from JSON
|
||||
* @param {object} data - Scene data
|
||||
* @param {SceneUI} sceneUI - UI instance to rebuild panels
|
||||
*/
|
||||
importFromJSON(data, sceneUI) {
|
||||
// Clear existing
|
||||
this.clear();
|
||||
if (sceneUI) {
|
||||
sceneUI.clearAllPanels();
|
||||
}
|
||||
|
||||
// Validate version
|
||||
if (!data.version || !data.layers) {
|
||||
throw new Error('Invalid scene data');
|
||||
}
|
||||
|
||||
// Recreate layers
|
||||
for (const layerData of data.layers) {
|
||||
try {
|
||||
// Check if shape type exists
|
||||
if (!shapeRegistry.get(layerData.name)) {
|
||||
console.warn(`Shape "${layerData.name}" not found, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const layerId = this.addShape(layerData.name, layerData.values);
|
||||
const layer = this.getLayer(layerId);
|
||||
|
||||
if (!layer || !layer.shape) {
|
||||
console.warn(`Failed to create layer for "${layerData.name}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
layer.visible = layerData.visible !== false;
|
||||
layer.paused = layerData.paused || false;
|
||||
layer.collapsed = layerData.collapsed || false;
|
||||
|
||||
// Create UI panel (this also creates controls, applies filters and bounds)
|
||||
if (sceneUI) {
|
||||
sceneUI.createLayerPanel(layer, layerData.filters, layerData.controlBounds);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load shape "${layerData.name}":`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global scene instance
|
||||
const scene = new Scene();
|
||||
89
docs/js/core/ShapeRegistry.js
Normal file
89
docs/js/core/ShapeRegistry.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* ShapeRegistry - Automatically registers and manages available shape classes
|
||||
* Shapes register themselves when loaded, no manual map needed
|
||||
*/
|
||||
class ShapeRegistry {
|
||||
constructor() {
|
||||
this.shapes = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a shape class
|
||||
* @param {string} name - The name of the shape
|
||||
* @param {class} shapeClass - The shape class constructor
|
||||
*/
|
||||
register(name, shapeClass) {
|
||||
if (this.shapes.has(name)) {
|
||||
console.warn(`Shape "${name}" is already registered. Overwriting.`);
|
||||
}
|
||||
this.shapes.set(name, shapeClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shape class by name
|
||||
* @param {string} name - The name of the shape
|
||||
* @returns {class|null} The shape class or null if not found
|
||||
*/
|
||||
get(name) {
|
||||
return this.shapes.get(name) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of a shape
|
||||
* @param {string} name - The name of the shape
|
||||
* @param {object} initialValues - Initial property values (overrides defaults)
|
||||
* @returns {BaseShape} The shape instance
|
||||
*/
|
||||
createInstance(name, initialValues = {}) {
|
||||
const ShapeClass = this.get(name);
|
||||
if (!ShapeClass) {
|
||||
throw new Error(`Unknown shape: "${name}". Available shapes: ${this.getAvailableNames().join(', ')}`);
|
||||
}
|
||||
|
||||
// Get the static config to determine constructor argument order
|
||||
const config = ShapeClass.config || [];
|
||||
|
||||
// Build positional arguments array from config defaults + initialValues overrides
|
||||
const args = config
|
||||
.filter(item => item.property) // Only items with properties
|
||||
.map(item => {
|
||||
// Use provided value or fall back to default
|
||||
return initialValues.hasOwnProperty(item.property)
|
||||
? initialValues[item.property]
|
||||
: item.defaultValue;
|
||||
});
|
||||
|
||||
return new ShapeClass(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered shape names (alphabetically sorted)
|
||||
* @returns {string[]} Array of shape names
|
||||
*/
|
||||
getAvailableNames() {
|
||||
return Array.from(this.shapes.keys()).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config definition for a shape
|
||||
* @param {string} name - The name of the shape
|
||||
* @returns {array|null} The config array or null
|
||||
*/
|
||||
getConfig(name) {
|
||||
const ShapeClass = this.get(name);
|
||||
if (!ShapeClass) return null;
|
||||
return ShapeClass.config || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a shape is registered
|
||||
* @param {string} name - The name of the shape
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(name) {
|
||||
return this.shapes.has(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Global singleton instance
|
||||
const shapeRegistry = new ShapeRegistry();
|
||||
Reference in New Issue
Block a user