Skip to content

Instantly share code, notes, and snippets.

@amit08255
Created September 29, 2025 02:39
Show Gist options
  • Save amit08255/aa5f35ff8ae25ba5dd571ba782dd2bcc to your computer and use it in GitHub Desktop.
Save amit08255/aa5f35ff8ae25ba5dd571ba782dd2bcc to your computer and use it in GitHub Desktop.
React Worker + OffscreenCanvas for Performant UI
import React, { useState } from 'react';
import { MapCanvas } from './components/MapCanvas.jsx';
import { HeatmapControls } from './components/HeatmapControls.jsx';
function App() {
const [region, setRegion] = useState('NYC');
const [zoom, setZoom] = useState(12);
const [heatmapOptions, setHeatmapOptions] = useState({
radius: 20,
gradient: 'viridis',
intensity: 0.8,
pointCount: 200,
clusters: 3
});
return (
<div style={{ margin: '20px' }}>
<HeatmapControls options={heatmapOptions} onChange={setHeatmapOptions} />
<MapCanvas region={region} zoom={zoom} heatmapOptions={heatmapOptions} />
</div>
);
}
export default App;
// src/components/HeatmapControls.jsx
import React from 'react';
// Available color gradients
const GRADIENTS = [
{ label: 'Viridis', value: 'viridis' },
{ label: 'Hot', value: 'hot' },
{ label: 'Cool', value: 'cool' },
{ label: 'Rainbow', value: 'rainbow' },
];
export function HeatmapControls({ options, onChange }) {
// Handlers for each control
const handleRadiusChange = (e) => {
onChange({ ...options, radius: Number(e.target.value) });
};
const handleIntensityChange = (e) => {
onChange({ ...options, intensity: Number(e.target.value) });
};
const handleGradientChange = (e) => {
onChange({ ...options, gradient: e.target.value });
};
const handlePointCountChange = (e) => {
onChange({ ...options, pointCount: Number(e.target.value) });
};
const handleClustersChange = (e) => {
onChange({ ...options, clusters: Number(e.target.value) });
};
return (
<div className="heatmap-controls" style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '18px',
marginBottom: '24px',
maxWidth: '700px',
background: 'linear-gradient(90deg, #f8fafc 60%, #e0e7ef 100%)',
boxShadow: '0 2px 8px 0 #0001'
}}>
<h3 style={{ marginBottom: '14px', fontWeight: 600, color: '#2d3748' }}>Heatmap Settings</h3>
{/* Point Count Slider */}
<label style={{ display: 'block', marginBottom: '10px' }}>
Points: {options.pointCount || 200}
<input
type="range"
min="10"
max="1000"
step="10"
value={options.pointCount || 200}
onChange={handlePointCountChange}
style={{ width: '100%', marginTop: '5px' }}
/>
</label>
{/* Clusters Slider */}
<label style={{ display: 'block', marginBottom: '10px' }}>
Clusters: {options.clusters || 3}
<input
type="range"
min="1"
max="10"
step="1"
value={options.clusters || 3}
onChange={handleClustersChange}
style={{ width: '100%', marginTop: '5px' }}
/>
</label>
{/* Radius Slider */}
<label style={{ display: 'block', marginBottom: '10px' }}>
Radius: {options.radius} px
<input
type="range"
min="5"
max="50"
step="1"
value={options.radius}
onChange={handleRadiusChange}
style={{ width: '100%', marginTop: '5px' }}
/>
</label>
{/* Intensity Slider */}
<label style={{ display: 'block', marginBottom: '10px' }}>
Intensity: {options.intensity}
<input
type="range"
min="0"
max="2"
step="0.01"
value={options.intensity}
onChange={handleIntensityChange}
style={{ width: '100%', marginTop: '5px' }}
/>
</label>
{/* Gradient Dropdown */}
<label style={{ display: 'block', marginBottom: '10px' }}>
Color Gradient:
<select
value={options.gradient}
onChange={handleGradientChange}
style={{ width: '100%', marginTop: '5px' }}
>
{GRADIENTS.map(grad => (
<option key={grad.value} value={grad.value}>{grad.label}</option>
))}
</select>
</label>
</div>
);
}
// src/workers/heatmapGenerator.worker.js
// HD sharp heatmap: direct pixel writing, no blur, small radius, high point count
self.onmessage = function(event) {
const { tiles, options } = event.data;
const width = 1024;
const height = 1024;
const intensityGrid = new Float32Array(width * height);
// HD: many points, very small radius, no kernel blur
const pointCount = options.pointCount || 8000;
const clusters = options.clusters || 10;
const radius = options.radius || 6;
const strength = options.intensity || 1;
// Generate cluster centers
const centers = [];
for (let c = 0; c < clusters; c++) {
centers.push({
x: Math.random() * width * 0.7 + width * 0.15,
y: Math.random() * height * 0.7 + height * 0.15
});
}
// Generate points around clusters
for (let i = 0; i < pointCount; i++) {
const center = centers[Math.floor(Math.random() * clusters)];
const angle = Math.random() * 2 * Math.PI;
const dist = Math.random() * radius * 8;
const x0 = Math.round(center.x + Math.cos(angle) * dist);
const y0 = Math.round(center.y + Math.sin(angle) * dist);
// Write a single pixel (or a tiny 3x3 block for visibility)
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const x = x0 + dx;
const y = y0 + dy;
if (x >= 0 && x < width && y >= 0 && y < height) {
intensityGrid[y * width + x] += strength;
}
}
}
}
self.postMessage({ heatmapData: intensityGrid.buffer }, [intensityGrid.buffer]);
};
// src/components/MapCanvas.jsx
import React, { useEffect, useRef } from 'react';
import { useWorkers } from '../hooks/useWorkers';
export function MapCanvas({ region, zoom, heatmapOptions }) {
const canvasRef = useRef(null);
const workers = useWorkers();
useEffect(() => {
// Handlers
const handleTileLoader = async (e) => {
const tiles = e.data;
workers.heatmapGen.postMessage({ tiles, options: heatmapOptions });
};
const handleHeatmapGen = (e) => {
const { heatmapData } = e.data;
let offscreen = null;
if (typeof OffscreenCanvas !== 'undefined') {
offscreen = new OffscreenCanvas(1024, 1024);
workers.renderer.postMessage({ canvas: offscreen, data: heatmapData, gradient: heatmapOptions.gradient }, [offscreen]);
}
};
const handleRenderer = (e) => {
const { bitmap } = e.data;
const ctx = canvasRef.current.getContext('bitmaprenderer');
ctx.transferFromImageBitmap(bitmap);
};
workers.tileLoader.onmessage = handleTileLoader;
workers.heatmapGen.onmessage = handleHeatmapGen;
workers.renderer.onmessage = handleRenderer;
// Start pipeline
workers.tileLoader.postMessage({ region, zoom });
// Cleanup
return () => {
workers.tileLoader.onmessage = null;
workers.heatmapGen.onmessage = null;
workers.renderer.onmessage = null;
};
}, [region, zoom, heatmapOptions, workers]);
return <canvas ref={canvasRef} width={1024} height={1024} />;
}
// src/workers/renderer.worker.js
// Color gradients
const viridis = [
[68, 1, 84], [68, 2, 86], [69, 4, 87], [69, 5, 89], [70, 7, 90], [70, 8, 92], [70, 10, 93], [70, 11, 94],
// ... (rest unchanged)
[72, 223, 255]
];
// Dark 'hot' colormap: black → red only
const hot = Array.from({length:256}, (_,i)=>{
let t = i/255;
return [
Math.round(255 * t),
0,
0
];
});
const cool = Array.from({length:256}, (_,i)=>{
let t = i/255;
return [Math.round(255*t), Math.round(255*(1-t)), 255];
});
const rainbow = Array.from({length:256}, (_,i)=>{
let t = i/255;
let r = Math.round(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(4*t-3))));
let g = Math.round(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(4*t-2))));
let b = Math.round(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(4*t-1))));
return [r,g,b];
});
self.onmessage = function(event) {
const { canvas, data, options = {}, gradient = 'viridis' } = event.data;
const width = canvas.width;
const height = canvas.height;
const ctx = canvas.getContext('2d');
const intensityGrid = new Float32Array(data);
// Find max intensity for normalization, ignore top 0.1% outliers for better color contrast
const sorted = Array.from(intensityGrid).sort((a, b) => a - b);
const max = sorted[Math.floor(sorted.length * 0.999)] || 1;
function getColor(i) {
// Log normalization for high contrast
const t = Math.min(1, Math.log1p(i) / Math.log1p(max));
let palette = viridis;
if (gradient === 'hot') palette = hot;
else if (gradient === 'cool') palette = cool;
else if (gradient === 'rainbow') palette = rainbow;
const idx = Math.floor(t * (palette.length - 1));
const [r, g, b] = palette[idx];
// Alpha: fade in with intensity, sharper
const a = Math.round(255 * Math.pow(t, 1.2));
return [r, g, b, a];
}
// Draw pixels
const imageData = ctx.createImageData(width, height);
for (let i = 0; i < intensityGrid.length; i++) {
const [r, g, b, a] = getColor(intensityGrid[i]);
const p = i * 4;
imageData.data[p] = r;
imageData.data[p+1] = g;
imageData.data[p+2] = b;
imageData.data[p+3] = a;
}
ctx.putImageData(imageData, 0, 0);
// Transfer rendered bitmap for display
canvas.convertToBlob().then(blob =>
createImageBitmap(blob).then(bitmap => {
self.postMessage({ bitmap });
})
);
};
// src/workers/tileLoader.worker.js
self.onmessage = async function(event) {
const { region, zoom } = event.data;
// Dummy: Generate 3 synthetic colored tiles (no network)
const tileCount = 3;
const width = 256;
const height = 256;
const colors = ['#d32f2f', '#1976d2', '#388e3c'];
// Helper to generate colored tile as ImageBitmap
async function createTile(color) {
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
// Add some noise/variation for demo
for (let i = 0; i < 500; i++) {
ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.15})`;
ctx.beginPath();
ctx.arc(
Math.random() * width,
Math.random() * height,
Math.random() * 8 + 2,
0, 2 * Math.PI
);
ctx.fill();
}
return await canvas.transferToImageBitmap();
}
// Create all tiles
const bitmaps = [];
for (let i = 0; i < tileCount; i++) {
bitmaps.push(await createTile(colors[i % colors.length]));
}
self.postMessage(bitmaps, bitmaps); // Transfer ImageBitmaps efficiently
};
// src/hooks/useOffscreenCanvas.js
import { useRef } from 'react';
export function useOffscreenCanvas(width, height) {
const offscreenRef = useRef(null);
if (!offscreenRef.current && typeof OffscreenCanvas !== 'undefined') {
offscreenRef.current = new OffscreenCanvas(width, height);
}
return offscreenRef.current;
}
// src/hooks/useWorkers.js
import { useRef, useEffect } from 'react';
import * as Comlink from 'comlink';
export function useWorkers() {
const workersRef = useRef({});
useEffect(() => {
workersRef.current.tileLoader = new Worker(new URL('../workers/tileLoader.worker.js', import.meta.url));
workersRef.current.heatmapGen = new Worker(new URL('../workers/heatmapGenerator.worker.js', import.meta.url));
workersRef.current.renderer = new Worker(new URL('../workers/renderer.worker.js', import.meta.url));
Comlink.wrap(workersRef.current.tileLoader); // for async calls
return () => {
Object.values(workersRef.current).forEach(w => w.terminate());
};
}, []);
return workersRef.current;
}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { comlink } from 'vite-plugin-comlink'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
worker: { plugins: [comlink()] },
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment