<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sara Loera</title>
    <description>The latest articles on DEV Community by Sara Loera (@saraeloop).</description>
    <link>https://dev.to/saraeloop</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866832%2F8f23f9b1-3c5e-4c62-b65e-56eeb42671e6.png</url>
      <title>DEV Community: Sara Loera</title>
      <link>https://dev.to/saraeloop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saraeloop"/>
    <language>en</language>
    <item>
      <title>HTML-in-Canvas Feels Fake Until You Try to Build With It</title>
      <dc:creator>Sara Loera</dc:creator>
      <pubDate>Wed, 13 May 2026 19:49:44 +0000</pubDate>
      <link>https://dev.to/saraeloop/html-in-canvas-feels-fake-until-you-try-to-build-with-it-53pp</link>
      <guid>https://dev.to/saraeloop/html-in-canvas-feels-fake-until-you-try-to-build-with-it-53pp</guid>
      <description>&lt;p&gt;HTML-in-Canvas feels fake the first time it works.&lt;/p&gt;

&lt;p&gt;Real DOM.&lt;br&gt;&lt;br&gt;
Real CSS.&lt;br&gt;&lt;br&gt;
Real layout.&lt;/p&gt;

&lt;p&gt;Inside canvas.&lt;/p&gt;

&lt;p&gt;You write a tiny &lt;code&gt;canvas.onpaint&lt;/code&gt;, call &lt;code&gt;drawElementImage()&lt;/code&gt;, and the browser just… does it. Your styled HTML is suddenly part of a canvas frame.&lt;/p&gt;

&lt;p&gt;Beautiful.&lt;/p&gt;

&lt;p&gt;Then you try to build an actual app.&lt;/p&gt;

&lt;p&gt;Now you have multiple surfaces, resize logic, export timing, React unmounts, cleanup, invalidation, and the classic:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;wait, is this number CSS pixels or canvas pixels?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the part where the cool browser primitive turns into lifecycle work.&lt;/p&gt;

&lt;p&gt;So I built Prism.&lt;/p&gt;


&lt;h2&gt;
  
  
  First: what is HTML-in-Canvas?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/WICG/html-in-canvas" rel="noopener noreferrer"&gt;WICG HTML-in-Canvas proposal&lt;/a&gt; lets a canvas render real HTML elements directly.&lt;/p&gt;

&lt;p&gt;Not screenshots.&lt;/p&gt;

&lt;p&gt;Not &lt;code&gt;html2canvas&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not SVG &lt;code&gt;foreignObject&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Actual DOM, painted into a canvas frame by the browser.&lt;/p&gt;

&lt;p&gt;A tiny example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;canvas&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"canvas"&lt;/span&gt; &lt;span class="na"&gt;layoutsubtree&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"panel"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 400px; height: 200px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Real HTML&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Real CSS. Real fonts. Real layout.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/canvas&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;panel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d context unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onpaint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawElementImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPaint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rough shape is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;layoutsubtree&lt;/code&gt; opts canvas children into layout.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drawElementImage()&lt;/code&gt; draws a child element into the canvas.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onpaint&lt;/code&gt; fires when the browser says the canvas subtree needs painting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For one surface, it is almost suspiciously nice.&lt;/p&gt;

&lt;p&gt;But apps are never one surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that gets messy
&lt;/h2&gt;

&lt;p&gt;With the raw API, your app has to answer a bunch of boring-but-important questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which DOM elements are canvas surfaces?&lt;/li&gt;
&lt;li&gt;Who updates their bounds?&lt;/li&gt;
&lt;li&gt;Who asks the browser for a new paint?&lt;/li&gt;
&lt;li&gt;What happens when a component unmounts?&lt;/li&gt;
&lt;li&gt;What happens when the runtime is destroyed?&lt;/li&gt;
&lt;li&gt;How do you wait for a paint-ready frame before export?&lt;/li&gt;
&lt;li&gt;How do you keep CSS pixels and backing-store pixels straight?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single React component can already start to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sceneRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the DOM element to draw&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onpaint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawElementImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Simplified: uses CSS pixels for backing-store size.&lt;/span&gt;
  &lt;span class="c1"&gt;// In production, use DPR-aware sizing via devicePixelContentBoxSize&lt;/span&gt;
  &lt;span class="c1"&gt;// or multiply contentRect dimensions by devicePixelRatio.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resizeObserver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResizeObserver&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPaint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;resizeObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPaint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resizeObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onpaint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not terrible.&lt;/p&gt;

&lt;p&gt;It is also not the app I wanted to write.&lt;/p&gt;

&lt;p&gt;Now add export. Add multiple DOM surfaces. Add transform sync. Add pointer interaction. Add route changes. Add a framework. Add "why is this blank PNG happening only sometimes?"&lt;/p&gt;

&lt;p&gt;That is where Prism comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Prism is
&lt;/h2&gt;

&lt;p&gt;Prism is a native-first HTML-in-Canvas runtime for managed DOM surfaces in canvas applications.&lt;/p&gt;

&lt;p&gt;It does not replace your renderer.&lt;/p&gt;

&lt;p&gt;Your app still owns the scene, drawing model, animation loop, state, data, interactions, and visual decisions.&lt;/p&gt;

&lt;p&gt;Prism owns the DOM-surface lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your app owns:
  scene, rendering, animation, state, interaction

Prism owns:
  surface registration, bounds, invalidation,
  paint readiness, coordinate helpers, cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @synthesisengineering/prism
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CanvasRuntime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@synthesisengineering/prism&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CanvasRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;surface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPaint&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;drawSurface&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;drawSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the core idea.&lt;/p&gt;

&lt;p&gt;The DOM stays DOM.&lt;br&gt;&lt;br&gt;
The canvas stays canvas.&lt;br&gt;&lt;br&gt;
Prism manages the boundary.&lt;/p&gt;


&lt;h2&gt;
  
  
  The shift: DOM as source material
&lt;/h2&gt;

&lt;p&gt;The important part is not that Prism makes prettier pixels than hand-written canvas.&lt;/p&gt;

&lt;p&gt;You can make beautiful things with Canvas 2D, SVG, WebGL, shaders, and all the usual tricks.&lt;/p&gt;

&lt;p&gt;The important part is that the source material can stay DOM.&lt;/p&gt;

&lt;p&gt;Your labels can be real HTML.&lt;br&gt;&lt;br&gt;
Your typography can stay CSS.&lt;br&gt;&lt;br&gt;
Your icons can stay SVG.&lt;br&gt;&lt;br&gt;
Your React components can stay React components.&lt;/p&gt;

&lt;p&gt;Prism lets the canvas treat those DOM-authored pieces as managed surfaces.&lt;/p&gt;

&lt;p&gt;So instead of rewriting every styled element as canvas drawing code, you can author the visual source with the browser's layout engine and compose it inside canvas.&lt;/p&gt;

&lt;p&gt;The browser gives us the primitive.&lt;br&gt;&lt;br&gt;
Prism provides an app lifecycle.&lt;/p&gt;


&lt;h2&gt;
  
  
  Use case 1: data visualization
&lt;/h2&gt;

&lt;p&gt;One example is Prism Atlantic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5h65sek2wvroike6a0re.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5h65sek2wvroike6a0re.png" alt="Prism Atlantic App" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It uses real NOAA/NHC HURDAT2 Atlantic storm-track data from 2000–2025. The canvas draws the storm paths. Prism manages the HTML/CSS surfaces: title, overview, legend, tooltip, detail panel, caption, and export button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://atlantic.runprism.dev" rel="noopener noreferrer"&gt;Open Prism Atlantic →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app owns the data visualization. Prism owns the surface lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CanvasRuntime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@synthesisengineering/prism&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CanvasRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tooltip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tooltipEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;legend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;legendEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPaint&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drawSurface&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pixelRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pixelRatio&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;drawStormTracks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;drawSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;drawSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;legend&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The export path is the part I really wanted to get right.&lt;/p&gt;

&lt;p&gt;With Prism, you wait for fonts, then wait for one Prism-owned paint pass, then use the normal canvas API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paintOnce&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Blob&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Canvas export failed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;paintOnce()&lt;/code&gt; does not export anything by itself.&lt;/p&gt;

&lt;p&gt;It just answers: &lt;em&gt;has Prism completed a paint-ready frame?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;canvas.toBlob()&lt;/code&gt; does the export.&lt;/p&gt;

&lt;p&gt;No screenshot library. No html2canvas. No foreignObject export path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Use case 2: React components as canvas surfaces
&lt;/h2&gt;

&lt;p&gt;Another example is React Composer Lite.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja0etps0ata8g3qip2ic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja0etps0ata8g3qip2ic.png" alt="Prism React Composer Lite" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It shows React-authored HTML/CSS components as movable, transformable, exportable canvas surfaces.&lt;/p&gt;

&lt;p&gt;The trick is not to let React and Prism fight over ownership.&lt;/p&gt;

&lt;p&gt;React owns component state.&lt;br&gt;&lt;br&gt;
Prism owns surface registration and cleanup.&lt;/p&gt;

&lt;p&gt;The pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the runtime once.&lt;/li&gt;
&lt;li&gt;Register the DOM node once for a runtime/element pair.&lt;/li&gt;
&lt;li&gt;Update bounds through &lt;code&gt;surface.setBounds()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Destroy and dispose on unmount.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RefObject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CanvasRuntime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@synthesisengineering/prism&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CanvasSurface&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@synthesisengineering/prism&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SurfaceBounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePrismRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CanvasRuntime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRuntime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CanvasRuntime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextRuntime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CanvasRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;nextRuntime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextRuntime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setRuntime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;nextRuntime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePrismSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CanvasRuntime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SurfaceBounds&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;surfaceRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CanvasSurface&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;surface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bounds&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;surfaceRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;surfaceRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// Register once for this runtime/element pair.&lt;/span&gt;
    &lt;span class="c1"&gt;// Bounds updates are handled by the effect below.&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;surfaceRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;setBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;surfaceRef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The important detail: &lt;code&gt;usePrismRuntime&lt;/code&gt; returns state, not a ref, so downstream &lt;code&gt;usePrismSurface&lt;/code&gt; re-runs correctly when the runtime is ready. Bounds updates do not require re-registering the surface — register once, then update through &lt;code&gt;surface.setBounds()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The boundary is clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React renders normal components.&lt;/li&gt;
&lt;li&gt;Prism registers those DOM nodes as surfaces on mount.&lt;/li&gt;
&lt;li&gt;Canvas composes them into a frame.&lt;/li&gt;
&lt;li&gt;Cleanup happens when components unmount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your React components do not need to become canvas drawing code.&lt;/p&gt;

&lt;p&gt;They can stay React components.&lt;/p&gt;


&lt;h2&gt;
  
  
  Use case 3: DOM as creative material
&lt;/h2&gt;

&lt;p&gt;The third example is Prism Atelier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dbnapwsk8n02sg5i1pk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dbnapwsk8n02sg5i1pk.png" alt="Prism Prism Atelier" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one is less practical and more fun.&lt;/p&gt;

&lt;p&gt;It uses DOM-authored HTML/CSS/SVG as visual material. A real DOM surface is registered once, then drawn repeatedly inside the canvas paint pass with transforms, opacity, shadows, blend modes, and pointer-driven motion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://atelier.runprism.dev" rel="noopener noreferrer"&gt;Open Prism Atelier →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source can be a normal element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"type-surface"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"type-surface"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;PRISM&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then Prism turns it into a reusable canvas surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;surface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;typeEl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;105&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;760&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the app can compose it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPaint&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drawSurface&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// runtime.canvas.width/height are backing-store pixels (CSS pixels × devicePixelRatio).&lt;/span&gt;
  &lt;span class="c1"&gt;// Surface bounds passed to registerSurface/setBounds are CSS pixels — keep them separate.&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#07070a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pixelRatio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;cx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;cy&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;drawSurface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could hand-roll something like this with the raw API.&lt;/p&gt;

&lt;p&gt;But then you own the browser paint hooks, bounds, invalidation, readiness, and cleanup yourself.&lt;/p&gt;

&lt;p&gt;Prism makes the DOM surface reusable as canvas material without making your app coordinate raw &lt;code&gt;onpaint&lt;/code&gt;, &lt;code&gt;requestPaint()&lt;/code&gt;, and &lt;code&gt;drawElementImage()&lt;/code&gt; directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The coordinate-space footgun
&lt;/h2&gt;

&lt;p&gt;One thing Prism makes explicit is coordinate space.&lt;/p&gt;

&lt;p&gt;Surface bounds are CSS pixels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBounds&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Direct canvas drawing uses backing-store pixels.&lt;/p&gt;

&lt;p&gt;So Prism exposes helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cssPointToCanvasPixels&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cssLengthToCanvasPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sounds boring until it saves you from the classic "everything is offset and blurry on my display" bug.&lt;/p&gt;

&lt;p&gt;Boring runtime helpers are good, actually.&lt;/p&gt;




&lt;h2&gt;
  
  
  An agent skill is included
&lt;/h2&gt;

&lt;p&gt;One thing I care about: Prism should be hard to misuse, even when an AI coding agent is writing the first draft.&lt;/p&gt;

&lt;p&gt;So Prism ships with an agent skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add synthesiseng/prism &lt;span class="nt"&gt;--skill&lt;/span&gt; prism-runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill teaches agents the Prism runtime contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;import from &lt;code&gt;@synthesisengineering/prism&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;register DOM nodes as surfaces&lt;/li&gt;
&lt;li&gt;draw surfaces inside &lt;code&gt;onPaint()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;wait for &lt;code&gt;document.fonts.ready&lt;/code&gt; and &lt;code&gt;runtime.paintOnce()&lt;/code&gt; before export&lt;/li&gt;
&lt;li&gt;avoid &lt;code&gt;html2canvas&lt;/code&gt;, &lt;code&gt;dom-to-image&lt;/code&gt;, raw &lt;code&gt;drawElementImage()&lt;/code&gt;, and deep imports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part matters.&lt;/p&gt;

&lt;p&gt;Without guidance, agents tend to reach for screenshot libraries or raw platform APIs. The skill keeps them on the Prism path.&lt;/p&gt;




&lt;h2&gt;
  
  
  The renderer boundary
&lt;/h2&gt;

&lt;p&gt;Prism can sit alongside renderers like Three.js because it does not try to own the scene. Today, the documented API remains 2D-first; renderer-specific integrations are future-facing.&lt;/p&gt;

&lt;p&gt;That boundary is the design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the renderer owns the scene&lt;/li&gt;
&lt;li&gt;the app owns state and interaction&lt;/li&gt;
&lt;li&gt;Prism owns DOM-surface lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to turn Prism into a renderer.&lt;/p&gt;

&lt;p&gt;The point is to let renderers use real DOM surfaces without every app rebuilding the same lifecycle layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;Prism is still early.&lt;/p&gt;

&lt;p&gt;Native fidelity currently requires Chromium with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chrome://flags/#canvas-draw-element
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prism detects native support and can fall back to a compatibility path, but fallback is lower fidelity. It is not equivalent to native HTML rendering.&lt;/p&gt;

&lt;p&gt;This is alpha software: &lt;code&gt;0.1.0-alpha.8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The current public API is 2D-first. WebGL, WebGPU, Three.js, Pixi, and Phaser integrations are future-facing, not the current public API center.&lt;/p&gt;

&lt;p&gt;Prism is not a renderer, UI kit, design tool, app framework, charting library, or game engine.&lt;/p&gt;

&lt;p&gt;It is a runtime for managed DOM surfaces in canvas applications.&lt;/p&gt;

&lt;p&gt;That is the whole point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docs and examples:&lt;/strong&gt; &lt;a href="https://runprism.dev" rel="noopener noreferrer"&gt;runprism.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/synthesiseng/prism" rel="noopener noreferrer"&gt;github.com/synthesiseng/prism&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://atlantic.runprism.dev" rel="noopener noreferrer"&gt;Prism Atlantic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://atelier.runprism.dev" rel="noopener noreferrer"&gt;Prism Atelier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://composer.runprism.dev" rel="noopener noreferrer"&gt;React Composer Lite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @synthesisengineering/prism
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Agent skill:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add synthesiseng/prism &lt;span class="nt"&gt;--skill&lt;/span&gt; prism-runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feedback welcome from people building canvas-heavy apps, visual tools, data viz, editors, and creative systems.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>html</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built Earth's Year Wrapped with real climate data. 2025 was rough.</title>
      <dc:creator>Sara Loera</dc:creator>
      <pubDate>Sun, 19 Apr 2026 21:53:49 +0000</pubDate>
      <link>https://dev.to/saraeloop/i-gave-earth-a-wrapped-thisyearearth-4ll0</link>
      <guid>https://dev.to/saraeloop/i-gave-earth-a-wrapped-thisyearearth-4ll0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Earth Wrapped · MMXXVI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Earth is the narrator.&lt;br&gt;
The year is the subject.&lt;br&gt;
The reader is asked to answer back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://thisyear.earth" rel="noopener noreferrer"&gt;&lt;strong&gt;thisyear.earth&lt;/strong&gt;&lt;/a&gt; is an immersive climate year-in-review told across eleven full screen chapters. It borrows the emotional shape of a wrapped recap, then turns the perspective inside out: the account belongs to Earth, the receipts are climate data, and the final action is not to share a playlist, it is to leave a pledge.&lt;/p&gt;

&lt;p&gt;The experience moves through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preface · The Record&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinates&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fever&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Atmosphere&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Melt&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Canopy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Ledger&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Roll Call&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Residue&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Turn&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Epilogue · Sincerely&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the end, Earth asks for one small thing.&lt;/p&gt;

&lt;p&gt;You write a pledge for the year ahead.&lt;br&gt;
You can sign it.&lt;br&gt;
You can leave it anonymous.&lt;br&gt;
And if you want it to remain, you can mint it to the ledger.&lt;/p&gt;

&lt;p&gt;That pledge becomes part of &lt;a href="https://thisyear.earth/ledger" rel="noopener noreferrer"&gt;&lt;strong&gt;thisyear.earth/ledger&lt;/strong&gt;&lt;/a&gt; — a public record with on-chain proof behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I made it
&lt;/h2&gt;

&lt;p&gt;Climate data is everywhere, and somehow still easy to ignore.&lt;/p&gt;

&lt;p&gt;The numbers exist. They are published. They are updated. They are cited. But a spreadsheet does not stop you in the middle of your day. A PDF does not follow you into Monday morning. A chart rarely makes you feel the weight of what it represents.&lt;/p&gt;

&lt;p&gt;I wanted to build something that made the data land. Something that could turn &lt;strong&gt;429 ppm&lt;/strong&gt; into a condition, &lt;strong&gt;1.17 trillion tonnes of ice lost&lt;/strong&gt; into memory, and &lt;strong&gt;renewables up 32%&lt;/strong&gt; into relief that had to be earned.&lt;/p&gt;

&lt;p&gt;So I gave Earth a Wrapped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🌍 &lt;a href="https://thisyear.earth" rel="noopener noreferrer"&gt;thisyear.earth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🗒️ &lt;a href="https://thisyear.earth/ledger" rel="noopener noreferrer"&gt;thisyear.earth/ledger&lt;/a&gt; — the permanent pledge record&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu7p6rw1h4co9vdcn4ka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu7p6rw1h4co9vdcn4ka.png" alt="renewables card" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwuo9mj4nq90pem1kydf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwuo9mj4nq90pem1kydf2.png" alt="Globe" width="800" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F398un4njyho5wdodhlwy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F398un4njyho5wdodhlwy.png" alt="The ledger" width="800" height="760"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Solana mattered
&lt;/h2&gt;

&lt;p&gt;The ledger is the part I care about most.&lt;/p&gt;

&lt;p&gt;Not because it is “web3.”  &lt;/p&gt;

&lt;p&gt;Because it turns a passing interaction into a record.&lt;/p&gt;

&lt;p&gt;When someone mints a pledge, the app creates a real memo transaction on &lt;strong&gt;Solana Devnet&lt;/strong&gt; for the challenge build. The transaction hash and mint metadata are stored in the app’s database, and the public ledger page renders those sealed pledges in a readable, shareable, and verifiable format.&lt;/p&gt;

&lt;p&gt;The ledger is not a crypto dashboard.  &lt;/p&gt;

&lt;p&gt;It is a book of names and promises.&lt;/p&gt;

&lt;p&gt;A reader can follow the Explorer link and verify that the pledge exists on-chain. For the challenge build, that proof currently lives on &lt;strong&gt;Devnet&lt;/strong&gt; rather than Mainnet.&lt;/p&gt;

&lt;p&gt;What mattered to me was the split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neon&lt;/strong&gt; serves the ledger and app data quickly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solana&lt;/strong&gt; acts as the proof layer underneath&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;p&gt;The site uses separate shells for mobile and desktop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile:&lt;/strong&gt; swipeable full-screen sequence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop:&lt;/strong&gt; scroll-driven atmospheric narrative&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; — App Router, Turbopack, React 19&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; — strict mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS + Framer Motion&lt;/strong&gt; — layout, motion, transitions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon&lt;/strong&gt; — pledge and location persistence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solana&lt;/strong&gt; — memo-based Devnet pledge minting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;globe.gl&lt;/strong&gt; — final globe visualization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lenis&lt;/strong&gt; — desktop scroll behavior&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;The climate data is real.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;CO₂&lt;/strong&gt; card fetches daily live readings from the &lt;strong&gt;NOAA GML CSV at the Mauna Loa Observatory&lt;/strong&gt;. The other cards use annual or authoritative reported figures, cited in the experience itself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Card&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CO₂ · 429 ppm&lt;/td&gt;
&lt;td&gt;NOAA GML — Mauna Loa Observatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temperature · +1.55°C&lt;/td&gt;
&lt;td&gt;NASA GISS + NOAA Climate.gov&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ice · 1.17T tonnes&lt;/td&gt;
&lt;td&gt;NSIDC + NASA GRACE-FO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forest · 14.9M hectares&lt;/td&gt;
&lt;td&gt;Global Forest Watch + WRI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Species · 41,046&lt;/td&gt;
&lt;td&gt;IUCN Red List&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plastic · 413 Mt&lt;/td&gt;
&lt;td&gt;OECD + UNEP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wildfires&lt;/td&gt;
&lt;td&gt;NASA FIRMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renewables · +32%&lt;/td&gt;
&lt;td&gt;IEA + IRENA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;This project could have gone in a loud, celebratory direction. I tried that path at first. It felt wrong. The subject matter did not want confetti. It wanted gravity.&lt;/p&gt;

