Created
April 29, 2025 16:31
-
-
Save darrenwiens/b386788b4a134f40bcc9c2d2a9c907fb to your computer and use it in GitHub Desktop.
Display a video derived from netcdf, all in browser
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Firesmoke Video Viewer</title> | |
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"> | |
<link href="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.css" rel="stylesheet"> | |
<script src="https://api.mapbox.com/mapbox-gl-js/v3.11.0/mapbox-gl.js"></script> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
} | |
#map { | |
position: absolute; | |
top: 0; | |
bottom: 0; | |
width: 100%; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="map"></div> | |
<canvas id="canvas" width="1081" height="381"></canvas> | |
<script type="module"> | |
import * as netcdfjs from 'https://esm.run/netcdfjs'; | |
import { FFmpeg } from "/assets/ffmpeg/package/dist/esm/index.js"; // https://ffmpegwasm.netlify.app/ | |
import { fetchFile } from "/assets/util/package/dist/esm/index.js"; // https://ffmpegwasm.netlify.app/ | |
let ffmpeg = null; | |
async function processNetCDF(url) { | |
const res = await fetch(url); | |
const buf = await res.arrayBuffer(); | |
const reader = new netcdfjs.NetCDFReader(buf); | |
console.log(reader.getDataVariable('PM25')); | |
let floatArrays = reader.getDataVariable('PM25'); | |
const width = 1081, height = 381; | |
if (ffmpeg === null) { | |
ffmpeg = new FFmpeg(); | |
await ffmpeg.load({ | |
coreURL: "https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/esm/ffmpeg-core.js", | |
}); | |
} | |
const promises = floatArrays.map(async (floatArray, i) => { | |
let normalized = floatArray.map(v => (v * 255)); | |
const rgba = new Uint8ClampedArray(width * height * 4); | |
for (let j = 0; j < normalized.length; j++) { | |
const val = Math.round(normalized[j]); | |
rgba[j * 4 + 0] = val; | |
rgba[j * 4 + 1] = val; | |
rgba[j * 4 + 2] = val; | |
rgba[j * 4 + 3] = 255; | |
} | |
const canvas = document.createElement('canvas'); | |
canvas.width = width; | |
canvas.height = height; | |
const ctx = canvas.getContext('2d'); | |
ctx.putImageData(new ImageData(rgba, width, height), 0, 0); | |
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')); | |
if (!blob) { | |
throw new Error("Canvas toBlob failed."); | |
} | |
const buffer = new Uint8Array(await blob.arrayBuffer()); | |
const filename = `frame_${String(i).padStart(3, '0')}.png`; | |
await ffmpeg.writeFile(filename, buffer); | |
}) | |
try { | |
await Promise.all(promises); | |
} catch (e) { | |
console.error("Error during ffmpeg write:", e); | |
} | |
await ffmpeg.exec([ | |
'-framerate', '10', | |
'-i', 'frame_%03d.png', | |
'-vf', 'vflip', | |
'output.mp4', | |
]); | |
const videoData = await ffmpeg.readFile('output.mp4'); | |
const videoUrl = URL.createObjectURL(new Blob([videoData.buffer], { type: 'video/mp4' })); | |
if (map.getSource('video')) { | |
let videoSource = map.getSource('video') | |
videoSource.setCoordinates([ | |
[-160, 70], | |
[-52, 70], | |
[-52, 32], | |
[-160, 32], | |
]); | |
let videoVideo = videoSource.getVideo(); | |
videoVideo.src = videoUrl; | |
videoVideo.load() | |
videoVideo.play() | |
} else { | |
console.log('making new video source') | |
map.addSource('video', { | |
type: 'video', | |
urls: [videoUrl], | |
coordinates: [ | |
[-160, 70], | |
[-52, 70], | |
[-52, 32], | |
[-160, 32], | |
] | |
}); | |
map.addLayer({ | |
id: 'video', | |
type: 'raster', | |
source: 'video', | |
paint: { | |
'raster-opacity': 0.7, | |
} | |
}); | |
} | |
} | |
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN'; | |
const map = new mapboxgl.Map({ | |
container: 'map', | |
center: [-74.5, 55], | |
zoom: 5 | |
}); | |
processNetCDF('./dispersion.nc'); // pre-downloaded, because CORS: https://firesmoke.ca/forecasts/current/dispersion.nc | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment