wgsl-play 0.0.31 → 0.0.33
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 +4 -6
- package/dist/Config-BV15Y2ZJ.d.ts +14 -0
- package/dist/WgslPlay-Ben4VdsC.js +203 -0
- package/dist/WgslPlay.d.ts +106 -1
- package/dist/WgslPlay.js +451 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/wgsl-play.js +655 -638
- package/package.json +4 -4
- package/src/Renderer.ts +2 -25
- package/src/WgslPlay.ts +61 -7
- package/src/test/WgslPlay.e2e.ts +55 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/connect-conditions-green-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/connect-conditions-red-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/connect-source-external-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/connect-source-multifile-chromium-darwin.png +0 -0
- package/dist/WgslPlay-B5UCLKGK.js +0 -629
- package/dist/WgslPlay-BUiSUZem.d.ts +0 -115
package/README.md
CHANGED
|
@@ -18,12 +18,10 @@ That's it. The component auto-fetches dependencies and starts animating.
|
|
|
18
18
|
accepts **fragment shaders**. Write a single `@fragment` function.
|
|
19
19
|
WESL extensions are supported (imports, conditional compilation).
|
|
20
20
|
|
|
21
|
-
Standard uniforms are
|
|
21
|
+
Standard uniforms are available via `env::u`:
|
|
22
22
|
|
|
23
23
|
```wgsl
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
24
|
+
import env::u;
|
|
27
25
|
|
|
28
26
|
@fragment fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
29
27
|
let uv = pos.xy / u.resolution;
|
|
@@ -44,8 +42,7 @@ You can include shader code inline if you'd prefer. Use a `<script type="text/wg
|
|
|
44
42
|
```html
|
|
45
43
|
<wgsl-play>
|
|
46
44
|
<script type="text/wesl">
|
|
47
|
-
import
|
|
48
|
-
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
45
|
+
import env::u;
|
|
49
46
|
|
|
50
47
|
@fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
51
48
|
let uv = pos.xy / u.resolution;
|
|
@@ -83,6 +80,7 @@ The `?raw` suffix imports the file as a string. This keeps shaders alongside you
|
|
|
83
80
|
- `shader-root` - Root path for internal imports (default: `/shaders`)
|
|
84
81
|
- `autoplay` - Start animating on load (default: `true`). Set `autoplay="false"` to start paused
|
|
85
82
|
- `transparent` - Use premultiplied alpha for transparent backgrounds (default: opaque)
|
|
83
|
+
- `fetch-libs` - Auto-fetch missing libraries from npm (default: `true`). Set `fetch-libs="false"` to disable
|
|
86
84
|
|
|
87
85
|
### Properties
|
|
88
86
|
- `source: string` - Get/set shader source
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/Config.d.ts
|
|
2
|
+
/** Configuration for wgsl-play. */
|
|
3
|
+
interface WgslPlayConfig {
|
|
4
|
+
/** Root path for internal imports (package::, super::). Default: "/shaders" */
|
|
5
|
+
shaderRoot: string;
|
|
6
|
+
}
|
|
7
|
+
/** Set global defaults for all wgsl-play instances. */
|
|
8
|
+
declare function defaults(config: Partial<WgslPlayConfig>): void;
|
|
9
|
+
/** Get resolved config, merging element overrides with global defaults. */
|
|
10
|
+
declare function getConfig(overrides?: Partial<WgslPlayConfig>): WgslPlayConfig;
|
|
11
|
+
/** Reset config to defaults (useful for testing). */
|
|
12
|
+
declare function resetConfig(): void;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { resetConfig as i, defaults as n, getConfig as r, WgslPlayConfig as t };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { requestWeslDevice } from "wesl";
|
|
2
|
+
import { linkAndCreatePipeline, renderFrame, updateRenderUniforms } from "wesl-gpu";
|
|
3
|
+
|
|
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
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/ErrorOverlay.ts
|
|
28
|
+
/** Manages an error overlay element within a shadow DOM. */
|
|
29
|
+
var ErrorOverlay = class {
|
|
30
|
+
el;
|
|
31
|
+
_message = null;
|
|
32
|
+
constructor(container) {
|
|
33
|
+
this.el = document.createElement("div");
|
|
34
|
+
this.el.className = "error-overlay";
|
|
35
|
+
container.appendChild(this.el);
|
|
36
|
+
}
|
|
37
|
+
show(message) {
|
|
38
|
+
this._message = message;
|
|
39
|
+
this.el.textContent = message;
|
|
40
|
+
this.el.classList.add("visible");
|
|
41
|
+
console.error("[wgsl-play]", message);
|
|
42
|
+
}
|
|
43
|
+
hide() {
|
|
44
|
+
this._message = null;
|
|
45
|
+
this.el.classList.remove("visible");
|
|
46
|
+
}
|
|
47
|
+
get visible() {
|
|
48
|
+
return this.el.classList.contains("visible");
|
|
49
|
+
}
|
|
50
|
+
get message() {
|
|
51
|
+
return this._message;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/icons/backToStart.svg?raw
|
|
57
|
+
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";
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/icons/expand.svg?raw
|
|
61
|
+
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";
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/icons/pause.svg?raw
|
|
65
|
+
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";
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/icons/play.svg?raw
|
|
69
|
+
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";
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/icons/shrink.svg?raw
|
|
73
|
+
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";
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/PlaybackControls.ts
|
|
77
|
+
/** Playback controls overlay for wgsl-play. */
|
|
78
|
+
var PlaybackControls = class {
|
|
79
|
+
container;
|
|
80
|
+
playPauseBtn;
|
|
81
|
+
fullscreenBtn;
|
|
82
|
+
playing = true;
|
|
83
|
+
constructor(shadow, onPlay, onPause, onRewind, onFullscreen) {
|
|
84
|
+
this.container = document.createElement("div");
|
|
85
|
+
this.container.className = "controls";
|
|
86
|
+
this.fullscreenBtn = document.createElement("button");
|
|
87
|
+
this.fullscreenBtn.innerHTML = expand_default;
|
|
88
|
+
this.fullscreenBtn.addEventListener("click", onFullscreen);
|
|
89
|
+
const rewindBtn = document.createElement("button");
|
|
90
|
+
rewindBtn.innerHTML = backToStart_default;
|
|
91
|
+
rewindBtn.addEventListener("click", onRewind);
|
|
92
|
+
this.playPauseBtn = document.createElement("button");
|
|
93
|
+
this.playPauseBtn.innerHTML = pause_default;
|
|
94
|
+
this.playPauseBtn.addEventListener("click", () => this.playing ? onPause() : onPlay());
|
|
95
|
+
this.container.append(this.fullscreenBtn, rewindBtn, this.playPauseBtn);
|
|
96
|
+
shadow.appendChild(this.container);
|
|
97
|
+
}
|
|
98
|
+
setPlaying(playing) {
|
|
99
|
+
this.playing = playing;
|
|
100
|
+
this.playPauseBtn.innerHTML = playing ? pause_default : play_default;
|
|
101
|
+
}
|
|
102
|
+
setFullscreen(isFullscreen) {
|
|
103
|
+
this.fullscreenBtn.innerHTML = isFullscreen ? shrink_default : expand_default;
|
|
104
|
+
}
|
|
105
|
+
show() {
|
|
106
|
+
this.container.style.display = "";
|
|
107
|
+
}
|
|
108
|
+
hide() {
|
|
109
|
+
this.container.style.display = "none";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/Renderer.ts
|
|
115
|
+
/** Initialize WebGPU for a canvas element. */
|
|
116
|
+
async function initWebGPU(canvas, alphaMode = "opaque") {
|
|
117
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
118
|
+
if (!adapter) throw new Error("WebGPU adapter not available");
|
|
119
|
+
const device = await requestWeslDevice(adapter);
|
|
120
|
+
const context = canvas.getContext("webgpu");
|
|
121
|
+
if (!context) throw new Error("WebGPU context not available");
|
|
122
|
+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
|
123
|
+
context.configure({
|
|
124
|
+
device,
|
|
125
|
+
format: presentationFormat,
|
|
126
|
+
alphaMode
|
|
127
|
+
});
|
|
128
|
+
const uniformBuffer = device.createBuffer({
|
|
129
|
+
size: 32,
|
|
130
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
131
|
+
});
|
|
132
|
+
const bindGroupLayout = device.createBindGroupLayout({ entries: [{
|
|
133
|
+
binding: 0,
|
|
134
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
135
|
+
buffer: {}
|
|
136
|
+
}] });
|
|
137
|
+
return {
|
|
138
|
+
device,
|
|
139
|
+
canvas,
|
|
140
|
+
context,
|
|
141
|
+
presentationFormat,
|
|
142
|
+
uniformBuffer,
|
|
143
|
+
pipelineLayout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
|
|
144
|
+
bindGroup: device.createBindGroup({
|
|
145
|
+
layout: bindGroupLayout,
|
|
146
|
+
entries: [{
|
|
147
|
+
binding: 0,
|
|
148
|
+
resource: { buffer: uniformBuffer }
|
|
149
|
+
}]
|
|
150
|
+
}),
|
|
151
|
+
frameCount: 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Compile WESL fragment shader and create render pipeline. */
|
|
155
|
+
async function createPipeline(state, fragmentSource, options) {
|
|
156
|
+
state.device.pushErrorScope("validation");
|
|
157
|
+
const pipeline = await linkAndCreatePipeline({
|
|
158
|
+
device: state.device,
|
|
159
|
+
fragmentSource,
|
|
160
|
+
format: state.presentationFormat,
|
|
161
|
+
layout: state.pipelineLayout,
|
|
162
|
+
...options
|
|
163
|
+
});
|
|
164
|
+
const gpuError = await state.device.popErrorScope();
|
|
165
|
+
if (gpuError) {
|
|
166
|
+
state.pipeline = void 0;
|
|
167
|
+
throw gpuError;
|
|
168
|
+
}
|
|
169
|
+
state.pipeline = pipeline;
|
|
170
|
+
}
|
|
171
|
+
/** Start the render loop. Returns a stop function. */
|
|
172
|
+
function startRenderLoop(state, playback) {
|
|
173
|
+
let animationId;
|
|
174
|
+
function render() {
|
|
175
|
+
if (!state.pipeline) {
|
|
176
|
+
animationId = requestAnimationFrame(render);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const time = calculateTime(playback);
|
|
180
|
+
const resolution = [state.canvas.width, state.canvas.height];
|
|
181
|
+
updateRenderUniforms(state.uniformBuffer, state.device, resolution, time, [0, 0]);
|
|
182
|
+
renderFrame({
|
|
183
|
+
device: state.device,
|
|
184
|
+
pipeline: state.pipeline,
|
|
185
|
+
bindGroup: state.bindGroup,
|
|
186
|
+
targetView: state.context.getCurrentTexture().createView()
|
|
187
|
+
});
|
|
188
|
+
state.frameCount++;
|
|
189
|
+
animationId = requestAnimationFrame(render);
|
|
190
|
+
}
|
|
191
|
+
animationId = requestAnimationFrame(render);
|
|
192
|
+
return () => cancelAnimationFrame(animationId);
|
|
193
|
+
}
|
|
194
|
+
function calculateTime(playback) {
|
|
195
|
+
return ((playback.isPlaying ? performance.now() : playback.startTime + playback.pausedDuration) - playback.startTime) / 1e3;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/WgslPlay.css?inline
|
|
200
|
+
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";
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
export { PlaybackControls as a, getConfig as c, startRenderLoop as i, resetConfig as l, createPipeline as n, ErrorOverlay as o, initWebGPU as r, defaults as s, WgslPlay_default as t };
|
package/dist/WgslPlay.d.ts
CHANGED
|
@@ -1,2 +1,107 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as resetConfig, n as defaults, r as getConfig } from "./Config-BV15Y2ZJ.js";
|
|
2
|
+
import { Conditions, LinkParams } from "wesl";
|
|
3
|
+
|
|
4
|
+
//#region src/WgslPlay.d.ts
|
|
5
|
+
/** Project configuration for multi-file shaders (subset of wesl link() API). */
|
|
6
|
+
type WeslProject = Pick<LinkParams, "weslSrc" | "rootModuleName" | "conditions" | "constants" | "libs" | "packageName">;
|
|
7
|
+
/** One source location within a compile error. */
|
|
8
|
+
interface CompileErrorLocation {
|
|
9
|
+
file?: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
length?: number;
|
|
13
|
+
severity: "error" | "warning" | "info";
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
/** Compile error detail for events. */
|
|
17
|
+
interface CompileErrorDetail {
|
|
18
|
+
message: string;
|
|
19
|
+
source: "wesl" | "webgpu";
|
|
20
|
+
locations: CompileErrorLocation[];
|
|
21
|
+
}
|
|
22
|
+
/** <wgsl-play> web component for rendering WESL/WGSL fragment shaders. */
|
|
23
|
+
declare class WgslPlay extends HTMLElement {
|
|
24
|
+
static observedAttributes: string[];
|
|
25
|
+
private canvas;
|
|
26
|
+
private errorOverlay;
|
|
27
|
+
private controls;
|
|
28
|
+
private resizeObserver;
|
|
29
|
+
private stopRenderLoop?;
|
|
30
|
+
private renderState?;
|
|
31
|
+
private playback;
|
|
32
|
+
private _weslSrc;
|
|
33
|
+
private _rootModuleName;
|
|
34
|
+
private _libs?;
|
|
35
|
+
private _linkOptions;
|
|
36
|
+
private _fromFullProject;
|
|
37
|
+
private _initPromise?;
|
|
38
|
+
private _sourceEl;
|
|
39
|
+
private _sourceListener;
|
|
40
|
+
private _fetchLibs;
|
|
41
|
+
private _theme;
|
|
42
|
+
private _mediaQuery;
|
|
43
|
+
private _onFullscreenChange;
|
|
44
|
+
/** Get config overrides from element attributes. */
|
|
45
|
+
private getConfigOverrides;
|
|
46
|
+
constructor();
|
|
47
|
+
connectedCallback(): void;
|
|
48
|
+
disconnectedCallback(): void;
|
|
49
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
50
|
+
/** Current shader source code (main module). */
|
|
51
|
+
get source(): string;
|
|
52
|
+
/** Set shader source directly. */
|
|
53
|
+
set source(value: string);
|
|
54
|
+
/** Conditions for conditional compilation (@if/@elif/@else). */
|
|
55
|
+
get conditions(): Conditions;
|
|
56
|
+
set conditions(value: Conditions);
|
|
57
|
+
/** Set project configuration (mirrors wesl link() API). */
|
|
58
|
+
set project(value: WeslProject);
|
|
59
|
+
/** Set sources from a full project with weslSrc. */
|
|
60
|
+
private setProjectSources;
|
|
61
|
+
/** Whether to auto-fetch missing library packages from npm (default: true). */
|
|
62
|
+
get fetchLibs(): boolean;
|
|
63
|
+
set fetchLibs(value: boolean);
|
|
64
|
+
/** Whether autoplay is enabled (default: true). Set autoplay="false" to start paused. */
|
|
65
|
+
get autoplay(): boolean;
|
|
66
|
+
set autoplay(value: boolean | string);
|
|
67
|
+
/** Whether the shader is currently playing. */
|
|
68
|
+
get isPlaying(): boolean;
|
|
69
|
+
/** Current animation time in seconds. */
|
|
70
|
+
get time(): number;
|
|
71
|
+
/** Number of frames rendered (for testing/debugging). */
|
|
72
|
+
get frameCount(): number;
|
|
73
|
+
/** Whether there's a compilation error. */
|
|
74
|
+
get hasError(): boolean;
|
|
75
|
+
/** Current error message, or null if no error. */
|
|
76
|
+
get errorMessage(): string | null;
|
|
77
|
+
/** Start playback. */
|
|
78
|
+
play(): void;
|
|
79
|
+
/** Pause playback. */
|
|
80
|
+
pause(): void;
|
|
81
|
+
private setPlaying;
|
|
82
|
+
/** Reset animation to time 0 and pause. */
|
|
83
|
+
rewind(): void;
|
|
84
|
+
/** Display error message in overlay. Pass empty string to clear. */
|
|
85
|
+
showError(message: string): void;
|
|
86
|
+
/** Toggle fullscreen on this element. */
|
|
87
|
+
toggleFullscreen(): void;
|
|
88
|
+
private updateTheme;
|
|
89
|
+
/** Set up WebGPU and load initial shader. Returns true if successful. */
|
|
90
|
+
private initialize;
|
|
91
|
+
private doInitialize;
|
|
92
|
+
/** Load from source element, src URL, script child, or inline textContent. */
|
|
93
|
+
private loadInitialContent;
|
|
94
|
+
/** Connect to a source provider element (e.g., wgsl-edit). */
|
|
95
|
+
private connectToSource;
|
|
96
|
+
/** Fetch shader from URL, auto-fetching any imported dependencies. */
|
|
97
|
+
private loadFromUrl;
|
|
98
|
+
/** Rebuild GPU pipeline using stored state. For full projects with all sources. */
|
|
99
|
+
private rebuildPipeline;
|
|
100
|
+
/** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
|
|
101
|
+
private discoverAndRebuild;
|
|
102
|
+
private handleCompileError;
|
|
103
|
+
/** Extract source locations from a WESL parse error or GPU compilation error. */
|
|
104
|
+
private extractLocations;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
2
107
|
export { CompileErrorDetail, CompileErrorLocation, WeslProject, WgslPlay, defaults, getConfig, resetConfig };
|