&lt;p&gt;So the final direction became typographic, atmospheric, and editorial, not because it was trendy, but because it was the only version that felt honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prize category
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Best Use of Solana
&lt;/h3&gt;

&lt;p&gt;Every minted pledge creates a real Solana Devnet memo transaction.&lt;/p&gt;

&lt;p&gt;The app keeps the ledger readable and fast by serving it from Neon, while each sealed pledge links to its on-chain proof on Solana. That gave me the best of both worlds: a public record people can actually read, and a proof layer underneath it that is real and verifiable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Special thanks
&lt;/h2&gt;

&lt;p&gt;Special thanks to the scientists and researchers who maintain these datasets year after year — &lt;strong&gt;NOAA GML at Mauna Loa Observatory, NASA GISS, NSIDC, Global Forest Watch, the IUCN Red List, and the IEA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover image uses Earth imagery courtesy of NASA.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;🌍 &lt;a href="https://thisyear.earth" rel="noopener noreferrer"&gt;thisyear.earth&lt;/a&gt;&lt;br&gt;
Code: &lt;a href="https://github.com/saraeloop/thisyearearth" rel="noopener noreferrer"&gt;github.com/saraeloop/thisyearearth&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read the year.&lt;br&gt;
Leave a pledge.&lt;br&gt;
Add your line to the record.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>earthday</category>
      <category>solana</category>
    </item>
    <item>
      <title>Managing Multiple Git Identities Was a Mess, So I Built gitrole</title>
      <dc:creator>Sara Loera</dc:creator>
      <pubDate>Tue, 14 Apr 2026 22:13:56 +0000</pubDate>
      <link>https://dev.to/saraeloop/managing-multiple-git-identities-was-a-mess-so-i-built-gitrole-184o</link>
      <guid>https://dev.to/saraeloop/managing-multiple-git-identities-was-a-mess-so-i-built-gitrole-184o</guid>
      <description>&lt;p&gt;&lt;em&gt;Multiple accounts were annoying. Multiple machines made it worse. Then came the agents.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I was supposed to be on vacation.&lt;/p&gt;

&lt;p&gt;Not fully offline. Just the kind of trip where you bring an old laptop, keep a loose eye on things, and tell yourself that if something urgent comes up, you can handle it.&lt;/p&gt;

&lt;p&gt;I had my dotfiles. I had the usual setup scripts for bootstrapping a machine and juggling Git accounts. I even had a shell wrapper and GIT_SSH_COMMAND to force the right key per repo. I thought that would be enough. It wasn’t.&lt;/p&gt;

&lt;p&gt;Halfway through the trip, a client messaged me. The project they had been sitting on for weeks suddenly became urgent.&lt;/p&gt;

&lt;p&gt;So there I was, on an old laptop, trying to jump back into work mode.&lt;/p&gt;

&lt;p&gt;I needed to pull private files from one GitHub account, set up a client repo on another, and keep my personal GitHub config intact on the same laptop. Three identities. One machine. As if that weren’t enough chaos, I also wanted an agent running overnight, and none of them were playing nicely together.&lt;/p&gt;

&lt;p&gt;I tried to make &lt;code&gt;gh&lt;/code&gt; behave across multiple accounts.&lt;/p&gt;

&lt;p&gt;Wrong account authenticating. Wrong identity on commits. Private repos inaccessible because the wrong SSH key was being used. Everything was tangled together.&lt;/p&gt;

&lt;p&gt;I got it working eventually. But it took way longer than it should have. And the worst part was knowing this was not a one time problem. It was going to happen again on the next machine, the next repo, the next account, the next agent.&lt;/p&gt;

&lt;p&gt;That was the moment I stopped patching the workflow and decided to build the missing tool.&lt;/p&gt;

&lt;p&gt;That tool is gitrole.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why separate identities at all?
&lt;/h2&gt;

&lt;p&gt;For me, this is not just cleanup. It is discipline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Work&lt;/strong&gt; is employer-owned code. Wrong identity on a commit is not just awkward; it's a problem. It is an accountability and IP problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freelance&lt;/strong&gt; is client-owned code. I also keep private client templates and tools there that I do not want mixed into anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personal&lt;/strong&gt; is my own work. I do not want employer email on side projects or personal experiments bleeding into client repos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents&lt;/strong&gt; make this even more important. If an agent commits as me and pushes something broken, was that me or the agent? There needs to be a clean boundary.&lt;/p&gt;

&lt;p&gt;Each identity has a different owner, audience, and level of accountability. Mixing them is not just messy. It creates real problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual problem
&lt;/h2&gt;

&lt;p&gt;Git identity is split across two systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit identity&lt;/strong&gt; is your &lt;code&gt;user.name&lt;/code&gt; and &lt;code&gt;user.email&lt;/code&gt; — what your commit says it is from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push identity&lt;/strong&gt; is your SSH setup — which key gets used, which GitHub account that key maps to, which host alias the repo remote points at.&lt;/p&gt;

&lt;p&gt;Those two things are related, but they are not the same. You can easily end up with commits that look right, but pushes that go through the wrong account. Git does not warn you when those two drift apart. Most people only notice after the damage is already in history.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gh&lt;/code&gt; helps with GitHub authentication. It does not manage your Git commit identity. You can &lt;code&gt;gh auth switch&lt;/code&gt; all day and still commit as the wrong person.&lt;/p&gt;

&lt;p&gt;That is the hole gitrole fills.&lt;/p&gt;




&lt;h2&gt;
  
  
  What gitrole gives you
&lt;/h2&gt;

