@vib3code/sdk 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/DOCS/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +34 -0
- package/DOCS/CI_TESTING.md +38 -0
- package/DOCS/CLI_ONBOARDING.md +75 -0
- package/DOCS/CONTROL_REFERENCE.md +64 -0
- package/DOCS/DEV_TRACK_ANALYSIS.md +77 -0
- package/DOCS/DEV_TRACK_PLAN_2026-01-07.md +42 -0
- package/DOCS/DEV_TRACK_SESSION_2026-01-31.md +220 -0
- package/DOCS/ENV_SETUP.md +189 -0
- package/DOCS/EXPORT_FORMATS.md +417 -0
- package/DOCS/GPU_DISPOSAL_GUIDE.md +21 -0
- package/DOCS/LICENSING_TIERS.md +275 -0
- package/DOCS/MASTER_PLAN_2026-01-31.md +570 -0
- package/DOCS/OBS_SETUP_GUIDE.md +98 -0
- package/DOCS/PROJECT_SETUP.md +66 -0
- package/DOCS/RENDERER_LIFECYCLE.md +40 -0
- package/DOCS/REPO_MANIFEST.md +121 -0
- package/DOCS/SESSION_014_PLAN.md +195 -0
- package/DOCS/SESSION_LOG_2026-01-07.md +56 -0
- package/DOCS/STRATEGIC_BLUEPRINT_2026-01-07.md +72 -0
- package/DOCS/SYSTEM_AUDIT_2026-01-30.md +738 -0
- package/DOCS/SYSTEM_INVENTORY.md +520 -0
- package/DOCS/TELEMETRY_EXPORTS.md +34 -0
- package/DOCS/WEBGPU_STATUS.md +38 -0
- package/DOCS/XR_BENCHMARKS.md +608 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/docs/.nojekyll +0 -0
- package/docs/01-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/02-hyperspatial_ego_death.html +346 -0
- package/docs/03-post_cartesian_sublime.html +346 -0
- package/docs/04-crystalline_void_meditation.html +346 -0
- package/docs/05-quantum_decoherence_ballet.html +346 -0
- package/docs/06-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/07-hyperspatial_ego_death.html +346 -0
- package/docs/08-post_cartesian_sublime.html +346 -0
- package/docs/09-crystalline_void_meditation.html +346 -0
- package/docs/10-quantum_decoherence_ballet.html +346 -0
- package/docs/11-dissolution_of_euclidean_hegemony.html +346 -0
- package/docs/12-hyperspatial_ego_death.html +346 -0
- package/docs/13-post_cartesian_sublime.html +346 -0
- package/docs/index.html +794 -0
- package/docs/test-hub.html +441 -0
- package/docs/url-state.js +102 -0
- package/docs/vib3-exports/01-quantum-quantum-tetrahedron-lattice.html +489 -0
- package/docs/vib3-exports/02-quantum-quantum-hypersphere-matrix.html +489 -0
- package/docs/vib3-exports/03-quantum-quantum-hypertetra-fractal.html +489 -0
- package/docs/vib3-exports/04-faceted-faceted-crystal-structure.html +407 -0
- package/docs/vib3-exports/05-faceted-faceted-klein-bottle.html +407 -0
- package/docs/vib3-exports/06-faceted-faceted-hypertetra-torus.html +407 -0
- package/docs/vib3-exports/07-holographic-holographic-wave-field.html +457 -0
- package/docs/vib3-exports/08-holographic-holographic-hypersphere-sphere.html +457 -0
- package/docs/vib3-exports/09-holographic-holographic-hypertetra-crystal.html +457 -0
- package/docs/vib3-exports/index.html +238 -0
- package/docs/webgpu-live.html +702 -0
- package/package.json +367 -0
- package/src/advanced/AIPresetGenerator.js +777 -0
- package/src/advanced/MIDIController.js +703 -0
- package/src/advanced/OffscreenWorker.js +1051 -0
- package/src/advanced/WebGPUCompute.js +1051 -0
- package/src/advanced/WebXRRenderer.js +680 -0
- package/src/agent/cli/AgentCLI.js +615 -0
- package/src/agent/cli/index.js +14 -0
- package/src/agent/index.js +73 -0
- package/src/agent/mcp/MCPServer.js +950 -0
- package/src/agent/mcp/index.js +9 -0
- package/src/agent/mcp/tools.js +548 -0
- package/src/agent/telemetry/EventStream.js +669 -0
- package/src/agent/telemetry/Instrumentation.js +618 -0
- package/src/agent/telemetry/TelemetryExporters.js +427 -0
- package/src/agent/telemetry/TelemetryService.js +464 -0
- package/src/agent/telemetry/index.js +52 -0
- package/src/benchmarks/BenchmarkRunner.js +381 -0
- package/src/benchmarks/MetricsCollector.js +299 -0
- package/src/benchmarks/index.js +9 -0
- package/src/benchmarks/scenes.js +259 -0
- package/src/cli/index.js +675 -0
- package/src/config/ApiConfig.js +88 -0
- package/src/core/CanvasManager.js +217 -0
- package/src/core/ErrorReporter.js +117 -0
- package/src/core/ParameterMapper.js +333 -0
- package/src/core/Parameters.js +396 -0
- package/src/core/RendererContracts.js +200 -0
- package/src/core/UnifiedResourceManager.js +370 -0
- package/src/core/VIB3Engine.js +636 -0
- package/src/core/renderers/FacetedRendererAdapter.js +32 -0
- package/src/core/renderers/HolographicRendererAdapter.js +29 -0
- package/src/core/renderers/QuantumRendererAdapter.js +29 -0
- package/src/core/renderers/RendererLifecycleManager.js +63 -0
- package/src/creative/ColorPresetsSystem.js +980 -0
- package/src/creative/ParameterTimeline.js +1061 -0
- package/src/creative/PostProcessingPipeline.js +1113 -0
- package/src/creative/TransitionAnimator.js +683 -0
- package/src/export/CSSExporter.js +226 -0
- package/src/export/CardGeneratorBase.js +279 -0
- package/src/export/ExportManager.js +580 -0
- package/src/export/FacetedCardGenerator.js +279 -0
- package/src/export/HolographicCardGenerator.js +543 -0
- package/src/export/LottieExporter.js +552 -0
- package/src/export/QuantumCardGenerator.js +315 -0
- package/src/export/SVGExporter.js +519 -0
- package/src/export/ShaderExporter.js +903 -0
- package/src/export/TradingCardGenerator.js +3055 -0
- package/src/export/TradingCardManager.js +181 -0
- package/src/export/VIB3PackageExporter.js +559 -0
- package/src/export/index.js +14 -0
- package/src/export/systems/TradingCardSystemFaceted.js +494 -0
- package/src/export/systems/TradingCardSystemHolographic.js +452 -0
- package/src/export/systems/TradingCardSystemQuantum.js +411 -0
- package/src/faceted/FacetedSystem.js +963 -0
- package/src/features/CollectionManager.js +433 -0
- package/src/gallery/CollectionManager.js +240 -0
- package/src/gallery/GallerySystem.js +485 -0
- package/src/geometry/GeometryFactory.js +314 -0
- package/src/geometry/GeometryLibrary.js +72 -0
- package/src/geometry/buffers/BufferBuilder.js +338 -0
- package/src/geometry/buffers/index.js +18 -0
- package/src/geometry/generators/Crystal.js +420 -0
- package/src/geometry/generators/Fractal.js +298 -0
- package/src/geometry/generators/KleinBottle.js +197 -0
- package/src/geometry/generators/Sphere.js +192 -0
- package/src/geometry/generators/Tesseract.js +160 -0
- package/src/geometry/generators/Tetrahedron.js +225 -0
- package/src/geometry/generators/Torus.js +304 -0
- package/src/geometry/generators/Wave.js +341 -0
- package/src/geometry/index.js +142 -0
- package/src/geometry/warp/HypersphereCore.js +211 -0
- package/src/geometry/warp/HypertetraCore.js +386 -0
- package/src/geometry/warp/index.js +57 -0
- package/src/holograms/HolographicVisualizer.js +1073 -0
- package/src/holograms/RealHolographicSystem.js +966 -0
- package/src/holograms/variantRegistry.js +69 -0
- package/src/integrations/FigmaPlugin.js +854 -0
- package/src/integrations/OBSMode.js +754 -0
- package/src/integrations/ThreeJsPackage.js +660 -0
- package/src/integrations/TouchDesignerExport.js +552 -0
- package/src/integrations/frameworks/Vib3React.js +591 -0
- package/src/integrations/frameworks/Vib3Svelte.js +654 -0
- package/src/integrations/frameworks/Vib3Vue.js +628 -0
- package/src/llm/LLMParameterInterface.js +240 -0
- package/src/llm/LLMParameterUI.js +577 -0
- package/src/math/Mat4x4.js +708 -0
- package/src/math/Projection.js +341 -0
- package/src/math/Rotor4D.js +637 -0
- package/src/math/Vec4.js +476 -0
- package/src/math/constants.js +164 -0
- package/src/math/index.js +68 -0
- package/src/math/projections.js +54 -0
- package/src/math/rotations.js +196 -0
- package/src/quantum/QuantumEngine.js +906 -0
- package/src/quantum/QuantumVisualizer.js +1103 -0
- package/src/reactivity/ReactivityConfig.js +499 -0
- package/src/reactivity/ReactivityManager.js +586 -0
- package/src/reactivity/SpatialInputSystem.js +1783 -0
- package/src/reactivity/index.js +93 -0
- package/src/render/CommandBuffer.js +465 -0
- package/src/render/MultiCanvasBridge.js +340 -0
- package/src/render/RenderCommand.js +514 -0
- package/src/render/RenderResourceRegistry.js +523 -0
- package/src/render/RenderState.js +552 -0
- package/src/render/RenderTarget.js +512 -0
- package/src/render/ShaderLoader.js +253 -0
- package/src/render/ShaderProgram.js +599 -0
- package/src/render/UnifiedRenderBridge.js +496 -0
- package/src/render/backends/WebGLBackend.js +1108 -0
- package/src/render/backends/WebGPUBackend.js +1409 -0
- package/src/render/commands/CommandBufferExecutor.js +607 -0
- package/src/render/commands/RenderCommandBuffer.js +661 -0
- package/src/render/commands/index.js +17 -0
- package/src/render/index.js +367 -0
- package/src/scene/Disposable.js +498 -0
- package/src/scene/MemoryPool.js +618 -0
- package/src/scene/Node4D.js +697 -0
- package/src/scene/ResourceManager.js +599 -0
- package/src/scene/Scene4D.js +540 -0
- package/src/scene/index.js +98 -0
- package/src/schemas/error.schema.json +84 -0
- package/src/schemas/extension.schema.json +88 -0
- package/src/schemas/index.js +214 -0
- package/src/schemas/parameters.schema.json +142 -0
- package/src/schemas/tool-pack.schema.json +44 -0
- package/src/schemas/tool-response.schema.json +127 -0
- package/src/shaders/common/fullscreen.vert.glsl +5 -0
- package/src/shaders/common/fullscreen.vert.wgsl +17 -0
- package/src/shaders/common/geometry24.glsl +65 -0
- package/src/shaders/common/geometry24.wgsl +54 -0
- package/src/shaders/common/rotation4d.glsl +85 -0
- package/src/shaders/common/rotation4d.wgsl +86 -0
- package/src/shaders/common/uniforms.glsl +44 -0
- package/src/shaders/common/uniforms.wgsl +48 -0
- package/src/shaders/faceted/faceted.frag.glsl +129 -0
- package/src/shaders/faceted/faceted.frag.wgsl +164 -0
- package/src/shaders/holographic/holographic.frag.glsl +406 -0
- package/src/shaders/holographic/holographic.frag.wgsl +185 -0
- package/src/shaders/quantum/quantum.frag.glsl +513 -0
- package/src/shaders/quantum/quantum.frag.wgsl +361 -0
- package/src/testing/ParallelTestFramework.js +519 -0
- package/src/testing/__snapshots__/exportFormats.test.js.snap +24 -0
- package/src/testing/exportFormats.test.js +8 -0
- package/src/testing/projections.test.js +14 -0
- package/src/testing/rotations.test.js +37 -0
- package/src/ui/InteractivityMenu.js +516 -0
- package/src/ui/StatusManager.js +96 -0
- package/src/ui/adaptive/renderers/webgpu/BufferLayout.ts +252 -0
- package/src/ui/adaptive/renderers/webgpu/PolytopeInstanceBuffer.ts +144 -0
- package/src/ui/adaptive/renderers/webgpu/TripleBufferedUniform.ts +170 -0
- package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +735 -0
- package/src/ui/adaptive/renderers/webgpu/index.ts +112 -0
- package/src/variations/VariationManager.js +431 -0
- package/src/viewer/AudioReactivity.js +505 -0
- package/src/viewer/CardBending.js +481 -0
- package/src/viewer/GalleryUI.js +832 -0
- package/src/viewer/ReactivityManager.js +590 -0
- package/src/viewer/TradingCardExporter.js +600 -0
- package/src/viewer/ViewerPortal.js +374 -0
- package/src/viewer/index.js +12 -0
- package/src/wasm/WasmLoader.js +296 -0
- package/src/wasm/index.js +132 -0
- package/tools/agentic/mcpTools.js +88 -0
- package/tools/cli/agent-cli.js +92 -0
- package/tools/export/formats.js +24 -0
- package/tools/math/rotation-baseline.mjs +64 -0
- package/tools/shader-sync-verify.js +937 -0
- package/tools/telemetry/manifestPipeline.js +141 -0
- package/tools/telemetry/telemetryEvents.js +35 -0
- package/types/adaptive-sdk.d.ts +185 -0
- package/types/advanced/AIPresetGenerator.d.ts +81 -0
- package/types/advanced/MIDIController.d.ts +100 -0
- package/types/advanced/OffscreenWorker.d.ts +82 -0
- package/types/advanced/WebGPUCompute.d.ts +52 -0
- package/types/advanced/WebXRRenderer.d.ts +77 -0
- package/types/advanced/index.d.ts +46 -0
- package/types/core/ErrorReporter.d.ts +50 -0
- package/types/core/VIB3Engine.d.ts +204 -0
- package/types/creative/ColorPresetsSystem.d.ts +91 -0
- package/types/creative/ParameterTimeline.d.ts +74 -0
- package/types/creative/PostProcessingPipeline.d.ts +109 -0
- package/types/creative/TransitionAnimator.d.ts +71 -0
- package/types/creative/index.d.ts +35 -0
- package/types/integrations/FigmaPlugin.d.ts +46 -0
- package/types/integrations/OBSMode.d.ts +74 -0
- package/types/integrations/ThreeJsPackage.d.ts +62 -0
- package/types/integrations/TouchDesignerExport.d.ts +36 -0
- package/types/integrations/Vib3React.d.ts +74 -0
- package/types/integrations/Vib3Svelte.d.ts +63 -0
- package/types/integrations/Vib3Vue.d.ts +55 -0
- package/types/integrations/index.d.ts +52 -0
- package/types/reactivity/SpatialInputSystem.d.ts +173 -0
- package/types/reactivity/index.d.ts +394 -0
- package/types/render/CommandBuffer.d.ts +169 -0
- package/types/render/RenderCommand.d.ts +312 -0
- package/types/render/RenderState.d.ts +279 -0
- package/types/render/RenderTarget.d.ts +254 -0
- package/types/render/ShaderProgram.d.ts +277 -0
- package/types/render/UnifiedRenderBridge.d.ts +143 -0
- package/types/render/WebGLBackend.d.ts +168 -0
- package/types/render/WebGPUBackend.d.ts +186 -0
- package/types/render/index.d.ts +141 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostProcessingPipeline.js - VIB3+ Composable Post-Processing FX Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Provides a chain of composable visual effects applied as a post-processing
|
|
5
|
+
* stage on top of the VIB3+ visualization output. Effects are implemented via
|
|
6
|
+
* a combination of CSS filters and off-screen canvas pixel manipulation,
|
|
7
|
+
* requiring no WebGL dependency. Effects can be freely reordered, enabled,
|
|
8
|
+
* disabled, and parameterized at runtime.
|
|
9
|
+
*
|
|
10
|
+
* @module creative/PostProcessingPipeline
|
|
11
|
+
* @version 1.0.0
|
|
12
|
+
* @author VIB3+ Creative Tooling - Phase B
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} EffectDefinition
|
|
17
|
+
* @property {string} name - Effect identifier
|
|
18
|
+
* @property {boolean} enabled - Whether the effect is currently active
|
|
19
|
+
* @property {string} type - 'css' for CSS filter, 'canvas' for pixel manipulation, 'hybrid' for both
|
|
20
|
+
* @property {Object<string, number>} params - Current parameter values
|
|
21
|
+
* @property {Object<string, {min: number, max: number, default: number, description: string}>} paramDefs - Parameter definitions
|
|
22
|
+
* @property {Function} [applyCss] - Returns a CSS filter string fragment
|
|
23
|
+
* @property {Function} [applyCanvas] - Applies pixel manipulation to an ImageData
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} PresetChain
|
|
28
|
+
* @property {string} name - Preset chain name
|
|
29
|
+
* @property {string} description - Human-readable description
|
|
30
|
+
* @property {Array<{effect: string, params: Object}>} effects - Ordered list of effects with params
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Composable post-processing FX pipeline for VIB3+ visualizations.
|
|
35
|
+
*
|
|
36
|
+
* Applies visual effects as a post-processing stage using CSS filters and
|
|
37
|
+
* canvas-based pixel manipulation. No WebGL dependency is required. Effects
|
|
38
|
+
* can be chained in any order and individually toggled.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const pipeline = new PostProcessingPipeline(document.getElementById('viz-container'));
|
|
42
|
+
*
|
|
43
|
+
* // Add individual effects
|
|
44
|
+
* pipeline.addEffect('bloom', { strength: 0.6, radius: 8 });
|
|
45
|
+
* pipeline.addEffect('scanlines', { spacing: 3, opacity: 0.15 });
|
|
46
|
+
* pipeline.addEffect('vignette', { strength: 0.4 });
|
|
47
|
+
*
|
|
48
|
+
* // Apply all active effects
|
|
49
|
+
* pipeline.apply();
|
|
50
|
+
*
|
|
51
|
+
* // Load a preset chain
|
|
52
|
+
* pipeline.loadPresetChain('Retro CRT');
|
|
53
|
+
*/
|
|
54
|
+
export class PostProcessingPipeline {
|
|
55
|
+
/**
|
|
56
|
+
* Create a new PostProcessingPipeline.
|
|
57
|
+
*
|
|
58
|
+
* @param {HTMLElement} targetElement - The DOM element to apply effects to.
|
|
59
|
+
* Typically the main visualization container or a canvas wrapper.
|
|
60
|
+
*/
|
|
61
|
+
constructor(targetElement) {
|
|
62
|
+
if (!targetElement) {
|
|
63
|
+
throw new Error('PostProcessingPipeline requires a target DOM element');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @type {HTMLElement} */
|
|
67
|
+
this.target = targetElement;
|
|
68
|
+
|
|
69
|
+
/** @type {Map<string, EffectDefinition>} All registered effect definitions */
|
|
70
|
+
this.effects = new Map();
|
|
71
|
+
|
|
72
|
+
/** @type {string[]} Ordered list of effect names in the chain */
|
|
73
|
+
this.chain = [];
|
|
74
|
+
|
|
75
|
+
/** @type {boolean} Master enable/disable switch */
|
|
76
|
+
this.enabled = true;
|
|
77
|
+
|
|
78
|
+
/** @type {HTMLCanvasElement|null} Off-screen canvas for pixel manipulation */
|
|
79
|
+
this._offscreenCanvas = null;
|
|
80
|
+
|
|
81
|
+
/** @type {CanvasRenderingContext2D|null} */
|
|
82
|
+
this._offscreenCtx = null;
|
|
83
|
+
|
|
84
|
+
/** @type {Map<string, PresetChain>} Built-in preset chains */
|
|
85
|
+
this._presetChains = new Map();
|
|
86
|
+
|
|
87
|
+
/** @type {string|null} Currently active preset chain name */
|
|
88
|
+
this._activePresetChain = null;
|
|
89
|
+
|
|
90
|
+
/** @type {string} Stores original CSS filter before pipeline was applied */
|
|
91
|
+
this._originalFilter = '';
|
|
92
|
+
|
|
93
|
+
this._initBuiltInEffects();
|
|
94
|
+
this._initPresetChains();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// -------------------------------------------------------------------------
|
|
98
|
+
// Initialization
|
|
99
|
+
// -------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register all built-in effects.
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
_initBuiltInEffects() {
|
|
106
|
+
// --- CSS Filter Effects ---
|
|
107
|
+
|
|
108
|
+
this._registerEffect({
|
|
109
|
+
name: 'bloom',
|
|
110
|
+
type: 'css',
|
|
111
|
+
enabled: false,
|
|
112
|
+
params: { strength: 0.5, radius: 6 },
|
|
113
|
+
paramDefs: {
|
|
114
|
+
strength: { min: 0, max: 2, default: 0.5, description: 'Bloom brightness boost' },
|
|
115
|
+
radius: { min: 1, max: 20, default: 6, description: 'Bloom blur radius (px)' }
|
|
116
|
+
},
|
|
117
|
+
applyCss(params) {
|
|
118
|
+
// Bloom approximated via brightness + blur layering
|
|
119
|
+
return `brightness(${1 + params.strength * 0.5})`;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this._registerEffect({
|
|
124
|
+
name: 'blur',
|
|
125
|
+
type: 'css',
|
|
126
|
+
enabled: false,
|
|
127
|
+
params: { radius: 2 },
|
|
128
|
+
paramDefs: {
|
|
129
|
+
radius: { min: 0, max: 20, default: 2, description: 'Blur radius (px)' }
|
|
130
|
+
},
|
|
131
|
+
applyCss(params) {
|
|
132
|
+
return `blur(${params.radius}px)`;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this._registerEffect({
|
|
137
|
+
name: 'hueRotate',
|
|
138
|
+
type: 'css',
|
|
139
|
+
enabled: false,
|
|
140
|
+
params: { angle: 0 },
|
|
141
|
+
paramDefs: {
|
|
142
|
+
angle: { min: 0, max: 360, default: 0, description: 'Hue rotation angle (degrees)' }
|
|
143
|
+
},
|
|
144
|
+
applyCss(params) {
|
|
145
|
+
return `hue-rotate(${params.angle}deg)`;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
this._registerEffect({
|
|
150
|
+
name: 'invert',
|
|
151
|
+
type: 'css',
|
|
152
|
+
enabled: false,
|
|
153
|
+
params: { amount: 1.0 },
|
|
154
|
+
paramDefs: {
|
|
155
|
+
amount: { min: 0, max: 1, default: 1.0, description: 'Inversion amount (0-1)' }
|
|
156
|
+
},
|
|
157
|
+
applyCss(params) {
|
|
158
|
+
return `invert(${params.amount})`;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this._registerEffect({
|
|
163
|
+
name: 'sharpen',
|
|
164
|
+
type: 'css',
|
|
165
|
+
enabled: false,
|
|
166
|
+
params: { amount: 1.5 },
|
|
167
|
+
paramDefs: {
|
|
168
|
+
amount: { min: 1, max: 5, default: 1.5, description: 'Contrast sharpen amount' }
|
|
169
|
+
},
|
|
170
|
+
applyCss(params) {
|
|
171
|
+
return `contrast(${params.amount})`;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- Canvas Pixel Effects ---
|
|
176
|
+
|
|
177
|
+
this._registerEffect({
|
|
178
|
+
name: 'chromaticAberration',
|
|
179
|
+
type: 'canvas',
|
|
180
|
+
enabled: false,
|
|
181
|
+
params: { offset: 3, angle: 0 },
|
|
182
|
+
paramDefs: {
|
|
183
|
+
offset: { min: 1, max: 20, default: 3, description: 'Color channel offset (px)' },
|
|
184
|
+
angle: { min: 0, max: 360, default: 0, description: 'Aberration angle (degrees)' }
|
|
185
|
+
},
|
|
186
|
+
applyCanvas(imageData, params) {
|
|
187
|
+
const { width, height, data } = imageData;
|
|
188
|
+
const output = new Uint8ClampedArray(data.length);
|
|
189
|
+
const rad = (params.angle * Math.PI) / 180;
|
|
190
|
+
const ox = Math.round(Math.cos(rad) * params.offset);
|
|
191
|
+
const oy = Math.round(Math.sin(rad) * params.offset);
|
|
192
|
+
|
|
193
|
+
for (let y = 0; y < height; y++) {
|
|
194
|
+
for (let x = 0; x < width; x++) {
|
|
195
|
+
const i = (y * width + x) * 4;
|
|
196
|
+
|
|
197
|
+
// Red channel shifted
|
|
198
|
+
const rx = Math.min(width - 1, Math.max(0, x + ox));
|
|
199
|
+
const ry = Math.min(height - 1, Math.max(0, y + oy));
|
|
200
|
+
const ri = (ry * width + rx) * 4;
|
|
201
|
+
|
|
202
|
+
// Blue channel shifted opposite
|
|
203
|
+
const bx = Math.min(width - 1, Math.max(0, x - ox));
|
|
204
|
+
const by = Math.min(height - 1, Math.max(0, y - oy));
|
|
205
|
+
const bi = (by * width + bx) * 4;
|
|
206
|
+
|
|
207
|
+
output[i] = data[ri]; // R from offset position
|
|
208
|
+
output[i + 1] = data[i + 1]; // G stays
|
|
209
|
+
output[i + 2] = data[bi + 2]; // B from opposite offset
|
|
210
|
+
output[i + 3] = data[i + 3]; // A stays
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
imageData.data.set(output);
|
|
215
|
+
return imageData;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this._registerEffect({
|
|
220
|
+
name: 'filmGrain',
|
|
221
|
+
type: 'canvas',
|
|
222
|
+
enabled: false,
|
|
223
|
+
params: { intensity: 0.15, size: 1 },
|
|
224
|
+
paramDefs: {
|
|
225
|
+
intensity: { min: 0, max: 1, default: 0.15, description: 'Grain noise intensity' },
|
|
226
|
+
size: { min: 1, max: 4, default: 1, description: 'Grain particle size' }
|
|
227
|
+
},
|
|
228
|
+
applyCanvas(imageData, params) {
|
|
229
|
+
const { data } = imageData;
|
|
230
|
+
const strength = params.intensity * 80;
|
|
231
|
+
const step = Math.max(1, Math.round(params.size));
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < data.length; i += 4 * step) {
|
|
234
|
+
const noise = (Math.random() - 0.5) * strength;
|
|
235
|
+
data[i] = Math.min(255, Math.max(0, data[i] + noise));
|
|
236
|
+
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + noise));
|
|
237
|
+
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + noise));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return imageData;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
this._registerEffect({
|
|
245
|
+
name: 'vignette',
|
|
246
|
+
type: 'canvas',
|
|
247
|
+
enabled: false,
|
|
248
|
+
params: { strength: 0.5, radius: 0.7 },
|
|
249
|
+
paramDefs: {
|
|
250
|
+
strength: { min: 0, max: 1, default: 0.5, description: 'Darkening strength' },
|
|
251
|
+
radius: { min: 0.1, max: 1.5, default: 0.7, description: 'Vignette inner radius' }
|
|
252
|
+
},
|
|
253
|
+
applyCanvas(imageData, params) {
|
|
254
|
+
const { width, height, data } = imageData;
|
|
255
|
+
const cx = width / 2;
|
|
256
|
+
const cy = height / 2;
|
|
257
|
+
const maxDist = Math.sqrt(cx * cx + cy * cy);
|
|
258
|
+
const innerRadius = params.radius;
|
|
259
|
+
const strength = params.strength;
|
|
260
|
+
|
|
261
|
+
for (let y = 0; y < height; y++) {
|
|
262
|
+
for (let x = 0; x < width; x++) {
|
|
263
|
+
const dx = (x - cx) / cx;
|
|
264
|
+
const dy = (y - cy) / cy;
|
|
265
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
266
|
+
const vignette = 1 - Math.max(0, (dist - innerRadius) / (1.5 - innerRadius)) * strength;
|
|
267
|
+
const i = (y * width + x) * 4;
|
|
268
|
+
data[i] = data[i] * vignette;
|
|
269
|
+
data[i + 1] = data[i + 1] * vignette;
|
|
270
|
+
data[i + 2] = data[i + 2] * vignette;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return imageData;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
this._registerEffect({
|
|
279
|
+
name: 'scanlines',
|
|
280
|
+
type: 'canvas',
|
|
281
|
+
enabled: false,
|
|
282
|
+
params: { spacing: 3, opacity: 0.2, thickness: 1 },
|
|
283
|
+
paramDefs: {
|
|
284
|
+
spacing: { min: 1, max: 10, default: 3, description: 'Line spacing (px)' },
|
|
285
|
+
opacity: { min: 0, max: 1, default: 0.2, description: 'Scanline darkness' },
|
|
286
|
+
thickness: { min: 1, max: 4, default: 1, description: 'Line thickness (px)' }
|
|
287
|
+
},
|
|
288
|
+
applyCanvas(imageData, params) {
|
|
289
|
+
const { width, height, data } = imageData;
|
|
290
|
+
const spacing = Math.max(1, Math.round(params.spacing));
|
|
291
|
+
const thickness = Math.max(1, Math.round(params.thickness));
|
|
292
|
+
const darken = 1 - params.opacity;
|
|
293
|
+
|
|
294
|
+
for (let y = 0; y < height; y++) {
|
|
295
|
+
if (y % spacing < thickness) {
|
|
296
|
+
for (let x = 0; x < width; x++) {
|
|
297
|
+
const i = (y * width + x) * 4;
|
|
298
|
+
data[i] = data[i] * darken;
|
|
299
|
+
data[i + 1] = data[i + 1] * darken;
|
|
300
|
+
data[i + 2] = data[i + 2] * darken;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return imageData;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
this._registerEffect({
|
|
310
|
+
name: 'glitch',
|
|
311
|
+
type: 'canvas',
|
|
312
|
+
enabled: false,
|
|
313
|
+
params: { intensity: 0.3, blockSize: 8, frequency: 0.5 },
|
|
314
|
+
paramDefs: {
|
|
315
|
+
intensity: { min: 0, max: 1, default: 0.3, description: 'Glitch displacement intensity' },
|
|
316
|
+
blockSize: { min: 2, max: 50, default: 8, description: 'Glitch block height (px)' },
|
|
317
|
+
frequency: { min: 0, max: 1, default: 0.5, description: 'Probability of a block glitching' }
|
|
318
|
+
},
|
|
319
|
+
applyCanvas(imageData, params) {
|
|
320
|
+
const { width, height, data } = imageData;
|
|
321
|
+
const output = new Uint8ClampedArray(data);
|
|
322
|
+
const blockH = Math.max(2, Math.round(params.blockSize));
|
|
323
|
+
const maxShift = Math.round(width * params.intensity * 0.3);
|
|
324
|
+
|
|
325
|
+
for (let y = 0; y < height; y += blockH) {
|
|
326
|
+
if (Math.random() > params.frequency) continue;
|
|
327
|
+
|
|
328
|
+
const shift = Math.round((Math.random() - 0.5) * 2 * maxShift);
|
|
329
|
+
const blockEnd = Math.min(y + blockH, height);
|
|
330
|
+
|
|
331
|
+
for (let row = y; row < blockEnd; row++) {
|
|
332
|
+
for (let x = 0; x < width; x++) {
|
|
333
|
+
const srcX = Math.min(width - 1, Math.max(0, x + shift));
|
|
334
|
+
const dstIdx = (row * width + x) * 4;
|
|
335
|
+
const srcIdx = (row * width + srcX) * 4;
|
|
336
|
+
output[dstIdx] = data[srcIdx];
|
|
337
|
+
output[dstIdx + 1] = data[srcIdx + 1];
|
|
338
|
+
output[dstIdx + 2] = data[srcIdx + 2];
|
|
339
|
+
output[dstIdx + 3] = data[srcIdx + 3];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
imageData.data.set(output);
|
|
345
|
+
return imageData;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
this._registerEffect({
|
|
350
|
+
name: 'pixelate',
|
|
351
|
+
type: 'canvas',
|
|
352
|
+
enabled: false,
|
|
353
|
+
params: { size: 4 },
|
|
354
|
+
paramDefs: {
|
|
355
|
+
size: { min: 1, max: 32, default: 4, description: 'Pixel block size' }
|
|
356
|
+
},
|
|
357
|
+
applyCanvas(imageData, params) {
|
|
358
|
+
const { width, height, data } = imageData;
|
|
359
|
+
const size = Math.max(1, Math.round(params.size));
|
|
360
|
+
|
|
361
|
+
for (let y = 0; y < height; y += size) {
|
|
362
|
+
for (let x = 0; x < width; x += size) {
|
|
363
|
+
// Average the block
|
|
364
|
+
let r = 0, g = 0, b = 0, count = 0;
|
|
365
|
+
for (let dy = 0; dy < size && y + dy < height; dy++) {
|
|
366
|
+
for (let dx = 0; dx < size && x + dx < width; dx++) {
|
|
367
|
+
const i = ((y + dy) * width + (x + dx)) * 4;
|
|
368
|
+
r += data[i];
|
|
369
|
+
g += data[i + 1];
|
|
370
|
+
b += data[i + 2];
|
|
371
|
+
count++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
r = Math.round(r / count);
|
|
375
|
+
g = Math.round(g / count);
|
|
376
|
+
b = Math.round(b / count);
|
|
377
|
+
|
|
378
|
+
// Fill the block
|
|
379
|
+
for (let dy = 0; dy < size && y + dy < height; dy++) {
|
|
380
|
+
for (let dx = 0; dx < size && x + dx < width; dx++) {
|
|
381
|
+
const i = ((y + dy) * width + (x + dx)) * 4;
|
|
382
|
+
data[i] = r;
|
|
383
|
+
data[i + 1] = g;
|
|
384
|
+
data[i + 2] = b;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return imageData;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
this._registerEffect({
|
|
395
|
+
name: 'kaleidoscope',
|
|
396
|
+
type: 'canvas',
|
|
397
|
+
enabled: false,
|
|
398
|
+
params: { segments: 6, rotation: 0 },
|
|
399
|
+
paramDefs: {
|
|
400
|
+
segments: { min: 2, max: 16, default: 6, description: 'Number of mirror segments' },
|
|
401
|
+
rotation: { min: 0, max: 360, default: 0, description: 'Rotation angle (degrees)' }
|
|
402
|
+
},
|
|
403
|
+
applyCanvas(imageData, params) {
|
|
404
|
+
const { width, height, data } = imageData;
|
|
405
|
+
const output = new Uint8ClampedArray(data);
|
|
406
|
+
const cx = width / 2;
|
|
407
|
+
const cy = height / 2;
|
|
408
|
+
const segments = Math.max(2, Math.round(params.segments));
|
|
409
|
+
const segAngle = (2 * Math.PI) / segments;
|
|
410
|
+
const rotRad = (params.rotation * Math.PI) / 180;
|
|
411
|
+
|
|
412
|
+
for (let y = 0; y < height; y++) {
|
|
413
|
+
for (let x = 0; x < width; x++) {
|
|
414
|
+
const dx = x - cx;
|
|
415
|
+
const dy = y - cy;
|
|
416
|
+
let angle = Math.atan2(dy, dx) - rotRad;
|
|
417
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
418
|
+
|
|
419
|
+
// Normalize angle to first segment
|
|
420
|
+
angle = ((angle % segAngle) + segAngle) % segAngle;
|
|
421
|
+
|
|
422
|
+
// Mirror within segment
|
|
423
|
+
if (angle > segAngle / 2) {
|
|
424
|
+
angle = segAngle - angle;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
angle += rotRad;
|
|
428
|
+
|
|
429
|
+
const srcX = Math.round(cx + Math.cos(angle) * dist);
|
|
430
|
+
const srcY = Math.round(cy + Math.sin(angle) * dist);
|
|
431
|
+
|
|
432
|
+
const dstIdx = (y * width + x) * 4;
|
|
433
|
+
|
|
434
|
+
if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) {
|
|
435
|
+
const srcIdx = (srcY * width + srcX) * 4;
|
|
436
|
+
output[dstIdx] = data[srcIdx];
|
|
437
|
+
output[dstIdx + 1] = data[srcIdx + 1];
|
|
438
|
+
output[dstIdx + 2] = data[srcIdx + 2];
|
|
439
|
+
output[dstIdx + 3] = data[srcIdx + 3];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
imageData.data.set(output);
|
|
445
|
+
return imageData;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
this._registerEffect({
|
|
450
|
+
name: 'mirror',
|
|
451
|
+
type: 'canvas',
|
|
452
|
+
enabled: false,
|
|
453
|
+
params: { axis: 0, position: 0.5 },
|
|
454
|
+
paramDefs: {
|
|
455
|
+
axis: { min: 0, max: 1, default: 0, description: '0 = horizontal mirror, 1 = vertical mirror' },
|
|
456
|
+
position: { min: 0, max: 1, default: 0.5, description: 'Mirror position (0-1)' }
|
|
457
|
+
},
|
|
458
|
+
applyCanvas(imageData, params) {
|
|
459
|
+
const { width, height, data } = imageData;
|
|
460
|
+
const output = new Uint8ClampedArray(data);
|
|
461
|
+
const isVertical = Math.round(params.axis) === 1;
|
|
462
|
+
|
|
463
|
+
if (isVertical) {
|
|
464
|
+
const mirrorY = Math.round(height * params.position);
|
|
465
|
+
for (let y = mirrorY; y < height; y++) {
|
|
466
|
+
const srcY = mirrorY - (y - mirrorY) - 1;
|
|
467
|
+
if (srcY < 0) continue;
|
|
468
|
+
for (let x = 0; x < width; x++) {
|
|
469
|
+
const dstIdx = (y * width + x) * 4;
|
|
470
|
+
const srcIdx = (srcY * width + x) * 4;
|
|
471
|
+
output[dstIdx] = data[srcIdx];
|
|
472
|
+
output[dstIdx + 1] = data[srcIdx + 1];
|
|
473
|
+
output[dstIdx + 2] = data[srcIdx + 2];
|
|
474
|
+
output[dstIdx + 3] = data[srcIdx + 3];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
const mirrorX = Math.round(width * params.position);
|
|
479
|
+
for (let y = 0; y < height; y++) {
|
|
480
|
+
for (let x = mirrorX; x < width; x++) {
|
|
481
|
+
const srcX = mirrorX - (x - mirrorX) - 1;
|
|
482
|
+
if (srcX < 0) continue;
|
|
483
|
+
const dstIdx = (y * width + x) * 4;
|
|
484
|
+
const srcIdx = (y * width + srcX) * 4;
|
|
485
|
+
output[dstIdx] = data[srcIdx];
|
|
486
|
+
output[dstIdx + 1] = data[srcIdx + 1];
|
|
487
|
+
output[dstIdx + 2] = data[srcIdx + 2];
|
|
488
|
+
output[dstIdx + 3] = data[srcIdx + 3];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
imageData.data.set(output);
|
|
494
|
+
return imageData;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
this._registerEffect({
|
|
499
|
+
name: 'posterize',
|
|
500
|
+
type: 'canvas',
|
|
501
|
+
enabled: false,
|
|
502
|
+
params: { levels: 4 },
|
|
503
|
+
paramDefs: {
|
|
504
|
+
levels: { min: 2, max: 32, default: 4, description: 'Number of color levels per channel' }
|
|
505
|
+
},
|
|
506
|
+
applyCanvas(imageData, params) {
|
|
507
|
+
const { data } = imageData;
|
|
508
|
+
const levels = Math.max(2, Math.round(params.levels));
|
|
509
|
+
const factor = 255 / (levels - 1);
|
|
510
|
+
|
|
511
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
512
|
+
data[i] = Math.round(Math.round(data[i] / factor) * factor);
|
|
513
|
+
data[i + 1] = Math.round(Math.round(data[i + 1] / factor) * factor);
|
|
514
|
+
data[i + 2] = Math.round(Math.round(data[i + 2] / factor) * factor);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return imageData;
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Initialize built-in preset chains.
|
|
524
|
+
* @private
|
|
525
|
+
*/
|
|
526
|
+
_initPresetChains() {
|
|
527
|
+
const chains = [
|
|
528
|
+
{
|
|
529
|
+
name: 'Retro CRT',
|
|
530
|
+
description: 'Classic CRT monitor look with scanlines, chromatic aberration, and vignette',
|
|
531
|
+
effects: [
|
|
532
|
+
{ effect: 'scanlines', params: { spacing: 3, opacity: 0.2, thickness: 1 } },
|
|
533
|
+
{ effect: 'chromaticAberration', params: { offset: 2, angle: 0 } },
|
|
534
|
+
{ effect: 'vignette', params: { strength: 0.6, radius: 0.6 } },
|
|
535
|
+
{ effect: 'filmGrain', params: { intensity: 0.08, size: 1 } },
|
|
536
|
+
{ effect: 'bloom', params: { strength: 0.3, radius: 4 } }
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'Holographic',
|
|
541
|
+
description: 'Iridescent holographic display with color shifts and bloom',
|
|
542
|
+
effects: [
|
|
543
|
+
{ effect: 'hueRotate', params: { angle: 30 } },
|
|
544
|
+
{ effect: 'bloom', params: { strength: 0.7, radius: 10 } },
|
|
545
|
+
{ effect: 'chromaticAberration', params: { offset: 5, angle: 45 } }
|
|
546
|
+
]
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: 'Glitch Art',
|
|
550
|
+
description: 'Aggressive digital glitch aesthetic',
|
|
551
|
+
effects: [
|
|
552
|
+
{ effect: 'glitch', params: { intensity: 0.6, blockSize: 12, frequency: 0.7 } },
|
|
553
|
+
{ effect: 'chromaticAberration', params: { offset: 8, angle: 0 } },
|
|
554
|
+
{ effect: 'posterize', params: { levels: 6 } },
|
|
555
|
+
{ effect: 'scanlines', params: { spacing: 4, opacity: 0.15, thickness: 1 } }
|
|
556
|
+
]
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
name: 'Clean',
|
|
560
|
+
description: 'Minimal processing, just subtle polish',
|
|
561
|
+
effects: [
|
|
562
|
+
{ effect: 'sharpen', params: { amount: 1.2 } },
|
|
563
|
+
{ effect: 'vignette', params: { strength: 0.15, radius: 0.9 } }
|
|
564
|
+
]
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: 'Cinematic',
|
|
568
|
+
description: 'Film-grade look with grain, vignette, and subtle color grading',
|
|
569
|
+
effects: [
|
|
570
|
+
{ effect: 'vignette', params: { strength: 0.45, radius: 0.65 } },
|
|
571
|
+
{ effect: 'filmGrain', params: { intensity: 0.1, size: 1 } },
|
|
572
|
+
{ effect: 'bloom', params: { strength: 0.2, radius: 6 } },
|
|
573
|
+
{ effect: 'sharpen', params: { amount: 1.15 } }
|
|
574
|
+
]
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: 'Psychedelic',
|
|
578
|
+
description: 'Wild color shifts with kaleidoscope and inversion',
|
|
579
|
+
effects: [
|
|
580
|
+
{ effect: 'kaleidoscope', params: { segments: 8, rotation: 15 } },
|
|
581
|
+
{ effect: 'hueRotate', params: { angle: 90 } },
|
|
582
|
+
{ effect: 'bloom', params: { strength: 0.6, radius: 8 } },
|
|
583
|
+
{ effect: 'chromaticAberration', params: { offset: 4, angle: 120 } }
|
|
584
|
+
]
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: 'Lo-Fi',
|
|
588
|
+
description: 'Low fidelity retro aesthetic',
|
|
589
|
+
effects: [
|
|
590
|
+
{ effect: 'pixelate', params: { size: 3 } },
|
|
591
|
+
{ effect: 'posterize', params: { levels: 8 } },
|
|
592
|
+
{ effect: 'filmGrain', params: { intensity: 0.2, size: 2 } },
|
|
593
|
+
{ effect: 'vignette', params: { strength: 0.35, radius: 0.7 } }
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
];
|
|
597
|
+
|
|
598
|
+
for (const chain of chains) {
|
|
599
|
+
this._presetChains.set(chain.name, chain);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Register an effect definition.
|
|
605
|
+
*
|
|
606
|
+
* @param {EffectDefinition} def - Effect definition
|
|
607
|
+
* @private
|
|
608
|
+
*/
|
|
609
|
+
_registerEffect(def) {
|
|
610
|
+
this.effects.set(def.name, {
|
|
611
|
+
name: def.name,
|
|
612
|
+
type: def.type,
|
|
613
|
+
enabled: def.enabled || false,
|
|
614
|
+
params: { ...def.params },
|
|
615
|
+
paramDefs: def.paramDefs ? { ...def.paramDefs } : {},
|
|
616
|
+
applyCss: def.applyCss || null,
|
|
617
|
+
applyCanvas: def.applyCanvas || null
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// -------------------------------------------------------------------------
|
|
622
|
+
// Public API - Effect Management
|
|
623
|
+
// -------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Add an effect to the active chain (enables it and appends to chain order).
|
|
627
|
+
*
|
|
628
|
+
* @param {string} name - Effect name
|
|
629
|
+
* @param {Object<string, number>} [params] - Optional parameter overrides
|
|
630
|
+
* @returns {boolean} true if the effect was added
|
|
631
|
+
*/
|
|
632
|
+
addEffect(name, params = {}) {
|
|
633
|
+
const effect = this.effects.get(name);
|
|
634
|
+
if (!effect) {
|
|
635
|
+
console.warn(`PostProcessingPipeline: Unknown effect "${name}"`);
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Apply parameter overrides
|
|
640
|
+
if (params && typeof params === 'object') {
|
|
641
|
+
for (const [key, value] of Object.entries(params)) {
|
|
642
|
+
if (key in effect.paramDefs) {
|
|
643
|
+
const def = effect.paramDefs[key];
|
|
644
|
+
effect.params[key] = this._clamp(Number(value), def.min, def.max);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
effect.enabled = true;
|
|
650
|
+
|
|
651
|
+
// Add to chain if not already present
|
|
652
|
+
if (!this.chain.includes(name)) {
|
|
653
|
+
this.chain.push(name);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Remove an effect from the active chain (disables it).
|
|
661
|
+
*
|
|
662
|
+
* @param {string} name - Effect name
|
|
663
|
+
* @returns {boolean} true if the effect was removed
|
|
664
|
+
*/
|
|
665
|
+
removeEffect(name) {
|
|
666
|
+
const effect = this.effects.get(name);
|
|
667
|
+
if (!effect) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
effect.enabled = false;
|
|
672
|
+
const idx = this.chain.indexOf(name);
|
|
673
|
+
if (idx >= 0) {
|
|
674
|
+
this.chain.splice(idx, 1);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Toggle an effect on or off.
|
|
682
|
+
*
|
|
683
|
+
* @param {string} name - Effect name
|
|
684
|
+
* @returns {boolean} New enabled state, or false if effect not found
|
|
685
|
+
*/
|
|
686
|
+
toggleEffect(name) {
|
|
687
|
+
const effect = this.effects.get(name);
|
|
688
|
+
if (!effect) return false;
|
|
689
|
+
|
|
690
|
+
if (effect.enabled) {
|
|
691
|
+
this.removeEffect(name);
|
|
692
|
+
return false;
|
|
693
|
+
} else {
|
|
694
|
+
this.addEffect(name);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Set a parameter on an effect.
|
|
701
|
+
*
|
|
702
|
+
* @param {string} effectName - Effect name
|
|
703
|
+
* @param {string} param - Parameter name
|
|
704
|
+
* @param {number} value - New parameter value
|
|
705
|
+
* @returns {boolean} true if the parameter was set
|
|
706
|
+
*/
|
|
707
|
+
setEffectParam(effectName, param, value) {
|
|
708
|
+
const effect = this.effects.get(effectName);
|
|
709
|
+
if (!effect) {
|
|
710
|
+
console.warn(`PostProcessingPipeline: Unknown effect "${effectName}"`);
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const def = effect.paramDefs[param];
|
|
715
|
+
if (!def) {
|
|
716
|
+
console.warn(`PostProcessingPipeline: Unknown param "${param}" on effect "${effectName}"`);
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
effect.params[param] = this._clamp(Number(value), def.min, def.max);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Set multiple parameters on an effect at once.
|
|
726
|
+
*
|
|
727
|
+
* @param {string} effectName - Effect name
|
|
728
|
+
* @param {Object<string, number>} params - Parameter key-value pairs
|
|
729
|
+
*/
|
|
730
|
+
setEffectParams(effectName, params) {
|
|
731
|
+
if (!params || typeof params !== 'object') return;
|
|
732
|
+
for (const [key, value] of Object.entries(params)) {
|
|
733
|
+
this.setEffectParam(effectName, key, value);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Reorder the effects chain.
|
|
739
|
+
*
|
|
740
|
+
* @param {string[]} newOrder - Array of effect names in desired order.
|
|
741
|
+
* Only effects that are currently in the chain are kept.
|
|
742
|
+
*/
|
|
743
|
+
reorder(newOrder) {
|
|
744
|
+
if (!Array.isArray(newOrder)) {
|
|
745
|
+
console.warn('PostProcessingPipeline: reorder expects an array of effect names');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Validate: keep only effects that exist and are in the current chain
|
|
750
|
+
const validOrder = newOrder.filter(name =>
|
|
751
|
+
this.effects.has(name) && this.chain.includes(name)
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
// Add any effects that were in the chain but not in the new order (at the end)
|
|
755
|
+
for (const name of this.chain) {
|
|
756
|
+
if (!validOrder.includes(name)) {
|
|
757
|
+
validOrder.push(name);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
this.chain = validOrder;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// -------------------------------------------------------------------------
|
|
765
|
+
// Public API - Applying Effects
|
|
766
|
+
// -------------------------------------------------------------------------
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Apply all enabled effects in the current chain order.
|
|
770
|
+
*
|
|
771
|
+
* CSS filter effects are combined into a single filter string and applied
|
|
772
|
+
* to the target element's style. Canvas effects are applied to any
|
|
773
|
+
* `<canvas>` children of the target element.
|
|
774
|
+
*/
|
|
775
|
+
apply() {
|
|
776
|
+
if (!this.enabled) {
|
|
777
|
+
this._clearEffects();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Separate CSS and canvas effects in chain order
|
|
782
|
+
const cssEffects = [];
|
|
783
|
+
const canvasEffects = [];
|
|
784
|
+
|
|
785
|
+
for (const name of this.chain) {
|
|
786
|
+
const effect = this.effects.get(name);
|
|
787
|
+
if (!effect || !effect.enabled) continue;
|
|
788
|
+
|
|
789
|
+
if ((effect.type === 'css' || effect.type === 'hybrid') && effect.applyCss) {
|
|
790
|
+
cssEffects.push(effect);
|
|
791
|
+
}
|
|
792
|
+
if ((effect.type === 'canvas' || effect.type === 'hybrid') && effect.applyCanvas) {
|
|
793
|
+
canvasEffects.push(effect);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Apply CSS filters
|
|
798
|
+
this._applyCssFilters(cssEffects);
|
|
799
|
+
|
|
800
|
+
// Apply canvas effects
|
|
801
|
+
if (canvasEffects.length > 0) {
|
|
802
|
+
this._applyCanvasEffects(canvasEffects);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Remove all applied effects and restore the target element.
|
|
808
|
+
*/
|
|
809
|
+
clear() {
|
|
810
|
+
this._clearEffects();
|
|
811
|
+
for (const [, effect] of this.effects) {
|
|
812
|
+
effect.enabled = false;
|
|
813
|
+
}
|
|
814
|
+
this.chain = [];
|
|
815
|
+
this._activePresetChain = null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// -------------------------------------------------------------------------
|
|
819
|
+
// Public API - Preset Chains
|
|
820
|
+
// -------------------------------------------------------------------------
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Load and apply a preset effect chain by name.
|
|
824
|
+
*
|
|
825
|
+
* @param {string} name - Preset chain name
|
|
826
|
+
* @returns {boolean} true if the preset was found and applied
|
|
827
|
+
*/
|
|
828
|
+
loadPresetChain(name) {
|
|
829
|
+
const preset = this._presetChains.get(name);
|
|
830
|
+
if (!preset) {
|
|
831
|
+
console.warn(`PostProcessingPipeline: Unknown preset chain "${name}"`);
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Clear current chain
|
|
836
|
+
this.clear();
|
|
837
|
+
|
|
838
|
+
// Apply each effect in the preset
|
|
839
|
+
for (const { effect, params } of preset.effects) {
|
|
840
|
+
this.addEffect(effect, params);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
this._activePresetChain = name;
|
|
844
|
+
this.apply();
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Get a list of all available preset chains.
|
|
850
|
+
*
|
|
851
|
+
* @returns {Array<{name: string, description: string}>}
|
|
852
|
+
*/
|
|
853
|
+
getPresetChains() {
|
|
854
|
+
const list = [];
|
|
855
|
+
for (const [, chain] of this._presetChains) {
|
|
856
|
+
list.push({ name: chain.name, description: chain.description });
|
|
857
|
+
}
|
|
858
|
+
return list;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Get the currently active preset chain name.
|
|
863
|
+
*
|
|
864
|
+
* @returns {string|null}
|
|
865
|
+
*/
|
|
866
|
+
getActivePresetChain() {
|
|
867
|
+
return this._activePresetChain;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// -------------------------------------------------------------------------
|
|
871
|
+
// Public API - Queries
|
|
872
|
+
// -------------------------------------------------------------------------
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Get all available effects and their current state.
|
|
876
|
+
*
|
|
877
|
+
* @returns {Array<{name: string, enabled: boolean, type: string, params: Object, paramDefs: Object}>}
|
|
878
|
+
*/
|
|
879
|
+
getEffects() {
|
|
880
|
+
const list = [];
|
|
881
|
+
for (const [, effect] of this.effects) {
|
|
882
|
+
list.push({
|
|
883
|
+
name: effect.name,
|
|
884
|
+
enabled: effect.enabled,
|
|
885
|
+
type: effect.type,
|
|
886
|
+
params: { ...effect.params },
|
|
887
|
+
paramDefs: { ...effect.paramDefs }
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return list;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Get the current ordered chain of enabled effects.
|
|
895
|
+
*
|
|
896
|
+
* @returns {string[]}
|
|
897
|
+
*/
|
|
898
|
+
getChain() {
|
|
899
|
+
return [...this.chain];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Get details of a specific effect.
|
|
904
|
+
*
|
|
905
|
+
* @param {string} name - Effect name
|
|
906
|
+
* @returns {{name: string, enabled: boolean, type: string, params: Object, paramDefs: Object}|null}
|
|
907
|
+
*/
|
|
908
|
+
getEffect(name) {
|
|
909
|
+
const effect = this.effects.get(name);
|
|
910
|
+
if (!effect) return null;
|
|
911
|
+
return {
|
|
912
|
+
name: effect.name,
|
|
913
|
+
enabled: effect.enabled,
|
|
914
|
+
type: effect.type,
|
|
915
|
+
params: { ...effect.params },
|
|
916
|
+
paramDefs: { ...effect.paramDefs }
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// -------------------------------------------------------------------------
|
|
921
|
+
// Public API - Serialization
|
|
922
|
+
// -------------------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Export the current pipeline state for storage.
|
|
926
|
+
*
|
|
927
|
+
* @returns {Object} Serializable pipeline state
|
|
928
|
+
*/
|
|
929
|
+
exportState() {
|
|
930
|
+
const chainState = this.chain.map(name => {
|
|
931
|
+
const effect = this.effects.get(name);
|
|
932
|
+
return {
|
|
933
|
+
effect: name,
|
|
934
|
+
enabled: effect ? effect.enabled : false,
|
|
935
|
+
params: effect ? { ...effect.params } : {}
|
|
936
|
+
};
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
type: 'vib3-postprocess-pipeline',
|
|
941
|
+
version: '1.0.0',
|
|
942
|
+
timestamp: new Date().toISOString(),
|
|
943
|
+
enabled: this.enabled,
|
|
944
|
+
chain: chainState
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Import a pipeline state from serialized data.
|
|
950
|
+
*
|
|
951
|
+
* @param {Object} data - Pipeline state data
|
|
952
|
+
* @returns {boolean} true if imported successfully
|
|
953
|
+
*/
|
|
954
|
+
importState(data) {
|
|
955
|
+
if (!data || data.type !== 'vib3-postprocess-pipeline' || !Array.isArray(data.chain)) {
|
|
956
|
+
console.warn('PostProcessingPipeline: Invalid import data');
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
this.clear();
|
|
961
|
+
this.enabled = data.enabled !== false;
|
|
962
|
+
|
|
963
|
+
for (const item of data.chain) {
|
|
964
|
+
if (item.effect && this.effects.has(item.effect)) {
|
|
965
|
+
this.addEffect(item.effect, item.params || {});
|
|
966
|
+
if (item.enabled === false) {
|
|
967
|
+
const effect = this.effects.get(item.effect);
|
|
968
|
+
if (effect) effect.enabled = false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// -------------------------------------------------------------------------
|
|
977
|
+
// Public API - Lifecycle
|
|
978
|
+
// -------------------------------------------------------------------------
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Set the master enabled state of the pipeline.
|
|
982
|
+
*
|
|
983
|
+
* @param {boolean} enabled
|
|
984
|
+
*/
|
|
985
|
+
setEnabled(enabled) {
|
|
986
|
+
this.enabled = Boolean(enabled);
|
|
987
|
+
if (!this.enabled) {
|
|
988
|
+
this._clearEffects();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Clean up resources and remove all effects.
|
|
994
|
+
*/
|
|
995
|
+
dispose() {
|
|
996
|
+
this._clearEffects();
|
|
997
|
+
this.effects.clear();
|
|
998
|
+
this.chain = [];
|
|
999
|
+
this._presetChains.clear();
|
|
1000
|
+
this._offscreenCanvas = null;
|
|
1001
|
+
this._offscreenCtx = null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// -------------------------------------------------------------------------
|
|
1005
|
+
// Private - CSS Filter Application
|
|
1006
|
+
// -------------------------------------------------------------------------
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Compose and apply CSS filter string from enabled effects.
|
|
1010
|
+
*
|
|
1011
|
+
* @param {EffectDefinition[]} effects - CSS effects to apply
|
|
1012
|
+
* @private
|
|
1013
|
+
*/
|
|
1014
|
+
_applyCssFilters(effects) {
|
|
1015
|
+
if (effects.length === 0) {
|
|
1016
|
+
this.target.style.filter = '';
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const filterParts = effects.map(effect => effect.applyCss(effect.params));
|
|
1021
|
+
this.target.style.filter = filterParts.join(' ');
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// -------------------------------------------------------------------------
|
|
1025
|
+
// Private - Canvas Pixel Effect Application
|
|
1026
|
+
// -------------------------------------------------------------------------
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Apply canvas-based pixel manipulation effects.
|
|
1030
|
+
*
|
|
1031
|
+
* Finds all `<canvas>` elements within the target element and applies
|
|
1032
|
+
* effects to each one using an off-screen canvas buffer.
|
|
1033
|
+
*
|
|
1034
|
+
* @param {EffectDefinition[]} effects - Canvas effects to apply
|
|
1035
|
+
* @private
|
|
1036
|
+
*/
|
|
1037
|
+
_applyCanvasEffects(effects) {
|
|
1038
|
+
// Find all canvas elements in the target
|
|
1039
|
+
const canvases = this.target.querySelectorAll
|
|
1040
|
+
? this.target.querySelectorAll('canvas')
|
|
1041
|
+
: [];
|
|
1042
|
+
|
|
1043
|
+
// If the target itself is a canvas, include it
|
|
1044
|
+
const targets = this.target.tagName === 'CANVAS'
|
|
1045
|
+
? [this.target]
|
|
1046
|
+
: Array.from(canvases);
|
|
1047
|
+
|
|
1048
|
+
for (const canvas of targets) {
|
|
1049
|
+
this._applyEffectsToCanvas(canvas, effects);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Apply a list of effects to a single canvas element.
|
|
1055
|
+
*
|
|
1056
|
+
* @param {HTMLCanvasElement} canvas
|
|
1057
|
+
* @param {EffectDefinition[]} effects
|
|
1058
|
+
* @private
|
|
1059
|
+
*/
|
|
1060
|
+
_applyEffectsToCanvas(canvas, effects) {
|
|
1061
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
1062
|
+
if (!ctx) return;
|
|
1063
|
+
|
|
1064
|
+
const width = canvas.width;
|
|
1065
|
+
const height = canvas.height;
|
|
1066
|
+
if (width === 0 || height === 0) return;
|
|
1067
|
+
|
|
1068
|
+
try {
|
|
1069
|
+
let imageData = ctx.getImageData(0, 0, width, height);
|
|
1070
|
+
|
|
1071
|
+
for (const effect of effects) {
|
|
1072
|
+
if (effect.applyCanvas) {
|
|
1073
|
+
imageData = effect.applyCanvas(imageData, effect.params);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
// getImageData can throw on tainted canvases (cross-origin)
|
|
1080
|
+
console.warn('PostProcessingPipeline: Cannot apply canvas effects -', err.message);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// -------------------------------------------------------------------------
|
|
1085
|
+
// Private - Cleanup
|
|
1086
|
+
// -------------------------------------------------------------------------
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Remove all visual effects from the target element.
|
|
1090
|
+
* @private
|
|
1091
|
+
*/
|
|
1092
|
+
_clearEffects() {
|
|
1093
|
+
this.target.style.filter = '';
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// -------------------------------------------------------------------------
|
|
1097
|
+
// Private - Utilities
|
|
1098
|
+
// -------------------------------------------------------------------------
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Clamp a value to a range.
|
|
1102
|
+
*
|
|
1103
|
+
* @param {number} value
|
|
1104
|
+
* @param {number} min
|
|
1105
|
+
* @param {number} max
|
|
1106
|
+
* @returns {number}
|
|
1107
|
+
* @private
|
|
1108
|
+
*/
|
|
1109
|
+
_clamp(value, min, max) {
|
|
1110
|
+
if (!Number.isFinite(value)) return min;
|
|
1111
|
+
return Math.max(min, Math.min(max, value));
|
|
1112
|
+
}
|
|
1113
|
+
}
|