Skip to main content

Canvas Components

The canvas components handle rendering the ray traced image and scene selection.

RaytracerCanvas​

The main rendering component that interfaces with the WASM module.

Location​

src/components/canvas/RaytracerCanvas.jsx

Props​

PropTypeDescription
wasmModuleObjectLoaded WASM module
lightObjectLight position {x, y, z}
materialObjectMaterial properties
cameraObjectCamera state
viewObjectView settings
scenePresetnumberActive scene preset ID
onCameraChangeFunctionCamera update callback
onRenderTimeFunctionRender time callback

Core Logic​

Render Function​

const renderFrame = useCallback(() => {
if (!wasmModule || !canvasRef.current) return;

const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const resolution = view.resolution;

// Update WASM state
wasmModule.updateLight(light.x, light.y, light.z);
wasmModule.updateMaterial(material.specular, material.shininess, material.reflectivity);
wasmModule.updateSphereColor(material.color.r, material.color.g, material.color.b);
wasmModule.updateCamera(camera.position.x, camera.position.y, camera.position.z);
wasmModule.setCameraTarget(camera.target.x, camera.target.y, camera.target.z);
wasmModule.setCameraFov(camera.fov);
// ... more updates

// Render and measure time
const startTime = performance.now();
const pixelVector = wasmModule.render(resolution, resolution);
const endTime = performance.now();
onRenderTime(endTime - startTime);

// Copy to JavaScript array
const pixelData = new Uint8ClampedArray(pixelVector.size());
for (let i = 0; i < pixelVector.size(); i++) {
pixelData[i] = pixelVector.get(i);
}
pixelVector.delete(); // Free C++ memory!

// Draw to canvas
const imageData = new ImageData(pixelData, resolution, resolution);
ctx.putImageData(imageData, 0, 0);
}, [wasmModule, light, material, camera, view, scenePreset, onRenderTime]);

Debounced Rendering​

useEffect(() => {
if (renderRequestRef.current) {
cancelAnimationFrame(renderRequestRef.current);
}
renderRequestRef.current = requestAnimationFrame(renderFrame);

return () => {
if (renderRequestRef.current) {
cancelAnimationFrame(renderRequestRef.current);
}
};
}, [renderFrame]);

Mouse Orbit​

const handleMouseMove = (e) => {
if (!isDragging || !wasmModule) return;

const deltaX = e.clientX - lastMousePos.current.x;
const deltaY = e.clientY - lastMousePos.current.y;
lastMousePos.current = { x: e.clientX, y: e.clientY };

wasmModule.orbitCamera(deltaX, deltaY);

onCameraChange({
...camera,
position: {
x: wasmModule.getCameraX(),
y: wasmModule.getCameraY(),
z: wasmModule.getCameraZ()
}
});
};

Scroll Zoom​

const handleWheel = (e) => {
if (!wasmModule) return;
e.preventDefault();

const delta = e.deltaY * 0.01;
wasmModule.zoomCamera(delta);

onCameraChange({
...camera,
position: {
x: wasmModule.getCameraX(),
y: wasmModule.getCameraY(),
z: wasmModule.getCameraZ()
}
});
};

Responsive Sizing​

The canvas dynamically sizes to fill its container:

useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const maxSize = Math.min(rect.width - 20, rect.height - 20);
const size = Math.max(300, maxSize);
setDisplaySize({ width: size, height: size });
}
};

updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);

SceneToolbar​

The toolbar above the canvas for quick scene preset selection.

Location​

src/components/canvas/SceneToolbar.jsx

Props​

PropTypeDescription
activePresetnumberCurrently selected preset
onPresetChangeFunctionCallback when preset changes
sphereCountnumberNumber of spheres in scene
disabledbooleanWhether controls are disabled

Preset Definitions​

const SCENE_PRESETS = [
{ id: 0, name: 'Single', icon: '🔴' },
{ id: 1, name: 'Three', icon: '🎱' },
{ id: 2, name: 'Mirror', icon: '🪞' },
{ id: 3, name: 'Rainbow', icon: '🌈' },
];

Rendering​

function SceneToolbar({ activePreset, onPresetChange, sphereCount, disabled }) {
return (
<div className="scene-toolbar">
<div className="toolbar-section">
<span className="toolbar-label">Scene</span>
<div className="preset-buttons">
{SCENE_PRESETS.map((preset) => (
<button
key={preset.id}
className={`preset-btn ${activePreset === preset.id ? 'active' : ''}`}
onClick={() => onPresetChange(preset.id)}
disabled={disabled}
>
<span className="preset-icon">{preset.icon}</span>
<span className="preset-name">{preset.name}</span>
</button>
))}
</div>
</div>

<div className="toolbar-info">
<span className="info-chip">
{sphereCount} sphere{sphereCount > 1 ? 's' : ''}
</span>
</div>
</div>
);
}

Styling​

.scene-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-sm) var(--space-md);
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
}

.preset-btn.active {
background: var(--accent-primary);
color: white;
}