// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.

#include <metal_stdlib>
using namespace metal;

// Vertex input structure containing position attribute
struct MAIVoiceEmbodimentVertexIn {
    float2 position [[attribute(0)]];  // Normalized device coordinates (-1 to 1)
};

// Vertex output structure passed to fragment shader
struct MAIVoiceEmbodimentVertexOut {
    float4 position [[position]];      // Clip space position
    float2 uv;                         // Texture coordinates for fragment shader
};

struct MAIVoiceEmbodimentUniforms {
    float time;                      // Animation time in seconds
    float outerDistortionIntensity;  // Controls the intensity of distortion for outer rings (0-15)
    float innerDistortionIntensity;  // Controls the intensity of distortion for inner rings (0-15)
    float innerWaviness[3];          // Wave frequency multipliers for inner rings per blob
    float outerWaviness[3];          // Wave frequency multipliers for outer rings per blob
    float circleProgress;            // Progress of circle animation (0-1)
    float animationSpeed;            // Global animation speed multiplier
    float frequencies[5];            // Base frequencies for wave patterns
    float speeds[5];                 // Speed multipliers for wave patterns
    float outerBlurRadius;           // Gaussian blur radius for outer ring edges
    float innerBlurRadius;           // Gaussian blur radius for inner ring edges
    float fullScale;                 // Overall scale factor for the entire effect
    float containerScale;            // Scale factor for the container
    float viewWidth;                 // View width in points for proper scaling
    float viewHeight;                // View height in points for proper scaling
    float containerSize;             // Target container size in points for consistent scaling
    float outerScale[3];            // Scale factors for outer rings per blob
    float innerScale[3];            // Scale factors for inner rings per blob
    float gradientPosition[3];       // Horizontal position of gradient per blob (0-2.6)
    float blobOpacity[3];           // Opacity values for each blob (0-1)
    float defaultLayerOpacity;      // Opacity for the default static gradient layer
    float defaultLayerRotation;     // Rotation angle for the default gradient layer in radians
    float respondingEdgeBlur;       // Additional edge blur when in responding state
    float disconnectedOverlayOpacity; // Opacity of the disconnected state overlay (0-1)
    bool showGradientDebug;         // Toggle for gradient debugging visualization
    int32_t activeBlob;            // Index of the currently active blob (0-2)
    // Total size: 172 bytes, aligned for optimal GPU performance
};

// Animation timing constants
constant float MAIVE_ROTATION_SPEED_1 = 75.0;  // Base rotation speed for blob 1 in degrees/second
constant float MAIVE_ROTATION_SPEED_2 = 90.0;  // Base rotation speed for blob 2 in degrees/second
constant float MAIVE_ROTATION_SPEED_3 = 70.0;  // Base rotation speed for blob 3 in degrees/second
constant float MAIVE_BLOB2_TIME_OFFSET = 1.15; // Animation start delay for blob 2 in seconds
constant float MAIVE_BLOB3_TIME_OFFSET = 0.25; // Animation start delay for blob 3 in seconds
constant float MAIVE_GRADIENT_CYCLE_DURATION = 6.0; // Complete gradient animation cycle duration in seconds
constant float MAIVE_GRADIENT_HALF_CYCLE = 3.0;    // Duration of one-way gradient movement in seconds
constant float MAIVE_GRADIENT_STEEPNESS = 10.0;    // Controls the steepness of the S-curve gradient transition

// Helper function for calculating gradient animation timing and position
float mai_voice_embodiment_computeGradientOffset(float time, float timeOffset) {
    float offsetTime = max(0.0, time - timeOffset);
    float cycleTime = fmod(offsetTime, MAIVE_GRADIENT_CYCLE_DURATION);

    if (cycleTime < MAIVE_GRADIENT_HALF_CYCLE) {
        // Forward phase: Moves gradient from 0 to 2.6 units using S-curve easing
        float t = cycleTime / MAIVE_GRADIENT_HALF_CYCLE;

        // Apply sigmoid-based S-curve easing for smooth acceleration and deceleration
        // Maps linear time to a smooth S-curve using configurable steepness
        float k = MAIVE_GRADIENT_STEEPNESS;
        float sigmoid = 1.0 / (1.0 + exp(-k * (t - 0.5)));
        float normalizedSigmoid = (sigmoid - 1.0/(1.0 + exp(k * 0.5))) /
                                 (1.0/(1.0 + exp(-k * 0.5)) - 1.0/(1.0 + exp(k * 0.5)));

        return 2.6 * normalizedSigmoid;
    } else {
        // Reverse phase: Moves gradient from 2.6 to 0 units using S-curve easing
        float t = (cycleTime - MAIVE_GRADIENT_HALF_CYCLE) / MAIVE_GRADIENT_HALF_CYCLE;

        // Apply the same S-curve easing for the reverse movement
        float k = MAIVE_GRADIENT_STEEPNESS;
        float sigmoid = 1.0 / (1.0 + exp(-k * (t - 0.5)));
        float normalizedSigmoid = (sigmoid - 1.0/(1.0 + exp(k * 0.5))) /
                                 (1.0/(1.0 + exp(-k * 0.5)) - 1.0/(1.0 + exp(k * 0.5)));

        return 2.6 * (1.0 - normalizedSigmoid);
    }
}

