summaryrefslogtreecommitdiff
path: root/pugl.js
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2023-08-23 22:14:38 -0400
committerpommicket <pommicket@gmail.com>2023-08-23 22:14:38 -0400
commitc841d1c47ae0679bff6b8138cf52eec61b2cd5a0 (patch)
tree88c67ebbeb960e12025f773643c46ce665e83298 /pugl.js
parent5a6ed46aaf5ac3402085d05ef0f31bd92771692f (diff)
rename
Diffstat (limited to 'pugl.js')
-rw-r--r--pugl.js2690
1 files changed, 2690 insertions, 0 deletions
diff --git a/pugl.js b/pugl.js
new file mode 100644
index 0000000..19333c9
--- /dev/null
+++ b/pugl.js
@@ -0,0 +1,2690 @@
+'use strict';
+
+/*
+TODO:
+- pause
+- settings:
+ - enable/disable auto-update
+ - resolution
+- mouse pos uniform
+*/
+
+const APP_ID = 'dh3YgVZQdX1Q';
+
+let gl;
+let program_main = null;
+let program_post = null;
+let vertex_buffer_rect;
+let vertex_buffer_main;
+let canvas_container;
+let canvas;
+let framebuffer;
+let framebuffer_color_texture;
+let sampler_texture;
+let current_time;
+let ui_shown = true;
+let ui_div;
+let ui_resize;
+let viewport_width, viewport_height;
+let next_html_id = 1;
+let next_widget_id = 1;
+let widget_choices;
+let widget_search;
+let widgets_container;
+let code_input;
+let error_element;
+let parsed_widgets;
+
+const render_width = 1080,
+ render_height = 1080;
+const GLSL_FLOAT_TYPES = ['float', 'vec2', 'vec3', 'vec4'];
+const GLSL_FLOAT_TYPE_PAIRS = GLSL_FLOAT_TYPES.flatMap((x) =>
+ GLSL_FLOAT_TYPES.map((y) => [x, y]),
+);
+
+const builtin_widgets = [
+ `
+//! .name: Buffer
+//! .category: basic
+//! .description: outputs its input unaltered. useful for defining constants.
+//! x.name: input
+//! x.id: input
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} buffer(${type} x) {
+ return x;
+}`,
+ ).join('\n'),
+ `
+//! .name: Slider
+//! .category: basic
+//! .description: an adjustable slider between two values.
+//! x.id: x
+//! x.default: 0.5
+//! x.control: slider
+//! min_val.id: min
+//! min_val.default: 0
+//! max_val.id: max
+//! max_val.default: 1
+
+float slider(float x, float min_val, float max_val) {
+ return mix(min_val, max_val, x);
+}
+`,
+ `
+//! .name: Mix (lerp)
+//! .category: basic
+//! .id: mix
+//! .description: weighted average of two inputs
+//! a.name: source 1
+//! a.default: 0
+//! b.name: source 2
+//! b.default: 1
+//! x.name: mix
+//! x.default: 0.5
+//! c.name: clamp mix
+//! c.control: checkbox
+//! c.description: clamp the mix input to the [0, 1] range
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} mix_(${type} a, ${type} b, ${type} x, int c) {
+ if (c != 0) x = clamp(x, 0.0, 1.0);
+ return mix(a, b, x);
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Last frame
+//! .category: basic
+//! .id: prev
+//! .description: sample from the previous frame
+//! pos.description: position to sample — bottom-left corner is (−1, −1), top-right corner is (1, 1)
+//! wrap.name: wrap mode
+//! wrap.control: select:clamp|wrap
+//! wrap.description: how to deal with the input components if they go outside [−1, 1]
+//! samp.id: sample
+//! samp.name: sample mode
+//! samp.control: select:linear|nearest
+//! samp.description: how positions in between pixels should be sampled
+
+vec3 last_frame(vec2 pos, int wrap, int samp) {
+ pos = pos * 0.5 + 0.5;
+ if (wrap == 0)
+ pos = clamp(pos, 0.0, 1.0);
+ else if (wrap == 1)
+ pos = mod(pos, 1.0);
+ if (samp == 1)
+ pos = floor(0.5 + pos * _texture_size) * (1.0 / _texture_size);
+ return texture(_texture, pos).xyz;
+}
+`,
+ `
+//! .name: Weighted add
+//! .category: math
+//! .description: add two numbers or vectors with weights
+//! aw.name: a weight
+//! aw.default: 1
+//! bw.name: b weight
+//! bw.default: 1
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} wtadd(${type} a, float aw, ${type} b, float bw) {
+ return a * aw + b * bw;
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Multiply
+//! .category: math
+//! .description: multiply two numbers, scale a vector by a number, or perform component-wise multiplication between vectors
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} mul(${type} a, ${type} b) {
+ return a * b;
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Power
+//! .category: math
+//! .id: pow
+//! .description: take one number to the power of another
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} pow_(${type} a, ${type} b) {
+ return pow(a, b);
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Modulo
+//! .category: math
+//! .id: mod
+//! .description: wrap a value at a certain limit
+//! a.name: a
+//! b.default: 1
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} mod_(${type} a, ${type} b) {
+ return mod(a, b);
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Square
+//! .category: geometry
+//! .description: select between two inputs depending on whether a point lies within a square (or cube in 3D)
+//! pos.name: pos
+//! pos.description: point to test
+//! pos.default: .pos
+//! inside.description: source to use if pos lies inside the square
+//! inside.default: #f00
+//! outside.description: source to use if pos lies outside the square
+//! outside.default: #0f0
+//! size.description: radius of the square
+//! size.default: 0.5
+
+` +
+ [
+ ['float', 'a'],
+ ['vec2', 'max(a.x, a.y)'],
+ ['vec3', 'max(a.x, max(a.y, a.z))'],
+ ['vec4', 'max(max(a.x, a.y), max(a.z, a.w))'],
+ ]
+ .map((x) => {
+ const type = x[0];
+ const max = x[1];
+ return ['float', 'vec2', 'vec3', 'vec4']
+ .map(
+ (type2) => `
+${type2} square(${type} pos, ${type2} inside, ${type2} outside, ${type} size) {
+ ${type} a = abs(pos) / size;
+ return ${max} < 1.0 ? inside : outside;
+}
+`,
+ )
+ .join('\n');
+ })
+ .join('\n'),
+ `
+//! .name: Circle
+//! .category: geometry
+//! .description: select between two inputs depending on whether a point lies within a circle (or sphere in 3D)
+//! pos.default: .pos
+//! pos.description: point to test
+//! inside.default: #f00
+//! inside.description: source to use if pos lies inside the circle
+//! outside.default: #0f0
+//! outside.description: source to use if pos lies outside the circle
+//! size.default: 0.5
+//! size.description: radius of the circle
+
+` +
+ GLSL_FLOAT_TYPE_PAIRS.map(
+ ([type, type2]) => `
+${type2} circle(${type} pos, ${type2} inside, ${type2} outside, ${type} size) {
+ pos /= size;
+ return dot(pos, pos) < 1.0 ? inside : outside;
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Comparator
+//! .category: basic
+//! .description: select between two inputs depending on a comparison between two values
+//! .id: cmp
+//! cmp1.name: compare 1
+//! cmp1.description: input to compare against "Compare 2"
+//! cmp2.name: compare 2
+//! cmp2.default: 0
+//! cmp2.description: input to compare against "Compare 1"
+//! less.name: if less
+//! less.default: 0
+//! less.description: value to output if "Compare 1" < "Compare 2"
+//! greater.name: if greater
+//! greater.default: 1
+//! greater.description: value to output if "Compare 1" ≥ "Compare 2"
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} compare(float cmp1, float cmp2, ${type} less, ${type} greater) {
+ return cmp1 < cmp2 ? less : greater;
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Sine wave
+//! .category: curves
+//! .description: sine, triangle, square, sawtooth waves
+//! .id: sin
+//! type.control: select:sin|tri|squ|saw
+//! t.description: position in the wave
+//! t.default: .time
+//! period.description: period of the wave
+//! period.default: 1
+//! amp.name: amplitude
+//! amp.default: 1
+//! amp.description: amplitude (maximum value) of the wave
+//! phase.default: 0
+//! phase.description: phase of the wave (0.5 = phase by ½ period)
+//! center.name: baseline
+//! center.default: 0
+//! center.description: this value is added to the output at the end
+//! nonneg.name: non-negative
+//! nonneg.description: make the wave go from baseline to baseline+amp, rather than baseline-amp to baseline+amp
+//! nonneg.control: checkbox
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} sine_wave(int type, ${type} t, ${type} period, ${type} amp, ${type} phase, ${type} center, int nonneg) {
+ ${type} v = ${type}(0.0);
+ t = t / period - phase;
+ if (type == 0) {
+ v = sin(t * 6.2831853);
+ } else if (type == 1) {
+ t = mod(t, 1.0);
+ ${type} s = step(${type}(0.5), t);
+ v = mix(4.0 * t - 1.0, 3.0 - 4.0 * t, s);
+ } else if (type == 2) {
+ v = mod(floor(2.0 * t) + 1.0, 2.0) * 2.0 - 1.0;
+ } else if (type == 3) {
+ v = mod(t, 1.0) * 2.0 - 1.0;
+ }
+ if (nonneg != 0) v = v * 0.5 + 0.5;
+ return amp * v + center;
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Rotate 2D
+//! .category: geometry
+//! .id: rot2
+//! .description: rotate a 2-dimensional vector
+//! v.description: vector to rotate
+//! theta.name: θ
+//! theta.description: angle to rotate by (in radians)
+//! dir.name: direction
+//! dir.description: direction of rotation
+//! dir.control: select:CCW|CW
+
+vec2 rotate2D(vec2 v, float theta, int dir) {
+ if (dir == 1) theta = -theta;
+ float c = cos(theta), s = sin(theta);
+ return vec2(c*v.x - s*v.y, s*v.x + c*v.y);
+}
+`,
+ `
+//! .name: Hue shift
+//! .category: colors
+//! .id: hue
+//! .description: shift hue of color
+//! color.description: input color
+//! shift.description: how much to shift hue by (0.5 = shift halfway across the rainbow)
+
+vec3 hue_shift(vec3 color, float shift) {
+ vec3 c = color;
+ // rgb to hsv
+ vec3 hsv;
+ {
+ vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+ vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+ vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+ float d = q.x - min(q.w, q.y);
+ float e = 1.0e-10;
+ hsv = vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+ }
+
+ hsv.x = mod(hsv.x + shift, 1.0);
+ c = hsv;
+
+ // hsv to rgb
+ {
+ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+ return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+ }
+}
+`,
+ `
+//! .name: Saturate
+//! .category: colors
+//! .id: saturate
+//! .description: change saturation of color
+//! color.description: input color
+//! amount.description: how much to change saturation by (−1 to 1 range)
+
+vec3 saturate(vec3 color, float amount) {
+ vec3 c = color;
+ // rgb to hsv
+ vec3 hsv;
+ {
+ vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+ vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+ vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+ float d = q.x - min(q.w, q.y);
+ float e = 1.0e-10;
+ hsv = vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+ }
+
+ hsv.y = clamp(hsv.y + amount, 0.0, 1.0);
+ c = hsv;
+
+ // hsv to rgb
+ {
+ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+ return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+ }
+}
+`,
+ `
+//! .name: Brightness-contrast
+//! .category: colors
+//! .description: change brightness/contrast of color
+//! color.description: input color
+//! brightness.description: how much to change brightness by (−1 to 1 range)
+//! contrast.description: how much to change contrast by (−1 to 1 range)
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} brightcont(${type} color, ${type} brightness, ${type} contrast) {
+ brightness = clamp(brightness, -1.0, 1.0);
+ contrast = clamp(contrast, -1.0, 1.0);
+ return clamp((contrast + 1.0) / (1.0 - contrast) * (color - 0.5) + (brightness + 0.5), 0.0, 1.0);
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Clamp
+//! .category: basic
+//! .id: clamp
+//! .description: clamp a value between a minimum and maximum
+//! x.name: value
+//! x.id: val
+//! x.description: input value
+//! minimum.name: min
+//! minimum.id: min
+//! maximum.name: max
+//! maximum.id: max
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} clamp_(${type} x, ${type} minimum, ${type} maximum) {
+ return clamp(x, minimum, maximum);
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Rotate 3D
+//! .id: rot3
+//! .category: geometry
+//! .description: rotate a 3D vector about an axis
+//! v.description: the vector to rotate
+//! axis.description: the axis to rotate around. the magnitude must be non-zero but otherwise is ignored.
+//! axis.default: 0,1,0
+//! angle.name: θ
+//! angle.description: the angle in radians
+//! angle.default: 0.57
+
+vec3 rot3(vec3 v, vec3 axis, float angle) {
+ axis = normalize(axis);
+ float c = cos(angle);
+ float s = sin(angle);
+ // https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
+ return v * c + cross(axis, v) * s + axis * dot(axis, v) * (1.0 - c);
+}
+`,
+ `
+//! .name: Remap
+//! .id: remap
+//! .category: basic
+//! .description: linearly remap a value from one interval to another
+//! x.id: x
+//! a1.name: a₁
+//! a1.default: 0
+//! a1.description: negative endpoint of source interval
+//! b1.name: b₁
+//! b1.default: 1
+//! b1.description: positive endpoint of source interval
+//! a2.name: a₂
+//! a2.default: -1
+//! a2.description: positive endpoint of source interval
+//! b2.name: b₂
+//! b2.default: 1
+//! b2.description: positive endpoint of destination interval
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} remap(${type} x, ${type} a1, ${type} b1, ${type} a2, ${type} b2) {
+ return (x - a1) / (b1 - a1) * (b2 - a2) + a2;
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Smoothstep
+//! .id: smoothstep
+//! .category: curves
+//! .description: smoothly transition between two values (with Hermite interpolation)
+//! t.id: t
+//! t1.name: t₁
+//! t1.description: first input point
+//! t1.default: 0
+//! t2.name: t₂
+//! t2.description: second input point
+//! t2.default: 1
+//! out1.name: out₁
+//! out1.description: output value when t ≤ t₁
+//! out1.default: 0
+//! out2.name: out₂
+//! out2.description: output value when t ≥ t₂
+//! out2.default: 1
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} smoothst(${type} t, ${type} t1, ${type} t2, ${type} out1, ${type} out2) {
+ return mix(out1, out2, smoothstep(t1, t2, t));
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Arctangent
+//! .id: arctan2
+//! .category: math
+//! .description: The arctangent function (radians) with 2 parameters (set x = 1 for normal arctangent)
+//! y.id: y
+//! x.id: x
+//! x.default: 1
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} arctan2(${type} y, ${type} x) {
+ return atan(y, x);
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Tangent
+//! .id: tan
+//! .category: math
+//! .description: The tangent function (radians)
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} tang(${type} x) {
+ return tan(x);
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Arcsine
+//! .id: arcsin
+//! .category: math
+//! .description: The arcsine function (radians) — input will be clamped to [−1, 1]
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} arcsin(${type} x) {
+ return asin(clamp(x, -1.0, 1.0));
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Sigmoid
+//! .id: sigmoid
+//! .category: curves
+//! .description: The sigmoid function — smoothly maps the interval (−∞, ∞) to (a, b)
+//! x.description: input value
+//! a.description: output value for very negative inputs
+//! b.description: output value for very positive inputs
+//! sharpness.description: scale factor for input value — higher = quicker transition from a to b
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} sigmoid(${type} x, ${type} a, ${type} b, ${type} sharpness) {
+ return mix(a, b, 1.0 / (1.0 + exp(-sharpness * x)));
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Staircase (floor)
+//! .id: floor
+//! .category: curves
+//! .description: The floor function — largest integer less than x
+//! x.description: input value
+//! stepw.name: step w
+//! stepw.description: step width
+//! steph.name: step h
+//! steph.description: step height
+//! phase.description: proportion of a step to be added to input
+//! phase.default: 0
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+${type} floorf(${type} x, ${type} stepw, ${type} steph, ${type} phase) {
+ return floor(x / stepw + phase) * steph;
+}
+`,
+ ).join('\n'),
+ `
+//! .name: Sin noise
+//! .category: noise
+//! .description: Noise generated from sine waves
+//! x.id: x
+//! falloff.description: values closer to 0 will emphasize lower-frequency noise, values towards 1 will emphasize higher-frequency noise
+//! falloff.default: 0.5
+//! freqstep.description: ratio between successive frequencies of noise
+//! freqstep.default: 2
+//! levels.description: number of frequencies of noises to add together
+//! levels.control: int:1|30
+//! levels.default: 8
+
+float noise_sin(float x, float falloff, float freqstep, int levels) {
+ float k = 1.0;
+ float phase = 2.45;
+
+ falloff = clamp(falloff, 0.0, 1.0);
+
+ float v = 0.0;
+ int i = 0;
+ for (i = 0; i < levels; i++) {
+ float s = sin(x + phase);
+ v += k * s * s;
+ x *= freqstep;
+ k *= falloff;
+ phase *= 1.7;
+ phase = mod(phase, 6.28);
+ }
+ return v * (1.0 - falloff);
+}
+
+float noise_sin(vec2 x, float falloff, float freqstep, int levels) {
+ float v = 0.0;
+ float k = 1.0;
+ vec2 phase = vec2(1.0, 3.6);
+ float theta = 2.7;
+ for (int i = 0; i < levels; i++) {
+ v += k * abs(sin(x.x + phase.x) * sin(x.y + phase.y));
+ phase *= 3.8;
+ phase = mod(phase, 6.28);
+ x *= freqstep;
+ x = mat2(cos(theta), sin(theta), -sin(theta), cos(theta)) * x;
+ k *= falloff;
+ theta *= 2.4;
+ theta = mod(theta, 6.28);
+ }
+ return v * (1.0 - falloff);
+}
+
+float noise_sin(vec3 x, float falloff, float freqstep, int levels) {
+ float v = 0.0;
+ float k = 1.0;
+ vec3 phase = vec3(1.0, 3.6, 2.2);
+ float theta = 2.7;
+ float phi = 4.6;
+ for (int i = 0; i < levels; i++) {
+ v += k * abs(sin(x.x + phase.x) * sin(x.y + phase.y) * sin(x.z + phase.z));
+ phase *= 4.7;
+ phase = mod(phase, 6.28);
+ x *= freqstep;
+ float ct = cos(theta), st = sin(theta);
+ float cp = cos(phi), sp = sin(phi);
+ x = mat3(st*cp, ct*cp, -sp, st*sp, ct*sp, cp, ct, -st, 0.0) * x;
+ k *= falloff;
+ theta *= 2.4;
+ theta = mod(theta, 6.28);
+ }
+ return v * (1.0 - falloff);
+}
+`,
+ `
+//! .name: Norm
+//! .alt: length/magnitude
+//! .description: the Euclidean norm ("length") of a vector
+//! .category: geometry
+
+float norm(float x) { return x; }
+float norm(vec2 x) { return length(x); }
+float norm(vec3 x) { return length(x); }
+float norm(vec4 x) { return length(x); }
+`,
+ `
+//! .name: Distance
+//! .description: the Euclidean distance between two points
+//! .category: geometry
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+float dist(${type} x, ${type} y) { return distance(x, y); }
+`,
+ ).join('\n'),
+ `
+//! .name: Dot product
+//! .description: the dot product between two vectors
+//! .category: geometry
+//! .id: dot
+
+` +
+ GLSL_FLOAT_TYPES.map(
+ (type) => `
+float dot_prod(${type} x, ${type} y) { return dot(x, y); }
+`,
+ ).join('\n'),
+ `
+//! .name: White noise
+//! .description: Uniform distribution over [0, 1)
+//! .category: noise
+
+float wnoise(float x)
+{
+ uint k = 134775813u;
+ uint u = floatBitsToUint(x) * k;
+ u = ((u >> 8) ^ u) * k;
+ u = ((u >> 8) ^ u) * k;
+ u = ((u >> 8) ^ u) * k;
+ return float(u) * (1.0 / 4294967296.0);
+}
+
+float wnoise(vec2 x)
+{
+ uint k = 134775813u;
+ uvec2 u = floatBitsToUint(x) * k;
+ u = ((u >> 8) ^ u.yx) * k;
+ u = ((u >> 8) ^ u.yx) * k;
+ u = ((u >> 8) ^ u.yx) * k;
+ return float(u) * (1.0 / 4294967296.0);
+}
+
+float wnoise(vec3 x)
+{
+ uint k = 134775813u;
+ uvec3 u = floatBitsToUint(x) * k;
+ u = ((u >> 8) ^ u.yzx) * k;
+ u = ((u >> 8) ^ u.yzx) * k;
+ u = ((u >> 8) ^ u.yzx) * k;
+ return float(u) * (1.0 / 4294967296.0);
+}
+
+float wnoise(vec4 x)
+{
+ uint k = 134775813u;
+ uvec4 u = floatBitsToUint(x) * k;
+ u = ((u >> 8) ^ u.yzwx) * k;
+ u = ((u >> 8) ^ u.yzwx) * k;
+ u = ((u >> 8) ^ u.yzwx) * k;
+ return float(u) * (1.0 / 4294967296.0);
+}
+`,
+ `
+//! .name: Perlin noise
+//! .description: Perlin noise with range [0, 1]
+//! .category: noise
+//! .require: wnoise
+//! x.default: .pos
+//! freq.description: input is scaled by this
+//! freq.default: 8
+
+float perlin(float x, float freq) {
+ x *= freq;
+ float grid0 = floor(x);
+ float grid1 = grid0 + 1.0;
+
+ float d0 = x - grid0;
+ float d1 = x - grid1;
+
+ float grad0 = wnoise(grid0) < 0.5 ? -1.0 : 1.0;
+ float grad1 = wnoise(grid1) < 0.5 ? -1.0 : 1.0;
+
+ float n0 = dot(grad0, d0);
+ float n1 = dot(grad1, d1);
+
+ float s = smoothstep(0.0, 1.0, d0);
+ float p = mix(n0, n1, s);
+ return p * 0.5 + 0.5;
+}
+
+float perlin(vec2 x, vec2 freq) {
+ x *= freq;
+ vec2 grid00 = floor(x);
+ vec2 grid01 = grid00 + vec2(0.0, 1.0);
+ vec2 grid10 = grid00 + vec2(1.0, 0.0);
+ vec2 grid11 = grid00 + 1.0;
+
+ vec2 d00 = x - grid00;
+ vec2 d01 = x - grid01;
+ vec2 d10 = x - grid10;
+ vec2 d11 = x - grid11;
+
+ float twopi = 6.2831853;
+ float theta00 = wnoise(grid00) * twopi;
+ float theta01 = wnoise(grid01) * twopi;
+ float theta10 = wnoise(grid10) * twopi;
+ float theta11 = wnoise(grid11) * twopi;
+
+ vec2 grad00 = vec2(cos(theta00), sin(theta00));
+ vec2 grad01 = vec2(cos(theta01), sin(theta01));
+ vec2 grad10 = vec2(cos(theta10), sin(theta10));
+ vec2 grad11 = vec2(cos(theta11), sin(theta11));
+
+ float n00 = dot(grad00, d00);
+ float n01 = dot(grad01, d01);
+ float n10 = dot(grad10, d10);
+ float n11 = dot(grad11, d11);
+
+ vec2 s = smoothstep(0.0, 1.0, d00);
+ float n0 = mix(n00, n10, s.x);
+ float n1 = mix(n01, n11, s.x);
+ float p = mix(n0, n1, s.y);
+ return p * 0.5 + 0.5;
+}
+
+float perlin(vec3 x, vec3 freq) {
+ x *= freq;
+ vec3 grid000 = floor(x);
+ vec3 grid001 = grid000 + vec3(0.0, 0.0, 1.0);
+ vec3 grid010 = grid000 + vec3(0.0, 1.0, 0.0);
+ vec3 grid011 = grid000 + vec3(0.0, 1.0, 1.0);
+ vec3 grid100 = grid000 + vec3(1.0, 0.0, 0.0);
+ vec3 grid101 = grid000 + vec3(1.0, 0.0, 1.0);
+ vec3 grid110 = grid000 + vec3(1.0, 1.0, 0.0);
+ vec3 grid111 = grid000 + vec3(1.0, 1.0, 1.0);
+
+ vec3 d000 = x - grid000;
+ vec3 d001 = x - grid001;
+ vec3 d010 = x - grid010;
+ vec3 d011 = x - grid011;
+ vec3 d100 = x - grid100;
+ vec3 d101 = x - grid101;
+ vec3 d110 = x - grid110;
+ vec3 d111 = x - grid111;
+
+ // thanks to https://math.stackexchange.com/a/1586185
+ // this behemoth computes 9 random points on the unit sphere,
+ // seeded by grid000–grid111
+ float halfpi = 1.5707963;
+ float twopi = 6.2831853;
+ float a000 = acos(2.0 * wnoise(grid000) - 1.0) - halfpi;
+ float a001 = acos(2.0 * wnoise(grid001) - 1.0) - halfpi;
+ float a010 = acos(2.0 * wnoise(grid010) - 1.0) - halfpi;
+ float a011 = acos(2.0 * wnoise(grid011) - 1.0) - halfpi;
+ float a100 = acos(2.0 * wnoise(grid100) - 1.0) - halfpi;
+ float a101 = acos(2.0 * wnoise(grid101) - 1.0) - halfpi;
+ float a110 = acos(2.0 * wnoise(grid110) - 1.0) - halfpi;
+ float a111 = acos(2.0 * wnoise(grid111) - 1.0) - halfpi;
+
+ float b000 = twopi * wnoise(vec4(grid000,3.0));
+ float b001 = twopi * wnoise(vec4(grid001,3.0));
+ float b010 = twopi * wnoise(vec4(grid010,3.0));
+ float b011 = twopi * wnoise(vec4(grid011,3.0));
+ float b100 = twopi * wnoise(vec4(grid100,3.0));
+ float b101 = twopi * wnoise(vec4(grid101,3.0));
+ float b110 = twopi * wnoise(vec4(grid110,3.0));
+ float b111 = twopi * wnoise(vec4(grid111,3.0));
+
+ vec3 grad000 = vec3(cos(a000)*cos(b000), cos(a000)*sin(b000), sin(a000));
+ vec3 grad001 = vec3(cos(a001)*cos(b001), cos(a001)*sin(b001), sin(a001));
+ vec3 grad010 = vec3(cos(a010)*cos(b010), cos(a010)*sin(b010), sin(a010));
+ vec3 grad011 = vec3(cos(a011)*cos(b011), cos(a011)*sin(b011), sin(a011));
+ vec3 grad100 = vec3(cos(a100)*cos(b100), cos(a100)*sin(b100), sin(a100));
+ vec3 grad101 = vec3(cos(a101)*cos(b101), cos(a101)*sin(b101), sin(a101));
+ vec3 grad110 = vec3(cos(a110)*cos(b110), cos(a110)*sin(b110), sin(a110));
+ vec3 grad111 = vec3(cos(a111)*cos(b111), cos(a111)*sin(b111), sin(a111));
+
+ float n000 = dot(grad000, d000);
+ float n001 = dot(grad001, d001);
+ float n010 = dot(grad010, d010);
+ float n011 = dot(grad011, d011);
+ float n100 = dot(grad100, d100);
+ float n101 = dot(grad101, d101);
+ float n110 = dot(grad110, d110);
+ float n111 = dot(grad111, d111);
+
+ vec3 s = smoothstep(0.0, 1.0, d000);
+ float n00 = mix(n000, n100, s.x);
+ float n10 = mix(n010, n110, s.x);
+ float n0 = mix(n00, n10, s.y);
+ float n01 = mix(n001, n101, s.x);
+ float n11 = mix(n011, n111, s.x);
+ float n1 = mix(n01, n11, s.y);
+ float p = mix(n0, n1, s.z);
+ return p * 0.5 + 0.5;
+}
+`,
+ `
+//! .name: Worley noise
+//! .description: n-dimensional Worley noise
+//! .category: noise
+//! p.name: x
+//! p.id: x
+//! p.default: .pos
+//! freq.default: 8
+//! .require: wnoise
+
+float worley(vec2 p, vec2 freq) {
+ p *= freq;
+ vec2 f = floor(p);
+ float sqd = 1.0;
+ for (float dx = -1.0; dx <= +1.0; dx += 1.0) {
+ for (float dy = -1.0; dy <= +1.0; dy += 1.0) {
+ vec2 g = f + vec2(dx, dy);
+ vec2 c = g + vec2(wnoise(g), wnoise(vec3(g, 1.0)));
+ sqd = min(sqd, dot(c - p, c - p));
+ }
+ }
+ return sqrt(sqd);
+}
+
+float worley(vec3 p, vec3 freq) {
+ p *= freq;
+ vec3 f = floor(p);
+ float sqd = 1.0;
+ for (float dx = -1.0; dx <= +1.0; dx += 1.0) {
+ for (float dy = -1.0; dy <= +1.0; dy += 1.0) {
+ for (float dz = -1.0; dz <= +1.0; dz += 1.0) {
+ vec3 g = f + vec3(dx, dy, dz);
+ vec3 c = g + vec3(wnoise(g), wnoise(vec4(g, 1.0)), wnoise(vec4(g, 2.0)));
+ sqd = min(sqd, dot(c - p, c - p));
+ }
+ }
+ }
+ return sqrt(sqd);
+}
+`,
+];
+
+function auto_update_enabled() {
+ return true;
+}
+
+function is_input(element) {
+ if (!element) return false;
+ for (let e = element; e; e = e.parentElement) {
+ if (
+ e.tagName === 'INPUT' ||
+ e.tagName === 'BUTTON' ||
+ e.tagName === 'SELECT' ||
+ e.isContentEditable
+ ) {
+ return true;
+ }
+ }
+ return false;
+}
+
+class Parser {
+ constructor(string, line_number) {
+ this.string = string;
+ this.line_number = line_number;
+ this.i = 0;
+ this.error = null;
+ }
+
+ set_error(e) {
+ if (!this.error) this.error = { line: this.line_number, message: e };
+ }
+
+ eof() {
+ this.skip_space();
+ return this.i >= this.string.length;
+ }
+
+ has(c) {
+ this.skip_space();
+ return this.string.substring(this.i, this.i + c.length) === c;
+ }
+
+ skip_space() {
+ while (this.i < this.string.length && this.string[this.i].match(/\s/)) {
+ if (this.string[this.i] === '\n') this.line_number += 1;
+ this.i += 1;
+ }
+ }
+
+ parse_type() {
+ this.skip_space();
+ const i = this.i;
+ for (const type of ['float', 'vec2', 'vec3', 'vec4', 'int']) {
+ if (
+ this.string.substring(i, i + type.length) === type &&
+ this.string[i + type.length] === ' '
+ ) {
+ this.i += type.length + 1;
+ return type;
+ }
+ }
+ let end = this.string.indexOf(' ', i);
+ if (end === -1) end = this.string.length;
+ this.set_error(`no such type: ${this.string.substring(i, end)}`);
+ }
+
+ parse_ident() {
+ this.skip_space();
+ if (this.eof()) {
+ this.set_error('expected identifier, got EOF');
+ return;
+ }
+ const first_char = this.string[this.i];
+ if (!first_char.match(/[a-zA-Z_]/)) {
+ this.set_error(`expected identifier, got '${first_char}'`);
+ return;
+ }
+ const start = this.i;
+ this.i += 1;
+ while (
+ this.i < this.string.length &&
+ this.string[this.i].match(/[a-zA-Z0-9_]/)
+ ) {
+ this.i += 1;
+ }
+ return this.string.substring(start, this.i);
+ }
+
+ expect(c) {
+ this.skip_space();
+ const got = this.string.substring(this.i, this.i + c.length);
+ if (got !== c) {
+ this.set_error(`expected ${c}, got ${got}`);
+ }
+ this.i += 1;
+ }
+
+ advance() {
+ this.i += 1;
+ }
+}
+
+function control_type(control) {
+ if (control.startsWith('select:')) {
+ return 'int';
+ } else if (control === 'checkbox') {
+ return 'int';
+ } else if (control === 'slider') {
+ return 'float';
+ } else if (control.startsWith('int:')) {
+ return 'int';
+ }
+ return null;
+}
+
+function parse_widget_definition(code) {
+ code = code.trim();
+ const params = new Map();
+ const info = {
+ alt: '',
+ params,
+ description: '',
+ definitions: [],
+ require: [],
+ };
+ let lines = code.split('\n');
+ let def_start = undefined;
+ let error = undefined;
+ const param_regex = /^[a-zA-Z_][a-zA-Z0-9_]*/gu;
+
+ lines.forEach((line, index) => {
+ if (error) return;
+ if (def_start !== undefined) return;
+
+ line = line.trim();
+ if (line.startsWith('//! ')) {
+ const parts = line.substring('//! '.length).split(': ');
+ if (parts.length !== 2) {
+ error = `on line ${index + 1}: line must contain ": " exactly once`;
+ return;
+ }
+ const key = parts[0].trim();
+ const value = parts[1].trim();
+ if (key === '.name') {
+ info.name = value;
+ } else if (key === '.description') {
+ info.description = value;
+ } else if (key === '.id') {
+ info.id = value;
+ } else if (key === '.category') {
+ info.category = value;
+ } else if (key === '.alt') {
+ info.alt = value;
+ } else if (key === '.require') {
+ for (const r of value.split(',')) {
+ info.require.push(r.trim());
+ }
+ } else if (key.startsWith('.')) {
+ error = `on line ${index + 1}: key ${key} not recognized`;
+ return;
+ } else {
+ const key_parts = key.split('.');
+ if (key_parts.length !== 2) {
+ error = `on line ${
+ index + 1
+ }: expected key to be of form parameter.property, got ${key}`;
+ return;
+ }
+ const param_name = key_parts[0];
+ const property = key_parts[1];
+ if (!param_name.match(param_regex)) {
+ error = `on line ${index + 1}: bad parameter name: ${param_name}`;
+ }
+ if (!params.has(param_name)) {
+ params.set(param_name, {});
+ }
+ const param = params.get(param_name);
+ switch (property) {
+ case 'id':
+ case 'name':
+ case 'description':
+ case 'default':
+ case 'control':
+ param[property] = value;
+ break;
+ default:
+ error = `on line ${
+ index + 1
+ }: parameter property '${property}' not recognized`;
+ return;
+ }
+ }
+ } else if (line.startsWith('//!')) {
+ error = `on line ${index + 1}: missing space after //!`;
+ } else if (line.startsWith('//')) {
+ // comment
+ } else {
+ def_start = index;
+ return;
+ }
+ });
+ if (error) {
+ return { error };
+ }
+ lines = lines.slice(def_start);
+ if (lines.some((x) => x.startsWith('//!'))) {
+ return { error: '//! appears after first function definition' };
+ }
+ lines = lines.map((x) => {
+ x = x.trim();
+ if (x.startsWith('//')) {
+ return '';
+ }
+ return x;
+ });
+
+ const parser = new Parser(lines.join('\n'), def_start + 1);
+ while (!parser.error && !parser.eof()) {
+ const definition_start = parser.i;
+ const return_type = parser.parse_type();
+ const fname = parser.parse_ident();
+ if (!info.function_name) info.function_name = fname;
+ if (!parser.error && fname !== info.function_name) {
+ return {
+ error: `function defined as both '${info.function_name}' and '${fname}'`,
+ };
+ }
+ if (!info.id) info.id = info.function_name;
+
+ const definition_params = [];
+ parser.expect('(');
+ while (!parser.eof() && !parser.has(')')) {
+ if (parser.has(',')) parser.expect(',');
+ const type = parser.parse_type();
+ const name = parser.parse_ident();
+ definition_params.push({ type, name });
+
+ if (!params.has(name)) {
+ if (!info.definitions.size) {
+ params.set(name, {});
+ } else if (!parser.error) {
+ return { error: `parameter ${name} does not exist` };
+ }
+ }
+ }
+
+ // we have all parameters now — fill out missing fields
+ if (!info.definitions.size) {
+ for (const param_name of params.keys()) {
+ const param = params.get(param_name);
+ if (!param.id) param.id = param_name;
+ if (!param.name) param.name = param.id;
+ if (!param.description) param.description = '';
+ }
+ }
+
+ const input_types = new Map();
+ const param_order = new Map();
+ definition_params.forEach((p, index) => {
+ const param = params.get(p.name);
+ if (param.control) {
+ const expected_type = control_type(param.control);
+ if (!expected_type) {
+ parser.set_error(`bad control type: '${param.control}'`);
+ }
+ if (p.type !== expected_type) {
+ parser.set_error(
+ `parameter ${p.name} should have type ${expected_type} since it's a ${param.control}, but it has type ${p.type}`,
+ );
+ }
+ }
+
+ if (!param.control && p.type === 'int') {
+ parser.set_error(
+ `parameter ${p.name} has type int, so you should set a control type for it, e.g. //! ${p.name}.control: checkbox`,
+ );
+ }
+ if (!param.control) {
+ input_types.set(param.id, p.type);
+ }
+ param_order.set(param.id, index);
+ });
+ for (const param of params.values()) {
+ if (!input_types.has(param.id) && !param.control) {
+ parser.set_error(
+ `parameter ${param.id} not specified in definition of ${info.function_name}`,
+ );
+ }
+ }
+
+ parser.expect(')');
+ parser.expect('{');
+ let brace_depth = 1;
+ while (!parser.eof() && brace_depth > 0) {
+ if (parser.has('{')) brace_depth += 1;
+ if (parser.has('}')) brace_depth -= 1;
+ parser.advance();
+ }
+ const definition_end = parser.i;
+ const definition = parser.string.substring(
+ definition_start,
+ definition_end,
+ );
+ info.definitions.push({
+ input_types,
+ param_order,
+ return_type,
+ code: definition,
+ });
+ }
+ if (parser.error) {
+ const err = parser.error;
+ return { error: `on line ${err.line}: ${err.message}` };
+ }
+ if (!info.name) info.name = info.id;
+ if (!info.category) {
+ return { error: `no category set for ${info.id}` };
+ }
+ return info;
+}
+
+const widget_info = new Map();
+for (const code of builtin_widgets) {
+ const result = parse_widget_definition(code);
+ if (result && result.error) {
+ console.error(result.error);
+ } else {
+ widget_info.set(result.id, result);
+ }
+}
+
+window.addEventListener('load', startup);
+
+function set_ui_shown(to) {
+ ui_shown = to;
+ const ui_viz = to ? 'visible' : 'collapse';
+ ui_div.style.visibility = ui_viz;
+ ui_resize.style.visibility = ui_viz;
+}
+
+function color_hex_to_float(hex) {
+ let r;
+ let g;
+ let b;
+ let a;
+ hex = hex.trim();
+
+ if (hex.length === 7 || hex.length === 9) {
+ // #rrggbb or #rrggbbaa
+ r = parseInt(hex.substring(1, 3), 16) / 255;
+ g = parseInt(hex.substring(3, 5), 16) / 255;
+ b = parseInt(hex.substring(5, 7), 16) / 255;
+ a = hex.length === 7 ? 1 : parseInt(hex.substring(7, 9), 16) / 255;
+ } else if (hex.length === 4 || hex.length === 5) {
+ // #rgb or #rgba
+ r = parseInt(hex[1], 16) / 15;
+ g = parseInt(hex[2], 16) / 15;
+ b = parseInt(hex[3], 16) / 15;
+ a = hex.length === 4 ? 1 : parseInt(hex[4], 16) / 15;
+ }
+
+ if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) {
+ return null;
+ }
+
+ const color = {
+ r: r,
+ g: g,
+ b: b,
+ a: a,
+ };
+ Object.preventExtensions(color);
+ return color;
+}
+
+function color_float_to_hex(color) {
+ const r = Math.round(color.r * 255);
+ const g = Math.round(color.g * 255);
+ const b = Math.round(color.b * 255);
+ const a = Math.round((color.a ?? 1) * 255);
+ function component(x) {
+ x = x.toString(16);
+ while (x.length < 2) x = '0' + x;
+ return x;
+ }
+ let ca = component(a);
+ if (ca === 'ff') ca = '';
+ return `#${component(r)}${component(g)}${component(b)}${ca}`;
+}
+
+function update_shader() {
+ clear_error();
+ const source = get_shader_source();
+ if (source === null) {
+ return;
+ }
+ const fragment_code = `#version 300 es
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+uniform sampler2D _texture;
+uniform float _time;
+uniform vec2 _texture_size;
+in vec2 _pos;
+out vec4 _out_color;
+
+${source}
+
+void main() {
+ _out_color = vec4(_get_color(), 1.0);
+}
+`;
+ const vertex_code = `#version 300 es
+in vec2 v_pos;
+out vec2 _pos;
+void main() {
+ _pos = v_pos;
+ gl_Position = vec4(v_pos, 0.0, 1.0);
+}
+`;
+ program_main = compile_program('main', {
+ vertex: vertex_code,
+ fragment: fragment_code,
+ });
+}
+
+function on_key_press(e) {
+ const code = e.keyCode;
+ if (is_input(e.target)) {
+ return;
+ }
+ console.log('key press', code);
+
+ switch (code) {
+ case 32: // space
+ perform_step();
+ break;
+ case 9: // tab
+ set_ui_shown(!ui_shown);
+ e.preventDefault();
+ break;
+ case 13: // return
+ update_shader();
+ e.preventDefault();
+ break;
+ }
+}
+
+function float_glsl(f) {
+ if (isNaN(f)) return '(0.0 / 0.0)';
+ if (f === Infinity) return '1e+1000';
+ if (f === -Infinity) return '-1e+1000';
+ const s = f + '';
+ if (s.indexOf('.') !== -1 || s.indexOf('e') !== -1) return s;
+ return s + '.0';
+}
+
+function type_component_count(type) {
+ switch (type) {
+ case 'float':
+ return 1;
+ case 'vec2':
+ return 2;
+ case 'vec3':
+ return 3;
+ case 'vec4':
+ return 4;
+ default:
+ return 0;
+ }
+}
+
+function type_base_type(type) {
+ switch (type) {
+ case 'float':
+ case 'vec2':
+ case 'vec3':
+ case 'vec4':
+ return 'float';
+ default:
+ return null;
+ }
+}
+
+function type_vec(base_type, component_count) {
+ switch (base_type) {
+ case 'float':
+ switch (component_count) {
+ case 1:
+ return 'float';
+ case 2:
+ return 'vec2';
+ case 3:
+ return 'vec3';
+ case 4:
+ return 'vec4';
+ default:
+ return null;
+ }
+ default:
+ return null;
+ }
+}
+
+function get_widget_by_name(name) {
+ for (const w of document.getElementsByClassName('widget')) {
+ if (get_widget_name(w) === name) {
+ return w;
+ }
+ }
+ return null;
+}
+
+function get_widget_by_id(id) {
+ return document.querySelector(`.widget[data-id="${id}"]`);
+}
+
+function get_widget_name(widget_div) {
+ const names = widget_div.getElementsByClassName('widget-name');
+ console.assert(
+ names.length === 1,
+ 'there should be exactly one widget-name input per widget',
+ );
+ return names[0].innerText;
+}
+
+function get_widget_names() {
+ const s = new Set();
+ for (const w of document.getElementsByClassName('widget-name')) {
+ s.add(w.innerText);
+ }
+ return s;
+}
+
+function set_display_output_and_update_shader(to) {
+ for (const widget of document.querySelectorAll('.widget[data-display="1"]')) {
+ widget.dataset.display = '0';
+ }
+ if (to) {
+ to.dataset.display = '1';
+ }
+ update_shader();
+}
+
+function update_input_element(input_element) {
+ const container = input_element.parentElement;
+
+ {
+ // add color input if the text is a color
+ let color_input = container.querySelector('input[type="color"]');
+ const color_value = color_hex_to_float(input_element.innerText);
+ if (color_value) {
+ if (!color_input) {
+ color_input = document.createElement('input');
+ color_input.type = 'color';
+ color_input.addEventListener('input', () => {
+ // this is kinda complicated because we
+ // want to preserve whether or not there's an alpha channel
+ // (but input[type=color] doesn't support alpha)
+ const prev_value = input_element.innerText;
+ const color = color_hex_to_float(color_input.value);
+ color.a = color_hex_to_float(prev_value).a;
+ const specify_alpha =
+ prev_value.length === 5 || prev_value.length === 9;
+ let new_value = color_float_to_hex(color);
+ console.assert(new_value.length === 7 || new_value.length === 9);
+ if (specify_alpha) {
+ if (new_value.length === 7) new_value += 'ff';
+ } else {
+ new_value = new_value.slice(0, 7);
+ }
+ input_element.innerText = new_value;
+ if (auto_update_enabled()) update_shader();
+ });
+ container.appendChild(color_input);
+ }
+ // if a color input has already been created for this input,
+ // we just need to update its value and show it.
+ color_input.value = color_float_to_hex({
+ r: color_value.r,
+ g: color_value.g,
+ b: color_value.b,
+ }).slice(0, 7);
+ color_input.style.display = 'inline-block';
+ } else {
+ if (color_input) {
+ color_input.style.display = 'none';
+ }
+ }
+ }
+}
+
+let dragging_widget = null;
+window.addEventListener('mouseup', () => {
+ dragging_widget = null;
+ const element = document.querySelector('.widget.dragging');
+ if (element) element.classList.remove('dragging');
+});
+
+function add_widget(func) {
+ const info = widget_info.get(func);
+ console.assert(info !== undefined, 'bad widget ID: ' + func);
+ const root = document.createElement('div');
+ root.dataset.func = func;
+ root.dataset.id = next_widget_id++;
+ root.classList.add('widget');
+ root.addEventListener('mouseover', () => {
+ if (!dragging_widget) return;
+
+ switch (root.compareDocumentPosition(dragging_widget)) {
+ case Node.DOCUMENT_POSITION_DISCONNECTED:
+ case Node.DOCUMENT_POSITION_CONTAINS:
+ case Node.DOCUMENT_POSITION_CONTAINED_BY:
+ console.error('unexpected compareDocumentPosition return value');
+ break;
+ case Node.DOCUMENT_POSITION_PRECEDING:
+ // dragging up
+ dragging_widget.before(root);
+ break;
+ case Node.DOCUMENT_POSITION_FOLLOWING:
+ // dragging down
+ dragging_widget.after(root);
+ break;
+ }
+ });
+
+ {
+ // delete button
+ const delete_button = document.createElement('button');
+ delete_button.ariaLabel = 'delete';
+ delete_button.classList.add('widget-delete');
+ delete_button.classList.add('widget-button');
+ delete_button.addEventListener('click', () => {
+ root.remove();
+ update_shader();
+ });
+ root.appendChild(delete_button);
+ }
+
+ {
+ // move button
+ const move_button = document.createElement('button');
+ move_button.ariaLabel = 'move';
+ move_button.classList.add('widget-move');
+ move_button.classList.add('widget-button');
+ move_button.addEventListener('mousedown', () => {
+ dragging_widget = root;
+ root.classList.add('dragging');
+ });
+ root.appendChild(move_button);
+ }
+
+ {
+ // title
+ const title = document.createElement('div');
+ title.classList.add('widget-title');
+ if (info.description) {
+ title.title = info.description;
+ }
+ const type = document.createElement('span');
+ type.classList.add('widget-type');
+ type.appendChild(document.createTextNode(info.name));
+ type.addEventListener('click', (e) => {
+ set_display_output_and_update_shader(root);
+ e.preventDefault();
+ });
+
+ title.appendChild(type);
+ title.appendChild(document.createTextNode(' '));
+
+ const name_input = document.createElement('div');
+ name_input.contentEditable = true;
+ name_input.spellcheck = false;
+ name_input.classList.add('widget-name');
+ name_input.addEventListener('input', () => update_shader());
+
+ // generate unique name
+ const names = get_widget_names();
+ let i;
+ for (i = 1; ; i++) {
+ if (!names.has(func + i)) {
+ break;
+ }
+ }
+ name_input.innerText = func + i;
+
+ title.appendChild(name_input);
+ root.appendChild(title);
+ }
+
+ // parameters
+ for (const param of info.params.values()) {
+ if (param.control) {
+ // control
+ const container = document.createElement('div');
+ container.classList.add('control');
+ container.dataset.id = param.id;
+ const type = param.control;
+ let input;
+ if (type === 'checkbox') {
+ input = document.createElement('input');
+ input.classList.add('entry');
+ input.type = 'checkbox';
+ if (param['default']) {
+ input.checked = 'checked';
+ }
+ } else if (type.startsWith('select:')) {
+ const options = type.substring('select:'.length).split('|');
+
+ input = document.createElement('select');
+ input.classList.add('entry');
+ for (const opt of options) {
+ const option = document.createElement('option');
+ option.appendChild(document.createTextNode(opt));
+ option.value = opt;
+ input.appendChild(option);
+ }
+
+ if (param['default']) {
+ input.value = param['default'];
+ }
+ } else if (type === 'slider') {
+ input = document.createElement('input');
+ input.classList.add('entry');
+ input.type = 'range';
+ input.min = 0;
+ input.max = 1;
+ input.step = 0.001;
+ input.value = 0;
+ const update_title = () => {
+ input.title = '' + input.value;
+ };
+ input.addEventListener('mouseover', update_title);
+ input.addEventListener('input', update_title);
+ if (param['default']) {
+ input.value = param['default'];
+ }
+ } else if (type.startsWith('int:')) {
+ const range = type.substring('int:'.length).split('|');
+ console.assert(range.length === 2, 'bad format for int control');
+ const [min, max] = range;
+ input = document.createElement('input');
+ input.dataset.isInt = true;
+ input.classList.add('entry');
+ input.type = 'number';
+ input.min = min;
+ input.max = max;
+ input.step = 1;
+ input.value = Math.round((min + max) / 2);
+ if (param['default']) {
+ input.value = param['default'];
+ }
+ } else {
+ console.error('bad control type');
+ }
+
+ input.id = 'gen-control-' + next_html_id++;
+ input.classList.add('control-input');
+ const label = document.createElement('label');
+ label.htmlFor = input.id;
+ label.appendChild(document.createTextNode(param.name));
+ if (param.description) {
+ container.title = param.description;
+ }
+ container.appendChild(label);
+ container.appendChild(document.createTextNode('='));
+ container.appendChild(input);
+ root.appendChild(container);
+ root.appendChild(document.createTextNode(' '));
+ } else {
+ // input
+ const container = document.createElement('div');
+ container.classList.add('in');
+ container.dataset.id = param.id;
+ const input_element = document.createElement('div');
+ input_element.contentEditable = true;
+ input_element.spellcheck = false;
+ input_element.addEventListener('keydown', (e) => {
+ if (e.keyCode === 13) {
+ input_element.blur();
+ e.preventDefault();
+ }
+ });
+ input_element.classList.add('entry');
+ input_element.appendChild(document.createElement('br'));
+ input_element.type = 'text';
+ input_element.id = 'gen-input-' + next_html_id++;
+ const label = document.createElement('label');
+ label.htmlFor = input_element.id;
+ if (param.description) {
+ container.title = param.description;
+ }
+ if (param['default']) {
+ input_element.innerText = param['default'];
+ }
+ label.appendChild(document.createTextNode(param.name));
+ container.appendChild(label);
+ container.appendChild(document.createTextNode('='));
+ container.appendChild(input_element);
+ root.appendChild(container);
+ root.appendChild(document.createTextNode(' '));
+
+ input_element.addEventListener('input', () => {
+ update_input_element(input_element);
+ if (auto_update_enabled()) {
+ update_shader();
+ }
+ });
+ update_input_element(input_element);
+ }
+ }
+
+ widgets_container.appendChild(root);
+ return root;
+}
+
+class GLSLGenerationState {
+ constructor(widgets) {
+ this.widgets = widgets;
+ this.declarations = new Set();
+ this.code = [];
+ this.computing_inputs = new Set();
+ this.variable = 0;
+ }
+
+ next_variable() {
+ this.variable += 1;
+ return 'v' + this.variable;
+ }
+
+ add_code(code) {
+ this.code.push(code);
+ }
+
+ get_code() {
+ return `
+${Array.from(this.declarations).join('')}
+vec3 _get_color() {
+${this.code.join('')}
+}`;
+ }
+
+ compute_input(input) {
+ input = input.trim();
+ if (input.length === 0) {
+ return { error: 'empty input' };
+ }
+ if (!isNaN(input)) {
+ return { code: float_glsl(parseFloat(input)), type: 'float' };
+ }
+
+ if (input.indexOf(',') !== -1) {
+ // vector construction
+ const items = input.split(',');
+ console.assert(items.length >= 2, 'huhhhhh??');
+ const components = [];
+ for (const item of items) {
+ const component = this.compute_input(item);
+ if ('error' in component) {
+ return component;
+ }
+ components.push(component);
+ }
+ let component_count = 0;
+ let base_type = undefined;
+ for (const component of components) {
+ const type = component.type;
+ const c = type_component_count(type);
+ if (c === 0) {
+ return { error: `cannot use type ${type} with ,` };
+ }
+ component_count += c;
+ if (base_type === undefined) {
+ base_type = type_base_type(type);
+ }
+ if (base_type !== type_base_type(type)) {
+ return { error: 'bad combination of types for ,' };
+ }
+ }
+ const type = type_vec(base_type, component_count);
+ if (type === null) {
+ // e.g. trying to combine 5 floats
+ return { error: 'bad combination of types for ,' };
+ }
+ const v = this.next_variable();
+ const component_values = components.map((c) => c.code);
+ this.add_code(`${type} ${v} = ${type}(${component_values.join()});\n`);
+ return { type: type, code: v };
+ }
+
+ if (input[0] === '#') {
+ const color = color_hex_to_float(input);
+ if (color === null) {
+ return { error: 'bad color: ' + input };
+ }
+ return input.length === 4 || input.length === 7
+ ? {
+ code: `vec3(${float_glsl(color.r)},${float_glsl(
+ color.g,
+ )},${float_glsl(color.b)})`,
+ type: 'vec3',
+ }
+ : {
+ code: `vec4(${float_glsl(color.r)},${float_glsl(
+ color.g,
+ )},${float_glsl(color.b)},${float_glsl(color.a)})`,
+ type: 'vec4',
+ };
+ }
+
+ const dot = input.lastIndexOf('.');
+ const field = dot === -1 ? 'out' : input.substring(dot + 1);
+
+ if (field.length === 0) {
+ return { error: 'inputs should not end in .' };
+ }
+
+ if (
+ field.length >= 1 &&
+ field.length <= 4 &&
+ field.split('').every((c) => 'xyzw'.indexOf(c) !== -1)
+ ) {
+ // swizzle
+ const vector = this.compute_input(input.substring(0, dot));
+ if ('error' in vector) {
+ return { error: vector.error };
+ }
+ const base = type_base_type(vector.type);
+ const count = type_component_count(vector.type);
+
+ for (const c of field) {
+ const i = 'xyzw'.indexOf(c);
+ if (i >= count) {
+ return { error: `type ${vector.type} has no field ${c}.` };
+ }
+ }
+
+ return {
+ code: `(${vector.code}).${field}`,
+ type: type_vec(base, field.length),
+ };
+ }
+
+ if (dot === 0) {
+ switch (input) {
+ case '.pos':
+ return { code: '_pos', type: 'vec2' };
+ case '.pos01':
+ return { code: '(0.5+0.5*_pos)', type: 'vec2' };
+ case '.time':
+ return { code: '_time', type: 'float' };
+ default:
+ return { error: `no such builtin: ${input}` };
+ }
+ }
+
+ if (field !== 'out') {
+ return { error: `no such field: ${field}` };
+ }
+ const widget = this.widgets.get(input);
+ if (widget === undefined) {
+ return { error: `cannot find widget '${input}'` };
+ }
+
+ if (this.computing_inputs.has(input)) {
+ return { error: 'circular dependency at ' + input };
+ }
+ this.computing_inputs.add(input);
+ const value = this.compute_widget_output(widget);
+ if (value.error) {
+ if (!value.widget) {
+ value.widget = widget.id;
+ }
+ return value;
+ }
+ this.computing_inputs.delete(input);
+ return value;
+ }
+
+ add_requirements_of(widget) {
+ for (const req_name of widget.require) {
+ const req = widget_info.get(req_name);
+ console.assert(req, 'bad widget requirement:', req_name);
+ const size0 = this.declarations.size;
+ for (const def of req.definitions) {
+ this.declarations.add(def.code);
+ }
+ if (this.declarations.size !== size0) {
+ this.add_requirements_of(req);
+ }
+ }
+ }
+
+ compute_widget_output(widget) {
+ if (widget.output) return widget.output;
+
+ const info = widget_info.get(widget.func);
+ this.add_requirements_of(info);
+ console.assert(info, 'bad widget func');
+ const args = new Map();
+ const input_types = new Map();
+ for (let [input, value] of widget.inputs) {
+ value = this.compute_input(value);
+ if (value.error) {
+ widget.output = value;
+ return value;
+ }
+ args.set(input, value.code);
+ input_types.set(input, value.type);
+ }
+ for (const control of widget.controls) {
+ args.set(control.id, control.uniform);
+ }
+
+ let best_definition = undefined;
+ let best_score = -Infinity;
+ for (const definition of info.definitions) {
+ if (definition.input_types.length !== input_types.length) continue;
+ if (definition.param_order.length !== args.length) continue;
+ let score = 0;
+ for (const [input_name, input_type] of definition.input_types) {
+ const got_type = input_types.get(input_name);
+ if (got_type === input_type) {
+ score += 1;
+ } else if (got_type === 'float') {
+ // implicit conversion
+ } else {
+ score = -Infinity;
+ }
+ }
+ if (score > best_score) {
+ best_definition = definition;
+ best_score = score;
+ }
+ }
+
+ if (!best_definition) {
+ const s = [];
+ for (const [n, t] of input_types) {
+ s.push(`${n}:${t}`);
+ }
+ return { error: `bad types for ${info.name}: ${s.join(', ')}` };
+ }
+
+ const output_var = this.next_variable();
+ const definition = best_definition;
+ const args_code = new Array(args.length);
+ for (let [arg_name, arg_code] of args) {
+ if (definition.input_types.has(arg_name)) {
+ const expected_type = definition.input_types.get(arg_name);
+ const got_type = input_types.get(arg_name);
+ if (got_type !== expected_type) {
+ arg_code = `${expected_type}(${arg_code})`;
+ }
+ }
+ args_code[definition.param_order.get(arg_name)] = arg_code;
+ }
+ const type = definition.return_type;
+ this.declarations.add(definition.code);
+ this.add_code(
+ `${type} ${output_var} = ${info.function_name}(${args_code.join(
+ ',',
+ )});\n`,
+ );
+ widget.output = {
+ code: output_var,
+ type,
+ };
+ return widget.output;
+ }
+}
+
+function parse_widgets() {
+ const widgets = new Map();
+ for (const widget_div of document.getElementsByClassName('widget')) {
+ const name = get_widget_name(widget_div);
+ const func = widget_div.dataset.func;
+ const widget_id = parseInt(widget_div.dataset.id);
+ if (!name) {
+ return {
+ error: 'widget has no name. please give it one.',
+ widget: widget_id,
+ };
+ }
+ for (const c of name) {
+ if ('.,;|/\\:(){}[]+-<>\'"`~?!#%^&*'.indexOf(c) !== -1) {
+ return {
+ error: `widget name cannot contain the character ${c}`,
+ widget: widget_id,
+ };
+ }
+ }
+ if (widgets.has(name)) {
+ return { error: `duplicate widget name: ${name}`, widget: widget_id };
+ }
+
+ const inputs = new Map();
+ const controls = [];
+ for (const input of widget_div.getElementsByClassName('in')) {
+ const input_id = input.dataset.id;
+ inputs.set(input_id, input.getElementsByClassName('entry')[0].innerText);
+ }
+ for (const control of widget_div.getElementsByClassName('control')) {
+ const control_id = control.dataset.id;
+ controls.push({
+ id: control_id,
+ uniform: `_control${widget_id}_${control_id}`,
+ type: get_control_value(widget_id, control_id).type,
+ });
+ }
+ widgets.set(name, {
+ func,
+ id: widget_id,
+ inputs,
+ controls,
+ });
+ }
+ parsed_widgets = widgets;
+ return widgets;
+}
+
+function get_control_value(widget_id, control_id) {
+ const widget = get_widget_by_id(widget_id);
+ const control = widget.querySelector(`.control[data-id="${control_id}"]`);
+ const input = control.querySelector('.control-input');
+ if (input.tagName === 'INPUT' && input.type === 'checkbox') {
+ return {
+ type: 'int',
+ value: input.checked ? 1 : 0,
+ };
+ } else if (input.tagName === 'INPUT') {
+ if (input.dataset.isInt) {
+ return {
+ type: 'int',
+ value: parseInt(input.value),
+ };
+ } else {
+ return {
+ type: 'float',
+ value: parseFloat(input.value),
+ };
+ }
+ } else if (input.tagName === 'SELECT') {
+ return {
+ type: 'int',
+ value: Array.from(input.getElementsByTagName('option'))
+ .map((o) => o.value)
+ .indexOf(input.value),
+ };
+ } else {
+ console.error(`unrecognized control tag: ${input.tagName}`);
+ }
+}
+
+function export_widgets() {
+ const widgets = parse_widgets();
+ if (widgets.error) {
+ show_error(widgets);
+ return;
+ }
+ console.assert(widgets instanceof Map);
+ const data = [];
+ for (const [name, widget] of widgets) {
+ data.push(widget.func);
+ data.push(';');
+ data.push('n:');
+ data.push(name);
+ data.push(';');
+ for (const [input, value] of widget.inputs) {
+ data.push('i');
+ data.push(input);
+ data.push(':');
+ data.push(value);
+ data.push(';');
+ }
+ for (const control of widget.controls) {
+ data.push('c');
+ data.push(control.id);
+ data.push(':');
+ data.push(get_control_value(widget.id, control.id).value);
+ data.push(';');
+ }
+ data.pop(); // remove terminal separator
+ data.push(';;');
+ }
+ data.push('_out=');
+ data.push(
+ get_widget_name(document.querySelector('.widget[data-display="1"]')),
+ );
+ return data.join('');
+}
+
+function import_widgets(string) {
+ let widgets = [];
+ let output = null;
+ if (string) {
+ console.log(string);
+ for (const widget_str of string.split(';;')) {
+ if (widget_str.startsWith('_out=')) {
+ output = widget_str.substring('_out='.length);
+ continue;
+ }
+
+ const parts = widget_str.split(';');
+ const func = parts[0];
+ const widget = {
+ name: null,
+ func,
+ inputs: new Map(),
+ controls: new Map(),
+ };
+ parts.splice(0, 1);
+ for (const part of parts) {
+ const kv = part.split(':');
+ if (kv.length !== 2) {
+ return { error: `bad key-value pair (kv count ${kv.length})` };
+ }
+ const type = kv[0][0];
+ const key = kv[0].substring(1);
+ const value = kv[1];
+ if (type === 'n') {
+ // name
+ widget.name = value;
+ } else if (type === 'i') {
+ // input
+ widget.inputs.set(key, value);
+ } else if (type === 'c') {
+ // control
+ widget.controls.set(key, value);
+ } else {
+ return { error: `bad widget part type: '${type}'` };
+ }
+ }
+
+ if (widget.name === null) {
+ return { error: 'widget has no name' };
+ }
+ widgets.push(widget);
+ }
+ } else {
+ widgets = [
+ {
+ name: 'output',
+ func: 'buffer',
+ inputs: new Map([['input', '#acabff']]),
+ controls: new Map(),
+ },
+ ];
+ output = 'output';
+ }
+
+ function assign_value(container, value) {
+ const element = container.getElementsByClassName('entry')[0];
+ if (!element) {
+ console.error('container', container, 'has no input entry');
+ } else if (element.type === 'checkbox') {
+ element.checked = value === 'true' || value === '1' ? 'checked' : '';
+ } else if (element.tagName === 'INPUT') {
+ element.value = value;
+ } else if (element.tagName === 'SELECT') {
+ const options = Array.from(element.getElementsByTagName('option')).map(
+ (o) => o.value,
+ );
+ if (value >= 0 && value < options.length) {
+ element.value = options[value];
+ } else if (options.indexOf(value) !== -1) {
+ element.value = value;
+ } else {
+ return { error: `bad import string (unrecognized value ${value})` };
+ }
+ } else if (element.tagName === 'DIV') {
+ element.innerText = value;
+ update_input_element(element);
+ } else {
+ console.error('bad element', element);
+ }
+ }
+ widgets_container.innerHTML = '';
+ for (const widget of widgets) {
+ const name = widget.name;
+ if (!widget_info.has(widget.func)) {
+ return {
+ error: `bad import string (widget type '${widget.func}' does not exist)`,
+ };
+ }
+ const element = add_widget(widget.func);
+ element.getElementsByClassName('widget-name')[0].innerText = name;
+
+ for (const [input, value] of widget.inputs) {
+ const container = Array.from(element.getElementsByClassName('in')).find(
+ (e) => e.dataset.id === input,
+ );
+ if (!container) {
+ return { error: `bad import string (input ${input} does not exist)` };
+ }
+ assign_value(container, value);
+ }
+ for (const [control, value] of widget.controls) {
+ const container = Array.from(
+ element.getElementsByClassName('control'),
+ ).find((e) => e.dataset.id === control);
+ if (!container) {
+ return {
+ error: `bad import string (control ${control} does not exist)`,
+ };
+ }
+ assign_value(container, value);
+ }
+ }
+
+ set_display_output_and_update_shader(get_widget_by_name(output));
+}
+
+function import_widgets_from_local_storage() {
+ const result = import_widgets(localStorage.getItem(`${APP_ID}-widgets`));
+ if (result && result.error) {
+ show_error(result);
+ }
+}
+
+function export_widgets_to_local_storage() {
+ const widget_str = export_widgets();
+ code_input.value = widget_str;
+ localStorage.setItem(`${APP_ID}-widgets`, widget_str);
+}
+
+function get_shader_source() {
+ const display_output = document.querySelector('.widget[data-display="1"]');
+ if (!display_output) {
+ show_error('no output chosen');
+ return null;
+ }
+ const widgets = parse_widgets();
+ if (widgets.error) {
+ show_error(widgets);
+ return null;
+ }
+ const state = new GLSLGenerationState(widgets);
+ for (const widget of widgets.values()) {
+ for (const control of widget.controls) {
+ state.declarations.add(`uniform ${control.type} ${control.uniform};\n`);
+ }
+ }
+ const output = state.compute_input(get_widget_name(display_output));
+ if (output.error) {
+ show_error(output);
+ return null;
+ }
+
+ switch (output.type) {
+ case 'float':
+ state.add_code(`return vec3(${output.code});\n`);
+ break;
+ case 'vec2':
+ state.add_code(`return vec3(${output.code}, 0.0);\n`);
+ break;
+ case 'vec3':
+ state.add_code(`return ${output.code};\n`);
+ break;
+ case 'vec4':
+ state.add_code(`return ${output.code}.xyz;\n`);
+ break;
+ default:
+ show_error(`bad type for output: ${output.type}`);
+ return null;
+ }
+
+ const code = state.get_code();
+ console.log(code);
+ export_widgets_to_local_storage();
+ return code;
+}
+
+function update_widget_choices() {
+ const search_term = widget_search.value.toLowerCase();
+ const choices = widget_choices.getElementsByClassName('widget-choice');
+ for (const choice of choices) {
+ const widget = widget_info.get(choice.dataset.id);
+ const shown =
+ widget.name.toLowerCase().indexOf(search_term) !== -1 ||
+ widget.alt.toLowerCase().indexOf(search_term) !== -1;
+ choice.style.display = shown ? 'block' : 'none';
+ }
+ for (const category of widget_choices.getElementsByClassName(
+ 'widget-category',
+ )) {
+ if (
+ Array.from(category.getElementsByClassName('widget-choice')).some(
+ (x) => x.style.display === 'block',
+ )
+ ) {
+ category.style.display = 'block';
+ category.open = search_term !== '';
+ } else {
+ category.style.display = 'none';
+ }
+ }
+}
+
+let resizing_ui = false;
+let ui_resize_offset = 0;
+
+function startup() {
+ canvas_container = document.getElementById('canvas-container');
+ canvas = document.getElementById('canvas');
+ ui_div = document.getElementById('ui');
+ ui_resize = document.getElementById('ui-resize');
+ widget_choices = document.getElementById('widget-choices');
+ widget_search = document.getElementById('widget-search');
+ widgets_container = document.getElementById('widgets-container');
+ code_input = document.getElementById('code');
+ error_element = document.getElementById('error');
+
+ ui_div.style.flexBasis = ui_div.offsetWidth + 'px'; // convert to px
+
+ // drag to resize ui
+ ui_resize.addEventListener('mousedown', (e) => {
+ resizing_ui = true;
+ const basis = ui_div.style.flexBasis;
+ console.assert(basis.endsWith('px'));
+ ui_resize_offset = basis.substring(0, basis.length - 2) - e.clientX;
+ e.preventDefault();
+ });
+ window.addEventListener('mouseup', () => {
+ resizing_ui = false;
+ });
+ window.addEventListener('mousemove', (e) => {
+ if (resizing_ui) {
+ if (e.buttons & 1) {
+ ui_div.style.flexBasis = e.clientX + ui_resize_offset + 'px';
+ } else {
+ resizing_ui = false;
+ }
+ e.preventDefault();
+ }
+ });
+
+ document.getElementById('code-form').addEventListener('submit', () => {
+ import_widgets(code_input.value);
+ });
+
+ gl = canvas.getContext('webgl2');
+ if (gl === null) {
+ // support for very-old-but-not-ancient browsers
+ gl = canvas.getContext('experimental-webgl2');
+ if (gl === null) {
+ show_error('your browser doesnt support webgl.\noh well.');
+ return;
+ }
+ }
+
+ program_post = compile_program('post', {
+ vertex: `#version 300 es
+in vec2 v_pos;
+out vec2 uv;
+
+void main() {
+ uv = v_pos * 0.5 + 0.5;
+ gl_Position = vec4(v_pos, 0.0, 1.0);
+}
+`,
+ fragment: `#version 300 es
+#ifdef GL_ES
+precision highp float;
+#endif
+uniform sampler2D u_texture;
+in vec2 uv;
+out vec4 color;
+
+void main() {
+ color = texture(u_texture, uv);
+}
+`,
+ });
+ if (program_post === null) {
+ return;
+ }
+
+ vertex_buffer_rect = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer_rect);
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array([
+ -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
+ ]),
+ gl.STATIC_DRAW,
+ );
+
+ vertex_buffer_main = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer_main);
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array([
+ -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0,
+ ]),
+ gl.STATIC_DRAW,
+ );
+
+ framebuffer_color_texture = gl.createTexture();
+ sampler_texture = gl.createTexture();
+
+ {
+ // add widget buttons
+ const categories = new Map();
+ for (const info of widget_info.values()) {
+ if (!categories.has(info.category)) {
+ categories.set(info.category, []);
+ }
+ categories.get(info.category).push(info.id);
+ }
+ const category_names = Array.from(categories.keys());
+ category_names.sort();
+
+ for (const cat of category_names) {
+ const category_element = document.createElement('details');
+ category_element.classList.add('widget-category');
+ const category_title = document.createElement('summary');
+ category_title.appendChild(document.createTextNode(cat));
+ category_element.appendChild(category_title);
+ widget_choices.appendChild(category_element);
+
+ const widgets = categories.get(cat);
+ widgets.sort((a, b) =>
+ widget_info.get(a).name.localeCompare(widget_info.get(b).name),
+ );
+ for (const id of widgets) {
+ const widget = widget_info.get(id);
+ const button = document.createElement('button');
+ button.classList.add('widget-choice');
+ if ('description' in widget) {
+ button.title = widget.description;
+ }
+ button.appendChild(document.createTextNode(widget.name));
+ button.dataset.id = id;
+ category_element.appendChild(button);
+ button.addEventListener('click', () => {
+ const root = add_widget(id);
+ const widget_name = root.querySelector('.widget-name');
+ widget_name.focus();
+ const range = document.createRange();
+ range.selectNodeContents(widget_name);
+ const sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ });
+ }
+ }
+ }
+
+ set_up_framebuffer();
+ update_widget_choices();
+ widget_search.addEventListener('input', () => {
+ update_widget_choices();
+ });
+ import_widgets_from_local_storage();
+
+ frame(0.0);
+ window.addEventListener('keydown', on_key_press);
+}
+
+function frame(time) {
+ current_time = time * 1e-3;
+
+ const container_width = canvas_container.offsetWidth;
+ const container_height = canvas_container.offsetHeight;
+ const aspect_ratio = render_width / render_height;
+ let canvas_x = 0,
+ canvas_y = 0;
+ if (container_width / aspect_ratio < container_height) {
+ // landscape mode
+ canvas_y = Math.floor((container_height - viewport_height) * 0.5);
+ viewport_width = container_width;
+ viewport_height = Math.floor(container_width / aspect_ratio);
+ } else {
+ // portrait mode
+ canvas_x = Math.floor((container_width - viewport_width) * 0.5);
+ viewport_width = Math.floor(container_height * aspect_ratio);
+ viewport_height = container_height;
+ }
+
+ canvas.width = viewport_width;
+ canvas.height = viewport_height;
+ canvas.style.left = canvas_x + 'px';
+ canvas.style.top = canvas_y + 'px';
+
+ perform_step();
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ gl.viewport(0, 0, viewport_width, viewport_height);
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.useProgram(program_post);
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer_rect);
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, sampler_texture);
+ gl.uniform1i(gl.getUniformLocation(program_post, 'u_texture'), 0);
+ const v_pos = gl.getAttribLocation(program_post, 'v_pos');
+ gl.enableVertexAttribArray(v_pos);
+ gl.vertexAttribPointer(v_pos, 2, gl.FLOAT, false, 0, 0);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+
+ if (!requestAnimationFrame) {
+ show_error('your browser doesnt support requestAnimationFrame.\noh well.');
+ return;
+ }
+ requestAnimationFrame(frame);
+}
+
+function perform_step() {
+ if (!program_main) {
+ // not properly loaded yet
+ return;
+ }
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
+ gl.viewport(0, 0, render_width, render_height);
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ gl.useProgram(program_main);
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(gl.TEXTURE_2D, sampler_texture);
+ gl.uniform1i(gl.getUniformLocation(program_main, '_texture'), 0);
+ gl.uniform1f(
+ gl.getUniformLocation(program_main, '_time'),
+ current_time % 3600,
+ );
+ gl.uniform2f(
+ gl.getUniformLocation(program_main, '_texture_size'),
+ render_width,
+ render_height,
+ );
+
+ if (parsed_widgets) {
+ for (const widget of parsed_widgets.values()) {
+ for (const control of widget.controls) {
+ const loc = gl.getUniformLocation(program_main, control.uniform);
+ const { type, value } = get_control_value(widget.id, control.id);
+ switch (type) {
+ case 'int':
+ gl.uniform1i(loc, value);
+ break;
+ case 'float':
+ gl.uniform1f(loc, value);
+ break;
+ }
+ }
+ }
+ }
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer_main);
+ const v_pos = gl.getAttribLocation(program_main, 'v_pos');
+ gl.enableVertexAttribArray(v_pos);
+ gl.vertexAttribPointer(v_pos, 2, gl.FLOAT, false, 8, 0);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+
+ gl.bindTexture(gl.TEXTURE_2D, sampler_texture);
+ gl.copyTexImage2D(
+ gl.TEXTURE_2D,
+ 0,
+ gl.RGBA,
+ 0,
+ 0,
+ render_width,
+ render_height,
+ 0,
+ );
+}
+
+function compile_program(name, shaders) {
+ const program = gl.createProgram();
+ for (const type in shaders) {
+ const source = shaders[type];
+ let gl_type;
+ if (type === 'vertex') {
+ gl_type = gl.VERTEX_SHADER;
+ } else if (type === 'fragment') {
+ gl_type = gl.FRAGMENT_SHADER;
+ } else {
+ show_error('unrecognized shader type: ' + type);
+ return null;
+ }
+ const shader = compile_shader(name + ' ' + type, gl_type, source);
+ if (shader === null) return null;
+ gl.attachShader(program, shader);
+ }
+
+ gl.linkProgram(program);
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ show_error(
+ 'Error linking shader program:\n' + gl.getProgramInfoLog(program),
+ );
+ return null;
+ }
+ return program;
+}
+
+function set_up_framebuffer() {
+ framebuffer = gl.createFramebuffer();
+ const sampler_pixels = new Uint8Array(render_width * render_height * 4);
+ sampler_pixels.fill(0);
+ set_up_rgba_texture(
+ sampler_texture,
+ render_width,
+ render_height,
+ sampler_pixels,
+ );
+ set_up_rgba_texture(
+ framebuffer_color_texture,
+ render_width,
+ render_height,
+ null,
+ );
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
+ gl.framebufferTexture2D(
+ gl.FRAMEBUFFER,
+ gl.COLOR_ATTACHMENT0,
+ gl.TEXTURE_2D,
+ framebuffer_color_texture,
+ 0,
+ );
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
+ if (status !== gl.FRAMEBUFFER_COMPLETE) {
+ show_error('Error: framebuffer incomplete (status ' + status + ')');
+ return;
+ }
+}
+
+function set_up_rgba_texture(texture, width, height, pixels) {
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(
+ gl.TEXTURE_2D,
+ 0,
+ gl.RGBA,
+ width,
+ height,
+ 0,
+ gl.RGBA,
+ gl.UNSIGNED_BYTE,
+ pixels,
+ );
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+}
+
+function compile_shader(name, type, source) {
+ const shader = gl.createShader(type);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ show_error(
+ 'Error compiling shader ' + name + ':\n' + gl.getShaderInfoLog(shader),
+ );
+ return null;
+ }
+ return shader;
+}
+
+function clear_error() {
+ error_element.style.display = 'none';
+ for (const widget of document.querySelectorAll('.widget.error')) {
+ widget.classList.remove('error');
+ }
+}
+
+function show_error(error) {
+ if (error.error) {
+ if (error.widget) {
+ get_widget_by_id(error.widget).classList.add('error');
+ }
+ error = error.error;
+ }
+ console.log('error:', error);
+ error_element.style.display = 'block';
+ error_element.innerText = error;
+}