&lt;p&gt;gitrole gives you one place to check the two things Git keeps separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who the next commit will say it is from&lt;/li&gt;
&lt;li&gt;which GitHub account will actually push it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of editing config by hand and hoping you remembered everything, you save your identities once, switch to the one you want, and check the repo before you commit or push.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three-command loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitrole use work &lt;span class="nt"&gt;--local&lt;/span&gt;
gitrole status
gitrole doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;use&lt;/code&gt; switches the identity for this repo only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt; tells you whether commit and push identity are aligned&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;doctor&lt;/code&gt; explains what is wrong if they are not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You save a role once per identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitrole add work &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Your Name"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--email&lt;/span&gt; &lt;span class="s2"&gt;"you@company.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssh&lt;/span&gt; ~/.ssh/id_work &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--github-user&lt;/span&gt; your-work-github-username &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--github-host&lt;/span&gt; github.com-work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use it and check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitrole use work &lt;span class="nt"&gt;--local&lt;/span&gt;
gitrole status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;work  aligned
  commit Your Name &amp;lt;you@company.com&amp;gt;
  push  your-work-github-username via github.com-work
  scope local override
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;status&lt;/code&gt; shows &lt;code&gt;aligned&lt;/code&gt;, you are done. If something looks off, &lt;code&gt;gitrole doctor&lt;/code&gt; tells you exactly what and why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitrole doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checks
  ok role   commit identity matches saved role work
  ok scope  selected role work is applied via local config
  ok commit effective commit identity matches selected role work
  ok auth   SSH auth matches role githubUser your-work-github-username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if the push path is still wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitrole remote &lt;span class="nb"&gt;set &lt;/span&gt;work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That rewrites &lt;code&gt;origin&lt;/code&gt; to use the SSH host alias configured for that role.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fctwl5hc2klo0yk3v12jv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fctwl5hc2klo0yk3v12jv.png" alt="gitrole catching a misaligned remote and fixing it in three commands" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Remote was still pointing at the personal account after switching to work. Doctor caught it. One command fixed it.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The fresh-machine moment
&lt;/h2&gt;

&lt;p&gt;On a clean machine, the problem becomes obvious fast. You install gitrole, run status, and see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;no matching role  warning
  commit Ada &amp;lt;ada@example.com&amp;gt;
  push  Ada via github.com
  scope global
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git is configured. gitrole just does not know that identity yet. So you capture it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitrole import current &lt;span class="nt"&gt;--name&lt;/span&gt; personal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;imported current identity as personal
  commit Ada &amp;lt;ada@example.com&amp;gt;
  scope global
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now gitrole knows that identity by name. No retyping.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agents made this more important, not less
&lt;/h2&gt;

&lt;p&gt;Humans are bad at remembering which identity belongs where.&lt;/p&gt;

&lt;p&gt;Agents are worse. They do not carry ambient context unless you make it explicit.&lt;/p&gt;

&lt;p&gt;That is why gitrole has &lt;code&gt;gitrole status --short&lt;/code&gt;, &lt;code&gt;gitrole doctor --json&lt;/code&gt;, and repo-local &lt;code&gt;.gitrole&lt;/code&gt; policy files. Not because I wanted to build an AI tool, but because explicit identity state matters a lot more once software is committing on your behalf.&lt;/p&gt;




&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;I built gitrole because I was tired of that moment before every push:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;wait — am I the right person right now?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Save your identities once. Switch with one command. Check before you push.&lt;/p&gt;

&lt;p&gt;It already works; I use it myself, and I'm still refining it as I pressure test it across machines and repos. If you try it, I'd love to hear what works and what still feels rough.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;npm install -g gitrole&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://docs.gitrole.dev" rel="noopener noreferrer"&gt;docs.gitrole.dev&lt;/a&gt; — repo pinning, agent identity setup, and machine-readable output for automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/synsoftworks/gitrole" rel="noopener noreferrer"&gt;github.com/synsoftworks/gitrole&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>productivity</category>
      <category>tooling</category>
      <category>agents</category>
    </item>
    <item>
      <title>I "accidentally" built an awesome-useless ecosystem. There is an owl. The Tamagotchi is dying. 1997 portfolio devcities.lol</title>
      <dc:creator>Sara Loera</dc:creator>
      <pubDate>Thu, 09 Apr 2026 03:29:45 +0000</pubDate>
      <link>https://dev.to/saraeloop/i-accidentally-built-an-awesome-useless-ecosystem-there-is-an-owl-the-tamagotchi-is-dying-3a1o</link>
      <guid>https://dev.to/saraeloop/i-accidentally-built-an-awesome-useless-ecosystem-there-is-an-owl-the-tamagotchi-is-dying-3a1o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Hello, my name is Sara, and I'm a software engineer. I build observable AI infrastructure. I make complex systems legible, auditable, and safe.&lt;/p&gt;

&lt;p&gt;Today, in full yolo mode, I used those skills to build &lt;strong&gt;&lt;a href="https://github.com/saraeloop/awesome-useless" rel="noopener noreferrer"&gt;awesome-useless&lt;/a&gt;&lt;/strong&gt; — a real &lt;code&gt;awesome-*&lt;/code&gt; list for useless software. Like &lt;code&gt;awesome-python&lt;/code&gt; but for things that should not exist.&lt;/p&gt;

&lt;p&gt;It is a russian doll. Open it and find something weirder inside.&lt;/p&gt;

&lt;p&gt;🪆 &lt;strong&gt;awesome-useless&lt;/strong&gt; — the repo. A curated list. Real badges. Real CI. Contributions welcome. Useful PRs will be rejected.&lt;/p&gt;

&lt;p&gt;🪆 &lt;strong&gt;oh-my-silly-me&lt;/strong&gt; — a shell framework that makes your terminal measurably worse. Has a Tamagotchi that always dies.&lt;/p&gt;

&lt;p&gt;🪆 &lt;strong&gt;.spells&lt;/strong&gt; — a hidden grimoire. &lt;code&gt;ls -la&lt;/code&gt; and you'll see things. We cannot be held responsible.&lt;/p&gt;

