Created
September 29, 2025 02:39
-
-
Save amit08255/aa5f35ff8ae25ba5dd571ba782dd2bcc to your computer and use it in GitHub Desktop.
React Worker + OffscreenCanvas for Performant UI
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
| 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; |
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
| // 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> | |
| ); | |
| } |
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
| // 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]); | |
| }; |
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
| // 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} />; | |
| } |
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
| // 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 }); | |
| }) | |
| ); | |
| }; |
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
| // 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 | |
| }; |
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
| // 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; | |
| } |
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
| // 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; | |
| } |
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
| 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