@@ -14,6 +14,22 @@ import {
1414const DEFAULT_ROWS = 64 ;
1515const DEFAULT_CHANNELS = 4 ;
1616
17+ // Define which shaders support the WebGL frosted caps overlay.
18+ // Standalone visualizer shaders (v0.47-v0.50) are excluded — they are
19+ // self-contained WebGPU experiences and caps create visual clutter.
20+ // Full-screen quad shaders (v0.43, v0.44) are excluded — they render their
21+ // own frosted glass caps in the fragment shader, and their scrolling grid
22+ // layout (X=channels, Y=time) is incompatible with the WebGL overlay's
23+ // instanced positioning (X=time, Y=channels).
24+ const WEBGL_HYBRID_SHADERS = new Set ( [
25+ 'patternv0.21.wgsl' ,
26+ 'patternv0.38.wgsl' ,
27+ 'patternv0.39.wgsl' ,
28+ 'patternv0.40.wgsl' ,
29+ 'patternv0.42.wgsl' ,
30+ 'patternv0.46.wgsl' ,
31+ ] ) ;
32+
1733const EMPTY_CHANNEL : ChannelShadowState = {
1834
1935 volume : 1.0 , pan : 0.5 , freq : 440 , trigger : 0 , noteAge : 1000 ,
@@ -667,44 +683,70 @@ export const PatternDisplay: React.FC<PatternDisplayProps> = ({
667683
668684 } else {
669685 // --- CIRCULAR LAYOUT ---
670- // Use exact radii from shared constants: INNER_RADIUS=0.3, OUTER_RADIUS=0.9
671-
686+ // Must match WGSL v0.46 pixel-space calculations exactly.
687+ // WGSL uses: center = (canvasW/2, canvasH/2), minDim = min(canvasW, canvasH)
688+ // maxRadius = minDim * 0.45, minRadius = minDim * 0.15
689+ // ringDepth = (maxRadius - minRadius) / numChannels
690+ // radius = minRadius + ringIndex * ringDepth
691+ // btnW = circumference/64 * 0.95, btnH = ringDepth * 0.95
692+
672693 float numTracks = u_cols;
673-
674- // Calculate track index with inversion support
694+
695+ // Calculate track index with inversion support (matches WGSL invertedChannel logic)
675696 float trackIndex = float(col);
676697 if (u_invertChannels == 0) { trackIndex = numTracks - 1.0 - trackIndex; }
677-
678- // Normalized radius for this track (centered in track band)
679- float trackWidth = (OUTER_RADIUS - INNER_RADIUS) / numTracks;
680- float normalizedRadius = INNER_RADIUS + (trackIndex + 0.5) * trackWidth;
681-
698+
699+ // Pixel-space radii matching WGSL v0.46 exactly
700+ float minDim = min(u_resolution.x, u_resolution.y);
701+ float maxRadius = minDim * 0.45;
702+ float minRadius = minDim * 0.15;
703+ float ringDepth = (maxRadius - minRadius) / numTracks;
704+
705+ // WGSL uses inner edge of ring for position: radius = minRadius + ringIndex * ringDepth
706+ // Cap center should be at mid-ring: radius + ringDepth/2
707+ float radius = minRadius + trackIndex * ringDepth + ringDepth * 0.5;
708+
682709 // Full circle angle
683710 float totalSteps = 64.0;
684711 float anglePerStep = (2.0 * PI) / totalSteps;
685- float theta = -1.570796 + float(row) * anglePerStep;
686-
687- // Convert polar to cartesian in normalized space [0,1]
688- vec2 center = vec2(0.5, 0.5);
689- vec2 normPos = center + vec2(cos(theta), sin(theta)) * normalizedRadius * 0.5;
690-
691- // Calculate btnW and btnH from angular arc length and radial width
692- float arcLength = normalizedRadius * anglePerStep;
693- float btnW = arcLength * CAP_SCALE_FACTOR;
694- float btnH = trackWidth * 0.92;
695-
696- // Local position with rotation
712+ float theta = -1.570796 + float(row % 64) * anglePerStep;
713+
714+ // Pixel-space center (matches WGSL: center = canvasW*0.5, canvasH*0.5)
715+ vec2 center = vec2(u_resolution.x * 0.5, u_resolution.y * 0.5);
716+
717+ // Button sizes matching WGSL v0.46 scale factors (0.95 for both)
718+ float circumference = 2.0 * PI * radius;
719+ float arcLength = circumference / totalSteps;
720+ float btnW = arcLength * 0.95;
721+ float btnH = ringDepth * 0.95;
722+
723+ // Hide empty steps and apply playhead pop effect (circular variant)
724+ if (note == 0u) { btnW = 0.0; btnH = 0.0; }
725+ float circPlayhead = mod(u_playhead, totalSteps);
726+ float circDist = abs(float(row % 64) - circPlayhead);
727+ circDist = min(circDist, totalSteps - circDist);
728+ float circActivation = 1.0 - smoothstep(0.0, 1.5, circDist);
729+ float popScale = 1.0 + 0.2 * circActivation;
730+ btnW *= popScale;
731+ btnH *= popScale;
732+ v_active = circActivation;
733+
734+ // Local position with rotation (a_pos is in [-0.5, 0.5])
697735 vec2 localPos = a_pos * vec2(btnW, btnH);
698736 float rotAng = theta + 1.570796;
699737 float cA = cos(rotAng); float sA = sin(rotAng);
700738 float rotX = localPos.x * cA - localPos.y * sA;
701739 float rotY = localPos.x * sA + localPos.y * cA;
702-
703- // Map to pixel space for NDC conversion
704- vec2 pixelPos = normPos * u_resolution + vec2(rotX, rotY) * u_resolution;
705-
706- vec2 ndc = (pixelPos / u_resolution) * 2.0 - 1.0;
707- ndc.y = -ndc.y;
740+
741+ // World position in pixels (matches WGSL: worldX = center.x + cos(theta)*radius + rotX)
742+ float worldX = center.x + cos(theta) * radius + rotX;
743+ float worldY = center.y + sin(theta) * radius + rotY;
744+
745+ // Convert to NDC (matches WGSL: clipX = (worldX/canvasW)*2-1, clipY = 1-(worldY/canvasH)*2)
746+ vec2 ndc = vec2(
747+ (worldX / u_resolution.x) * 2.0 - 1.0,
748+ 1.0 - (worldY / u_resolution.y) * 2.0
749+ );
708750 gl_Position = vec4(ndc, 0.0, 1.0);
709751 }
710752
@@ -713,9 +755,8 @@ export const PatternDisplay: React.FC<PatternDisplayProps> = ({
713755 }
714756 ` ;
715757
716- // Only compile if using a glass shader
717- const isOverlayShader = shaderFile . includes ( 'v0.21' ) || shaderFile . includes ( 'v0.38' ) || shaderFile . includes ( 'v0.39' ) || shaderFile . includes ( 'v0.40' ) || shaderFile . includes ( 'v0.42' ) || shaderFile . includes ( 'v0.43' ) || shaderFile . includes ( 'v0.44' ) || shaderFile . includes ( 'v0.45' ) || shaderFile . includes ( 'v0.46' ) || shaderFile . includes ( 'v0.47' ) || shaderFile . includes ( 'v0.48' ) || shaderFile . includes ( 'v0.49' ) || shaderFile . includes ( 'v0.50' ) ;
718- if ( ! isOverlayShader ) {
758+ // Only compile if using a hybrid shader that needs WebGL caps
759+ if ( ! WEBGL_HYBRID_SHADERS . has ( shaderFile ) ) {
719760 // Clear the canvas to prevent ghosting when switching to non-overlay shaders
720761 if ( glContextRef . current && glCanvasRef . current ) {
721762 const gl = glContextRef . current ;
@@ -1557,9 +1598,7 @@ export const PatternDisplay: React.FC<PatternDisplayProps> = ({
15571598 const drawWebGL = ( ) => {
15581599 const gl = glContextRef . current ;
15591600 const res = glResourcesRef . current ;
1560- const isOverlayShader = shaderFile . includes ( 'v0.21' ) || shaderFile . includes ( 'v0.38' ) || shaderFile . includes ( 'v0.39' ) || shaderFile . includes ( 'v0.40' ) || shaderFile . includes ( 'v0.42' ) || shaderFile . includes ( 'v0.43' ) || shaderFile . includes ( 'v0.44' ) || shaderFile . includes ( 'v0.45' ) || shaderFile . includes ( 'v0.46' ) || shaderFile . includes ( 'v0.47' ) || shaderFile . includes ( 'v0.48' ) || shaderFile . includes ( 'v0.49' ) || shaderFile . includes ( 'v0.50' ) ;
1561-
1562- if ( ! gl || ! res || ! isOverlayShader || ! matrix ) return ;
1601+ if ( ! gl || ! res || ! WEBGL_HYBRID_SHADERS . has ( shaderFile ) || ! matrix ) return ;
15631602
15641603 const errors : string [ ] = [ ] ;
15651604 const uniformVals : Record < string , number | string > = { } ;
@@ -1668,7 +1707,7 @@ export const PatternDisplay: React.FC<PatternDisplayProps> = ({
16681707 offsetY = 0 ;
16691708 layoutModeName = '32-STEP (v0.39)' ;
16701709 } else {
1671- // v0.21, v0.40, v0.42, v0.43, v0. 46: Use GRID_RECT (matches render() logic)
1710+ // v0.21, v0.40, v0.42, v0.46: Use GRID_RECT (matches render() logic)
16721711 const metrics = calculateHorizontalCellSize ( gl . canvas . width , gl . canvas . height , 32 , channelCount ) ;
16731712 effectiveCellW = metrics . cellW ;
16741713 effectiveCellH = metrics . cellH ;
0 commit comments