"use client"; import React, { useEffect, useRef } from 'react'; import type p5 from 'p5'; interface BlochSphereSketchProps { theta: number; phi: number; evolving: boolean; width?: number; height?: number; } const BlochSphereSketch: React.FC = ({ theta: initialTheta, phi: initialPhi, evolving: initialEvolving, width = 400, // Default width if not filling parent height = 400, // Default height }) => { const sketchRef = useRef(null); const p5InstanceRef = useRef(null); const timeRef = useRef(0); const currentPhiRef = useRef(initialPhi); // Ref to store current evolving phi useEffect(() => { if (typeof window !== 'undefined' && sketchRef.current) { import('p5').then(p5Module => { const P5 = p5Module.default; if (p5InstanceRef.current) { p5InstanceRef.current.remove(); } // Ensure the sketchRef has dimensions before creating canvas const parentWidth = sketchRef.current!.offsetWidth; const parentHeight = sketchRef.current!.offsetHeight; const canvasSize = Math.min(parentWidth, parentHeight, 400); const sketch = (p: p5) => { let currentTheta = initialTheta; // currentPhi is managed by currentPhiRef to persist across prop changes when evolving let currentEvolving = initialEvolving; p.setup = () => { p.createCanvas(canvasSize, canvasSize, p.WEBGL); p.textAlign(p.CENTER, p.CENTER); p.textSize(16); // Base text size currentPhiRef.current = initialPhi; // Initialize currentPhiRef }; p.updateWithProps = (props: BlochSphereSketchProps) => { currentTheta = props.theta; currentEvolving = props.evolving; if (!currentEvolving) { // If not evolving, update phi directly from props currentPhiRef.current = props.phi; timeRef.current = 0; // Reset time if we stop evolving } else { // if evolution starts, and phi was manually set, use that as the new base if (!initialEvolving && currentEvolving) { // Check if it just started evolving currentPhiRef.current = props.phi; // Sync with slider before starting evolution timeRef.current = 0; // Reset time for evolution from current slider phi } } }; // Initial call to set up based on props p.updateWithProps({ theta: initialTheta, phi: initialPhi, evolving: initialEvolving, width: canvasSize, height: canvasSize }); p.draw = () => { p.background(p.color('hsl(var(--card))')); // Use CSS variable for background p.orbitControl(2,2,0.1); // Adjust sensitivity for better control if (currentEvolving) { currentPhiRef.current = (initialPhi + timeRef.current) % (2 * Math.PI); // Ensure phi wraps around timeRef.current += 0.02; } // Sphere properties const sphereRadius = p.min(canvasSize * 0.35, 100); // Responsive radius const axisLength = sphereRadius * 1.3; const labelOffset = sphereRadius * 1.45; const sphereDetail = Math.max(8, Math.floor(sphereRadius / 10)); // Detail based on size // Draw Bloch sphere (wireframe) p.push(); p.noFill(); p.stroke(p.color('hsl(var(--muted-foreground))')); // Use CSS variable p.strokeWeight(0.5); p.sphere(sphereRadius, sphereDetail, sphereDetail - 4 > 0 ? sphereDetail -4 : 8); p.pop(); // Draw axes p.strokeWeight(1.5); // X-axis (red-ish, from chart colors) p.stroke(p.color('hsl(var(--chart-3))')); p.line(-axisLength, 0, 0, axisLength, 0, 0); // Y-axis (green-ish) p.stroke(p.color('hsl(var(--chart-4))')); p.line(0, -axisLength, 0, 0, axisLength, 0); // Z-axis (blue-ish, primary) p.stroke(p.color('hsl(var(--primary))')); p.line(0, 0, -axisLength, 0, 0, axisLength); // Label axes const labelSize = Math.max(10, sphereRadius / 8); p.push(); p.fill(p.color('hsl(var(--foreground))')); p.noStroke(); p.textSize(labelSize); // Defensive: get cam, pan, tilt let cam = (p as any)._renderer?.camera; const pan = typeof cam?.pan === 'function' ? cam.pan() : 0; const tilt = typeof cam?.tilt === 'function' ? cam.tilt() : 0; p.push(); p.translate(labelOffset, 0, 0); p.rotateY(-pan); p.rotateX(-tilt); p.text("X |+⟩", 0, 0); p.pop(); p.push(); p.translate(0, labelOffset, 0); p.rotateY(-pan); p.rotateX(-tilt); p.text("Y |+i⟩", 0, 0); p.pop(); p.push(); p.translate(0, 0, labelOffset); p.rotateY(-pan); p.rotateX(-tilt); p.text("Z |0⟩", 0, 0); p.pop(); p.push(); p.translate(0, 0, -labelOffset);p.rotateY(-pan); p.rotateX(-tilt); p.text(" |1⟩", 0, 0); p.pop(); p.pop(); // Calculate Bloch vector components const x = Math.sin(currentTheta) * Math.cos(currentPhiRef.current); const y = Math.sin(currentTheta) * Math.sin(currentPhiRef.current); const z = Math.cos(currentTheta); // Draw state vector p.push(); p.stroke(p.color('hsl(var(--accent))')); // Use CSS variable p.strokeWeight(2.5); p.line(0, 0, 0, x * sphereRadius, y * sphereRadius, z * sphereRadius); p.fill(p.color('hsl(var(--accent))')); // Use CSS variable p.noStroke(); p.translate(x * sphereRadius, y * sphereRadius, z * sphereRadius); p.sphere(sphereRadius * 0.05); // Smaller sphere for the tip p.pop(); }; (p as any).customProps = (newProps: BlochSphereSketchProps) => { p.updateWithProps(newProps); }; p.windowResized = () => { if (sketchRef.current) { const newSize = Math.min(sketchRef.current.offsetWidth, sketchRef.current.offsetHeight, 400); p.resizeCanvas(newSize, newSize); } }; }; p5InstanceRef.current = new P5(sketch, sketchRef.current!); }); } return () => { if (p5InstanceRef.current) { p5InstanceRef.current.remove(); p5InstanceRef.current = null; } }; }, []); // Runs once on mount useEffect(() => { if (p5InstanceRef.current && (p5InstanceRef.current as any).customProps) { (p5InstanceRef.current as any).customProps({ theta: initialTheta, phi: initialPhi, evolving: initialEvolving, width, height }); } if (!initialEvolving) { // If evolution is paused by prop change currentPhiRef.current = initialPhi; // Ensure the ref matches the slider timeRef.current = 0; // Reset internal time to avoid jump on resume if phi was changed } }, [initialTheta, initialPhi, initialEvolving, width, height]); return
; }; export default BlochSphereSketch;