function draw() {
// 1. Calculate mouse velocity
let dx = mouseX - prevMouseX;
let dy = mouseY - prevMouseY;
// Lerp (linear interpolate) velocity for smoothing and decay
// This makes the ripples settle slowly when the mouse stops moving
velocityX = lerp(velocityX, dx, 0.1);
velocityY = lerp(velocityY, dy, 0.1);
// Update previous mouse position for the next frame's calculation
prevMouseX = mouseX;
prevMouseY = mouseY;
// 2. Pass uniforms to the shader
// Uniforms are variables that you pass from p5.js to your shader
myShader.setUniform('u_resolution', [width, height]);
// Invert Y for mouse position and velocity to match WebGL coordinate system
myShader.setUniform('u_mouse', [mouseX, height - mouseY]);
myShader.setUniform('u_velocity', [velocityX, -velocityY]);
// Pass time for animations within the shader (e.g., color shifts, ripple speed)
myShader.setUniform('u_time', frameCount * 0.01); // frameCount is a p5.js built-in, 0.01 controls time speed
// 3. Draw a rectangle that covers the entire canvas
// This triggers the fragment shader for every pixel on the screen
rect(0, 0, width, height);
}
🔧 Subcomponents:
calculation
Mouse Velocity Calculation
let dx = mouseX - prevMouseX;
let dy = mouseY - prevMouseY;
Calculates how far the mouse moved in the current frame by comparing current position to the previous frame's position.
calculation
Velocity Smoothing with Lerp
velocityX = lerp(velocityX, dx, 0.1);
velocityY = lerp(velocityY, dy, 0.1);
Smooths the velocity values using linear interpolation. This prevents jerky motion and creates a decay effect where ripples gradually settle when the mouse stops moving.
calculation
Previous Position Update
prevMouseX = mouseX;
prevMouseY = mouseY;
Updates the 'previous' position for the next frame so velocity calculation works correctly in the next iteration.
calculation
Full Canvas Rectangle Draw
rect(0, 0, width, height);
Draws a rectangle covering the entire canvas. This triggers the fragment shader to run for every pixel on screen, generating the final image.
Line by Line:
let dx = mouseX - prevMouseX;
- Calculates the horizontal distance the mouse moved this frame. Positive values mean the mouse moved right, negative means left.
let dy = mouseY - prevMouseY;
- Calculates the vertical distance the mouse moved this frame. Positive values mean the mouse moved down, negative means up.
velocityX = lerp(velocityX, dx, 0.1);
- Uses lerp (linear interpolation) to smoothly blend the old velocityX toward the new dx value. The 0.1 parameter means it moves 10% of the way toward the target each frame, creating smooth deceleration.
velocityY = lerp(velocityY, dy, 0.1);
- Same as velocityX but for vertical velocity. This smoothing is what makes the ripples gradually settle when you stop moving the mouse.
prevMouseX = mouseX;
- Stores the current mouse X position so it can be compared to the next frame's position to calculate velocity.
prevMouseY = mouseY;
- Stores the current mouse Y position for the same reason as prevMouseX.
myShader.setUniform('u_resolution', [width, height]);
- Sends the canvas width and height to the shader. The shader uses this to convert pixel coordinates to normalized coordinates (-1 to 1).
myShader.setUniform('u_mouse', [mouseX, height - mouseY]);
- Sends the mouse position to the shader. The Y coordinate is inverted (height - mouseY) because WebGL uses a different coordinate system than p5.js (origin at bottom-left instead of top-left).
myShader.setUniform('u_velocity', [velocityX, -velocityY]);
- Sends the calculated velocity to the shader. The Y velocity is negated to match the WebGL coordinate system. The shader uses this to determine ripple strength.
myShader.setUniform('u_time', frameCount * 0.01);
- Sends an animated time value to the shader. frameCount increases by 1 each frame, and multiplying by 0.01 slows down the animation. The shader uses this for color shifts and ripple timing.
rect(0, 0, width, height);
- Draws a rectangle from (0,0) to (width, height), covering the entire canvas. This triggers the fragment shader to execute for every pixel, generating the final rendered image.
float df(vec3 p) {
float sphereRadius = 1.0;
float baseDist = sdSphere(p, sphereRadius); // Base sphere shape
// Mouse velocity determines the strength of the ripples
float velLen = length(u_velocity) * 0.01; // Scale velocity for appropriate ripple strength
velLen = clamp(velLen, 0.0, 0.5); // Cap the ripple strength to a maximum value
// Apply the ripple deformation
float rippleDist = rippleSDF(p, velLen, 2.0); // strength, speed
return baseDist + rippleDist; // Combine sphere and ripple SDFs
}
🔧 Subcomponents:
calculation
Base Sphere Distance
float baseDist = sdSphere(p, sphereRadius);
Calculates the distance to the base sphere shape with radius 1.0.
calculation
Velocity to Ripple Strength Conversion
float velLen = length(u_velocity) * 0.01;
velLen = clamp(velLen, 0.0, 0.5);
Converts the mouse velocity magnitude into ripple strength. The 0.01 factor scales it appropriately, and clamp() ensures it stays between 0.0 and 0.5 to prevent excessive distortion.
calculation
Ripple Deformation
float rippleDist = rippleSDF(p, velLen, 2.0);
Calculates the ripple deformation using the velocity-based strength. The 2.0 parameter controls ripple animation speed.
calculation
Combined Distance Function
return baseDist + rippleDist;
Combines the base sphere and ripple deformations to create the final shape. Adding SDFs creates a blended effect.
Line by Line:
float sphereRadius = 1.0;
- Sets the base radius of the sphere to 1.0 unit.
float baseDist = sdSphere(p, sphereRadius);
- Calculates the signed distance from point p to the sphere's surface.
float velLen = length(u_velocity) * 0.01;
- Calculates the magnitude (length) of the velocity vector and scales it by 0.01. This converts the 2D velocity into a single ripple strength value.
velLen = clamp(velLen, 0.0, 0.5);
- Clamps the ripple strength to a maximum of 0.5. This prevents the ripples from becoming too extreme when the mouse moves very fast.
float rippleDist = rippleSDF(p, velLen, 2.0);
- Calculates the ripple deformation. The velLen controls how strong the ripples are (based on mouse velocity), and 2.0 controls how fast they animate.
return baseDist + rippleDist;
- Returns the combined distance. Adding the ripple to the sphere distance deforms the sphere's surface with the ripple pattern.
vec3 fakeEnvReflection(vec3 refDir) {
refDir = normalize(refDir); // Ensure reflection direction is a unit vector
// Convert reflection direction to spherical coordinates for pattern generation
float u = atan(refDir.z, refDir.x) / TWO_PI + 0.5;
float v = asin(refDir.y) / PI + 0.5;
// Create various patterns using sin/cos functions
float pattern1 = sin(u * 10.0 + u_time * 0.1) * 0.5 + 0.5;
float pattern2 = cos(v * 8.0 + u_time * 0.05) * 0.5 + 0.5;
// Define base colors for the chrome reflection (blues and purples)
vec3 color1 = vec3(0.1, 0.2, 0.5); // Deep blue
vec3 color2 = vec3(0.4, 0.2, 0.6); // Purple
vec3 color3 = vec3(0.7, 0.8, 0.9); // Light metallic sheen
// Mix colors based on patterns to create the reflective effect
vec3 reflectionColor = mix(color1, color2, pattern1);
reflectionColor = mix(reflectionColor, color3, pattern2 * 0.5);
// Add subtle, time-based color shifts for more dynamism
reflectionColor += sin(u_time * 0.2) * vec3(0.05, 0.0, 0.05); // Subtle purple shift
reflectionColor += cos(u_time * 0.15) * vec3(0.0, 0.05, 0.05); // Subtle cyan shift
return reflectionColor;
}
🔧 Subcomponents:
calculation
Spherical Coordinate Conversion
float u = atan(refDir.z, refDir.x) / TWO_PI + 0.5;
float v = asin(refDir.y) / PI + 0.5;
Converts the 3D reflection direction into 2D spherical coordinates (u, v) for pattern generation. This maps the sphere's surface to a 2D texture space.
calculation
Procedural Pattern Generation
float pattern1 = sin(u * 10.0 + u_time * 0.1) * 0.5 + 0.5;
float pattern2 = cos(v * 8.0 + u_time * 0.05) * 0.5 + 0.5;
Creates two animated patterns using sine and cosine waves. These patterns vary across the surface and animate over time.
calculation
Color Blending
vec3 reflectionColor = mix(color1, color2, pattern1);
reflectionColor = mix(reflectionColor, color3, pattern2 * 0.5);
Blends between three colors (deep blue, purple, light metallic) based on the patterns, creating a dynamic metallic appearance.
calculation
Animated Color Shifts
reflectionColor += sin(u_time * 0.2) * vec3(0.05, 0.0, 0.05);
reflectionColor += cos(u_time * 0.15) * vec3(0.0, 0.05, 0.05);
Adds subtle, time-based color shifts (purple and cyan) that pulse over time, enhancing the liquid, living quality of the surface.
Line by Line:
refDir = normalize(refDir);
- Normalizes the reflection direction to a unit vector. This ensures consistent calculations regardless of the input magnitude.
float u = atan(refDir.z, refDir.x) / TWO_PI + 0.5;
- Converts the X and Z components to an angle (atan2), then normalizes it to a 0-1 range. This is the horizontal coordinate on the sphere.
float v = asin(refDir.y) / PI + 0.5;
- Converts the Y component to an angle (asin), then normalizes it to a 0-1 range. This is the vertical coordinate on the sphere.
float pattern1 = sin(u * 10.0 + u_time * 0.1) * 0.5 + 0.5;
- Creates a sine wave pattern based on u coordinate. Multiplying by 10.0 increases frequency, adding u_time makes it animate, and the * 0.5 + 0.5 maps the result to 0-1 range.
float pattern2 = cos(v * 8.0 + u_time * 0.05) * 0.5 + 0.5;
- Similar to pattern1 but uses cosine, the v coordinate, and a different frequency (8.0) and animation speed (0.05).
vec3 color1 = vec3(0.1, 0.2, 0.5);
- Defines a deep blue color (low red, low green, high blue).
vec3 color2 = vec3(0.4, 0.2, 0.6);
- Defines a purple color (medium red, low green, high blue).
vec3 color3 = vec3(0.7, 0.8, 0.9);
- Defines a light metallic/cyan color (high values across all channels).
vec3 reflectionColor = mix(color1, color2, pattern1);
- Blends between deep blue and purple based on pattern1. When pattern1 is 0, the result is color1; when it's 1, the result is color2.
reflectionColor = mix(reflectionColor, color3, pattern2 * 0.5);
- Further blends the result toward the light metallic color based on pattern2 (scaled by 0.5 for subtlety).
reflectionColor += sin(u_time * 0.2) * vec3(0.05, 0.0, 0.05);
- Adds a subtle purple shift that pulses over time. The 0.05 values mean the shift is very subtle.
reflectionColor += cos(u_time * 0.15) * vec3(0.0, 0.05, 0.05);
- Adds a subtle cyan shift that pulses at a different rate. Together with the purple shift, this creates a shimmering effect.
void main() {
// Convert vTexCoord (0-1) to fragment coordinates (0-resolution)
vec2 fragCoord = vTexCoord * u_resolution;
vec2 uv = fragCoord / u_resolution;
uv = uv * 2.0 - 1.0; // Map to -1 to 1 range
// Adjust for aspect ratio
uv.x *= u_resolution.x / u_resolution.y;
// Raymarching setup
vec3 camPos = vec3(0.0, 0.0, -2.0); // Camera position slightly back along Z-axis
vec3 rayDir = normalize(vec3(uv, 1.0)); // Direction of the ray from camera through current pixel
vec3 currentPos = camPos; // Start ray at camera position
float totalDist = 0.0; // Total distance traveled by the ray
bool hit = false; // Flag to check if the ray hit the object
// Raymarching loop
for (int i = 0; i < MAX_STEPS; i++) {
float dist = df(currentPos); // Get distance to the object from current ray position
totalDist += dist; // Accumulate total distance
currentPos += rayDir * dist; // Move the ray forward by the distance
if (dist < EPSILON) { // If distance is very small, we've hit the object
hit = true;
break;
}
if (totalDist > MAX_DIST) { // If ray traveled too far, it missed the object
break;
}
}
vec3 finalColor = vec3(0.05, 0.05, 0.1); // Dark background color
if (hit) {
vec3 N = calcNormal(currentPos); // Calculate normal at the hit point
vec3 R = reflect(rayDir, N); // Calculate reflection vector
vec3 reflectionColor = fakeEnvReflection(R); // Get the reflective color
// Fresnel effect: Makes surfaces more reflective at grazing angles
float fresnel = pow(1.0 + dot(rayDir, N), 5.0);
fresnel = clamp(fresnel, 0.0, 1.0); // Clamp fresnel to 0-1 range
// Mix reflection color with a brighter color based on fresnel for edge glow
finalColor = mix(reflectionColor, vec3(1.0), fresnel * 0.5);
}
gl_FragColor = vec4(finalColor, 1.0); // Set the final pixel color
}
🔧 Subcomponents:
calculation
UV Coordinate Setup
vec2 fragCoord = vTexCoord * u_resolution;
vec2 uv = fragCoord / u_resolution;
uv = uv * 2.0 - 1.0;
uv.x *= u_resolution.x / u_resolution.y;
Converts texture coordinates (0-1) to normalized device coordinates (-1 to 1) and adjusts for aspect ratio. This maps each pixel to a ray direction.
calculation
Raymarching Initialization
vec3 camPos = vec3(0.0, 0.0, -2.0);
vec3 rayDir = normalize(vec3(uv, 1.0));
vec3 currentPos = camPos;
float totalDist = 0.0;
bool hit = false;
Sets up the raymarching algorithm by defining camera position, ray direction, and tracking variables.
for-loop
Raymarching Loop
for (int i = 0; i < MAX_STEPS; i++) {
float dist = df(currentPos);
totalDist += dist;
currentPos += rayDir * dist;
if (dist < EPSILON) { hit = true; break; }
if (totalDist > MAX_DIST) { break; }
}
Marches a ray through space, stepping toward the surface until it hits or exceeds maximum distance.
conditional
Hit Shading and Lighting
if (hit) {
vec3 N = calcNormal(currentPos);
vec3 R = reflect(rayDir, N);
vec3 reflectionColor = fakeEnvReflection(R);
float fresnel = pow(1.0 + dot(rayDir, N), 5.0);
fresnel = clamp(fresnel, 0.0, 1.0);
finalColor = mix(reflectionColor, vec3(1.0), fresnel * 0.5);
}
If the ray hit the object, calculates normals, reflection, and applies lighting with fresnel effect.
Line by Line:
vec2 fragCoord = vTexCoord * u_resolution;
- Converts the texture coordinate (0-1) to pixel coordinates by multiplying by canvas resolution.
vec2 uv = fragCoord / u_resolution;
- Normalizes the pixel coordinates back to 0-1 range.
uv = uv * 2.0 - 1.0;
- Maps the 0-1 range to -1 to 1 range. This centers the coordinates so the center of the screen is (0, 0).
uv.x *= u_resolution.x / u_resolution.y;
- Adjusts the X coordinate by the aspect ratio so the sphere doesn't look stretched on non-square screens.
vec3 camPos = vec3(0.0, 0.0, -2.0);
- Places the camera 2 units back along the Z-axis, looking toward the origin where the sphere is.
vec3 rayDir = normalize(vec3(uv, 1.0));
- Creates a ray direction from the camera through the current pixel. The Z component (1.0) points forward. Normalizing ensures it's a unit vector.
vec3 currentPos = camPos;
- Initializes the ray's current position at the camera.
float totalDist = 0.0;
- Tracks the total distance the ray has traveled.
bool hit = false;
- A flag that will be set to true if the ray hits the object.
for (int i = 0; i < MAX_STEPS; i++) {
- Loops up to MAX_STEPS (100) times. Each iteration is one step of the raymarching algorithm.
float dist = df(currentPos);
- Calculates the distance from the current position to the nearest surface using the distance function.
totalDist += dist;
- Accumulates the distance traveled so far.
currentPos += rayDir * dist;
- Moves the ray forward by the calculated distance. This is the core of raymarching—we take safe steps toward the surface.
if (dist < EPSILON) { hit = true; break; }
- If the distance is very small (less than EPSILON = 0.005), we've hit the surface. Set hit to true and exit the loop.
if (totalDist > MAX_DIST) { break; }
- If the ray has traveled too far (more than MAX_DIST = 10.0), it missed the object. Exit the loop.
vec3 finalColor = vec3(0.05, 0.05, 0.1);
- Initializes the final color to a dark blue-gray (the background color).
vec3 N = calcNormal(currentPos);
- Calculates the surface normal at the hit point.
vec3 R = reflect(rayDir, N);
- Calculates the reflection direction using the ray direction and surface normal. This is used to look up the environment reflection.
vec3 reflectionColor = fakeEnvReflection(R);
- Gets the reflective color based on the reflection direction.
float fresnel = pow(1.0 + dot(rayDir, N), 5.0);
- Calculates the Fresnel effect. The dot product measures the angle between the ray and normal. Raising to the 5th power makes the effect more pronounced at grazing angles.
fresnel = clamp(fresnel, 0.0, 1.0);
- Clamps the fresnel value to 0-1 range to ensure valid blending values.
finalColor = mix(reflectionColor, vec3(1.0), fresnel * 0.5);
- Blends the reflection color with white based on the fresnel effect. Higher fresnel values add more white, creating an edge glow effect.
gl_FragColor = vec4(finalColor, 1.0);
- Sets the final pixel color. The vec4 wraps the RGB color with an alpha value of 1.0 (fully opaque).