Created
June 8, 2025 00:48
-
-
Save mikebuss/b3d8f4841c37a57a37ec3411faae2d8c to your computer and use it in GitHub Desktop.
Calibration Visual for Blog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client' | |
import React, { useRef, useEffect, useState } from 'react' | |
import { Canvas, useFrame } from '@react-three/fiber' | |
import { OrbitControls } from '@react-three/drei' | |
import * as THREE from 'three' | |
function randomPointInUnitSphere(): THREE.Vector3 { | |
const u = Math.random() | |
const v = Math.random() | |
const theta = 2 * Math.PI * u | |
const phi = Math.acos(2 * v - 1) | |
const r = Math.cbrt(Math.random()) | |
return new THREE.Vector3( | |
r * Math.sin(phi) * Math.cos(theta), | |
r * Math.sin(phi) * Math.sin(theta), | |
r * Math.cos(phi) | |
) | |
} | |
interface SphereBoundsProps { | |
centerRef: React.MutableRefObject<THREE.Vector3[]> | |
scaleRef: React.MutableRefObject<THREE.Vector3[]> | |
index: number | |
radius: number | |
color: string | |
calibrated: boolean | |
} | |
const centerYPoint = 0.4; | |
function SphereBounds({ centerRef, scaleRef, index, radius, color, calibrated }: SphereBoundsProps) { | |
const meshRef = useRef<THREE.Mesh>(null!) | |
useFrame(() => { | |
// If calibrated, force-snap to (0, centerYPoint, 0) so the circle is visually centered | |
meshRef.current.position.copy( | |
calibrated ? new THREE.Vector3(0, centerYPoint, 0) : centerRef.current[index] | |
) | |
meshRef.current.scale.copy(scaleRef.current[index]) | |
}) | |
return ( | |
<mesh ref={meshRef}> | |
<sphereGeometry args={[radius, 32, 32]} /> | |
<meshBasicMaterial | |
color={color} | |
wireframe | |
transparent | |
opacity={calibrated ? 0.02 : 0.1} | |
depthWrite={false} | |
/> | |
</mesh> | |
) | |
} | |
interface ParticlesProps { | |
count: number | |
clusterCentersRef: React.MutableRefObject<THREE.Vector3[]> | |
clusterScalesRef: React.MutableRefObject<THREE.Vector3[]> | |
initialCentersRef: React.MutableRefObject<THREE.Vector3[]> | |
initialScalesRef: React.MutableRefObject<THREE.Vector3[]> | |
calibrated: boolean | |
radius: number | |
moveSpeed: number | |
calibrateSpeed: number | |
} | |
function Particles({ | |
count, | |
clusterCentersRef, | |
clusterScalesRef, | |
initialCentersRef, | |
initialScalesRef, | |
calibrated, | |
radius, | |
moveSpeed, | |
calibrateSpeed, | |
}: ParticlesProps) { | |
const positionArray = useRef<Float32Array>(new Float32Array(count * 3)) | |
const colorArray = useRef<Float32Array>(new Float32Array(count * 3)) | |
const velocities = useRef<THREE.Vector3[]>( | |
Array.from({ length: count }, () => | |
new THREE.Vector3( | |
(Math.random() - 0.5) * moveSpeed, | |
(Math.random() - 0.5) * moveSpeed, | |
(Math.random() - 0.5) * moveSpeed | |
) | |
) | |
) | |
const geomRef = useRef<THREE.BufferGeometry>(null!) | |
useEffect(() => { | |
const centers = clusterCentersRef.current | |
const scales = clusterScalesRef.current | |
for (let i = 0; i < count; i++) { | |
const cluster = i % 3 | |
const p = randomPointInUnitSphere() | |
.multiply(scales[cluster]) | |
.multiplyScalar(radius) | |
.add(centers[cluster]) | |
positionArray.current[i * 3] = p.x | |
positionArray.current[i * 3 + 1] = p.y | |
positionArray.current[i * 3 + 2] = p.z | |
const base = i * 3 | |
if (cluster === 0) { | |
colorArray.current[base] = 1 | |
colorArray.current[base + 1] = 0 | |
colorArray.current[base + 2] = 0 | |
} else if (cluster === 1) { | |
colorArray.current[base] = 0 | |
colorArray.current[base + 1] = 1 | |
colorArray.current[base + 2] = 0 | |
} else { | |
colorArray.current[base] = 0 | |
colorArray.current[base + 1] = 0 | |
colorArray.current[base + 2] = 1 | |
} | |
} | |
if (geomRef.current) { | |
geomRef.current.setAttribute('position', new THREE.BufferAttribute(positionArray.current, 3)) | |
geomRef.current.setAttribute('color', new THREE.BufferAttribute(colorArray.current, 3)) | |
} | |
}, [count, radius, clusterCentersRef, clusterScalesRef]) | |
// Center for calibrated state is (0, centerYPoint, 0) to match SphereBounds | |
const calibratedCenter = useRef<THREE.Vector3>(new THREE.Vector3(0, centerYPoint, 0)) | |
const unitScale = useRef<THREE.Vector3>(new THREE.Vector3(1, 1, 1)) | |
useFrame(() => { | |
const geometry = geomRef.current | |
if (!geometry || !geometry.attributes.position) return | |
const centers = clusterCentersRef.current | |
const scales = clusterScalesRef.current | |
const initialCenters = initialCentersRef.current | |
const initialScales = initialScalesRef.current | |
for (let c = 0; c < 3; c++) { | |
const targetCenter = calibrated ? calibratedCenter.current : initialCenters[c] | |
centers[c].lerp(targetCenter, calibrateSpeed) | |
// Snap once very close | |
if (calibrated && centers[c].distanceToSquared(calibratedCenter.current) < 1e-6) { | |
centers[c].copy(calibratedCenter.current) | |
} | |
const targetScale = calibrated ? unitScale.current : initialScales[c] | |
scales[c].lerp(targetScale, calibrateSpeed) | |
} | |
for (let i = 0; i < count; i++) { | |
const idx = i * 3 | |
const cluster = i % 3 | |
const center = centers[cluster] | |
const scaleVec = scales[cluster] | |
positionArray.current[idx] += velocities.current[i].x | |
positionArray.current[idx + 1] += velocities.current[i].y | |
positionArray.current[idx + 2] += velocities.current[i].z | |
const dx = positionArray.current[idx] - center.x | |
const dy = positionArray.current[idx + 1] - center.y | |
const dz = positionArray.current[idx + 2] - center.z | |
const a = scaleVec.x * radius | |
const b = scaleVec.y * radius | |
const c = scaleVec.z * radius | |
const ellDistSq = dx * dx / (a * a) + dy * dy / (b * b) + dz * dz / (c * c) | |
if (ellDistSq > 1) { | |
const factor = 0.999 / Math.sqrt(ellDistSq) | |
const newX = center.x + dx * factor | |
const newY = center.y + dy * factor | |
const newZ = center.z + dz * factor | |
positionArray.current[idx] = newX | |
positionArray.current[idx + 1] = newY | |
positionArray.current[idx + 2] = newZ | |
const normal = new THREE.Vector3(dx / (a * a), dy / (b * b), dz / (c * c)).normalize() | |
velocities.current[i].reflect(normal) | |
} | |
} | |
geometry.attributes.position.needsUpdate = true | |
}) | |
return ( | |
<points> | |
<bufferGeometry ref={geomRef} /> | |
<pointsMaterial vertexColors size={0.05} sizeAttenuation /> | |
</points> | |
) | |
} | |
interface CalibrationParticlesProps { | |
particleCount?: number | |
offset?: number | |
radius?: number | |
moveSpeed?: number | |
calibrateSpeed?: number | |
className?: string | |
hideControls?: boolean | |
} | |
export default function CalibrationParticles({ | |
particleCount = 300, | |
offset = 0.9, | |
radius = 1, | |
moveSpeed = 0.035, | |
calibrateSpeed = 0.1, | |
className = 'w-full h-[300px] relative', | |
hideControls = false, | |
}: CalibrationParticlesProps) { | |
const [calibrated, setCalibrated] = useState(false) | |
useEffect(() => { | |
if (hideControls) { | |
const id = setInterval(() => setCalibrated(prev => !prev), 4000) | |
return () => clearInterval(id) | |
} | |
}, [hideControls]) | |
const initialCentersRef = useRef<THREE.Vector3[]>([ | |
new THREE.Vector3(-offset, 0, 0), | |
new THREE.Vector3(offset, 0, 0), | |
new THREE.Vector3(0, offset, 0), | |
]) | |
const initialScalesRef = useRef<THREE.Vector3[]>([ | |
new THREE.Vector3(1.3, 0.8, 1), | |
new THREE.Vector3(0.8, 1.3, 1), | |
new THREE.Vector3(1, 0.8, 1.3), | |
]) | |
const clusterCentersRef = useRef<THREE.Vector3[]>( | |
initialCentersRef.current.map(v => v.clone()) | |
) | |
const clusterScalesRef = useRef<THREE.Vector3[]>( | |
initialScalesRef.current.map(v => v.clone()) | |
) | |
return ( | |
<div className={className}> | |
<Canvas | |
camera={{ position: [0, 0, 4], fov: 60 }} | |
className="w-full h-full" | |
> | |
<ambientLight intensity={0.5} /> | |
<Particles | |
count={particleCount} | |
clusterCentersRef={clusterCentersRef} | |
clusterScalesRef={clusterScalesRef} | |
initialCentersRef={initialCentersRef} | |
initialScalesRef={initialScalesRef} | |
calibrated={calibrated} | |
radius={radius} | |
moveSpeed={moveSpeed} | |
calibrateSpeed={calibrateSpeed} | |
/> | |
<SphereBounds | |
centerRef={clusterCentersRef} | |
scaleRef={clusterScalesRef} | |
index={0} | |
radius={radius} | |
color="#ff0000" | |
calibrated={calibrated} | |
/> | |
<SphereBounds | |
centerRef={clusterCentersRef} | |
scaleRef={clusterScalesRef} | |
index={1} | |
radius={radius} | |
color="#00ff00" | |
calibrated={calibrated} | |
/> | |
<SphereBounds | |
centerRef={clusterCentersRef} | |
scaleRef={clusterScalesRef} | |
index={2} | |
radius={radius} | |
color="#0000ff" | |
calibrated={calibrated} | |
/> | |
<OrbitControls | |
enableDamping | |
minDistance={3} | |
maxDistance={6} | |
maxPolarAngle={Math.PI * 0.6} | |
/> | |
</Canvas> | |
{!hideControls && ( | |
<button | |
style={{ | |
position: 'absolute', | |
bottom: 20, | |
left: '50%', | |
transform: 'translateX(-50%)', | |
padding: '8px 16px', | |
background: '#111', | |
color: '#fff', | |
border: 'none', | |
borderRadius: 4, | |
cursor: 'pointer', | |
}} | |
onClick={() => setCalibrated(!calibrated)} | |
> | |
{calibrated ? 'Reset' : 'Calibrate'} | |
</button> | |
)} | |
{hideControls && <div className="-mt-6" />} | |
</div> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment