From 806cb9655ff924e4c25bc29ce9ed96a17dada3d4 Mon Sep 17 00:00:00 2001 From: Artur Date: Tue, 24 Feb 2026 14:20:58 +0100 Subject: [PATCH] add 10 effects + firefox fix --- src/lib/effects/ASCIIEffect.js | 55 ++++++++++++++ src/lib/effects/ChromaticWarpEffect.js | 49 ++++++++++++ src/lib/effects/EdgeDetectionEffect.js | 66 +++++++++++++++++ src/lib/effects/FeedbackEffect.js | 53 +++++++++++++ src/lib/effects/FilmGrainEffect.js | 64 ++++++++++++++++ src/lib/effects/HeatDistortionEffect.js | 73 ++++++++++++++++++ src/lib/effects/HueShiftEffect.js | 63 ++++++++++++++++ src/lib/effects/ShockwaveEffect.js | 74 +++++++++++++++++++ src/lib/effects/VHSEffect.js | 66 +++++++++++++++++ src/lib/effects/VignetteEffect.js | 57 ++++++++++++++ src/lib/effects/index.js | 10 +++ src/lib/effects/passes/FeedbackPass.js | 45 +++++++++++ src/lib/shaders/ASCIIShader.js | 14 ++++ src/lib/shaders/ChromaticWarpShader.js | 12 +++ src/lib/shaders/EdgeDetectionShader.js | 15 ++++ src/lib/shaders/FeedbackShader.js | 13 ++++ src/lib/shaders/FilmGrainShader.js | 14 ++++ src/lib/shaders/HeatDistortionShader.js | 14 ++++ src/lib/shaders/HueShiftShader.js | 12 +++ src/lib/shaders/ShockwaveShader.js | 14 ++++ src/lib/shaders/VHSShader.js | 15 ++++ src/lib/shaders/VignetteShader.js | 13 ++++ src/lib/shaders/glsl/fragment/ascii.glsl | 61 +++++++++++++++ .../shaders/glsl/fragment/chromatic-warp.glsl | 34 +++++++++ .../shaders/glsl/fragment/edge-detection.glsl | 41 ++++++++++ src/lib/shaders/glsl/fragment/feedback.glsl | 17 +++++ src/lib/shaders/glsl/fragment/film-grain.glsl | 27 +++++++ .../glsl/fragment/heat-distortion.glsl | 23 ++++++ src/lib/shaders/glsl/fragment/hue-shift.glsl | 27 +++++++ src/lib/shaders/glsl/fragment/shockwave.glsl | 23 ++++++ src/lib/shaders/glsl/fragment/vhs.glsl | 50 +++++++++++++ src/lib/shaders/glsl/fragment/vignette.glsl | 21 ++++++ src/lib/view/components/window/StatusBar.jsx | 2 +- 33 files changed, 1136 insertions(+), 1 deletion(-) create mode 100644 src/lib/effects/ASCIIEffect.js create mode 100644 src/lib/effects/ChromaticWarpEffect.js create mode 100644 src/lib/effects/EdgeDetectionEffect.js create mode 100644 src/lib/effects/FeedbackEffect.js create mode 100644 src/lib/effects/FilmGrainEffect.js create mode 100644 src/lib/effects/HeatDistortionEffect.js create mode 100644 src/lib/effects/HueShiftEffect.js create mode 100644 src/lib/effects/ShockwaveEffect.js create mode 100644 src/lib/effects/VHSEffect.js create mode 100644 src/lib/effects/VignetteEffect.js create mode 100644 src/lib/effects/passes/FeedbackPass.js create mode 100644 src/lib/shaders/ASCIIShader.js create mode 100644 src/lib/shaders/ChromaticWarpShader.js create mode 100644 src/lib/shaders/EdgeDetectionShader.js create mode 100644 src/lib/shaders/FeedbackShader.js create mode 100644 src/lib/shaders/FilmGrainShader.js create mode 100644 src/lib/shaders/HeatDistortionShader.js create mode 100644 src/lib/shaders/HueShiftShader.js create mode 100644 src/lib/shaders/ShockwaveShader.js create mode 100644 src/lib/shaders/VHSShader.js create mode 100644 src/lib/shaders/VignetteShader.js create mode 100644 src/lib/shaders/glsl/fragment/ascii.glsl create mode 100644 src/lib/shaders/glsl/fragment/chromatic-warp.glsl create mode 100644 src/lib/shaders/glsl/fragment/edge-detection.glsl create mode 100644 src/lib/shaders/glsl/fragment/feedback.glsl create mode 100644 src/lib/shaders/glsl/fragment/film-grain.glsl create mode 100644 src/lib/shaders/glsl/fragment/heat-distortion.glsl create mode 100644 src/lib/shaders/glsl/fragment/hue-shift.glsl create mode 100644 src/lib/shaders/glsl/fragment/shockwave.glsl create mode 100644 src/lib/shaders/glsl/fragment/vhs.glsl create mode 100644 src/lib/shaders/glsl/fragment/vignette.glsl diff --git a/src/lib/effects/ASCIIEffect.js b/src/lib/effects/ASCIIEffect.js new file mode 100644 index 00000000..e0aa3cc9 --- /dev/null +++ b/src/lib/effects/ASCIIEffect.js @@ -0,0 +1,55 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import ASCIIShader from "@/lib/shaders/ASCIIShader"; + +export default class ASCIIEffect extends Effect { + static config = { + name: "ASCIIEffect", + description: "Converts image to ASCII art style using luminance patterns.", + type: "effect", + label: "ASCII", + defaultProperties: { + charSize: 8, + colored: true, + }, + controls: { + charSize: { + label: "Char Size", + type: "number", + min: 4, + max: 32, + step: 1, + withRange: true, + withReactor: true, + }, + colored: { + label: "Colored", + type: "toggle", + }, + }, + }; + + constructor(properties) { + super(ASCIIEffect, properties); + } + + updatePass() { + const { charSize, colored } = this.properties; + const { width, height } = this.scene.getSize(); + + this.pass.setUniforms({ + charSize, + colored: colored ? 1 : 0, + resolution: [width, height], + }); + } + + addToScene() { + this.pass = new ShaderPass(ASCIIShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } +} diff --git a/src/lib/effects/ChromaticWarpEffect.js b/src/lib/effects/ChromaticWarpEffect.js new file mode 100644 index 00000000..03897c56 --- /dev/null +++ b/src/lib/effects/ChromaticWarpEffect.js @@ -0,0 +1,49 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import ChromaticWarpShader from "@/lib/shaders/ChromaticWarpShader"; + +export default class ChromaticWarpEffect extends Effect { + static config = { + name: "ChromaticWarpEffect", + description: "Barrel lens distortion with chromatic aberration.", + type: "effect", + label: "Chromatic Warp", + defaultProperties: { + warp: 0.3, + chromatic: 0.5, + }, + controls: { + warp: { + label: "Warp", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + chromatic: { + label: "Chromatic", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + }, + }; + + constructor(properties) { + super(ChromaticWarpEffect, properties); + } + + addToScene() { + this.pass = new ShaderPass(ChromaticWarpShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } +} diff --git a/src/lib/effects/EdgeDetectionEffect.js b/src/lib/effects/EdgeDetectionEffect.js new file mode 100644 index 00000000..a5237313 --- /dev/null +++ b/src/lib/effects/EdgeDetectionEffect.js @@ -0,0 +1,66 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import EdgeDetectionShader from "@/lib/shaders/EdgeDetectionShader"; + +export default class EdgeDetectionEffect extends Effect { + static config = { + name: "EdgeDetectionEffect", + description: "Sobel edge detection. Outline or neon glow mode.", + type: "effect", + label: "Edge Detection", + defaultProperties: { + thickness: 1.0, + neon: false, + color: "#ffffff", + }, + controls: { + thickness: { + label: "Thickness", + type: "number", + min: 0.5, + max: 5, + step: 0.1, + withRange: true, + withReactor: true, + }, + neon: { + label: "Neon Mode", + type: "toggle", + }, + color: { + label: "Edge Color", + type: "color", + }, + }, + }; + + constructor(properties) { + super(EdgeDetectionEffect, properties); + } + + updatePass() { + const { thickness, neon, color } = this.properties; + const { width, height } = this.scene.getSize(); + + // Parse hex color to RGB 0-1 + const r = parseInt(color.slice(1, 3), 16) / 255; + const g = parseInt(color.slice(3, 5), 16) / 255; + const b = parseInt(color.slice(5, 7), 16) / 255; + + this.pass.setUniforms({ + thickness, + neon: neon ? 1 : 0, + edgeColor: [r, g, b], + resolution: [width, height], + }); + } + + addToScene() { + this.pass = new ShaderPass(EdgeDetectionShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } +} diff --git a/src/lib/effects/FeedbackEffect.js b/src/lib/effects/FeedbackEffect.js new file mode 100644 index 00000000..b453e588 --- /dev/null +++ b/src/lib/effects/FeedbackEffect.js @@ -0,0 +1,53 @@ +import Effect from "@/lib/core/Effect"; +import FeedbackPass from "@/lib/effects/passes/FeedbackPass"; + +export default class FeedbackEffect extends Effect { + static config = { + name: "FeedbackEffect", + description: "Frame feedback echo that accumulates previous frames with decay.", + type: "effect", + label: "Feedback Echo", + defaultProperties: { + decay: 0.85, + zoom: 1.0, + }, + controls: { + decay: { + label: "Decay", + type: "number", + min: 0, + max: 0.99, + step: 0.01, + withRange: true, + withReactor: true, + }, + zoom: { + label: "Zoom", + type: "number", + min: 1.0, + max: 1.1, + step: 0.001, + withRange: true, + withReactor: true, + }, + }, + }; + + constructor(properties) { + super(FeedbackEffect, properties); + } + + updatePass() { + const { decay, zoom } = this.properties; + this.pass.setUniforms({ decay, zoom }); + } + + addToScene() { + this.pass = new FeedbackPass(); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } +} diff --git a/src/lib/effects/FilmGrainEffect.js b/src/lib/effects/FilmGrainEffect.js new file mode 100644 index 00000000..75e64ffd --- /dev/null +++ b/src/lib/effects/FilmGrainEffect.js @@ -0,0 +1,64 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import FilmGrainShader from "@/lib/shaders/FilmGrainShader"; + +export default class FilmGrainEffect extends Effect { + static config = { + name: "FilmGrainEffect", + description: "Film grain noise overlay.", + type: "effect", + label: "Film Grain", + defaultProperties: { + intensity: 0.3, + size: 512, + colored: false, + }, + controls: { + intensity: { + label: "Intensity", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + size: { + label: "Grain Size", + type: "number", + min: 50, + max: 2000, + step: 10, + withRange: true, + }, + colored: { + label: "Colored", + type: "toggle", + }, + }, + }; + + constructor(properties) { + super(FilmGrainEffect, properties); + this.time = 0; + } + + updatePass() { + const { intensity, size, colored } = this.properties; + this.pass.setUniforms({ intensity, size, colored: colored ? 1 : 0 }); + } + + addToScene() { + this.pass = new ShaderPass(FilmGrainShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } + + render(scene, data) { + this.time += data.delta * 0.001; + this.pass.setUniforms({ time: this.time }); + } +} diff --git a/src/lib/effects/HeatDistortionEffect.js b/src/lib/effects/HeatDistortionEffect.js new file mode 100644 index 00000000..865f4ed0 --- /dev/null +++ b/src/lib/effects/HeatDistortionEffect.js @@ -0,0 +1,73 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import HeatDistortionShader from "@/lib/shaders/HeatDistortionShader"; + +export default class HeatDistortionEffect extends Effect { + static config = { + name: "HeatDistortionEffect", + description: "Heat shimmer distortion using simplex noise.", + type: "effect", + label: "Heat Distortion", + defaultProperties: { + intensity: 0.5, + scale: 3.0, + speed: 0.5, + }, + controls: { + intensity: { + label: "Intensity", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + scale: { + label: "Scale", + type: "number", + min: 1, + max: 10, + step: 0.1, + withRange: true, + }, + speed: { + label: "Speed", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + }, + }; + + constructor(properties) { + super(HeatDistortionEffect, properties); + this.time = 0; + } + + updatePass() { + const { intensity, scale, speed } = this.properties; + this.pass.setUniforms({ intensity, scale, speed }); + } + + addToScene() { + this.pass = new ShaderPass(HeatDistortionShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } + + render(scene, data) { + if (!data.hasUpdate) return; + + const { speed } = this.properties; + this.time += data.delta / (1000 / Math.max(speed, 0.01)); + + this.pass.setUniforms({ time: this.time }); + } +} diff --git a/src/lib/effects/HueShiftEffect.js b/src/lib/effects/HueShiftEffect.js new file mode 100644 index 00000000..0b83cd00 --- /dev/null +++ b/src/lib/effects/HueShiftEffect.js @@ -0,0 +1,63 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import HueShiftShader from "@/lib/shaders/HueShiftShader"; + +export default class HueShiftEffect extends Effect { + static config = { + name: "HueShiftEffect", + description: "Continuously rotates the hue of the image over time.", + type: "effect", + label: "Hue Shift", + defaultProperties: { + speed: 0.3, + saturation: 1.0, + }, + controls: { + speed: { + label: "Speed", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + saturation: { + label: "Saturation", + type: "number", + min: 0, + max: 2.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + }, + }; + + constructor(properties) { + super(HueShiftEffect, properties); + this.hue = 0; + } + + updatePass() { + this.pass.setUniforms({ saturation: this.properties.saturation }); + } + + addToScene() { + this.pass = new ShaderPass(HueShiftShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } + + render(scene, data) { + if (!data.hasUpdate) return; + + const { speed } = this.properties; + this.hue = (this.hue + (data.delta / 5000) * speed) % 1.0; + + this.pass.setUniforms({ hue: this.hue }); + } +} diff --git a/src/lib/effects/ShockwaveEffect.js b/src/lib/effects/ShockwaveEffect.js new file mode 100644 index 00000000..d807af61 --- /dev/null +++ b/src/lib/effects/ShockwaveEffect.js @@ -0,0 +1,74 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import ShockwaveShader from "@/lib/shaders/ShockwaveShader"; + +export default class ShockwaveEffect extends Effect { + static config = { + name: "ShockwaveEffect", + description: "Radial shockwave distortion emanating from center.", + type: "effect", + label: "Shockwave", + defaultProperties: { + amplitude: 0.5, + frequency: 5.0, + speed: 0.5, + }, + controls: { + amplitude: { + label: "Amplitude", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + frequency: { + label: "Frequency", + type: "number", + min: 1, + max: 20, + step: 0.5, + withRange: true, + withReactor: true, + }, + speed: { + label: "Speed", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + }, + }; + + constructor(properties) { + super(ShockwaveEffect, properties); + this.time = 0; + } + + updatePass() { + const { amplitude, frequency, speed } = this.properties; + this.pass.setUniforms({ amplitude, frequency, speed }); + } + + addToScene() { + this.pass = new ShaderPass(ShockwaveShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } + + render(scene, data) { + if (!data.hasUpdate) return; + + const { speed } = this.properties; + this.time += data.delta / (1000 / Math.max(speed, 0.01)); + + this.pass.setUniforms({ time: this.time }); + } +} diff --git a/src/lib/effects/VHSEffect.js b/src/lib/effects/VHSEffect.js new file mode 100644 index 00000000..999d83c8 --- /dev/null +++ b/src/lib/effects/VHSEffect.js @@ -0,0 +1,66 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import VHSShader from "@/lib/shaders/VHSShader"; + +export default class VHSEffect extends Effect { + static config = { + name: "VHSEffect", + description: "VHS tape distortion with scanlines, noise and color bleeding.", + type: "effect", + label: "VHS", + defaultProperties: { + intensity: 0.5, + speed: 0.5, + }, + controls: { + intensity: { + label: "Intensity", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + speed: { + label: "Speed", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + }, + }; + + constructor(properties) { + super(VHSEffect, properties); + this.time = 0; + } + + updatePass() { + const { intensity, speed } = this.properties; + const { width, height } = this.scene.getSize(); + + this.pass.setUniforms({ intensity, speed, resolution: [width, height] }); + } + + addToScene() { + this.pass = new ShaderPass(VHSShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } + + render(scene, data) { + if (!data.hasUpdate) return; + + const { speed } = this.properties; + this.time += data.delta / (1000 / Math.max(speed, 0.01)); + + this.pass.setUniforms({ time: this.time }); + } +} diff --git a/src/lib/effects/VignetteEffect.js b/src/lib/effects/VignetteEffect.js new file mode 100644 index 00000000..f1cfaa0e --- /dev/null +++ b/src/lib/effects/VignetteEffect.js @@ -0,0 +1,57 @@ +import Effect from "@/lib/core/Effect"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import VignetteShader from "@/lib/shaders/VignetteShader"; + +export default class VignetteEffect extends Effect { + static config = { + name: "VignetteEffect", + description: "Darkens the edges of the frame.", + type: "effect", + label: "Vignette", + defaultProperties: { + intensity: 1.0, + radius: 0.75, + softness: 0.45, + }, + controls: { + intensity: { + label: "Intensity", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + withReactor: true, + }, + radius: { + label: "Radius", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + }, + softness: { + label: "Softness", + type: "number", + min: 0, + max: 1.0, + step: 0.01, + withRange: true, + }, + }, + }; + + constructor(properties) { + super(VignetteEffect, properties); + } + + addToScene() { + this.pass = new ShaderPass(VignetteShader); + this.updatePass(); + } + + removeFromScene() { + this.pass = null; + } +} diff --git a/src/lib/effects/index.js b/src/lib/effects/index.js index 6ad7ba74..2e31aec0 100644 --- a/src/lib/effects/index.js +++ b/src/lib/effects/index.js @@ -10,3 +10,13 @@ export { default as LEDEffect } from "./LEDEffect"; export { default as MirrorEffect } from "./MirrorEffect"; export { default as PixelateEffect } from "./PixelateEffect"; export { default as RGBShiftEffect } from "./RGBShiftEffect"; +export { default as ASCIIEffect } from "./ASCIIEffect"; +export { default as ChromaticWarpEffect } from "./ChromaticWarpEffect"; +export { default as EdgeDetectionEffect } from "./EdgeDetectionEffect"; +export { default as FeedbackEffect } from "./FeedbackEffect"; +export { default as FilmGrainEffect } from "./FilmGrainEffect"; +export { default as HeatDistortionEffect } from "./HeatDistortionEffect"; +export { default as HueShiftEffect } from "./HueShiftEffect"; +export { default as ShockwaveEffect } from "./ShockwaveEffect"; +export { default as VHSEffect } from "./VHSEffect"; +export { default as VignetteEffect } from "./VignetteEffect"; diff --git a/src/lib/effects/passes/FeedbackPass.js b/src/lib/effects/passes/FeedbackPass.js new file mode 100644 index 00000000..b9c514af --- /dev/null +++ b/src/lib/effects/passes/FeedbackPass.js @@ -0,0 +1,45 @@ +import Pass from "@/lib/graphics/Pass"; +import ShaderPass from "@/lib/graphics/ShaderPass"; +import CopyShader from "@/lib/shaders/CopyShader"; +import FeedbackShader from "@/lib/shaders/FeedbackShader"; +import { createRenderTarget } from "@/lib/graphics/common"; + +export default class FeedbackPass extends Pass { + constructor() { + super(); + + this.needsSwap = true; + this.feedbackBuffer = null; + + this.blendPass = new ShaderPass(FeedbackShader); + this.copyPass = new ShaderPass(CopyShader); + this.copyPass.needsSwap = false; + } + + init() { + this.feedbackBuffer = createRenderTarget(); + } + + setUniforms({ decay, zoom }) { + this.blendPass.setUniforms({ decay, zoom }); + } + + setSize(width, height) { + if (this.feedbackBuffer) { + this.feedbackBuffer.setSize(width, height); + } + } + + render(renderer, inputBuffer, outputBuffer) { + if (!this.feedbackBuffer) { + this.init(); + } + + // Blend current frame with accumulated feedback → outputBuffer + this.blendPass.setUniforms({ feedbackTexture: this.feedbackBuffer.texture }); + this.blendPass.render(renderer, inputBuffer, outputBuffer); + + // Copy outputBuffer into feedbackBuffer for next frame + this.copyPass.render(renderer, outputBuffer, this.feedbackBuffer); + } +} diff --git a/src/lib/shaders/ASCIIShader.js b/src/lib/shaders/ASCIIShader.js new file mode 100644 index 00000000..d00674e2 --- /dev/null +++ b/src/lib/shaders/ASCIIShader.js @@ -0,0 +1,14 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/ascii.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; +import { Vector2 } from "three"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + resolution: { type: "v2", value: new Vector2(1, 1) }, + charSize: { type: "f", value: 8.0 }, + colored: { type: "i", value: 1 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/ChromaticWarpShader.js b/src/lib/shaders/ChromaticWarpShader.js new file mode 100644 index 00000000..aa72da47 --- /dev/null +++ b/src/lib/shaders/ChromaticWarpShader.js @@ -0,0 +1,12 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/chromatic-warp.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + warp: { type: "f", value: 0.3 }, + chromatic: { type: "f", value: 0.5 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/EdgeDetectionShader.js b/src/lib/shaders/EdgeDetectionShader.js new file mode 100644 index 00000000..6923ca86 --- /dev/null +++ b/src/lib/shaders/EdgeDetectionShader.js @@ -0,0 +1,15 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/edge-detection.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; +import { Color, Vector2, Vector3 } from "three"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + resolution: { type: "v2", value: new Vector2(1, 1) }, + thickness: { type: "f", value: 1.0 }, + neon: { type: "i", value: 0 }, + edgeColor: { type: "v3", value: new Vector3(1, 1, 1) }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/FeedbackShader.js b/src/lib/shaders/FeedbackShader.js new file mode 100644 index 00000000..d1a0905d --- /dev/null +++ b/src/lib/shaders/FeedbackShader.js @@ -0,0 +1,13 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/feedback.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + feedbackTexture: { type: "t", value: null }, + decay: { type: "f", value: 0.85 }, + zoom: { type: "f", value: 1.0 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/FilmGrainShader.js b/src/lib/shaders/FilmGrainShader.js new file mode 100644 index 00000000..1421dd5f --- /dev/null +++ b/src/lib/shaders/FilmGrainShader.js @@ -0,0 +1,14 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/film-grain.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + time: { type: "f", value: 0.0 }, + intensity: { type: "f", value: 0.5 }, + size: { type: "f", value: 512.0 }, + colored: { type: "i", value: 0 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/HeatDistortionShader.js b/src/lib/shaders/HeatDistortionShader.js new file mode 100644 index 00000000..74607399 --- /dev/null +++ b/src/lib/shaders/HeatDistortionShader.js @@ -0,0 +1,14 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/heat-distortion.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + time: { type: "f", value: 0.0 }, + intensity: { type: "f", value: 0.5 }, + scale: { type: "f", value: 3.0 }, + speed: { type: "f", value: 0.5 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/HueShiftShader.js b/src/lib/shaders/HueShiftShader.js new file mode 100644 index 00000000..3060441b --- /dev/null +++ b/src/lib/shaders/HueShiftShader.js @@ -0,0 +1,12 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/hue-shift.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + hue: { type: "f", value: 0.0 }, + saturation: { type: "f", value: 1.0 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/ShockwaveShader.js b/src/lib/shaders/ShockwaveShader.js new file mode 100644 index 00000000..86b9b5a0 --- /dev/null +++ b/src/lib/shaders/ShockwaveShader.js @@ -0,0 +1,14 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/shockwave.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + time: { type: "f", value: 0.0 }, + amplitude: { type: "f", value: 0.5 }, + frequency: { type: "f", value: 5.0 }, + speed: { type: "f", value: 0.5 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/VHSShader.js b/src/lib/shaders/VHSShader.js new file mode 100644 index 00000000..3739e5a1 --- /dev/null +++ b/src/lib/shaders/VHSShader.js @@ -0,0 +1,15 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/vhs.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; +import { Vector2 } from "three"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + time: { type: "f", value: 0.0 }, + intensity: { type: "f", value: 0.5 }, + speed: { type: "f", value: 0.5 }, + resolution: { type: "v2", value: new Vector2(1, 1) }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/VignetteShader.js b/src/lib/shaders/VignetteShader.js new file mode 100644 index 00000000..9aa21d10 --- /dev/null +++ b/src/lib/shaders/VignetteShader.js @@ -0,0 +1,13 @@ +import fragmentShader from "@/lib/shaders/glsl/fragment/vignette.glsl"; +import vertexShader from "@/lib/shaders/glsl/vertex/basic.glsl"; + +export default { + uniforms: { + inputTexture: { type: "t", value: null }, + intensity: { type: "f", value: 1.0 }, + radius: { type: "f", value: 0.75 }, + softness: { type: "f", value: 0.45 }, + }, + vertexShader, + fragmentShader, +}; diff --git a/src/lib/shaders/glsl/fragment/ascii.glsl b/src/lib/shaders/glsl/fragment/ascii.glsl new file mode 100644 index 00000000..711fbd4f --- /dev/null +++ b/src/lib/shaders/glsl/fragment/ascii.glsl @@ -0,0 +1,61 @@ +uniform sampler2D inputTexture; +uniform vec2 resolution; +uniform float charSize; +uniform int colored; +varying vec2 vUv; + +float getLuminance(vec3 c) { + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +// Returns fill pattern intensity for a given density level (0-1) +// at local UV within the character cell (0-1) +float charPattern(vec2 p, float level) { + // Level 0.00-0.20: empty (space) + if (level < 0.20) return 0.0; + + // Level 0.20-0.40: dots at center + if (level < 0.40) { + float d = length(p - 0.5); + return 1.0 - step(0.15, d); + } + + // Level 0.40-0.60: cross / plus sign + if (level < 0.60) { + float h = 1.0 - step(0.1, abs(p.y - 0.5)); + float v = 1.0 - step(0.1, abs(p.x - 0.5)); + return max(h, v); + } + + // Level 0.60-0.80: grid (#-like) + if (level < 0.80) { + float h = step(0.45, fract(p.y * 2.0)); + float v = step(0.45, fract(p.x * 2.0)); + return max(h, v); + } + + // Level 0.80-1.00: solid fill + return 1.0; +} + +void main() { + vec2 blockSize = vec2(charSize) / resolution; + + // UV of the top-left corner of the current character block + vec2 blockOrigin = floor(vUv / blockSize) * blockSize; + + // Sample from center of block to get representative color + vec4 blockColor = texture2D(inputTexture, blockOrigin + blockSize * 0.5); + float lum = getLuminance(blockColor.rgb); + + // Local position within the block (0..1) + vec2 localUV = fract(vUv / blockSize); + + float pattern = charPattern(localUV, lum); + + if (colored > 0) { + gl_FragColor = vec4(blockColor.rgb * pattern, blockColor.a); + } else { + gl_FragColor = vec4(vec3(pattern), blockColor.a); + } +} diff --git a/src/lib/shaders/glsl/fragment/chromatic-warp.glsl b/src/lib/shaders/glsl/fragment/chromatic-warp.glsl new file mode 100644 index 00000000..de6517b8 --- /dev/null +++ b/src/lib/shaders/glsl/fragment/chromatic-warp.glsl @@ -0,0 +1,34 @@ +uniform sampler2D inputTexture; +uniform float warp; +uniform float chromatic; +varying vec2 vUv; + +vec2 barrelDistort(vec2 uv, float k) { + vec2 cc = uv - 0.5; + float dist = dot(cc, cc); + return uv + cc * dist * k; +} + +void main() { + vec2 uv = vUv; + + float k = warp * 0.8; + float ca = chromatic * 0.015; + + // Sample each channel with a slightly different warp strength + vec2 uvR = barrelDistort(uv, k + ca); + vec2 uvG = barrelDistort(uv, k); + vec2 uvB = barrelDistort(uv, k - ca); + + // Clamp to avoid sampling outside bounds + uvR = clamp(uvR, 0.0, 1.0); + uvG = clamp(uvG, 0.0, 1.0); + uvB = clamp(uvB, 0.0, 1.0); + + float r = texture2D(inputTexture, uvR).r; + float g = texture2D(inputTexture, uvG).g; + float b = texture2D(inputTexture, uvB).b; + float a = texture2D(inputTexture, uvG).a; + + gl_FragColor = vec4(r, g, b, a); +} diff --git a/src/lib/shaders/glsl/fragment/edge-detection.glsl b/src/lib/shaders/glsl/fragment/edge-detection.glsl new file mode 100644 index 00000000..38cc7703 --- /dev/null +++ b/src/lib/shaders/glsl/fragment/edge-detection.glsl @@ -0,0 +1,41 @@ +uniform sampler2D inputTexture; +uniform vec2 resolution; +uniform float thickness; +uniform int neon; +uniform vec3 edgeColor; +varying vec2 vUv; + +float getLuminance(vec3 c) { + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +void main() { + vec2 texel = thickness / resolution; + + // Sobel kernel samples + vec3 tl = texture2D(inputTexture, vUv + vec2(-texel.x, texel.y)).rgb; + vec3 tc = texture2D(inputTexture, vUv + vec2( 0.0, texel.y)).rgb; + vec3 tr = texture2D(inputTexture, vUv + vec2( texel.x, texel.y)).rgb; + vec3 ml = texture2D(inputTexture, vUv + vec2(-texel.x, 0.0 )).rgb; + vec3 mr = texture2D(inputTexture, vUv + vec2( texel.x, 0.0 )).rgb; + vec3 bl = texture2D(inputTexture, vUv + vec2(-texel.x, -texel.y)).rgb; + vec3 bc = texture2D(inputTexture, vUv + vec2( 0.0, -texel.y)).rgb; + vec3 br = texture2D(inputTexture, vUv + vec2( texel.x, -texel.y)).rgb; + + // Sobel in X and Y directions + vec3 sobelX = -tl - 2.0*ml - bl + tr + 2.0*mr + br; + vec3 sobelY = -tl - 2.0*tc - tr + bl + 2.0*bc + br; + float edge = length(vec2(getLuminance(sobelX), getLuminance(sobelY))); + edge = clamp(edge, 0.0, 1.0); + + vec4 original = texture2D(inputTexture, vUv); + + if (neon > 0) { + // Neon: edges glow on black background + gl_FragColor = vec4(edgeColor * edge, 1.0); + } else { + // Outline: draw edges over original image + vec3 col = mix(original.rgb, edgeColor, edge); + gl_FragColor = vec4(col, original.a); + } +} diff --git a/src/lib/shaders/glsl/fragment/feedback.glsl b/src/lib/shaders/glsl/fragment/feedback.glsl new file mode 100644 index 00000000..4c152a97 --- /dev/null +++ b/src/lib/shaders/glsl/fragment/feedback.glsl @@ -0,0 +1,17 @@ +uniform sampler2D inputTexture; +uniform sampler2D feedbackTexture; +uniform float decay; +uniform float zoom; +varying vec2 vUv; + +void main() { + vec4 current = texture2D(inputTexture, vUv); + + // Sample previous frame with slight zoom for psychedelic tunnel effect + vec2 feedbackUV = (vUv - 0.5) / zoom + 0.5; + feedbackUV = clamp(feedbackUV, 0.0, 1.0); + vec4 feedback = texture2D(feedbackTexture, feedbackUV) * decay; + + // Additive blend: current frame always visible, echoes accumulate + gl_FragColor = clamp(current + feedback * (1.0 - current.a * 0.5), 0.0, 1.0); +} diff --git a/src/lib/shaders/glsl/fragment/film-grain.glsl b/src/lib/shaders/glsl/fragment/film-grain.glsl new file mode 100644 index 00000000..27d77e58 --- /dev/null +++ b/src/lib/shaders/glsl/fragment/film-grain.glsl @@ -0,0 +1,27 @@ +uniform sampler2D inputTexture; +uniform float time; +uniform float intensity; +uniform float size; +uniform int colored; +varying vec2 vUv; + +#include "../func/random.glsl" + +void main() { + vec4 color = texture2D(inputTexture, vUv); + + // Quantize UVs to grain cell size + vec2 grainUV = floor(vUv * size) / size; + + if (colored > 0) { + float r = random(grainUV + vec2(time * 1.3, 0.0)); + float g = random(grainUV + vec2(0.0, time * 1.7)); + float b = random(grainUV + vec2(time * 0.9, time * 1.1)); + color.rgb += (vec3(r, g, b) - 0.5) * intensity; + } else { + float grain = random(grainUV + vec2(time)); + color.rgb += (grain - 0.5) * intensity; + } + + gl_FragColor = clamp(color, 0.0, 1.0); +} diff --git a/src/lib/shaders/glsl/fragment/heat-distortion.glsl b/src/lib/shaders/glsl/fragment/heat-distortion.glsl new file mode 100644 index 00000000..c96c3040 --- /dev/null +++ b/src/lib/shaders/glsl/fragment/heat-distortion.glsl @@ -0,0 +1,23 @@ +uniform sampler2D inputTexture; +uniform float time; +uniform float intensity; +uniform float scale; +uniform float speed; +varying vec2 vUv; + +#include "../func/simplex-noise-2d.glsl" + +void main() { + vec2 uv = vUv; + + // Two layers of simplex noise for organic look + float n1 = snoise(uv * scale + vec2(time * speed * 0.7, 0.0)); + float n2 = snoise(uv * scale * 2.3 + vec2(0.0, time * speed * 0.5)); + + // Combine and apply as UV offset + vec2 offset = vec2(n1, n2) * intensity * 0.04; + uv += offset; + uv = clamp(uv, 0.0, 1.0); + + gl_FragColor = texture2D(inputTexture, uv); +} diff --git a/src/lib/shaders/glsl/fragment/hue-shift.glsl b/src/lib/shaders/glsl/fragment/hue-shift.glsl new file mode 100644 index 00000000..60b54eb9 --- /dev/null +++ b/src/lib/shaders/glsl/fragment/hue-shift.glsl @@ -0,0 +1,27 @@ +uniform sampler2D inputTexture; +uniform float hue; +uniform float saturation; +varying vec2 vUv; + +vec3 rgb2hsv(vec3 c) { + vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0*d + e)), d / (q.x + e), q.x); +} + +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() { + vec4 color = texture2D(inputTexture, vUv); + vec3 hsv = rgb2hsv(color.rgb); + hsv.x = fract(hsv.x + hue); + hsv.y = clamp(hsv.y * saturation, 0.0, 1.0); + gl_FragColor = vec4(hsv2rgb(hsv), color.a); +} diff --git a/src/lib/shaders/glsl/fragment/shockwave.glsl b/src/lib/shaders/glsl/fragment/shockwave.glsl new file mode 100644 index 00000000..ce27069e --- /dev/null +++ b/src/lib/shaders/glsl/fragment/shockwave.glsl @@ -0,0 +1,23 @@ +uniform sampler2D inputTexture; +uniform float time; +uniform float amplitude; +uniform float frequency; +uniform float speed; +varying vec2 vUv; + +void main() { + vec2 uv = vUv; + vec2 center = vec2(0.5, 0.5); + vec2 delta = uv - center; + float dist = length(delta); + + // Radial sine wave emanating from center + float wave = sin(dist * frequency * 20.0 - time * speed * 8.0); + float attenuation = max(0.0, 1.0 - dist * 2.0); + + vec2 offset = normalize(delta + vec2(0.0001)) * wave * amplitude * 0.05 * attenuation; + uv += offset; + uv = clamp(uv, 0.0, 1.0); + + gl_FragColor = texture2D(inputTexture, uv); +} diff --git a/src/lib/shaders/glsl/fragment/vhs.glsl b/src/lib/shaders/glsl/fragment/vhs.glsl new file mode 100644 index 00000000..54ea715c --- /dev/null +++ b/src/lib/shaders/glsl/fragment/vhs.glsl @@ -0,0 +1,50 @@ +uniform sampler2D inputTexture; +uniform float time; +uniform float intensity; +uniform float speed; +uniform vec2 resolution; +varying vec2 vUv; + +#include "../func/random.glsl" + +void main() { + vec2 uv = vUv; + + // Horizontal line jitter: each scanline row randomly shifts on X + float lineY = floor(uv.y * resolution.y); + float jitter = random(vec2(lineY, floor(time * 10.0))) * 2.0 - 1.0; + uv.x += jitter * intensity * 0.04; + + // Occasional full-frame horizontal roll + float roll = random(vec2(floor(time * 3.0), 0.5)); + if (roll > 0.92) { + float rollAmt = random(vec2(floor(time * 5.0), 1.0)) * intensity * 0.15; + uv.y = fract(uv.y + rollAmt); + } + + // Clamp UVs + uv = clamp(uv, 0.0, 1.0); + + // Sample image with distorted UVs + vec4 color = texture2D(inputTexture, uv); + + // Color bleeding: red channel slightly offset to the left + float bleedAmt = intensity * 0.008; + vec4 colorLeft = texture2D(inputTexture, clamp(uv - vec2(bleedAmt, 0.0), 0.0, 1.0)); + color.r = mix(color.r, colorLeft.r, intensity * 0.6); + + // Scanlines + float scanline = sin(uv.y * resolution.y * 3.14159) * 0.5 + 0.5; + scanline = pow(scanline, 0.3); + color.rgb *= mix(1.0, scanline, intensity * 0.35); + + // Random noise + float noise = random(uv + vec2(time * 7.3, time * 3.1)) - 0.5; + color.rgb += noise * intensity * 0.12; + + // Slight desaturation (tape degradation) + float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114)); + color.rgb = mix(color.rgb, vec3(lum), intensity * 0.2); + + gl_FragColor = clamp(color, 0.0, 1.0); +} diff --git a/src/lib/shaders/glsl/fragment/vignette.glsl b/src/lib/shaders/glsl/fragment/vignette.glsl new file mode 100644 index 00000000..e961de69 --- /dev/null +++ b/src/lib/shaders/glsl/fragment/vignette.glsl @@ -0,0 +1,21 @@ +uniform sampler2D inputTexture; +uniform float intensity; +uniform float radius; +uniform float softness; +varying vec2 vUv; + +void main() { + vec4 color = texture2D(inputTexture, vUv); + + // Distance from center + vec2 uv = vUv - 0.5; + float dist = length(uv); + + // Smooth vignette + float vignette = smoothstep(radius, radius - softness, dist); + + // Lerp between darkened and original based on intensity + color.rgb *= mix(1.0, vignette, intensity); + + gl_FragColor = color; +} diff --git a/src/lib/view/components/window/StatusBar.jsx b/src/lib/view/components/window/StatusBar.jsx index 204f1af8..f2393e75 100644 --- a/src/lib/view/components/window/StatusBar.jsx +++ b/src/lib/view/components/window/StatusBar.jsx @@ -14,7 +14,7 @@ export default function StatusBar() { function updateStats() { setState({ fps: `${renderer.getFPS()} FPS`, - mem: formatSize(window.performance.memory.usedJSHeapSize, 2), + mem: window.performance.memory ? formatSize(window.performance.memory.usedJSHeapSize, 2) : undefined, }); }