&lt;p&gt;🪆 &lt;strong&gt;devCities.lol&lt;/strong&gt; — the innermost doll. You describe yourself. A tired government owl named Agent Hoot builds you a personal 1997 Devcities homepage. You get a shareable URL. Your portfolio is now safe from modern web design. Forever. In 1997.&lt;/p&gt;

&lt;p&gt;I have no regrets. My git log tells a different story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://devcities.lol" rel="noopener noreferrer"&gt;→ Try devCities.lol&lt;/a&gt;— yeah, I went there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Land on devcities.lol — a Windows 98 portal&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Describe yourself in the prompt box:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I'm ___, frontend dev with strong feelings about border-radius. My stack is TypeScript and regret. My cat, Mr. Whiskers, reviews all my PRs. He has never approved one."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hit &lt;strong&gt;🦉 ENTER O.W.L.S PROTECTION&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Behind the scenes, the Gemini API wakes up Agent Hoot. &lt;br&gt;
He reads your description. He built your 1997 homepage. &lt;br&gt;
He does not ask questions. This also concerns us.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Watch the terminal go...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your personal 1997 devCities homepage appears. It is not pretty. It was never going to be pretty. Your name is in rainbow Comic Sans. Your stack has been renamed to chaos. Your hot take is marked CLASSIFIED. The guestbook has one entry — Agent Hoot. Also classified. You made this. You cannot unmake this. This is yours now.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopktp3vnr2263msdfujw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopktp3vnr2263msdfujw.png" alt="devcities website" width="800" height="711"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;--&amp;gt; Hit "🔗 SHARE YOUR PAGE" — the entire Devcities nightmare &lt;br&gt;
   gets compressed into a URL. No backend. No database. Just pure 1997 chaos encoded into a link. Send it to your friends. Send it to your enemies. Send it to your tech lead, who keeps saying "we should rewrite this in Rust."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;oh-my-silly-me in action:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;oh-my-silly-me/silly.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ksshhhhh...bing bing bing...CONNECTED AT 28.8 KBPS
MEMORANDUM: Please report any unauthorized use of the 'Tab' key to Agent Hoot.
🐣 [TAMAGOTCHI] is currently 0 minutes old. It is 1799 seconds from its inevitable doom.
&lt;/span&gt;&lt;span class="gp"&gt;🦉 [CLASSIFIED] ~/your-directory $&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can also cast spells:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cast ghostbuster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Gemini identified 3 ghosts, 7 personality enhancements, and 13 anomalies in total. It was recommended to leave offerings in the node_modules folder at midnight. Agent Hoot will collect them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/saraeloop" rel="noopener noreferrer"&gt;
        saraeloop
      &lt;/a&gt; / &lt;a href="https://github.com/saraeloop/awesome-useless" rel="noopener noreferrer"&gt;
        awesome-useless
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An awesome list for software that solves nothing. Contributions welcome.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/saraeloop/awesome-useless/assets/awesome-useless-glasses.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fsaraeloop%2Fawesome-useless%2FHEAD%2Fassets%2Fawesome-useless-glasses.png" width="250"&gt;&lt;/a&gt;

