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;
}