@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/.github/workflows/deploy-demo-to-pages.yml +63 -0
- package/PLUGINS.md +269 -0
- package/README.md +159 -0
- package/demo.js +1044 -0
- package/index.html +194 -0
- package/package.json +23 -0
- package/src/index.ts +5 -0
- package/src/lib/components/web-component.ts +198 -0
- package/src/lib/core/ShaderCanvas.ts +235 -0
- package/src/lib/core/types.ts +26 -0
- package/src/lib/plugins/AuroraWavesPlugin.ts +128 -0
- package/src/lib/plugins/CausticsPlugin.ts +128 -0
- package/src/lib/plugins/ContourLinesPlugin.ts +148 -0
- package/src/lib/plugins/DreamyBokehPlugin.ts +191 -0
- package/src/lib/plugins/GradientPlugin.ts +445 -0
- package/src/lib/plugins/GrainyFogPlugin.ts +139 -0
- package/src/lib/plugins/InkWashPlugin.ts +182 -0
- package/src/lib/plugins/LiquidOrbPlugin.ts +140 -0
- package/src/lib/plugins/RetroGridPlugin.ts +77 -0
- package/src/lib/plugins/SoftStarfieldPlugin.ts +156 -0
- package/src/lib/plugins/StainedGlassPlugin.ts +261 -0
- package/src/lib/plugins/index.ts +11 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +19 -0
- package/vite.demo.config.ts +13 -0
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,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
|
+
}
|