// Helper function for calculating rotation angles for all three blobs
float3 mai_voice_embodiment_computeRotationAngles(float time) {
    // Initial rotation angles in radians (35°, 315°, 135°)
    float startAngle1 = 35.0 * M_PI_F / 180.0;
    float startAngle2 = 315.0 * M_PI_F / 180.0;
    float startAngle3 = 135.0 * M_PI_F / 180.0;

    // Calculate continuous rotation with different speeds and start delays
    float rotation1 = startAngle1 - (MAIVE_ROTATION_SPEED_1 * time);
    float rotation2 = startAngle2 - (MAIVE_ROTATION_SPEED_2 * max(0.0, time - MAIVE_BLOB2_TIME_OFFSET));
    float rotation3 = startAngle3 - (MAIVE_ROTATION_SPEED_3 * max(0.0, time - MAIVE_BLOB3_TIME_OFFSET));

    return float3(rotation1, rotation2, rotation3);
}

// Helper function for calculating gradient positions for all three blobs
float3 mai_voice_embodiment_computeGradientPositions(float time) {
    return float3(
                  mai_voice_embodiment_computeGradientOffset(time, 0.0),
                  mai_voice_embodiment_computeGradientOffset(time, MAIVE_BLOB2_TIME_OFFSET),
                  mai_voice_embodiment_computeGradientOffset(time, MAIVE_BLOB3_TIME_OFFSET)
    );
}

// Helper function for 2D rotation around origin
float2 mai_voice_embodiment_rotate2D(float2 position, float angle) {
    float s = sin(angle);
    float c = cos(angle);
    float2x2 rotationMatrix = float2x2(float2(c, -s), float2(s, c));
    return rotationMatrix * position;
}

// Helper function: Approximation of the error function (erf)
// Used for smooth transitions in Gaussian-based effects
float mai_voice_embodiment_erf_approx(float x) {
    // Constants for Abramowitz and Stegun approximation formula 7.1.26
    // Provides accuracy to ~1e-7
    float a1 =  0.254829592;
    float a2 = -0.284496736;
    float a3 =  1.421413741;
    float a4 = -1.453152027;
    float a5 =  1.061405429;
    float p  =  0.3275911;
    float sign = (x < 0.0) ? -1.0 : 1.0;
    x = fabs(x);
    float t = 1.0 / (1.0 + p*x);
    float y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)* t * exp(-x*x);
    return sign * y;
}

// Helper function for creating radial gradients with Gaussian blur
// Simulates the convolution of a binary circle with a Gaussian kernel
float4 mai_voice_embodiment_radialGradient(float2 uv, float2 center, float circleRadius, float blurSigma, float4 fullColor) {
    float dist = length(uv - center);
    // Calculate alpha using error function for smooth Gaussian falloff
    // At dist == circleRadius, alpha = 0.5, creating a smooth transition
    float alpha = 0.5 * (1.0 + mai_voice_embodiment_erf_approx((circleRadius - dist) / (blurSigma * sqrt(2.0))));
    return float4(fullColor.rgb, saturate(alpha));
}

// Parameters for defining ellipse shapes in the gradient
struct MAIVoiceEmbodimentEllipseParams {
    float2 center;    // Center position relative to canvas height
    float2 size;      // Width and height relative to canvas height
    float3 color;     // RGB color values (0-1)
    bool isScreen;    // Whether to use screen blend mode instead of normal
    float blur;       // Gaussian blur radius as percentage of canvas height
};

// Detailed information about blob shape calculations
struct MAIVoiceEmbodimentBlobShapeInfo {
    float alpha;           // Opacity value for the shape
    float outerDistance;  // Signed distance to outer ring edge (positive outside)
    float innerDistance;  // Signed distance to inner ring edge (positive inside)
    bool isOuterRing;    // Identifies if this shape is part of the outer ring
};

