wgsl-play 0.0.36 → 0.0.38

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/README.md CHANGED
@@ -56,7 +56,7 @@ You can include shader code inline if you'd prefer. Use a `<script type="text/wg
56
56
 
57
57
  ```typescript
58
58
  const player = document.querySelector("wgsl-play");
59
- player.source = shaderCode;
59
+ player.shader = shaderCode;
60
60
  player.pause();
61
61
  player.rewind();
62
62
  player.play();
@@ -68,7 +68,7 @@ player.play();
68
68
  import shader from './examples/noise.wesl?raw';
69
69
 
70
70
  const player = document.querySelector("wgsl-play");
71
- player.source = shader;
71
+ player.shader = shader;
72
72
  ```
73
73
 
74
74
  The `?raw` suffix imports the file as a string. This keeps shaders alongside your source files with HMR support.
@@ -80,12 +80,14 @@ The `?raw` suffix imports the file as a string. This keeps shaders alongside you
80
80
  - `shader-root` - Root path for internal imports (default: `/shaders`)
81
81
  - `autoplay` - Start animating on load (default: `true`). Set `autoplay="false"` to start paused
82
82
  - `transparent` - Use premultiplied alpha for transparent backgrounds (default: opaque)
83
+ - `from` - Element ID of a source provider (e.g., wgsl-edit) to connect to
83
84
  - `fetch-libs` - Auto-fetch missing libraries from npm (default: `true`). Set `fetch-libs="false"` to disable
85
+ - `fetch-sources` - Auto-fetch local .wesl source files via HTTP (default: `true`). Set `fetch-sources="false"` to disable
84
86
 
85
87
  ### Properties
86
- - `source: string` - Get/set shader source
88
+ - `shader: string` - Get/set shader source (single-file convenience)
87
89
  - `conditions: Record<string, boolean>` - Get/set conditions for conditional compilation (`@if`/`@elif`/`@else`)
88
- - `project: WeslProject` - Set full project config (weslSrc, libs, conditions, constants)
90
+ - `project: WeslProject` - Get/set full project config (weslSrc, libs, conditions, constants)
89
91
  - `isPlaying: boolean` - Playback state (readonly)
90
92
  - `time: number` - Animation time in seconds (readonly)
91
93
  - `hasError: boolean` - Compilation error state (readonly)
@@ -147,7 +149,7 @@ import super::common::tint;
147
149
 
148
150
  ## Using with wesl-plugin
149
151
 
