instantshader 0.5.0 → 0.6.1

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
@@ -3,7 +3,8 @@
3
3
  # instantshader
4
4
 
5
5
  Animated WebGL gradient shaders with zero dependencies. Mount a live, resizable
6
- gradient into any DOM element, or render a single frame to a detached canvas
6
+ gradient into any DOM element, put dither, pixelate, halftone or ASCII effects
7
+ over it or over your own image, or render a single frame to a detached canvas
7
8
  for export pipelines. Built by [InstantGradient](https://instantgradient.com/app).
8
9
 
9
10
  ## Install
@@ -51,21 +52,58 @@ import { mountGradient, bloom } from "instantshader";
51
52
  mountGradient(el, {
52
53
  shader: bloom,
53
54
  colors: ["#ffd9e8", "#ffb300", "#ff8fc0", "#3d7bff", "#0a2e14"],
54
- // bloom has two independent motion modes — petals that breathe and lean,
55
+ // bloom has two independent motion modes: petals that breathe and lean,
55
56
  // and colours that travel outward through a still pattern. Mix freely.
56
57
  params: { sway: 0.5, colorflow: 0.4 },
57
58
  });
58
59
  ```
59
60
 
60
- Ranges, defaults and labels are all discoverable at runtime — `shader.params`
61
+ Ranges, defaults and labels are all discoverable at runtime. `shader.params`
61
62
  is an array of `ParamDef`, and `shader.randomParams(rand)` produces a full,
62
63
  sensible param set for "randomize" flows. `shaders` and `getShader(id)` expose
63
64
  the whole registry (importing either pulls every shader).
64
65
 
66
+ ## Effects
67
+
68
+ An effect redraws a picture: a shader's output, or an image, canvas or video
69
+ frame you supply. Four are included: `pixelate`, `dither`, `halftone` and
70
+ `ascii`.
71
+
72
+ ```ts
73
+ import { mountStack, bloom, dither } from "instantshader";
74
+
75
+ mountStack(el, {
76
+ source: { kind: "generator", shader: bloom },
77
+ colors: ["#140f30", "#9c2168", "#eb6a4e", "#fcd87c"],
78
+ effects: [{ effect: dither, params: { pattern: "blueNoise", colorMode: "palette", levels: 4 } }],
79
+ loopSeconds: 30,
80
+ });
81
+ ```
82
+
83
+ For an image, the source is `{ kind: "media", media: img }` with an optional
84
+ `fit` of `"cover"` (default) or `"contain"`. `effects` is a list, bottom
85
+ layer first. `mountStack` returns the `mountGradient` handle plus
86
+ `setSource`, `setSourceParams`, `setEffects`, `setEffectParams(index, params)`,
87
+ `refreshMedia()` and `getGridInfo()`. `renderStackFrame` renders one frame to
88
+ a detached canvas and `createStackRenderer` is the seekable renderer for
89
+ video export, both with the same options.
90
+
91
+ Each effect's `params` array describes its controls the way `shader.params`
92
+ does, with four kinds: float, enum (a string value from `options`), bool and
93
+ colour (a `#rrggbb` string). A `when` field on a param says which other
94
+ param's value makes it relevant, for building UI. `effects` and
95
+ `getEffect(id)` expose the registry, and importing either pulls every effect.
96
+
97
+ Effect sizes are in pixels at 1080p, and each effect computes one value per
98
+ cell into a buffer that is the same at every output size, so a preview and a
99
+ 4K export contain identical cells. The [repository
100
+ README](https://github.com/ugolbck/instantshader#effects) has images, every
101
+ param with its range, and the details of how preview and export line up.
102
+
65
103
  ## Seamless loops
66
104
 
67
105
  Set `loopSeconds` and the animation repeats exactly, with no visible seam at
68
- the wrap — the frame at `t` and at `t + loopSeconds` are identical pixel for
106
+ the wrap. The frame at `t` and at `t + loopSeconds` are identical pixel for
69
107
  pixel. Built for video export and for backgrounds that must not betray a
70
108
  restart.
71
109
 
@@ -100,7 +138,7 @@ Notes:
100
138
  light video file without slowing the animation down.
101
139
  - **`flow` ties its travel speed to the loop length.** It animates by
102
140
  translating in a straight line through a noise field that tiles, and it
103
- covers exactly one tile per cycle — so a short loop flows fast and a long
141
+ covers exactly one tile per cycle, so a short loop flows fast and a long
104
142
  one flows slowly. The hand-tuned drift rate corresponds to a period around
105
143
  60–90s; below ~30s the currents move noticeably faster than the look was
106
144
  designed for. Compensate with `speed` rather than by shortening the loop.
@@ -111,7 +149,7 @@ Notes:
111
149
  slow oscillations (petal breathing ~20s, fan lean ~30s, petal flex ~42s)
112
150
  layered over a noise wander. Each oscillation holds still once the loop is
113
151
  shorter than about half its own period, so below ~10s the wander is the only
114
- thing left moving. `colorflow` is unaffected — it always fits at least one
152
+ thing left moving. `colorflow` is unaffected. It always fits at least one
115
153
  full cycle into the loop, flowing faster on a short one.
116
154
  - `halo`, `dune` and `whorl` always complete at least one full cycle of their
117
155
  main motion per loop, so a short loop simply runs them faster.
package/dist/index.d.ts CHANGED
@@ -12,6 +12,29 @@ type Renderer = {
12
12
  };
13
13
  declare function createRenderer(opts: RendererOptions): Renderer;
14
14
  //#endregion
15
+ //#region src/stack.d.ts
16
+ type StackRenderer = {
17
+ renderAt(timeMs: number): void;
18
+ setColors(colors: string[]): void;
19
+ /** Swaps the source. Recompiles only when the generator shader changed. */
20
+ setSource(source: Source): void;
21
+ /** Replaces the generator source's params. No-op for a media source. */
22
+ setSourceParams(params: Record<string, number>): void;
23
+ /** Re-uploads the media element's current pixels, e.g. after a video
24
+ * element advanced a frame. */
25
+ refreshMedia(): void;
26
+ /** Replaces the layer list. Layers whose EffectDef is unchanged at the same
27
+ * position keep their compiled programs. */
28
+ setEffects(layers: EffectLayer[]): void;
29
+ /** Merges params into one layer. */
30
+ setEffectParams(index: number, params: Record<string, ParamValue>): void;
31
+ setLoopSeconds(seconds: number | undefined): void;
32
+ resize(width: number, height: number): void;
33
+ getGridInfo(): GridInfo[];
34
+ dispose(): void;
35
+ };
36
+ declare function createStackRenderer(opts: StackOptions): StackRenderer;
37
+ //#endregion
15
38
  //#region src/types.d.ts
16
39
  /**
17
40
  * Describes a single tunable knob exposed by a shader (e.g. "frequency",
@@ -116,6 +139,185 @@ type RendererOptions = {
116
139
  * See MountOptions.loopSeconds. */
117
140
  loopSeconds?: number;
118
141
  };
142
+ /** A generator is what this library has always called a shader. */
143
+ type GeneratorDef = ShaderDef;
144
+ /** Effect params are wider than generator params: a dither needs a pattern
145
+ * choice and two colors, which floats cannot express. */
146
+ type ParamValue = number | boolean | string;
147
+ type EffectParamBase = {
148
+ /** Param identifier. The GLSL uniform name is always "u_" + key. */
149
+ key: string;
150
+ label: string;
151
+ /** UI hint: show this control only while another param holds one of these
152
+ * values. The runtime ignores it. */
153
+ when?: {
154
+ key: string;
155
+ in: ParamValue[];
156
+ };
157
+ };
158
+ /** `type` is optional so that a generator's ParamDef is a valid FloatParamDef. */
159
+ type FloatParamDef = EffectParamBase & {
160
+ type?: "float";
161
+ min: number;
162
+ max: number;
163
+ step: number;
164
+ default: number;
165
+ };
166
+ /** Values are strings so that reordering `options` never changes a saved
167
+ * design. The uniform is a float holding the option's index. */
168
+ type EnumParamDef = EffectParamBase & {
169
+ type: "enum";
170
+ options: {
171
+ value: string;
172
+ label: string;
173
+ }[];
174
+ default: string;
175
+ };
176
+ /** Uniform is a float, 0 or 1. */
177
+ type BoolParamDef = EffectParamBase & {
178
+ type: "bool";
179
+ default: boolean;
180
+ };
181
+ /** "#rrggbb". Uniform is a vec3 of gamma-encoded sRGB in 0-1. */
182
+ type ColorParamDef = EffectParamBase & {
183
+ type: "color";
184
+ default: string;
185
+ };
186
+ type EffectParamDef = FloatParamDef | EnumParamDef | BoolParamDef | ColorParamDef;
187
+ /** What an EffectTexture's key() and build() get to look at. */
188
+ type TextureEnv = {
189
+ /** The layer's fully resolved params. */
190
+ params: Record<string, ParamValue>;
191
+ /** Frame size in device pixels. */
192
+ width: number;
193
+ height: number;
194
+ /** Device pixels per reference pixel. */
195
+ outputScale: number;
196
+ /** From StackOptions.fontFamily, for effects that draw text. */
197
+ fontFamily: string;
198
+ };
199
+ type TextureData = {
200
+ /** Raw bytes are read as one channel when data.length === width * height,
201
+ * as RGBA when it is four times that. */
202
+ source: TexImageSource | {
203
+ data: Uint8Array;
204
+ width: number;
205
+ height: number;
206
+ };
207
+ filter: "nearest" | "linear";
208
+ /** "repeat" is only legal for power-of-two sizes in WebGL1. */
209
+ wrap: "clamp" | "repeat";
210
+ /** Extra uniforms describing the texture, e.g. an atlas's grid and glyph
211
+ * count. A number sets a float, an array a vec2/vec3/vec4. */
212
+ uniforms?: Record<string, number | number[]>;
213
+ };
214
+ /** An extra texture an effect needs, built by JS: a glyph atlas, a noise tile. */
215
+ type EffectTexture = {
216
+ /** Sampler uniform name, e.g. "u_atlas". */
217
+ uniform: string;
218
+ /** Cache key. The runtime rebuilds the texture when this changes. */
219
+ key: (env: TextureEnv) => string;
220
+ build: (env: TextureEnv) => TextureData;
221
+ };
222
+ /**
223
+ * A registered effect. Two shapes:
224
+ *
225
+ * - SOFT effect: only `fragment`. Runs once per output pixel and reads the
226
+ * layer below as `u_source`.
227
+ * - GRID effect: has `grid`. `grid.fragment` is the CELL STAGE, run once per
228
+ * cell into a small cell buffer whose size depends on params and aspect
229
+ * ratio only. `fragment` is then the DRAW STAGE that paints that buffer
230
+ * onto the frame; omit it to get the default coverage upscale.
231
+ *
232
+ * The cell buffer is what makes a 900px preview and a 3840px export contain
233
+ * the same cells. See SPEC-effects.md section 6.
234
+ */
235
+ type EffectDef = {
236
+ id: string;
237
+ label: string;
238
+ params: EffectParamDef[];
239
+ fragment?: string;
240
+ grid?: {
241
+ /** Cell size in reference pixels, from the layer's resolved params.
242
+ * Rounded to whole reference pixels unless `lattice` is given. */
243
+ cell: (params: Record<string, ParamValue>) => [number, number];
244
+ /**
245
+ * Makes the grid a LATTICE: rotated by `angle` degrees about the frame
246
+ * centre, with an unrounded pitch. For effects whose draw stage paints
247
+ * soft antialiased shapes (halftone) rather than filling cells, so that
248
+ * nothing needs to land on whole pixels. coverage() is meaningless for a
249
+ * lattice; such an effect must supply its own draw `fragment`.
250
+ */
251
+ lattice?: (params: Record<string, ParamValue>) => {
252
+ angle: number;
253
+ };
254
+ fragment: string;
255
+ };
256
+ textures?: EffectTexture[];
257
+ /** Same contract as ShaderDef.randomParams. */
258
+ randomParams: (rand: () => number) => Record<string, ParamValue>;
259
+ };
260
+ /** The bottom of a stack. */
261
+ type Source = {
262
+ kind: "generator";
263
+ shader: ShaderDef;
264
+ params?: Record<string, number>;
265
+ } | {
266
+ kind: "media";
267
+ media: TexImageSource;
268
+ fit?: "cover" | "contain";
269
+ };
270
+ /** One effect in a stack. */
271
+ type EffectLayer = {
272
+ effect: EffectDef;
273
+ /** Overrides; unset keys fall back to the param's default. */
274
+ params?: Record<string, ParamValue>;
275
+ /** Defaults to true. A disabled layer renders as if it were absent. */
276
+ enabled?: boolean;
277
+ };
278
+ type StackOptions = {
279
+ canvas: HTMLCanvasElement;
280
+ source: Source;
281
+ effects?: EffectLayer[];
282
+ /** Hex color stops of the palette ramp. Generators draw with it; effects
283
+ * in "palette" color mode map tone through it. */
284
+ colors: string[];
285
+ seed?: number;
286
+ /** See MountOptions.loopSeconds. Shared by the source and every layer. */
287
+ loopSeconds?: number;
288
+ /** Shown behind contain-fit media and under media alpha. Default black. */
289
+ background?: string;
290
+ /** CSS font-family for effects that draw text. The host must have loaded
291
+ * it (document.fonts.load) before creating the stack. */
292
+ fontFamily?: string;
293
+ };
294
+ /** Per grid layer: how the grid lands on the current frame. A host can use
295
+ * pxPerCell to warn that a small preview cannot resolve a fine pattern. */
296
+ type GridInfo = {
297
+ /** Index into the layer list. */
298
+ index: number;
299
+ cols: number;
300
+ rows: number;
301
+ pxPerCell: [number, number];
302
+ };
303
+ /** Options accepted by mountStack(): StackOptions without the canvas (the
304
+ * mount owns one), plus playback speed. */
305
+ type StackMountOptions = Omit<StackOptions, "canvas"> & {
306
+ /** Animation speed multiplier. Defaults to 1. */
307
+ speed?: number;
308
+ };
309
+ /** Live handle returned by mountStack(). */
310
+ type StackHandle = Omit<MountHandle, "setParams"> & {
311
+ setSource(source: Source): void;
312
+ /** Merges params into a generator source. No-op for a media source. */
313
+ setSourceParams(params: Record<string, number>): void;
314
+ setEffects(layers: EffectLayer[]): void;
315
+ /** Merges params into one layer. */
316
+ setEffectParams(index: number, params: Record<string, ParamValue>): void;
317
+ /** Re-uploads the media element's current pixels. */
318
+ refreshMedia(): void;
319
+ getGridInfo(): GridInfo[];
320
+ };
119
321
  //#endregion
120
322
  //#region src/shaders/flow.d.ts
121
323
  declare const flow: ShaderDef;
@@ -138,13 +340,29 @@ declare const dune: ShaderDef;
138
340
  //#region src/shaders/whorl.d.ts
139
341
  declare const whorl: ShaderDef;
140
342
  //#endregion
343
+ //#region src/effects/pixelate.d.ts
344
+ declare const pixelate: EffectDef;
345
+ //#endregion
346
+ //#region src/effects/dither.d.ts
347
+ declare const dither: EffectDef;
348
+ //#endregion
349
+ //#region src/effects/halftone.d.ts
350
+ declare const halftone: EffectDef;
351
+ //#endregion
352
+ //#region src/effects/ascii.d.ts
353
+ declare const ascii: EffectDef;
354
+ //#endregion
141
355
  //#region src/registry.d.ts
142
356
  declare const shaders: readonly ShaderDef[];
143
357
  declare function getShader(id: string): ShaderDef | undefined;
144
358
  //#endregion
359
+ //#region src/effectRegistry.d.ts
360
+ declare const effects: readonly EffectDef[];
361
+ declare function getEffect(id: string): EffectDef | undefined;
362
+ //#endregion
145
363
  //#region src/mount.d.ts
146
364
  /**
147
- * Mounts a live, animated gradient into `container` and returns a handle to
365
+ * Mounts a live, animated stack (a source plus effect layers) into `container` and returns a handle to
148
366
  * control it. Owns a canvas (sized to the container via ResizeObserver, DPR
149
367
  * capped at 2 to bound fill-rate cost on high-density displays) and a RAF
150
368
  * loop that runs ONLY while playing — the same lifecycle used by the
@@ -159,19 +377,35 @@ declare function getShader(id: string): ShaderDef | undefined;
159
377
  * callers may mount many simultaneously-paused instances (e.g. a screenshot
160
378
  * grid) that must never carry a perpetual 60fps draw loop each.
161
379
  */
380
+ declare function mountStack(container: HTMLElement, opts: StackMountOptions): StackHandle;
381
+ /** mountStack() for the common case of one generator and no effects. */
162
382
  declare function mountGradient(container: HTMLElement, opts: MountOptions): MountHandle;
163
383
  //#endregion
164
384
  //#region src/frame.d.ts
165
385
  /**
166
- * Renders a single frame into a detached (not-in-DOM) canvas at an exact
167
- * pixel size, for export/thumbnail use cases that need a synchronous
168
- * snapshot rather than a live animation.
386
+ * Renders a single frame of a stack (a source plus effect layers) into a
387
+ * detached (not-in-DOM) canvas at an exact pixel size, for export/thumbnail
388
+ * use cases that need a synchronous snapshot rather than a live animation.
169
389
  *
170
390
  * The returned canvas is NOT disposed automatically — its GL context must
171
391
  * stay alive after this function returns so callers can scrape pixels from
172
392
  * it (toDataURL/toBlob/getImageData/drawImage). Once the caller is done
173
393
  * with it, release the GL context by calling the returned `dispose()`.
174
394
  */
395
+ declare function renderStackFrame(opts: {
396
+ source: Source;
397
+ effects?: EffectLayer[];
398
+ colors: string[];
399
+ seed?: number;
400
+ timeMs?: number;
401
+ /** See MountOptions.loopSeconds. */
402
+ loopSeconds?: number;
403
+ background?: string;
404
+ fontFamily?: string;
405
+ width: number;
406
+ height: number;
407
+ }): RenderFrameResult;
408
+ /** renderStackFrame() for the common case of one generator and no effects. */
175
409
  declare function renderGradientFrame(opts: {
176
410
  shader: ShaderDef;
177
411
  colors: string[];
@@ -187,6 +421,42 @@ declare function renderGradientFrame(opts: {
187
421
  height: number;
188
422
  }): RenderFrameResult;
189
423
  //#endregion
424
+ //#region src/grid.d.ts
425
+ type Grid = {
426
+ cols: number;
427
+ rows: number;
428
+ /** Cell size in reference pixels. */
429
+ cell: [number, number];
430
+ /** Frame size in reference pixels. */
431
+ ref: [number, number];
432
+ /** Output pixels per cell on each axis. Whole numbers mean an exact export. */
433
+ pxPerCell: [number, number];
434
+ /** Rotation of the grid about the frame centre, in radians. 0 unless the
435
+ * effect asked for a lattice. */
436
+ angle: number;
437
+ /** How far an unrotated grid extends past the frame, as a multiple of the
438
+ * frame (>= 1 on each axis). Partial cells at the edges make this exceed 1. */
439
+ extent: [number, number];
440
+ };
441
+ /**
442
+ * The grid a cell size produces on a frame. Centred on the frame with a cell
443
+ * boundary at the centre, so cols and rows are always even and a different
444
+ * aspect ratio crops the grid evenly. Depends on the frame's aspect ratio
445
+ * and the cell size only, never on its pixel dimensions.
446
+ *
447
+ * Two flavours:
448
+ *
449
+ * - default: cells are whole reference pixels and axis-aligned, so that
450
+ * 1080p-class and 4K-class exports get whole output pixels per cell.
451
+ * - `lattice`: for effects that draw soft, antialiased shapes (halftone).
452
+ * The grid may be rotated and its pitch need not be whole, since nothing
453
+ * snaps to pixels; it is sized to cover the rotated frame plus a margin of
454
+ * two cells, because a shape can spill into its neighbours' cells.
455
+ */
456
+ declare function gridFor(width: number, height: number, cell: [number, number], lattice?: {
457
+ angle: number;
458
+ }): Grid;
459
+ //#endregion
190
460
  //#region src/palette.d.ts
191
461
  /**
192
462
  * Builds a 1024-texel RGBA ramp (Uint8Array, length 1024*4) by interpolating
@@ -203,4 +473,4 @@ declare function renderGradientFrame(opts: {
203
473
  */
204
474
  declare function buildPaletteRamp(colors: string[]): Uint8Array;
205
475
  //#endregion
206
- export { type MountHandle, type MountOptions, type ParamDef, type RenderFrameResult, type Renderer, type RendererOptions, type ShaderDef, beam, bloom, buildPaletteRamp, createRenderer, dune, flow, getShader, halo, mountGradient, renderGradientFrame, shaders, strata, whorl };
476
+ export { type BoolParamDef, type ColorParamDef, type EffectDef, type EffectLayer, type EffectParamDef, type EffectTexture, type EnumParamDef, type FloatParamDef, type GeneratorDef, type Grid, type GridInfo, type MountHandle, type MountOptions, type ParamDef, type ParamValue, type RenderFrameResult, type Renderer, type RendererOptions, type ShaderDef, type Source, type StackHandle, type StackMountOptions, type StackOptions, type StackRenderer, type TextureData, type TextureEnv, ascii, beam, bloom, buildPaletteRamp, createRenderer, createStackRenderer, dither, dune, effects, flow, getEffect, getShader, gridFor, halftone, halo, mountGradient, mountStack, pixelate, renderGradientFrame, renderStackFrame, shaders, strata, whorl };