// Calculates the shape and appearance of a blob component (inner or outer ring)
// Parameters:
//   uv: UV coordinates relative to blob center
//   radius: Base radius of the blob component
//   time: Current animation time
//   uniforms: Global uniform values for animation and appearance
//   distortionIntensity: Amount of wave distortion to apply
//   blurRadius: Gaussian blur radius for edges
//   blobIndex: Which blob this is (0-2) for coordinated animations
//   extraEdgeBlur: Additional edge blur amount
//   isOuterRing: Whether this is the outer ring (true) or inner ring (false)
// Returns:
//   MAIVoiceEmbodimentBlobShapeInfo containing:
//   - alpha: Opacity value for the shape
//   - outerDistance: Signed distance to outer ring edge (positive outside)
//   - innerDistance: Signed distance to inner ring edge (positive inside)
//   - isOuterRing: Whether this is part of the outer ring
MAIVoiceEmbodimentBlobShapeInfo calculateBlobShapeForIndex(float2 uv, float radius, float time, constant MAIVoiceEmbodimentUniforms &uniforms, float distortionIntensity, float blurRadius, int blobIndex, float extraEdgeBlur = 0.0, bool isOuterRing = true) {
    float angle = atan2(uv.y, uv.x);
    float dist = length(uv);
    float deformation = 0.0;

    if (distortionIntensity > 0.0) {
        float intensityProgress = distortionIntensity / 15.0;

        // Create three different but balanced frequency patterns
        // All patterns use integer frequencies to maintain perfect periodicity
        float frequencies[5];
        float phases[5];

        if (blobIndex == 0) {
            // First blob: Base pattern (2,3,1,4,1)
            frequencies[0] = 2.0; phases[0] = uniforms.speeds[0];
            frequencies[1] = 3.0; phases[1] = uniforms.speeds[1];
            frequencies[2] = 1.0; phases[2] = uniforms.speeds[2];
            frequencies[3] = 4.0; phases[3] = uniforms.speeds[3];
            frequencies[4] = 1.0; phases[4] = uniforms.speeds[4];
        }
        else if (blobIndex == 1) {
            // Second blob: Rotated frequencies but same wave count
            frequencies[0] = 1.0; phases[0] = uniforms.speeds[0] * -0.8;
            frequencies[1] = 4.0; phases[1] = uniforms.speeds[1] * 0.75;
            frequencies[2] = 2.0; phases[2] = uniforms.speeds[2] * -0.8;
            frequencies[3] = 1.0; phases[3] = uniforms.speeds[3] * -1.0;
            frequencies[4] = 3.0; phases[4] = uniforms.speeds[4] * -1.0;
        }
        else {
            // Third blob: Another unique but balanced pattern
            frequencies[0] = 3.0; phases[0] = uniforms.speeds[0] * 0.5;
            frequencies[1] = 1.0; phases[1] = uniforms.speeds[1] * 0.75;
            frequencies[2] = 4.0; phases[2] = uniforms.speeds[2] * 0.6;
            frequencies[3] = 2.0; phases[3] = uniforms.speeds[3] * -1.67;
            frequencies[4] = 1.0; phases[4] = uniforms.speeds[4];
        }

        // Apply the frequencies with the same total wave energy
        for (int i = 0; i < 5; i++) {
            float frequency = floor(frequencies[i] * (isOuterRing ? uniforms.outerWaviness[blobIndex] : uniforms.innerWaviness[blobIndex]));
            // Use the phase directly instead of calculating it from time
            float wavePhase = frequency * angle + phases[i];
            deformation += sin(wavePhase) * (distortionIntensity / 200.0);
        }

        deformation *= (1.0 - uniforms.circleProgress) * intensityProgress;
    }

    float finalRadius = radius + deformation;

    // Increase base smoothing and use screen-space derivatives for better AA
    float minSmoothing = 0.008; // Increased from 0.002
    float pixelWidth = fwidth(dist) * 2.0; // Screen-space derivative for AA
    float aaWidth = max(minSmoothing, pixelWidth);
    float shadowWidth = max(aaWidth, blurRadius * 1.5 + extraEdgeBlur); // Add extra edge blur

  MAIVoiceEmbodimentBlobShapeInfo result;
    result.isOuterRing = isOuterRing;

    if (radius == 1.0) {  // Outer ring
        float d = dist - finalRadius;
        float edge = d / aaWidth;
        float aaFactor = smoothstep(0.0, 1.0, edge);
        float t = max(d, 0.0) / shadowWidth;
        float decay = 0.95 * exp(-20.0 * t) * (1.0 + t * 3.0 + t * t * 1.5);

        result.alpha = mix(1.0, decay, aaFactor);
        result.outerDistance = d;  // Positive outside the ring, negative inside
        result.innerDistance = 0.0;  // Not used for outer ring
    } else {  // Inner ring
        float d = finalRadius - dist;
        float edge = d / aaWidth;
        float aaFactor = smoothstep(0.0, 1.0, edge);
        float t = max(d, 0.0) / shadowWidth;
        float decay = 0.95 * exp(-20.0 * t) * (1.0 + t * 3.0 + t * t * 1.5);

        result.alpha = mix(1.0, decay, aaFactor);
        result.innerDistance = d;  // Positive inside the ring, negative outside
        result.outerDistance = 0.0;  // Not used for inner ring
    }

    return result;
}

// Helper function for proper alpha compositing
float3 mai_voice_embodiment_compositeOver(float3 top, float aTop, float3 bottom, float aBottom) {
    float aOut = aTop + aBottom * (1.0 - aTop);
    return (top * aTop + bottom * aBottom * (1.0 - aTop)) / max(aOut, 0.0001);
}

// Helper functions for HSL-RGB color space conversion
float3 mai_voice_embodiment_rgbToHsl(float3 c) {
    float maxVal = max(max(c.r, c.g), c.b);
    float minVal = min(min(c.r, c.g), c.b);
    float h, s, l = (maxVal + minVal) / 2.0;
    float d = maxVal - minVal;

    if (d == 0.0) {
        h = s = 0.0;  // Achromatic case (grayscale)
    } else {
        s = l > 0.5 ? d / (2.0 - maxVal - minVal) : d / (maxVal + minVal);
        // Calculate hue
        if (maxVal == c.r) {
            h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
        } else if (maxVal == c.g) {
            h = (c.b - c.r) / d + 2.0;
        } else {
            h = (c.r - c.g) / d + 4.0;
        }
        h /= 6.0;  // Normalize hue to [0,1] range
    }
    return float3(h, s, l);
}

float3 mai_voice_embodiment_hslToRgb(float3 hsl) {
    float h = hsl.x, s = hsl.y, l = hsl.z;

    if (s == 0.0) {
        return float3(l);  // Achromatic case (grayscale)
    }

    // Calculate helper values for RGB conversion
    float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
    float p = 2.0 * l - q;

    // Normalize and adjust hue
    float hk = fmod(h, 1.0);
    float3 ts = float3(hk + 1.0/3.0, hk, hk - 1.0/3.0);
    ts = select(ts, ts + 1.0, ts < 0.0);
    ts = select(ts, ts - 1.0, ts > 1.0);

    // Convert each component using the HSL to RGB formula
    float3 rgb;
    rgb.r = ts.r < 1.0/6.0 ? p + (q - p) * 6.0 * ts.r :
            ts.r < 1.0/2.0 ? q :
            ts.r < 2.0/3.0 ? p + (q - p) * (2.0/3.0 - ts.r) * 6.0 : p;

    rgb.g = ts.g < 1.0/6.0 ? p + (q - p) * 6.0 * ts.g :
            ts.g < 1.0/2.0 ? q :
            ts.g < 2.0/3.0 ? p + (q - p) * (2.0/3.0 - ts.g) * 6.0 : p;

    rgb.b = ts.b < 1.0/6.0 ? p + (q - p) * 6.0 * ts.b :
            ts.b < 1.0/2.0 ? q :
            ts.b < 2.0/3.0 ? p + (q - p) * (2.0/3.0 - ts.b) * 6.0 : p;

    return rgb;
}

// Helper function for applying color blend mode
float3 mai_voice_embodiment_applyColorBlend(float3 base, float3 blend) {
    float3 baseHsl = mai_voice_embodiment_rgbToHsl(base);
    float3 blendHsl = mai_voice_embodiment_rgbToHsl(blend);

    // Create new color using hue and saturation from blend color, luminance from base
    float3 resultHsl = float3(blendHsl.xy, baseHsl.z);

    return mai_voice_embodiment_hslToRgb(resultHsl);
}

// Helper function for soft light blend
float3 mai_voice_embodiment_softLight(float3 base, float3 blend) {
    float3 result;
    for (int i = 0; i < 3; i++) {
        if (blend[i] <= 0.5) {
            result[i] = base[i] - (1.0 - 2.0 * blend[i]) * base[i] * (1.0 - base[i]);
        } else {
            float d = (base[i] <= 0.25) ?
                ((16.0 * base[i] - 12.0) * base[i] + 4.0) * base[i] :
                sqrt(base[i]);
            result[i] = base[i] + (2.0 * blend[i] - 1.0) * (d - base[i]);
        }
    }
    return result;
}

// Define the gradient ellipses from bottom to top layer
constant MAIVoiceEmbodimentEllipseParams ellipses[15] = {
    // Light blue screen-blended background ellipses
    {float2(0.625, 0.0), float2(0.96, 0.96), float3(0.0, 0.502, 1.0), true, 0.48},    // #0080FF with 48% blur
    {float2(1.73, 1.1), float2(0.96, 0.96), float3(0.0, 0.502, 1.0), true, 0.48},     // #0080FF with 48% blur
    {float2(3.04, 0.0), float2(0.96, 0.96), float3(0.0, 0.502, 1.0), true, 0.48},     // #0080FF with 48% blur

    // First set of normal-blended cyan ellipses
    {float2(0.0, 0.5), float2(1.0, 1.0), float3(0.071, 0.714, 0.949), false, 0.42},   // #12B6F2 with 42% blur
    {float2(0.0, 0.5), float2(0.44, 1.0), float3(0.486, 0.953, 0.737), false, 0.34},  // #7CF3BC with 34% blur
    {float2(0.0, 0.5), float2(0.07, 0.42), float3(0.545, 1.0, 0.831), false, 0.64},   // #8BFFD4 with 64% blur

    // Second set of normal-blended cyan ellipses
    {float2(2.39, 0.5), float2(1.0, 1.0), float3(0.071, 0.714, 0.949), false, 0.42},  // #12B6F2 with 42% blur
    {float2(2.39, 0.5), float2(0.44, 1.0), float3(0.486, 0.953, 0.737), false, 0.34}, // #7CF3BC with 34% blur
    {float2(2.39, 0.5), float2(0.07, 0.42), float3(0.545, 1.0, 0.831), false, 0.64},  // #8BFFD4 with 64% blur

    // First set of normal-blended purple-pink ellipses
    {float2(1.18, 0.5), float2(0.6, 1.0), float3(0.431, 0.098, 0.882), false, 0.30},  // #6E19E1 with 30% blur
    {float2(1.18, 0.5), float2(0.48, 1.0), float3(0.980, 0.431, 0.871), false, 0.30}, // #FA6EDE with 30% blur
    {float2(1.18, 0.5), float2(0.15, 0.29), float3(1.0, 0.667, 0.902), false, 0.60},  // #FFAAE6 with 60% blur

    // Second set of normal-blended purple-pink ellipses
    {float2(3.6, 0.5), float2(0.6, 1.0), float3(0.431, 0.098, 0.882), false, 0.30},   // #6E19E1 with 30% blur
    {float2(3.6, 0.5), float2(0.48, 1.0), float3(0.980, 0.431, 0.871), false, 0.30},  // #FA6EDE with 30% blur
    {float2(3.6, 0.5), float2(0.15, 0.29), float3(1.0, 0.667, 0.902), false, 0.60}    // #FFAAE6 with 60% blur
};

// Helper function for calculating ellipse value with Gaussian blur
float maivoice_embodiment_ellipseValue(float2 pos, float2 center, float2 size, float sigma) {
    // Calculate normalized distance from point to ellipse center
    float2 delta = (pos - center) / (size * 0.5);
    // Compute radial distance considering elliptical shape
    float dist = length(delta);
    // Apply Gaussian blur using error function for smooth falloff
    return 0.5 * (1.0 + mai_voice_embodiment_erf_approx((1.0 - dist) / (sigma * sqrt(2.0))));
}

