Controls the frequency spectrum directly, allowing precise frequency band manipulation.
+
+
+
+
+
3D Projection
+
Projects three-dimensional noise onto a 2D plane, creating depth and volumetric effects.
+
+
+
+
+
Curl Noise
+
Generates divergence-free vector fields that create perfect fluid-like flow patterns.
+
+
+
+
+
+
๐ Unlock Exclusive Shader Noise Palettes!
+
Become a valued member to access an expanded library of unique shader noise archetypes, advanced shader features, and custom ComfyUI tools. Your support helps fuel further development!
+
+
+
+
+
+
+
+
โ๏ธ Noise Mathematics ๐ The Underlying Formulations
+
Each shader noise archetype in ShaderNoiseKSampler is built upon rigorous mathematical foundations. Understanding these mathematical principles reveals the sacred geometry inherent in these patterns and allows for more intentional application in the creative process.
+
+
+
Perlin
+
Cellular
+
Tensor Field
+
Curl Noise
+
Domain Warp
+
Fractal
+
Waves
+
Gaussian
+
Heterogeneous FBM
+
Interference
+
Spectral
+
3D Projection
+
+
+
+
Perlin Noise Mathematics
+
Perlin noise, developed by Ken Perlin in 1983, uses a grid of random gradient vectors with interpolation to create smooth, natural-looking noise. The core computation involves:
+
+ n(x,y) = โ ฯi ยท gi ยท ((x,y) - (xi,yi))
+
+
Where gi are random gradient vectors at grid points, and ฯi are interpolation weights. The smoothstep function provides the basis for the interpolation:
+
+ smoothstep(t) = t2(3 - 2t)
+
+
This smooth interpolation creates the characteristic flowing appearance of Perlin noise, making it ideal for natural phenomena like terrain, clouds, and flowing water.
+
+
+ Perlin Noise Implementation
+
+
+
def perlin_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters for noise generation
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = shader_params.get("shaderOctaves", 3)
+ warp_strength = shader_params.get("shaderWarpStrength", 0.5)
+ phase_shift = shader_params.get("shaderPhaseShift", 0.5)
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Combine into coordinate tensor [batch, height, width, 2]
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Apply Perlin noise with multiple octaves
+ result = torch.zeros(batch_size, height, width, 1, device=device)
+ amplitude = 0.5
+ frequency = 1.0
+
+ for i in range(int(octaves)):
+ # Apply domain warping if warp_strength > 0
+ if i > 0 and warp_strength > 0.0:
+ warp = compute_warp_field(p, frequency, time, seed + i)
+ noise = sample_perlin(p * frequency + warp * warp_strength, seed + i)
+ else:
+ noise = sample_perlin(p * frequency, seed + i)
+
+ # Add to result with decreasing amplitude
+ result += amplitude * noise
+
+ # Prepare for next octave
+ frequency *= 2.0
+ amplitude *= 0.5
+
+ # Apply phase shift (contrast)
+ result *= (1.0 + phase_shift)
+
+ return result
+
+
+
+
+
Cellular Noise Mathematics
+
Cellular noise (also known as Worley noise) creates Voronoi-like patterns based on distance metrics to feature points:
Where dist() is a distance function (typically Euclidean) and Fn returns the distance to the nth closest feature point. Various cellular patterns emerge by combining these distances:
+
+ F2 - F1 : cell edges
+ F1 : cell interiors
+ 2F1 - F2 : cracks
+
+
These combinations create patterns reminiscent of cellular structures, bubbles, and organic tissues.
+
+
+ Cellular Noise Implementation
+
+
+
def cellular_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = shader_params.get("shaderOctaves", 1)
+ # warp_strength = shader_params.get("shaderWarpStrength", 0.5) # Not used in this snippet
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Apply cellular noise
+ result = torch.zeros(batch_size, height, width, 1, device=device)
+
+ # Generate cellular pattern based on octaves mode
+ pattern_type = int(octaves) % 4 # Use octaves to select pattern type
+
+ # Calculate F1 and F2 distances (assuming compute_feature_distances exists)
+ f1, f2 = compute_feature_distances(p * scale, seed)
+
+ # Select pattern based on type
+ if pattern_type == 0:
+ result = f1 # Basic Worley/cellular noise
+ elif pattern_type == 1:
+ result = f2 # Second closest point
+ elif pattern_type == 2:
+ result = f2 - f1 # Classic cellular look with ridges
+ else:
+ result = f1 * f2 # Different look with smaller features
+
+ return result
+
+
+
+
+
Tensor Field Mathematics
+
Tensor fields represent directional information at every point in space, creating flow-like patterns. The core mathematics involves computing the eigenvalues ฮป and eigenvectors v of tensor matrices:
In 2D, this simplifies to a scalar field where the curl is perpendicular to the plane:
+
+ curl(ฮจ)(x,y) = โฮจy/โx - โฮจx/โy
+
+
This mathematical property ensures the resulting vector field has zero divergence (โยทv = 0), creating perfect flow patterns without sources or sinks.
+
+
+ Curl Noise Implementation
+
+
+
def curl_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ # warp_strength = shader_params.get("shaderWarpStrength", 0.5) # Not used directly here for simple curl value
+
+ # Create coordinate grid
+ y_coords, x_coords = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ # p = torch.stack([x_coords, y_coords], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1) # Not used
+
+ # Compute potential fields (scalar fields to take curl of, assuming compute_noise_field)
+ # For 2D curl, we need two scalar potential fields or components of a vector potential field
+ # Let's assume ฮจ = (ฯx(x,y,t), ฯy(x,y,t)) and we want curl_z = d(ฯy)/dx - d(ฯx)/dy
+
+ # Simplified: generate two noise fields for potential components
+ # These would typically be generated by a noise function like Perlin or Simplex
+ potential_y_coords = torch.stack([x_coords * scale, y_coords * scale + time], dim=-1)
+ potential_x_coords = torch.stack([x_coords * scale + time, y_coords * scale], dim=-1)
+
+ # Placeholder for actual noise generation (e.g., simplex_noise(potential_coords, seed))
+ potential_y = torch.sin(potential_y_coords[...,0] * 5) * torch.cos(potential_y_coords[...,1] * 5)
+ potential_x = torch.cos(potential_x_coords[...,0] * 5) * torch.sin(potential_x_coords[...,1] * 5)
+
+ potential_y = potential_y.unsqueeze(0).unsqueeze(-1).repeat(batch_size,1,1,1)
+ potential_x = potential_x.unsqueeze(0).unsqueeze(-1).repeat(batch_size,1,1,1)
+
+
+ # Calculate curl using finite differences
+ epsilon = 0.01 # step for derivative
+
+ # Partial derivative of potential_y with respect to x
+ # potential_y(x + eps) - potential_y(x - eps) / (2*eps)
+ # We'd need to sample noise at shifted coords or use analytical derivatives if possible
+ # Simplified finite difference on the grid:
+ dpy_dx = (torch.roll(potential_y, shifts=-1, dims=2) - torch.roll(potential_y, shifts=1, dims=2)) / (2 * (2.0/width) ) # dx = 2.0/width for range -1 to 1
+
+ # Partial derivative of potential_x with respect to y
+ # potential_x(y + eps) - potential_x(y - eps) / (2*eps)
+ dpx_dy = (torch.roll(potential_x, shifts=-1, dims=1) - torch.roll(potential_x, shifts=1, dims=1)) / (2 * (2.0/height) ) # dy = 2.0/height
+
+ # Curl in 2D (z-component)
+ curl_z = dpy_dx - dpx_dy
+
+ return curl_z # This is a scalar field representing curl strength
+
+
+
+
+
+
Domain Warp Mathematics
+
Domain warping is a technique where the input coordinates (the domain) of a noise function are displaced or distorted by another noise function. This creates swirling, turbulent, or flowing effects in the final pattern. Instead of sampling noise at point P, we sample at P + offset(P), where offset(P) is itself a noise function.
Noisebase: The primary noise function (e.g., Perlin, Simplex).
+
Noisedisplacement: A noise function generating the offset vectors.
+
Swarp: Warp strength, controlling the magnitude of distortion.
+
Fwarp: Warp frequency, controlling the detail of the distortion.
+
+
By applying this recursively (warping the domain of the displacement noise itself), more complex fractal warping effects can be achieved.
+
+
+ Domain Warp Implementation
+
+
+
def domain_warp_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = shader_params.get("shaderOctaves", 3)) # Used for the base noise often
+ warp_strength = shader_params.get("shaderWarpStrength", 0.5)
+ warp_frequency_mult = shader_params.get("shaderWarpFrequency", 0.5) # Relative frequency for warp noise
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Time component for animation
+ t_offset = time * 0.1
+
+ # Generate displacement fields (two noise channels for 2D offset)
+ # It's common to use a simpler noise like Perlin for displacement
+ # Assuming a 'sample_perlin_octaves' function for simplicity
+
+ # Coordinates for displacement noise
+ p_warp = p * scale * warp_frequency_mult
+
+ # Simplified Perlin sampling for displacement
+ # In a real scenario, you'd use an actual Perlin/Simplex implementation
+ q_x = sample_perlin_like(p_warp + torch.tensor([10.3 + t_offset, 20.7], device=device), seed + 100)
+ q_y = sample_perlin_like(p_warp + torch.tensor([-5.1, -15.9 + t_offset], device=device), seed + 200)
+
+ displacement = torch.cat([q_x, q_y], dim=-1) * warp_strength
+
+ # Apply displacement to original coordinates
+ p_displaced = p * scale + displacement
+
+ # Generate base noise with displaced coordinates
+ # Again, assuming a base noise function (e.g., Perlin with octaves)
+ # result = sample_perlin_octaves(p_displaced, octaves, seed)
+ result = sample_perlin_like(p_displaced, seed) # Simplified
+
+ return result
+
+# Helper placeholder for a Perlin-like noise generation
+def sample_perlin_like(coords, seed):
+ # This is a highly simplified stand-in for actual Perlin noise
+ # A real implementation would involve gradients, lattice points, interpolation etc.
+ torch.manual_seed(seed)
+ # Create some pseudo-randomness based on coords and seed
+ # This is NOT Perlin noise but serves as a placeholder structure
+ noise = torch.sin(coords[..., 0] * 5.0 + coords[..., 1] * 3.0 + seed * 0.1) * \
+ torch.cos(coords[..., 0] * 2.0 - coords[..., 1] * 6.0 + seed * 0.05)
+ return noise.unsqueeze(-1) # Add channel dimension
+
+
+
+
+
+
Fractal Noise (FBM) Mathematics
+
Fractal noise, often implemented as Fractal Brownian Motion (FBM), is a fundamental technique for generating natural-looking textures. It is constructed by summing multiple layers (octaves) of a base noise function (like Perlin or Simplex). Each successive octave has a higher frequency and a lower amplitude.
+
+ FBM(P) = โN-1i=0 Ai ร Noise(Fi ร P)
+
+
Where:
+
+
P: The input coordinate.
+
N: The number of octaves (layers of detail).
+
Noise: The base coherent noise function (e.g., Perlin).
+
A: Amplitude factor (persistence, typically < 1, e.g., 0.5). Controls how much amplitude decreases per octave.
+
F: Frequency factor (lacunarity, typically > 1, e.g., 2.0). Controls how much frequency increases per octave.
+
+
The summation creates a pattern that exhibits self-similarity across different scales, characteristic of many natural phenomena.
+
+
+ Fractal Noise (FBM) Implementation
+
+
+
def fractal_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = int(shader_params.get("shaderOctaves", 4))
+ persistence = shader_params.get("shaderPersistence", 0.5) # Amplitude scaling
+ lacunarity = shader_params.get("shaderLacunarity", 2.0) # Frequency scaling
+ phase_shift = shader_params.get("shaderPhaseShift", 0.0) # For adding variation
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ total_noise = torch.zeros(batch_size, height, width, 1, device=device)
+ current_amplitude = 1.0
+ current_frequency = 1.0
+ normalization_factor = 0.0
+
+ for i in range(octaves):
+ # Apply scale and current frequency to coordinates
+ # Add time and phase_shift for variation / animation
+ coords_at_octave = p * scale * current_frequency + time * 0.05 * current_frequency + phase_shift * i
+
+ # Generate base noise (e.g., Perlin or Simplex)
+ # Assuming 'sample_perlin_like' as a placeholder for a proper noise function
+ noise_val = sample_perlin_like(coords_at_octave, seed + i)
+
+ total_noise += noise_val * current_amplitude
+ normalization_factor += current_amplitude
+
+ current_amplitude *= persistence
+ current_frequency *= lacunarity
+
+ # Normalize the result to roughly [-1, 1] range
+ if normalization_factor > 0:
+ total_noise /= normalization_factor
+
+ return total_noise
+
+
+
+
+
+
Waves Noise Mathematics
+
Waves noise is typically generated by summing multiple sine or cosine wave functions. Each wave can have its own amplitude, frequency, phase, and direction. The superposition of these waves can create a wide variety_of_patterns, from simple ripples to complex interference effects.
+
+ Waves(P) = โi Ai ร sin(ki โ P + ฯit + ฯi)
+
+
Where for each wave i:
+
+
P: The input coordinate (e.g., (x,y)).
+
Ai: Amplitude of the wave.
+
ki: Wave vector (determines direction and spatial frequency/wavelength). Its magnitude |ki| = 2ฯ / ฮปi.
+
ฯi: Angular frequency (determines temporal oscillation speed).
+
t: Time.
+
ฯi: Phase offset.
+
โ : Dot product.
+
+
Simpler forms might fix directions or use scalar frequencies if directionality is not complex.
+
+
+ Waves Noise Implementation
+
+
+
def waves_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale = shader_params.get("shaderScale", 1.0) # General scale for coordinates
+ num_waves = int(shader_params.get("shaderOctaves", 3)) # Use 'octaves' as number of waves
+ base_frequency = shader_params.get("shaderBaseFrequency", 5.0)
+ amplitude_variation = shader_params.get("shaderAmplitudeVariation", 0.5)
+ phase_shift_speed = shader_params.get("shaderPhaseShift", 0.2) # Use for time-based phase shift
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+ p = p * scale # Apply general scale
+
+ total_waves = torch.zeros(batch_size, height, width, 1, device=device)
+
+ torch.manual_seed(seed) # For reproducible wave directions/phases
+
+ for i in range(num_waves):
+ # Generate random direction for each wave
+ angle = torch.rand(1, device=device) * 2.0 * 3.14159265 # math.pi
+ direction = torch.tensor([torch.cos(angle), torch.sin(angle)], device=device).view(1, 1, 1, 2)
+
+ frequency = base_frequency * (1.0 + (torch.rand(1, device=device) - 0.5) * 0.5 * i) # Vary frequency slightly
+ amplitude = 1.0 / num_waves * (1.0 + (torch.rand(1, device=device) - 0.5) * amplitude_variation)
+ phase = torch.rand(1, device=device) * 2.0 * 3.14159265 + time * phase_shift_speed * (i + 1)
+
+ # Project coordinates onto wave direction: dot(p, direction)
+ wave_input = (p * direction).sum(dim=-1, keepdim=True)
+
+ wave_val = torch.sin(wave_input * frequency + phase) * amplitude
+ total_waves += wave_val
+
+ # Normalize to be roughly in [-1, 1], though sum of sines can exceed this
+ # A more robust normalization might be needed depending on num_waves and amplitudes
+ # total_waves = torch.clamp(total_waves, -1.0, 1.0)
+
+ return total_waves
+
+
+
+
+
+
Gaussian Noise Mathematics
+
Gaussian noise is a statistical noise characterized by a probability density function (PDF) that follows the Gaussian (or normal) distribution. Its values are typically clustered around a mean (ฮผ), with a spread determined by the standard deviation (ฯ). In image processing, it's often used to simulate random sensor noise or as a basis for other effects.
For generating Gaussian noise, random numbers are drawn from this distribution. In practice, libraries provide functions to directly generate such noise (e.g., torch.randn which samples from N(0,1)).
+
+
ฮผ (Mean): The average value, often 0 for noise.
+
ฯ (Standard Deviation): Controls the "spread" or intensity of the noise. ฯ2 is the variance.
+
+
Gaussian noise is "white" if its values are statistically independent and identically distributed at each point.
+
+
+ Gaussian Noise Implementation
+
+
+
def gaussian_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ mean = shader_params.get("shaderMean", 0.0)
+ std_dev = shader_params.get("shaderStdDev", 1.0) # This is 'scale' in some contexts
+ # 'scale' from shader_params usually means spatial scale for other noises,
+ # for Gaussian, it often refers to standard deviation or amplitude.
+ # Let's use a specific param like shaderStdDev.
+
+ # Ensure seed is used for reproducibility
+ torch.manual_seed(seed)
+
+ # Generate Gaussian noise
+ # torch.randn generates noise from a standard normal distribution (mean 0, std 1)
+ noise_tensor = torch.randn(batch_size, 1, height, width, device=device)
+
+ # Adjust mean and standard deviation
+ # Noise = Mean + StdDev * StandardNormalNoise
+ noise_tensor = mean + std_dev * noise_tensor
+
+ # Unlike coherent noise (Perlin, etc.), Gaussian noise typically doesn't use a coordinate grid
+ # for its generation, as each pixel's value is independent (for white Gaussian noise).
+ # Parameters like 'scale', 'octaves' are less relevant in the typical sense.
+ # 'time' could be used to modulate mean or std_dev for animated effects if desired.
+ # For example: animated_std_dev = std_dev * (1.0 + 0.5 * torch.sin(torch.tensor(time)))
+ # noise_tensor = mean + animated_std_dev * noise_tensor
+
+ # Output is typically 1 channel, but can be expanded if needed for coloring or blending.
+ # The KSampler expects 4 channels if blending with latent.
+ # This function would typically output raw noise, channel expansion happens later.
+
+ return noise_tensor # Shape: [batch_size, 1, height, width]
+
+
+
+
+
+
Heterogeneous FBM Mathematics
+
Heterogeneous Fractal Brownian Motion (Hetero FBM) is an extension of standard FBM. While standard FBM uses constant parameters (like persistence/amplitude falloff and lacunarity/frequency gain) across all octaves and spatial locations, Hetero FBM allows these parameters, or the base noise characteristics, to vary spatially. This creates textures with non-uniform complexity, where some areas might be smoother and others rougher or more detailed.
A(P, i): Spatially varying amplitude for octave i at point P.
+
F(P, i): Spatially varying frequency for octave i at point P.
+
Noise(..., params(P,i)): Base noise function whose own internal parameters might also vary spatially.
+
+
The spatial variation itself is often controlled by another noise function or a predefined map. For example, the Hurst exponent (H), which relates to persistence, could be made to vary across the domain.
+
+
+ Heterogeneous FBM Implementation
+
+
+
def heterogeneous_fbm_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract base parameters
+ scale = shader_params.get("shaderScale", 1.0)
+ octaves = int(shader_params.get("shaderOctaves", 4))
+
+ # Parameters for heterogeneity - these could be controlled by other noise fields
+ base_persistence = shader_params.get("shaderPersistence", 0.5)
+ base_lacunarity = shader_params.get("shaderLacunarity", 2.0)
+ heterogeneity_strength = shader_params.get("shaderHeteroStrength", 0.3)
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Generate a control noise field for varying persistence/lacunarity
+ # This control noise determines how parameters change spatially
+ control_coords = p * scale * 0.3 + time * 0.02 # Slower varying control
+ control_noise = sample_perlin_like(control_coords, seed + 500) # Placeholder
+
+ total_noise = torch.zeros(batch_size, height, width, 1, device=device)
+ current_amplitude = torch.ones(batch_size, height, width, 1, device=device)
+ current_frequency = torch.ones(batch_size, height, width, 1, device=device) * scale
+ normalization_factor = torch.zeros(batch_size, height, width, 1, device=device)
+
+ for i in range(octaves):
+ # Modulate persistence and lacunarity spatially using control_noise
+ # This is a simplified modulation
+ spatial_persistence_mod = (control_noise * heterogeneity_strength) # Range e.g. [-0.3, 0.3]
+ spatial_lacunarity_mod = (control_noise * heterogeneity_strength * 0.5)
+
+ current_persistence = torch.clamp(base_persistence + spatial_persistence_mod, 0.1, 0.9)
+ current_lacunarity = torch.clamp(base_lacunarity + spatial_lacunarity_mod, 1.1, 3.0)
+
+ coords_at_octave = p * current_frequency + time * 0.05 * current_frequency.mean() # Use mean freq for time anim
+
+ noise_val = sample_perlin_like(coords_at_octave, seed + i)
+
+ total_noise += noise_val * current_amplitude
+ normalization_factor += current_amplitude
+
+ current_amplitude *= current_persistence
+ current_frequency *= current_lacunarity
+
+ if normalization_factor.min() > 1e-5: # Avoid division by zero
+ total_noise /= normalization_factor
+ else: # Fallback if normalization factor is too small everywhere
+ total_noise = torch.clamp(total_noise, -1.0, 1.0)
+
+ return total_noise
+
+
+
+
+
+
Interference Noise Mathematics
+
Interference patterns arise from the superposition of two or more waves. When waves meet, they can reinforce each other (constructive interference) or cancel each other out (destructive interference), depending on their relative phases and amplitudes. This principle is fundamental in physics (e.g., light and sound waves) and can be used to generate complex visual patterns.
A common way to generate interference is by summing or multiplying wave functions:
+
Example with two sine waves:
+
+ V(P) = A1sin(k1โ P + ฯ1) + A2sin(k2โ P + ฯ2)
+
+
Or using noise functions as sources:
+
+ V(P) = Noise1(P) + Noise2(P ร S + O)
+
+
Where S is a scale and O is an offset for Noise2 to make it different from Noise1. The visual character depends heavily on the frequencies and relative phases of the interfering sources.
+
+
+ Interference Noise Implementation
+
+
+
def interference_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale1 = shader_params.get("shaderScale", 1.0)
+ scale2 = shader_params.get("shaderScale2", 1.5) # Scale for the second noise source
+ octaves1 = int(shader_params.get("shaderOctaves", 3))
+ octaves2 = int(shader_params.get("shaderOctaves2", 2))
+ phase_offset = shader_params.get("shaderPhaseShift", 0.5) # Controls relative phase or timing
+
+ # Create coordinate grid
+ y, x = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ p = torch.stack([x, y], dim=-1).unsqueeze(0).repeat(batch_size, 1, 1, 1)
+
+ # Generate two separate noise fields
+ # Using placeholder 'sample_fbm_like' for simplicity
+ noise_field1_coords = p * scale1 + time * 0.03
+ noise_field1 = sample_fbm_like(noise_field1_coords, octaves1, seed, persistence=0.5, lacunarity=2.0)
+
+ noise_field2_coords = p * scale2 + time * 0.05 + phase_offset # Add phase_offset to vary interaction
+ noise_field2 = sample_fbm_like(noise_field2_coords, octaves2, seed + 10, persistence=0.6, lacunarity=1.8)
+
+ # Combine the noise fields to create interference
+ # Different combination methods yield different patterns:
+ # 1. Addition: result = noise_field1 + noise_field2
+ # 2. Multiplication: result = noise_field1 * noise_field2
+ # 3. Difference: result = noise_field1 - noise_field2
+ # 4. Using trigonometric functions: result = torch.cos(noise_field1 * math.pi) + torch.sin(noise_field2 * math.pi)
+
+ # Example: Cosine/Sine interference for more distinct patterns
+ pi = 3.14159265
+ interference_pattern = torch.cos(noise_field1 * pi) + torch.sin(noise_field2 * pi * (1.0 + phase_offset*0.2))
+
+ # Normalize the result (sum of two sines/cosines can range from -2 to 2)
+ result = interference_pattern / 2.0
+ result = torch.clamp(result, -1.0, 1.0)
+
+ return result
+
+# Placeholder for FBM-like noise generation
+def sample_fbm_like(coords, octaves, seed, persistence, lacunarity):
+ total_noise = torch.zeros_like(coords[..., 0:1]) # Ensure output has 1 channel
+ current_amplitude = 1.0
+ current_frequency = 1.0
+ temp_coords = coords # Avoid modifying original coords if it's used elsewhere
+
+ for i in range(int(octaves)):
+ noise_val = sample_perlin_like(temp_coords * current_frequency, seed + i)
+ total_noise += noise_val * current_amplitude
+ current_amplitude *= persistence
+ current_frequency *= lacunarity
+
+ # Basic normalization attempt
+ max_possible_amp = sum([persistence**i for i in range(int(octaves))])
+ if max_possible_amp > 0:
+ total_noise /= max_possible_amp
+
+ return total_noise
+
+
+
+
+
+
Spectral Noise Mathematics
+
Spectral noise generation involves directly defining or manipulating the noise's properties in the frequency domain (its spectrum) using techniques like the Fourier Transform. By controlling the amplitude and phase of different frequencies, a wide variety_of_textures can be created, from smooth to rough, or with specific directional biases.
+
The general process is:
+
+
Start with white noise in the spatial domain or directly create a spectrum.
+
Compute its Fast Fourier Transform (FFT) to get the frequency domain representation (spectrum).
+
Modify the spectrum: Apply a filter (e.g., 1/fฮฒ for pink/brown/blue noise, band-pass, directional filters).
+
Compute the Inverse Fast Fourier Transform (IFFT) to convert the modified spectrum back to the spatial domain.
The filter often takes the form 1/fฮฒ, where f is frequency and ฮฒ controls the "color" of the noise (e.g., ฮฒ=0 for white, ฮฒ=1 for pink, ฮฒ=2 for brown/red).
+
+
+ Spectral Noise Implementation
+
+
+
def spectral_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ beta = shader_params.get("shaderSpectralBeta", 1.0) # Exponent for 1/f^beta filter
+ # A low-pass/high-pass cutoff might also be a parameter
+ low_freq_boost = shader_params.get("shaderLowFreqBoost", 1.0)
+ high_freq_attenuation = shader_params.get("shaderHighFreqAtten", 1.0)
+
+ torch.manual_seed(seed)
+
+ # 1. Start with white noise in spatial domain
+ white_noise_spatial = torch.rand(batch_size, 1, height, width, device=device) * 2.0 - 1.0
+
+ # 2. Compute FFT
+ # torch.fft.fft2 performs 2D FFT. For real input, rfft2 is more efficient.
+ spectrum = torch.fft.rfft2(white_noise_spatial, norm="ortho") # norm="ortho" is often good
+
+ # Create frequency coordinates (kx, ky)
+ # Frequencies for rfft2: ky goes from 0 to H//2. kx goes from 0 to W-1, then wraps around.
+ ky = torch.fft.rfftfreq(height, d=1.0/height, device=device) # Normalized frequencies
+ kx = torch.fft.fftfreq(width, d=1.0/width, device=device) # Frequencies for full FFT
+
+ # For rfft2, kx used will be kx[:W//2 + 1]
+ kx_r = kx[:width//2 + 1]
+
+ # Create a 2D grid of frequencies (squared magnitude for filter)
+ # Broadcasting ky.unsqueeze(1) and kx_r.unsqueeze(0)
+ freq_sq = ky.unsqueeze(1)**2 + kx_r.unsqueeze(0)**2
+ freq_sq = freq_sq.unsqueeze(0).unsqueeze(0) # Add batch and channel dims
+
+ # Avoid division by zero at DC component (freq=0)
+ freq_sq[freq_sq < 1e-6] = 1e-6
+
+ # 3. Create and apply the 1/f^beta filter
+ # Power spectrum is proportional to 1 / (freq_magnitude ^ beta)
+ # Amplitude is proportional to 1 / (freq_magnitude ^ (beta/2))
+ filter_amplitude = (1.0 / (torch.sqrt(freq_sq)**(beta/2.0)))
+
+ # Apply additional boosts/attenuations
+ # Example: Boost low frequencies, attenuate high frequencies
+ # This is a very simplified frequency band control
+ freq_magnitude = torch.sqrt(freq_sq)
+ filter_amplitude *= torch.exp(- (freq_magnitude / (height*0.5)) * (1.0-low_freq_boost) ) # Boost low (smaller effect here)
+ filter_amplitude *= torch.exp(- (freq_magnitude / (height*0.1)) * (high_freq_attenuation-1.0) ) # Attenuate high
+
+ # Ensure DC component (mean) is not overly amplified if beta is high
+ # This depends on specific needs. Sometimes DC is zeroed out or handled separately.
+ # filter_amplitude[..., 0, 0] = 1.0 # Or some other controlled value
+
+ modified_spectrum = spectrum * filter_amplitude
+
+ # Add some phase shift based on time for animation (subtle effect)
+ if time > 0:
+ phase_shift_val = torch.exp(1j * freq_magnitude * time * 0.01)
+ modified_spectrum *= phase_shift_val
+
+ # 4. Compute IFFT
+ spatial_noise_filtered = torch.fft.irfft2(modified_spectrum, s=(height, width), norm="ortho")
+
+ # Normalize the result to roughly [-1, 1]
+ std_val = torch.std(spatial_noise_filtered)
+ if std_val > 1e-5:
+ spatial_noise_filtered = (spatial_noise_filtered - torch.mean(spatial_noise_filtered)) / std_val
+
+ return torch.clamp(spatial_noise_filtered, -1.0, 1.0)
+
+
+
+
+
+
3D Projection Noise Mathematics
+
3D Projection noise involves generating a 3D noise field (e.g., 3D Perlin, Simplex, or FBM) and then sampling a 2D slice from it. This technique creates 2D patterns that appear to have depth, volume, or temporal evolution if the slice position changes over time.
+
+ Noise2D(x, y) = Noise3D(x ร Sxy, y ร Sxy, zslice ร Sz + T ร Vt)
+
+
Where:
+
+
(x, y): 2D coordinates for the output noise.
+
Noise3D: A 3D coherent noise function.
+
Sxy: Spatial scaling factor for x and y axes.
+
zslice: The depth or position of the 2D slice along the third dimension (often z).
+
Sz: Scaling factor for the z-dimension, controlling "thickness" or detail along z.
+
T: Time parameter for animation.
+
Vt: Velocity of slicing through the 3D noise field over time.
+
+
By animating zslice or rotating the sampling plane, dynamic evolving textures can be created from a static 3D noise volume.
+
+
+ 3D Projection Noise Implementation
+
+
+
def projection_3d_noise_with_params(cls, batch_size, height, width, shader_params, time, device, seed):
+ # Extract parameters
+ scale_xy = shader_params.get("shaderScale", 1.0) # Spatial scale for X and Y
+ scale_z = shader_params.get("shaderZScale", 1.0) # Scale for the Z dimension of the 3D noise
+ octaves = int(shader_params.get("shaderOctaves", 3))
+ z_slice_pos = shader_params.get("shaderZSlice", 0.0) # Static Z position for the slice
+ time_travel_speed = shader_params.get("shaderTimeSpeed", 0.1) # How fast 'time' moves the slice in Z
+
+ # Create 2D coordinate grid for x, y
+ y_coords, x_coords = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+
+ # Prepare 3D coordinates for sampling 3D noise
+ # X and Y are scaled by scale_xy
+ # Z is determined by z_slice_pos, animated by time, and scaled by scale_z
+
+ x_3d = x_coords.unsqueeze(0).repeat(batch_size, 1, 1) * scale_xy
+ y_3d = y_coords.unsqueeze(0).repeat(batch_size, 1, 1) * scale_xy
+
+ # Calculate Z coordinate for the slice
+ # Static part from z_slice_pos, dynamic part from time
+ z_val_static = z_slice_pos
+ z_val_dynamic = time * time_travel_speed
+ z_3d_scalar = (z_val_static + z_val_dynamic) * scale_z
+
+ # Expand z_scalar to match dimensions of x_3d, y_3d for stacking
+ z_3d = torch.full_like(x_3d, z_3d_scalar)
+
+ # Stack to form [Batch, Height, Width, 3] coordinates
+ p_3d = torch.stack([x_3d, y_3d, z_3d], dim=-1)
+
+ # Generate 3D noise (e.g., 3D Perlin or Simplex FBM)
+ # Assuming a placeholder 'sample_3d_fbm_like'
+ # This function would take 3D coordinates and produce a scalar noise value.
+ result_3d_noise = sample_3d_fbm_like(p_3d, octaves, seed, persistence=0.5, lacunarity=2.0)
+
+ return result_3d_noise # Expected shape: [Batch, Height, Width, 1]
+
+# Placeholder for 3D FBM-like noise generation
+def sample_3d_fbm_like(coords_3d, octaves, seed, persistence, lacunarity):
+ # coords_3d shape: [B, H, W, 3]
+ total_noise = torch.zeros_like(coords_3d[..., 0:1]) # Output [B,H,W,1]
+ current_amplitude = 1.0
+ current_frequency = 1.0
+
+ for i in range(int(octaves)):
+ # In a real 3D noise, this would use a 3D hash/gradient function
+ # Simplified pseudo-3D noise for placeholder:
+ temp_coords = coords_3d * current_frequency
+ noise_val = (torch.sin(temp_coords[..., 0] + temp_coords[..., 2] * 0.5 + seed * 0.1 * (i+1)) *
+ torch.cos(temp_coords[..., 1] - temp_coords[..., 2] * 0.3 + seed * 0.05 * (i+1)))
+ noise_val = noise_val.unsqueeze(-1) # Add channel dim
+
+ total_noise += noise_val * current_amplitude
+ current_amplitude *= persistence
+ current_frequency *= lacunarity
+
+ max_possible_amp = sum([persistence**i for i in range(int(octaves))])
+ if max_possible_amp > 0:
+ total_noise /= max_possible_amp
+
+ return torch.clamp(total_noise, -1.0, 1.0)
+
+
+
+
+
+
+
+
+
+
๐ญ Blend Modes โ๏ธ The Alchemical Combinations
+
ShaderNoiseKSampler offers various blend modes to combine shader noise with base noise, each creating distinct alchemical transformations:
+
+
+
๐งฉ Blend Operations
+
+
+
Normal
+
Simple linear interpolation between base and shader noise, controlled by shader_strength
+
result = base * (1.0 - ฮฑ) + shader * ฮฑ
+
+
+
Add
+
Adds shader noise to base noise, creating brightened areas where patterns align
+
result = base + shader * ฮฑ
+
+
+
Multiply
+
Multiplies base and shader noise, darkening the overall pattern
+
result = base * (shader * ฮฑ + (1.0 - ฮฑ))
+
+
+
Screen
+
Inverts, multiplies, then inverts again, brightening patterns
+
result = 1.0 - (1.0 - base) * (1.0 - shader * ฮฑ)
+
+
+
Overlay
+
Combines Multiply and Screen modes for enhanced contrast
+
result = base < 0.5 ? 2.0 * base * shader : 1.0 - 2.0 * (1.0 - base) * (1.0 - shader)
+
+
+
Soft Light
+
Darkens or lightens colors depending on shader noise
+
result = (1.0 - base) * base * shader + base * (1.0 - (1.0 - base) * (1.0 - shader))
+
+
+
Hard Light
+
More intense version of Overlay with sharper contrast
Subtracts darker color from lighter color, creating distinctive edges
+
result = base * (1.0 - ฮฑ) + abs(base - shader) * ฮฑ
+
+
+
+
+
+
๐งฟ Blend Mode Harmonics
+
The choice of blend mode significantly impacts the generated image. Different blend modes resonate with different shader noise types and prompt themes:
Beyond the core shader noise patterns, ShaderNoiseKSampler offers additional mathematical transformations that can dramatically alter the character of the generated shader noise:
+
+
+
๐งฎ Mathematical Operators
+
+
+
Reverse
+
Inverts the sign of the noise, turning peaks into valleys and vice versa
+
T(noise) = -noise
+
+
+
Inverse
+
Reverses the values within the 0-1 range, preserving the overall pattern
+
T(noise) = 1.0 - noise
+
+
+
Absolute
+
Takes the absolute value, creating sharp ridges at zero-crossings
+
T(noise) = |noise|
+
+
+
Square
+
Squares the values, enhancing high values and diminishing low values
+
T(noise) = noise2
+
+
+
Square Root
+
Takes the square root, enhancing low values and compressing high values
+
T(noise) = โnoise
+
+
+
Log
+
Takes the logarithm, greatly enhancing low values
+
T(noise) = log(noise + ฮต)
+
+
+
Sin
+
Applies the sine function, creating oscillating patterns
+
T(noise) = sin(noise ร ฯ)
+
+
+
Cos
+
Applies the cosine function, creating alternating bands
+
T(noise) = cos(noise ร ฯ)
+
+
+
+
+
+
๐งช Transformation Alchemy
+
These transformations can dramatically alter the character of shader noise patterns, creating new structural properties:
+
+
Absolute transform creates ridge-like features along zero-crossings, perfect for geological formations
+
Square transform enhances high-intensity regions while subduing low-intensity areas, ideal for creating focused areas of interest
+
Sin/Cos transforms create banded patterns, excellent for stratified or layered structures
These transformations can be combined with different shader noise types and blend modes to create an almost infinite variety of structural guidance patterns.
+
+
+
+
+
+
+
๐ฌ Temporal Coherence โณ Evolving Noise for Animations
+
ShaderNoiseKSampler's temporal coherence feature ensures consistent noise patterns. For sequences like video frames, it helps maintain frame-to-frame consistency by evolving noise naturally over time. For single image generations, it ensures the base noise is derived consistently from the main seed, leading to predictable noise structures when parameters are tweaked.
+
+
+
โ๏ธ Enabling Evolving Animations
+
Temporal coherence fundamentally changes how shader noise is generated:
+
+
use_temporal_coherence (Boolean): This is the primary toggle. When enabled, ShaderNoiseKSampler ensures noise is generated consistently. For videos, it uses techniques (like the TemporalCoherentNoiseGenerator) that treat time as an evolving dimension. For images, it ensures the same base noise is used if the main seed is unchanged.
+
Consistent Base Seed: For temporal coherence to be effective (for both images and videos), the main seed input must remain constant. For videos, variation then comes from the noise evolving over an internal 'time' parameter. For images, this ensures that if other shader parameters are changed, the underlying base noise structure remains the same.
+
Consistent/Evolving Patterns: For videos, this mode generates patterns that transform coherently from one frame to the next, leading to more stable animations. For single images, it means the generated noise pattern will be the same for a given seed, even if other parameters (like shader strength or blend mode) are changed, allowing for more predictable exploration.
+
+
+
Conceptual animation preview placeholder.
+
+
+
+
+
+ Conceptual Temporal Noise (Python)
+
+
+
+import torch
+
+# Placeholder for a 3D simplex noise function
+def simplex_noise_3d_placeholder(coords_xyt, seed):
+ # coords_xyt is expected to be [B, H, W, 3] (x, y, time)
+ torch.manual_seed(seed)
+ noise = torch.sin(coords_xyt[..., 0] * 2.0 + coords_xyt[..., 2] * 1.5) * \
+ torch.cos(coords_xyt[..., 1] * 2.0 - coords_xyt[..., 2] * 1.0)
+ return noise.unsqueeze(-1) # Output [B, H, W, 1]
+
+def generate_animated_noise_frame(batch_size, height, width, scale, current_time, device, base_seed):
+ y_coords, x_coords = torch.meshgrid(
+ torch.linspace(-1, 1, height, device=device),
+ torch.linspace(-1, 1, width, device=device),
+ indexing='ij'
+ )
+ spatial_coords = torch.stack([x_coords, y_coords], dim=-1) * scale
+ time_coord_val = torch.full((height, width, 1), current_time, device=device)
+ p_temporal = torch.cat([spatial_coords, time_coord_val], dim=-1)
+ p_temporal_batch = p_temporal.unsqueeze(0).repeat(batch_size, 1, 1, 1)
+ frame_noise = simplex_noise_3d_placeholder(p_temporal_batch, base_seed)
+ return frame_noise.permute(0, 3, 1, 2)
+
+# --- Example Conceptual Usage for an Animation ---
+# total_frames = 100; animation_seed = 12345
+# for frame_idx in range(total_frames):
+# time_for_frame = frame_idx / total_frames
+# noise = generate_animated_noise_frame(1, 64, 64, 1.0, time_for_frame, 'cpu', animation_seed)
+ # noise would then be used by KSampler for this frame.
+
+
+
+
+
+
+
+
๐ญ Shape Masks ๐ Geometric Modulation
+
Shape masks provide spatial control by modulating shader noise patterns with procedurally generated geometric overlays. These masks, typically grayscale tensors (0.0 for no effect, 1.0 for full effect), guide the generative process, influencing compositions and textural details.
+
+
+
Mask Generation & Application
+
Shape masks are procedurally generated within the node as fixed grayscale patterns where values range from 0.0 (no effect area) to 1.0 (full effect area). The generated mask \( M \) is then used to modulate an input field (e.g., a noise or velocity field \( V \)) based on the shape_mask_strength \( \\alpha_s \). The application is a linear interpolation:
+
+ V_{masked} = V \\times (1 - \\alpha_s) + (V \\times M) \\times \\alpha_s
+
+
This means if \( \\alpha_s = 1 \), the field is fully multiplied by the mask pattern (\( V \\times M \)). If \( \\alpha_s = 0 \), the mask has no effect. Intermediate values blend between the original and the mask-modulated field. Key characteristics include:
+
+
Mask Type: Defines the base geometry from a selection including Radial, Linear, and various Geometric or Procedural patterns.
+
Mask Strength: The \( \\alpha_s \) parameter, controlling the blend intensity of the mask\'s effect.
+
Edge Handling: Mask types inherently produce different edge styles, some with soft, feathered falloffs (often using smoothstep internally) and others with hard, thresholded edges.
+
+
Note that while masks might appear animated in visualization previews, in the backend implementation they are static patterns applied at generation time. Any animation effects in the final output would come from changing parameters between frames.
import torch
+
+def apply_shape_mask_illustrative(input_field, coords_bhwc, mask_type="radial", shape_mask_strength=0.7):
+ '''Illustrative: Generate and apply a shape mask to an input field.
+ Args:
+ input_field (torch.Tensor): Field to be masked [B, H, W, C].
+ coords_bhwc (torch.Tensor): Coordinates for mask generation (0-1 range) [B, H, W, 2].
+ mask_type (str): Type of mask to generate.
+ shape_mask_strength (float): Strength of the mask application.
+ Returns:
+ torch.Tensor: Masked input_field.
+ '''
+ batch_size, height, width, _ = coords_bhwc.shape
+ device = coords_bhwc.device
+
+ # --- Mask Generation (Simplified Radial Example) ---
+ # Initialize shape_mask with ones (no effect by default if mask_type is unrecognized)
+ shape_mask = torch.ones_like(coords_bhwc[..., 0])
+
+ if mask_type == "radial":
+ center_x, center_y = 0.5, 0.5
+
+ # Calculate distance from the center
+ y_diff = coords_bhwc[..., 1] - center_y
+ x_diff = coords_bhwc[..., 0] - center_x
+ # Normalized distance, similar to GLSL masks (stretching to fill 0-1 range)
+ dist = torch.sqrt(x_diff**2 + y_diff**2) * 2.0
+ current_mask_values = torch.clamp(1.0 - dist, 0.0, 1.0)
+ shape_mask = current_mask_values
+ # ... (other mask_type implementations would go here)
+
+ # Ensure mask is [B, H, W, 1] for broadcasting with input_field [B, H, W, C]
+ shape_mask = shape_mask.unsqueeze(-1)
+
+ # --- Mask Application ---
+ # Modulate the input field with the generated mask pattern
+ field_modulated_by_mask_pattern = input_field * shape_mask
+
+ # Apply shape_mask_strength via linear interpolation (lerp)
+ # output_field = base_value + (target_value - base_value) * weight
+ # output_field = input_field + (field_modulated_by_mask_pattern - input_field) * shape_mask_strength
+ # This is equivalent to PyTorch's torch.lerp:
+ output_field = torch.lerp(input_field, field_modulated_by_mask_pattern, shape_mask_strength)
+
+ return output_field
+
+# --- Example Conceptual Usage ---
+# B, H, W, C_noise = 1, 64, 64, 2 # Example: 2-channel velocity field
+# example_field = torch.rand(B, H, W, C_noise, device='cpu') # Random field
+
+# # Create normalized coordinates (0 to 1 range)
+# y_coords_tensor = torch.linspace(0, 1, H, device='cpu').view(1, H, 1, 1).expand(B, H, W, 1)
+# x_coords_tensor = torch.linspace(0, 1, W, device='cpu').view(1, 1, W, 1).expand(B, H, W, 1)
+# # Coordinates are expected as [x, y] in the last dimension for mask generation
+# coordinates_tensor = torch.cat((x_coords_tensor, y_coords_tensor), dim=-1)
+
+# masked_field = apply_shape_mask_illustrative(example_field, coordinates_tensor, mask_type="radial", shape_mask_strength=0.8)
+# print(f"Masked field tensor shape: {masked_field.shape}")
+
+
+
+
+
+
๐จ Creative Applications
+
The interplay between different mask types, the noise they modulate, and various parameter settings (like shape_mask_strength, noise_scale, etc.) allows for a wide range of artistic effects. Consider these combinations:
+
+
Vignettes & Focus: Soft Radial or Vignette masks to draw attention centrally.
+
Structured Organics: Grid Masks combined with Cellular Noise for bio-mechanical effects.
+
Atmospheric Depth: Linear Gradient Masks to simulate fog or distance with flowing noise.
+
Selective Detailing: Geometric Masks to apply high-frequency noise to specific regions.
+
+
Experiment with mask types and their strengths in conjunction with noise parameters to guide the generative process.
+
+
+
+
+
+
+
๐ Color Schemes โ๏ธ Chromatic Adjustments
+
ShaderNoiseKSampler allows for the application of color transformations directly to the generated shader noise patterns. It's important to understand that this is not a traditional post-processing color adjustment. Instead, the color information is integrated into the shader noise before it influences the diffusion model during the sampling process. Consequently, these 'colors' can function as an integral part of the shader noise's texture and structure, subtly guiding the model's interpretation and shaping the features, patterns, and overall aesthetic of the final image. This technique offers a unique way to enhance visual impact and steer the generative outcome.
+
+
+
Color Transformation Process
+
The color mapping function transforms a normalized noise value t โ [0,1] into RGB color space through gradient functions. Each color scheme defines specific transfer functions for each channel, creating distinctive visual characteristics that can alter how the diffusion model interprets the noise structure.
+
+
+ C(t) = (R(t), G(t), B(t))
+
+
+
Key characteristics of the coloring process include:
+
+
Color Scheme Selection: Different schemes provide various perceptual encodings of the noise data.
+
Color Intensity: Controls how strongly the coloring affects the final noise blend, typically between 0.0 (no effect) and 1.0 (full effect).
+
Channel Blending: The colored information is blended into the noise channels before they influence the diffusion process.
+
+
+
+
+
+
+ The choice of color scheme significantly impacts how shader noise patterns guide the generative process. Different schemes create various perceptual encodings that can emphasize different aspects of the shader noise structure, potentially influencing the final image's mood, texture, and compositional elements.
+
+
+
Spectral Schemes - Emphasizing Transitions
+
+
+ Inferno
+
+
+ Magma
+
+
+ Plasma
+
+
+ Viridis
+
+
+
+
Technical Schemes - Enhancing Perception
+
+
+ Turbo
+
+
+ Jet
+
+
+ Parula
+
+
+ Rainbow
+
+
+ Hot
+
+
+
+
+
+
+
Applying Color Schemes
+
ShaderNoiseKSampler allows for the selection of various color schemes that are applied to the generated noise. The intensity of this coloration can also be controlled, determining how strongly the chosen color scheme influences the noise that guides the diffusion model.
+
+
+
Color Scheme Effects on Generation
+
+
Contrast Enhancement: Color schemes like Inferno and Jet can increase the perceived contrast in the noise pattern, potentially leading to more defined boundaries in the generated image.
+
Perceptual Organization: Schemes like Viridis and Turbo help to organize the noise data perceptually, which can influence how the model interprets spatial relationships.
+
Mood and Tone: The predominant hues of a color scheme can subtly influence the mood or tonal quality of the generated image, even when the colors themselves aren't directly visible.
+
Detail Emphasis: Some schemes are better at emphasizing small details within the noise pattern, which can lead to enhanced textural complexity in the final output.
+
+
+
+
+
+ Color Scheme Implementation
+
+
+
+# How color schemes are applied in the backend (simplified from curl_noise.py)
+def apply_color_scheme(normalized_value, color_scheme, device):
+ """
+ Apply a color scheme to normalized noise values (0-1 range).
+
+ Args:
+ normalized_value: Tensor of shape [batch, 1, height, width] with values in [0,1]
+ color_scheme: String identifier for the chosen color scheme
+ device: Computation device (cuda or cpu)
+
+ Returns:
+ Tuple of (r, g, b) channel tensors
+ """
+ # Helper function for lerping colors
+ def lerp(a, b, t):
+ return a + (b - a) * t
+
+ # Helper for color stops interpolation
+ def interpolate_colors(stops, t):
+ # stops: list of [value, color_tensor]
+ # Find which segment t falls into and interpolate
+ idx = torch.zeros_like(t, dtype=torch.long)
+ for i in range(len(stops) - 1):
+ idx = torch.where((t >= stops[i][0]) & (t < stops[i+1][0]),
+ torch.full_like(idx, i), idx)
+
+ # Handle edge case for t >= last stop
+ idx = torch.where(t >= stops[-1][0],
+ torch.full_like(idx, len(stops) - 2), idx)
+
+ # Initialize output tensor
+ final_color = torch.zeros_like(stops[0][1].expand(-1, -1, t.shape[2], t.shape[3]))
+
+ # Apply interpolation for each segment
+ for i in range(len(stops) - 1):
+ mask = (idx == i)
+ t0, c0 = stops[i]
+ t1, c1 = stops[i+1]
+
+ local_t = torch.clamp((t - t0) / (t1 - t0 + 1e-8), 0.0, 1.0)
+ segment_color = lerp(c0, c1, local_t)
+ final_color = torch.where(mask.expand_as(segment_color),
+ segment_color, final_color)
+
+ return final_color[:, 0:1], final_color[:, 1:2], final_color[:, 2:3]
+
+ # Different color schemes using the normalized value tensor
+ if color_scheme == "viridis":
+ stops = [
+ (0.0, torch.tensor([0.267, 0.005, 0.329], device=device).view(1, 3, 1, 1)), # #440154
+ (0.33, torch.tensor([0.188, 0.407, 0.553], device=device).view(1, 3, 1, 1)), # #30678D
+ (0.66, torch.tensor([0.208, 0.718, 0.471], device=device).view(1, 3, 1, 1)), # #35B778
+ (1.0, torch.tensor([0.992, 0.906, 0.143], device=device).view(1, 3, 1, 1)) # #FDE724
+ ]
+ r, g, b = interpolate_colors(stops, normalized_value)
+
+ elif color_scheme == "magma":
+ stops = [
+ (0.0, torch.tensor([0.001, 0.001, 0.016], device=device).view(1, 3, 1, 1)), # #000004
+ (0.25, torch.tensor([0.231, 0.059, 0.439], device=device).view(1, 3, 1, 1)), # #3B0F70
+ (0.5, torch.tensor([0.549, 0.161, 0.506], device=device).view(1, 3, 1, 1)), # #8C2981
+ (0.75, torch.tensor([0.871, 0.288, 0.408], device=device).view(1, 3, 1, 1)), # #DE4968
+ (0.85, torch.tensor([0.996, 0.624, 0.427], device=device).view(1, 3, 1, 1)), # #FE9F6D
+ (1.0, torch.tensor([0.988, 0.992, 0.749], device=device).view(1, 3, 1, 1)) # #FCFDBF
+ ]
+ r, g, b = interpolate_colors(stops, normalized_value)
+
+ elif color_scheme == "rainbow":
+ # Use HSV color space, with hue from the normalized value
+ h = normalized_value # Hue from normalized value [0,1]
+ s = torch.ones_like(h) * 0.8 # Fixed saturation
+ v = torch.ones_like(h) * 0.9 # Fixed value/brightness
+ r, g, b = hsv_to_rgb(h, s, v) # Convert to RGB
+
+ # ... other color schemes ...
+
+ else:
+ # Default grayscale if color scheme unknown
+ r = g = b = normalized_value
+
+ return r, g, b
+
+# Usage example in ShaderNoiseKSampler
+def apply_color_to_noise(noise_tensor, shader_params):
+ """Apply color scheme to noise tensor before blending with base noise"""
+ color_scheme = shader_params.get("color_scheme", "none")
+ color_intensity = shader_params.get("color_intensity", 0.8)
+
+ if color_scheme == "none" or color_intensity <= 0.0:
+ return noise_tensor # Skip coloring if disabled
+
+ # Normalize a channel (usually magnitude) for color mapping
+ channel_for_color = noise_tensor[:, 0:1] # Use first channel
+ normalized = torch.clamp((channel_for_color + 1.0) / 2.0, 0.0, 1.0) # Map [-1,1] to [0,1]
+
+ # Apply the color scheme
+ r, g, b = apply_color_scheme(normalized, color_scheme, noise_tensor.device)
+
+ # Create colored noise (first 3 channels get RGB, 4th unchanged or derived from magnitude)
+ colored_tensor = torch.cat([r, g, b, noise_tensor[:, 3:4]], dim=1)
+
+ # Blend with original noise based on intensity
+ if color_intensity < 1.0:
+ # Linear interpolation between original and colored noise
+ result = lerp(noise_tensor, colored_tensor, color_intensity)
+ return result
+ else:
+ return colored_tensor
+
+
+
+
+
+
+
+
+
๐ ๏ธ Usage ๐ง Working with this Tool
+
ShaderNoiseKSampler integrates advanced noise generation directly into the ComfyUI sampling process. It replaces the standard KSampler's noise generation with a sophisticated system that allows for multi-stage shader application, diverse noise types, transformations, and blending, offering fine-grained control over the creative output.
+
+
+
Core Workflow Integration
+
+
Connect Inputs:
+
+
model: The primary AI model for generation.
+
positive & negative: Conditioning prompts.
+
latent_image: The input latent to be processed.
+
+
+
Basic Sampling Parameters:
+
+
seed: For reproducibility use a fixed seed value for consistent results.
+
steps: Number of sampling iterations.
+
cfg: Classifier-Free Guidance scale.
+
sampler_name & scheduler: Standard KSampler choices.
+
denoise: Controls the extent of denoising.
+
+
+
Shader Noise Configuration: This is where ShaderNoiseKSampler shines:
+
+
sequential_stages: Number of shader stages applied sequentially over defined step ranges.
+
injection_stages: Number of shader stages applied at specific injection points during sampling.
+
shader_strength: Global strength of the shader noise influence. Set to 0.0 to use only base noise.
+
blend_mode: How shader noise combines with base noise (e.g., multiply, add, overlay).
+
noise_transform: Mathematical operations applied to the generated noise (e.g., absolute, sin, sqrt).
+
use_temporal_coherence: For generating frame-consistent noise, useful in animations.
+
+
+
(Optional) Advanced Control:
+
+
custom_sigmas: Provide a custom sigma schedule to override the model's default.
+
+
+
Run Workflow: Execute the ComfyUI graph. The ShaderNoiseKSampler will dynamically generate and blend noise according to your settings throughout the sampling process.
+
+
+
+
+
โ๏ธ Key Parameter Explanations
+
+
+
Sequential vs. Injection Stages
+
sequential_stages apply shader noise over segments of the diffusion process. The total steps are divided among these stages.
+ injection_stages apply shader noise at specific, discrete steps.
+ Sequential stages don't seem to add time to the generation but injection stages will add some time to the generation.
+
+
+
Shader Strength & Distributions
+
shader_strength is a global multiplier. Higher shader strength can lead to increased artifacting or generational oddities, but not always and not with all models or shader noise types. Small variations in shader strength is a good way to maintain the sum concepts of an image and not heavily vary its degree of difference.
+
+
+
Noise Transformation
+
The noise_transform (e.g., absolute, sin, sqrt) is a global setting applied to the generated shader noise within each stage before it's blended with the base noise. The chosen transformation is used consistently across all stages.
+
+
+
Temporal Coherence
+
When use_temporal_coherence is enabled, the node aims to generate consistent noise. For videos, this means noise evolves smoothly over a "time" dimension (often using 3D/4D noise with one dimension as time), with the base seed consistent across frames. For images, it ensures the noise pattern is consistently derived from the main seed, even with other parameter changes.
+
+
+
Custom Sigmas
+
custom_sigmas allows advanced users to define their own noise schedule (sigmas), giving more control over how noise is added and removed at each step. This can significantly alter the sampling dynamics.
+
+
+
Blend Mode
+
The blend_mode (e.g., multiply, add, overlay) is a global setting that determines how the shader noise combines with the base noise. This selected blend mode is used for all stages.
+
+
+
+
+
+
๐ง Creative Techniques & Advanced Usage
+
+
Shader Visualizer as a Guide: Using the shader visualizer in connection to the generation process is crucial for gaining insights into your parameter adjustments. The shader display was designed as a visual aid, not just for presentation. It helps you understand the shader noise\'s impact on the output image and serves as a navigational tool for latent space, mirroring backend shader parameters.
+
Detail through Noise Scale & Octaves: Higher noise scale and octaves can contribute to more detailed images. Essentially, a more "noisy" input increases the likelihood of observing finer-grained detail in the output.
+
Parameter Interdependence: Parameters are not always independent. If phase shift adjustments seem insufficient, try incorporating a bit of warp strength. Conversely, if your noise scale and octaves are already high, parameters like warp strength and phase shift might appear less impactful.
+
Color Intensity with Shape Masks: Color intensity is only effective when a shape mask is active. Once active, it can be used to introduce subtle to significant variations in your image, depending on the input values.
+
Shape Mask Strength: Similar to color intensity, shape mask strength is only usable when a shape mask is active. When enabled, it allows for small to large image variations based on your input values.
+
Warp & Phase with Active Masks/Colors: You might notice that when a color scheme or a shape mask (especially with higher strength/intensity) is active, the perceptual impact of warp_strength and phase_shift can seem diminished. This is likely because the structural influence of the mask or the channel modifications from coloring are significantly shaping the noise, potentially overshadowing the more subtle distortions or shifts from warp and phase. Experiment with moderating mask/color influence if you need to bring out the warp/phase effects more clearly.
+
Quick Ideation: For rapid idea generation without the need to constantly save parameters, consider using the (direct) version of this node. As is rapid parameter changes cannot be made to queue in succession, due to the way the node is currently implemented.
+
Saving Parameters: The "Save Shader Parameters" button (found on the ShaderNoiseKSampler node) allows you to save your current shader settings. These are saved to custom_nodes/ComfyUI-ShaderNoiseKsampler/data/shader_params.json. Note: this file must be named exactly shader_params.json in this specific directory and will be overwritten on each save. This is crucial for features that load these parameters. Use the (direct) version of this node to avoid saving parameters.
+
Shader Noise Palettes & Performance: Each Shader Noise Type (or palette) offers a distinct lens through which to explore the latent space, revealing different facets and possibilities within a seed. Most shader noise types process at roughly comparable speeds, typically adding only about 3-4 seconds to the generation time compared to a standard KSampler. While some may occasionally take a moment longer to initialize or load, the overall impact on generation time is generally minimal.
+
+
+
+
+
+
+
+
๐บ๏ธ Navigating Latent Space ๐งญ Shader Controls for Exploration
+
ShaderNoiseKSampler offers a way to explore latent space with more deliberate control compared to relying solely on random seeds. Shader parameters act as navigational tools, allowing users to chart the 'territory' within a given seed. This approach can lead to a more nuanced manipulation of the generative process, turning image generation into a more methodical exploration.
+
+
+
๐น๏ธ Parameter Deep Dive: Your Exploration Toolkit
+
Understanding how each core shader parameter influences the noise provides a map for your creative journey:
+
+
+
Noise Scale: The Zoom Control
+
Think of Noise Scale as the 'zoom lens' for your exploration within the latent space.
+
+
Low scale values: You're examining fine details and subtle variations within a small, localized region of the seed's potential. Ideal for nuanced adjustments.
+
High scale values: You're taking broader steps, observing more sweeping changes across a wider expanse. This can lead to more dramatic shifts in the output.
+
+
Effect on Generation: Controls how 'localized' or 'expansive' your exploration is. Smaller scales keep you closer to the seed's core characteristics, while larger scales venture further afield.
+
+
+
Octaves: The Detail Slider
+
Octaves determine the level of detail and complexity embedded within the noise pattern, and consequently, in the generated variations.
+
+
Lower octaves: Produce simpler, smoother noise, leading to more foundational or gentle variations of the theme.
+
Higher octaves: Introduce more intricate, layered patterns with multiple frequencies of detail, revealing complex elaborations and richer textures from the seed's inherent elements.
+
+
Effect on Generation: As you increase octaves, you're effectively adding finer and finer layers of detail, uncovering more complex permutations of the same underlying concepts.
+
+
+
Warp Strength: The Non-Linear Navigator
+
Warp Strength introduces non-linear distortions to the noise pattern, carving unconventional and often surprising paths through the latent space.
+
+
Low warp: Results in more predictable, somewhat linear modifications to the noise structure.
+
High warp: Creates significant distortions, 'folding,' and 'twisting' in the noise pattern. This allows you to unearth unusual or 'hidden' variations that might reside between the more straightforward pathways.
+
+
Effect on Generation: Enables the discovery of unique aesthetics by navigating the latent space in a less constrained, more fluid manner.
+
+
+
Phase Shift: The Perspective Shifter
+
Phase Shift functions like adjusting your viewpoint or the 'timing' of the noise pattern, subtly altering its manifestation in latent space.
+
+
Different phase values: Shift the entire noise pattern while generally maintaining its internal structural characteristics. It's akin to viewing the same intricate object from slightly different angles or under slightly different lighting conditions.
+
+
Effect on Generation: Reveals different 'facets' or 'expressions' of the same core elements within the seed's potential, offering a way to cycle through related variations smoothly.
+
+
+
Shape Mask Strength: The Selective Amplifier
+
Shape Mask Strength controls the intensity of geometric masks that modulate the noise pattern, allowing for spatial control.
+
+
Note: This parameter only has effect when a mask type is actively selected.
+
Small value changes: Even minor adjustments (0.01-0.05) can produce subtle yet noticeable variations in the final image, as they alter how the mask influences the noise structure.
+
+
Effect on Generation: Acts as a fine-tuning dial for how strongly the selected geometric pattern guides the noise formation, enabling controlled exploration of variations with spatial emphasis.
+
+
+
Color Intensity: The Chromatic Influence
+
Color Intensity determines how strongly the selected color scheme affects the noise pattern's structure and influence.
+
+
Note: Similar to Shape Mask Strength, this parameter only takes effect when a color scheme is enabled.
+
Small adjustments: Like shape mask strength, minor tweaks can subtly alter the generated outcome by changing how color information is integrated into the noise structure.
+
+
Effect on Generation: Provides nuanced control over how color information within the noise influences the diffusion process, allowing for exploration of variations with different color-driven structural emphasis.
+
+
+
+
+
+
+
+
๐ Potential of Guided Exploration
+
This methodical approach to navigating latent space using shader parameters can offer interesting possibilities:
+
+
Latent Space Cartography: You're not just generating images; you're effectively creating a map of the conceptual territory surrounding your chosen seed. Different parameter combinations become 'landmarks,' helping you develop an intuition for navigating towards specific aesthetic effects or thematic elements.
+
Persistent Identities in Variation: The remarkable tendency for similar elements (characters, objects, styles) to persist across various parameter adjustments suggests that certain semantic features are robustly encoded within the latent space neighborhood of a seed. This offers glimpses into how the model 'conceptualizes' and relates visual information.
+
Discovering "Hidden Gems": Traditional random seed exploration, by its nature, can easily miss nuanced or specific variations that exist in the 'gaps' between broadly different seeds. Deliberate navigation with shader parameters allows for the systematic discovery of these unique possibilities that random sampling might statistically overlook.
+
Creative Control Meets Serendipity: This methodology provides a powerful balance between intentional artistic direction and the joy of unexpected discovery. You're not rigidly dictating the output, nor are you entirely at the mercy of randomness; instead, you're skillfully steering your exploration through a rich space of possibilities.
+
Frequency Domain Exploration (Implicit): Given the mathematical underpinnings of shader noise (like Perlin, Fractal, etc.), you are, in essence, exploring how different frequency characteristics and patterns within the latent space map to tangible semantic features and visual styles in the generated images. This can reveal fundamental patterns in how the model encodes and interprets visual information.
+
+
+ "The true power lies in shifting from 'random sampling' to 'deliberate exploration'โ empowering artists to develop an intuition for navigating towards desired variations, rather than merely hoping to stumble upon them."
+
+
+
+
+
+
+
+
๐ Conclusion โฏ The Path of Creation
+
ShaderNoiseKSampler is a bridge between the mathematical sublime and practical artistry. By understanding and harnessing the sacred geometry of shader noise, artists can guide the generative process with precision and intentionality, transforming pure mathematical concepts into tools of creative expression.
+
It is important to note that this project represents a concept in its infancyโa minimal viable product of ongoing research. Much remains to be explored and understood in this fascinating intersection of mathematical noise patterns and generative art. The techniques and approaches presented here are foundational stepping stones in a journey that has only just begun.
+
This work constitutes original, independent investigation and implementationโa product of genuine curiosity and hands-on experimentation rather than an adaptation of existing research. The path forward relies on community support, both in collaborative exploration and financial backing, to fuel further innovation. By supporting grassroots research like this, you contribute directly to the advancement of creative technology driven by unfiltered exploration and authentic discovery.
+
May your journey with ShaderNoiseKSampler be one of discovery, inspiration, and creationโof digital tapestries woven from the very fabric of mathematical reality.
+
+ "In every shader noise pattern lies a universe of possibility
+ In every parameter, a doorway to creation"
+ - The Shader Matrix
+
+
+
+
+
+
+
+
+
+
+ `; // This line should end the template literal correctly
+ content.innerHTML = treatiseHTML;
+
+ // Attach listener to the close button *inside* the treatiseHTML
+ const closeButtonInTreatise = content.querySelector('.close-button');
+ if (closeButtonInTreatise) {
+ closeButtonInTreatise.onclick = (e) => {
+ e.stopPropagation();
+ closeModalCleanup();
+ };
+ }
+
+ modal.appendChild(content);
+ document.body.appendChild(modal);
+
+ // Call the renderer for noise visualizations
+ if (window.NoiseVisualizer && window.NoiseVisualizer.renderAllInModal) {
+ // Defer to ensure layout is complete and modal content is fully rendered
+ setTimeout(() => {
+ window.NoiseVisualizer.renderAllInModal(content); // 'content' is the div with class 'shader-matrix-treatise'
+ }, 0);
+ } else {
+ console.warn("NoiseVisualizer not found. Ensure noise_visualizer.js is loaded and available on the window object.");
+ }
+
+ // Initial scroll to top of modal content
+ content.scrollTop = 0;
+
+ // Activate the scroll-to-top button functionality
+ window.setupScrollTop(content); // Pass the 'content' element
+
+ });
+
+ // Add tooltip to the button
+ button.tooltip = "Show Shader Matrix & Documentation (Alt+M)";
+
+ // Position the button appropriately based on node type (from matrix_button - Copy.js)
+ if (nodeData.name === "ShaderNoiseKsampler") {
+ // For KSampler node, add to a specific section or position
+ if (!button.options) {
+ button.options = {};
+ }
+ button.options.section = "advanced";
+
+ // Custom button style for the KSampler node
+ button.label = "๐ Show Shader Matrix";
+ } else {
+ // Default styling for ShaderDisplay (label is already "Show Matrix")
+ // button.label = "Show Matrix"; // No change needed if initialized with "Show Matrix"
+ }
+
+ // Set button appearance (from matrix_button - Copy.js)
+ button.name = "๐ Show Shader Matrix";
+ button.serialize = false; // Don't include in serialization
+
+ // For ShaderDisplay, the button will use default positioning.
+ // For ShaderNoiseKsampler, without a section, it should append after other widgets.
+
+ };
+
+ // Call addMatrixButton conditionally (from matrix_button - Copy.js)
+ if (self.constructor.type_name === "ShaderNoiseKsampler") {
+ addMatrixButton();
+ } else {
+ // For other nodes, add after a small delay to ensure all widgets are ready
+ setTimeout(addMatrixButton, 50);
+ }
+
+ // --- Keybinding Logic for Matrix Button ---
+ const triggerMatrixButton = () => {
+ const matrixButtonWidget = self.widgets.find(w => w.name === "๐ Show Shader Matrix" && w.type === "button");
+ if (matrixButtonWidget && typeof matrixButtonWidget.callback === 'function') {
+ matrixButtonWidget.callback.call(matrixButtonWidget.value, app.canvas, self, null, null);
+ } else {
+ console.warn("Matrix button widget not found or callback is not a function for Alt+M.");
+ }
+ };
+
+ const handleMatrixKeyDown = (event) => {
+ if (event.altKey && event.key.toLowerCase() === 'm') {
+ if (app.canvas && (app.canvas.current_node === self || (app.canvas.selected_nodes && app.canvas.selected_nodes[self.id]))) {
+ if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA' || document.activeElement.isContentEditable)) {
+ return; // Don't interfere with text input
+ }
+ console.log("Alt+M detected for current/selected node to show matrix.");
+ event.preventDefault();
+ event.stopPropagation();
+ triggerMatrixButton();
+ }
+ }
+ };
+
+ document.addEventListener('keydown', handleMatrixKeyDown);
+ self.handleMatrixButtonKeyDown = handleMatrixKeyDown; // Store for removal
+
+ const originalOnRemoved = self.onRemoved;
+ self.onRemoved = function() {
+ if (self.handleMatrixButtonKeyDown) {
+ document.removeEventListener('keydown', self.handleMatrixButtonKeyDown);
+ delete self.handleMatrixButtonKeyDown;
+ console.log("Removed Alt+M keydown listener for matrix button on node:", self.id);
+ }
+ if (originalOnRemoved) {
+ originalOnRemoved.apply(self, arguments);
+ }
+ };
+ // --- End Keybinding Logic ---
+ }
+ }
+ });
+})();
\ No newline at end of file
diff --git a/web/noise_visualizer.js b/web/noise_visualizer.js
new file mode 100644
index 0000000..c23c31f
--- /dev/null
+++ b/web/noise_visualizer.js
@@ -0,0 +1,1608 @@
+// web/noise_visualizer.js
+if (!window.NoiseVisualizer) {
+ window.NoiseVisualizer = {
+ kofiCupImageBitmap: null, // To store the preloaded ImageBitmap object
+ kofiImageLoaded: false,
+ kofiImageLoadAttempted: false,
+
+ _preloadKofiImage: async function() { // Made async
+ if (this.kofiImageLoadAttempted) return;
+ this.kofiImageLoadAttempted = true;
+
+ const absolutePath = '/extensions/ComfyUI-ShaderNoiseKsampler/images/kofi_symbol.svg';
+
+ // Attempt 1: Direct Image load
+ try {
+ console.log("Attempting Ko-fi SVG load using new Image() with direct path (Primary Attempt)...");
+ const img = new Image();
+ await new Promise((resolve, reject) => {
+ img.onload = () => {
+ this.kofiCupImageBitmap = img; // Store the HTMLImageElement
+ this.kofiImageLoaded = true;
+ console.log("Ko-fi symbol SVG loaded via Image() successfully (Primary).");
+ resolve();
+ };
+ img.onerror = (e) => {
+ console.warn("Primary Ko-fi symbol SVG load via Image() failed. Proceeding to secondary attempt.", e);
+ reject(e); // Reject to proceed to the next try block
+ };
+ img.src = absolutePath;
+ });
+ return; // Successfully loaded, no need to proceed
+ } catch (error) {
+ // Error logged by the promise, clear flags before next attempt
+ this.kofiImageLoaded = false;
+ this.kofiCupImageBitmap = null;
+ }
+
+ // Attempt 2: Fetch -> Blob -> Intermediate Image -> ImageBitmap
+ let objectURL = null;
+ try {
+ console.log("Attempting Ko-fi SVG load via Blob -> Image -> ImageBitmap (Secondary Attempt)...");
+ const response = await fetch(absolutePath);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status} for ${absolutePath}`);
+ }
+ const svgText = await response.text();
+ const blob = new Blob([svgText], { type: 'image/svg+xml' });
+ objectURL = URL.createObjectURL(blob);
+
+ const intermediateImg = new Image();
+ await new Promise((resolve, reject) => {
+ intermediateImg.onload = resolve;
+ intermediateImg.onerror = (e) => reject(new Error("Intermediate Image() load failed for SVG blob (Secondary)."));
+ intermediateImg.src = objectURL;
+ });
+
+ this.kofiCupImageBitmap = await createImageBitmap(intermediateImg);
+ this.kofiImageLoaded = true;
+ console.log("Ko-fi symbol SVG processed to ImageBitmap successfully (Secondary).");
+
+ } catch (error) {
+ this.kofiImageLoaded = false;
+ this.kofiCupImageBitmap = null;
+ console.error("All Ko-fi SVG load attempts failed: ", error, ". Will use fallback drawing.");
+ } finally {
+ if (objectURL) {
+ URL.revokeObjectURL(objectURL); // Clean up the object URL
+ }
+ }
+ // If all attempts fail, kofiImageLoaded remains false, and manual drawing will be used.
+ },
+
+ renderAllInModal: async function(modalContentElement) { // Made async
+ if (!this.kofiImageLoadAttempted) {
+ await this._preloadKofiImage(); // Ensure preloading is complete
+ }
+
+ const noiseCanvases = modalContentElement.querySelectorAll('.noise-canvas');
+ noiseCanvases.forEach(canvasDiv => {
+ const canvasId = canvasDiv.id;
+ if (!canvasId || !canvasId.startsWith("noise-canvas-")) return;
+ const noiseType = canvasId.substring("noise-canvas-".length);
+
+ let canvas = canvasDiv.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ canvas.width = 130;
+ canvas.height = 130;
+ canvasDiv.innerHTML = '';
+ canvasDiv.appendChild(canvas);
+ }
+
+ const functionNameSuffix = noiseType.split('_').map(word => {
+ if (word.toLowerCase() === 'fbm') return 'FBM';
+ return word.charAt(0).toUpperCase() + word.slice(1);
+ }).join('');
+ const renderFunctionName = `render${functionNameSuffix.replace("3d", "3D")}`;
+
+ if (typeof this[renderFunctionName] === 'function') {
+ this[renderFunctionName](canvas);
+ } else {
+ console.warn("No renderer function found:", renderFunctionName, "for noise type:", noiseType);
+ this.renderPlaceholder(canvas, noiseType.replace(/_/g, ' '));
+ }
+ });
+
+ // Add rendering for mask canvases
+ const maskCanvases = modalContentElement.querySelectorAll('.mask-canvas');
+ maskCanvases.forEach(canvasDiv => {
+ const canvasId = canvasDiv.id;
+ if (!canvasId || !canvasId.startsWith("mask-canvas-")) return;
+ const maskType = canvasId.substring("mask-canvas-".length);
+
+ let canvas = canvasDiv.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ canvas.width = 100;
+ canvas.height = 70;
+ canvasDiv.innerHTML = '';
+ canvasDiv.appendChild(canvas);
+ }
+
+ const functionNameSuffix = maskType.split('_').map(word => {
+ return word.charAt(0).toUpperCase() + word.slice(1);
+ }).join('');
+ const renderFunctionName = `renderMask${functionNameSuffix}`;
+
+ if (typeof this[renderFunctionName] === 'function') {
+ this[renderFunctionName](canvas);
+ } else {
+ console.warn("No renderer function found:", renderFunctionName, "for mask type:", maskType);
+ this.renderMaskPlaceholder(canvas, maskType.replace(/_/g, ' '));
+ }
+ });
+
+ // Add rendering for color scheme swatches
+ const colorSwatches = modalContentElement.querySelectorAll('.color-swatch');
+ colorSwatches.forEach(swatchDiv => {
+ // Skip if it already has a gradient background
+ if (swatchDiv.style.background) return;
+
+ // Extract color scheme name from the div's text content
+ const schemeName = swatchDiv.textContent.trim().toLowerCase();
+ const renderFunctionName = `renderColor${schemeName.charAt(0).toUpperCase() + schemeName.slice(1)}`;
+
+ if (typeof this[renderFunctionName] === 'function') {
+ this[renderFunctionName](swatchDiv);
+ } else {
+ // For schemes without specific renderers, use the style already in the HTML
+ console.log("Using existing style for color scheme:", schemeName);
+ }
+ });
+
+ // Add rendering for the temporal animation demo
+ const animationDemoContainer = modalContentElement.querySelector('#animation-demo-placeholder');
+ if (animationDemoContainer) {
+ let canvas = animationDemoContainer.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ // Set canvas dimensions based on its container, respecting aspect ratio if possible
+ // For simplicity, let's use fixed dimensions or dimensions derived from container style
+ const containerStyle = getComputedStyle(animationDemoContainer);
+ canvas.width = parseInt(containerStyle.width) || 300; // Default if not specified
+ canvas.height = parseInt(containerStyle.height) || 200; // Default if not specified
+ animationDemoContainer.innerHTML = ''; // Clear placeholder text/content
+ animationDemoContainer.appendChild(canvas);
+ animationDemoContainer.style.display = 'block'; // Ensure container is block for canvas
+ }
+ if (typeof this.renderTemporalAnimation === 'function') {
+ this.renderTemporalAnimation(canvas);
+ } else {
+ console.warn("renderTemporalAnimation function not found in NoiseVisualizer.");
+ this.renderMaskPlaceholder(canvas, "Animation Demo"); // Fallback placeholder
+ }
+ }
+
+ // Add rendering for the intro noise demo
+ const introNoiseDemoContainer = modalContentElement.querySelector('#intro-noise-demo');
+ if (introNoiseDemoContainer) {
+ let canvas = introNoiseDemoContainer.querySelector('canvas');
+ if (!canvas) {
+ canvas = document.createElement('canvas');
+ const containerStyle = getComputedStyle(introNoiseDemoContainer);
+ canvas.width = parseInt(containerStyle.width) || 300;
+ canvas.height = parseInt(containerStyle.height) || 350; // Match defined height in CSS
+ introNoiseDemoContainer.innerHTML = ''; // Clear placeholder
+ introNoiseDemoContainer.appendChild(canvas);
+ introNoiseDemoContainer.style.display = 'block';
+ }
+ if (typeof this.renderIntroNoiseDemo === 'function') {
+ this.renderIntroNoiseDemo(canvas);
+ } else {
+ console.warn("renderIntroNoiseDemo function not found in NoiseVisualizer.");
+ this.renderMaskPlaceholder(canvas, "Intro Noise Demo");
+ }
+ }
+ },
+
+ _clearCanvas: function(canvas, backgroundColor = '#1a1a2e') {
+ const ctx = canvas.getContext('2d');
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ return ctx;
+ },
+
+ _drawKofiIcon: function(ctx) {
+ const iconSize = 18;
+ const padding = 3;
+ const x = ctx.canvas.width - iconSize - padding;
+ const y = ctx.canvas.height - iconSize - padding;
+
+ // Now checks for kofiCupImageBitmap
+ if (this.kofiImageLoaded && this.kofiCupImageBitmap) {
+ try {
+ // drawImage can take an ImageBitmap, HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, etc.
+ ctx.drawImage(this.kofiCupImageBitmap, x, y, iconSize, iconSize);
+ } catch (e) {
+ console.error("Error drawing local Ko-fi SVG ImageBitmap, falling back to manual draw:", e);
+ this._drawManualKofiCup(ctx, x, y, iconSize);
+ }
+ } else {
+ this._drawManualKofiCup(ctx, x, y, iconSize);
+ }
+ },
+
+ _drawManualKofiCup: function(ctx, x, y, iconSize) {
+ ctx.save();
+ ctx.fillStyle = '#FFDD99';
+ ctx.strokeStyle = '#D2B48C';
+ ctx.lineWidth = 1;
+
+ ctx.beginPath();
+ ctx.moveTo(x, y + iconSize * 0.2);
+ ctx.lineTo(x, y + iconSize * 0.9);
+ ctx.quadraticCurveTo(x + iconSize * 0.5, y + iconSize * 1.1, x + iconSize, y + iconSize * 0.9);
+ ctx.lineTo(x + iconSize, y + iconSize * 0.2);
+ ctx.quadraticCurveTo(x + iconSize * 0.5, y, x, y + iconSize * 0.2);
+ ctx.fill();
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(x + iconSize * 0.9, y + iconSize * 0.5, iconSize * 0.25, -Math.PI/2, Math.PI/2);
+ ctx.stroke();
+
+ ctx.strokeStyle = '#FFFFFF';
+ ctx.lineWidth = 0.5;
+ ctx.beginPath();
+ ctx.moveTo(x + iconSize * 0.3, y + iconSize * 0.1);
+ ctx.quadraticCurveTo(x + iconSize * 0.2, y - iconSize * 0.2, x + iconSize * 0.4, y - iconSize * 0.3);
+ ctx.moveTo(x + iconSize * 0.6, y + iconSize * 0.05);
+ ctx.quadraticCurveTo(x + iconSize * 0.5, y - iconSize * 0.3, x + iconSize * 0.7, y - iconSize * 0.4);
+ ctx.stroke();
+
+ ctx.restore();
+ },
+
+ renderPlaceholder: function(canvas, noiseName) {
+ const ctx = this._clearCanvas(canvas, '#2c2c34');
+ const nameToShow = noiseName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+
+ ctx.fillStyle = '#e0e0e8';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.font = 'bold 12px "Segoe UI", Tahoma, Geneva, Verdana, sans-serif';
+
+ const words = nameToShow.split(' ');
+ if (words.length > 2) {
+ ctx.fillText(words.slice(0,2).join(' '), canvas.width / 2, canvas.height / 2 - 7);
+ ctx.fillText(words.slice(2).join(' '), canvas.width / 2, canvas.height / 2 + 7);
+ } else {
+ ctx.fillText(nameToShow, canvas.width / 2, canvas.height / 2);
+ }
+ ctx.strokeStyle = '#555';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ // --- Visualization Functions ---
+ renderTensorField: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.strokeStyle = 'rgba(138, 43, 226, 0.7)';
+ ctx.lineWidth = 1;
+ for (let i = 0; i < 10; i++) {
+ ctx.beginPath();
+ const y = (i + 0.5) * (canvas.height / 10);
+ ctx.moveTo(0, y);
+ for (let x = 0; x <= canvas.width; x += 5) {
+ const angle = Math.sin(x * 0.1 + y * 0.05) * Math.PI * 0.25;
+ ctx.lineTo(x + Math.cos(angle) * 5, y + Math.sin(angle) * 5);
+ ctx.moveTo(x,y);
+ }
+ ctx.stroke();
+ }
+ },
+
+ renderCellular: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const numCells = 8;
+ const points = [];
+ for (let i = 0; i < numCells; i++) {
+ points.push({x: Math.random() * canvas.width, y: Math.random() * canvas.height});
+ }
+ for (let x = 0; x < canvas.width; x += 4) {
+ for (let y = 0; y < canvas.height; y += 4) {
+ let minDist = Infinity;
+ points.forEach(p => { minDist = Math.min(minDist, Math.hypot(p.x - x, p.y - y)); });
+ const intensity = Math.min(255, minDist * 2);
+ ctx.fillStyle = `rgb(${intensity},${intensity},${intensity + 50})`;
+ ctx.fillRect(x, y, 4, 4);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderDomainWarp: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.lineWidth = 1.5;
+ for (let i = 0; i < 20; i++) {
+ ctx.beginPath();
+ ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
+ ctx.strokeStyle = `rgba(${100 + Math.random()*155}, ${100 + Math.random()*155}, ${200 + Math.random()*55}, 0.6)`;
+ for (let j = 0; j < 5; j++) {
+ const x = Math.random() * canvas.width;
+ const y = Math.random() * canvas.height;
+ const cp1x = Math.random() * canvas.width;
+ const cp1y = Math.random() * canvas.height;
+ const cp2x = Math.random() * canvas.width;
+ const cp2y = Math.random() * canvas.height;
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
+ }
+ ctx.stroke();
+ }
+ },
+
+ renderFractal: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const scale = 0.05;
+ const octaves = 4;
+ const persistence = 0.5;
+ const lacunarity = 2.0;
+
+ for (let x = 0; x < canvas.width; x += 2) {
+ for (let y = 0; y < canvas.height; y += 2) {
+ let total = 0;
+ let frequency = 1;
+ let amplitude = 1;
+ let maxValue = 0;
+
+ for (let i = 0; i < octaves; i++) {
+ // Using a simple pseudo-random noise based on sin/cos
+ // A more robust noise function (like Simplex or Perlin) would be better for true FBM.
+ // This is a visual approximation for the small canvas.
+ const noiseVal = (Math.sin(x * scale * frequency + y * scale * frequency * 0.7) +
+ Math.cos(y * scale * frequency - x * scale * frequency * 0.3)) / 2;
+ total += noiseVal * amplitude;
+
+ maxValue += amplitude;
+ amplitude *= persistence;
+ frequency *= lacunarity;
+ }
+
+ const normalizedTotal = (total / maxValue + 1) / 2; // Normalize to 0-1 range
+ const colorVal = Math.floor(normalizedTotal * 200) + 55; // Map to 55-255 range
+
+ // A more neutral, less blue color scheme
+ ctx.fillStyle = `rgb(${colorVal}, ${colorVal * 0.95}, ${colorVal * 0.9})`;
+ ctx.fillRect(x, y, 2, 2);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderPerlin: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let x = 0; x < canvas.width; x+=3) {
+ for (let y = 0; y < canvas.height; y+=3) {
+ const noiseVal = (Math.sin(x * 0.05 + Math.cos(y*0.08)) + Math.cos(y * 0.06)) / 2;
+ const intensity = (noiseVal + 1) / 2 * 200 + 55;
+ ctx.fillStyle = `rgba(${intensity * 0.8}, ${intensity * 0.9}, ${intensity}, 1)`;
+ ctx.fillRect(x, y, 3, 3);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderWaves: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.strokeStyle = 'rgba(52, 152, 219, 0.7)';
+ ctx.lineWidth = 1.5;
+ for (let i = 0; i < 15; i++) {
+ ctx.beginPath();
+ const startY = i * (canvas.height / 15);
+ ctx.moveTo(0, startY);
+ for (let x = 0; x <= canvas.width; x += 5) {
+ const yOffset = Math.sin(x * 0.1 + i * 0.5) * 10;
+ ctx.lineTo(x, startY + yOffset);
+ }
+ ctx.stroke();
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderGaussian: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let i = 0; i < 10000; i++) {
+ const x = Math.random() * canvas.width;
+ const y = Math.random() * canvas.height;
+ const intensity = Math.floor(Math.random() * 100) + 100;
+ ctx.fillStyle = `rgba(${intensity * 0.8}, ${intensity * 0.9}, ${intensity}, ${Math.random() * 0.5 + 0.1})`;
+ ctx.fillRect(x - 1, y - 1, 2, 2);
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderHeterogeneousFBM: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let region = 0; region < 3; region++) {
+ const regionX = (canvas.width / 3) * region;
+ const regionW = canvas.width / 3;
+ const step = 2 + region * 2;
+ for (let x = 0; x < regionW; x += step) {
+ for (let y = 0; y < canvas.height; y += step) {
+ const noiseVal = Math.random();
+ const intensity = noiseVal * 255;
+ ctx.fillStyle = `rgb(${intensity}, ${intensity}, ${intensity})`;
+ ctx.fillRect(regionX + x, y, step, step);
+ }
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderInterference: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.lineWidth = 0.5;
+ const sources = [
+ {x: canvas.width * 0.2, y: canvas.height * 0.3, phase: 0},
+ {x: canvas.width * 0.8, y: canvas.height * 0.7, phase: Math.PI/2}
+ ];
+ for (let x = 0; x < canvas.width; x += 3) {
+ for (let y = 0; y < canvas.height; y += 3) {
+ let sum = 0;
+ sources.forEach(s => {
+ const dist = Math.hypot(s.x - x, s.y - y);
+ sum += Math.sin(dist * 0.1 + s.phase);
+ });
+ const intensity = (sum / sources.length + 1) / 2 * 255;
+ ctx.fillStyle = `rgb(${intensity * 0.7}, ${intensity}, ${intensity * 0.8})`;
+ ctx.fillRect(x, y, 3, 3);
+ }
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderSpectral: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let i = 0; i < canvas.height; i+=4) {
+ const freqComponent = Math.sin(i * 0.1) * 0.3 + Math.cos(i*0.05) * 0.3 + Math.random()*0.4;
+ const intensity = (freqComponent + 1)/2 * 255;
+ ctx.fillStyle = `rgb(${intensity}, ${intensity*0.8}, ${intensity*0.6})`;
+ ctx.fillRect(0, i, canvas.width, 4);
+ ctx.strokeStyle = 'rgba(0,0,0,0.2)';
+ ctx.strokeRect(0, i, canvas.width, 4);
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderProjection3D: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ for (let i = 0; i < 50; i++) {
+ const z = Math.random();
+ const x = (Math.random() - 0.5) * canvas.width * (1 + z) + canvas.width / 2;
+ const y = (Math.random() - 0.5) * canvas.height * (1 + z) + canvas.height / 2;
+ const size = (1 - z) * 10 + 2;
+ const opacity = (1 - z) * 0.7 + 0.1;
+ ctx.fillStyle = `rgba(${150 + z*105}, ${150 + z*105}, ${200 + z*55}, ${opacity})`;
+ ctx.beginPath();
+ ctx.arc(x, y, size, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ this._drawKofiIcon(ctx.canvas.getContext('2d'));
+ },
+
+ renderCurlNoise: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ ctx.lineWidth = 1;
+ for (let i = 0; i < 50; i++) {
+ let x = Math.random() * canvas.width;
+ let y = Math.random() * canvas.height;
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ ctx.strokeStyle = `rgba(${100 + Math.random()*100}, ${150 + Math.random()*105}, ${200 + Math.random()*55}, 0.5)`;
+ for (let step = 0; step < 20; step++) {
+ const angle = Math.sin(x * 0.02 + y * 0.03) * Math.PI + Math.cos(y * 0.02 - x*0.01) * Math.PI;
+ x += Math.cos(angle) * 5;
+ y += Math.sin(angle) * 5;
+ if (x < 0 || x > canvas.width || y < 0 || y > canvas.height) break;
+ ctx.lineTo(x, y);
+ }
+ ctx.stroke();
+ }
+ },
+
+ // Mask Placeholder
+ renderMaskPlaceholder: function(canvas, maskName) {
+ const ctx = this._clearCanvas(canvas, '#2c2c34');
+ const nameToShow = maskName.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
+
+ ctx.fillStyle = '#e0e0e8';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.font = 'bold 10px "Segoe UI", Tahoma, Geneva, Verdana, sans-serif';
+ ctx.fillText(nameToShow, canvas.width / 2, canvas.height / 2);
+ ctx.strokeStyle = '#555';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ // Mask Visualization Functions
+ renderMaskRadial: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxRadius = Math.min(canvas.width, canvas.height) / 2;
+
+ // Create gradient for radial mask
+ const gradient = ctx.createRadialGradient(
+ centerX, centerY, 0,
+ centerX, centerY, maxRadius
+ );
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 1.0)');
+ gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)');
+
+ // Draw the gradient as the mask
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskLinear: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+
+ // Create gradient for linear mask
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 1.0)');
+ gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)');
+
+ // Draw the gradient as the mask
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskGrid: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const cellSize = 20;
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+
+ // Draw a grid pattern
+ for (let x = 0; x < canvas.width; x += cellSize) {
+ for (let y = 0; y < canvas.height; y += cellSize) {
+ if ((Math.floor(x / cellSize) + Math.floor(y / cellSize)) % 2 === 0) {
+ ctx.fillRect(x, y, cellSize, cellSize);
+ }
+ }
+ }
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskVignette: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxRadius = Math.sqrt(centerX * centerX + centerY * centerY);
+
+ // Create gradient for vignette mask (inverse of radial)
+ const gradient = ctx.createRadialGradient(
+ centerX, centerY, 0,
+ centerX, centerY, maxRadius
+ );
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 0.0)');
+ gradient.addColorStop(0.6, 'rgba(255, 255, 255, 0.3)');
+ gradient.addColorStop(1, 'rgba(255, 255, 255, 1.0)');
+
+ // Draw the gradient as the mask
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskSpiral: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxRadius = Math.min(canvas.width, canvas.height) / 2;
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
+ ctx.lineWidth = 5;
+
+ // Draw a spiral
+ ctx.beginPath();
+ for (let theta = 0; theta < 8 * Math.PI; theta += 0.1) {
+ const radius = (maxRadius / (8 * Math.PI)) * theta;
+ const x = centerX + radius * Math.cos(theta);
+ const y = centerY + radius * Math.sin(theta);
+
+ if (theta === 0) {
+ ctx.moveTo(x, y);
+ } else {
+ ctx.lineTo(x, y);
+ }
+ }
+ ctx.stroke();
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskHexgrid: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const hexSize = 15;
+ const hexHeight = hexSize * Math.sqrt(3);
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+
+ // Draw a hexagonal grid
+ for (let row = 0; row < canvas.height / hexHeight + 1; row++) {
+ for (let col = 0; col < canvas.width / (hexSize * 3) + 1; col++) {
+ const offsetX = (row % 2) * hexSize * 1.5;
+ const x = col * hexSize * 3 + offsetX;
+ const y = row * hexHeight;
+
+ ctx.beginPath();
+ for (let i = 0; i < 6; i++) {
+ const angle = (i * Math.PI / 3);
+ const pX = x + hexSize * Math.cos(angle);
+ const pY = y + hexSize * Math.sin(angle);
+
+ if (i === 0) {
+ ctx.moveTo(pX, pY);
+ } else {
+ ctx.lineTo(pX, pY);
+ }
+ }
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
+ }
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskWavy: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
+ ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
+ ctx.lineWidth = 3;
+
+ const amplitude = 10;
+ const frequency = 0.05;
+
+ // Draw wavy lines
+ for (let y = 20; y < canvas.height; y += 20) {
+ ctx.beginPath();
+ for (let x = 0; x <= canvas.width; x += 2) {
+ const yOffset = Math.sin(x * frequency) * amplitude;
+ if (x === 0) {
+ ctx.moveTo(x, y + yOffset);
+ } else {
+ ctx.lineTo(x, y + yOffset);
+ }
+ }
+ ctx.stroke();
+ }
+
+ // Add a subtle border
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ renderMaskConcentricRings: function(canvas) {
+ const ctx = this._clearCanvas(canvas);
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+
+ ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
+ ctx.lineWidth = 2;
+
+ // Draw concentric rings
+ const ringCount = 6;
+ const maxRadius = Math.min(canvas.width, canvas.height) / 2;
+ const step = maxRadius / ringCount;
+
+ for (let i = 1; i <= ringCount; i++) {
+ const radius = i * step;
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+ ctx.stroke();
+
+ // Add some fill for alternating rings
+ if (i % 2 === 0) {
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ // Add a subtle border around the canvas
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(0, 0, canvas.width, canvas.height);
+ },
+
+ // Color scheme rendering functions
+ renderColorInferno: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000004, #3b0f70, #8c2981, #de4968, #fe9f6d, #fcfdbf)';
+ },
+
+ renderColorMagma: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000004, #3b0f70, #8c2981, #de4968, #fe9f6d, #fcfdbf)';
+ },
+
+ renderColorPlasma: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0d0887, #7e03a8, #cc4678, #f89441, #f0f921)';
+ },
+
+ renderColorViridis: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #440154, #30678d, #35b778, #fde724)';
+ },
+
+ renderColorTurbo: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #30123b, #4669db, #26bf8c, #d4ff50, #fab74c, #ba0100)';
+ },
+
+ renderColorJet: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #00007f, #0000ff, #00ffff, #ffff00, #ff0000, #7f0000)';
+ },
+
+ renderColorParula: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #352a87, #0f5cdd, #00b5a6, #ffc337, #fcfea4)';
+ },
+
+ renderColorRainbow: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0000ff, #00ffff, #00ff00, #ffff00, #ff0000)';
+ },
+
+ renderColorHot: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #ff0000, #ffff00, #ffffff)';
+ },
+
+ renderColorBlueRed: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0000ff, #7777ff, #ffffff, #ff7777, #ff0000)';
+ },
+
+ renderColorCool: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #00ffff, #77aaff, #aa77ff, #ff00ff)';
+ },
+
+ renderColorHsv: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000)';
+ },
+
+ renderColorAutumn: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff0000, #ff7700, #ffaa00, #ffdd00, #ffff00)';
+ },
+
+ renderColorWinter: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0000ff, #0077cc, #00aabb, #00ddaa)';
+ },
+
+ renderColorSpring: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff00ff, #ff33cc, #ff77aa, #ffaa77, #ffdd44, #ffff00)';
+ },
+
+ renderColorSummer: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #004433, #008855, #44aa66, #88cc77, #ccee88, #ffff66)';
+ },
+
+ renderColorCopper: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #331100, #662200, #993300, #cc6644, #ff9966)';
+ },
+
+ renderColorPink: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #0a0a0a, #550055, #aa0066, #ff44aa, #ffaadd, #ffffff)';
+ },
+
+ renderColorBone: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #2a2a3a, #5a748a, #9ebacb, #dfdfef, #ffffff)';
+ },
+
+ renderColorOcean: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #000066, #0000bb, #0066cc, #00ccff, #99ffff)';
+ },
+
+ renderColorTerrain: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #333399, #009966, #66cc33, #cccc33, #ff9933, #ffffff)';
+ },
+
+ renderColorNeon: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #ff00ff, #aa00ff, #5500ff, #0000ff, #00aaff, #00ffff, #00ff00, #aaff00, #ffff00)';
+ },
+
+ renderColorFire: function(swatchDiv) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #000000, #330000, #660000, #bb0000, #ff0000, #ff7700, #ffdd00, #ffffff)';
+ },
+
+ // New function for temporal animation
+ renderTemporalAnimation: function(canvas) {
+ const ctx = this._clearCanvas(canvas, '#0a0a0a'); // Dark background for animation
+ let time = 0;
+
+ function drawPattern(currentTime) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = '#0a0a0a';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ const numLines = 20;
+ const maxOffset = 20;
+
+ ctx.strokeStyle = 'rgba(52, 152, 219, 0.6)';
+ ctx.lineWidth = 1.5;
+
+ for (let i = 0; i < numLines; i++) {
+ ctx.beginPath();
+ const startY = (i / numLines) * canvas.height;
+ ctx.moveTo(0, startY);
+ for (let x = 0; x <= canvas.width; x += 5) {
+ // Create a wave that evolves with time and differs per line
+ const yOffset = Math.sin(x * 0.02 + i * 0.3 + currentTime * 0.05) * maxOffset * Math.sin(currentTime * 0.02 + i * 0.1);
+ const actualY = startY + yOffset;
+ ctx.lineTo(x, actualY);
+ }
+ ctx.stroke();
+ }
+ }
+
+ function animate() {
+ drawPattern(time);
+ time += 0.1; // Increment time for animation
+ // Only request next frame if the canvas is still in the document (modal is open)
+ if (canvas.isConnected) {
+ requestAnimationFrame(animate);
+ }
+ }
+
+ // Check if an animation is already running on this canvas
+ if (!canvas.dataset.animationRunning) {
+ canvas.dataset.animationRunning = 'true';
+ animate();
+ }
+ },
+
+ // New function for the intro noise demo
+ renderIntroNoiseDemo: function(canvas) {
+ const ctx = this._clearCanvas(canvas, '#0f0f12'); // Darker background for intro
+ let time = 0;
+ const particles = [];
+ const numParticles = 120; // Slightly reduced for clarity with trails
+ const particleColorBase = [138, 43, 226];
+ const particleColorHighlight = [224, 224, 232];
+ const loopCycleDuration = 420; // Extended to 7 seconds
+ let lastLoopInstance = -1;
+ let currentBackgroundPattern = { type: null, variantSeed: 0, alpha: 0, startTime: 0 };
+
+ // Helper function to get spawn points based on pattern
+ function getSpawnPointForPattern(patternType, variantSeed, canvas, patternTimeVal) {
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxPatternSize = Math.min(centerX, centerY) * 0.95;
+ let spawnX = Math.random() * canvas.width;
+ let spawnY = Math.random() * canvas.height;
+
+ // timeVal for spawning should reflect the pattern at its initial state (or very early evolution)
+ const timeVal = patternTimeVal * 0.00015; // Match background pattern evolution rate
+
+ if (patternType === 'tensor_field') {
+ const gridSize = 30;
+ const gx = Math.floor(Math.random() * (canvas.width / gridSize)) * gridSize + gridSize/2;
+ const gy = Math.floor(Math.random() * (canvas.height / gridSize)) * gridSize + gridSize/2;
+ const tfScale = 0.025 + (variantSeed % 5) * 0.003;
+ const angleOffset = variantSeed * 0.3;
+ const fieldAngle = Math.sin(gx * tfScale + timeVal * 5 + angleOffset) * Math.PI +
+ Math.cos(gy * tfScale - timeVal * 3) * Math.PI;
+ const fieldMagnitude = (Math.sin(gx * tfScale * 0.7 + gy * tfScale * 0.5 - timeVal * 2) + 1) / 2;
+ const lineLength = 10 * (0.5 + fieldMagnitude * 0.7);
+ // Spawn along the vector line
+ const r = Math.random() * lineLength - lineLength/2;
+ spawnX = gx + Math.cos(fieldAngle) * r;
+ spawnY = gy + Math.sin(fieldAngle) * r;
+ } else if (patternType === 'cellular') {
+ const numPoints = 3 + Math.floor(variantSeed % 4);
+ const pointIndex = Math.floor(Math.random() * numPoints);
+ const angle = (pointIndex / numPoints) * Math.PI * 2 + timeVal * (0.1 + (variantSeed % 3)*0.03) + variantSeed * 0.7;
+ const radius = maxPatternSize * (0.4 + ((pointIndex + variantSeed) % 4) * 0.12) * (0.85 + Math.sin(timeVal*1.5 + pointIndex) * 0.15);
+ const pX = centerX + Math.cos(angle) * radius;
+ const pY = centerY + Math.sin(angle) * radius;
+ // Spawn near a cell center point
+ spawnX = pX + (Math.random() - 0.5) * 30;
+ spawnY = pY + (Math.random() - 0.5) * 30;
+ } else if (patternType === 'domain_warp') {
+ const gridSize = 25;
+ const c = Math.floor(Math.random() * (canvas.width / gridSize));
+ const r = Math.floor(Math.random() * (canvas.height / gridSize));
+ const origX = c * gridSize;
+ const origY = r * gridSize;
+ const warpScale = 0.015 + (variantSeed % 5) * 0.002;
+ const warpIntensity = canvas.width * (0.05 + (variantSeed % 3) * 0.02);
+ const offsetX = Math.sin(origY * warpScale * 1.2 + timeVal * 8 + variantSeed) * warpIntensity;
+ const offsetY = Math.cos(origX * warpScale * 0.8 - timeVal * 6 + variantSeed) * warpIntensity;
+ spawnX = origX + offsetX;
+ spawnY = origY + offsetY;
+ } else if (patternType === 'perlin') {
+ // Try a few random spots and pick one with mid-range noise intensity
+ let bestX = spawnX, bestY = spawnY, minDiff = Infinity;
+ for(let i=0; i<10; i++) {
+ let sx = Math.random() * canvas.width;
+ let sy = Math.random() * canvas.height;
+ let noiseVal = 0;
+ let freq = 0.015 + (variantSeed % 7) * 0.003;
+ let amp = 0.6;
+ for (let oct = 0; oct < 3; oct++) {
+ noiseVal += Math.sin(sx * freq + timeVal*10 + variantSeed*0.2) * Math.cos(sy * freq - timeVal*12 - variantSeed*0.1) * amp;
+ freq *= 1.8 + (variantSeed % 3) * 0.1;
+ amp *= 0.45 + (variantSeed % 4) * 0.02;
+ }
+ const currentDiff = Math.abs(noiseVal); // Target noiseVal around 0 (mid-range for bipolar noise)
+ if (currentDiff < minDiff) {
+ minDiff = currentDiff;
+ bestX = sx; bestY = sy;
+ }
+ }
+ spawnX = bestX; spawnY = bestY;
+ } else if (patternType === 'curl_noise') {
+ // Spawn along a few conceptual streamlines
+ const numStreamlines = 5;
+ const streamlineIndex = Math.floor(Math.random() * numStreamlines);
+ let pX = canvas.width * (streamlineIndex / numStreamlines + Math.random() * 0.1 - 0.05);
+ let pY = canvas.height * Math.random();
+ const curlScale = 0.018 + (variantSeed % 5) * 0.002;
+ const Gx_val = Math.sin(pY*curlScale + timeVal*7 + variantSeed + streamlineIndex*0.1);
+ const Gy_val = Math.cos(pX*curlScale - timeVal*9 + variantSeed + streamlineIndex*0.1);
+ const flowX = Gy_val;
+ const flowY = -Gx_val;
+ const norm = Math.hypot(flowX, flowY);
+ if (norm > 0.01) {
+ const step = (Math.random() - 0.5) * 50; // Spawn somewhere along the local flow vector
+ spawnX = pX + (flowX / norm) * step;
+ spawnY = pY + (flowY / norm) * step;
+ } else {
+ spawnX = pX; spawnY = pY;
+ }
+ } else if (patternType === 'domain_warp') {
+ const gridSize = 25;
+ const c = Math.floor(Math.random() * (canvas.width / gridSize));
+ const r = Math.floor(Math.random() * (canvas.height / gridSize));
+ const origX = c * gridSize;
+ const origY = r * gridSize;
+ const warpScale = 0.015 + (variantSeed % 5) * 0.002;
+ const warpIntensity = canvas.width * (0.05 + (variantSeed % 3) * 0.02);
+ const offsetX = Math.sin(origY * warpScale * 1.2 + timeVal * 8 + variantSeed) * warpIntensity;
+ const offsetY = Math.cos(origX * warpScale * 0.8 - timeVal * 6 + variantSeed) * warpIntensity;
+ spawnX = origX + offsetX;
+ spawnY = origY + offsetY;
+ } else if (patternType === 'perlin') {
+ // Try a few random spots and pick one with mid-range noise intensity
+ let bestX = spawnX, bestY = spawnY, minDiff = Infinity;
+ for(let i=0; i<10; i++) {
+ let sx = Math.random() * canvas.width;
+ let sy = Math.random() * canvas.height;
+ let noiseVal = 0;
+ let freq = 0.015 + (variantSeed % 7) * 0.003;
+ let amp = 0.6;
+ for (let oct = 0; oct < 3; oct++) {
+ noiseVal += Math.sin(sx * freq + timeVal*10 + variantSeed*0.2) * Math.cos(sy * freq - timeVal*12 - variantSeed*0.1) * amp;
+ freq *= 1.8 + (variantSeed % 3) * 0.1;
+ amp *= 0.45 + (variantSeed % 4) * 0.02;
+ }
+ const currentDiff = Math.abs(noiseVal); // Target noiseVal around 0 (mid-range for bipolar noise)
+ if (currentDiff < minDiff) {
+ minDiff = currentDiff;
+ bestX = sx; bestY = sy;
+ }
+ }
+ spawnX = bestX; spawnY = bestY;
+ } else if (patternType === 'curl_noise') {
+ // Spawn along a few conceptual streamlines
+ const numStreamlines = 5;
+ const streamlineIndex = Math.floor(Math.random() * numStreamlines);
+ let pX = canvas.width * (streamlineIndex / numStreamlines + Math.random() * 0.1 - 0.05);
+ let pY = canvas.height * Math.random();
+ const curlScale = 0.018 + (variantSeed % 5) * 0.002;
+ const Gx_val = Math.sin(pY*curlScale + timeVal*7 + variantSeed + streamlineIndex*0.1);
+ const Gy_val = Math.cos(pX*curlScale - timeVal*9 + variantSeed + streamlineIndex*0.1);
+ const flowX = Gy_val;
+ const flowY = -Gx_val;
+ const norm = Math.hypot(flowX, flowY);
+ if (norm > 0.01) {
+ const step = (Math.random() - 0.5) * 50; // Spawn somewhere along the local flow vector
+ spawnX = pX + (flowX / norm) * step;
+ spawnY = pY + (flowY / norm) * step;
+ } else {
+ spawnX = pX; spawnY = pY;
+ }
+ } else if (patternType === 'waves_interference') {
+ const numWaveSets = 2 + Math.floor(variantSeed % 2);
+ const ws = Math.floor(Math.random()*numWaveSets); // Pick a wave set
+ const lineIdx = Math.floor(Math.random()*15); // Pick a line within the set
+
+ const amplitude = maxPatternSize * (0.15 + (variantSeed % 3) * 0.05);
+ const frequency = 0.025 + ((variantSeed+ws*2) % 7) * 0.004;
+ const angle = (variantSeed * 0.2 + ws * Math.PI / (numWaveSets * 1.5)) + timeVal * (3 + ws*0.5);
+ const cosA = Math.cos(angle);
+ const sinA = Math.sin(angle);
+
+ const lineOffset = (lineIdx - 15/2) * maxPatternSize * 0.1;
+ const j = (Math.random() - 0.5) * maxPatternSize * 2.0; // Random point along the line
+ const waveDisplacement = Math.sin(j * frequency + timeVal * (10 + ws*3) + ws*Math.PI) * amplitude;
+ const originalX = j;
+ const originalY = lineOffset + waveDisplacement;
+
+ spawnX = centerX + (originalX * cosA - originalY * sinA);
+ spawnY = centerY + (originalX * sinA + originalY * cosA);
+ }
+
+ return {
+ x: Math.max(0, Math.min(canvas.width, spawnX)),
+ y: Math.max(0, Math.min(canvas.height, spawnY))
+ };
+ }
+
+ // Noise function modified to include influence from background patterns
+ function noiseField(x, y, t, patternType, patternVariantSeed, patternTime) {
+ const baseScale = 0.009;
+ const timeScale = 0.0023;
+ const loopTime = t % loopCycleDuration;
+ const currentLoopInstance = Math.floor(t / loopCycleDuration);
+
+ const globalRotationAngle = currentLoopInstance * 0.15;
+ const cosA = Math.cos(globalRotationAngle);
+ const sinA = Math.sin(globalRotationAngle);
+
+ let rotatedX = x * cosA - y * sinA;
+ let rotatedY = x * sinA + y * cosA;
+
+ // --- Base Chaotic Noise Calculation (similar to before) ---
+ let baseChaosVal = 0;
+ const scale1 = baseScale * 1.8;
+ baseChaosVal += Math.sin(rotatedX * scale1 + loopTime * timeScale * 1.1) *
+ Math.cos(rotatedY * scale1 * 0.8 - loopTime * timeScale * 0.9);
+ const scale2 = baseScale * 0.9;
+ baseChaosVal += Math.sin(rotatedY * scale2 * 1.1 - loopTime * timeScale * 1.3) *
+ Math.cos(rotatedX * scale2 * 0.9 + loopTime * timeScale * 1.0) * 0.8;
+ const scale3 = baseScale * 1.2;
+ const innerAngleOffset = Math.sin(loopTime * 0.0008 + currentLoopInstance * 0.05) * 0.5;
+ const N_x_chaos = rotatedX * Math.cos(innerAngleOffset) - rotatedY * Math.sin(innerAngleOffset);
+ const N_y_chaos = rotatedX * Math.sin(innerAngleOffset) + rotatedY * Math.cos(innerAngleOffset);
+ baseChaosVal += Math.sin(N_x_chaos * scale3 * 0.6 + loopTime * timeScale * 0.7) *
+ Math.cos(N_y_chaos * scale3 * 0.7 - loopTime * timeScale * 0.5) * 0.9;
+ let baseAngle = (baseChaosVal / 2.7) * Math.PI * 4.0; // Base movement angle
+
+ // --- Background Pattern Influence Calculation ---
+ let influenceAngle = baseAngle;
+ let influenceStrength = 0.0; // 0.0 to 1.0 (max influence)
+ const patternEvoTime = patternTime * 0.00015;
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+
+ if (patternType === 'tensor_field') {
+ const tfScale = 0.025 + (patternVariantSeed % 5) * 0.003;
+ const angleOffset = patternVariantSeed * 0.3;
+ // Match the angle calculation from drawBackgroundShaderPattern
+ influenceAngle = Math.sin(x * tfScale + patternEvoTime * 5 + angleOffset) * Math.PI +
+ Math.cos(y * tfScale - patternEvoTime * 3) * Math.PI;
+ influenceStrength = 0.75;
+ } else if (patternType === 'cellular') {
+ const numPoints = 3 + Math.floor(patternVariantSeed % 4);
+ const points = [];
+ for (let i = 0; i < numPoints; i++) {
+ const angle = (i / numPoints) * Math.PI * 2 + patternEvoTime * (0.1 + (patternVariantSeed % 3)*0.03) + patternVariantSeed * 0.7;
+ const radiusFactor = (0.4 + ((i + patternVariantSeed) % 4) * 0.12) * (0.85 + Math.sin(patternEvoTime*1.5 + i) * 0.15);
+ points.push({
+ x: centerX + Math.cos(angle) * (Math.min(centerX,centerY)*0.95) * radiusFactor,
+ y: centerY + Math.sin(angle) * (Math.min(centerX,centerY)*0.95) * radiusFactor
+ });
+ }
+
+ let p1_idx = -1, p2_idx = -1;
+ let minDist1 = Infinity, minDist2 = Infinity;
+
+ for(let k=0; kpB, flow one way, else other.
+ // Vector from pA to particle (particleRelX, particleRelY)
+ // Vector pA to pB (edgeDx, edgeDy)
+ // 2D cross product: particleRelX * edgeDy - particleRelY * edgeDx
+ if (particleRelX * edgeDy - particleRelY * edgeDx > 0) {
+ influenceAngle += Math.PI; // Reverse direction
+ }
+ }
+ influenceStrength = 0.7;
+ } else {
+ // If not near an edge, maybe weakly attract to the closest point or a mix?
+ // For now, let's stick to edge following for clear "lines"
+ influenceStrength = 0.1; // Weak influence if not near edge
+ }
+ }
+ } else if (patternType === 'domain_warp') {
+ const warpScale = 0.015 + (patternVariantSeed % 5) * 0.002;
+ const warpIntensity = 25 + (patternVariantSeed % 3) * 5;
+ const displacementX = Math.sin(y * warpScale * 1.2 + patternEvoTime * 8 + patternVariantSeed) * warpIntensity;
+ const displacementY = Math.cos(x * warpScale * 0.8 - patternEvoTime * 6 + patternVariantSeed) * warpIntensity;
+ if (Math.hypot(displacementX, displacementY) > 0.1) {
+ influenceAngle = Math.atan2(displacementY, displacementX);
+ influenceStrength = 0.7; // Increased strength
+ }
+ } else if (patternType === 'perlin') {
+ let noiseValCenter = 0, noiseValX = 0, noiseValY = 0;
+ let freq = 0.015 + (patternVariantSeed % 7) * 0.003;
+ let amp = 0.6;
+ for (let oct = 0; oct < 3; oct++) {
+ noiseValCenter += Math.sin(x * freq + patternEvoTime*10 + patternVariantSeed*0.2) * Math.cos(y * freq - patternEvoTime*12 - patternVariantSeed*0.1) * amp;
+ noiseValX += Math.sin((x+1) * freq + patternEvoTime*10+ patternVariantSeed*0.2) * Math.cos(y * freq - patternEvoTime*12 - patternVariantSeed*0.1) * amp;
+ noiseValY += Math.sin(x * freq + patternEvoTime*10+ patternVariantSeed*0.2) * Math.cos((y+1) * freq - patternEvoTime*12 - patternVariantSeed*0.1) * amp;
+ freq *= 1.8 + (patternVariantSeed % 3) * 0.1;
+ amp *= 0.45 + (patternVariantSeed % 4) * 0.02;
+ }
+ const gradX = noiseValX - noiseValCenter;
+ const gradY = noiseValY - noiseValCenter;
+ if (Math.hypot(gradX, gradY) > 0.005) {
+ influenceAngle = Math.atan2(gradY, gradX) + (Math.PI / 2); // Perpendicular to gradient
+ influenceStrength = 0.7; // Increased strength
+ }
+ } else if (patternType === 'waves_interference') {
+ let waveSum = 0;
+ const numWaveSources = 2 + (patternVariantSeed % 2);
+ for(let i=0; i 0.01) {
+ influenceAngle = Math.atan2(gradY, gradX) + (Math.PI / 2); // Perpendicular to gradient
+ influenceStrength = 0.65;
+ }
+ } else if (patternType === 'curl_noise') {
+ const curlScale = 0.018 + (patternVariantSeed % 5) * 0.002;
+ const Gx_val = Math.sin(y*curlScale + patternEvoTime*7 + patternVariantSeed);
+ const Gy_val = Math.cos(x*curlScale - patternEvoTime*9 + patternVariantSeed);
+ const flowX = Gy_val;
+ const flowY = -Gx_val;
+ if (Math.hypot(flowX, flowY) > 0.01) {
+ influenceAngle = Math.atan2(flowY, flowX);
+ influenceStrength = 0.85; // Increased strength
+ }
+ }
+
+ // Blend base chaos with pattern influence
+ // Normalize angles for smoother blending if they wrap around PI
+ let finalAngle;
+ let diff = influenceAngle - baseAngle;
+ while (diff > Math.PI) diff -= 2 * Math.PI;
+ while (diff < -Math.PI) diff += 2 * Math.PI;
+ finalAngle = baseAngle + diff * influenceStrength;
+
+ return finalAngle;
+ }
+
+ for (let i = 0; i < numParticles; i++) {
+ particles.push({
+ x: Math.random() * canvas.width,
+ y: Math.random() * canvas.height,
+ vx: 0,
+ vy: 0,
+ life: 0, // Start dead, will be revived in primordial state
+ initialLife: Math.random() * 100 + 90,
+ radius: Math.random() * 1.7 + 0.6,
+ trail: [],
+ isPrimordial: true, // New flag
+ primordialTime: 0 // Counter for primordial phase
+ });
+ }
+ const primordialDuration = 60; // Approx 1 second for primordial phase
+
+ function drawBackgroundShaderPattern(ctx, type, variantSeed, targetAlpha, patternTime) {
+ ctx.save();
+ ctx.globalAlpha = targetAlpha * 0.07; // Slightly increased base visibility for new patterns
+ ctx.strokeStyle = 'rgba(180, 190, 200, 0.75)';
+ ctx.lineWidth = 0.7;
+
+ const centerX = canvas.width / 2;
+ const centerY = canvas.height / 2;
+ const maxPatternSize = Math.min(centerX, centerY) * 0.95;
+ const timeVal = patternTime * 0.00015; // Consistent slow evolution
+
+ if (type === 'cellular') {
+ const numPoints = 3 + Math.floor(variantSeed % 4); // 3 to 6 points
+ const points = [];
+ for (let i = 0; i < numPoints; i++) {
+ const angle = (i / numPoints) * Math.PI * 2 + timeVal * (0.1 + (variantSeed % 3)*0.03) + variantSeed * 0.7;
+ const radius = maxPatternSize * (0.4 + ((i + variantSeed) % 4) * 0.12) * (0.85 + Math.sin(timeVal*1.5 + i) * 0.15);
+ points.push({
+ x: centerX + Math.cos(angle) * radius,
+ y: centerY + Math.sin(angle) * radius
+ });
+ }
+ // Draw Voronoi edges (simplified)
+ ctx.beginPath();
+ const step = 15;
+ for (let cy = 0; cy < canvas.height; cy += step) {
+ for (let cx = 0; cx < canvas.width; cx += step) {
+ let minDist1 = Infinity, minDist2 = Infinity;
+ let p1_idx = -1;
+ for(let k=0; k canvas.width || pY < 0 || pY > canvas.height) break;
+ ctx.lineTo(pX,pY);
+ }
+ ctx.stroke();
+ }
+ }
+
+ ctx.restore();
+ }
+
+ function drawIntroPattern() {
+ const currentLoopInstance = Math.floor(time / loopCycleDuration);
+ const isNewLoopStart = (time % loopCycleDuration) < (60 / 2.5);
+
+ if (isNewLoopStart && currentLoopInstance !== lastLoopInstance) {
+ ctx.fillStyle = 'rgba(15, 15, 18, 0.45)';
+ lastLoopInstance = currentLoopInstance;
+
+ const shaderPatternTypes = ['tensor_field', 'cellular', 'domain_warp', 'perlin', 'curl_noise', 'waves_interference'];
+ currentBackgroundPattern.type = shaderPatternTypes[currentLoopInstance % shaderPatternTypes.length];
+ currentBackgroundPattern.variantSeed = currentLoopInstance; // Use loop instance for variation
+ currentBackgroundPattern.alpha = 1.0;
+ currentBackgroundPattern.startTime = time;
+
+ const patternSpawnTimeVal = 0; // Use 0 for the conceptual "start time" of the pattern for consistent spawning
+
+ particles.forEach(p => {
+ const spawnPoint = getSpawnPointForPattern(currentBackgroundPattern.type, currentBackgroundPattern.variantSeed, canvas, patternSpawnTimeVal);
+ p.x = spawnPoint.x;
+ p.y = spawnPoint.y;
+ // p.x = Math.random() * canvas.width; // Old random spawn
+ // p.y = Math.random() * canvas.height; // Old random spawn
+ p.vx = (Math.random() - 0.5) * 0.1;
+ p.vy = (Math.random() - 0.5) * 0.1;
+ p.life = p.initialLife;
+ p.trail = [];
+ p.isPrimordial = true;
+ p.primordialTime = 0;
+ });
+
+ } else {
+ ctx.fillStyle = 'rgba(15, 15, 18, 0.015)';
+ }
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ if (currentBackgroundPattern.type && currentBackgroundPattern.alpha > 0) {
+ drawBackgroundShaderPattern(ctx, currentBackgroundPattern.type, currentBackgroundPattern.variantSeed, currentBackgroundPattern.alpha, time - currentBackgroundPattern.startTime);
+ currentBackgroundPattern.alpha = Math.max(0, 1.0 - ((time - currentBackgroundPattern.startTime) / loopCycleDuration) * 0.95 ); // Adjusted fade for longer loop
+ }
+
+ particles.forEach(p => {
+ if (p.isPrimordial) {
+ p.primordialTime++;
+ if (p.primordialTime >= primordialDuration) {
+ p.isPrimordial = false;
+ }
+ }
+
+ const finalAngle = noiseField(p.x, p.y, time, currentBackgroundPattern.type, currentBackgroundPattern.variantSeed, time - currentBackgroundPattern.startTime);
+ // const angle = noiseVal * Math.PI * 4.0; // Old way
+
+ p.vx += Math.cos(finalAngle) * 0.10;
+ p.vy += Math.sin(finalAngle) * 0.10;
+
+ p.vx *= 0.94;
+ p.vy *= 0.94;
+
+ p.x += p.vx;
+ p.y += p.vy;
+
+ p.trail.push({x: p.x, y: p.y});
+ if (p.trail.length > 25) {
+ p.trail.shift();
+ }
+
+ let trailBaseAlpha = 0.8;
+ let particleHeadBaseAlpha = 0.65;
+ let currentParticleRadius = p.radius;
+ let r = particleColorBase[0], g = particleColorBase[1], b = particleColorBase[2];
+
+ if (p.isPrimordial) {
+ const primordialRatio = p.primordialTime / primordialDuration;
+ trailBaseAlpha *= primordialRatio * 0.5; // Trails fainter during primordial
+ particleHeadBaseAlpha = primordialRatio * 0.5;
+ currentParticleRadius = p.radius * (0.3 + primordialRatio * 0.7); // Grow from small
+ // Desaturated primordial color (e.g., grayish-purple)
+ const primordialColorLerp = primordialRatio;
+ r = (particleColorBase[0] * primordialColorLerp) + (70 * (1 - primordialColorLerp));
+ g = (particleColorBase[1] * primordialColorLerp) + (70 * (1 - primordialColorLerp));
+ b = (particleColorBase[2] * primordialColorLerp) + (90 * (1 - primordialColorLerp));
+ }
+
+ if (p.trail.length > 1) {
+ ctx.beginPath();
+ ctx.moveTo(p.trail[0].x, p.trail[0].y);
+ for (let i = 1; i < p.trail.length; i++) {
+ const trailSegmentAlpha = (i / p.trail.length) * trailBaseAlpha;
+ const trailColor = `rgba(200, 200, 200, ${trailSegmentAlpha})`;
+ ctx.strokeStyle = trailColor;
+ ctx.lineWidth = currentParticleRadius * (i / p.trail.length) * 1.1;
+ ctx.lineTo(p.trail[i].x, p.trail[i].y);
+ }
+ ctx.stroke();
+ }
+
+ const lifeRatio = p.life / p.initialLife;
+ const emergenceFactor = p.isPrimordial ? (p.primordialTime / primordialDuration) : (1.0 - Math.pow(1.0 - lifeRatio, 2));
+ const finalRadius = p.isPrimordial ? currentParticleRadius : p.radius * emergenceFactor;
+ const mainParticleAlpha = emergenceFactor * particleHeadBaseAlpha;
+ const highlightAlpha = emergenceFactor * (particleHeadBaseAlpha + 0.1);
+
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, Math.max(0.1, finalRadius), 0, Math.PI * 2);
+ if (!p.isPrimordial) { // Use speed-based color only after primordial phase
+ const speed = Math.hypot(p.vx, p.vy);
+ r = Math.min(255, particleColorBase[0] + speed * 15);
+ g = Math.min(255, particleColorBase[1] - speed * 5);
+ b = Math.min(255, particleColorBase[2] + speed * 5);
+ }
+ ctx.fillStyle = `rgba(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(b)}, ${mainParticleAlpha})`;
+ ctx.fill();
+
+ if (finalRadius > 0.5) {
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, Math.max(0.1, finalRadius * 0.5), 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(${particleColorHighlight[0]}, ${particleColorHighlight[1]}, ${particleColorHighlight[2]}, ${highlightAlpha})`;
+ ctx.fill();
+ }
+
+ p.life--;
+
+ if (p.life <= 0 || p.x < -finalRadius * 2 || p.x > canvas.width + finalRadius*2 || p.y < -finalRadius*2 || p.y > canvas.height + finalRadius*2) {
+ // Reset to primordial state when respawning
+ p.x = Math.random() * canvas.width;
+ p.y = Math.random() * canvas.height;
+ p.vx = (Math.random() - 0.5) * 0.1;
+ p.vy = (Math.random() - 0.5) * 0.1;
+ p.life = p.initialLife;
+ p.trail = [];
+ p.isPrimordial = true;
+ p.primordialTime = 0;
+ }
+ });
+ }
+
+ function animateIntro() {
+ drawIntroPattern();
+ time++;
+ if (canvas.isConnected) {
+ requestAnimationFrame(animateIntro);
+ }
+ }
+
+ if (!canvas.dataset.animationRunningIntro) {
+ canvas.dataset.animationRunningIntro = 'true';
+ animateIntro();
+ }
+ },
+
+ // Color scheme placeholder (fallback)
+ renderColorPlaceholder: function(swatchDiv, schemeName) {
+ swatchDiv.style.background = 'linear-gradient(to bottom, #333333, #666666, #999999, #cccccc)';
+
+ // Add text
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = schemeName;
+ nameSpan.style.position = 'absolute';
+ nameSpan.style.top = '50%';
+ nameSpan.style.left = '50%';
+ nameSpan.style.transform = 'translate(-50%, -50%)';
+ nameSpan.style.color = 'white';
+ nameSpan.style.textShadow = '1px 1px 1px black';
+ nameSpan.style.fontSize = '10px';
+
+ swatchDiv.appendChild(nameSpan);
+ }
+ };
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
+ // Make it async call immediately
+ (async () => { await window.NoiseVisualizer._preloadKofiImage(); })();
+ } else {
+ document.addEventListener('DOMContentLoaded', async () => {
+ await window.NoiseVisualizer._preloadKofiImage();
+ });
+ }
+}
\ No newline at end of file
diff --git a/web/shader_params_save_button.js b/web/shader_params_save_button.js
new file mode 100644
index 0000000..bbf612a
--- /dev/null
+++ b/web/shader_params_save_button.js
@@ -0,0 +1,420 @@
+// Add a button to manually save shader parameters to file
+import { app } from "../../scripts/app.js";
+
+app.registerExtension({
+ name: "ShaderParamsSaveButton",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "ShaderNoiseKSampler") {
+ // Store original onNodeCreated to maintain the node's behavior
+ const origOnNodeCreated = nodeType.prototype.onNodeCreated;
+
+ // Add our button to the node
+ nodeType.prototype.onNodeCreated = function() {
+ // Call original function to preserve existing behavior
+ if (origOnNodeCreated) {
+ origOnNodeCreated.apply(this, arguments);
+ }
+
+ // Keep track of the node instance
+ const node = this;
+
+ // Track if we're currently saving (to prevent double-clicks)
+ let isSaving = false;
+
+ // Add indicator widget that will show when parameters need saving
+ // Create a custom widget object instead of using the built-in text widget
+ const indicatorWidget = {
+ name: "โ ๏ธ Parameters with ๐ must be saved โ ๏ธ",
+ type: "custom_indicator",
+ value: "",
+ options: { className: "shader-params-indicator" },
+ tooltip: "parameters marked with ๐ require saving to take effect in the generation process",
+ disabled: true,
+ // Add computeSize method to properly handle resizing
+ computeSize: function() {
+ // Return fixed height but variable width based on parent node width
+ if (this.parent && this.parent.size) {
+ // Adjust width to match node width with some padding
+ return [this.parent.size[0] - 30, 20];
+ }
+ return [220, 28]; // Default size if parent not available
+ },
+ // Custom draw method for the widget
+ draw: function(ctx, node, widget_width, y, widget_height) {
+ if (!ctx) return;
+
+ // Draw background
+ ctx.fillStyle = "rgba(255, 119, 0, 0.1)";
+ ctx.strokeStyle = "#ff7700";
+ ctx.lineWidth = 1;
+
+ // Draw rounded rectangle for the widget background
+ const radius = 4;
+ const x = 15; // Padding from left edge
+ const width = widget_width - 30; // Subtract padding from both sides
+
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + widget_height - radius);
+ ctx.quadraticCurveTo(x + width, y + widget_height, x + width - radius, y + widget_height);
+ ctx.lineTo(x + radius, y + widget_height);
+ ctx.quadraticCurveTo(x, y + widget_height, x, y + widget_height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+
+ ctx.fill();
+ ctx.stroke();
+
+ // Draw text
+ ctx.fillStyle = "#ff7700";
+ ctx.font = "bold 12px Arial";
+ ctx.textAlign = "center";
+ ctx.fillText(this.name, x + width / 2, y + widget_height / 2 + 4);
+
+ return widget_height;
+ }
+ };
+
+ // Add widget reference to node
+ indicatorWidget.parent = this;
+ this.widgets.push(indicatorWidget);
+
+ // --- Refactored Save Function ---
+ const saveParameters = () => {
+ if (isSaving) return;
+ isSaving = true;
+
+ // Change button text to indicate saving
+ if (saveButtonWidget) saveButtonWidget.name = "Saving...";
+ console.log("Starting save process (triggered)...");
+
+ // Print each widget and its properties for deep debugging
+ if (node.widgets) {
+ console.log("Widget details:");
+ node.widgets.forEach((widget, index) => {
+ console.log(`Widget ${index}: name="${widget.name}", type=${widget.type}, value=${widget.value}`);
+ });
+ }
+
+ // Find the widgets that contain our values
+ // Default values
+ let shaderScale = 1.0;
+ let shaderOctaves = 1;
+ let shaderWarpStrength = 0.5;
+ let shaderShapeStrength = 1.0;
+ let shaderPhaseShift = 0.5;
+ let shaderColorIntensity = 0.8;
+ let shaderType = "tensor_field";
+ let shaderShapeType = "none";
+ let colorScheme = "none";
+
+ // Try to find widgets by partial name match
+ if (node.widgets) {
+ for (const widget of node.widgets) {
+ // Ensure widget and widget.name exist before accessing
+ if (!widget || typeof widget.name !== 'string') continue;
+
+ const name = widget.name.toLowerCase();
+
+ // Use partial name matching for more flexibility
+ if (name.includes("scale") && !name.includes("color")) {
+ shaderScale = widget.value;
+ console.log(`Found shader scale: ${widget.value}`);
+ }
+ else if (name.includes("octaves")) {
+ shaderOctaves = widget.value;
+ console.log(`Found octaves: ${widget.value}`);
+ }
+ else if (name.includes("warp")) {
+ shaderWarpStrength = widget.value;
+ console.log(`Found warp strength: ${widget.value}`);
+ }
+ else if (name.includes("shape") && name.includes("strength")) {
+ shaderShapeStrength = widget.value;
+ console.log(`Found shape strength: ${widget.value}`);
+ }
+ else if (name.includes("phase")) {
+ shaderPhaseShift = widget.value;
+ console.log(`Found phase: ${widget.value}`);
+ }
+ else if (name.includes("color") && name.includes("intensity")) {
+ shaderColorIntensity = widget.value;
+ console.log(`Found color intensity: ${widget.value}`);
+ }
+ else if (name.includes("shader") && name.includes("type")) {
+ shaderType = widget.value;
+ console.log(`Found shader type: ${widget.value}`);
+ }
+ else if (name.includes("shape") && name.includes("type")) {
+ shaderShapeType = widget.value;
+ console.log(`Found shape type: ${widget.value}`);
+ }
+ else if (name.includes("color") && name.includes("scheme")) {
+ colorScheme = widget.value;
+ console.log(`Found color scheme: ${widget.value}`);
+ }
+ }
+ }
+
+ // Create the properties object with the gathered values
+ const currentProps = {
+ shaderType: shaderType,
+ shaderScale: shaderScale,
+ shaderOctaves: shaderOctaves,
+ shaderWarpStrength: shaderWarpStrength,
+ shaderShapeType: shaderShapeType,
+ shaderShapeStrength: shaderShapeStrength,
+ shaderPhaseShift: shaderPhaseShift,
+ colorScheme: colorScheme,
+ shaderColorIntensity: shaderColorIntensity
+ };
+
+ console.log("Saving shader properties:", currentProps);
+
+ try {
+ // Serialize with pretty printing
+ const jsonData = JSON.stringify(currentProps, null, 2);
+
+ // Save to localStorage as well for redundancy
+ try {
+ localStorage.setItem('shader_params', jsonData);
+ console.log("Saved to localStorage successfully");
+ } catch (localErr) {
+ console.error("Failed to save to localStorage:", localErr);
+ }
+
+ // Send the file directly to the data directory - most reliable method
+ // Create a simple download link to trigger the file save
+ const blob = new Blob([jsonData], {type: 'application/json'});
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'shader_params.json';
+ document.body.appendChild(a);
+ a.click();
+
+ // Show instructions in console AFTER download is triggered
+ console.log("%cIMPORTANT: Please save the downloaded file to the following location (overwrite if exists):", "color: red; font-weight: bold");
+ console.log("%ccustom_nodes/ComfyUI-ShaderNoiseKsampler/data/shader_params.json", "color: blue; font-weight: bold");
+
+ // Clean up and update button state on success
+ setTimeout(() => {
+ try {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ console.log("Shader params JSON download link removed.");
+ } catch (cleanupError) {
+ console.warn("Could not clean up download link:", cleanupError);
+ }
+
+ // if (saveButtonWidget) saveButtonWidget.name = "โ Parameters Not Saved";
+ setTimeout(() => {
+ if (saveButtonWidget) saveButtonWidget.name = "๐พ Save Shader Parameters";
+ isSaving = false;
+ }, 2000);
+ }, 500);
+
+ } catch (error) {
+ console.error("Error saving shader parameters:", error);
+ if (saveButtonWidget) saveButtonWidget.name = "Error Saving!";
+ setTimeout(() => {
+ if (saveButtonWidget) saveButtonWidget.name = "๐พ Save Shader Parameters";
+ isSaving = false;
+ }, 3000);
+ }
+ }; // --- End of Refactored Save Function ---
+
+ // Add save button using the refactored function
+ const saveButtonWidget = this.addWidget("button", " ๐พ Save Shader Parameters", null, saveParameters);
+
+ // Extend the widget with a tooltip property that ComfyUI's system recognizes
+ saveButtonWidget.options = saveButtonWidget.options || {};
+ saveButtonWidget.options.className = "save-shader-params";
+
+ // Add tooltip following ComfyUI's pattern
+ saveButtonWidget.tooltip = "Save parameters (Alt+S) -- file must be named shader_params.json -- always overwrite old file -- [Save location: `custom_nodes/ComfyUI-ShaderNoiseKsampler/data/shader_params.json`][WIP]";
+
+ // Move both the indicator and save button widgets to the end of all widgets
+ // This ensures they appear at the bottom of all settings
+ setTimeout(() => {
+ if (this.widgets && this.widgets.length > 0) {
+ // Get the indicators's current index
+ const indicatorIndex = this.widgets.indexOf(indicatorWidget);
+ if (indicatorIndex !== -1) {
+ // Remove it from its current position
+ this.widgets.splice(indicatorIndex, 1);
+ }
+
+ // Get the save button's current index
+ const buttonIndex = this.widgets.indexOf(saveButtonWidget);
+ if (buttonIndex !== -1) {
+ // Remove it from its current position
+ this.widgets.splice(buttonIndex, 1);
+ }
+
+ // Add them back at the end, indicator first then save button
+ this.widgets.push(indicatorWidget);
+ this.widgets.push(saveButtonWidget);
+
+ // Ensure the node is redrawn to show the updated widget positions
+ this.setDirtyCanvas(true, true);
+ }
+ }, 100);
+
+ // --- Keybinding Logic ---
+ const handleKeyDown = (event) => {
+ // Check for Alt+S and if this node is currently selected
+ if (event.altKey && event.key === 's') {
+ // Check if the graph canvas and selected nodes exist
+ if (app.canvas && app.canvas.current_node) {
+ // Check if the currently selected node is this node
+ if (app.canvas.current_node === node) {
+ console.log("Alt+S detected for selected ShaderNoiseKSampler node.");
+ event.preventDefault(); // Prevent browser's default Alt+S action
+ event.stopPropagation(); // Stop event from bubbling up
+ saveParameters(); // Trigger the save function
+ }
+ } else if (app.canvas && app.canvas.selected_nodes && Object.keys(app.canvas.selected_nodes).length === 1 && app.canvas.selected_nodes[node.id]) {
+ // Fallback check for selected_nodes if current_node isn't reliable
+ console.log("Alt+S detected for selected ShaderNoiseKSampler node (using selected_nodes).");
+ event.preventDefault();
+ event.stopPropagation();
+ saveParameters();
+ }
+ }
+ };
+
+ // Attach the event listener to the document
+ document.addEventListener('keydown', handleKeyDown);
+
+ // Store handler reference for removal
+ this.handleKeyDown = handleKeyDown;
+
+ // Original onRemoved method if it exists
+ const origOnRemoved = this.onRemoved;
+
+ // Add logic to remove the event listener when the node is removed
+ this.onRemoved = function() {
+ console.log("Removing keydown listener for node:", this.id);
+ document.removeEventListener('keydown', this.handleKeyDown);
+
+ // Call original onRemoved if it existed
+ if (origOnRemoved) {
+ origOnRemoved.apply(this, arguments);
+ }
+ };
+ // --- End Keybinding Logic ---
+ };
+ }
+ },
+ // Add CSS styling for the button
+ async setup(app) {
+ // Add a small CSS rule for spacing and tooltip styling
+ const style = document.createElement("style");
+ style.textContent = `
+ .save-shader-params {
+ padding: 6px;
+ background-color: #5c5c5c;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+
+ .shader-params-indicator {
+ padding: 4px;
+ font-weight: bold;
+ color: #ff7700;
+ background-color: rgba(255, 119, 0, 0.1);
+ border-left: 3px solid #ff7700;
+ border-radius: 2px;
+ text-align: center;
+ pointer-events: none;
+ user-select: none;
+ }
+
+ /* The tooltip container needs positioning */
+ .tooltip-container {
+ position: relative;
+ display: inline-block;
+ }
+
+ /* Style for ComfyUI-compatible tooltips */
+ .comfy-tooltip {
+ visibility: hidden;
+ background-color: rgba(40, 40, 40, 0.95);
+ color: #fff;
+ text-align: center;
+ padding: 8px;
+ border-radius: 6px;
+ position: absolute;
+ z-index: 1000;
+ bottom: 125%;
+ left: 50%;
+ transform: translateX(-50%);
+ white-space: nowrap;
+ font-size: 12px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
+ }
+
+ /* Show tooltip on hover */
+ .tooltip-container:hover .comfy-tooltip {
+ visibility: visible;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Hook into the app's widget drawing system to add tooltip support
+ // This is done after initial setup to ensure proper integration
+ const originalDrawNodeWidgets = LGraphCanvas.prototype.drawNodeWidgets;
+ if (originalDrawNodeWidgets) {
+ LGraphCanvas.prototype.drawNodeWidgets = function(node, pos, ctx, active_widget) {
+ // Call the original method first
+ const result = originalDrawNodeWidgets.call(this, node, pos, ctx, active_widget);
+
+ // After rendering widgets, check for our tooltip property
+ if (node && node.widgets) {
+ for (const widget of node.widgets) {
+ // If widget has our tooltip property and mouse is over it
+ if (widget === active_widget && widget.tooltip) {
+ // Get canvas position
+ const rect = this.canvas.getBoundingClientRect();
+
+ // Create tooltip if it doesn't exist yet
+ if (!widget._tooltip_elem) {
+ const tooltipContainer = document.createElement('div');
+ tooltipContainer.className = 'tooltip-container';
+
+ const tooltip = document.createElement('span');
+ tooltip.className = 'comfy-tooltip';
+ tooltip.textContent = widget.tooltip;
+
+ tooltipContainer.appendChild(tooltip);
+ document.body.appendChild(tooltipContainer);
+
+ widget._tooltip_elem = tooltipContainer;
+ }
+
+ // Position tooltip
+ if (widget._tooltip_elem) {
+ const x = pos[0] + rect.left;
+ const y = pos[1] + rect.top;
+
+ widget._tooltip_elem.style.left = x + 'px';
+ widget._tooltip_elem.style.top = y - 20 + 'px';
+ widget._tooltip_elem.style.display = 'block';
+ }
+ } else if (widget._tooltip_elem) {
+ // Hide tooltip when not hovering
+ widget._tooltip_elem.style.display = 'none';
+ }
+ }
+ }
+
+ return result;
+ };
+ }
+ }
+});
\ No newline at end of file
diff --git a/web/shader_renderer.js b/web/shader_renderer.js
new file mode 100644
index 0000000..fcfc2d4
--- /dev/null
+++ b/web/shader_renderer.js
@@ -0,0 +1,2395 @@
+// shader_renderer.js - Adds shader visualization to ShaderNoiseKSampler node
+// This file has a single responsibility: rendering shaders using a canvas approach
+
+import { app } from "../../scripts/app.js";
+
+//-------------------------------------------------------------------------
+// HELPER FUNCTIONS
+//-------------------------------------------------------------------------
+// Helper function to get color scheme name from string representation
+function getColorSchemeName(scheme) {
+ const schemeNames = {
+ "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"
+ };
+ return schemeNames[scheme] || scheme;
+}
+
+// Register a callback to run when ComfyUI is fully loaded
+app.registerExtension({
+ name: "ShaderNoiseKSampler.ShaderRenderer",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // Apply only to the shader noise sampler node
+ if (nodeData.name === "ShaderNoiseKSampler") {
+ // Store the original onNodeCreated function if it exists
+ const origOnNodeCreated = nodeType.prototype.onNodeCreated;
+
+ // Add our own onNodeCreated function
+ nodeType.prototype.onNodeCreated = function() {
+ // Call the original onNodeCreated if it exists
+ if (origOnNodeCreated) {
+ origOnNodeCreated.apply(this, arguments);
+ }
+
+ // Add shader properties
+ this.properties = this.properties || {};
+ this.properties.shaderVisible = false;
+ this.properties.tooltipsVisible = true; // Add new property for tooltips visibility
+ 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; // 0=all, 1=low, 2=mid, 3=high, 4=custom
+ this.properties.shaderDistribution = 0; // 0=cauchy, 1=laplace
+ this.properties.shaderAdaptationStrength = 0.5;
+ this.properties.shaderResolutionScale = 512; // Medium resolution
+
+ // Store the shader height
+ this.shaderHeight = 200;
+
+ // Track animation state
+ this.animationFrameId = null;
+ this.isShaderActive = false;
+
+ // Initialize shader canvas once
+ this.initShaderCanvas();
+
+ // Custom widget for shader rendering
+ this.addWidget("toggle", "Show Shader", this.properties.shaderVisible, (v) => {
+ this.properties.shaderVisible = v;
+
+ // Update node size
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ if (v && this.gl) {
+ // If turning on, calculate shader height for current node size
+ if (!this.shaderHeight || this.shaderHeight < 50) {
+ this.shaderHeight = 200; // Default height
+ }
+
+ // Update node size to include shader
+ this.size[1] = baseHeight + this.shaderHeight;
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+
+ // Activate shader
+ this.isShaderActive = true;
+
+ // Reset render time to avoid jumps in animation
+ this.properties.lastRenderTime = 0;
+ } else {
+ // If turning off, remove shader height
+ this.size[1] = baseHeight;
+
+ // Deactivate shader
+ this.isShaderActive = false;
+
+ // Cancel any pending animation frame
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Toggle the visibility of the shader preview visualization [visual aid to help you see the shader noise pattern before it is applied for sampling]";
+
+ // Add toggle for tooltips visibility
+ this.addWidget("toggle", "Show Tooltips", this.properties.tooltipsVisible, (v) => {
+ this.properties.tooltipsVisible = v;
+
+ // Apply tooltip visibility to all widgets
+ for (let i = 0; i < this.widgets.length; i++) {
+ // Skip the Show Tooltips widget itself to always show its tooltip
+ if (this.widgets[i].name === "Show Tooltips") continue;
+
+ // Store original tooltip if not already stored
+ if (!this.widgets[i]._originalTooltip && this.widgets[i].tooltip) {
+ this.widgets[i]._originalTooltip = this.widgets[i].tooltip;
+ }
+
+ // Set or clear tooltip based on visibility setting
+ if (v && this.widgets[i]._originalTooltip) {
+ this.widgets[i].tooltip = this.widgets[i]._originalTooltip;
+ } else {
+ this.widgets[i].tooltip = "";
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Toggle the visibility of tooltips for all shader parameters [disable if tooltips get in the way of controls]";
+
+ // Updated shader noise type combo with all the new types
+ this.addWidget("combo", "Shader Noise Type ๐", this.properties.shaderType, (v) => {
+ this.properties.shaderType = v;
+
+ // Load the shader if it's not already loaded and shader is active
+ if (this.loadShader && (this.isShaderActive || this.properties.shaderVisible)) {
+ this.loadShader(v);
+ }
+
+ this.setDirtyCanvas(true, true);
+ }, {
+ values: [
+ "domain_warp", "tensor_field", "curl_noise"
+ ]
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Select the type of shader noise pattern to use in the visualization [different types have different characteristic outputs]";
+
+ // Add shape mask type widget
+ 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"]
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Apply a shape mask to the shader noise pattern to create more complex structures [not post processing - is applied to shader noise pattern before rendering]";
+
+ // Add color scheme widget
+ this.properties.colorScheme = this.properties.colorScheme || "none";
+ 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"
+ ]
+ });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Choose a color palette to apply to the shader noise visualization [not post processing - is applied to the shader noise pattern before rendering]";
+
+ 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 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Adjust the scale of the shader noise pattern - lower values create larger, zoomed-in features; higher values create smaller, zoomed-out features [small value shifts can lead to larger variations]";
+
+ 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 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Number of shader noise layers to combine - higher values add more detail and complexity [small value shifts can lead to larger variations]";
+
+ 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 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Control how much the shader noise pattern warps and distorts - higher values create more swirling or complex transformations [small adjustments are good for subtle variations]";
+
+ 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 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Adjust the intensity of the shape mask's effect on the shader noise pattern - higher values make the shape more prominent [small adjustments are good for subtle variations - not effective without shape mask]";
+
+ 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 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Shift the phase of the shader noise pattern to create different variations or animate patterns over time [small adjustments are good for subtle variations]";
+
+ 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 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Adjust the intensity of the color scheme application - lower values are more desaturated, higher values are more vibrant [small adjustments are good for subtle variations - not effective without color scheme]";
+
+ 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 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Control how quickly the shader noise pattern animates in the preview [UI effect only - not used in final image generation]";
+
+ // Add resolution quality slider
+ this.addWidget("slider", "Pixel Resolution ๐ฅ๏ธ", this.properties.shaderResolutionScale, (v) => {
+ this.properties.shaderResolutionScale = v;
+ // Trigger canvas resize to apply the new resolution
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+ this.setDirtyCanvas(true, true);
+ }, { min: 128, max: 1024, step: 1, precision: 0 });
+ // Set tooltip directly on the widget
+ this.widgets[this.widgets.length-1].tooltip = "Set the resolution of the shader preview - higher values increase quality but require more processing power [UI effect only - not used in final image generation]";
+
+ // Ensure initial tooltip visibility is correctly applied and _originalTooltip is captured
+ const initialTooltipsStateIsOn = this.properties.tooltipsVisible;
+ for (let i = 0; i < this.widgets.length; i++) {
+ const widget = this.widgets[i];
+
+ if (widget.name === "Show Tooltips") {
+ // The "Show Tooltips" widget's tooltip is managed by its own definition
+ // and should always be visible.
+ continue;
+ }
+
+ // Capture the hardcoded tooltip text as _originalTooltip.
+ // This ensures _originalTooltip always reflects the intended full tooltip text.
+ if (widget.tooltip && typeof widget.tooltip === 'string' && widget.tooltip.trim() !== "") {
+ widget._originalTooltip = widget.tooltip;
+ } else {
+ widget._originalTooltip = ""; // Mark as having no original tooltip if none was defined.
+ }
+
+ // Set the currently visible tooltip based on the initial state.
+ if (initialTooltipsStateIsOn && widget._originalTooltip) {
+ widget.tooltip = widget._originalTooltip;
+ } else {
+ widget.tooltip = ""; // Hide tooltip if initially off or no original text.
+ }
+ }
+
+ // Make the node resizable
+ this.resizable = true;
+
+ // Set minimum size for the node to prevent it from becoming too small
+ this.min_height = 100; // Minimum height including the shader area
+ this.min_width = 300; // Minimum width
+
+ // Add a method to handle node resize
+ nodeType.prototype.onResize = function(size) {
+ // Set flag to indicate we're in a resize operation
+ this._isResizing = true;
+
+ // Enforce minimum size constraints
+ size[0] = Math.max(this.min_width || 300, size[0]);
+
+ // Get base height without shader area
+ const baseSize = origComputeSize ? origComputeSize.call(this, [size[0], 0]) : [size[0], 0];
+ const baseHeight = baseSize[1];
+
+ // Calculate minimum required height
+ const minTotalHeight = baseHeight + 50; // base + minimum shader height
+ size[1] = Math.max(minTotalHeight, size[1]);
+
+ // Calculate new shader height based on requested size
+ if (this.properties.shaderVisible) {
+ // Update shader height based on the requested node size
+ this.shaderHeight = Math.max(50, size[1] - baseHeight);
+
+ // Resize the shader canvas to match new dimensions
+ this.resizeShaderCanvas(size[0], this.shaderHeight);
+ }
+
+ // Update node size
+ this.size = size;
+
+ // Clear the resize flag
+ this._isResizing = false;
+
+ // Tell the system to recalculate positions
+ this.setDirtyCanvas(true, true);
+ };
+
+ // Method to resize the shader canvas
+ nodeType.prototype.resizeShaderCanvas = function(width, height) {
+ if (!this.gl || !this.shaderCanvas) return;
+
+ // Ensure minimum canvas height
+ height = Math.max(50, height);
+
+ // Calculate aspect ratio
+ const aspectRatio = width / height;
+
+ // Get target resolution (square resolution that maintains aspect ratio)
+ const targetRes = Math.round(this.properties.shaderResolutionScale);
+
+ // Calculate width and height at target resolution, maintaining aspect ratio
+ let canvasWidth, canvasHeight;
+ if (aspectRatio >= 1) {
+ // Wider than tall
+ canvasWidth = targetRes;
+ canvasHeight = Math.round(targetRes / aspectRatio);
+ } else {
+ // Taller than wide
+ canvasHeight = targetRes;
+ canvasWidth = Math.round(targetRes * aspectRatio);
+ }
+
+ // Set internal canvas resolution (actual pixels)
+ this.shaderCanvas.width = canvasWidth;
+ this.shaderCanvas.height = canvasHeight;
+
+ // Set displayed size via CSS (this is what the user sees)
+ this.shaderCanvas.style.width = width + "px";
+ this.shaderCanvas.style.height = height + "px";
+
+ // Update WebGL viewport to match internal resolution
+ this.gl.viewport(0, 0, canvasWidth, canvasHeight);
+
+ // Store the display dimensions
+ this.displayWidth = width;
+ this.displayHeight = height;
+
+ // Update shader height property to match the passed height
+ // This ensures all parts of the code are using the same value
+ this.shaderHeight = height;
+ };
+ };
+
+ // Override getExtraMenuOptions to add shader visibility toggle
+ const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
+ nodeType.prototype.getExtraMenuOptions = function(canvas, options) {
+ // Call original if it exists
+ if (origGetExtraMenuOptions) {
+ origGetExtraMenuOptions.call(this, canvas, options);
+ }
+
+ // Add toggle for shader visibility
+ const toggleShaderPreview = {
+ content: this.properties.shaderVisible ? "Hide Shader Preview" : "Show Shader Preview",
+ callback: () => {
+ // Toggle shader visibility
+ this.properties.shaderVisible = !this.properties.shaderVisible;
+
+ // Update the shader toggle widget to match
+ const shaderWidget = this.widgets?.find(w => w.name === "Show Shader");
+ if (shaderWidget) {
+ shaderWidget.value = this.properties.shaderVisible;
+ }
+
+ // Update node size
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ if (this.properties.shaderVisible && this.gl) {
+ // If turning on, calculate shader height for current node size
+ if (!this.shaderHeight || this.shaderHeight < 50) {
+ this.shaderHeight = 200; // Default height
+ }
+
+ // Update node size to include shader
+ this.size[1] = baseHeight + this.shaderHeight;
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+
+ // Activate shader
+ this.isShaderActive = true;
+
+ // Reset render time to avoid jumps in animation
+ this.properties.lastRenderTime = 0;
+ } else {
+ // If turning off, remove shader height
+ this.size[1] = baseHeight;
+
+ // Deactivate shader
+ this.isShaderActive = false;
+
+ // Cancel any pending animation frame
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ }
+ };
+
+ // Add toggle for tooltips visibility
+ const toggleTooltips = {
+ content: this.properties.tooltipsVisible ? "Hide Tooltips" : "Show Tooltips",
+ callback: () => {
+ // Toggle tooltips visibility
+ this.properties.tooltipsVisible = !this.properties.tooltipsVisible;
+
+ // Update the tooltips toggle widget to match
+ const tooltipsWidget = this.widgets?.find(w => w.name === "Show Tooltips");
+ if (tooltipsWidget) {
+ tooltipsWidget.value = this.properties.tooltipsVisible;
+
+ // Trigger the widget's callback to update all tooltips
+ if (tooltipsWidget.callback) {
+ tooltipsWidget.callback(this.properties.tooltipsVisible);
+ }
+ }
+
+ this.setDirtyCanvas(true, true);
+ }
+ };
+
+ // Add resolution quality submenu
+ const resolutionLevels = [
+ ["Very Low (128px)", 128],
+ ["Low (256px)", 256],
+ ["Medium (512px)", 512],
+ ["High (768px)", 768],
+ ["Very High (1024px)", 1024]
+ ];
+
+ const resolutionSubmenu = {
+ content: "Pixel Resolution",
+ submenu: {
+ options: resolutionLevels.map(([label, value]) => ({
+ content: label,
+ callback: () => {
+ // Update resolution quality property
+ this.properties.shaderResolutionScale = value;
+
+ // Update the resolution quality widget to match
+ const resolutionWidget = this.widgets?.find(w => w.name === "Pixel Resolution");
+ if (resolutionWidget) {
+ resolutionWidget.value = value;
+ }
+
+ // Trigger canvas resize to apply the new resolution
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: Math.abs(this.properties.shaderResolutionScale - value) < 1.0
+ }))
+ }
+ };
+
+ // Add shader noise type selection submenu
+ const shaderTypes = [
+ ["Domain Warping", "domain_warp"],
+ ["Tensor Field", "tensor_field"],
+ ["Curl Noise", "curl_noise"],
+ ];
+
+ const shaderTypeSubmenu = {
+ content: "Shader Noise Type",
+ submenu: {
+ options: shaderTypes.map(([label, type]) => ({
+ content: label,
+ callback: () => {
+ // Update shader noise type property
+ this.properties.shaderType = type;
+
+ // Update the shader noise type widget to match
+ const typeWidget = this.widgets?.find(w => w.name === "Shader Noise Type");
+ if (typeWidget) {
+ typeWidget.value = type;
+ }
+
+ // Load the shader if it's not already loaded and shader is active
+ if (this.loadShader && (this.isShaderActive || this.properties.shaderVisible)) {
+ this.loadShader(type);
+ }
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: this.properties.shaderType === type
+ }))
+ }
+ };
+
+ // Add shape mask type selection submenu
+ const shapeTypes = [
+ ["None", "none", 0],
+ ["Radial", "radial", 1],
+ ["Linear", "linear", 2],
+ ["Spiral", "spiral", 3],
+ ["Checkerboard", "checkerboard", 4],
+ ["Spots", "spots", 5],
+ ["Hex Grid", "hexgrid", 6],
+ ["Stripes", "stripes", 7],
+ ["Gradient", "gradient", 8],
+ ["Vignette", "vignette", 9],
+ ["Cross", "cross", 10],
+ ["Stars", "stars", 11],
+ ["Triangles", "triangles", 12],
+ ["Concentric", "concentric", 13],
+ ["Rays", "rays", 14],
+ ["ZigZag", "zigzag", 15]
+ ];
+
+ const shapeTypeSubmenu = {
+ content: "Shape Mask Type",
+ submenu: {
+ options: shapeTypes.map(([label, type, index]) => ({
+ content: label,
+ callback: () => {
+ // Update shape mask type property
+ this.properties.shaderShapeType = type;
+
+ // Update the shape mask type widget to match
+ const shapeWidget = this.widgets?.find(w => w.name === "Shape Mask Type");
+ if (shapeWidget) {
+ shapeWidget.value = type;
+ }
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: this.properties.shaderShapeType === type
+ }))
+ }
+ };
+
+ // Add color scheme selection submenu
+ const colorSchemes = [
+ ["Black & White", "none"],
+ ["Blue to Red", "blue_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"]
+ ];
+
+ const colorSchemeSubmenu = {
+ content: "Color Scheme",
+ submenu: {
+ options: colorSchemes.map(([label, scheme]) => ({
+ content: label,
+ callback: () => {
+ // Update color scheme property
+ this.properties.colorScheme = scheme;
+
+ // Update the color scheme widget to match
+ const colorSchemeWidget = this.widgets?.find(w => w.name === "Color Scheme");
+ if (colorSchemeWidget) {
+ colorSchemeWidget.value = scheme;
+ }
+
+ // Redraw canvas
+ this.setDirtyCanvas(true, true);
+ },
+ checked: this.properties.colorScheme === scheme
+ }))
+ }
+ };
+
+ options.push(null, toggleShaderPreview, toggleTooltips, shaderTypeSubmenu, shapeTypeSubmenu, colorSchemeSubmenu, resolutionSubmenu);
+
+ return options;
+ };
+
+ // Override onConfigure to ensure widgets reflect loaded properties
+ const origOnConfigure = nodeType.prototype.onConfigure;
+ nodeType.prototype.onConfigure = function(info) {
+ if (origOnConfigure) {
+ origOnConfigure.apply(this, arguments);
+ }
+
+ // Ensure widgets display the correct values after loading from JSON
+ if (this.widgets && this.properties) {
+ const updateWidgetValue = (widgetName, propertyName) => {
+ const widget = this.widgets.find(w => w.name === widgetName);
+ if (widget && this.properties[propertyName] !== undefined) {
+ // Check if the value exists in the widget's options (for combos)
+ if (widget.options?.values && !widget.options.values.includes(this.properties[propertyName])) {
+ console.warn(`Value "${this.properties[propertyName]}" for property "${propertyName}" not found in options for widget "${widgetName}". Using default or first option.`);
+ // Optionally set to default or first value if invalid
+ // widget.value = widget.options.values[0];
+ } else {
+ widget.value = this.properties[propertyName];
+ }
+ }
+ };
+
+ updateWidgetValue("Show Shader", "shaderVisible");
+ updateWidgetValue("Show Tooltips", "tooltipsVisible");
+ updateWidgetValue("Shader Noise Type ๐", "shaderType");
+ updateWidgetValue("Shape Mask Type ๐", "shaderShapeType");
+ updateWidgetValue("Color Scheme ๐", "colorScheme");
+ updateWidgetValue("Noise Scale ๐", "shaderScale");
+ updateWidgetValue("Octaves ๐", "shaderOctaves");
+ updateWidgetValue("Warp Strength ๐", "shaderWarpStrength");
+ updateWidgetValue("Shape Mask Strength ๐", "shaderShapeStrength");
+ updateWidgetValue("Phase Shift ๐", "shaderPhaseShift");
+ updateWidgetValue("Animation Speed ๐ฅ๏ธ", "shaderSpeed");
+ updateWidgetValue("Color Intensity ๐", "shaderColorIntensity");
+ updateWidgetValue("Pixel Resolution ๐ฅ๏ธ", "shaderResolutionScale");
+ // Add any other widgets that need synchronization here
+
+ // After updating widget values, if the tooltips widget exists and has a callback,
+ // call it to ensure tooltip visibility is correctly applied based on the loaded state.
+ const tooltipsWidget = this.widgets.find(w => w.name === "Show Tooltips");
+ if (tooltipsWidget && tooltipsWidget.callback) {
+ // Ensure the widget.value (this.properties.tooltipsVisible) is up-to-date before calling back
+ // The updateWidgetValue call above should have handled this.
+ tooltipsWidget.callback(tooltipsWidget.value);
+ }
+ }
+
+ // If the shader should be visible based on loaded properties, ensure size is correct
+ if (this.properties?.shaderVisible) {
+ // Calculate base height
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ // Ensure shader height is valid
+ if (!this.shaderHeight || this.shaderHeight < 50) {
+ this.shaderHeight = 200; // Default height if needed
+ }
+
+ // Update node size if necessary
+ const expectedHeight = baseHeight + this.shaderHeight;
+ if (Math.abs(this.size[1] - expectedHeight) > 1) { // Use tolerance for float comparison
+ this.size[1] = expectedHeight;
+ // Also resize the canvas to match the potentially updated shaderHeight
+ this.resizeShaderCanvas(this.size[0], this.shaderHeight);
+ this.setDirtyCanvas(true, true);
+ }
+
+ // Ensure shader is active if it's supposed to be visible
+ this.isShaderActive = true;
+ } else {
+ // Ensure shader is inactive if not visible
+ this.isShaderActive = false;
+ }
+ };
+
+ // Initialize WebGL for shader rendering
+ nodeType.prototype.initShaderCanvas = function() {
+ // Create a canvas for shader rendering
+ this.shaderCanvas = document.createElement('canvas');
+ this.shaderCanvas.width = this.size ? this.size[0] : 250;
+ this.shaderCanvas.height = this.shaderHeight;
+
+ // Get WebGL context
+ this.gl = this.shaderCanvas.getContext('webgl');
+ if (!this.gl) {
+ console.error('WebGL not supported');
+ return;
+ }
+
+ // Set the viewport to match the canvas size
+ this.gl.viewport(0, 0, this.shaderCanvas.width, this.shaderCanvas.height);
+
+ // Setup shader programs for each shader type
+ this.shaderPrograms = {};
+
+ // Create shader programs
+ const vertexShaderSource = `
+ 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
+ const fragmentShaderHeader = `
+ precision mediump float;
+ 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);
+ }
+
+ // 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) * 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) *
+ smoothstep(0.5 + thickness, 0.5 + thickness - 0.02, rotated.y);
+ float v_bar = smoothstep(0.5 - thickness, 0.5 - thickness + 0.02, rotated.x) *
+ smoothstep(0.5 + thickness, 0.5 + thickness - 0.02, 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) * 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 = normalized * 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);
+ }
+ }
+ `;
+
+ // Common fragment shader footer
+ const fragmentShaderFooter = `
+ 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);
+ }
+ `;
+
+ // Domain Warp shader
+ const domainWarpShader = `
+ // 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 shader
+ const tensorFieldShader = `
+ // 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 shader
+ const curlNoiseShader = `
+ // 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 stream = sin(dot(particlePos, normalize(velocity)) * 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 length = max(length(velocity), 0.001);
+ float logScale = log(length * 9.0 + 1.0) * warp;
+ return normalize(velocity) * 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);
+ }
+ `;
+
+ // Store shader sources for lazy loading
+ this.shaderSources = {
+ // gaussian: gaussianShader,
+ // perlin: perlinShader,
+ // cellular: cellularShader,
+ // fractal: fractalShader,
+ // waves: wavesShader,
+ domain_warp: domainWarpShader,
+ tensor_field: tensorFieldShader,
+ // heterogeneous_fbm: heterogeneousFbmShader,
+ // interference: interferenceShader,
+ // spectral: spectralShader,
+ // "3d_projection": projectionShader,
+ curl_noise: curlNoiseShader
+ };
+
+ // Initialize shader programs object
+ this.shaderPrograms = {};
+
+ // Store common shader parts
+ this.vertexShaderSource = vertexShaderSource;
+ this.fragmentShaderHeader = fragmentShaderHeader;
+ this.fragmentShaderFooter = fragmentShaderFooter;
+
+ // Track shader loading
+ this.pendingShaders = [];
+ this.loadingShader = false;
+
+ // Load only the default shader initially
+ const defaultShader = this.properties.shaderType || "domain_warp";
+ this.loadShader(defaultShader);
+
+ // Remove the background loading timer completely
+ // this.backgroundLoadTimer = setTimeout(() => {
+ // this.startBackgroundLoading();
+ // }, 2000); // Wait 2 seconds before starting background loads
+
+ // 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;
+ };
+
+ // Helper function to create shader program
+ nodeType.prototype.createShaderProgram = function(vsSource, fsSource) {
+ const gl = this.gl;
+
+ // Create shaders
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
+ gl.shaderSource(vertexShader, vsSource);
+ gl.compileShader(vertexShader);
+
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
+ gl.shaderSource(fragmentShader, fsSource);
+ gl.compileShader(fragmentShader);
+
+ // Create program
+ const program = gl.createProgram();
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+ gl.linkProgram(program);
+
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ console.error('Shader program error:', gl.getProgramInfoLog(program));
+ return null;
+ }
+
+ return program;
+ };
+
+ // Load shader on demand
+ nodeType.prototype.loadShader = function(shaderType) {
+ // Skip shader loading if inactive
+ if (!this.isShaderActive && !this.properties.shaderVisible) {
+ return;
+ }
+
+ // Check if shader is already loaded
+ if (this.shaderPrograms[shaderType]) {
+ return;
+ }
+
+ // Check if shader source exists
+ if (!this.shaderSources[shaderType]) {
+ console.error('Shader source not found:', shaderType);
+ return;
+ }
+
+ // Add to pending shaders queue if not already there
+ if (!this.pendingShaders.includes(shaderType)) {
+ this.pendingShaders.push(shaderType);
+ }
+
+ // Start loading process if not already in progress
+ if (!this.loadingShader) {
+ this.processNextShader();
+ }
+ };
+
+ // Process the next shader in the queue
+ nodeType.prototype.processNextShader = function() {
+ // Skip processing if shader is now inactive
+ if (!this.isShaderActive && !this.properties.shaderVisible) {
+ this.loadingShader = false;
+ this.pendingShaders = []; // Clear pending shaders
+ return;
+ }
+
+ if (this.pendingShaders.length === 0) {
+ this.loadingShader = false;
+ return;
+ }
+
+ this.loadingShader = true;
+ const shaderType = this.pendingShaders.shift();
+
+ // Compile the shader program
+ console.log('Loading shader:', shaderType);
+ this.shaderPrograms[shaderType] = this.createShaderProgram(
+ this.vertexShaderSource,
+ this.fragmentShaderHeader + this.shaderSources[shaderType] + this.fragmentShaderFooter
+ );
+
+ // Process next shader on next frame to avoid blocking UI
+ setTimeout(() => {
+ this.processNextShader();
+ }, 10); // Short delay to let UI breathe
+ };
+
+ // Render the current shader
+ nodeType.prototype.renderShader = function() {
+ // Skip rendering if shader is inactive or WebGL context is not available
+ if (!this.isShaderActive || !this.gl || !this.shaderPrograms) return;
+
+ const gl = this.gl;
+ const currentShaderType = this.properties.shaderType;
+
+ // Check if the current shader program is loaded
+ if (!this.shaderPrograms[currentShaderType]) {
+ // Try to use a fallback shader while loading
+ const fallbackShader = Object.keys(this.shaderPrograms)[0];
+
+ // Request loading of the current shader
+ this.loadShader(currentShaderType);
+
+ // If no shaders are loaded at all, we can't render
+ if (!fallbackShader) {
+ // Draw loading indicator (using WebGL since 2D context may not be available)
+ gl.clearColor(0.1, 0.1, 0.1, 1.0); // Dark gray background
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ // We can't use text in WebGL easily, so we'll draw a simple loading indicator
+ // using a pulsing rectangle in the center
+ const now = performance.now() / 1000;
+ const pulseSize = 0.2 + 0.1 * Math.sin(now * 3); // Pulsing size
+
+ // Create a simple program for drawing a colored rectangle
+ if (!this.loadingIndicatorProgram) {
+ const vsSource = `
+ attribute vec2 a_position;
+ void main() {
+ gl_Position = vec4(a_position, 0.0, 1.0);
+ }
+ `;
+ const fsSource = `
+ precision mediump float;
+ uniform vec4 u_color;
+ void main() {
+ gl_FragColor = u_color;
+ }
+ `;
+ this.loadingIndicatorProgram = this.createShaderProgram(vsSource, fsSource);
+ }
+
+ if (this.loadingIndicatorProgram) {
+ gl.useProgram(this.loadingIndicatorProgram);
+
+ // Set color (light blue)
+ const colorLocation = gl.getUniformLocation(this.loadingIndicatorProgram, "u_color");
+ gl.uniform4f(colorLocation, 0.2, 0.6, 1.0, 1.0);
+
+ // Draw a rectangle in the center
+ const positionLocation = gl.getAttribLocation(this.loadingIndicatorProgram, "a_position");
+ gl.enableVertexAttribArray(positionLocation);
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
+
+ // Set viewport to maintain aspect ratio
+ gl.viewport(0, 0, this.shaderCanvas.width, this.shaderCanvas.height);
+
+ // Draw the rectangle
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ }
+
+ return;
+ }
+
+ // Use fallback shader
+ const program = this.shaderPrograms[fallbackShader];
+ if (!program) return;
+
+ // Clear canvas
+ gl.clearColor(0, 0, 0, 1);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ // Update animation time
+ this.updateAnimationTime();
+
+ // Draw with fallback shader
+ this.drawShader(program);
+ return;
+ }
+
+ const program = this.shaderPrograms[currentShaderType];
+
+ // Clear canvas
+ gl.clearColor(0, 0, 0, 1);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+
+ // Update animation time
+ this.updateAnimationTime();
+
+ // Draw with current shader
+ this.drawShader(program);
+ };
+
+ // Update animation time
+ nodeType.prototype.updateAnimationTime = function() {
+ // Only update if shader is active
+ 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;
+ };
+
+ // Draw using the provided shader program
+ nodeType.prototype.drawShader = function(program) {
+ const gl = this.gl;
+
+ // Use the provided shader program
+ gl.useProgram(program);
+
+ // Set common uniform values
+ const uniformLocations = {
+ 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')
+ };
+
+ // Set all uniforms
+ gl.uniform1f(uniformLocations.time, this.properties.shaderTime);
+ gl.uniform1f(uniformLocations.intensity, this.properties.shaderColorIntensity);
+ gl.uniform1f(uniformLocations.scale, this.properties.shaderScale);
+ gl.uniform1f(uniformLocations.octaves, this.properties.shaderOctaves);
+ gl.uniform1f(uniformLocations.persistence, 0.5); // Default value
+ gl.uniform1f(uniformLocations.lacunarity, 2.0); // Default value
+
+ // Map shapeType string to integer value
+ const shapeTypeMap = {
+ '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
+ };
+ gl.uniform1i(uniformLocations.shapeType, shapeTypeMap[this.properties.shaderShapeType] || 0);
+
+ // Map colorScheme string to integer value
+ const colorSchemeMap = {
+ '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
+ };
+ // Original line had an issue with " none\ (0) being treated as falsy: // gl.uniform1i(uniformLocations.colorScheme, colorSchemeMap[this.properties.colorScheme] || 1); // Default to blue_red
+
+ // Set colorScheme - properly handle "none" (value 0) without defaulting to blue_red
+ const colorSchemeValue = colorSchemeMap[this.properties.colorScheme];
+ gl.uniform1i(uniformLocations.colorScheme, colorSchemeValue !== undefined ? colorSchemeValue : 1); // Default to blue_red only if not found
+
+ gl.uniform1f(uniformLocations.shapeStrength, this.properties.shaderShapeStrength);
+ gl.uniform1f(uniformLocations.warpStrength, this.properties.shaderWarpStrength);
+ gl.uniform1f(uniformLocations.phaseShift, this.properties.shaderPhaseShift);
+ gl.uniform1i(uniformLocations.frequencyRange, this.properties.shaderFrequencyRange);
+ gl.uniform1i(uniformLocations.distribution, this.properties.shaderDistribution);
+ gl.uniform1f(uniformLocations.adaptationStrength, this.properties.shaderAdaptationStrength);
+
+ // We no longer need to send resolution to shader since we directly control pixel density via canvas size
+ // gl.uniform1f(uniformLocations.resolutionScale, this.properties.shaderResolutionScale);
+
+ // Set up position attribute
+ const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
+ gl.enableVertexAttribArray(positionAttributeLocation);
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+ gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
+
+ // Draw
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ };
+
+ // Override onDrawForeground to render the shader
+ const origOnDrawForeground = nodeType.prototype.onDrawForeground;
+ nodeType.prototype.onDrawForeground = function(ctx) {
+ // Render shader if visible
+ if (this.properties.shaderVisible && this.gl) {
+ // Calculate where to position the shader
+ let shaderWidth, shaderHeight, shaderY;
+
+ if (this.flags.collapsed) {
+ shaderWidth = 190;
+ shaderHeight = 20;
+ shaderY = 20; // Position below the title text
+ } else {
+ // Get the height of the node without shader area
+ const baseSize = origComputeSize ? origComputeSize.call(this, [this.size[0], 0]) : [this.size[0], 0];
+ const baseHeight = baseSize[1];
+
+ // Set shader dimensions based on available space
+ shaderWidth = this.size[0];
+
+ // Use the current shaderHeight that's been set during resize
+ shaderHeight = this.shaderHeight;
+
+ // Ensure minimum height
+ if (!shaderHeight || shaderHeight < 50) {
+ shaderHeight = 50;
+ this.shaderHeight = 50;
+ }
+
+ shaderY = baseHeight;
+
+ // Resize the canvas if needed to match current dimensions
+ this.resizeShaderCanvas(shaderWidth, shaderHeight);
+ }
+
+ // Render shader to its canvas
+ this.renderShader();
+
+ // Call original onDrawForeground to draw the title
+ if (origOnDrawForeground) {
+ origOnDrawForeground.call(this, ctx);
+ }
+
+ // Save context for drawing with rounded corners
+ ctx.save();
+
+ // Define corner radius - not too large to maintain clean look
+ const cornerRadius = 8;
+
+ // Create clipping path with rounded corners for the shader display
+ if (!this.flags.collapsed) {
+ ctx.beginPath();
+ ctx.moveTo(0, shaderY + cornerRadius);
+ ctx.lineTo(0, shaderY + shaderHeight - cornerRadius);
+ ctx.arcTo(0, shaderY + shaderHeight, cornerRadius, shaderY + shaderHeight, cornerRadius);
+ ctx.lineTo(shaderWidth - cornerRadius, shaderY + shaderHeight);
+ ctx.arcTo(shaderWidth, shaderY + shaderHeight, shaderWidth, shaderY + shaderHeight - cornerRadius, cornerRadius);
+ ctx.lineTo(shaderWidth, shaderY + cornerRadius);
+ ctx.arcTo(shaderWidth, shaderY, shaderWidth - cornerRadius, shaderY, cornerRadius);
+ ctx.lineTo(cornerRadius, shaderY);
+ ctx.arcTo(0, shaderY, 0, shaderY + cornerRadius, cornerRadius);
+ ctx.closePath();
+ ctx.clip();
+ }
+
+ // Draw shader canvas
+ ctx.drawImage(
+ this.shaderCanvas,
+ 0, shaderY,
+ shaderWidth, shaderHeight
+ );
+
+ // Restore context after clipped drawing
+ ctx.restore();
+
+ // Draw a styled border with rounded corners around the shader
+ if (!this.flags.collapsed) {
+ // Save context for border drawing
+ ctx.save();
+
+ // Create path for border with rounded corners
+ ctx.beginPath();
+ ctx.moveTo(0, shaderY + cornerRadius);
+ ctx.lineTo(0, shaderY + shaderHeight - cornerRadius);
+ ctx.arcTo(0, shaderY + shaderHeight, cornerRadius, shaderY + shaderHeight, cornerRadius);
+ ctx.lineTo(shaderWidth - cornerRadius, shaderY + shaderHeight);
+ ctx.arcTo(shaderWidth, shaderY + shaderHeight, shaderWidth, shaderY + shaderHeight - cornerRadius, cornerRadius);
+ ctx.lineTo(shaderWidth, shaderY + cornerRadius);
+ ctx.arcTo(shaderWidth, shaderY, shaderWidth - cornerRadius, shaderY, cornerRadius);
+ ctx.lineTo(cornerRadius, shaderY);
+ ctx.arcTo(0, shaderY, 0, shaderY + cornerRadius, cornerRadius);
+ ctx.closePath();
+
+ // Create a subtle glow effect
+ ctx.shadowColor = "rgba(255, 255, 255, 0.2)";
+ ctx.shadowBlur = 2;
+ ctx.strokeStyle = "rgba(200, 200, 200, 0.6)";
+ ctx.lineWidth = 1;
+ ctx.stroke();
+
+ // Restore context
+ ctx.restore();
+ }
+
+ // Mark shader as active
+ this.isShaderActive = true;
+
+ // Request animation frame to keep updating - only when shader is active
+ // Store the requestAnimationFrame ID for potential cancellation
+ this.animationFrameId = requestAnimationFrame(() => {
+ // Only continue rendering if the shader is still active
+ if (this.isShaderActive && this.properties.shaderVisible) {
+ this.setDirtyCanvas(true, true);
+ }
+ });
+ } else {
+ // If shader not visible, just call original onDrawForeground
+ if (origOnDrawForeground) {
+ origOnDrawForeground.call(this, ctx);
+ }
+
+ // Ensure shader is marked as inactive
+ this.isShaderActive = false;
+
+ // Make sure no animation frame is pending
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+ }
+ };
+
+ // Override computeSize to adjust node height
+ const origComputeSize = nodeType.prototype.computeSize;
+ nodeType.prototype.computeSize = function(size) {
+ size = size || [this.size[0] || 300, 0];
+
+ // Handle collapsed state
+ if (this.flags && this.flags.collapsed) {
+ size[0] = 190; // Collapsed width
+ size[1] = 40; // Collapsed height (title + shader)
+ return size;
+ }
+
+ // Get base size without shader
+ if (origComputeSize) {
+ origComputeSize.call(this, size);
+ }
+
+ // Store the base height before adding shader height
+ const baseHeight = size[1];
+
+ // Add height for shader area if visible
+ if (this.properties && this.properties.shaderVisible) {
+ // If this is called during user resize, respect the provided size
+ // This is crucial for allowing Y-axis shrinking
+ if (this._isResizing) {
+ // The actual shader height will be calculated in onResize
+ return size;
+ }
+
+ // Otherwise use the stored shader height for normal rendering
+ const minShaderHeight = 50;
+ const currentShaderHeight = this.shaderHeight || 200;
+
+ // Set the size to be exactly baseHeight + shaderHeight
+ size[1] = baseHeight + Math.max(minShaderHeight, currentShaderHeight);
+ }
+
+ return size;
+ };
+
+ // Add clean-up on node removal
+ const origOnRemoved = nodeType.prototype.onRemoved;
+ nodeType.prototype.onRemoved = function() {
+ if (origOnRemoved) {
+ origOnRemoved.call(this);
+ }
+
+ // Cancel any pending animation frame
+ if (this.animationFrameId) {
+ cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = null;
+ }
+
+ // Clean up WebGL resources
+ if (this.gl) {
+ // Delete shader programs
+ for (const programType in this.shaderPrograms) {
+ if (this.shaderPrograms[programType]) {
+ this.gl.deleteProgram(this.shaderPrograms[programType]);
+ }
+ }
+
+ // Delete loading indicator program if it exists
+ if (this.loadingIndicatorProgram) {
+ this.gl.deleteProgram(this.loadingIndicatorProgram);
+ this.loadingIndicatorProgram = null;
+ }
+
+ // Delete buffers
+ if (this.positionBuffer) {
+ this.gl.deleteBuffer(this.positionBuffer);
+ }
+
+ // Clear references
+ this.shaderPrograms = null;
+ this.positionBuffer = null;
+ this.gl = null;
+ }
+
+ // Clear shader sources to save memory
+ this.shaderSources = null;
+
+ // Remove canvas reference
+ this.shaderCanvas = null;
+
+ // Clear any pending shaders
+ this.pendingShaders = [];
+ this.loadingShader = false;
+ };
+
+ // Start loading remaining shaders in the background with longer delays between each
+ nodeType.prototype.startBackgroundLoading = function() {
+ // This function is now a no-op - we only want to load shaders on demand
+ return;
+ };
+ }
+ }
+});