A Virtual Dmd (Dot Matrix Display) powered by HTML5 Canvas and WebGPU(optional)

Quick demonstration videos (tests before using mpf):
https://www.youtube.com/watch?v=MtAN4vKRLQ0
https://www.youtube.com/watch?v=MaJZQCTSiOg
https://www.youtube.com/watch?v=q58dZAbNXe8
WebGPU now ships by default in modern browsers (Chrome/Edge 113+), so the GPU-enabling flags that
were once required (such as --enable-unsafe-webgpu, --enable-features=Vulkan,UseSkiaRenderer,
--enable-gpu-rasterization) are no longer needed.
The demo plays a video without a user gesture, so depending on your platform you may still want to launch the browser with autoplay enabled:
--autoplay-policy=no-user-gesture-required
--ignore-autoplay-restrictions
https://bzhmaddog.github.io/H5DMD/index.html
If it doesn't load check the developer console for errors
import {
CanvasLayer, ChromaKeyRenderer, Dmd, DmdOptions, DotShape, Easing,
LayerGroup, rendererEntry, ShakyRenderer, TextLayer,
} from "h5dmd";
const canvas = document.getElementById("output") as HTMLCanvasElement;
const dmd = new Dmd(canvas, {
dotSize: 4,
dotSpace: 1,
dotShape: DotShape.Square,
backgroundBrightness: 14,
brightness: 1,
showFPS: true,
});
await dmd.init(); // set up the renderers (WebGPU when available)
dmd.run(); // start the render loop
// Add a canvas layer and draw an image into it
dmd.addLayer(CanvasLayer, "bg", {}, (layer) => {
fetch("background.png")
.then((response) => response.blob())
.then(createImageBitmap)
.then((bitmap) => layer.setDrawFunction(({ drawBitmap }) => drawBitmap(bitmap)));
});
// Add a layer with a renderer declared up-front in the options.
// rendererEntry() infers the params type from the class, so excess/wrong-typed
// properties are caught at compile time.
dmd.addLayer(VideoLayer, "video-chroma", {
autoplay: true,
renderers: [
rendererEntry("chroma", ChromaKeyRenderer, { color: [0, 255, 0], threshold: 20 })
]
}, (layer) => {
const video = document.createElement("video");
video.addEventListener("loadeddata", () => layer.setVideo(video));
video.src = "green-screen.webm";
});
// Or add a renderer after construction using the async addRenderer() method.
// The loaded callback accepts async/await so you can await initialisation.
dmd.addLayer(CanvasLayer, "shaky", {}, async (layer) => {
// await ensures the renderer is fully initialised before drawing starts
await layer.addRenderer("shake", ShakyRenderer, { intensity: 2, speed: 8, mode: "sine" });
layer.setDrawFunction(({ fillColor }) => fillColor("#FF0000"));
layer.draw();
});
// LayerGroup is a "virtual" layer: it can be dimensioned, positioned, given renderers,
// a background color/opacity and shown/hidden like any other layer, but it cannot draw
// content itself - instead it composites a set of child layers (which may themselves be
// LayerGroups, nested arbitrarily deep).
const hud = dmd.addLayer(LayerGroup, "hud", {
width: 80,
height: 20,
position: {hAlign: "right", vAlign: "top"},
backgroundColor: "#000000",
backgroundOpacity: 0.5,
});
hud.addLayer(TextLayer, "score", {text: "0", position: {left: 2, top: 2}});
hud.addLayer(CanvasLayer, "icon", {position: {left: 60, top: 2}}, (layer) => {
layer.fillColor("#FFFFFF");
});
// Hiding a group cascades to its children (pausing e.g. a child VideoLayer's playback) and
// restores each child's own prior visibility when the group is shown again.
hud.setVisibility(false);
hud.setVisibility(true);
// Position a layer relative to a sibling instead of the container, via hAlign/vAlign:
// 'constraint' and a *To*Of field naming both the relationship and the target (a sibling's
// id, already added to the same container, or 'parent' - the container itself, the default).
hud.addLayer(TextLayer, "label", {
text: "SCORE",
position: {
hAlign: "constraint", leftToRightOf: "icon", // my left edge, against icon's right edge
vAlign: "constraint", topToTopOf: "icon", // my top edge, against icon's top edge
},
});
// Fade a layer in/out (operates on layer opacity)
const layer = dmd.getLayer("bg");
await layer.fadeOut(1000); // fade out over 1s (default easing: easeOutSine)
await layer.fadeIn(500, Easing.easeInSine); // fade in with custom easing
// Or use the Dmd convenience methods (handles visibility automatically)
await dmd.fadeLayerOut("bg", 1000);
await dmd.fadeLayerIn("bg", 500);
// Fade the entire DMD brightness
await dmd.fadeOut(1000);
await dmd.fadeIn(1000);
For a complete example see Demo/advanced/main.ts.
npm install
#Transpile all files
npm run build
#Watch file changes
npm run watch
#Run eslinter
npm run lint
#Run tests
npm run test
#Build documentation
npm run build:docs
The Demo app imports this library as the h5dmd package. To run it against your local
build, expose the library through a global link:
# From the repository root: build the library so dist/ exists, then link it
npm install
npm run build
npm link
Then link it inside the demo and start the dev server:
cd Demo
npm install
npm link h5dmd
npm run dev
See Demo/README.md for more details.
The version number lives in package.json, in src/dmd.ts (Dmd.version) and in the
git release tag. Use npm version to bump all of them in one step — a version
lifecycle script (scripts/sync-version.mjs) rewrites Dmd.version and stages it so it
lands in the same commit as package.json:
# bump patch / minor / major (creates the commit and the vX.Y.Z tag)
npm version patch
# push the commit and the tag
git push --follow-tags
Then publish a GitHub Release pointing at the new tag. That triggers the
Release workflow, which verifies that the tag,
package.json and Dmd.version all match before publishing to npm.