// Helper function for sampling the gradient at a specific 2D position
float3 maivoice_embodiment_sampleGradient(float2 position) {
    // Initialize with background color (#0047D6)
    float3 result = float3(0.0, 0.278, 0.839);

    // Apply each ellipse in sequence from bottom to top
    for (int i = 0; i < 15; i++) {
        // Calculate the contribution of this ellipse using its custom blur
        float value = maivoice_embodiment_ellipseValue(position, ellipses[i].center, ellipses[i].size, ellipses[i].blur);

        if (ellipses[i].isScreen) {
            // Apply screen blend mode for light blue background ellipses
            float3 screenBlend = float3(1.0) - (float3(1.0) - ellipses[i].color) * (float3(1.0) - result);
            result = mix(result, screenBlend, value);
        } else {
            // Apply normal blend mode for foreground ellipses
            result = mai_voice_embodiment_compositeOver(ellipses[i].color, value, result, 1.0);
        }
    }

    return result;
}

// Vertex shader for the voice embodiment effect
vertex MAIVoiceEmbodimentVertexOut maivoice_embodiment_blob_vertex(
    const MAIVoiceEmbodimentVertexIn vertex_in [[stage_in]],
    constant MAIVoiceEmbodimentUniforms &uniforms [[buffer(1)]]
) {
    MAIVoiceEmbodimentVertexOut out;

    // Scale vertex position by container scale for proper sizing
    float2 scaledPosition = vertex_in.position * uniforms.containerScale;

    // Output position in clip space
    out.position = float4(scaledPosition, 0.0, 1.0);

    // Convert position to UV coordinates [0,1]
    out.uv = vertex_in.position * 0.5 + 0.5;

    return out;
}


// Helper function for soft light blend
float3 softLight(float3 base, float3 blend) {
    float3 result;
    for (int i = 0; i < 3; i++) {
        if (blend[i] <= 0.5) {
            result[i] = base[i] - (1.0 - 2.0 * blend[i]) * base[i] * (1.0 - base[i]);
        } else {
            float d = (base[i] <= 0.25) ?
                ((16.0 * base[i] - 12.0) * base[i] + 4.0) * base[i] :
                sqrt(base[i]);
            result[i] = base[i] + (2.0 * blend[i] - 1.0) * (d - base[i]);
        }
    }
    return result;
};

