2396 lines
125 KiB
JavaScript
2396 lines
125 KiB
JavaScript
// 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;
|
|
};
|
|
}
|
|
}
|
|
});
|