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.