mirror of
https://github.com/SamEyeBam/animate.git
synced 2026-02-04 09:20:25 +00:00
268 lines
7.8 KiB
JavaScript
268 lines
7.8 KiB
JavaScript
/**
|
|
* 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();
|