Files
animate/docs/js/core/Scene.js
Sam 14ec23237f V1.1
Giant refactor. added layers. ui overhaul. added save/load and we now got presets
2025-12-28 03:21:25 +13:00

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();