@brochington/shader-backgrounds 0.1.0

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/index.html ADDED
@@ -0,0 +1,194 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Shader Library Demo</title>
8
+ <style>
9
+ body,
10
+ html {
11
+ margin: 0;
12
+ padding: 0;
13
+ width: 100%;
14
+ height: 100%;
15
+ }
16
+
17
+ .container {
18
+ width: 100vw;
19
+ height: 100vh;
20
+ }
21
+
22
+ .overlay {
23
+ width: 400px;
24
+ margin: 40px;
25
+ background: white;
26
+ padding: 20px;
27
+ font-family: sans-serif;
28
+ border-radius: 8px;
29
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
30
+ }
31
+
32
+ .overlay-header {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ gap: 10px;
37
+ }
38
+
39
+ .overlay-header h1 {
40
+ margin: 0;
41
+ font-size: 22px;
42
+ line-height: 1.2;
43
+ }
44
+
45
+ .overlay.collapsed {
46
+ width: 400px;
47
+ padding: 20px;
48
+ background: rgba(255, 255, 255, 0.92);
49
+ backdrop-filter: blur(8px);
50
+ -webkit-backdrop-filter: blur(8px);
51
+ }
52
+
53
+ .overlay.collapsed .overlay-content {
54
+ display: none;
55
+ }
56
+
57
+ .overlay-toggle {
58
+ padding: 6px 10px;
59
+ font-size: 13px;
60
+ border-radius: 999px;
61
+ white-space: nowrap;
62
+ }
63
+
64
+ .controls {
65
+ margin-top: 20px;
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 10px;
69
+ }
70
+
71
+ .field {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 6px;
75
+ font-size: 14px;
76
+ }
77
+
78
+ .params {
79
+ padding: 10px;
80
+ border: 1px solid #eee;
81
+ border-radius: 6px;
82
+ background: #fafafa;
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 10px;
86
+ }
87
+
88
+ .params h3 {
89
+ margin: 0;
90
+ font-size: 14px;
91
+ }
92
+
93
+ .row {
94
+ display: grid;
95
+ grid-template-columns: 1fr 1fr;
96
+ gap: 10px;
97
+ }
98
+
99
+ select {
100
+ padding: 8px 10px;
101
+ border: 1px solid #ccc;
102
+ background: white;
103
+ border-radius: 4px;
104
+ cursor: pointer;
105
+ font-size: 14px;
106
+ }
107
+
108
+ input {
109
+ padding: 8px 10px;
110
+ border: 1px solid #ccc;
111
+ background: white;
112
+ border-radius: 4px;
113
+ font-size: 14px;
114
+ }
115
+
116
+ button {
117
+ padding: 8px 16px;
118
+ border: 1px solid #ccc;
119
+ background: white;
120
+ border-radius: 4px;
121
+ cursor: pointer;
122
+ }
123
+
124
+ button:hover {
125
+ background: #f5f5f5;
126
+ }
127
+ </style>
128
+ </head>
129
+
130
+ <body>
131
+
132
+ <div class="container">
133
+ <shader-background id="bg">
134
+ <div class="overlay">
135
+ <div class="overlay-header">
136
+ <h1>Shader Backgrounds</h1>
137
+ <button class="overlay-toggle" id="overlayToggle" type="button">Hide</button>
138
+ </div>
139
+ <div class="overlay-content">
140
+ <p>Using OGL + Web Components</p>
141
+ <div class="controls">
142
+ <label class="field">
143
+ <span>Plugin</span>
144
+ <select id="pluginSelect">
145
+ <option value="gradient" selected>Gradient</option>
146
+ <option value="grainy-fog">Grainy Fog</option>
147
+ <option value="retro-grid">Retro Grid</option>
148
+ <option value="liquid-orb">Liquid Orb</option>
149
+ <option value="caustics">Caustics</option>
150
+ <option value="aurora-waves">Aurora Waves</option>
151
+ <option value="soft-starfield">Soft Starfield</option>
152
+ <option value="contour-lines">Contour Lines</option>
153
+ <option value="dreamy-bokeh">Dreamy Bokeh</option>
154
+ <option value="ink-wash">Ink Wash</option>
155
+ <option value="stained-glass">Stained Glass</option>
156
+ </select>
157
+ </label>
158
+ <label class="field">
159
+ <span>Canvas Filter</span>
160
+ <select id="filterPreset">
161
+ <option value="none" selected>None</option>
162
+ <option value="saturate(1.2) contrast(1.05)">Pop</option>
163
+ <option value="blur(8px) saturate(1.1) brightness(1.05)">Soft Blur</option>
164
+ <option value="sepia(0.25) saturate(1.3) contrast(1.05)">Warm</option>
165
+ <option value="hue-rotate(45deg) saturate(1.2)">Hue Shift</option>
166
+ <option value="grayscale(0.25) contrast(1.2)">Muted</option>
167
+ </select>
168
+ </label>
169
+ <label class="field">
170
+ <span>Custom Filter (CSS)</span>
171
+ <input id="filterInput" placeholder="e.g. blur(6px) saturate(1.2)" />
172
+ </label>
173
+ <button onclick="applyFilter()">Apply Filter</button>
174
+
175
+ <div class="params">
176
+ <h3>Plugin Parameters</h3>
177
+ <div id="pluginParams"></div>
178
+ </div>
179
+ <button onclick="changeColors()">Change Colors</button>
180
+ <button onclick="toggleRenderScale()">Toggle Render Scale (0.5x)</button>
181
+ <button onclick="toggleSingleRender()">Toggle Single Render Mode</button>
182
+ <button onclick="manualRender()" id="manualRenderBtn" style="display: none;">Manual Render</button>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </shader-background>
187
+ </div>
188
+
189
+
190
+ <script type="module" src="./src/index.ts"></script>
191
+ <script type="module" src="./demo.js"></script>
192
+ </body>
193
+
194
+ </html>
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@brochington/shader-backgrounds",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/shader-backgrounds.umd.cjs",
6
+ "module": "./dist/shader-backgrounds.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "build": "tsc && vite build",
11
+ "build:demo": "vite build --config vite.demo.config.ts",
12
+ "preview": "vite preview",
13
+ "preview:demo": "vite preview --config vite.demo.config.ts"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.9.3",
17
+ "vite": "^7.3.0",
18
+ "vite-plugin-dts": "^4.5.4"
19
+ },
20
+ "dependencies": {
21
+ "ogl": "^1.0.11"
22
+ }
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './lib/core/types';
2
+ export * from './lib/core/ShaderCanvas';
3
+ export * from './lib/plugins/GradientPlugin';
4
+ export * from './lib/plugins';
5
+ export * from './lib/components/web-component';
@@ -0,0 +1,198 @@
1
+ import { ShaderCanvas } from '../core/ShaderCanvas';
2
+ import { ShaderPlugin } from '../core/types';
3
+
4
+ export class ShaderBackgroundElement extends HTMLElement {
5
+ #canvas: HTMLCanvasElement;
6
+ #engine: ShaderCanvas | null = null;
7
+ #observer: ResizeObserver;
8
+ #plugin: ShaderPlugin | null = null;
9
+ #renderScale: number = 1.0;
10
+ #singleRender: boolean = false;
11
+ constructor() {
12
+ super();
13
+
14
+ this.attachShadow({ mode: 'open' });
15
+
16
+ // Styles
17
+ const style = document.createElement('style');
18
+ style.textContent = `
19
+ :host {
20
+ display: block;
21
+ width: 100%;
22
+ height: 100%;
23
+ position: relative;
24
+ overflow: hidden;
25
+ }
26
+ canvas {
27
+ display: block;
28
+ position: absolute;
29
+ top: 0;
30
+ left: 0;
31
+ /* Pixelated rendering for crisp upscaling of low-res buffers */
32
+ image-rendering: pixelated;
33
+ image-rendering: -webkit-optimize-contrast;
34
+ image-rendering: crisp-edges;
35
+ }
36
+ ::slotted(*) {
37
+ position: relative;
38
+ z-index: 1;
39
+ }
40
+ `;
41
+ this.shadowRoot!.appendChild(style);
42
+
43
+ // Canvas
44
+ this.#canvas = document.createElement('canvas');
45
+ // Allow styling the internal canvas from outside the shadow root:
46
+ // e.g. `shader-background::part(canvas) { filter: blur(8px); }`
47
+ this.#canvas.setAttribute('part', 'canvas');
48
+ this.shadowRoot!.appendChild(this.#canvas);
49
+
50
+ // Slot for children
51
+ const slot = document.createElement('slot');
52
+ this.shadowRoot!.appendChild(slot);
53
+
54
+ // Resize Observer
55
+ this.#observer = new ResizeObserver((entries) => {
56
+ if (this.#engine && entries.length > 0) {
57
+ const { width, height } = entries[0].contentRect;
58
+ this.#engine.resize(width, height);
59
+ }
60
+ });
61
+ }
62
+
63
+ connectedCallback() {
64
+ // Read initial attribute values
65
+ const renderScaleAttr = this.getAttribute('render-scale');
66
+ if (renderScaleAttr) {
67
+ const scale = parseFloat(renderScaleAttr);
68
+ if (!isNaN(scale)) {
69
+ this.#renderScale = Math.max(0.1, Math.min(2.0, scale));
70
+ }
71
+ }
72
+
73
+ const singleRenderAttr = this.getAttribute('single-render');
74
+ if (singleRenderAttr) {
75
+ this.#singleRender = singleRenderAttr === 'true' || singleRenderAttr === '';
76
+ }
77
+
78
+ this.#observer.observe(this);
79
+ if (this.#plugin) {
80
+ this.init();
81
+ }
82
+ // Ensure canvas is properly sized initially
83
+ if (this.#engine) {
84
+ const rect = this.getBoundingClientRect();
85
+ this.#engine.resize(rect.width, rect.height);
86
+ }
87
+ }
88
+
89
+ disconnectedCallback() {
90
+ this.#observer.disconnect();
91
+ if (this.#engine) {
92
+ this.#engine.dispose();
93
+ this.#engine = null;
94
+ }
95
+ }
96
+
97
+ set plugin(plugin: ShaderPlugin) {
98
+ this.#plugin = plugin;
99
+ if (this.#engine) {
100
+ this.#engine.stop();
101
+ this.#engine.dispose();
102
+ this.#engine = null;
103
+ }
104
+ this.init();
105
+ }
106
+
107
+ get plugin() {
108
+ if (!this.#plugin) {
109
+ throw new Error('Plugin is required');
110
+ }
111
+ return this.#plugin;
112
+ }
113
+
114
+ set renderScale(value: number) {
115
+ this.#renderScale = Math.max(0.1, Math.min(2.0, value)); // Clamp between 0.1 and 2.0
116
+ if (this.#engine) {
117
+ this.#engine.stop();
118
+ this.#engine.dispose();
119
+ this.#engine = null;
120
+ }
121
+ this.init();
122
+ }
123
+
124
+ get renderScale() {
125
+ return this.#renderScale;
126
+ }
127
+
128
+ set singleRender(value: boolean) {
129
+ this.#singleRender = value;
130
+ if (this.#engine) {
131
+ this.#engine.stop();
132
+ this.#engine.dispose();
133
+ this.#engine = null;
134
+ }
135
+ this.init();
136
+ }
137
+
138
+ get singleRender() {
139
+ return this.#singleRender;
140
+ }
141
+
142
+ public render() {
143
+ if (this.#engine && this.#singleRender) {
144
+ this.#engine.render();
145
+ }
146
+ }
147
+
148
+ init() {
149
+ // If we haven't received a plugin yet (common during element upgrade/attribute parsing),
150
+ // don't throw—just defer init until `plugin` is set.
151
+ if (!this.#plugin) return;
152
+
153
+ // Clean up previous engine if resetting config
154
+ if (this.#engine) {
155
+ this.#engine.stop();
156
+ this.#engine.dispose();
157
+ this.#engine = null;
158
+ }
159
+
160
+ // Get initial dimensions from host element
161
+ const rect = this.getBoundingClientRect();
162
+ const width = rect.width > 0 ? rect.width : 300;
163
+ const height = rect.height > 0 ? rect.height : 300;
164
+
165
+ this.#engine = new ShaderCanvas(this.#canvas, this.#plugin, {
166
+ width,
167
+ height,
168
+ renderScale: this.#renderScale,
169
+ singleRender: this.#singleRender,
170
+ });
171
+
172
+ this.#engine.start();
173
+ }
174
+
175
+ static get observedAttributes() {
176
+ return ['render-scale', 'single-render'];
177
+ }
178
+
179
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
180
+ if (oldValue === newValue) return;
181
+
182
+ switch (name) {
183
+ case 'render-scale': {
184
+ const scale = parseFloat(newValue);
185
+ if (!isNaN(scale)) {
186
+ this.renderScale = scale;
187
+ }
188
+ break;
189
+ }
190
+ case 'single-render':
191
+ this.singleRender = newValue === 'true' || newValue === '';
192
+ break;
193
+ }
194
+ }
195
+ }
196
+
197
+ // Define the custom element
198
+ customElements.define('shader-background', ShaderBackgroundElement);
@@ -0,0 +1,235 @@
1
+ import { Renderer, Program, Mesh, Triangle, Vec2, OGLRenderingContext } from 'ogl';
2
+ import { ShaderPlugin } from './types';
3
+
4
+ export class ShaderCanvas {
5
+ public gl: OGLRenderingContext;
6
+ #renderer: Renderer;
7
+ #mesh!: Mesh;
8
+ #program!: Program;
9
+ #plugin: ShaderPlugin;
10
+
11
+ #animationId: number | null = null;
12
+ #lastTime: number = 0;
13
+ #totalTime: number = 0;
14
+ #isPlaying: boolean = false;
15
+
16
+ #renderScale: number;
17
+ #singleRender: boolean;
18
+ #displayWidth: number;
19
+ #displayHeight: number;
20
+ #pendingResize: { renderWidth: number; renderHeight: number } | null = null;
21
+
22
+ constructor(
23
+ private canvas: HTMLCanvasElement,
24
+ plugin: ShaderPlugin,
25
+ options: {
26
+ pixelRatio?: number;
27
+ width?: number;
28
+ height?: number;
29
+ renderScale?: number;
30
+ singleRender?: boolean;
31
+ } = {}
32
+ ) {
33
+ this.#plugin = plugin;
34
+ // `pixelRatio` is an optional extra multiplier for the internal buffer size.
35
+ // Kept separate from `renderScale` so you can do things like: renderScale=0.5 but pixelRatio=2.
36
+ const pixelRatio = options.pixelRatio ?? 1;
37
+ const pr = Math.max(0.1, Math.min(4.0, pixelRatio));
38
+ this.#renderScale = (options.renderScale ?? 1.0) * pr;
39
+ this.#singleRender = options.singleRender ?? false;
40
+
41
+ // Get canvas dimensions, either from options or canvas element
42
+ let width = options.width || 300;
43
+ let height = options.height || 300;
44
+
45
+ if (!options.width || !options.height) {
46
+ const rect = this.canvas.getBoundingClientRect();
47
+ if (rect.width > 0) width = rect.width;
48
+ if (rect.height > 0) height = rect.height;
49
+ }
50
+
51
+ this.#displayWidth = width;
52
+ this.#displayHeight = height;
53
+
54
+ // Apply render scale to rendering resolution
55
+ const renderWidth = Math.max(1, Math.floor(width * this.#renderScale));
56
+ const renderHeight = Math.max(1, Math.floor(height * this.#renderScale));
57
+
58
+ // Initialize OGL Renderer to match canvas buffer size
59
+ this.#renderer = new Renderer({
60
+ canvas: this.canvas,
61
+ width: renderWidth,
62
+ height: renderHeight,
63
+ alpha: false, // Opaque background
64
+ dpr: 1, // Don't apply device pixel ratio since we're already scaling
65
+ });
66
+
67
+ this.gl = this.#renderer.gl;
68
+
69
+ // Important: OGL's internal sizing can update the canvas CSS size.
70
+ // We want CSS size to remain at the display size, while only the drawing buffer scales.
71
+ this.canvas.width = renderWidth;
72
+ this.canvas.height = renderHeight;
73
+ this.#renderer.setSize(renderWidth, renderHeight);
74
+ this.canvas.style.width = `${width}px`;
75
+ this.canvas.style.height = `${height}px`;
76
+
77
+ this.init();
78
+ }
79
+
80
+ private init() {
81
+ // Triangle creates a geometry with 3 vertices that covers the screen.
82
+ // It provides 'position' and 'uv' attributes automatically.
83
+ const geometry = new Triangle(this.gl);
84
+
85
+ const commonUniforms = {
86
+ uTime: { value: 0 },
87
+ uResolution: {
88
+ // Display-space resolution (CSS pixels); shaders typically use this for aspect-correct math.
89
+ value: new Vec2(this.#displayWidth, this.#displayHeight),
90
+ },
91
+ };
92
+
93
+ this.#program = new Program(this.gl, {
94
+ vertex: this.#plugin.vertexShader || defaultVertex,
95
+ fragment: this.#plugin.fragmentShader,
96
+ uniforms: {
97
+ ...commonUniforms,
98
+ ...this.#plugin.uniforms,
99
+ },
100
+ // Ensure depth test is off for background
101
+ depthTest: false,
102
+ cullFace: false,
103
+ });
104
+
105
+ this.#mesh = new Mesh(this.gl, { geometry, program: this.#program });
106
+
107
+ if (this.#plugin.onInit) {
108
+ this.#plugin.onInit(this.gl, this.#program);
109
+ }
110
+ }
111
+
112
+ public start() {
113
+ if (this.#isPlaying) return;
114
+ this.#isPlaying = true;
115
+ this.#lastTime = performance.now();
116
+
117
+ if (this.#singleRender) {
118
+ // In single render mode, just render once and stop
119
+ this.render();
120
+ this.#isPlaying = false;
121
+ } else {
122
+ // Normal animation loop
123
+ this.loop();
124
+ }
125
+ }
126
+
127
+ public stop() {
128
+ this.#isPlaying = false;
129
+ if (this.#animationId !== null) {
130
+ cancelAnimationFrame(this.#animationId);
131
+ this.#animationId = null;
132
+ }
133
+ }
134
+
135
+ public render() {
136
+ // In single-render mode we may defer buffer resizing to avoid clearing the canvas on window resize.
137
+ if (this.#pendingResize) {
138
+ const { renderWidth, renderHeight } = this.#pendingResize;
139
+ this.#pendingResize = null;
140
+
141
+ this.canvas.width = renderWidth;
142
+ this.canvas.height = renderHeight;
143
+ this.#renderer.setSize(renderWidth, renderHeight);
144
+
145
+ // Re-apply CSS sizing because OGL can overwrite it.
146
+ this.canvas.style.width = `${this.#displayWidth}px`;
147
+ this.canvas.style.height = `${this.#displayHeight}px`;
148
+ }
149
+
150
+ const now = performance.now();
151
+ const dt = now - this.#lastTime;
152
+ this.#lastTime = now;
153
+ this.#totalTime += dt * 0.001; // Convert to seconds
154
+
155
+ // Update global time uniform
156
+ this.#program.uniforms.uTime.value = this.#totalTime;
157
+
158
+ // Run Plugin Update Logic
159
+ if (this.#plugin.onRender) {
160
+ this.#plugin.onRender(dt, this.#totalTime);
161
+ }
162
+
163
+ this.#renderer.render({ scene: this.#mesh });
164
+ }
165
+
166
+ public resize(width?: number, height?: number) {
167
+ // If dimensions not provided, get from canvas element
168
+ if (width === undefined || height === undefined) {
169
+ const rect = this.canvas.getBoundingClientRect();
170
+ width = Math.max(1, rect.width);
171
+ height = Math.max(1, rect.height);
172
+ }
173
+
174
+ this.#displayWidth = width;
175
+ this.#displayHeight = height;
176
+
177
+ // Calculate scaled render dimensions
178
+ const renderWidth = Math.max(1, Math.floor(width * this.#renderScale));
179
+ const renderHeight = Math.max(1, Math.floor(height * this.#renderScale));
180
+
181
+ // Always update CSS size so the last rendered frame stays stretched to the element size.
182
+ this.canvas.style.width = `${width}px`;
183
+ this.canvas.style.height = `${height}px`;
184
+
185
+ if (this.#singleRender) {
186
+ // Critical: Changing canvas.width/height clears the drawing buffer.
187
+ // In single-render mode, defer buffer resize until the next manual render.
188
+ this.#pendingResize = { renderWidth, renderHeight };
189
+ } else {
190
+ // Update canvas buffer size to scaled dimensions
191
+ this.canvas.width = renderWidth;
192
+ this.canvas.height = renderHeight;
193
+
194
+ // Resize internal renderer to match canvas buffer size
195
+ // OGL may update the canvas CSS size; we re-apply CSS sizing right after.
196
+ this.#renderer.setSize(renderWidth, renderHeight);
197
+
198
+ // Re-apply CSS size (setSize may overwrite it)
199
+ this.canvas.style.width = `${width}px`;
200
+ this.canvas.style.height = `${height}px`;
201
+ }
202
+
203
+ // Update uniforms with actual display dimensions (not scaled buffer size)
204
+ if (this.#program) {
205
+ this.#program.uniforms.uResolution.value.set(width, height);
206
+ }
207
+
208
+ // Notify plugin with actual display dimensions
209
+ if (this.#plugin.onResize) {
210
+ this.#plugin.onResize(width, height);
211
+ }
212
+ }
213
+
214
+ private loop = () => {
215
+ if (!this.#isPlaying) return;
216
+
217
+ this.render();
218
+ this.#animationId = requestAnimationFrame(this.loop);
219
+ };
220
+
221
+ public dispose() {
222
+ this.stop();
223
+ // basic cleanup, more can be done here (gl deleteProgram etc)
224
+ }
225
+ }
226
+
227
+ const defaultVertex = /* glsl */ `
228
+ attribute vec2 uv;
229
+ attribute vec2 position;
230
+ varying vec2 vUv;
231
+ void main() {
232
+ vUv = uv;
233
+ gl_Position = vec4(position, 0, 1);
234
+ }
235
+ `;
@@ -0,0 +1,26 @@
1
+ // src/core/types.ts
2
+ import { Program, OGLRenderingContext } from 'ogl';
3
+
4
+ export interface ShaderPlugin {
5
+ // Unique name for the plugin
6
+ name: string;
7
+
8
+ // The Fragment shader string
9
+ fragmentShader: string;
10
+
11
+ // Optional Vertex shader (defaults to a pass-through if generic)
12
+ vertexShader?: string;
13
+
14
+ // Initial Uniforms (values only, OGL handles wrapping)
15
+ uniforms: Record<string, { value: any }>;
16
+
17
+ // Lifecycle hook: Called when the OGL Program is created
18
+ onInit?: (gl: OGLRenderingContext, program: Program) => void;
19
+
20
+ // Lifecycle hook: Called every frame
21
+ // dt = delta time in ms, totalTime = elapsed in seconds
22
+ onRender?: (dt: number, totalTime: number) => void;
23
+
24
+ // Lifecycle hook: Called on resize
25
+ onResize?: (width: number, height: number) => void;
26
+ }