Skip to content

Commit df2a991

Browse files
authored
Refactor WebGL hybrid shader detection and fix circular layout math
2 parents 55ef2ff + ce5e7f5 commit df2a991

File tree

1 file changed

+73
-34
lines changed

1 file changed

+73
-34
lines changed

components/PatternDisplay.tsx

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ import {
1414
const DEFAULT_ROWS = 64;
1515
const 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+
1733
const 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

Comments
 (0)