// Fragment shader for the voice embodiment effect
fragment float4 maivoice_embodiment_blob_fragment(
    MAIVoiceEmbodimentVertexOut in [[stage_in]],
    constant MAIVoiceEmbodimentUniforms &uniforms [[buffer(0)]]
) {
    // Calculate animation parameters
    float3 rotationAngles = mai_voice_embodiment_computeRotationAngles(uniforms.time);
    float3 gradientPositions = mai_voice_embodiment_computeGradientPositions(uniforms.time);

    // Handle gradient debug visualization mode
    if (uniforms.showGradientDebug) {
        float2 uv = in.uv;

        // In the actual blob rendering:
        // - The gradient container layer (mask) is 1.0 units wide
        // - The dynamic gradient layer is 3.6 units wide
        // - The gradient slides within a range of 2.6 units (3.6 - 1.0)

        // Simulate this arrangement in the debug view
        float containerWidth = 1.0;  // Width of the container mask
        float gradientWidth = 3.6;   // Width of the dynamic gradient layer
        float slideRange = 2.6;      // Available sliding range (3.6 - 1.0)

        // Get the current offset from the animation
        float xOffset = gradientPositions[uniforms.activeBlob];

        // Create two visualization modes:
        bool showFullGradient = false;  // Toggle between full gradient and container view

        float2 gradientUV;

        if (showFullGradient) {
            // Mode 1: Show the full 3.6-wide gradient with a 1.0-wide window overlay
            // This helps visualize the entire gradient and how the container mask frames it

            // Map screen coordinates to the full gradient space
            gradientUV.x = uv.x * gradientWidth;
            gradientUV.y = uv.y;

            // Sample the gradient at this position
            float3 gradientColor = maivoice_embodiment_sampleGradient(gradientUV);

            // Draw a rectangle showing the current container mask position
            float containerLeft = xOffset;
            float containerRight = xOffset + containerWidth;

            // Highlight the container mask area
            if (gradientUV.x >= containerLeft && gradientUV.x <= containerRight) {
                // Add a subtle highlight to the visible area
                gradientColor = mix(gradientColor, float3(1.0, 1.0, 1.0), 0.1);

                // Add stronger borders to the container mask
                if (abs(gradientUV.x - containerLeft) < 0.01 || abs(gradientUV.x - containerRight) < 0.01) {
                    gradientColor = mix(gradientColor, float3(1.0, 1.0, 1.0), 0.5);
                }
            } else {
                // Dim the areas outside the container mask
                gradientColor = mix(gradientColor, float3(0.0, 0.0, 0.0), 0.5);
            }

            // Draw ellipse boundaries
            for (int i = 0; i < 15; i++) {
                // Get ellipse parameters
                float2 center = ellipses[i].center;
                float2 size = ellipses[i].size;

                // Calculate normalized distance to ellipse boundary
                float2 delta = (gradientUV - center) / (size * 0.5);
                float dist = length(delta);

                // Draw a thin line at the ellipse boundary (dist = 1.0)
                if (abs(dist - 1.0) < 0.01) {
                    // Use a color based on the blend mode
                    float3 boundaryColor = ellipses[i].isScreen ?
                        float3(0.0, 0.7, 1.0) :  // Light blue for screen blend
                        float3(1.0, 0.7, 0.0);   // Orange for normal blend

                    gradientColor = mix(gradientColor, boundaryColor, 0.5);
                }

                // Draw a dot at the center of each ellipse
                if (length(gradientUV - center) < 0.02) {
                    gradientColor = ellipses[i].isScreen ?
                        float3(0.0, 1.0, 1.0) :  // Cyan for screen blend
                        float3(1.0, 1.0, 0.0);   // Yellow for normal blend
                }
            }

            return float4(gradientColor, 1.0);
        } else {
            // Mode 2: Show only what's visible through the container mask (1.0 wide)
            // This simulates what's actually visible in the blob

            // Map screen coordinates to the container mask space (1.0 wide)
            float2 containerUV;
            containerUV.x = uv.x;
            containerUV.y = uv.y;

            // Calculate the corresponding position in the gradient layer
            gradientUV.x = xOffset + containerUV.x;
            gradientUV.y = containerUV.y;

            // Sample the gradient at this position
            float3 gradientColor = maivoice_embodiment_sampleGradient(gradientUV);

            // Draw a border around the container mask
            if (uv.x < 0.005 || uv.x > 0.995 || uv.y < 0.005 || uv.y > 0.995) {
                gradientColor = mix(gradientColor, float3(1.0, 1.0, 1.0), 0.3);
            }

            // Add vertical grid lines to show position within the gradient
            float gridX = fmod(gradientUV.x, 0.5);
            if (gridX < 0.005 || gridX > 0.495) {
                float intensity = (fmod(gradientUV.x, 1.0) < 0.005 || fmod(gradientUV.x, 1.0) > 0.995) ? 0.3 : 0.15;
                gradientColor = mix(gradientColor, float3(1.0, 1.0, 1.0), intensity);
            }

            // Add horizontal grid lines
            if (abs(fmod(uv.y * 4.0, 1.0)) < 0.003) {
                float intensity = (abs(uv.y - 0.5) < 0.003) ? 0.3 : 0.15;
                gradientColor = mix(gradientColor, float3(1.0, 1.0, 1.0), intensity);
            }

            // Add position indicator
            float positionIndicator = 0.05 + (xOffset / slideRange) * 0.9; // Map 0-2.6 to 0.05-0.95
            if (abs(uv.x - positionIndicator) < 0.01 && uv.y > 0.95) {
                gradientColor = float3(1.0, 1.0, 1.0);
            }

            // Add text indicators for position
            if (uv.y > 0.95) {
                if (abs(uv.x - 0.05) < 0.01) {
                    gradientColor = float3(0.7, 0.7, 0.7); // Left marker
                }
                if (abs(uv.x - 0.95) < 0.01) {
                    gradientColor = float3(0.7, 0.7, 0.7); // Right marker
                }
            }

            return float4(gradientColor, 1.0);
        }
    }

    // Calculate target size for consistent rendering across different screen sizes
    float targetSize = min(uniforms.viewWidth, uniforms.viewHeight) * 0.7 * 1.25;

    // Center and scale UV coordinates
    float2 centeredUV = in.uv - 0.5;
    float scaleFactorX = uniforms.viewWidth / targetSize;
    float scaleFactorY = uniforms.viewHeight / targetSize;
    float2 scaledUV = float2(centeredUV.x * scaleFactorX, centeredUV.y * scaleFactorY);
    float2 adjustedUV = scaledUV + 0.5;

    // Convert to [-1,1] range and apply full scale
    float2 uv = (adjustedUV * 2.0 - 1.0) / uniforms.fullScale;

    // Use stored angles directly - they already include both base rotation and animation
    float containerRotation0 = rotationAngles.x * M_PI_F / 180.0;
    float containerRotation1 = rotationAngles.y * M_PI_F / 180.0;
    float containerRotation2 = rotationAngles.z * M_PI_F / 180.0;

    float2 rotatedContainerUV0 = mai_voice_embodiment_rotate2D(uv, containerRotation0);
    float2 rotatedContainerUV1 = mai_voice_embodiment_rotate2D(uv, containerRotation1);
    float2 rotatedContainerUV2 = mai_voice_embodiment_rotate2D(uv, containerRotation2);

    // Map UV from [-1,1] to [0,1] for the container mask space
    float2 containerUV0 = (rotatedContainerUV0 + 1.0) * 0.5;
    float2 containerUV1 = (rotatedContainerUV1 + 1.0) * 0.5;
    float2 containerUV2 = (rotatedContainerUV2 + 1.0) * 0.5;

    // Calculate the corresponding positions in the gradient layer
    // The gradient layer is 3.6 units wide and slides within a range of 2.6 units
    float2 gradientUV0, gradientUV1, gradientUV2;

    // Apply the same gradient positioning logic as in the debug view
    gradientUV0.x = gradientPositions.x + containerUV0.x;
    gradientUV0.y = containerUV0.y;

    gradientUV1.x = gradientPositions.y + containerUV1.x;
    gradientUV1.y = containerUV1.y;

    gradientUV2.x = gradientPositions.z + containerUV2.x;
    gradientUV2.y = containerUV2.y;

    // Sample the gradient at the calculated 2D positions
    float4 gradientColor0 = float4(maivoice_embodiment_sampleGradient(gradientUV0), 1.0);
    float4 gradientColor1 = float4(maivoice_embodiment_sampleGradient(gradientUV1), 1.0);
    float4 gradientColor2 = float4(maivoice_embodiment_sampleGradient(gradientUV2), 1.0);

    // Add color blend overlay layer (#839EB5) only in disconnected state
    float3 overlayColor = float3(0.514, 0.620, 0.710);
    if (uniforms.disconnectedOverlayOpacity > 0.0) {
        gradientColor0.rgb = mix(gradientColor0.rgb, mai_voice_embodiment_applyColorBlend(gradientColor0.rgb, overlayColor), uniforms.disconnectedOverlayOpacity);
        gradientColor1.rgb = mix(gradientColor1.rgb, mai_voice_embodiment_applyColorBlend(gradientColor1.rgb, overlayColor), uniforms.disconnectedOverlayOpacity);
        gradientColor2.rgb = mix(gradientColor2.rgb, mai_voice_embodiment_applyColorBlend(gradientColor2.rgb, overlayColor), uniforms.disconnectedOverlayOpacity);
    }

    // Compute separate UVs for each blob using that blob's own scales
    float2 outerUV0 = uv / uniforms.outerScale[0];
    float2 innerUV0 = uv / uniforms.innerScale[0];
    float2 outerUV1 = uv / uniforms.outerScale[1];
    float2 innerUV1 = uv / uniforms.innerScale[1];
    float2 outerUV2 = uv / uniforms.outerScale[2];
    float2 innerUV2 = uv / uniforms.innerScale[2];

    // Compute each blob's shape info with different distortion shuffles
    // Add extra edge blur for the outer ring in responding state
    float extraEdgeBlur = uniforms.respondingEdgeBlur;
  MAIVoiceEmbodimentBlobShapeInfo outerShape0 = calculateBlobShapeForIndex(outerUV0, 1.0, uniforms.time, uniforms, uniforms.outerDistortionIntensity, uniforms.outerBlurRadius, 0, extraEdgeBlur, true);
  MAIVoiceEmbodimentBlobShapeInfo innerShape0 = calculateBlobShapeForIndex(innerUV0, 1.0, uniforms.time + 0.5, uniforms, uniforms.innerDistortionIntensity, uniforms.innerBlurRadius, 0, 0.0, false);

  MAIVoiceEmbodimentBlobShapeInfo outerShape1 = calculateBlobShapeForIndex(outerUV1, 1.0, uniforms.time, uniforms, uniforms.outerDistortionIntensity, uniforms.outerBlurRadius, 1, extraEdgeBlur, true);
  MAIVoiceEmbodimentBlobShapeInfo innerShape1 = calculateBlobShapeForIndex(innerUV1, 1.0, uniforms.time + 0.5, uniforms, uniforms.innerDistortionIntensity, uniforms.innerBlurRadius, 1, 0.0, false);

  MAIVoiceEmbodimentBlobShapeInfo outerShape2 = calculateBlobShapeForIndex(outerUV2, 1.0, uniforms.time, uniforms, uniforms.outerDistortionIntensity, uniforms.outerBlurRadius, 2, extraEdgeBlur, true);
  MAIVoiceEmbodimentBlobShapeInfo innerShape2 = calculateBlobShapeForIndex(innerUV2, 1.0, uniforms.time + 0.5, uniforms, uniforms.innerDistortionIntensity, uniforms.innerBlurRadius, 2, 0.0, false);

    // Compute the main blob mask (ring shape)
    float a0 = max(0.0, outerShape0.alpha - innerShape0.alpha);
    float a1 = max(0.0, outerShape1.alpha - innerShape1.alpha);
    float a2 = max(0.0, outerShape2.alpha - innerShape2.alpha);

    // Apply blob opacity
    a0 *= uniforms.blobOpacity[0];
    a1 *= uniforms.blobOpacity[1];
    a2 *= uniforms.blobOpacity[2];

    // For blob 1 only (activeBlob == 0) and only in default state (defaultLayerOpacity > 0),
    // blend in custom colored radial gradients on dark blue background
    if (uniforms.activeBlob == 0 && uniforms.defaultLayerOpacity > 0.001) {
        // Define all colors
        float4 background = float4(0.0, 0.278, 0.839, 1.0);         // #0047D6 dark blue
        float4 lightBlueGradient = float4(0.0, 0.502, 1.0, 1.0);    // #0080FF light blue
        float4 purpleGradient = float4(0.431, 0.106, 0.882, 1.0);   // #6E1BE1 purple
        float4 tealGradient = float4(0.0, 0.851, 0.667, 1.0);       // #00D9AA teal
        float4 pinkGradient = float4(0.98, 0.431, 0.871, 1.0);      // #FA6EDE pink

        // Apply rotation to UV coordinates for the radial gradients
        float2 rotatedUV = mai_voice_embodiment_rotate2D(adjustedUV - float2(0.5, 0.5), uniforms.defaultLayerRotation) + float2(0.5, 0.5);

        // Create the radial gradients using rotated coordinates
        float4 lightBlueGrad = mai_voice_embodiment_radialGradient(rotatedUV, float2(0.18, 0.18), 0.48, 0.24, lightBlueGradient);
        float4 purpleGrad = mai_voice_embodiment_radialGradient(rotatedUV, float2(0.82, 0.82), 0.48, 0.24, purpleGradient);
        float4 tealGrad = mai_voice_embodiment_radialGradient(rotatedUV, float2(0.1, 0.1), 0.32, 0.16, tealGradient);
        float4 pinkGrad = mai_voice_embodiment_radialGradient(rotatedUV, float2(0.9, 0.9), 0.32, 0.16, pinkGradient);

        // Start with background color and alpha
        float3 result = background.rgb;
        float resultAlpha = background.a;

        // Layer the gradients in order.
        // For light blue and purple use proper screen blend mode:
        result = float3(1.0) - (float3(1.0) - result) * (float3(1.0) - lightBlueGrad.rgb * lightBlueGrad.a);
        resultAlpha = max(lightBlueGrad.a, resultAlpha);

        result = float3(1.0) - (float3(1.0) - result) * (float3(1.0) - purpleGrad.rgb * purpleGrad.a);
        resultAlpha = max(purpleGrad.a, resultAlpha);

        // For teal and pink use normal blend mode
        result = mix(result, tealGrad.rgb, tealGrad.a);
        resultAlpha = max(tealGrad.a, resultAlpha);

        result = mix(result, pinkGrad.rgb, pinkGrad.a);
        resultAlpha = max(pinkGrad.a, resultAlpha);

        // Stroke parameters defined as percentages of canvas size
        float outerStrokeRadius = uniforms.outerScale[0] * 0.5;      // Use half of outerScale for blob 0
        float outerStrokeWidth = 0.032 * 2.0;     // 3.2% of canvas - total width (doubled for [-1,1] space)
        float outerStrokeAlpha = 0.6;       // 60% opacity
        float outerStrokeBlur = 0.04;       // 4% blur radius

        float innerStrokeRadius = uniforms.innerScale[0] * 0.5;    // Use half of innerScale for blob 0
        float innerStrokeWidth = 0.04 * 2.0;      // 4% of canvas - total width (doubled for [-1,1] space)
        float innerStrokeAlpha = 0.7;       // 70% opacity
        float innerStrokeBlur = 0.04;       // 4% blur radius

        // Calculate half-widths for the strokes
        float halfOuterWidth = outerStrokeWidth * 0.5;
        float halfInnerWidth = innerStrokeWidth * 0.5;

        // Create stroke masks
        float2 normalizedUV = (adjustedUV - 0.5) * 2.0;
        float distFromCenter = length(normalizedUV);

        // Calculate distances from the stroke center lines
        float outerDist = abs(distFromCenter - outerStrokeRadius * 2.0);
        float innerDist = abs(distFromCenter - innerStrokeRadius * 2.0);

        // Create blurred stroke masks using error function
        float outerStroke = 0.5 * (1.0 + mai_voice_embodiment_erf_approx((halfOuterWidth - outerDist) / (outerStrokeBlur * sqrt(2.0))));
        float innerStroke = 0.5 * (1.0 + mai_voice_embodiment_erf_approx((halfInnerWidth - innerDist) / (innerStrokeBlur * sqrt(2.0))));

        // Create stroke layers
        float3 strokeColor = float3(1.0);  // Pure white
        float4 baseLayer = float4(result, resultAlpha);

        // Apply outer stroke - soft light blend with alpha
        float3 softLightOuter = mai_voice_embodiment_softLight(baseLayer.rgb, strokeColor);
        float3 withOuterStroke = mix(
            baseLayer.rgb,
            softLightOuter,
            outerStroke * outerStrokeAlpha
        );

        // Apply inner stroke - soft light blend with alpha
        float3 softLightInner = mai_voice_embodiment_softLight(withOuterStroke, strokeColor);
        float3 withBothStrokes = mix(
            withOuterStroke,
            softLightInner,
            innerStroke * innerStrokeAlpha
        );

        // Calculate the donut ring mask
        float ringMask = max(0.0, outerShape0.alpha - innerShape0.alpha);

        // Apply the ring mask and default layer opacity
        float3 maskedResult = withBothStrokes * ringMask;
        gradientColor0.rgb = mix(gradientColor0.rgb, maskedResult, uniforms.defaultLayerOpacity);
    }

    // Render only the active blob using the computed ring mask.
    if (uniforms.activeBlob != 0) {
        float mask;
        float4 blobColor;
        if (uniforms.activeBlob == 1) {
            mask = a1;
            blobColor = gradientColor1;
        } else { // activeBlob == 2
            mask = a2;
            blobColor = gradientColor2;
        }
        // Return the blob's gradient color multiplied by the computed mask (premultiplied alpha).
        return float4(blobColor.rgb * mask, mask);
    }

    // Composite the three blobs using the standard "over" operator for non-premultiplied colors:
    // First composite blob 0 and blob 1:
    float outAlpha01 = a0 + a1 * (1.0 - a0);
    float3 compColor01 = (gradientColor0.rgb * a0) + (gradientColor1.rgb * a1 * (1.0 - a0));
    if (outAlpha01 > 0.0001) {
        compColor01 /= outAlpha01;
    } else {
        compColor01 = float3(0.0);
    }

    // Now composite blob 2 over the result:
    float outAlpha = outAlpha01 + a2 * (1.0 - outAlpha01);
    float3 compColor = (compColor01 * outAlpha01) + (gradientColor2.rgb * a2 * (1.0 - outAlpha01));
    if (outAlpha > 0.0001) {
        compColor /= outAlpha;
    } else {
        compColor = float3(0.0);
    }

    // Convert the final color to premultiplied form.
    return float4(compColor * outAlpha, outAlpha);
}