&lt;p&gt;&lt;a href="https://awesome.re" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/9d49598b873146ec650fb3f275e8a532c765dabb1f61d5afa25be41e79891aa7/68747470733a2f2f617765736f6d652e72652f62616467652e737667" alt="awesome"&gt;&lt;/a&gt;
&lt;a href=""&gt;&lt;img src="https://camo.githubusercontent.com/306ff9324f38c1e385af4cbca56429d3e0882a88eb95a020de0f8168f0e2a6a1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7573656c6573732d3130302532352d627269676874677265656e" alt="useless"&gt;&lt;/a&gt;
&lt;a href=""&gt;&lt;img src="https://camo.githubusercontent.com/7860ac554a0fcb00c06566dcc45cc967c9861ee5bb82143cd1e8dca396078286/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61696e7461696e65642d70726f6261626c792d79656c6c6f77" alt="maintained"&gt;&lt;/a&gt;
&lt;a href=""&gt;&lt;img src="https://camo.githubusercontent.com/866813dc5cfc27680798feb5902b5b91d8f2a043e21f8472ee56e6ef4601a461/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5052732d7573656c6573732532306f6e6c792d726564" alt="PRs"&gt;&lt;/a&gt;
&lt;a href=""&gt;&lt;img src="https://camo.githubusercontent.com/0c10b75ce2e770ce3119e8ca7ab52c5a1996f3c94fd9d34c395777762fbf1fb4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6365727469666965642d4167656e74253230486f6f742532302546302539462541362538392d626c7565" alt="Agent Hoot"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;awesome-useless ✨&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A curated list of awesome useless code, tools, and resources.&lt;br&gt;
Contributions welcome. Useful PRs will be rejected.&lt;/p&gt;
&lt;p&gt;🏆 An entry in the &lt;a href="https://dev.to/challenges" rel="nofollow"&gt;DEV.to April Fools Challenge&lt;/a&gt; · &lt;code&gt;#418challenge&lt;/code&gt; · &lt;code&gt;#devchallenge&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Contents&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/saraeloop/awesome-useless#tools" rel="noopener noreferrer"&gt;Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/saraeloop/awesome-useless#projects" rel="noopener noreferrer"&gt;Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/saraeloop/awesome-useless#prompts" rel="noopener noreferrer"&gt;Prompts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/saraeloop/awesome-useless#spells" rel="noopener noreferrer"&gt;Spells&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🛠️ Tools&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/saraeloop/awesome-useless/tools/oh-my-silly-me/" rel="noopener noreferrer"&gt;oh-my-silly-me&lt;/a&gt; — A shell framework for the unproductive. Like oh-my-zsh but worse in every measurable way. On purpose.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🏛️ Projects&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/saraeloop/awesome-useless/projects/devcities/" rel="noopener noreferrer"&gt;devCities&lt;/a&gt; — Your dev portfolio. In 1997. A government owl builds your Devcities homepage from a prompt.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🧠 Prompts&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/saraeloop/awesome-useless/prompts/unhinged-prompts/" rel="noopener noreferrer"&gt;unhinged-prompts&lt;/a&gt; — AI prompts that should not exist. Curated by someone who regrets all of them.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🧙 Spells&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/saraeloop/awesome-useless/.spells/" rel="noopener noreferrer"&gt;.spells&lt;/a&gt; — A hidden grimoire. &lt;code&gt;ls -la&lt;/code&gt; and you'll see things. Cast them on any AI: Gemini, Claude, Codex, or other. We cannot be held responsible.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;⚰️ Graveyard&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/saraeloop/awesome-useless/graveyard/" rel="noopener noreferrer"&gt;graveyard&lt;/a&gt; — Where projects go to die. Even AI projects. Especially AI projects. Submit yours. Be proud.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🖥️ Useless Downloads&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/saraeloop/awesome-useless/assets/418-i-am-a-teapot-wallpaper.jpg" rel="noopener noreferrer"&gt;418 I Am A Teapot Wallpaper&lt;/a&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/saraeloop/awesome-useless" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The repo is a russian doll. Each layer contains something weirder.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ls -la&lt;/code&gt; when you get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript + Vite (the app that hides websites in 1997 has strict mode enabled)&lt;/li&gt;
&lt;li&gt;98.css (Windows 98 government portal aesthetic)&lt;/li&gt;
&lt;li&gt;Gemini API / gemini-3.1-pro-preview (Agent Hoot, the Identity Reassignment Officer)&lt;/li&gt;
&lt;li&gt;lz-string (compress entire Geocities pages into shareable URLs)&lt;/li&gt;
&lt;li&gt;Vanilla chaos (CSS, HTML, the spirit of 1997)&lt;/li&gt;
&lt;li&gt;Bash/zsh (oh-my-silly-me shell framework)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Gemini API does the actual work:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you describe yourself, the prompt tells Gemini:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"You are Agent Hoot, O.W.L.S. Identity Reassignment Officer. This page was made by a developer in 1997 who was EXTREMELY proud of it. It should look intentional. Chaotic but lovingly crafted. It looks like someone spent their whole weekend on this. Useless ≠ ugly. Charming chaos ≠ visual disaster."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiax0ds5bt11v3jt9bd4c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiax0ds5bt11v3jt9bd4c.png" alt="Gemini Cooking" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last instruction went through 5 iterations. The first 4 produced visual disasters. The 5th produced Sara's CyberDomain. Sara's CyberDomain is beautiful. In 1997.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini CLI built oh-my-silly-me:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I prompted Gemini CLI to build a shell framework that makes your terminal worse. It built Tamagotchis, Enya error handlers, dial-up simulators, and a &lt;code&gt;cast&lt;/code&gt; command for AI spells. It did not object once. This concerns me deeply.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6etr4w2x9r5xkxceonr7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6etr4w2x9r5xkxceonr7.png" alt="Oh-my-silly-me" width="800" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Imagen created Agent Hoot:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;He did not consent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total cost of all Gemini API calls:&lt;/strong&gt; approximately $0.83 &lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ The Incident Report
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Every mission has casualties.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O.W.L.S. INCIDENT LOG
======================
Classification: EYES ONLY

INCIDENT #001: THE MODEL THAT DID NOT EXIST
gemini-3.0-flash is not found for API version v1beta.
We asked for the future. The future was not available.
We settled for 2.5. for a bit, then 3.0 flasharoooo!

INCIDENT #002: THE TYPING THAT WOULD NOT STOP
&amp;gt; Agent Hoot is still typing.
&amp;gt; Agent Hoot is still typing..
&amp;gt; Agent Hoot is still typing...
[× 87]
Agent Hoot typed for 4 minutes.
Agent Hoot was very thorough.
We could not stop him.

INCIDENT #003: THE LINTER THAT HAD OPINIONS
The CI markdown linter told us:
"This repo is a joke —
let's not fight the linter on every creative choice."
We disabled the rules.
The linter was right.
We were also right.

INCIDENT #004: THE TAMAGOTCHI
It died.
We could not save it.
Nobody can save it.
This is by design.
This is fine.

TOTAL CASUALTIES: 4 incidents, 1 Tamagotchi
STATUS: Operational. Mostly. It is 1997.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo is real. The contributions are welcome.&lt;br&gt;
The useless PRs will be merged with great pride.&lt;br&gt;
The useful PRs will be rejected with great sorrow.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Star the repo. Submit your useless thing. &lt;br&gt;
The grimoire is open. &lt;code&gt;ls -la&lt;/code&gt; and you'll see.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Google AI Usage&lt;/strong&gt; — Gemini CLI built the shell framework. Gemini API powers the identity reassignment. Google Imagen created Agent Hoot. Total Google AI involvement: everything except the existential crisis, which was my own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Ode to Larry Masinter&lt;/strong&gt; — Our servers are teapots. HTTP 418. This is not an error. This is a lifestyle. The HTCPCP/1.0 certification appears in the footer of every page. Agent Hoot has been a teapot since 1997. Agent Hoot did not choose this. Free pocket computer skins ship with every generated page. Larry Masinter has not been informed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community Favorite&lt;/strong&gt; — devCities gives every developer their own shareable 1997 homepage. The repo accepts PRs for new useless projects. The tamagotchi dies for everyone equally. This is community.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The visitor counter is at 000312.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;It will reach 000313 eventually.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Agent Hoot is still watching.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;It is 1997.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;It is always 1997.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;🦉&lt;/p&gt;

</description>
      <category>418challenge</category>
      <category>jokes</category>
      <category>watercooler</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
