- kaleido: a field folded about the frame centre for symmetric compositions; octaves is the fold order - temporal_rate socket on Direct, Walk and Source sets how fast a held pattern evolves across a clip - drift with use_temporal_coherence no longer collapses to jump; each draw holds its own seed - gaussian no longer shares its first channel's seed with the sampler's base noise - verification/describe_field.py scores a shader type without rendering; analyze.py --bands Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2022 lines
110 KiB
JavaScript
2022 lines
110 KiB
JavaScript
/**
|
|
* shader_renderer.ts - Adds shader visualization to ShaderNoiseKSampler node
|
|
* This file has a single responsibility: rendering shaders using a canvas approach
|
|
*/
|
|
// @ts-ignore - Runtime ComfyUI import
|
|
import { app } from "../../../scripts/app.js";
|
|
// ============================
|
|
// Helper Functions
|
|
// ============================
|
|
const COLOR_SCHEME_NAMES = {
|
|
"none": "Black & White", "blue_red": "Blue to Red", "viridis": "Viridis",
|
|
"plasma": "Plasma", "inferno": "Inferno", "magma": "Magma", "turbo": "Turbo",
|
|
"jet": "Jet", "rainbow": "Rainbow", "cool": "Cool", "hot": "Hot",
|
|
"parula": "Parula", "hsv": "HSV", "autumn": "Autumn", "winter": "Winter",
|
|
"spring": "Spring", "summer": "Summer", "copper": "Copper", "pink": "Pink",
|
|
"bone": "Bone", "ocean": "Ocean", "terrain": "Terrain", "neon": "Neon", "fire": "Fire"
|
|
};
|
|
function getColorSchemeName(scheme) {
|
|
return COLOR_SCHEME_NAMES[scheme] || scheme;
|
|
}
|
|
const SHAPE_TYPE_MAP = {
|
|
'none': 0, 'radial': 1, 'linear': 2, 'spiral': 3, 'checkerboard': 4,
|
|
'spots': 5, 'hexgrid': 6, 'stripes': 7, 'gradient': 8, 'vignette': 9,
|
|
'cross': 10, 'stars': 11, 'triangles': 12, 'concentric': 13, 'rays': 14, 'zigzag': 15
|
|
};
|
|
const COLOR_SCHEME_MAP = {
|
|
'none': 0, 'blue_red': 1, 'viridis': 2, 'plasma': 3, 'inferno': 4,
|
|
'magma': 5, 'turbo': 6, 'jet': 7, 'rainbow': 8, 'cool': 9, 'hot': 10,
|
|
'parula': 11, 'hsv': 12, 'autumn': 13, 'winter': 14, 'spring': 15,
|
|
'summer': 16, 'copper': 17, 'pink': 18, 'bone': 19, 'ocean': 20,
|
|
'terrain': 21, 'neon': 22, 'fire': 23
|
|
};
|
|
// ============================
|
|
// GLSL Shader Sources
|
|
// ============================
|
|
const VERTEX_SHADER_SOURCE = `
|
|
attribute vec2 a_position;
|
|
varying vec2 v_texCoord;
|
|
void main() {
|
|
v_texCoord = a_position * 0.5 + 0.5;
|
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
}
|
|
`;
|
|
// Common fragment shader header with shared noise functions, shape masks, and color schemes
|
|
const FRAGMENT_SHADER_HEADER = `
|
|
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
precision highp float;
|
|
#else
|
|
precision mediump float;
|
|
#endif
|
|
uniform float u_time;
|
|
uniform float u_intensity;
|
|
uniform float u_scale;
|
|
uniform float u_octaves;
|
|
uniform float u_persistence;
|
|
uniform float u_lacunarity;
|
|
uniform int u_shapeType;
|
|
uniform float u_shapeStrength;
|
|
uniform float u_warpStrength;
|
|
uniform float u_phaseShift;
|
|
uniform int u_frequencyRange;
|
|
uniform int u_distribution;
|
|
uniform float u_adaptationStrength;
|
|
uniform float u_resolutionScale;
|
|
uniform int u_colorScheme;
|
|
varying vec2 v_texCoord;
|
|
|
|
// Note: We no longer use resolution scaling in the shader
|
|
// The pixel density is now directly controlled by the canvas resolution
|
|
|
|
// Shared utility functions
|
|
vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
|
|
vec4 permute(vec4 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
|
|
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
|
|
vec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }
|
|
|
|
float snoise(vec2 v){
|
|
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
|
|
-0.577350269189626, 0.024390243902439);
|
|
vec2 i = floor(v + dot(v, C.yy));
|
|
vec2 x0 = v - i + dot(i, C.xx);
|
|
vec2 i1;
|
|
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
|
vec4 x12 = x0.xyxy + C.xxzz;
|
|
x12.xy -= i1;
|
|
i = mod(i, 289.0);
|
|
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
|
|
+ i.x + vec3(0.0, i1.x, 1.0 ));
|
|
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
|
|
dot(x12.zw,x12.zw)), 0.0);
|
|
m = m*m;
|
|
m = m*m;
|
|
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
|
vec3 h = abs(x) - 0.5;
|
|
vec3 ox = floor(x + 0.5);
|
|
vec3 a0 = x - ox;
|
|
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
|
|
vec3 g;
|
|
g.x = a0.x * x0.x + h.x * x0.y;
|
|
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
|
return 130.0 * dot(m, g);
|
|
}
|
|
|
|
// 3D simplex noise (Ashima), for the types whose Python evaluates a 3D field
|
|
float snoise3(vec3 v) {
|
|
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
|
|
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
vec3 i = floor(v + dot(v, C.yyy));
|
|
vec3 x0 = v - i + dot(i, C.xxx);
|
|
vec3 g = step(x0.yzx, x0.xyz);
|
|
vec3 l = 1.0 - g;
|
|
vec3 i1 = min(g.xyz, l.zxy);
|
|
vec3 i2 = max(g.xyz, l.zxy);
|
|
vec3 x1 = x0 - i1 + C.xxx;
|
|
vec3 x2 = x0 - i2 + C.yyy;
|
|
vec3 x3 = x0 - D.yyy;
|
|
i = mod(i, 289.0);
|
|
vec4 p = permute(permute(permute(
|
|
i.z + vec4(0.0, i1.z, i2.z, 1.0))
|
|
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
|
|
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
|
|
float n_ = 0.142857142857;
|
|
vec3 ns = n_ * D.wyz - D.xzx;
|
|
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
vec4 x_ = floor(j * ns.z);
|
|
vec4 y_ = floor(j - 7.0 * x_);
|
|
vec4 x = x_ * ns.x + ns.yyyy;
|
|
vec4 y = y_ * ns.x + ns.yyyy;
|
|
vec4 h = 1.0 - abs(x) - abs(y);
|
|
vec4 b0 = vec4(x.xy, y.xy);
|
|
vec4 b1 = vec4(x.zw, y.zw);
|
|
vec4 s0 = floor(b0) * 2.0 + 1.0;
|
|
vec4 s1 = floor(b1) * 2.0 + 1.0;
|
|
vec4 sh = -step(h, vec4(0.0));
|
|
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
|
|
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
|
|
vec3 p0 = vec3(a0.xy, h.x);
|
|
vec3 p1 = vec3(a0.zw, h.y);
|
|
vec3 p2 = vec3(a1.xy, h.z);
|
|
vec3 p3 = vec3(a1.zw, h.w);
|
|
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
|
|
p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
|
|
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
|
|
m = m * m;
|
|
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
|
|
}
|
|
|
|
// Random function
|
|
float random(vec2 st) {
|
|
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
|
}
|
|
|
|
// Shape mask function that varies based on type
|
|
float applyShapeMask(vec2 st, int type) {
|
|
if (type == 0) { // none
|
|
return 1.0;
|
|
} else if (type == 1) { // radial
|
|
// Create a radial gradient from center with animation
|
|
vec2 center = vec2(0.5, 0.5);
|
|
|
|
// Animate center position
|
|
center += 0.2 * vec2(cos(u_time), sin(u_time));
|
|
|
|
float dist = distance(st, center) * 2.0;
|
|
return clamp(1.0 - dist, 0.0, 1.0);
|
|
} else if (type == 2) { // linear
|
|
// Create a linear gradient with animation
|
|
float x_offset = fract(u_time * 0.2) * 2.0;
|
|
float shifted_x = fract(st.x + x_offset);
|
|
return shifted_x;
|
|
} else if (type == 3) { // spiral
|
|
// Create a spiral pattern with animation
|
|
vec2 centered = st - vec2(0.5, 0.5);
|
|
float theta = atan(centered.y, centered.x);
|
|
float r = length(centered) * 2.0;
|
|
theta += u_time;
|
|
return fract((theta / (2.0 * 3.14159265) + r));
|
|
} else if (type == 4) { // checkerboard
|
|
// Create animated checkerboard
|
|
float grid_size = 8.0;
|
|
float x_offset = u_time * grid_size * 0.2;
|
|
float y_offset = u_time * grid_size * 0.1;
|
|
float x_grid = floor((st.x + x_offset / grid_size) * grid_size) * 0.5;
|
|
float y_grid = floor((st.y + y_offset / grid_size) * grid_size) * 0.5;
|
|
return mod(x_grid + y_grid, 1.0);
|
|
} else if (type == 5) { // spots
|
|
// Create animated spots
|
|
float mask = 0.0;
|
|
int num_spots = 10;
|
|
|
|
// Use deterministic pseudo-random positions based on index
|
|
for (int i = 0; i < 10; i++) {
|
|
if (i >= num_spots) break;
|
|
// Better randomization
|
|
float rand_x = fract(sin(float(i) * 78.233) * 43758.5453);
|
|
float rand_y = fract(sin(float(i) * 12.9898) * 43758.5453);
|
|
float size = fract(sin(float(i) * 93.719) * 43758.5453) * 0.3 + 0.1;
|
|
|
|
// Animate spots
|
|
float angle = u_time + float(i);
|
|
vec2 spot_pos = vec2(
|
|
0.5 + cos(angle) * 0.4 * rand_x,
|
|
0.5 + sin(angle) * 0.4 * rand_y
|
|
);
|
|
|
|
// Pulse size
|
|
size *= 1.0 + 0.2 * sin(u_time * 2.0 + float(i));
|
|
|
|
// Calculate spot mask
|
|
float dist = distance(st, spot_pos);
|
|
float spot_mask = clamp(1.0 - dist / size, 0.0, 1.0);
|
|
mask = max(mask, spot_mask);
|
|
}
|
|
|
|
return mask;
|
|
} else if (type == 6) { // hexgrid
|
|
// Create animated hexagonal grid
|
|
vec2 hex_uv = st * 6.0; // Scale for hex grid
|
|
|
|
// Apply animation
|
|
hex_uv.x += sin(u_time * 0.5) * 0.5;
|
|
hex_uv.y += cos(u_time * 0.3) * 0.5;
|
|
|
|
// Hexagon grid math
|
|
vec2 r = vec2(1.0, 1.73); // Hexagon ratio
|
|
vec2 h = r * 0.5;
|
|
vec2 a = mod(hex_uv, r) - h;
|
|
vec2 b = mod(hex_uv + h, r) - h;
|
|
|
|
// Determine distance to hexagon centers
|
|
float dist = min(length(a), length(b));
|
|
|
|
// Create cells with smooth borders
|
|
float cell_size = 0.3 + 0.1 * sin(u_time);
|
|
return smoothstep(cell_size + 0.05, cell_size - 0.05, dist);
|
|
} else if (type == 7) { // stripes
|
|
// Animated stripes pattern
|
|
float freq = 10.0;
|
|
float angle = 0.5 * sin(u_time * 0.2);
|
|
|
|
// Compute rotated coordinates
|
|
vec2 rotated = vec2(
|
|
st.x * cos(angle) - st.y * sin(angle),
|
|
st.x * sin(angle) + st.y * cos(angle)
|
|
);
|
|
|
|
// Animated stripe pattern
|
|
float stripes = sin(rotated.x * freq + u_time);
|
|
|
|
// Create binary stripes with smoothed edges
|
|
return smoothstep(0.0, 0.1, stripes) * (1.0 - smoothstep(0.0, 0.1, -stripes));
|
|
} else if (type == 8) { // gradient
|
|
// Animated moving gradient
|
|
float angle = u_time * 0.2;
|
|
vec2 dir = vec2(cos(angle), sin(angle));
|
|
|
|
// Project position onto direction vector
|
|
float proj = dot(st - 0.5, dir) + 0.5;
|
|
|
|
// Smooth gradient
|
|
return proj;
|
|
} else if (type == 9) { // vignette
|
|
// Animated vignette effect
|
|
vec2 center = vec2(0.5) + vec2(
|
|
0.2 * sin(u_time * 0.3),
|
|
0.2 * cos(u_time * 0.4)
|
|
);
|
|
|
|
float dist = distance(st, center);
|
|
|
|
// Animated vignette radius
|
|
float radius = 0.6 + 0.2 * sin(u_time * 0.5);
|
|
float smoothness = 0.3;
|
|
|
|
return 1.0 - smoothstep(radius - smoothness, radius, dist);
|
|
} else if (type == 10) { // cross
|
|
// Animated cross pattern
|
|
float thickness = 0.1 + 0.05 * sin(u_time);
|
|
float rotation = u_time * 0.2;
|
|
|
|
// Rotate the coordinates
|
|
vec2 centered = st - 0.5;
|
|
vec2 rotated = vec2(
|
|
centered.x * cos(rotation) - centered.y * sin(rotation),
|
|
centered.x * sin(rotation) + centered.y * cos(rotation)
|
|
);
|
|
rotated += 0.5;
|
|
|
|
// Create horizontal and vertical bars
|
|
float h_bar = smoothstep(0.5 - thickness, 0.5 - thickness + 0.02, rotated.y) *
|
|
(1.0 - smoothstep(0.5 + thickness - 0.02, 0.5 + thickness, rotated.y));
|
|
float v_bar = smoothstep(0.5 - thickness, 0.5 - thickness + 0.02, rotated.x) *
|
|
(1.0 - smoothstep(0.5 + thickness - 0.02, 0.5 + thickness, rotated.x));
|
|
|
|
return max(h_bar, v_bar);
|
|
} else if (type == 11) { // stars
|
|
// Animated star field
|
|
float mask = 0.0;
|
|
int num_stars = 20;
|
|
|
|
// Generate star field
|
|
for (int i = 0; i < 20; i++) {
|
|
if (i >= num_stars) break;
|
|
|
|
// Deterministic star positions
|
|
float rand_x = fract(sin(float(i) * 78.233) * 43758.5453);
|
|
float rand_y = fract(sin(float(i) * 12.9898) * 43758.5453);
|
|
|
|
// Star position with slow drift
|
|
vec2 star_pos = vec2(
|
|
fract(rand_x + 0.05 * sin(u_time * 0.1 + float(i))),
|
|
fract(rand_y + 0.05 * cos(u_time * 0.15 + float(i) * 1.5))
|
|
);
|
|
|
|
// Star size and brightness (twinkling)
|
|
float brightness = 0.5 + 0.5 * sin(u_time * (0.5 + rand_x * 0.5) + float(i));
|
|
float size = 0.01 + 0.015 * rand_y * brightness;
|
|
|
|
// Calculate star mask with softer edge
|
|
float dist = distance(st, star_pos);
|
|
float star_mask = smoothstep(size, size * 0.5, dist) * brightness;
|
|
|
|
// Accumulate stars
|
|
mask = max(mask, star_mask);
|
|
}
|
|
|
|
return mask;
|
|
} else if (type == 12) { // triangles
|
|
// Animated triangle pattern
|
|
float time = u_time * 0.2;
|
|
float scale = 5.0;
|
|
|
|
// Apply animation to coordinates
|
|
vec2 uv = st * scale;
|
|
uv.x += sin(time) * 0.5;
|
|
uv.y += cos(time * 0.7) * 0.5;
|
|
|
|
// Triangle grid
|
|
vec2 grid = floor(uv);
|
|
vec2 gv = fract(uv) - 0.5;
|
|
|
|
// Determine which half of the square we're in
|
|
float t = step(gv.x, gv.y);
|
|
|
|
// Calculate distance to triangle edge
|
|
vec2 ab = vec2(t, t);
|
|
vec2 bc = vec2(0.5 - gv.y, 0.5 - gv.x) * (1.0 - t) + vec2(-0.5 - gv.y, 0.5 - gv.x) * t;
|
|
vec2 ca = vec2(-0.5 - gv.x, -0.5 - gv.y) * (1.0 - t) + vec2(0.5 - gv.x, -0.5 - gv.y) * t;
|
|
|
|
// Minimum distance to the triangle edges
|
|
float d_ab = dot(gv - ab * 0.5, normalize(vec2(-ab.y, ab.x)));
|
|
float d_bc = dot(gv - ab - bc * 0.5, normalize(vec2(-bc.y, bc.x)));
|
|
float d_ca = dot(gv - ab - bc - ca * 0.5, normalize(vec2(-ca.y, ca.x)));
|
|
|
|
float d = min(min(d_ab, d_bc), d_ca);
|
|
|
|
// Create triangle pattern with pulsing border width
|
|
float border_width = 0.05 + 0.03 * sin(time * 1.5);
|
|
return smoothstep(border_width, border_width - 0.02, abs(d));
|
|
} else if (type == 13) { // concentric
|
|
// Animated concentric circles
|
|
vec2 center = vec2(0.5) + vec2(
|
|
0.2 * sin(u_time * 0.3),
|
|
0.2 * cos(u_time * 0.4)
|
|
);
|
|
|
|
float dist = distance(st, center);
|
|
|
|
// Animated frequency and phase
|
|
float freq = 10.0 + 5.0 * sin(u_time * 0.1);
|
|
float phase = u_time * 0.5;
|
|
|
|
// Create concentric rings
|
|
float rings = sin(dist * freq + phase);
|
|
|
|
// Create binary rings with smoothed edges
|
|
return smoothstep(0.0, 0.1, rings) * (1.0 - smoothstep(0.0, 0.1, -rings));
|
|
} else if (type == 14) { // rays
|
|
// Animated rays from center
|
|
vec2 center = vec2(0.5) + vec2(
|
|
0.1 * sin(u_time * 0.3),
|
|
0.1 * cos(u_time * 0.4)
|
|
);
|
|
|
|
vec2 toCenter = st - center;
|
|
float angle = atan(toCenter.y, toCenter.x);
|
|
|
|
// Animated frequency and phase for rays
|
|
float freq = 8.0;
|
|
float phase = u_time * 0.5;
|
|
|
|
// Create rays with smooth transitions
|
|
float rays = sin(angle * freq + phase);
|
|
|
|
// Create binary rays with smoothed edges and distance falloff
|
|
float dist = length(toCenter);
|
|
float falloff = 1.0 - smoothstep(0.0, 0.8, dist);
|
|
|
|
return smoothstep(0.0, 0.3, rays) * falloff;
|
|
} else if (type == 15) { // zigzag
|
|
// Animated zigzag pattern
|
|
float freq = 10.0;
|
|
float angle = 0.5 * sin(u_time * 0.2);
|
|
|
|
// Compute rotated coordinates
|
|
vec2 rotated = vec2(
|
|
st.x * cos(angle) - st.y * sin(angle),
|
|
st.x * sin(angle) + st.y * cos(angle)
|
|
);
|
|
|
|
// Create two perpendicular triangle waves
|
|
float zigzag1 = abs(2.0 * fract(rotated.x * freq - u_time * 0.5) - 1.0);
|
|
float zigzag2 = abs(2.0 * fract(rotated.y * freq + u_time * 0.3) - 1.0);
|
|
|
|
// Combine zigzag patterns
|
|
float zigzag = min(zigzag1, zigzag2);
|
|
|
|
// Create crisp zigzag lines with varying thickness
|
|
float thickness = 0.3 + 0.1 * sin(u_time);
|
|
return step(thickness, zigzag);
|
|
}
|
|
|
|
return 1.0; // Fallback
|
|
}
|
|
|
|
// Color mapping function based on the selected color scheme
|
|
vec3 getColor(float t, int colorScheme) {
|
|
// Map t from [-1, 1] to [0, 1] for color mapping
|
|
float normalized = (t + 1.0) * 0.5;
|
|
|
|
vec3 color;
|
|
|
|
// Switch based on color scheme
|
|
if (colorScheme == 0) { // none (Black & White)
|
|
return vec3(normalized);
|
|
}
|
|
else if (colorScheme == 1) { // blue_red
|
|
return mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), normalized);
|
|
}
|
|
else if (colorScheme == 2) { // viridis
|
|
vec3 c0 = vec3(0.267, 0.005, 0.329); // #440154
|
|
vec3 c1 = vec3(0.188, 0.407, 0.553); // #30678D
|
|
vec3 c2 = vec3(0.208, 0.718, 0.471); // #35B778
|
|
vec3 c3 = vec3(0.992, 0.906, 0.143); // #FDE724
|
|
|
|
if (normalized < 0.33) {
|
|
return mix(c0, c1, normalized * 3.0);
|
|
} else if (normalized < 0.66) {
|
|
return mix(c1, c2, (normalized - 0.33) * 3.0);
|
|
} else {
|
|
return mix(c2, c3, (normalized - 0.66) * 3.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 3) { // plasma
|
|
vec3 c0 = vec3(0.050, 0.031, 0.529); // #0D0887
|
|
vec3 c1 = vec3(0.494, 0.012, 0.659); // #7E03A8
|
|
vec3 c2 = vec3(0.800, 0.275, 0.471); // #CC4678
|
|
vec3 c3 = vec3(0.973, 0.584, 0.255); // #F89441
|
|
vec3 c4 = vec3(0.941, 0.973, 0.129); // #F0F921
|
|
|
|
if (normalized < 0.25) {
|
|
return mix(c0, c1, normalized * 4.0);
|
|
} else if (normalized < 0.5) {
|
|
return mix(c1, c2, (normalized - 0.25) * 4.0);
|
|
} else if (normalized < 0.75) {
|
|
return mix(c2, c3, (normalized - 0.5) * 4.0);
|
|
} else {
|
|
return mix(c3, c4, (normalized - 0.75) * 4.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 4) { // inferno
|
|
vec3 c0 = vec3(0.001, 0.001, 0.016); // #000004
|
|
vec3 c1 = vec3(0.259, 0.039, 0.408); // #420A68
|
|
vec3 c2 = vec3(0.576, 0.149, 0.404); // #932667
|
|
vec3 c3 = vec3(0.867, 0.318, 0.227); // #DD513A
|
|
vec3 c4 = vec3(0.988, 0.647, 0.039); // #FCA50A
|
|
vec3 c5 = vec3(0.988, 1.000, 0.643); // #FCFFA4
|
|
|
|
if (normalized < 0.2) {
|
|
return mix(c0, c1, normalized * 5.0);
|
|
} else if (normalized < 0.4) {
|
|
return mix(c1, c2, (normalized - 0.2) * 5.0);
|
|
} else if (normalized < 0.6) {
|
|
return mix(c2, c3, (normalized - 0.4) * 5.0);
|
|
} else if (normalized < 0.8) {
|
|
return mix(c3, c4, (normalized - 0.6) * 5.0);
|
|
} else {
|
|
return mix(c4, c5, (normalized - 0.8) * 5.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 5) { // magma
|
|
vec3 c0 = vec3(0.001, 0.001, 0.016); // #000004
|
|
vec3 c1 = vec3(0.231, 0.059, 0.439); // #3B0F70
|
|
vec3 c2 = vec3(0.549, 0.161, 0.506); // #8C2981
|
|
vec3 c3 = vec3(0.871, 0.288, 0.408); // #DE4968
|
|
vec3 c4 = vec3(0.996, 0.624, 0.427); // #FE9F6D
|
|
vec3 c5 = vec3(0.988, 0.992, 0.749); // #FCFDBF
|
|
|
|
if (normalized < 0.2) {
|
|
return mix(c0, c1, normalized * 5.0);
|
|
} else if (normalized < 0.4) {
|
|
return mix(c1, c2, (normalized - 0.2) * 5.0);
|
|
} else if (normalized < 0.6) {
|
|
return mix(c2, c3, (normalized - 0.4) * 5.0);
|
|
} else if (normalized < 0.8) {
|
|
return mix(c3, c4, (normalized - 0.6) * 5.0);
|
|
} else {
|
|
return mix(c4, c5, (normalized - 0.8) * 5.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 6) { // turbo
|
|
vec3 c0 = vec3(0.188, 0.071, 0.235); // #30123b
|
|
vec3 c1 = vec3(0.275, 0.408, 0.859); // #4669db
|
|
vec3 c2 = vec3(0.149, 0.749, 0.549); // #26bf8c
|
|
vec3 c3 = vec3(0.831, 1.000, 0.314); // #d4ff50
|
|
vec3 c4 = vec3(0.980, 0.718, 0.298); // #fab74c
|
|
vec3 c5 = vec3(0.729, 0.004, 0.000); // #ba0100
|
|
|
|
if (normalized < 0.2) {
|
|
return mix(c0, c1, normalized * 5.0);
|
|
} else if (normalized < 0.4) {
|
|
return mix(c1, c2, (normalized - 0.2) * 5.0);
|
|
} else if (normalized < 0.6) {
|
|
return mix(c2, c3, (normalized - 0.4) * 5.0);
|
|
} else if (normalized < 0.8) {
|
|
return mix(c3, c4, (normalized - 0.6) * 5.0);
|
|
} else {
|
|
return mix(c4, c5, (normalized - 0.8) * 5.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 7) { // jet
|
|
vec3 c0 = vec3(0.000, 0.000, 0.498); // #00007f
|
|
vec3 c1 = vec3(0.000, 0.000, 1.000); // #0000ff
|
|
vec3 c2 = vec3(0.000, 1.000, 1.000); // #00ffff
|
|
vec3 c3 = vec3(1.000, 1.000, 0.000); // #ffff00
|
|
vec3 c4 = vec3(1.000, 0.000, 0.000); // #ff0000
|
|
vec3 c5 = vec3(0.498, 0.000, 0.000); // #7f0000
|
|
|
|
if (normalized < 0.2) {
|
|
return mix(c0, c1, normalized * 5.0);
|
|
} else if (normalized < 0.4) {
|
|
return mix(c1, c2, (normalized - 0.2) * 5.0);
|
|
} else if (normalized < 0.6) {
|
|
return mix(c2, c3, (normalized - 0.4) * 5.0);
|
|
} else if (normalized < 0.8) {
|
|
return mix(c3, c4, (normalized - 0.6) * 5.0);
|
|
} else {
|
|
return mix(c4, c5, (normalized - 0.8) * 5.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 8) { // rainbow
|
|
vec3 c0 = vec3(0.431, 0.251, 0.667); // #6e40aa
|
|
vec3 c1 = vec3(0.075, 0.600, 0.851); // #1399d9
|
|
vec3 c2 = vec3(0.122, 0.745, 0.243); // #1fbe3e
|
|
vec3 c3 = vec3(0.816, 0.757, 0.004); // #d0c101
|
|
vec3 c4 = vec3(0.694, 0.027, 0.478); // #b1077a
|
|
|
|
if (normalized < 0.25) {
|
|
return mix(c0, c1, normalized * 4.0);
|
|
} else if (normalized < 0.5) {
|
|
return mix(c1, c2, (normalized - 0.25) * 4.0);
|
|
} else if (normalized < 0.75) {
|
|
return mix(c2, c3, (normalized - 0.5) * 4.0);
|
|
} else {
|
|
return mix(c3, c4, (normalized - 0.75) * 4.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 9) { // cool
|
|
vec3 c0 = vec3(0.000, 1.000, 1.000); // #00ffff
|
|
vec3 c1 = vec3(1.000, 0.000, 1.000); // #ff00ff
|
|
|
|
return mix(c0, c1, normalized);
|
|
}
|
|
else if (colorScheme == 10) { // hot
|
|
vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
|
|
vec3 c1 = vec3(1.000, 0.000, 0.000); // #ff0000
|
|
vec3 c2 = vec3(1.000, 1.000, 0.000); // #ffff00
|
|
vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
|
|
|
|
if (normalized < 0.33) {
|
|
return mix(c0, c1, normalized * 3.0);
|
|
} else if (normalized < 0.66) {
|
|
return mix(c1, c2, (normalized - 0.33) * 3.0);
|
|
} else {
|
|
return mix(c2, c3, (normalized - 0.66) * 3.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 11) { // parula
|
|
vec3 c0 = vec3(0.208, 0.165, 0.529); // #352a87
|
|
vec3 c1 = vec3(0.059, 0.361, 0.867); // #0f5cdd
|
|
vec3 c2 = vec3(0.000, 0.710, 0.651); // #00b5a6
|
|
vec3 c3 = vec3(1.000, 0.765, 0.216); // #ffc337
|
|
vec3 c4 = vec3(0.988, 0.996, 0.643); // #fcfea4
|
|
|
|
if (normalized < 0.25) {
|
|
return mix(c0, c1, normalized * 4.0);
|
|
} else if (normalized < 0.5) {
|
|
return mix(c1, c2, (normalized - 0.25) * 4.0);
|
|
} else if (normalized < 0.75) {
|
|
return mix(c2, c3, (normalized - 0.5) * 4.0);
|
|
} else {
|
|
return mix(c3, c4, (normalized - 0.75) * 4.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 12) { // hsv
|
|
// HSV color wheel implemented directly
|
|
float h = clamp(normalized, 0.0, 0.9999) * 6.0;
|
|
int i = int(floor(h));
|
|
float f = h - float(i);
|
|
|
|
float v = 1.0;
|
|
float s = 1.0;
|
|
float p = v * (1.0 - s);
|
|
float q = v * (1.0 - s * f);
|
|
float t = v * (1.0 - s * (1.0 - f));
|
|
|
|
if (i == 0) return vec3(v, t, p);
|
|
else if (i == 1) return vec3(q, v, p);
|
|
else if (i == 2) return vec3(p, v, t);
|
|
else if (i == 3) return vec3(p, q, v);
|
|
else if (i == 4) return vec3(t, p, v);
|
|
else return vec3(v, p, q);
|
|
}
|
|
else if (colorScheme == 13) { // autumn
|
|
vec3 c0 = vec3(1.000, 0.000, 0.000); // #ff0000
|
|
vec3 c1 = vec3(1.000, 1.000, 0.000); // #ffff00
|
|
|
|
return mix(c0, c1, normalized);
|
|
}
|
|
else if (colorScheme == 14) { // winter
|
|
vec3 c0 = vec3(0.000, 0.000, 1.000); // #0000ff
|
|
vec3 c1 = vec3(0.000, 1.000, 1.000); // #00ffff
|
|
|
|
return mix(c0, c1, normalized);
|
|
}
|
|
else if (colorScheme == 15) { // spring
|
|
vec3 c0 = vec3(1.000, 0.000, 1.000); // #ff00ff
|
|
vec3 c1 = vec3(1.000, 1.000, 0.000); // #ffff00
|
|
|
|
return mix(c0, c1, normalized);
|
|
}
|
|
else if (colorScheme == 16) { // summer
|
|
vec3 c0 = vec3(0.000, 0.502, 0.400); // #008066
|
|
vec3 c1 = vec3(1.000, 1.000, 0.400); // #ffff66
|
|
|
|
return mix(c0, c1, normalized);
|
|
}
|
|
else if (colorScheme == 17) { // copper
|
|
vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
|
|
vec3 c1 = vec3(1.000, 0.600, 0.400); // #ff9966
|
|
|
|
return mix(c0, c1, normalized);
|
|
}
|
|
else if (colorScheme == 18) { // pink
|
|
vec3 c0 = vec3(0.051, 0.051, 0.051); // #0d0d0d
|
|
vec3 c1 = vec3(1.000, 0.000, 1.000); // #ff00ff
|
|
vec3 c2 = vec3(1.000, 1.000, 1.000); // #ffffff
|
|
|
|
if (normalized < 0.5) {
|
|
return mix(c0, c1, normalized * 2.0);
|
|
} else {
|
|
return mix(c1, c2, (normalized - 0.5) * 2.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 19) { // bone
|
|
vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
|
|
vec3 c1 = vec3(0.329, 0.329, 0.455); // #545474
|
|
vec3 c2 = vec3(0.627, 0.757, 0.757); // #a0c1c1
|
|
vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
|
|
|
|
if (normalized < 0.33) {
|
|
return mix(c0, c1, normalized * 3.0);
|
|
} else if (normalized < 0.66) {
|
|
return mix(c1, c2, (normalized - 0.33) * 3.0);
|
|
} else {
|
|
return mix(c2, c3, (normalized - 0.66) * 3.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 20) { // ocean
|
|
vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
|
|
vec3 c1 = vec3(0.000, 0.000, 0.600); // #000099
|
|
vec3 c2 = vec3(0.000, 0.600, 1.000); // #0099ff
|
|
vec3 c3 = vec3(0.600, 1.000, 1.000); // #99ffff
|
|
|
|
if (normalized < 0.33) {
|
|
return mix(c0, c1, normalized * 3.0);
|
|
} else if (normalized < 0.66) {
|
|
return mix(c1, c2, (normalized - 0.33) * 3.0);
|
|
} else {
|
|
return mix(c2, c3, (normalized - 0.66) * 3.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 21) { // terrain
|
|
vec3 c0 = vec3(0.200, 0.200, 0.600); // #333399
|
|
vec3 c1 = vec3(0.000, 0.800, 0.400); // #00cc66
|
|
vec3 c2 = vec3(1.000, 0.800, 0.000); // #ffcc00
|
|
vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
|
|
|
|
if (normalized < 0.33) {
|
|
return mix(c0, c1, normalized * 3.0);
|
|
} else if (normalized < 0.66) {
|
|
return mix(c1, c2, (normalized - 0.33) * 3.0);
|
|
} else {
|
|
return mix(c2, c3, (normalized - 0.66) * 3.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 22) { // neon
|
|
vec3 c0 = vec3(1.000, 0.000, 1.000); // #ff00ff
|
|
vec3 c1 = vec3(0.000, 1.000, 1.000); // #00ffff
|
|
vec3 c2 = vec3(1.000, 1.000, 0.000); // #ffff00
|
|
|
|
if (normalized < 0.5) {
|
|
return mix(c0, c1, normalized * 2.0);
|
|
} else {
|
|
return mix(c1, c2, (normalized - 0.5) * 2.0);
|
|
}
|
|
}
|
|
else if (colorScheme == 23) { // fire
|
|
vec3 c0 = vec3(0.000, 0.000, 0.000); // #000000
|
|
vec3 c1 = vec3(1.000, 0.000, 0.000); // #ff0000
|
|
vec3 c2 = vec3(1.000, 1.000, 0.000); // #ffff00
|
|
vec3 c3 = vec3(1.000, 1.000, 1.000); // #ffffff
|
|
|
|
if (normalized < 0.33) {
|
|
return mix(c0, c1, normalized * 3.0);
|
|
} else if (normalized < 0.66) {
|
|
return mix(c1, c2, (normalized - 0.33) * 3.0);
|
|
} else {
|
|
return mix(c2, c3, (normalized - 0.66) * 3.0);
|
|
}
|
|
}
|
|
else {
|
|
// Default to blue-red gradient
|
|
return mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), normalized);
|
|
}
|
|
}
|
|
`;
|
|
// Full shader-specific noise functions (each defines fbm() called by the footer)
|
|
const SHADER_SOURCES = {
|
|
"domain_warp": `
|
|
// More precise domain warping implementation
|
|
float fbm_base(vec2 p) {
|
|
// Standard FBM using user-controlled octaves
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= u_octaves) break;
|
|
sum += amp * snoise(p * freq);
|
|
freq *= 2.0;
|
|
amp *= 0.5;
|
|
}
|
|
|
|
return sum;
|
|
}
|
|
|
|
// Improved domain warping with different warp types
|
|
float domainWarp(vec2 p, int warpType) {
|
|
// Note: Resolution is now controlled by canvas size
|
|
|
|
// Base unwarped coordinates scaled by user scale parameter
|
|
vec2 p0 = p * u_scale;
|
|
|
|
// Basic domain warp using noise as a displacement vector field
|
|
if (warpType == 0) {
|
|
// Create distortion vector
|
|
float angle = u_time * 0.1;
|
|
vec2 d = vec2(cos(angle), sin(angle));
|
|
|
|
// Generate the warp field
|
|
float warpNoise1 = snoise(p0 * 0.5);
|
|
float warpNoise2 = snoise(p0 * 0.5 + vec2(5.2, 1.3));
|
|
|
|
// Create the warp displacement
|
|
vec2 warpVec = vec2(warpNoise1, warpNoise2) * u_warpStrength;
|
|
|
|
// Apply the warp
|
|
vec2 warped = p0 + warpVec;
|
|
|
|
// Sample the base pattern with warped coordinates
|
|
return snoise(warped);
|
|
}
|
|
// Fractal domain warp (warp the warp)
|
|
else if (warpType == 1) {
|
|
// Apply progressive warping with multiple layers
|
|
vec2 warped = p0;
|
|
|
|
// Progressive warp layers
|
|
for (int i = 0; i < 3; i++) {
|
|
// Scale decreases for each iteration
|
|
float warpScale = 1.0 / pow(2.0, float(i));
|
|
|
|
// Generate warp vectors
|
|
float warpNoise1 = snoise(warped * warpScale + vec2(u_time * 0.05, 0.0));
|
|
float warpNoise2 = snoise(warped * warpScale + vec2(0.0, u_time * 0.05) + vec2(43.13, 17.21));
|
|
|
|
// Apply warp with decreasing strength for each iteration
|
|
float warpFactor = u_warpStrength * warpScale;
|
|
warped += vec2(warpNoise1, warpNoise2) * warpFactor;
|
|
}
|
|
|
|
// Sample final fbm with fully warped coordinates
|
|
return fbm_base(warped);
|
|
}
|
|
// Advanced vector field warping
|
|
else if (warpType == 2) {
|
|
// Use a separate noise function to determine flow direction
|
|
float flowNoise = snoise(p0 * 0.2 + vec2(u_time * 0.1, 0.0));
|
|
float flowAngle = flowNoise * 6.28318530718; // Map to full rotation
|
|
|
|
// Create flow direction vector
|
|
vec2 flowDir = vec2(cos(flowAngle), sin(flowAngle));
|
|
|
|
// Apply directional warp
|
|
vec2 warped = p0 + flowDir * u_warpStrength * snoise(p0 * 0.4);
|
|
|
|
// Add secondary orthogonal flow
|
|
vec2 perpDir = vec2(-flowDir.y, flowDir.x); // Perpendicular vector
|
|
warped += perpDir * u_warpStrength * 0.5 * snoise(p0 * 0.3 + vec2(10.0, 20.0));
|
|
|
|
return fbm_base(warped);
|
|
}
|
|
// Swirl warp
|
|
else {
|
|
// Calculate distance from center
|
|
vec2 centered = p0 - 0.5;
|
|
float dist = length(centered);
|
|
|
|
// Calculate angle based on distance
|
|
float angle = u_time + dist * u_warpStrength * 10.0;
|
|
|
|
// Create rotation matrix
|
|
float s = sin(angle);
|
|
float c = cos(angle);
|
|
mat2 rot = mat2(c, -s, s, c);
|
|
|
|
// Apply rotational warping
|
|
vec2 warped = rot * centered + 0.5;
|
|
|
|
return fbm_base(warped);
|
|
}
|
|
}
|
|
|
|
float fbm(vec2 p) {
|
|
int warpType = int(mod(u_octaves, 4.0));
|
|
float result = domainWarp(p, warpType);
|
|
|
|
// Apply user's phase shift to control contrast and distribution
|
|
float contrast = 1.0 + u_phaseShift;
|
|
result *= contrast;
|
|
|
|
// Ensure output is in valid [-1,1] range
|
|
return clamp(result, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"tensor_field": `
|
|
// Improved tensor field implementation with better mathematical representation
|
|
|
|
// Helper function for computing tensor eigenvectors and eigenvalues
|
|
void computeTensorProperties(vec2 p, out float magnitude1, out float magnitude2,
|
|
out vec2 direction1, out vec2 direction2) {
|
|
// Note: Resolution is now controlled by canvas size
|
|
|
|
// Generate a tensor field using noise gradients
|
|
vec2 offset = vec2(u_time * 0.05);
|
|
vec2 p1 = p * u_scale + offset;
|
|
|
|
// Apply warp to coordinates if warp strength is non-zero
|
|
if (u_warpStrength > 0.0) {
|
|
// Generate warp field based on noise
|
|
float warpNoise1 = snoise(p1 * 0.3 + vec2(0.0, 1.0));
|
|
float warpNoise2 = snoise(p1 * 0.3 + vec2(1.0, 0.0));
|
|
|
|
// Apply warp to coordinates
|
|
p1 += vec2(warpNoise1, warpNoise2) * u_warpStrength;
|
|
}
|
|
|
|
// Use noise derivatives to generate tensor field
|
|
float eps = 0.01;
|
|
|
|
// Compute approximate derivatives of noise field
|
|
float n00 = snoise(p1);
|
|
float n10 = snoise(p1 + vec2(eps, 0.0));
|
|
float n01 = snoise(p1 + vec2(0.0, eps));
|
|
float n11 = snoise(p1 + vec2(eps, eps));
|
|
|
|
// Calculate derivatives (gradient components)
|
|
float dx = (n10 - n00) / eps;
|
|
float dy = (n01 - n00) / eps;
|
|
|
|
// Second order derivatives for tensor components
|
|
float dxx = (n10 - 2.0 * n00 + snoise(p1 - vec2(eps, 0.0))) / (eps * eps);
|
|
float dyy = (n01 - 2.0 * n00 + snoise(p1 - vec2(0.0, eps))) / (eps * eps);
|
|
float dxy = (n11 - n10 - n01 + n00) / (eps * eps);
|
|
|
|
// Construct tensor matrix components
|
|
float T00 = dxx;
|
|
float T01 = dxy;
|
|
float T10 = dxy;
|
|
float T11 = dyy;
|
|
|
|
// Calculate eigenvalues
|
|
float trace = T00 + T11;
|
|
float det = T00 * T11 - T01 * T10;
|
|
float discriminant = sqrt(trace * trace - 4.0 * det);
|
|
|
|
// Two eigenvalues
|
|
magnitude1 = (trace + discriminant) * 0.5;
|
|
magnitude2 = (trace - discriminant) * 0.5;
|
|
|
|
// Calculate first eigenvector
|
|
if (abs(T01) > 0.0001) {
|
|
direction1 = normalize(vec2(T01, magnitude1 - T00));
|
|
} else if (abs(T10) > 0.0001) {
|
|
direction1 = normalize(vec2(magnitude1 - T11, T10));
|
|
} else {
|
|
// Diagonal tensor
|
|
direction1 = vec2(1.0, 0.0);
|
|
}
|
|
|
|
// Second eigenvector is perpendicular to first
|
|
direction2 = vec2(-direction1.y, direction1.x);
|
|
}
|
|
|
|
// Improved tensor field visualization
|
|
float tensorField(vec2 p) {
|
|
// Calculate tensor field components
|
|
float lambda1, lambda2;
|
|
vec2 v1, v2;
|
|
computeTensorProperties(p, lambda1, lambda2, v1, v2);
|
|
|
|
// Choose visualization based on octaves
|
|
int visualizationType = int(mod(u_octaves, 4.0));
|
|
|
|
// Different visualization modes
|
|
if (visualizationType == 0) {
|
|
// Eigenvalue visualization - shows magnitude of deformation
|
|
float maxEig = max(abs(lambda1), abs(lambda2));
|
|
return clamp(maxEig, -1.0, 1.0);
|
|
}
|
|
else if (visualizationType == 1) {
|
|
// Eigenvector streamlines - shows direction of principal stress
|
|
|
|
// Direction-based visualization with animating flow
|
|
float lineWidth = 0.08;
|
|
vec2 st = p;
|
|
|
|
// Calculate distance to streamline along first eigenvector
|
|
float flowPhase = u_time * 0.2;
|
|
float t = st.x * v1.x + st.y * v1.y;
|
|
float streamline1 = abs(fract(t * 5.0 + flowPhase) - 0.5) * 2.0;
|
|
|
|
// Calculate distance to streamline along second eigenvector
|
|
t = st.x * v2.x + st.y * v2.y;
|
|
float streamline2 = abs(fract(t * 5.0 - flowPhase) - 0.5) * 2.0;
|
|
|
|
// Combine streamlines
|
|
float pattern = min(streamline1, streamline2);
|
|
return 1.0 - smoothstep(0.0, lineWidth, pattern) * 2.0;
|
|
}
|
|
else if (visualizationType == 2) {
|
|
// Hyperstreamlines - thickness varies with eigenvalue magnitude
|
|
|
|
vec2 dir1 = v1 * sign(lambda1);
|
|
vec2 dir2 = v2 * sign(lambda2);
|
|
|
|
float weight1 = abs(lambda1) / (abs(lambda1) + abs(lambda2) + 0.001);
|
|
float weight2 = 1.0 - weight1;
|
|
|
|
float angle1 = atan(dir1.y, dir1.x);
|
|
float angle2 = atan(dir2.y, dir2.x);
|
|
|
|
float t1 = cos(5.0 * (p.x * dir1.x + p.y * dir1.y) + u_time);
|
|
float t2 = cos(5.0 * (p.x * dir2.x + p.y * dir2.y) - u_time);
|
|
|
|
return (t1 * weight1 + t2 * weight2) * 0.5;
|
|
}
|
|
else {
|
|
// Tensor ellipses
|
|
|
|
// Create an elliptical pattern aligned with eigenvectors and scaled by eigenvalues
|
|
vec2 centered = p - floor(p * 4.0 + 0.5) / 4.0; // Create grid
|
|
|
|
// Transform point to eigenvector basis
|
|
float x = dot(centered, v1);
|
|
float y = dot(centered, v2);
|
|
|
|
// Scale by eigenvalues (normalized to prevent distortion)
|
|
float maxEig = max(abs(lambda1), abs(lambda2)) + 0.1;
|
|
float scaledX = x * abs(lambda1) / maxEig;
|
|
float scaledY = y * abs(lambda2) / maxEig;
|
|
|
|
// Create ellipse
|
|
float ellipse = length(vec2(scaledX, scaledY));
|
|
float radius = 0.05;
|
|
|
|
// Animate pulsing ellipses
|
|
radius *= 1.0 + 0.3 * sin(u_time * 2.0);
|
|
|
|
// Return elliptical pattern
|
|
return 1.0 - smoothstep(0.0, 0.01, ellipse - radius) * 2.0;
|
|
}
|
|
}
|
|
|
|
float fbm(vec2 p) {
|
|
float result = tensorField(p);
|
|
|
|
// Apply user's phase shift to control contrast and distribution
|
|
float contrast = 1.0 + u_phaseShift;
|
|
result *= contrast;
|
|
|
|
// Ensure output is in valid [-1,1] range
|
|
return clamp(result, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"curl_noise": `
|
|
// Compute gradient of scalar field
|
|
vec2 computeGradient(vec2 p, float epsilon) {
|
|
// Sample the potential field at nearby points
|
|
float dx = snoise(vec2(p.x + epsilon, p.y)) - snoise(vec2(p.x - epsilon, p.y));
|
|
float dy = snoise(vec2(p.x, p.y + epsilon)) - snoise(vec2(p.x, p.y - epsilon));
|
|
|
|
// Normalize by epsilon and return
|
|
return vec2(dx, dy) / (2.0 * epsilon);
|
|
}
|
|
|
|
// Compute curl of vector field (z component in 2D)
|
|
float computeCurl(vec2 p, float epsilon) {
|
|
// For 2D curl, we need partial derivatives of two potential fields
|
|
// We'll use two offset perlin noise functions as our potential fields
|
|
|
|
// Sample two potential fields (offset for independence)
|
|
float pot1_dx = snoise(vec2(p.x + epsilon, p.y)) - snoise(vec2(p.x - epsilon, p.y));
|
|
float pot1_dy = snoise(vec2(p.x, p.y + epsilon)) - snoise(vec2(p.x, p.y - epsilon));
|
|
|
|
float pot2_dx = snoise(vec2(p.x + epsilon, p.y + 100.0)) - snoise(vec2(p.x - epsilon, p.y + 100.0));
|
|
float pot2_dy = snoise(vec2(p.x, p.y + epsilon + 100.0)) - snoise(vec2(p.x, p.y - epsilon + 100.0));
|
|
|
|
// Normalize gradients
|
|
pot1_dx /= (2.0 * epsilon);
|
|
pot1_dy /= (2.0 * epsilon);
|
|
pot2_dx /= (2.0 * epsilon);
|
|
pot2_dy /= (2.0 * epsilon);
|
|
|
|
// Compute curl (cross product in 2D: ∂pot2/∂x - ∂pot1/∂y)
|
|
return pot2_dx - pot1_dy;
|
|
}
|
|
|
|
// Get a fluid velocity field based on curl
|
|
vec2 getVelocityField(vec2 p, float time) {
|
|
// Use multiple frequencies for more detailed flow
|
|
vec2 velocity = vec2(0.0);
|
|
float epsilon = 0.01;
|
|
|
|
// Base frequency
|
|
float frequency = 1.0;
|
|
float amplitude = 1.0;
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
if (float(i) >= u_octaves) break;
|
|
|
|
// Time-varied position
|
|
vec2 pos = p * frequency + vec2(time * 0.1 * frequency);
|
|
|
|
// Compute curl at this frequency
|
|
float curl = computeCurl(pos, epsilon);
|
|
|
|
// Use curl to derive a velocity field
|
|
// The gradient of the curl gives us a divergence-free field
|
|
vec2 vel = vec2(
|
|
computeCurl(pos + vec2(0.0, epsilon), epsilon) - curl,
|
|
curl - computeCurl(pos + vec2(epsilon, 0.0), epsilon)
|
|
) / epsilon;
|
|
|
|
// Add to total velocity
|
|
velocity += vel * amplitude;
|
|
|
|
// Prepare for next octave
|
|
frequency *= 2.0;
|
|
amplitude *= 0.5;
|
|
epsilon *= 0.5; // Adjust epsilon for higher frequencies
|
|
}
|
|
|
|
return velocity;
|
|
}
|
|
|
|
// Advect a property along the velocity field
|
|
float advect(vec2 p, vec2 velocity, float time, float dt) {
|
|
// Trace particle backward in time
|
|
vec2 particlePos = p - velocity * dt;
|
|
|
|
// Sample different noise patterns based on octave setting
|
|
int patternType = int(mod(u_octaves, 4.0));
|
|
float result;
|
|
|
|
if (patternType == 0) {
|
|
// Classic advected noise
|
|
result = snoise(particlePos * u_scale + vec2(time * 0.2));
|
|
}
|
|
else if (patternType == 1) {
|
|
// Dye injection visualization
|
|
float dist = length(fract(particlePos) - 0.5) * 2.0;
|
|
float spots = smoothstep(0.4, 0.0, dist);
|
|
result = spots * 2.0 - 1.0; // Remap to [-1, 1]
|
|
}
|
|
else if (patternType == 2) {
|
|
// Flow lines visualization
|
|
float vLen = length(velocity);
|
|
vec2 safeDir = vLen > 0.001 ? normalize(velocity) : vec2(1.0, 0.0);
|
|
float stream = sin(dot(particlePos, safeDir) * 10.0 + time);
|
|
result = stream;
|
|
}
|
|
else {
|
|
// Vorticity visualization (shows rotations in the flow)
|
|
float vorticity = computeCurl(particlePos * u_scale, 0.01);
|
|
result = vorticity * 2.0; // Amplify for better visibility
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Apply the warp control to intensify curl
|
|
vec2 applyWarpIntensity(vec2 velocity, float warp) {
|
|
float vLen = max(length(velocity), 0.001);
|
|
float logScale = log(vLen * 9.0 + 1.0) * warp;
|
|
vec2 safeDir = vLen > 0.001 ? normalize(velocity) : vec2(0.0);
|
|
return safeDir * logScale;
|
|
}
|
|
|
|
float fbm(vec2 p) {
|
|
// Note: Resolution is now controlled by canvas size
|
|
|
|
// Scale coordinates
|
|
p *= u_scale;
|
|
|
|
// Compute velocity field
|
|
vec2 velocity = getVelocityField(p, u_time);
|
|
|
|
// Apply warp control to intensify curl
|
|
velocity = applyWarpIntensity(velocity, u_warpStrength);
|
|
|
|
// Vary the advection time step based on phase shift
|
|
float dt = mix(0.2, 2.0, u_phaseShift);
|
|
|
|
// Advect along the curl field
|
|
float result = advect(p, velocity, u_time, dt);
|
|
|
|
// Return noise within proper range
|
|
return clamp(result, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"temporal_coherent": `
|
|
// shaders/temporal_coherent_noise.py: a 3D simplex FBM with time as the third
|
|
// axis, warped by two more. phase_shift is ignored, as the Python ignores it at
|
|
// the node's settings.
|
|
float fbm(vec2 p) {
|
|
vec2 q = (p * 2.0 - 1.0) * u_scale;
|
|
vec3 P = vec3(q, u_time);
|
|
if (u_warpStrength > 0.0) {
|
|
P.xy += u_warpStrength * vec2(snoise3(P * 0.4), snoise3(P * 0.4 + 5.0));
|
|
}
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= u_octaves) break;
|
|
sum += amp * snoise3(P * freq + vec3(0.0, 0.0, 1.5 * float(i)));
|
|
freq *= 2.0;
|
|
amp *= 0.5;
|
|
}
|
|
sum *= 1.0 + 0.1 * sin(u_time * 0.3);
|
|
return clamp(sum * 1.5, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"spectral": `
|
|
// shaders/spectral.py synthesises its field from a shaped spectrum. No FFT per
|
|
// pixel here: 32 fixed random-phase plane waves whose amplitudes follow the same
|
|
// envelope, (1 + (|k|/corner)^2)^(-slope/2), stretched by warp_strength and
|
|
// turned by phase_shift, each drifting at its own rate with time.
|
|
float fbm(vec2 p) {
|
|
float corner = clamp(0.15 * u_scale, 0.02, 0.5);
|
|
float slope = clamp(2.5 - 0.25 * (u_octaves - 1.0), 0.5, 3.0);
|
|
float stretch = 1.0 + u_warpStrength;
|
|
float sum = 0.0;
|
|
float norm = 0.0;
|
|
for (int m = 0; m < 32; m++) {
|
|
float fm = float(m);
|
|
float mag = pow(2.0, mix(-2.0, 3.0, random(vec2(fm, 1.0))));
|
|
float th = random(vec2(fm, 2.0)) * 6.28318530718;
|
|
vec2 k = mag * corner * 32.0 * vec2(cos(th) / stretch, sin(th) * stretch);
|
|
float env = pow(1.0 + mag * mag, -slope * 0.5);
|
|
float ph = random(vec2(fm, 3.0)) * 6.28318530718 + u_phaseShift * 3.14159265
|
|
+ u_time * (random(vec2(fm, 4.0)) - 0.5) * 2.0;
|
|
sum += env * sin(6.28318530718 * dot(p, k) + ph);
|
|
norm += env * env;
|
|
}
|
|
return clamp(sum / sqrt(norm) * 0.7, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"gaussian": `
|
|
// shaders/gaussian.py: white noise, the control. Box-Muller on a fixed 128-cell
|
|
// grid, so pixel density does not change the look, stepping a few times a second.
|
|
float fbm(vec2 p) {
|
|
vec2 cell = floor(p * 128.0) + floor(u_time * 4.0) * 7.0;
|
|
float u1 = max(random(cell), 0.0001);
|
|
float u2 = random(cell + 0.5);
|
|
float g = sqrt(-2.0 * log(u1)) * cos(6.28318530718 * u2);
|
|
return clamp(g * 0.5, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"fractal": `
|
|
// shaders/fractal.py: the reference FBM. warp_strength is the layer spacing,
|
|
// phase_shift slides each layer, time is the third axis.
|
|
float fbm(vec2 p) {
|
|
vec2 q = p * u_scale * 2.0;
|
|
float lac = min(1.5 + u_warpStrength, 4.0);
|
|
vec2 offset = u_phaseShift * vec2(0.37, 0.61);
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
float norm = 0.0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= u_octaves) break;
|
|
vec2 r = q * freq + offset * float(i);
|
|
sum += amp * snoise3(vec3(r, u_time * (0.2 + 0.05 * float(i))));
|
|
norm += amp;
|
|
amp *= 0.5;
|
|
freq *= lac;
|
|
}
|
|
return clamp(sum / norm * 1.5, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"perlin": `
|
|
// shaders/perlin.py: gradient noise, warp on the detail layers only, contrast
|
|
// from phase_shift.
|
|
vec2 grad2(vec2 c) {
|
|
float a = random(c) * 6.28318530718;
|
|
return vec2(cos(a), sin(a));
|
|
}
|
|
|
|
float perlin(vec2 p) {
|
|
vec2 c = floor(p);
|
|
vec2 f = p - c;
|
|
vec2 u = fade(f);
|
|
float n00 = dot(grad2(c), f);
|
|
float n10 = dot(grad2(c + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
|
|
float n01 = dot(grad2(c + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
|
|
float n11 = dot(grad2(c + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
|
|
return mix(mix(n00, n10, u.x), mix(n01, n11, u.x), u.y) * 1.41421356;
|
|
}
|
|
|
|
float fbm(vec2 p) {
|
|
vec2 q = p * u_scale * 3.0;
|
|
vec2 w = vec2(snoise(q * 0.5 + u_time * 0.1), snoise(q * 0.5 + vec2(31.7, 7.3) + u_time * 0.1))
|
|
* u_warpStrength * 0.5;
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
float norm = 0.0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= u_octaves) break;
|
|
vec2 r = (i == 0 ? q : q + w) * freq + u_time * 0.05 * float(i + 1);
|
|
sum += amp * perlin(r);
|
|
norm += amp;
|
|
amp *= 0.5;
|
|
freq *= 2.0;
|
|
}
|
|
return clamp(sum / norm * 2.0 * (1.0 + u_phaseShift * 0.5), -1.0, 1.0);
|
|
}
|
|
`,
|
|
"heterogeneous_fbm": `
|
|
// shaders/heterogeneous_fbm.py: a slow control field sets the persistence per
|
|
// pixel, so the frame has rough patches and smooth ones.
|
|
float fbm(vec2 p) {
|
|
vec2 q = p * u_scale * 2.0;
|
|
float hetero = min(0.7 * u_warpStrength, 1.0);
|
|
float control = snoise3(vec3(q * 0.5, u_time * 0.2 + 50.0));
|
|
float pers = clamp(0.5 + hetero * (control + (u_phaseShift - 0.5)), 0.15, 0.9);
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
float norm = 0.0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= u_octaves) break;
|
|
sum += amp * snoise3(vec3(q * freq, u_time * (0.2 + 0.05 * float(i))));
|
|
norm += amp;
|
|
amp *= pers;
|
|
freq *= 2.0;
|
|
}
|
|
return clamp(sum / norm * 1.5, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"interference": `
|
|
// shaders/interference.py: two FBMs cut into cos and sin fringes that cross.
|
|
float layers(vec2 q, float octaves, float pers, float lac, float zoff) {
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
float norm = 0.0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= octaves) break;
|
|
sum += amp * snoise3(vec3(q * freq, zoff + u_time * (0.2 + 0.05 * float(i))));
|
|
norm += amp;
|
|
amp *= pers;
|
|
freq *= lac;
|
|
}
|
|
return sum / norm;
|
|
}
|
|
|
|
float fbm(vec2 p) {
|
|
vec2 q = p * u_scale * 2.0;
|
|
float gain = 3.14159265 * (0.5 + 0.5 * u_warpStrength);
|
|
float a = layers(q, u_octaves, 0.5, 2.0, 0.0);
|
|
float b = layers(q * 1.5 + 17.3, max(u_octaves - 1.0, 1.0), 0.6, 1.8, 11.0);
|
|
return clamp((cos(gain * a) + sin(gain * (1.0 + 0.2 * u_phaseShift) * b)) * 0.7, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"projection_3d": `
|
|
// shaders/projection_3d.py: a plane through a 3D FBM. phase_shift is the depth
|
|
// of the slice, time slides it, warp_strength bends it.
|
|
float fbm(vec2 p) {
|
|
vec2 q = p * u_scale * 2.0;
|
|
q += u_warpStrength * 0.5 * vec2(snoise(q * 0.3 + 12.3), snoise(q * 0.3 + 45.6));
|
|
float depth = u_phaseShift + u_time * 0.5;
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
float norm = 0.0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= u_octaves) break;
|
|
sum += amp * snoise3(vec3(q * freq, depth * freq + 1.5 * float(i)));
|
|
norm += amp;
|
|
amp *= 0.5;
|
|
freq *= 2.0;
|
|
}
|
|
return clamp(sum / norm * 1.5, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"cellular": `
|
|
// shaders/cellular.py: Worley cells. octaves picks F1, F2, edges or product,
|
|
// phase_shift the Minkowski exponent (diamond, round, square), warp_strength
|
|
// bends the lattice. The points wobble in their cells with time.
|
|
vec2 worley(vec2 q, float e) {
|
|
vec2 cell = floor(q);
|
|
vec2 f = q - cell;
|
|
float f1 = 64.0;
|
|
float f2 = 64.0;
|
|
for (int y = -1; y <= 1; y++) {
|
|
for (int x = -1; x <= 1; x++) {
|
|
vec2 o = vec2(float(x), float(y));
|
|
vec2 c = cell + o;
|
|
vec2 jitter = 0.5 + 0.5 * sin(u_time * 0.5 + 6.28318530718 * vec2(random(c), random(c + 41.3)));
|
|
vec2 point = o + jitter - f;
|
|
float d = pow(pow(abs(point.x), e) + pow(abs(point.y), e), 1.0 / e);
|
|
if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) { f2 = d; }
|
|
}
|
|
}
|
|
return vec2(f1, f2);
|
|
}
|
|
|
|
float fbm(vec2 p) {
|
|
vec2 q = p * u_scale * 4.0;
|
|
q += u_warpStrength * 0.5 * vec2(snoise(q * 0.5 + 3.1), snoise(q * 0.5 + 9.7));
|
|
float e = 1.0 + 2.0 * u_phaseShift;
|
|
vec2 d = worley(q, e);
|
|
int pattern = int(mod(max(u_octaves, 1.0) - 1.0, 4.0));
|
|
float v;
|
|
if (pattern == 0) v = d.x;
|
|
else if (pattern == 1) v = d.y * 0.7;
|
|
else if (pattern == 2) v = d.y - d.x;
|
|
else v = d.x * d.y * 0.7;
|
|
return clamp(v * 2.0 - 1.0, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"waves": `
|
|
// shaders/waves.py: octaves seeded plane waves, straight at warp 0, drifting
|
|
// at their own rates with time.
|
|
float fbm(vec2 p) {
|
|
vec2 q = p * u_scale;
|
|
q += u_warpStrength * 0.15 * vec2(snoise(q + u_time * 0.1), snoise(q + vec2(9.1, 3.3)));
|
|
float count = min(max(u_octaves, 1.0), 8.0);
|
|
float sum = 0.0;
|
|
for (int i = 0; i < 8; i++) {
|
|
if (float(i) >= count) break;
|
|
float fi = float(i);
|
|
float th = random(vec2(fi, 1.0)) * 3.14159265;
|
|
float fr = 3.0 * (1.0 + 0.5 * fi + 0.5 * random(vec2(fi, 2.0)));
|
|
float ph = random(vec2(fi, 3.0)) * 6.28318530718 + u_phaseShift * 3.14159265 * (fi + 1.0);
|
|
float rate = 0.25 * (0.5 + random(vec2(fi, 4.0)));
|
|
float along = q.x * cos(th) + q.y * sin(th);
|
|
sum += sin(6.28318530718 * (fr * along + rate * u_time) + ph);
|
|
}
|
|
return clamp(sum / sqrt(count) * 0.9, -1.0, 1.0);
|
|
}
|
|
`,
|
|
"kaleido": `
|
|
// shaders/kaleido.py: an FBM over folded coordinates. octaves is the order of
|
|
// the fold (1 mirrors, 2 mirrors both ways, 3 and up are wedges), phase_shift
|
|
// turns it, warp_strength bends the pattern inside the fold.
|
|
float fbm(vec2 p) {
|
|
vec2 c = p - 0.5;
|
|
float turn = (u_phaseShift - 0.5) * 3.14159265;
|
|
c = vec2(c.x * cos(turn) - c.y * sin(turn), c.x * sin(turn) + c.y * cos(turn));
|
|
float order = floor(min(max(u_octaves, 1.0), 8.0));
|
|
if (order < 1.5) {
|
|
c.x = abs(c.x);
|
|
} else if (order < 2.5) {
|
|
c = abs(c);
|
|
} else {
|
|
float wedge = 6.28318530718 / order;
|
|
float a = abs(mod(atan(c.y, c.x), wedge) - wedge * 0.5);
|
|
c = length(c) * vec2(cos(a), sin(a));
|
|
}
|
|
vec2 q = c * u_scale * 3.0;
|
|
q += u_warpStrength * 0.15 * vec2(snoise(q + 100.0), snoise(q + 200.0));
|
|
float sum = 0.0;
|
|
float amp = 1.0;
|
|
float freq = 1.0;
|
|
float norm = 0.0;
|
|
for (int i = 0; i < 3; i++) {
|
|
sum += amp * snoise3(vec3(q * freq, u_time * (0.2 + 0.05 * float(i))));
|
|
norm += amp;
|
|
amp *= 0.5;
|
|
freq *= 2.0;
|
|
}
|
|
return clamp(sum / norm * 1.5, -1.0, 1.0);
|
|
}
|
|
`
|
|
};
|
|
// Common fragment shader footer - calls fbm(), applies shape mask and color scheme
|
|
const FRAGMENT_SHADER_FOOTER = `
|
|
void main() {
|
|
// Generate noise
|
|
float noise = fbm(v_texCoord);
|
|
|
|
// Apply shape mask if enabled
|
|
float shape_mask = applyShapeMask(v_texCoord, u_shapeType);
|
|
noise = noise * (shape_mask * u_shapeStrength + (1.0 - u_shapeStrength));
|
|
|
|
// Map to [0,1] range for color
|
|
vec3 color = getColor(noise, u_colorScheme);
|
|
|
|
gl_FragColor = vec4(color * u_intensity, 1.0);
|
|
}
|
|
`;
|
|
// ============================
|
|
// Extension Registration
|
|
// ============================
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
app.registerExtension({
|
|
name: "ShaderNoiseKSampler.ShaderRenderer",
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
// The Direct node carries the same shader controls as real node inputs, so
|
|
// there the preview mirrors those widgets instead of adding its own copies.
|
|
const isDirect = nodeData.name === "ShaderNoiseKSamplerDirect";
|
|
if (nodeData.name !== "ShaderNoiseKSampler" && !isDirect)
|
|
return;
|
|
/** Direct node input widget -> the property the WebGL uniforms read */
|
|
const INPUT_TO_PROPERTY = {
|
|
shader_type: "shaderType",
|
|
shape_type: "shaderShapeType",
|
|
color_scheme: "colorScheme",
|
|
noise_scale: "shaderScale",
|
|
octaves: "shaderOctaves",
|
|
warp_strength: "shaderWarpStrength",
|
|
shape_mask_strength: "shaderShapeStrength",
|
|
phase_shift: "shaderPhaseShift",
|
|
color_intensity: "shaderColorIntensity",
|
|
};
|
|
/** Copy the node's input values into the properties the preview renders from. */
|
|
const syncFromInputs = (node) => {
|
|
if (!node.widgets)
|
|
return;
|
|
for (const widget of node.widgets) {
|
|
const property = widget.name ? INPUT_TO_PROPERTY[widget.name] : undefined;
|
|
if (property && widget.value !== undefined) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
node.properties[property] = widget.value;
|
|
}
|
|
}
|
|
};
|
|
/** Keep the preview in step with the inputs as the user edits them. */
|
|
const followInputs = (node) => {
|
|
if (!node.widgets)
|
|
return;
|
|
for (const widget of node.widgets) {
|
|
const property = widget.name ? INPUT_TO_PROPERTY[widget.name] : undefined;
|
|
if (!property)
|
|
continue;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const original = widget.callback;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
widget.callback = function (value, ...rest) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
node.properties[property] = value;
|
|
if (property === "shaderType" && node.loadShader &&
|
|
(node.isShaderActive || node.properties.shaderVisible)) {
|
|
node.loadShader(value);
|
|
}
|
|
node.setDirtyCanvas(true, true);
|
|
return original ? original.call(this, value, ...rest) : undefined;
|
|
};
|
|
}
|
|
};
|
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
const origComputeSize = nodeType.prototype.computeSize;
|
|
const origOnDrawForeground = nodeType.prototype.onDrawForeground;
|
|
const origOnRemoved = nodeType.prototype.onRemoved;
|
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
|
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (origOnNodeCreated)
|
|
origOnNodeCreated.apply(this, arguments);
|
|
// Initialize shader properties
|
|
this.properties = this.properties || {};
|
|
this.properties.shaderVisible = false;
|
|
this.properties.tooltipsVisible = true;
|
|
this.properties.shaderType = "domain_warp";
|
|
this.properties.shaderSpeed = 0.2;
|
|
this.properties.shaderColorIntensity = 0.8;
|
|
this.properties.shaderTime = 0;
|
|
this.properties.lastRenderTime = 0;
|
|
this.properties.shaderScale = 1.0;
|
|
this.properties.shaderOctaves = 1;
|
|
this.properties.shaderShapeType = "none";
|
|
this.properties.shaderShapeStrength = 1.0;
|
|
this.properties.shaderWarpStrength = 0.5;
|
|
this.properties.shaderPhaseShift = 0.5;
|
|
this.properties.shaderFrequencyRange = 0;
|
|
this.properties.shaderDistribution = 0;
|
|
this.properties.shaderAdaptationStrength = 0.5;
|
|
this.properties.shaderResolutionScale = 512;
|
|
this.properties.colorScheme = "none";
|
|
this.shaderHeight = 200;
|
|
this.animationFrameId = null;
|
|
this.isShaderActive = false;
|
|
this.initShaderCanvas();
|
|
// Add widgets - use any cast for callbacks since WidgetCallback has different signature than actual usage
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
this.addWidget("toggle", "Show Shader", this.properties.shaderVisible, ((v) => {
|
|
this.properties.shaderVisible = v;
|
|
const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
|
|
const baseHeight = baseSize[1];
|
|
if (v && this.gl) {
|
|
this.shaderHeight = this.shaderHeight || 200;
|
|
this.size[1] = baseHeight + this.shaderHeight;
|
|
this.resizeShaderCanvas(this.size[0], this.shaderHeight);
|
|
this.isShaderActive = true;
|
|
this.properties.lastRenderTime = 0;
|
|
}
|
|
else {
|
|
this.size[1] = baseHeight;
|
|
this.isShaderActive = false;
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
}
|
|
this.setDirtyCanvas(true, true);
|
|
}));
|
|
if (this.widgets?.length)
|
|
this.widgets[this.widgets.length - 1].tooltip = "Toggle shader preview visibility";
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
this.addWidget("toggle", "Show Tooltips", this.properties.tooltipsVisible, ((v) => {
|
|
this.properties.tooltipsVisible = v;
|
|
if (this.widgets) {
|
|
for (let i = 0; i < this.widgets.length; i++) {
|
|
const w = this.widgets[i];
|
|
if (w.name === "Show Tooltips")
|
|
continue;
|
|
if (!w._originalTooltip && w.tooltip)
|
|
w._originalTooltip = w.tooltip;
|
|
w.tooltip = v && w._originalTooltip ? w._originalTooltip : "";
|
|
}
|
|
}
|
|
this.setDirtyCanvas(true, true);
|
|
}));
|
|
if (this.widgets?.length)
|
|
this.widgets[this.widgets.length - 1].tooltip = "Toggle tooltip visibility";
|
|
if (isDirect) {
|
|
// These exist as real node inputs here; mirror them rather than
|
|
// adding a second, unconnected copy of every control.
|
|
syncFromInputs(this);
|
|
followInputs(this);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("combo", "Shader Noise Type 🔄", this.properties.shaderType, ((v) => {
|
|
this.properties.shaderType = v;
|
|
if (this.loadShader && (this.isShaderActive || this.properties.shaderVisible))
|
|
this.loadShader(v);
|
|
this.setDirtyCanvas(true, true);
|
|
}), { values: Object.keys(SHADER_SOURCES) });
|
|
if (!isDirect && this.widgets?.length)
|
|
this.widgets[this.widgets.length - 1].tooltip = "Select shader noise pattern type";
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("combo", "Shape Mask Type 🔄", this.properties.shaderShapeType, ((v) => {
|
|
this.properties.shaderShapeType = v;
|
|
this.setDirtyCanvas(true, true);
|
|
}), { values: ["none", "radial", "linear", "spiral", "checkerboard", "spots", "hexgrid", "stripes", "gradient", "vignette", "cross", "stars", "triangles", "concentric", "rays", "zigzag"] });
|
|
if (!isDirect && this.widgets?.length)
|
|
this.widgets[this.widgets.length - 1].tooltip = "Apply shape mask to shader pattern";
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("combo", "Color Scheme 🔄", this.properties.colorScheme, ((v) => {
|
|
this.properties.colorScheme = v;
|
|
this.setDirtyCanvas(true, true);
|
|
}), { values: ["none", "blue_red", "viridis", "plasma", "inferno", "magma", "turbo", "jet", "rainbow", "cool", "hot", "parula", "hsv", "autumn", "winter", "spring", "summer", "copper", "pink", "bone", "ocean", "terrain", "neon", "fire"] });
|
|
if (!isDirect && this.widgets?.length)
|
|
this.widgets[this.widgets.length - 1].tooltip = "Choose color palette for visualization";
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("slider", "Noise Scale 🔄", this.properties.shaderScale, ((v) => { this.properties.shaderScale = v; this.setDirtyCanvas(true, true); }), { min: 0.1, max: 10.0, step: 0.001, precision: 3 });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("slider", "Octaves 🔄", this.properties.shaderOctaves, ((v) => { this.properties.shaderOctaves = v; this.setDirtyCanvas(true, true); }), { min: 1, max: 8, step: 0.1, precision: 1 });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("slider", "Warp Strength 🔄", this.properties.shaderWarpStrength, ((v) => { this.properties.shaderWarpStrength = v; this.setDirtyCanvas(true, true); }), { min: 0.0, max: 5.0, step: 0.001, precision: 3 });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("slider", "Shape Mask Strength 🔄", this.properties.shaderShapeStrength, ((v) => { this.properties.shaderShapeStrength = v; this.setDirtyCanvas(true, true); }), { min: 0.0, max: 2.0, step: 0.0005, precision: 4 });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("slider", "Phase Shift 🔄", this.properties.shaderPhaseShift, ((v) => { this.properties.shaderPhaseShift = v; this.setDirtyCanvas(true, true); }), { min: 0.0, max: 2.0, step: 0.0005, precision: 4 });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (!isDirect)
|
|
this.addWidget("slider", "Color Intensity 🔄", this.properties.shaderColorIntensity, ((v) => { this.properties.shaderColorIntensity = v; this.setDirtyCanvas(true, true); }), { min: 0.0, max: 1.0, step: 0.0005, precision: 4 });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
this.addWidget("slider", "Animation Speed 🖥️", this.properties.shaderSpeed, ((v) => { this.properties.shaderSpeed = v; this.setDirtyCanvas(true, true); }), { min: 0.1, max: 3.0, step: 0.001, precision: 3 });
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
this.addWidget("slider", "Pixel Resolution 🖥️", this.properties.shaderResolutionScale, ((v) => { this.properties.shaderResolutionScale = v; this.resizeShaderCanvas(this.size[0], this.shaderHeight); this.setDirtyCanvas(true, true); }), { min: 128, max: 1024, step: 1, precision: 0 });
|
|
if (isDirect) {
|
|
// Preview-only controls must not claim saved widget slots: workflows
|
|
// map widget values by position, so serializing these would shift
|
|
// every stored input when an older workflow is loaded.
|
|
const displayOnly = ["Show Shader", "Show Tooltips", "Animation Speed 🖥️", "Pixel Resolution 🖥️"];
|
|
for (const widget of this.widgets ?? []) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (widget.name && displayOnly.includes(widget.name))
|
|
widget.serialize = false;
|
|
}
|
|
}
|
|
this.resizable = true;
|
|
this.min_height = 100;
|
|
this.min_width = 300;
|
|
};
|
|
nodeType.prototype.initShaderCanvas = function () {
|
|
this.shaderCanvas = document.createElement('canvas');
|
|
this.shaderCanvas.width = this.size ? this.size[0] : 250;
|
|
this.shaderCanvas.height = this.shaderHeight;
|
|
this.gl = this.shaderCanvas.getContext('webgl', { preserveDrawingBuffer: true });
|
|
if (!this.gl) {
|
|
console.error('WebGL not supported');
|
|
return;
|
|
}
|
|
this.gl.viewport(0, 0, this.shaderCanvas.width, this.shaderCanvas.height);
|
|
this.shaderPrograms = {};
|
|
this.vertexShaderSource = VERTEX_SHADER_SOURCE;
|
|
this.fragmentShaderHeader = FRAGMENT_SHADER_HEADER;
|
|
this.fragmentShaderFooter = FRAGMENT_SHADER_FOOTER;
|
|
this.shaderSources = SHADER_SOURCES;
|
|
this.pendingShaders = [];
|
|
this.loadingShader = false;
|
|
// Create position buffer
|
|
const positionBuffer = this.gl.createBuffer();
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]), this.gl.STATIC_DRAW);
|
|
this.positionBuffer = positionBuffer;
|
|
};
|
|
nodeType.prototype.resizeShaderCanvas = function (width, height) {
|
|
if (!this.gl || !this.shaderCanvas)
|
|
return;
|
|
height = Math.max(50, height);
|
|
const aspectRatio = width / height;
|
|
const targetRes = Math.round(this.properties.shaderResolutionScale);
|
|
let canvasWidth, canvasHeight;
|
|
if (aspectRatio >= 1) {
|
|
canvasWidth = targetRes;
|
|
canvasHeight = Math.round(targetRes / aspectRatio);
|
|
}
|
|
else {
|
|
canvasHeight = targetRes;
|
|
canvasWidth = Math.round(targetRes * aspectRatio);
|
|
}
|
|
this.shaderCanvas.width = canvasWidth;
|
|
this.shaderCanvas.height = canvasHeight;
|
|
this.shaderCanvas.style.width = width + "px";
|
|
this.shaderCanvas.style.height = height + "px";
|
|
this.gl.viewport(0, 0, canvasWidth, canvasHeight);
|
|
this.displayWidth = width;
|
|
this.displayHeight = height;
|
|
this.shaderHeight = height;
|
|
};
|
|
nodeType.prototype.createShaderProgram = function (vsSource, fsSource) {
|
|
const gl = this.gl;
|
|
if (!gl)
|
|
return null;
|
|
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
if (!vertexShader)
|
|
return null;
|
|
gl.shaderSource(vertexShader, vsSource);
|
|
gl.compileShader(vertexShader);
|
|
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
console.error('Vertex shader compile error:', gl.getShaderInfoLog(vertexShader));
|
|
gl.deleteShader(vertexShader);
|
|
return null;
|
|
}
|
|
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
if (!fragmentShader) {
|
|
gl.deleteShader(vertexShader);
|
|
return null;
|
|
}
|
|
gl.shaderSource(fragmentShader, fsSource);
|
|
gl.compileShader(fragmentShader);
|
|
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
console.error('Fragment shader compile error:', gl.getShaderInfoLog(fragmentShader));
|
|
gl.deleteShader(vertexShader);
|
|
gl.deleteShader(fragmentShader);
|
|
return null;
|
|
}
|
|
const program = gl.createProgram();
|
|
if (!program) {
|
|
gl.deleteShader(vertexShader);
|
|
gl.deleteShader(fragmentShader);
|
|
return null;
|
|
}
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
console.error('Shader program link error:', gl.getProgramInfoLog(program));
|
|
gl.deleteProgram(program);
|
|
gl.deleteShader(vertexShader);
|
|
gl.deleteShader(fragmentShader);
|
|
return null;
|
|
}
|
|
return program;
|
|
};
|
|
nodeType.prototype.loadShader = function (shaderType) {
|
|
if (!this.isShaderActive && !this.properties.shaderVisible)
|
|
return;
|
|
if (this.shaderPrograms[shaderType])
|
|
return;
|
|
if (!this.shaderSources[shaderType]) {
|
|
console.error('Shader source not found:', shaderType);
|
|
return;
|
|
}
|
|
if (!this.pendingShaders.includes(shaderType))
|
|
this.pendingShaders.push(shaderType);
|
|
if (!this.loadingShader)
|
|
this.processNextShader();
|
|
};
|
|
nodeType.prototype.processNextShader = function () {
|
|
if (!this.isShaderActive && !this.properties.shaderVisible) {
|
|
this.loadingShader = false;
|
|
this.pendingShaders = [];
|
|
return;
|
|
}
|
|
if (this.pendingShaders.length === 0) {
|
|
this.loadingShader = false;
|
|
return;
|
|
}
|
|
this.loadingShader = true;
|
|
const shaderType = this.pendingShaders.shift();
|
|
this.shaderPrograms[shaderType] = this.createShaderProgram(this.vertexShaderSource, this.fragmentShaderHeader + this.shaderSources[shaderType] + this.fragmentShaderFooter);
|
|
setTimeout(() => { this.processNextShader(); }, 10);
|
|
};
|
|
nodeType.prototype.updateAnimationTime = function () {
|
|
if (!this.isShaderActive)
|
|
return;
|
|
const now = performance.now();
|
|
if (this.properties.lastRenderTime > 0) {
|
|
const delta = (now - this.properties.lastRenderTime) / 1000;
|
|
this.properties.shaderTime += delta * this.properties.shaderSpeed;
|
|
}
|
|
this.properties.lastRenderTime = now;
|
|
};
|
|
nodeType.prototype.drawShader = function (program) {
|
|
const gl = this.gl;
|
|
if (!gl)
|
|
return;
|
|
// Ensure viewport matches canvas dimensions (canvas resize can reset it)
|
|
gl.viewport(0, 0, this.shaderCanvas.width, this.shaderCanvas.height);
|
|
gl.useProgram(program);
|
|
const locs = {
|
|
time: gl.getUniformLocation(program, 'u_time'),
|
|
intensity: gl.getUniformLocation(program, 'u_intensity'),
|
|
scale: gl.getUniformLocation(program, 'u_scale'),
|
|
octaves: gl.getUniformLocation(program, 'u_octaves'),
|
|
persistence: gl.getUniformLocation(program, 'u_persistence'),
|
|
lacunarity: gl.getUniformLocation(program, 'u_lacunarity'),
|
|
shapeType: gl.getUniformLocation(program, 'u_shapeType'),
|
|
shapeStrength: gl.getUniformLocation(program, 'u_shapeStrength'),
|
|
warpStrength: gl.getUniformLocation(program, 'u_warpStrength'),
|
|
phaseShift: gl.getUniformLocation(program, 'u_phaseShift'),
|
|
frequencyRange: gl.getUniformLocation(program, 'u_frequencyRange'),
|
|
distribution: gl.getUniformLocation(program, 'u_distribution'),
|
|
adaptationStrength: gl.getUniformLocation(program, 'u_adaptationStrength'),
|
|
resolutionScale: gl.getUniformLocation(program, 'u_resolutionScale'),
|
|
colorScheme: gl.getUniformLocation(program, 'u_colorScheme'),
|
|
};
|
|
gl.uniform1f(locs.time, this.properties.shaderTime);
|
|
gl.uniform1f(locs.intensity, this.properties.shaderColorIntensity);
|
|
gl.uniform1f(locs.scale, this.properties.shaderScale);
|
|
gl.uniform1f(locs.octaves, this.properties.shaderOctaves);
|
|
gl.uniform1f(locs.persistence, 0.5);
|
|
gl.uniform1f(locs.lacunarity, 2.0);
|
|
gl.uniform1i(locs.shapeType, SHAPE_TYPE_MAP[this.properties.shaderShapeType] || 0);
|
|
gl.uniform1f(locs.shapeStrength, this.properties.shaderShapeStrength);
|
|
gl.uniform1f(locs.warpStrength, this.properties.shaderWarpStrength);
|
|
gl.uniform1f(locs.phaseShift, this.properties.shaderPhaseShift);
|
|
gl.uniform1i(locs.frequencyRange, this.properties.shaderFrequencyRange || 0);
|
|
gl.uniform1i(locs.distribution, this.properties.shaderDistribution || 0);
|
|
gl.uniform1f(locs.adaptationStrength, this.properties.shaderAdaptationStrength || 0.5);
|
|
gl.uniform1f(locs.resolutionScale, this.properties.shaderResolutionScale || 512);
|
|
const colorSchemeValue = COLOR_SCHEME_MAP[this.properties.colorScheme];
|
|
gl.uniform1i(locs.colorScheme, colorSchemeValue !== undefined ? colorSchemeValue : 0);
|
|
const positionLocation = gl.getAttribLocation(program, 'a_position');
|
|
gl.enableVertexAttribArray(positionLocation);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
};
|
|
nodeType.prototype.renderShader = function () {
|
|
if (!this.isShaderActive || !this.gl || !this.shaderPrograms)
|
|
return;
|
|
const gl = this.gl;
|
|
const currentShaderType = this.properties.shaderType;
|
|
if (!this.shaderPrograms[currentShaderType]) {
|
|
this.loadShader(currentShaderType);
|
|
gl.clearColor(0.1, 0.1, 0.1, 1.0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
return;
|
|
}
|
|
const program = this.shaderPrograms[currentShaderType];
|
|
if (!program)
|
|
return;
|
|
gl.clearColor(0, 0, 0, 1);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
this.updateAnimationTime();
|
|
this.drawShader(program);
|
|
};
|
|
nodeType.prototype.onResize = function (size) {
|
|
this._isResizing = true;
|
|
size[0] = Math.max(this.min_width || 300, size[0]);
|
|
const baseSize = origComputeSize ? origComputeSize.call(this, [size[0], 0]) : [size[0], 0];
|
|
const baseHeight = baseSize[1];
|
|
const minTotalHeight = baseHeight + 50;
|
|
size[1] = Math.max(minTotalHeight, size[1]);
|
|
if (this.properties.shaderVisible) {
|
|
this.shaderHeight = Math.max(50, size[1] - baseHeight);
|
|
this.resizeShaderCanvas(size[0], this.shaderHeight);
|
|
}
|
|
this.size = size;
|
|
this._isResizing = false;
|
|
this.setDirtyCanvas(true, true);
|
|
};
|
|
nodeType.prototype.onDrawForeground = function (ctx) {
|
|
if (this.properties?.shaderVisible && this.shaderCanvas && !this.flags?.collapsed) {
|
|
this.isShaderActive = true;
|
|
// Call origOnDrawForeground FIRST so gradient title acts as background
|
|
if (origOnDrawForeground)
|
|
origOnDrawForeground.call(this, ctx);
|
|
// Render shader synchronously before drawing to ensure fresh content
|
|
this.renderShader();
|
|
if (!this.animationFrameId) {
|
|
const animate = () => {
|
|
if (!this.isShaderActive || !this.properties.shaderVisible) {
|
|
this.animationFrameId = null;
|
|
return;
|
|
}
|
|
this.setDirtyCanvas(true, false);
|
|
this.animationFrameId = requestAnimationFrame(animate);
|
|
};
|
|
this.animationFrameId = requestAnimationFrame(animate);
|
|
}
|
|
const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
|
|
const shaderY = baseSize[1];
|
|
const shaderHeight = Math.max(50, this.size[1] - shaderY);
|
|
// Draw shader canvas AFTER origOnDrawForeground so it renders on top of the gradient
|
|
try {
|
|
ctx.drawImage(this.shaderCanvas, 0, shaderY, this.size[0], shaderHeight);
|
|
}
|
|
catch (e) { /* ignore */ }
|
|
}
|
|
else {
|
|
if (origOnDrawForeground)
|
|
origOnDrawForeground.call(this, ctx);
|
|
this.isShaderActive = false;
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
}
|
|
};
|
|
nodeType.prototype.computeSize = function (size) {
|
|
size = size || [this.size?.[0] || 300, 0];
|
|
if (this.flags?.collapsed) {
|
|
size[0] = 190;
|
|
size[1] = 40;
|
|
return size;
|
|
}
|
|
if (origComputeSize)
|
|
origComputeSize.call(this, size);
|
|
const baseHeight = size[1];
|
|
if (this.properties?.shaderVisible && !this._isResizing) {
|
|
const currentShaderHeight = this.shaderHeight || 200;
|
|
size[1] = baseHeight + Math.max(50, currentShaderHeight);
|
|
}
|
|
return size;
|
|
};
|
|
nodeType.prototype.onRemoved = function () {
|
|
if (origOnRemoved)
|
|
origOnRemoved.call(this);
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
if (this.gl) {
|
|
for (const programType in this.shaderPrograms) {
|
|
if (this.shaderPrograms[programType])
|
|
this.gl.deleteProgram(this.shaderPrograms[programType]);
|
|
}
|
|
if (this.loadingIndicatorProgram) {
|
|
this.gl.deleteProgram(this.loadingIndicatorProgram);
|
|
this.loadingIndicatorProgram = null;
|
|
}
|
|
if (this.positionBuffer)
|
|
this.gl.deleteBuffer(this.positionBuffer);
|
|
this.shaderPrograms = {};
|
|
this.positionBuffer = null;
|
|
this.gl = null;
|
|
}
|
|
this.shaderSources = {};
|
|
this.shaderCanvas = null;
|
|
this.pendingShaders = [];
|
|
this.loadingShader = false;
|
|
};
|
|
nodeType.prototype.startBackgroundLoading = function () { return; };
|
|
nodeType.prototype.onConfigure = function (info) {
|
|
if (origOnConfigure)
|
|
origOnConfigure.apply(this, arguments);
|
|
if (this.widgets && this.properties) {
|
|
const updateWidget = (name, prop) => {
|
|
const w = this.widgets?.find(w => w.name === name);
|
|
if (w && this.properties[prop] !== undefined)
|
|
w.value = this.properties[prop];
|
|
};
|
|
updateWidget("Show Shader", "shaderVisible");
|
|
updateWidget("Show Tooltips", "tooltipsVisible");
|
|
updateWidget("Animation Speed 🖥️", "shaderSpeed");
|
|
updateWidget("Pixel Resolution 🖥️", "shaderResolutionScale");
|
|
if (isDirect) {
|
|
// The saved input values are the source of truth here.
|
|
syncFromInputs(this);
|
|
followInputs(this);
|
|
}
|
|
else {
|
|
updateWidget("Shader Noise Type 🔄", "shaderType");
|
|
updateWidget("Shape Mask Type 🔄", "shaderShapeType");
|
|
updateWidget("Color Scheme 🔄", "colorScheme");
|
|
updateWidget("Noise Scale 🔄", "shaderScale");
|
|
updateWidget("Octaves 🔄", "shaderOctaves");
|
|
updateWidget("Warp Strength 🔄", "shaderWarpStrength");
|
|
updateWidget("Shape Mask Strength 🔄", "shaderShapeStrength");
|
|
updateWidget("Phase Shift 🔄", "shaderPhaseShift");
|
|
updateWidget("Color Intensity 🔄", "shaderColorIntensity");
|
|
}
|
|
}
|
|
if (this.properties?.shaderVisible) {
|
|
const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
|
|
this.shaderHeight = this.shaderHeight || 200;
|
|
const expectedHeight = baseSize[1] + this.shaderHeight;
|
|
if (Math.abs(this.size[1] - expectedHeight) > 1) {
|
|
this.size[1] = expectedHeight;
|
|
this.resizeShaderCanvas(this.size[0], this.shaderHeight);
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
this.isShaderActive = true;
|
|
}
|
|
else {
|
|
this.isShaderActive = false;
|
|
}
|
|
};
|
|
nodeType.prototype.getExtraMenuOptions = function (canvas, options) {
|
|
if (origGetExtraMenuOptions)
|
|
origGetExtraMenuOptions.call(this, canvas, options);
|
|
options.push(null, {
|
|
content: this.properties.shaderVisible ? "Hide Shader Preview" : "Show Shader Preview",
|
|
callback: () => {
|
|
this.properties.shaderVisible = !this.properties.shaderVisible;
|
|
const shaderWidget = this.widgets?.find(w => w.name === "Show Shader");
|
|
if (shaderWidget)
|
|
shaderWidget.value = this.properties.shaderVisible;
|
|
const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
|
|
if (this.properties.shaderVisible && this.gl) {
|
|
this.shaderHeight = this.shaderHeight || 200;
|
|
this.size[1] = baseSize[1] + this.shaderHeight;
|
|
this.resizeShaderCanvas(this.size[0], this.shaderHeight);
|
|
this.isShaderActive = true;
|
|
this.properties.lastRenderTime = 0;
|
|
}
|
|
else {
|
|
this.size[1] = baseSize[1];
|
|
this.isShaderActive = false;
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
}
|
|
this.setDirtyCanvas(true, true);
|
|
}
|
|
});
|
|
return options;
|
|
};
|
|
}
|
|
});
|
|
//# sourceMappingURL=shader_renderer.js.map
|