@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.
Files changed (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DOCS/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +34 -0
  3. package/DOCS/CI_TESTING.md +38 -0
  4. package/DOCS/CLI_ONBOARDING.md +75 -0
  5. package/DOCS/CONTROL_REFERENCE.md +64 -0
  6. package/DOCS/DEV_TRACK_ANALYSIS.md +77 -0
  7. package/DOCS/DEV_TRACK_PLAN_2026-01-07.md +42 -0
  8. package/DOCS/DEV_TRACK_SESSION_2026-01-31.md +220 -0
  9. package/DOCS/ENV_SETUP.md +189 -0
  10. package/DOCS/EXPORT_FORMATS.md +417 -0
  11. package/DOCS/GPU_DISPOSAL_GUIDE.md +21 -0
  12. package/DOCS/LICENSING_TIERS.md +275 -0
  13. package/DOCS/MASTER_PLAN_2026-01-31.md +570 -0
  14. package/DOCS/OBS_SETUP_GUIDE.md +98 -0
  15. package/DOCS/PROJECT_SETUP.md +66 -0
  16. package/DOCS/RENDERER_LIFECYCLE.md +40 -0
  17. package/DOCS/REPO_MANIFEST.md +121 -0
  18. package/DOCS/SESSION_014_PLAN.md +195 -0
  19. package/DOCS/SESSION_LOG_2026-01-07.md +56 -0
  20. package/DOCS/STRATEGIC_BLUEPRINT_2026-01-07.md +72 -0
  21. package/DOCS/SYSTEM_AUDIT_2026-01-30.md +738 -0
  22. package/DOCS/SYSTEM_INVENTORY.md +520 -0
  23. package/DOCS/TELEMETRY_EXPORTS.md +34 -0
  24. package/DOCS/WEBGPU_STATUS.md +38 -0
  25. package/DOCS/XR_BENCHMARKS.md +608 -0
  26. package/LICENSE +21 -0
  27. package/README.md +426 -0
  28. package/docs/.nojekyll +0 -0
  29. package/docs/01-dissolution_of_euclidean_hegemony.html +346 -0
  30. package/docs/02-hyperspatial_ego_death.html +346 -0
  31. package/docs/03-post_cartesian_sublime.html +346 -0
  32. package/docs/04-crystalline_void_meditation.html +346 -0
  33. package/docs/05-quantum_decoherence_ballet.html +346 -0
  34. package/docs/06-dissolution_of_euclidean_hegemony.html +346 -0
  35. package/docs/07-hyperspatial_ego_death.html +346 -0
  36. package/docs/08-post_cartesian_sublime.html +346 -0
  37. package/docs/09-crystalline_void_meditation.html +346 -0
  38. package/docs/10-quantum_decoherence_ballet.html +346 -0
  39. package/docs/11-dissolution_of_euclidean_hegemony.html +346 -0
  40. package/docs/12-hyperspatial_ego_death.html +346 -0
  41. package/docs/13-post_cartesian_sublime.html +346 -0
  42. package/docs/index.html +794 -0
  43. package/docs/test-hub.html +441 -0
  44. package/docs/url-state.js +102 -0
  45. package/docs/vib3-exports/01-quantum-quantum-tetrahedron-lattice.html +489 -0
  46. package/docs/vib3-exports/02-quantum-quantum-hypersphere-matrix.html +489 -0
  47. package/docs/vib3-exports/03-quantum-quantum-hypertetra-fractal.html +489 -0
  48. package/docs/vib3-exports/04-faceted-faceted-crystal-structure.html +407 -0
  49. package/docs/vib3-exports/05-faceted-faceted-klein-bottle.html +407 -0
  50. package/docs/vib3-exports/06-faceted-faceted-hypertetra-torus.html +407 -0
  51. package/docs/vib3-exports/07-holographic-holographic-wave-field.html +457 -0
  52. package/docs/vib3-exports/08-holographic-holographic-hypersphere-sphere.html +457 -0
  53. package/docs/vib3-exports/09-holographic-holographic-hypertetra-crystal.html +457 -0
  54. package/docs/vib3-exports/index.html +238 -0
  55. package/docs/webgpu-live.html +702 -0
  56. package/package.json +367 -0
  57. package/src/advanced/AIPresetGenerator.js +777 -0
  58. package/src/advanced/MIDIController.js +703 -0
  59. package/src/advanced/OffscreenWorker.js +1051 -0
  60. package/src/advanced/WebGPUCompute.js +1051 -0
  61. package/src/advanced/WebXRRenderer.js +680 -0
  62. package/src/agent/cli/AgentCLI.js +615 -0
  63. package/src/agent/cli/index.js +14 -0
  64. package/src/agent/index.js +73 -0
  65. package/src/agent/mcp/MCPServer.js +950 -0
  66. package/src/agent/mcp/index.js +9 -0
  67. package/src/agent/mcp/tools.js +548 -0
  68. package/src/agent/telemetry/EventStream.js +669 -0
  69. package/src/agent/telemetry/Instrumentation.js +618 -0
  70. package/src/agent/telemetry/TelemetryExporters.js +427 -0
  71. package/src/agent/telemetry/TelemetryService.js +464 -0
  72. package/src/agent/telemetry/index.js +52 -0
  73. package/src/benchmarks/BenchmarkRunner.js +381 -0
  74. package/src/benchmarks/MetricsCollector.js +299 -0
  75. package/src/benchmarks/index.js +9 -0
  76. package/src/benchmarks/scenes.js +259 -0
  77. package/src/cli/index.js +675 -0
  78. package/src/config/ApiConfig.js +88 -0
  79. package/src/core/CanvasManager.js +217 -0
  80. package/src/core/ErrorReporter.js +117 -0
  81. package/src/core/ParameterMapper.js +333 -0
  82. package/src/core/Parameters.js +396 -0
  83. package/src/core/RendererContracts.js +200 -0
  84. package/src/core/UnifiedResourceManager.js +370 -0
  85. package/src/core/VIB3Engine.js +636 -0
  86. package/src/core/renderers/FacetedRendererAdapter.js +32 -0
  87. package/src/core/renderers/HolographicRendererAdapter.js +29 -0
  88. package/src/core/renderers/QuantumRendererAdapter.js +29 -0
  89. package/src/core/renderers/RendererLifecycleManager.js +63 -0
  90. package/src/creative/ColorPresetsSystem.js +980 -0
  91. package/src/creative/ParameterTimeline.js +1061 -0
  92. package/src/creative/PostProcessingPipeline.js +1113 -0
  93. package/src/creative/TransitionAnimator.js +683 -0
  94. package/src/export/CSSExporter.js +226 -0
  95. package/src/export/CardGeneratorBase.js +279 -0
  96. package/src/export/ExportManager.js +580 -0
  97. package/src/export/FacetedCardGenerator.js +279 -0
  98. package/src/export/HolographicCardGenerator.js +543 -0
  99. package/src/export/LottieExporter.js +552 -0
  100. package/src/export/QuantumCardGenerator.js +315 -0
  101. package/src/export/SVGExporter.js +519 -0
  102. package/src/export/ShaderExporter.js +903 -0
  103. package/src/export/TradingCardGenerator.js +3055 -0
  104. package/src/export/TradingCardManager.js +181 -0
  105. package/src/export/VIB3PackageExporter.js +559 -0
  106. package/src/export/index.js +14 -0
  107. package/src/export/systems/TradingCardSystemFaceted.js +494 -0
  108. package/src/export/systems/TradingCardSystemHolographic.js +452 -0
  109. package/src/export/systems/TradingCardSystemQuantum.js +411 -0
  110. package/src/faceted/FacetedSystem.js +963 -0
  111. package/src/features/CollectionManager.js +433 -0
  112. package/src/gallery/CollectionManager.js +240 -0
  113. package/src/gallery/GallerySystem.js +485 -0
  114. package/src/geometry/GeometryFactory.js +314 -0
  115. package/src/geometry/GeometryLibrary.js +72 -0
  116. package/src/geometry/buffers/BufferBuilder.js +338 -0
  117. package/src/geometry/buffers/index.js +18 -0
  118. package/src/geometry/generators/Crystal.js +420 -0
  119. package/src/geometry/generators/Fractal.js +298 -0
  120. package/src/geometry/generators/KleinBottle.js +197 -0
  121. package/src/geometry/generators/Sphere.js +192 -0
  122. package/src/geometry/generators/Tesseract.js +160 -0
  123. package/src/geometry/generators/Tetrahedron.js +225 -0
  124. package/src/geometry/generators/Torus.js +304 -0
  125. package/src/geometry/generators/Wave.js +341 -0
  126. package/src/geometry/index.js +142 -0
  127. package/src/geometry/warp/HypersphereCore.js +211 -0
  128. package/src/geometry/warp/HypertetraCore.js +386 -0
  129. package/src/geometry/warp/index.js +57 -0
  130. package/src/holograms/HolographicVisualizer.js +1073 -0
  131. package/src/holograms/RealHolographicSystem.js +966 -0
  132. package/src/holograms/variantRegistry.js +69 -0
  133. package/src/integrations/FigmaPlugin.js +854 -0
  134. package/src/integrations/OBSMode.js +754 -0
  135. package/src/integrations/ThreeJsPackage.js +660 -0
  136. package/src/integrations/TouchDesignerExport.js +552 -0
  137. package/src/integrations/frameworks/Vib3React.js +591 -0
  138. package/src/integrations/frameworks/Vib3Svelte.js +654 -0
  139. package/src/integrations/frameworks/Vib3Vue.js +628 -0
  140. package/src/llm/LLMParameterInterface.js +240 -0
  141. package/src/llm/LLMParameterUI.js +577 -0
  142. package/src/math/Mat4x4.js +708 -0
  143. package/src/math/Projection.js +341 -0
  144. package/src/math/Rotor4D.js +637 -0
  145. package/src/math/Vec4.js +476 -0
  146. package/src/math/constants.js +164 -0
  147. package/src/math/index.js +68 -0
  148. package/src/math/projections.js +54 -0
  149. package/src/math/rotations.js +196 -0
  150. package/src/quantum/QuantumEngine.js +906 -0
  151. package/src/quantum/QuantumVisualizer.js +1103 -0
  152. package/src/reactivity/ReactivityConfig.js +499 -0
  153. package/src/reactivity/ReactivityManager.js +586 -0
  154. package/src/reactivity/SpatialInputSystem.js +1783 -0
  155. package/src/reactivity/index.js +93 -0
  156. package/src/render/CommandBuffer.js +465 -0
  157. package/src/render/MultiCanvasBridge.js +340 -0
  158. package/src/render/RenderCommand.js +514 -0
  159. package/src/render/RenderResourceRegistry.js +523 -0
  160. package/src/render/RenderState.js +552 -0
  161. package/src/render/RenderTarget.js +512 -0
  162. package/src/render/ShaderLoader.js +253 -0
  163. package/src/render/ShaderProgram.js +599 -0
  164. package/src/render/UnifiedRenderBridge.js +496 -0
  165. package/src/render/backends/WebGLBackend.js +1108 -0
  166. package/src/render/backends/WebGPUBackend.js +1409 -0
  167. package/src/render/commands/CommandBufferExecutor.js +607 -0
  168. package/src/render/commands/RenderCommandBuffer.js +661 -0
  169. package/src/render/commands/index.js +17 -0
  170. package/src/render/index.js +367 -0
  171. package/src/scene/Disposable.js +498 -0
  172. package/src/scene/MemoryPool.js +618 -0
  173. package/src/scene/Node4D.js +697 -0
  174. package/src/scene/ResourceManager.js +599 -0
  175. package/src/scene/Scene4D.js +540 -0
  176. package/src/scene/index.js +98 -0
  177. package/src/schemas/error.schema.json +84 -0
  178. package/src/schemas/extension.schema.json +88 -0
  179. package/src/schemas/index.js +214 -0
  180. package/src/schemas/parameters.schema.json +142 -0
  181. package/src/schemas/tool-pack.schema.json +44 -0
  182. package/src/schemas/tool-response.schema.json +127 -0
  183. package/src/shaders/common/fullscreen.vert.glsl +5 -0
  184. package/src/shaders/common/fullscreen.vert.wgsl +17 -0
  185. package/src/shaders/common/geometry24.glsl +65 -0
  186. package/src/shaders/common/geometry24.wgsl +54 -0
  187. package/src/shaders/common/rotation4d.glsl +85 -0
  188. package/src/shaders/common/rotation4d.wgsl +86 -0
  189. package/src/shaders/common/uniforms.glsl +44 -0
  190. package/src/shaders/common/uniforms.wgsl +48 -0
  191. package/src/shaders/faceted/faceted.frag.glsl +129 -0
  192. package/src/shaders/faceted/faceted.frag.wgsl +164 -0
  193. package/src/shaders/holographic/holographic.frag.glsl +406 -0
  194. package/src/shaders/holographic/holographic.frag.wgsl +185 -0
  195. package/src/shaders/quantum/quantum.frag.glsl +513 -0
  196. package/src/shaders/quantum/quantum.frag.wgsl +361 -0
  197. package/src/testing/ParallelTestFramework.js +519 -0
  198. package/src/testing/__snapshots__/exportFormats.test.js.snap +24 -0
  199. package/src/testing/exportFormats.test.js +8 -0
  200. package/src/testing/projections.test.js +14 -0
  201. package/src/testing/rotations.test.js +37 -0
  202. package/src/ui/InteractivityMenu.js +516 -0
  203. package/src/ui/StatusManager.js +96 -0
  204. package/src/ui/adaptive/renderers/webgpu/BufferLayout.ts +252 -0
  205. package/src/ui/adaptive/renderers/webgpu/PolytopeInstanceBuffer.ts +144 -0
  206. package/src/ui/adaptive/renderers/webgpu/TripleBufferedUniform.ts +170 -0
  207. package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +735 -0
  208. package/src/ui/adaptive/renderers/webgpu/index.ts +112 -0
  209. package/src/variations/VariationManager.js +431 -0
  210. package/src/viewer/AudioReactivity.js +505 -0
  211. package/src/viewer/CardBending.js +481 -0
  212. package/src/viewer/GalleryUI.js +832 -0
  213. package/src/viewer/ReactivityManager.js +590 -0
  214. package/src/viewer/TradingCardExporter.js +600 -0
  215. package/src/viewer/ViewerPortal.js +374 -0
  216. package/src/viewer/index.js +12 -0
  217. package/src/wasm/WasmLoader.js +296 -0
  218. package/src/wasm/index.js +132 -0
  219. package/tools/agentic/mcpTools.js +88 -0
  220. package/tools/cli/agent-cli.js +92 -0
  221. package/tools/export/formats.js +24 -0
  222. package/tools/math/rotation-baseline.mjs +64 -0
  223. package/tools/shader-sync-verify.js +937 -0
  224. package/tools/telemetry/manifestPipeline.js +141 -0
  225. package/tools/telemetry/telemetryEvents.js +35 -0
  226. package/types/adaptive-sdk.d.ts +185 -0
  227. package/types/advanced/AIPresetGenerator.d.ts +81 -0
  228. package/types/advanced/MIDIController.d.ts +100 -0
  229. package/types/advanced/OffscreenWorker.d.ts +82 -0
  230. package/types/advanced/WebGPUCompute.d.ts +52 -0
  231. package/types/advanced/WebXRRenderer.d.ts +77 -0
  232. package/types/advanced/index.d.ts +46 -0
  233. package/types/core/ErrorReporter.d.ts +50 -0
  234. package/types/core/VIB3Engine.d.ts +204 -0
  235. package/types/creative/ColorPresetsSystem.d.ts +91 -0
  236. package/types/creative/ParameterTimeline.d.ts +74 -0
  237. package/types/creative/PostProcessingPipeline.d.ts +109 -0
  238. package/types/creative/TransitionAnimator.d.ts +71 -0
  239. package/types/creative/index.d.ts +35 -0
  240. package/types/integrations/FigmaPlugin.d.ts +46 -0
  241. package/types/integrations/OBSMode.d.ts +74 -0
  242. package/types/integrations/ThreeJsPackage.d.ts +62 -0
  243. package/types/integrations/TouchDesignerExport.d.ts +36 -0
  244. package/types/integrations/Vib3React.d.ts +74 -0
  245. package/types/integrations/Vib3Svelte.d.ts +63 -0
  246. package/types/integrations/Vib3Vue.d.ts +55 -0
  247. package/types/integrations/index.d.ts +52 -0
  248. package/types/reactivity/SpatialInputSystem.d.ts +173 -0
  249. package/types/reactivity/index.d.ts +394 -0
  250. package/types/render/CommandBuffer.d.ts +169 -0
  251. package/types/render/RenderCommand.d.ts +312 -0
  252. package/types/render/RenderState.d.ts +279 -0
  253. package/types/render/RenderTarget.d.ts +254 -0
  254. package/types/render/ShaderProgram.d.ts +277 -0
  255. package/types/render/UnifiedRenderBridge.d.ts +143 -0
  256. package/types/render/WebGLBackend.d.ts +168 -0
  257. package/types/render/WebGPUBackend.d.ts +186 -0
  258. 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
+ }