150
- For more control, use the [wesl-plugin](https://github.com/wgsl-tooling-wg/wesl-js/tree/main/tools/packages/wesl-plugin) to
152
+ For more control, use the [wesl-plugin](https://github.com/wgsl-tooling-wg/wesl-js/tree/main/packages/wesl-plugin) to
151
153
  assemble shaders and libraries at build time and provide
152
154
  them wgsl-play in JavaScript or TypeScript.
153
155
  - provides full support for Hot Module Reloading during development
@@ -0,0 +1,620 @@
1
+ import { fetchDependencies, loadShaderFromUrl } from "wesl-fetch";
2
+ import { WeslParseError, fileToModulePath, requestWeslDevice } from "wesl";
3
+ import { linkAndCreatePipeline, renderFrame, updateRenderUniforms } from "wesl-gpu";
4
+ //#region src/Config.ts
5
+ const defaultConfig = { shaderRoot: "/shaders" };
6
+ let globalConfig = { ...defaultConfig };
7
+ /** Set global defaults for all wgsl-play instances. */
8
+ function defaults(config) {
9
+ globalConfig = {
10
+ ...globalConfig,
11
+ ...config
12
+ };
13
+ }
14
+ /** Get resolved config, merging element overrides with global defaults. */
15
+ function getConfig(overrides) {
16
+ return {
17
+ ...globalConfig,
18
+ ...overrides
19
+ };
20
+ }
21
+ /** Reset config to defaults (useful for testing). */
22
+ function resetConfig() {
23
+ globalConfig = { ...defaultConfig };
24
+ }
25
+ //#endregion
26
+ //#region src/ErrorOverlay.ts
27
+ /** Manages an error overlay element within a shadow DOM. */
28
+ var ErrorOverlay = class {
29
+ el;
30
+ _message = null;
31
+ constructor(container) {
32
+ this.el = document.createElement("div");
33
+ this.el.className = "error-overlay";
34
+ container.appendChild(this.el);
35
+ }
36
+ show(message) {
37
+ this._message = message;
38
+ this.el.textContent = message;
39
+ this.el.classList.add("visible");
40
+ console.error("[wgsl-play]", message);
41
+ }
42
+ hide() {
43
+ this._message = null;
44
+ this.el.classList.remove("visible");
45
+ }
46
+ get visible() {
47
+ return this.el.classList.contains("visible");
48
+ }
49
+ get message() {
50
+ return this._message;
51
+ }
52
+ };
53
+ //#endregion
54
+ //#region src/icons/backToStart.svg?raw
55
+ var backToStart_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><rect x=\"4\" y=\"4\" width=\"3\" height=\"16\" fill=\"currentColor\"/><polygon points=\"20,4 9,12 20,20\" fill=\"currentColor\"/></svg>\n";
56
+ //#endregion
57
+ //#region src/icons/expand.svg?raw
58
+ var expand_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><path d=\"M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z\" fill=\"currentColor\"/></svg>\n";
59
+ //#endregion
60
+ //#region src/icons/pause.svg?raw
61
+ var pause_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><rect x=\"5\" y=\"4\" width=\"4\" height=\"16\" fill=\"currentColor\"/><rect x=\"15\" y=\"4\" width=\"4\" height=\"16\" fill=\"currentColor\"/></svg>\n";
62
+ //#endregion
63
+ //#region src/icons/play.svg?raw
64
+ var play_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><polygon points=\"6,4 20,12 6,20\" fill=\"currentColor\"/></svg>\n";
65
+ //#endregion
66
+ //#region src/icons/shrink.svg?raw
67
+ var shrink_default = "<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\"><path d=\"M9 3v6H3v-2h4V3h2zm6 0h2v4h4v2h-6V3zM3 15h6v6H7v-4H3v-2zm12 0h6v2h-4v4h-2v-6z\" fill=\"currentColor\"/></svg>\n";
68
+ //#endregion
69
+ //#region src/PlaybackControls.ts
70
+ /** Playback controls overlay for wgsl-play. */
71
+ var PlaybackControls = class {
72
+ container;
73
+ playPauseBtn;
74
+ fullscreenBtn;
75
+ playing = true;
76
+ constructor(shadow, onPlay, onPause, onRewind, onFullscreen) {
77
+ this.container = document.createElement("div");
78
+ this.container.className = "controls";
79
+ this.fullscreenBtn = document.createElement("button");
80
+ this.fullscreenBtn.innerHTML = expand_default;
81
+ this.fullscreenBtn.addEventListener("click", onFullscreen);
82
+ const rewindBtn = document.createElement("button");
83
+ rewindBtn.innerHTML = backToStart_default;
84
+ rewindBtn.addEventListener("click", onRewind);
85
+ this.playPauseBtn = document.createElement("button");
86
+ this.playPauseBtn.innerHTML = pause_default;
87
+ this.playPauseBtn.addEventListener("click", () => this.playing ? onPause() : onPlay());
88
+ this.container.append(this.fullscreenBtn, rewindBtn, this.playPauseBtn);
89
+ shadow.appendChild(this.container);
90
+ }
91
+ setPlaying(playing) {
92
+ this.playing = playing;
93
+ this.playPauseBtn.innerHTML = playing ? pause_default : play_default;
94
+ }
95
+ setFullscreen(isFullscreen) {
96
+ this.fullscreenBtn.innerHTML = isFullscreen ? shrink_default : expand_default;
97
+ }
98
+ show() {
99
+ this.container.style.display = "";
100
+ }
101
+ hide() {
102
+ this.container.style.display = "none";
103
+ }
104
+ };
105
+ //#endregion
106
+ //#region src/Renderer.ts
107
+ /** Initialize WebGPU for a canvas element. */
108
+ async function initWebGPU(canvas, alphaMode = "opaque") {
109
+ const adapter = await navigator.gpu.requestAdapter();
110
+ if (!adapter) throw new Error("WebGPU adapter not available");
111
+ const device = await requestWeslDevice(adapter);
112
+ const context = canvas.getContext("webgpu");
113
+ if (!context) throw new Error("WebGPU context not available");
114
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
115
+ context.configure({
116
+ device,
117
+ format: presentationFormat,
118
+ alphaMode
119
+ });
120
+ const uniformBuffer = device.createBuffer({
121
+ size: 32,
122
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
123
+ });
124
+ const bindGroupLayout = device.createBindGroupLayout({ entries: [{
125
+ binding: 0,
126
+ visibility: GPUShaderStage.FRAGMENT,
127
+ buffer: {}
128
+ }] });
129
+ return {
130
+ device,
131
+ canvas,
132
+ context,
133
+ presentationFormat,
134
+ uniformBuffer,
135
+ pipelineLayout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
136
+ bindGroup: device.createBindGroup({
137
+ layout: bindGroupLayout,
138
+ entries: [{
139
+ binding: 0,
140
+ resource: { buffer: uniformBuffer }
141
+ }]
142
+ }),
143
+ frameCount: 0
144
+ };
145
+ }
146
+ /** Compile WESL fragment shader and create render pipeline. */
147
+ async function createPipeline(state, fragmentSource, options) {
148
+ state.device.pushErrorScope("validation");
149
+ let gpuError;
150
+ let jsError;
151
+ try {
152
+ state.pipeline = await linkAndCreatePipeline({
153
+ device: state.device,
154
+ fragmentSource,
155
+ format: state.presentationFormat,
156
+ layout: state.pipelineLayout,
157
+ ...options
158
+ });
159
+ } catch (e) {
160
+ jsError = e;
161
+ } finally {
162
+ gpuError = await state.device.popErrorScope();
163
+ }
164
+ if (jsError || gpuError) {
165
+ state.pipeline = void 0;
166
+ throw jsError ?? gpuError;
167
+ }
168
+ }
169
+ /** Start the render loop. Returns a stop function. */
170
+ function startRenderLoop(state, playback) {
171
+ let animationId;
172
+ function render() {
173
+ if (!state.pipeline) {
174
+ animationId = requestAnimationFrame(render);
175
+ return;
176
+ }
177
+ const time = calculateTime(playback);
178
+ const resolution = [state.canvas.width, state.canvas.height];
179
+ updateRenderUniforms(state.uniformBuffer, state.device, resolution, time, [0, 0]);
180
+ renderFrame({
181
+ device: state.device,
182
+ pipeline: state.pipeline,
183
+ bindGroup: state.bindGroup,
184
+ targetView: state.context.getCurrentTexture().createView()
185
+ });
186
+ state.frameCount++;
187
+ animationId = requestAnimationFrame(render);
188
+ }
189
+ animationId = requestAnimationFrame(render);
190
+ return () => cancelAnimationFrame(animationId);
191
+ }
192
+ function calculateTime(playback) {
193
+ return ((playback.isPlaying ? performance.now() : playback.startTime + playback.pausedDuration) - playback.startTime) / 1e3;
194
+ }
195
+ //#endregion
196
+ //#region src/WgslPlay.css?inline
197
+ var WgslPlay_default = ":host {\n --error-bg: rgba(220, 102, 18, 0.8);\n --error-color: white;\n --controls-bg: rgba(0, 0, 0, 0.4);\n --controls-color: rgba(255, 255, 255, 0.8);\n --controls-hover-color: white;\n --controls-hover-bg: rgba(255, 255, 255, 0.15);\n\n display: block;\n position: relative;\n overflow: hidden;\n}\n\n:host(.dark) {\n --error-bg: rgba(180, 60, 10, 0.85);\n --controls-bg: rgba(0, 0, 0, 0.5);\n}\n\ncanvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.controls {\n background: var(--controls-bg);\n position: absolute;\n bottom: 8px;\n right: 8px;\n display: flex;\n gap: 20px;\n border-radius: 6px;\n padding: 2px;\n opacity: 0;\n transition: opacity 0.2s;\n z-index: 1;\n}\n\n:host(:hover) .controls {\n opacity: 1;\n}\n\n.controls button {\n border: none;\n background: none;\n color: var(--controls-color);\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n display: flex;\n align-items: center;\n}\n\n.controls button:hover {\n color: var(--controls-hover-color);\n background: var(--controls-hover-bg);\n}\n\n.error-overlay {\n position: absolute;\n inset: 0;\n background: var(--error-bg);\n color: var(--error-color);\n padding: 1rem;\n font-family: monospace;\n font-size: 0.875rem;\n white-space: pre-wrap;\n overflow: auto;\n display: none;\n}\n\n.error-overlay.visible {\n display: block;\n}\n";
198
+ //#endregion
199
+ //#region src/WgslPlay.ts
200
+ let styles = null;
201
+ let template = null;
202
+ /** <wgsl-play> web component for rendering WESL/WGSL fragment shaders. */
203
+ var WgslPlay = class extends HTMLElement {
204
+ static observedAttributes = [
205
+ "src",
206
+ "shader-root",
207
+ "from",
208
+ "no-controls",
209
+ "theme",
210
+ "autoplay",
211
+ "transparent",
212
+ "fetch-libs",
213
+ "fetch-sources"
214
+ ];
215
+ canvas;
216
+ errorOverlay;
217
+ controls;
218
+ resizeObserver;
219
+ stopRenderLoop;
220
+ renderState;
221
+ playback = {
222
+ isPlaying: true,
223
+ startTime: performance.now(),
224
+ pausedDuration: 0
225
+ };
226
+ _weslSrc = {};
227
+ _rootModuleName = "package::main";
228
+ _libs;
229
+ _linkOptions = {};
230
+ _fetchSources = true;
231
+ _initPromise;
232
+ _sourceEl = null;
233
+ _sourceListener = null;
234
+ _fetchLibs = true;
235
+ _dirty = false;
236
+ _building = false;
237
+ _theme = "auto";
238
+ _mediaQuery = null;
239
+ _onFullscreenChange = () => this.controls.setFullscreen(!!document.fullscreenElement);
240
+ /** Get config overrides from element attributes. */
241
+ getConfigOverrides() {
242
+ const shaderRoot = this.getAttribute("shader-root");
243
+ if (!shaderRoot) return void 0;
244
+ return { shaderRoot };
245
+ }
246
+ constructor() {
247
+ super();
248
+ const shadow = this.attachShadow({ mode: "open" });
249
+ shadow.adoptedStyleSheets = [getStyles()];
250
+ shadow.appendChild(getTemplate().content.cloneNode(true));
251
+ this.canvas = shadow.querySelector("canvas");
252
+ this.errorOverlay = new ErrorOverlay(shadow);
253
+ this.controls = new PlaybackControls(shadow, () => this.play(), () => this.pause(), () => this.rewind(), () => this.toggleFullscreen());
254
+ this.resizeObserver = new ResizeObserver((entries) => {
255
+ for (const entry of entries) {
256
+ const { width, height } = entry.contentRect;
257
+ if (width > 0 && height > 0) {
258
+ this.canvas.width = Math.floor(width * devicePixelRatio);
259
+ this.canvas.height = Math.floor(height * devicePixelRatio);
260
+ }
261
+ }
262
+ });
263
+ }
264
+ connectedCallback() {
265
+ this.resizeObserver.observe(this);
266
+ const themeAttr = this.getAttribute("theme");
267
+ if (themeAttr) this._theme = themeAttr;
268
+ this._mediaQuery = matchMedia("(prefers-color-scheme: dark)");
269
+ this._mediaQuery.addEventListener("change", () => this.updateTheme());
270
+ this.updateTheme();
271
+ document.addEventListener("fullscreenchange", this._onFullscreenChange);
272
+ if (!this.autoplay) {
273
+ this.playback.isPlaying = false;
274
+ this.controls.setPlaying(false);
275
+ }
276
+ this.initialize();
277
+ upgradeProperty(this, "conditions");
278
+ upgradeProperty(this, "shader");
279
+ upgradeProperty(this, "project");
280
+ }
281
+ disconnectedCallback() {
282
+ this.resizeObserver.disconnect();
283
+ this.stopRenderLoop?.();
284
+ document.removeEventListener("fullscreenchange", this._onFullscreenChange);
285
+ if (this._sourceEl && this._sourceListener) this._sourceEl.removeEventListener("change", this._sourceListener);
286
+ }
287
+ attributeChangedCallback(name, oldValue, newValue) {
288
+ if (oldValue === newValue) return;
289
+ if (name === "no-controls") {
290
+ newValue !== null ? this.controls.hide() : this.controls.show();
291
+ return;
292
+ }
293
+ if (name === "theme") {
294
+ this._theme = newValue || "auto";
295
+ this.updateTheme();
296
+ return;
297
+ }
298
+ if (name === "autoplay") {
299
+ newValue === "false" ? this.pause() : this.play();
300
+ return;
301
+ }
302
+ if (name === "fetch-libs") {
303
+ this._fetchLibs = newValue !== "false";
304
+ return;
305
+ }
306
+ if (name === "fetch-sources") {
307
+ this._fetchSources = newValue !== "false";
308
+ return;
309
+ }
310
+ if (name === "src" && newValue && this._initPromise) this.loadFromUrl(newValue);
311
+ }
312
+ /** Current shader source code (main module). */
313
+ get shader() {
314
+ return this._weslSrc[this._rootModuleName] ?? "";
315
+ }
316
+ /** Set shader source directly (single-file convenience). */
317
+ set shader(value) {
318
+ this._weslSrc = { [this._rootModuleName]: value };
319
+ this._libs = void 0;
320
+ this.requestBuild();
321
+ }
322
+ /** Conditions for conditional compilation (@if/@elif/@else). */
323
+ get conditions() {
324
+ return this._linkOptions.conditions ?? {};
325
+ }
326
+ set conditions(value) {
327
+ this._linkOptions = {
328
+ ...this._linkOptions,
329
+ conditions: value
330
+ };
331
+ if (Object.keys(this._weslSrc).length === 0) return;
332
+ this.requestBuild();
333
+ }
334
+ /** Set project configuration (mirrors wesl link() API). */
335
+ set project(value) {
336
+ const { weslSrc, rootModuleName, libs, packageName, conditions, constants } = value;
337
+ if (packageName !== void 0) this._linkOptions.packageName = packageName;
338
+ if (conditions !== void 0) this._linkOptions.conditions = conditions;
339
+ if (constants !== void 0) this._linkOptions.constants = constants;
340
+ if (libs) this._libs = libs;
341
+ if (weslSrc) {
342
+ const pkg = this._linkOptions.packageName || "package";
343
+ const root = rootModuleName ?? "main";
344
+ this._weslSrc = toModulePaths(weslSrc, pkg);
345
+ this._rootModuleName = fileToModulePath(root, pkg, false);
346
+ this.requestBuild();
347
+ return;
348
+ }
349
+ if (Object.keys(this._weslSrc).length === 0) return;
350
+ this.requestBuild();
351
+ }
352
+ /** Whether to auto-fetch missing library packages from npm (default: true). */
353
+ get fetchLibs() {
354
+ return this._fetchLibs;
355
+ }
356
+ set fetchLibs(value) {
357
+ this._fetchLibs = value;
358
+ if (value) this.removeAttribute("fetch-libs");
359
+ else this.setAttribute("fetch-libs", "false");
360
+ }
361
+ /** Whether to fetch local .wesl source files via HTTP (default: true). */
362
+ get fetchSources() {
363
+ return this._fetchSources;
364
+ }
365
+ set fetchSources(value) {
366
+ this._fetchSources = value;
367
+ if (value) this.removeAttribute("fetch-sources");
368
+ else this.setAttribute("fetch-sources", "false");
369
+ }
370
+ /** Whether autoplay is enabled (default: true). Set autoplay="false" to start paused. */
371
+ get autoplay() {
372
+ return this.getAttribute("autoplay") !== "false";
373
+ }
374
+ set autoplay(value) {
375
+ if (value !== false && value !== "false") this.removeAttribute("autoplay");
376
+ else this.setAttribute("autoplay", "false");
377
+ }
378
+ /** Whether the shader is currently playing. */
379
+ get isPlaying() {
380
+ return this.playback.isPlaying;
381
+ }
382
+ /** Current animation time in seconds. */
383
+ get time() {
384
+ const { isPlaying, startTime, pausedDuration } = this.playback;
385
+ return ((isPlaying ? performance.now() : startTime + pausedDuration) - startTime) / 1e3;
386
+ }
387
+ /** Number of frames rendered (for testing/debugging). */
388
+ get frameCount() {
389
+ return this.renderState?.frameCount ?? 0;
390
+ }
391
+ /** Whether there's a compilation error. */
392
+ get hasError() {
393
+ return this.errorOverlay.visible;
394
+ }
395
+ /** Current error message, or null if no error. */
396
+ get errorMessage() {
397
+ return this.errorOverlay.message;
398
+ }
399
+ /** Start playback. */
400
+ play() {
401
+ const { isPlaying, pausedDuration } = this.playback;
402
+ if (isPlaying) return;
403
+ this.playback.startTime = performance.now() - pausedDuration;
404
+ this.setPlaying(true);
405
+ }
406
+ /** Pause playback. */
407
+ pause() {
408
+ if (!this.playback.isPlaying) return;
409
+ this.playback.pausedDuration = performance.now() - this.playback.startTime;
410
+ this.setPlaying(false);
411
+ }
412
+ setPlaying(playing) {
413
+ this.playback.isPlaying = playing;
414
+ this.controls.setPlaying(playing);
415
+ this.dispatchEvent(new CustomEvent("playback-change", { detail: { isPlaying: playing } }));
416
+ }
417
+ /** Reset animation to time 0 and pause. */
418
+ rewind() {
419
+ this.playback.startTime = performance.now();
420
+ this.playback.pausedDuration = 0;
421
+ this.setPlaying(false);
422
+ }
423
+ /** Display error message in overlay. Pass empty string to clear. */
424
+ showError(message) {
425
+ if (!message) {
426
+ this.errorOverlay.hide();
427
+ return;
428
+ }
429
+ this.errorOverlay.show(message);
430
+ this.pause();
431
+ }
432
+ /** Toggle fullscreen on this element. */
433
+ toggleFullscreen() {
434
+ if (document.fullscreenElement) document.exitFullscreen();
435
+ else this.requestFullscreen();
436
+ }
437
+ updateTheme() {
438
+ const isDark = this._theme === "dark" || this._theme === "auto" && matchMedia("(prefers-color-scheme: dark)").matches;
439
+ this.classList.toggle("dark", isDark);
440
+ }
441
+ /** Set up WebGPU and load initial shader. Returns true if successful. */
442
+ initialize() {
443
+ if (this.renderState) return Promise.resolve(true);
444
+ if (!this._initPromise) this._initPromise = this.doInitialize();
445
+ return this._initPromise;
446
+ }
447
+ async doInitialize() {
448
+ try {
449
+ const alphaMode = this.hasAttribute("transparent") ? "premultiplied" : "opaque";
450
+ this.renderState = await initWebGPU(this.canvas, alphaMode);
451
+ this.loadInitialContent();
452
+ this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
453
+ this.dispatchEvent(new CustomEvent("ready"));
454
+ return true;
455
+ } catch (error) {
456
+ const message = !navigator.gpu ? "WebGPU is not supported in this browser.\nTry Chrome 113+, Edge 113+, or Safari 18+." : `WebGPU initialization failed: ${error}`;
457
+ this.errorOverlay.show(message);
458
+ this.pause();
459
+ this.dispatchEvent(new CustomEvent("init-error", { detail: { message } }));
460
+ return false;
461
+ }
462
+ }
463
+ /** Load from source element, src URL, script child, or inline textContent. */
464
+ loadInitialContent() {
465
+ const fromId = this.getAttribute("from");
466
+ if (fromId) {
467
+ this.connectFrom(fromId);
468
+ return;
469
+ }
470
+ const src = this.getAttribute("src");
471
+ if (src) {
472
+ this.loadFromUrl(src);
473
+ return;
474
+ }
475
+ const inlineSource = this.querySelector("script[type=\"text/wgsl\"], script[type=\"text/wesl\"]")?.textContent?.trim() ?? this.textContent?.trim();
476
+ if (!inlineSource) return;
477
+ this._weslSrc = { [this._rootModuleName]: inlineSource };
478
+ this.requestBuild();
479
+ }
480
+ /** Connect to a source provider element (e.g., wgsl-edit). */
481
+ connectFrom(id) {
482
+ const el = document.getElementById(id);
483
+ if (!el) {
484
+ console.error(`wgsl-play: source element "${id}" not found`);
485
+ return;
486
+ }
487
+ this._sourceEl = el;
488
+ const p = el.project;
489
+ if (p) this.project = p;
490
+ this._sourceListener = (e) => {
491
+ const detail = e.detail;
492
+ if (detail) this.project = detail;
493
+ };
494
+ el.addEventListener("change", this._sourceListener);
495
+ }
496
+ /** Fetch shader from URL, then trigger a build. */
497
+ async loadFromUrl(url) {
498
+ try {
499
+ const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(url, this.getConfigOverrides()?.shaderRoot);
500
+ this._weslSrc = weslSrc;
501
+ this._libs = libs;
502
+ if (rootModuleName) this._rootModuleName = rootModuleName;
503
+ this.requestBuild();
504
+ } catch (error) {
505
+ this.handleCompileError(error);
506
+ }
507
+ }
508
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
509
+ requestBuild() {
510
+ this._dirty = true;
511
+ if (!this._building) this.runBuild();
512
+ }
513
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
514
+ async runBuild() {
515
+ this._building = true;
516
+ while (this._dirty) {
517
+ this._dirty = false;
518
+ if (!await this.initialize()) break;
519
+ const mainSource = this._weslSrc[this._rootModuleName];
520
+ if (!mainSource) {
521
+ console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
522
+ continue;
523
+ }
524
+ try {
525
+ this.errorOverlay.hide();
526
+ if (this._fetchSources || this._fetchLibs) {
527
+ const { weslSrc, libs } = await fetchDependencies(mainSource, {
528
+ shaderRoot: this.getConfigOverrides()?.shaderRoot,
529
+ existingSources: this._weslSrc,
530
+ fetchLibs: this._fetchLibs,
531
+ fetchSources: this._fetchSources
532
+ });
533
+ this._weslSrc = {
534
+ ...this._weslSrc,
535
+ ...weslSrc
536
+ };
537
+ this._libs = dedupLibs(this._libs, libs);
538
+ }
539
+ await createPipeline(this.renderState, mainSource, {
540
+ ...this._linkOptions,
541
+ weslSrc: this._weslSrc,
542
+ libs: this._libs,
543
+ rootModuleName: this._rootModuleName
544
+ });
545
+ if (!this._dirty) this.dispatchEvent(new CustomEvent("compile-success"));
546
+ } catch (error) {
547
+ if (!this._dirty) this.handleCompileError(error);
548
+ }
549
+ }
550
+ this._building = false;
551
+ }
552
+ handleCompileError(error) {
553
+ const message = error?.message ?? String(error);
554
+ this.errorOverlay.show(message);
555
+ const detail = {
556
+ message,
557
+ source: error instanceof WeslParseError ? "wesl" : "webgpu",
558
+ locations: this.extractLocations(error)
559
+ };
560
+ this.dispatchEvent(new CustomEvent("compile-error", { detail }));
561
+ }
562
+ /** Extract source locations from a WESL parse error or GPU compilation error. */
563
+ extractLocations(error) {
564
+ const loc = error?.weslLocation;
565
+ if (loc) return [{
566
+ file: loc.file,
567
+ line: loc.line,
568
+ column: loc.column - 1,
569
+ length: loc.length,
570
+ severity: "error",
571
+ message: error?.message ?? ""
572
+ }];
573
+ const msgs = error?.compilationInfo?.messages;
574
+ if (msgs) return msgs.map((m) => ({
575
+ file: m.module?.url,
576
+ line: m.lineNum,
577
+ column: m.linePos - 1,
578
+ length: m.length,
579
+ severity: m.type === "warning" ? "warning" : m.type === "info" ? "info" : "error",
580
+ message: m.message
581
+ }));
582
+ return [];
583
+ }
584
+ };
585
+ function getTemplate() {
586
+ if (!template) {
587
+ template = document.createElement("template");
588
+ template.innerHTML = `<canvas part="canvas"></canvas>`;
589
+ }
590
+ return template;
591
+ }
592
+ function getStyles() {
593
+ if (!styles) {
594
+ styles = new CSSStyleSheet();
595
+ styles.replaceSync(WgslPlay_default);
596
+ }
597
+ return styles;
598
+ }
599
+ /** Absorb instance properties set before custom element upgrade. */
600
+ function upgradeProperty(el, prop) {
601
+ if (Object.hasOwn(el, prop)) {
602
+ const value = el[prop];
603
+ delete el[prop];
604
+ el[prop] = value;
605
+ }
606
+ }
607
+ /** Merge new libs, deduplicating by bundle name. */
608
+ function dedupLibs(existing, newLibs) {
609
+ if (!existing || newLibs.length === 0) return [...existing ?? [], ...newLibs];
610
+ const names = new Set(newLibs.map((b) => b.name));
611
+ return [...existing.filter((b) => !names.has(b.name)), ...newLibs];
612
+ }
613
+ /** Normalize all keys in a weslSrc record to module paths. */
614
+ function toModulePaths(weslSrc, pkg) {
615
+ const result = {};
616
+ for (const [key, value] of Object.entries(weslSrc)) result[fileToModulePath(key, pkg, false)] = value;
617
+ return result;
618
+ }
619
+ //#endregion
620
+ export { resetConfig as i, defaults as n, getConfig as r, WgslPlay as t };