All notable changes to this project are documented here. The format follows Keep a Changelog, and this project adheres to Semantic Versioning.
[0.1.1] - 2026-08-21
Fixed
- WebGPU is no longer requested without the shader compilers it needs.
PlayCanvas's WebGPU backend requires
glslangUrlandtwgslUrl— two assets this package does not ship and cannot guess the location of. Asking for it without them made the engine log aTypeErroron every mount, in every browser with a partial WebGPU stack, before quietly falling back to WebGL2. It worked, and it was noise in someone else's console on a page they did not write. WebGL2 is now the default; supply:glslang_urland:twgsl_urlto opt back in.
Documented
app.start()is deliberately never called — it would run PlayCanvas's own tick loop alongside this one and render every frame twice. The consequence, which was not written down anywhere, is that the engine'sframerenderevent never fires, so an LOD or octree splat would never stream. A plain.sogis unaffected.
[0.1.0] - 2026-08-21
Initial release.
Added
<.splat_viewer />— a Phoenix component rendering a Gaussian splat, withsrc,camera,height,autorotate,background,dpr_cap,pause_offscreenandinteractive.- A LiveView hook, shipped in
priv/static/splat_viewer.jsand importable as a node module fromdeps/splat_viewer. SplatViewer.Camera— a viewpoint, accepting either the structsplat_toolsproduces or the JSON that comes back out of a database column.
Fixed — first review pass
Found by running the hook against real PlayCanvas 2.21.4 in headless Chrome. The package's own stub suite passed 17/17 throughout, and could not see any of the first four — the stub was wrong in the same direction as the code, which is its own lesson.
- The device-pixel-ratio cap broke every HiDPI display.
AppBase#resizeCanvastakes CSS pixels and setscanvas.style, applying the device's ownmaxPixelRatioto get the backing store. Passing it DPR-multiplied numbers set the canvas's CSS size to the backing-store number, so the canvas overflowed a container withoverflow: hiddenand the visitor saw a cropped corner of the scene, upscaled. The headline feature did the opposite of what it claimed. - Two viewers on one page broke the first one.
new pc.Entity(name)falls back to a module-global app set by the most recentAppBaseconstructor, so two hooks mounting in the same tick gave the first one's entities to the second one's app — andaddComponentreturnednullagainst systems that did not exist yet. - Teardown during load threw and leaked the canvas.
this.appwas assigned beforeinit(), andAppBase#destroy()readsgraphicsDevice.canvasimmediately — so the throw aborted the rest ofdispose(), out of LiveView'sdestroyed(). - Auto-framing was dead.
gsplat.instanceis alwaysnullin unified mode, which is the 2.x default, so the documented bounding-box fallback silently framed the origin at radius one and every viewer without a stored camera pointed at empty space. :backgroundaccepted colours the renderer cannot read.#fffrendered blue and every named colour rendered black, becauseColor#fromStringisparseInton the hex digits. Now hex only, with shorthand expanded.- One drag put a degenerate camera permanently at NaN —
Math.min/maxdo not filter a NaN. Guarded in the viewer, andCameranow refuses a pose whose position equals its target. - The engine cache ignored the URL, so a second viewer pointed elsewhere silently received the first one's engine and reported itself ready.
engine_path/0was wired to nothing. The config was documented in two places and had no effect; only the JS global worked.- The status element was removed on first success, so no later message — the context-lost notice, a failed rebuild — could ever appear. It is hidden now.
- Restoring a WebGL context ignored
pause_offscreenand forced the state to ready even from error. applySizereallocated the drawing buffer on every call, which during a drag-resize is dozens of multi-megabyte reallocations a second.:heightwas interpolated into the same style attribute as:backgroundwithout a guard, so the one defence was on one of the two values.updated()cleared the flag in-flight builds were watching, so a stale build carried on and overwrote the new one's observers. Replaced with a build token.- A browser with neither WebGL2 nor WebGPU got a null device and a
readystate, showing a blank box with no error.
Design notes
- The engine is not bundled. PlayCanvas is about two megabytes; a Hex package should neither ship that nor pin which copy an application uses. It is imported at runtime from a configurable URL.
- Options are validated in Elixir. A bad camera or a bad colour raises at
render time with a message naming the problem. The same mistake caught in
the hook is a console warning nobody reads, on a page showing an empty box.
:backgroundis interpolated into a style attribute, so it is checked against a colour grammar rather than trusted. - The lifecycle is the substance. Teardown, container-based resizing,
capped device pixel ratio, WebGL context loss, pausing off-screen, and
surviving LiveView re-renders — each is covered by the hook's own test
suite, which runs against a stub DOM under
mix test.