{"id":9400,"date":"2026-04-21T08:34:17","date_gmt":"2026-04-21T13:34:17","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=9400"},"modified":"2026-04-21T09:00:29","modified_gmt":"2026-04-21T14:00:29","slug":"the-web-is-fun-again-first-experiments-with-html-in-canvas","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/the-web-is-fun-again-first-experiments-with-html-in-canvas\/","title":{"rendered":"The Web Is Fun Again: First Experiments with HTML in Canvas"},"content":{"rendered":"\n<p>Every once in a while, the platform drops something that makes you want to build strange demos again, or at least weirder ones. The <a href=\"https:\/\/github.com\/WICG\/html-in-canvas\">new HTML in Canvas API<\/a> is a perfect example of one of those moments.<\/p>\n\n\n\n<p>The promise is simple and exciting: take native HTML, render it into canvas workflows, and then apply visual effects with 2D Canvas, WebGL, or WebGPU. In other words, you can keep real semantic elements in your markup while treating their rendered output as pixels.<\/p>\n\n\n\n<div class=\"wp-block-group learn-more\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<p><strong>Support Status (Important)<\/strong><\/p>\n\n\n\n<p>To enable it, go to <code>chrome:\/\/flags\/#canvas-draw-element<\/code> and turn on the &#8220;Canvas Draw Element&#8221; flag. After enabling, you can start experimenting with the API in your local environment.<\/p>\n\n\n\n<p>As of now, this API is still experimental, only available in Chromium-based browsers (146+) and behind a flag. That means you need to enable it manually to experiment with it, and it is not yet a production-ready feature.<\/p>\n\n\n\n<p>The main demos below have a collapsed video after them so you can see the effect if you happen to be in a non-supporting browser.<\/p>\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_EagOyyK\/e82acffbffe52508b5691bcae49a5aab\" src=\"\/\/codepen.io\/anon\/embed\/EagOyyK\/e82acffbffe52508b5691bcae49a5aab?height=450&amp;theme-id=1&amp;slug-hash=EagOyyK\/e82acffbffe52508b5691bcae49a5aab&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed EagOyyK\/e82acffbffe52508b5691bcae49a5aab\" title=\"CodePen Embed EagOyyK\/e82acffbffe52508b5691bcae49a5aab\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Video<\/summary>\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='277' src='https:\/\/videopress.com\/embed\/GS825p1k?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t<\/details>\n\n\n\n<p>This combination of HTML rendering and semantics, with Canvas&#8217;s visual freedom and shader-style effects, feels like a missing piece we have wanted for years. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Basic Elements of HTML in Canvas<\/h2>\n\n\n\n<p>To understand the HTML in Canvas APIs, we&#8217;ll start with a simple example that demonstrates the core concepts.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1) Plain HTML First<\/h3>\n\n\n\n<p>Let&#8217;s start with a plain <code>div<\/code> that contains real content: a heading, a card, a short paragraph, and a tiny form with an input and a button, so we&#8217;ll have some interactive elements to play with.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"content\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span>&gt;<\/span>HTML in Canvas<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>...text<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">form<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"text\"<\/span> <span class=\"hljs-attr\">placeholder<\/span>=<span class=\"hljs-string\">\"name\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span>&gt;<\/span>Submit<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">form<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_xbEQOOE\/ab2a0337c735df78eada1f0c16556e8f\" src=\"\/\/codepen.io\/anon\/embed\/xbEQOOE\/ab2a0337c735df78eada1f0c16556e8f?height=450&amp;theme-id=1&amp;slug-hash=xbEQOOE\/ab2a0337c735df78eada1f0c16556e8f&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed xbEQOOE\/ab2a0337c735df78eada1f0c16556e8f\" title=\"CodePen Embed xbEQOOE\/ab2a0337c735df78eada1f0c16556e8f\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>This is just regular HTML and CSS. Nothing special yet.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2) Wrap it with Canvas<\/h3>\n\n\n\n<p>Now, to render that content inside a canvas, we wrap it with <code>&lt;canvas&gt;<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">canvas<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"500\"<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"300\"<\/span> <span class=\"hljs-attr\">layoutsubtree<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"content\"<\/span>&gt;<\/span>\n    ... content ...\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">canvas<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"learn-more\">Notice that I added the <code>layoutsubtree<\/code> attribute to the canvas, <strong>this is mandatory for the HTML-in-Canvas API to work<\/strong>, as this attribute opts canvas descendants into layout and hit testing so they behave like real DOM content. In practice, <code>layoutsubtree<\/code> is the opt-in switch that turns your canvas children into a proper render source for the HTML-in-Canvas pipeline.<\/p>\n\n\n\n<p>Putting content inside the canvas means it is treated as real DOM elements, and you can interact with them as usual, but they are not rendered (i.e., they are &#8216;invisible&#8217;) until you explicitly draw them into the canvas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Write JavaScript<\/h2>\n\n\n\n<p>Now let&#8217;s wire up the JavaScript APIs. First, we need to get references to the canvas, its 2D context, and the content element we want to draw:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> canvas = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'canvas'<\/span>);\n<span class=\"hljs-keyword\">const<\/span> ctx = canvas.getContext(<span class=\"hljs-string\">'2d'<\/span>);\n<span class=\"hljs-keyword\">const<\/span> content = canvas.querySelector(<span class=\"hljs-string\">'.content'<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Next, we need to trigger the <code>canvas.requestPaint()<\/code> for the browser to fire a <code>paint<\/code> event. It&#8217;s important to call <code>requestPaint()<\/code> at least once to kickstart the rendering pipeline, even if nothing changed yet, as it gives us the initial snapshot of the content. Without this initial call, you may have no paint event yet, and no frame to draw from.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">canvas<\/span><span class=\"hljs-selector-class\">.requestPaint<\/span>();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And finally, we set up the <code>paint<\/code> event listener to draw the content into the canvas. This is your render callback for HTML-in-Canvas: when paint happens, this is where you copy DOM rendering into canvas pixels.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">canvas.addEventListener(<span class=\"hljs-string\">'paint'<\/span>, () =&gt; {\n  ctx.reset();\n  ctx.drawElementImage(content, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Inside the <code>paint<\/code> event listener, we call <code>ctx.drawElementImage(content, 0, 0)<\/code>. This is the core method of the HTML-in-Canvas API that takes the current rendered output of the specified DOM element (in this case, <code>.content<\/code>) and draws it into the canvas at the specified coordinates (0, 0).<\/p>\n\n\n\n<p>The <code>ctx.reset()<\/code> call before it is important to clear any previous drawing state, ensuring that each paint starts with a clean slate. This is especially crucial if you plan to apply transformations or styles in the future, as it prevents unintended carryover from previous frames.<\/p>\n\n\n\n<p>And now we have the (same) content rendered inside the canvas!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_XJjyeYw\/ed4f4061ea4e525c4f21ddc2a849c3f4\" src=\"\/\/codepen.io\/anon\/embed\/XJjyeYw\/ed4f4061ea4e525c4f21ddc2a849c3f4?height=450&amp;theme-id=1&amp;slug-hash=XJjyeYw\/ed4f4061ea4e525c4f21ddc2a849c3f4&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed XJjyeYw\/ed4f4061ea4e525c4f21ddc2a849c3f4\" title=\"CodePen Embed XJjyeYw\/ed4f4061ea4e525c4f21ddc2a849c3f4\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Note that the content is still fully interactive, and you can click the input and button as usual, but their visual representation is now part of the canvas rendering. What you <strong>see<\/strong> are pixels on a canvas, that are generated from real HTML elements, allowing you to apply any canvas effects or transformations to them as needed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Thing About Size<\/h2>\n\n\n\n<p>In the previous example, we set the canvas size explicitly with <code>width=\"500\"<\/code> and <code>height=\"300\"<\/code>. If I&#8217;m being honest, in all my experiments with this API, size and resizing are the only areas that felt a bit undercooked in its current state, and I hope this area gets smoother over time.<\/p>\n\n\n\n<p>There are two main reasons why sizing here feels different.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Originally, canvas was not meant to have children. It does not behave like a regular <code>div<\/code> container, it doesn&#8217;t default to <code>width: 100%<\/code>, and doesn&#8217;t know how to grow its height based on its content.<\/li>\n\n\n\n<li>The canvas element has its own <code>width<\/code> and <code>height<\/code> attributes that define its drawing surface, and these <strong>can be<\/strong> independent of the size of the content inside it, which can be very confusing at first.<\/li>\n<\/ol>\n\n\n\n<p>This means we need to size the canvas intentionally. One option is to use absolute values on the width and height attributes, as in the earlier example, which sets both the element size and the drawing surface size. The other option is to skip fixed attributes, size the canvas dynamically with CSS, and then sync the drawing surface with the element&#8217;s rendered size.<\/p>\n\n\n\n<p>The most convenient way to do that is with a simple observer that forwards the element&#8217;s external dimensions into the canvas itself:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> observer = <span class=\"hljs-keyword\">new<\/span> ResizeObserver(<span class=\"hljs-function\">(<span class=\"hljs-params\">&#91;entry]<\/span>) =&gt;<\/span> {\n  canvas.width = entry.devicePixelContentBoxSize&#91;<span class=\"hljs-number\">0<\/span>].inlineSize;\n  canvas.height = entry.devicePixelContentBoxSize&#91;<span class=\"hljs-number\">0<\/span>].blockSize;\n});\nobserver.observe(canvas, { <span class=\"hljs-attr\">box<\/span>: <span class=\"hljs-string\">'device-pixel-content-box'<\/span> });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And here is a simple example of a canvas with some responsive cards. The canvas has no explicit size, and is set to <code>width: 100%<\/code> and <code>height: 100%<\/code> in the CSS, so it&#8217;s adapting to the viewport size. The observer is syncing the canvas drawing surface to match the rendered size of the canvas element, so the pixels are always crisp and not stretched.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_WbGYxRm\/6b9541f02a58ce8491d9289bd98a94af\" src=\"\/\/codepen.io\/anon\/embed\/WbGYxRm\/6b9541f02a58ce8491d9289bd98a94af?height=450&amp;theme-id=1&amp;slug-hash=WbGYxRm\/6b9541f02a58ce8491d9289bd98a94af&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed WbGYxRm\/6b9541f02a58ce8491d9289bd98a94af\" title=\"CodePen Embed WbGYxRm\/6b9541f02a58ce8491d9289bd98a94af\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>(<a href=\"https:\/\/codepen.io\/amit_sheen\/pen\/WbGYxRm\/6b9541f02a58ce8491d9289bd98a94af\">Open the demo in a new tab<\/a> and resize the window to see how the grid adapts.)<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Video<\/summary>\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='370' src='https:\/\/videopress.com\/embed\/Orza9FBd?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=1&amp;persistVolume=0&amp;playsinline=1&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p><\/p>\n<\/details>\n\n\n\n<h2 class=\"wp-block-heading\">Moving Things Around<\/h2>\n\n\n\n<p>So now we have painted pixels on a canvas that represent real elements still sitting in the DOM. But what if we want to move those elements around? What if we want to apply transforms to the canvas, or to the source DOM elements?<\/p>\n\n\n\n<p>The official explainer is very explicit about this: &#8220;The canvas&#8217;s current transformation matrix is applied when drawing into the canvas. CSS transforms on the source element are <strong>ignored<\/strong> for drawing (but continue to affect hit testing\/accessibility).&#8221;<\/p>\n\n\n\n<p>This means that if we apply <code>translate<\/code> on the source HTML element, interaction moves with that element, but the pixels in the canvas are still drawn from the element&#8217;s original drawing position.<\/p>\n\n\n\n<p>On the other hand, if we apply transform on the canvas drawing context itself, the pixels move, but the DOM elements do not, and interaction stays where the elements are in the DOM.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"319\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/gSoZIXYc.png?resize=1024%2C319&#038;ssl=1\" alt=\"\" class=\"wp-image-9403\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/gSoZIXYc.png?resize=1024%2C319&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/gSoZIXYc.png?resize=300%2C93&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/gSoZIXYc.png?resize=768%2C239&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/gSoZIXYc.png?w=1122&amp;ssl=1 1122w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>In the image above, the container on the right has <code>translate: 0 100px<\/code>, so the DOM elements move down, but in the canvas they are still drawn in their original location. On the left side, the canvas uses <code>ctx.translate(0, 100)<\/code>, so the drawing moves down, but the DOM elements stay in place.<\/p>\n\n\n\n<p>The fix is to synchronize the transform on both sides. Since the <code>drawElementImage()<\/code> method returns a transform value, we can set the transform in the canvas, and use the returned value to apply the same transform to the DOM element.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">canvas.addEventListener(<span class=\"hljs-string\">'paint'<\/span>, () =&gt; {\n  ctx.reset();\n\n  <span class=\"hljs-comment\">\/* Add transform to canvas *\/<\/span>\n  ctx.translate(<span class=\"hljs-number\">250<\/span>, <span class=\"hljs-number\">150<\/span>);\n  ctx.rotate((input.value.length - <span class=\"hljs-number\">30<\/span>) * <span class=\"hljs-built_in\">Math<\/span>.PI \/ <span class=\"hljs-number\">180<\/span>);\n  ctx.translate(<span class=\"hljs-number\">-250<\/span>, <span class=\"hljs-number\">-150<\/span>);\n\n  <span class=\"hljs-comment\">\/* Draw HTML content into canvas and get the transform applied to it. *\/<\/span>\n  <span class=\"hljs-keyword\">const<\/span> transform = ctx.drawElementImage(content, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>);\n\n  <span class=\"hljs-comment\">\/* Apply the same transform to the HTML content, so it matches the canvas. *\/<\/span>\n  content.style.transform = transform.toString();\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Notice that the rotation is driven by the input&#8217;s text length. Click into the input and start typing to see the element rotate. The canvas rendering rotates along with the DOM elements, because the same transform is applied to both, and visuals and interaction stay aligned.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_LERXzJp\/c4f61ba29e228a7ebe819c3a1406e7e2\" src=\"\/\/codepen.io\/anon\/embed\/LERXzJp\/c4f61ba29e228a7ebe819c3a1406e7e2?height=450&amp;theme-id=1&amp;slug-hash=LERXzJp\/c4f61ba29e228a7ebe819c3a1406e7e2&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed LERXzJp\/c4f61ba29e228a7ebe819c3a1406e7e2\" title=\"CodePen Embed LERXzJp\/c4f61ba29e228a7ebe819c3a1406e7e2\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>And if we\u2019re playing with transforms, I wanted to push that idea a bit further. In the next demo, I mapped the same HTML content between four draggable control points using homography and a bit of math. This one is less about practical UI and more about exploring how far this API can be stretched while still keeping real DOM content in the loop. Feel free to drag the points and play with it.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ogzQGOL\/767a0207778c26cd8e887371a53d125a\" src=\"\/\/codepen.io\/anon\/embed\/ogzQGOL\/767a0207778c26cd8e887371a53d125a?height=650&amp;theme-id=1&amp;slug-hash=ogzQGOL\/767a0207778c26cd8e887371a53d125a&amp;default-tab=result\" height=\"650\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ogzQGOL\/767a0207778c26cd8e887371a53d125a\" title=\"CodePen Embed ogzQGOL\/767a0207778c26cd8e887371a53d125a\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Video<\/summary>\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='271' src='https:\/\/videopress.com\/embed\/Up4CNUgg?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p><\/p>\n<\/details>\n\n\n\n<h2 class=\"wp-block-heading\">Basic Pixel Manipulation<\/h2>\n\n\n\n<p>Okay, we turned real elements into pixels on a canvas, but what does that actually give us? Until now, we haven\u2019t really treated pixels as independent units, so let&#8217;s start.<\/p>\n\n\n\n<p>The idea here is simple: create an array of all canvas pixels, iterate through it one pixel at a time, and do whatever you want with those values. We&#8217;ll begin with a simple example of replacing all pure-green pixels (which we will use for text and borders) with a colorful gradient based on their position.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.content<\/span> {\n  <span class=\"hljs-comment\">\/* Pure green text and border *\/<\/span>\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-number\">#0f0<\/span>;\n  <span class=\"hljs-attribute\">border<\/span>: <span class=\"hljs-number\">4px<\/span> solid currentColor;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>To get the pixel array, we call <code>getImageData<\/code> inside the <code>paint<\/code> event listener, right after drawing the HTML content into the canvas.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">canvas.addEventListener(<span class=\"hljs-string\">'paint'<\/span>, () =&gt; {\n  <span class=\"hljs-comment\">\/\/ First draw the HTML content into the canvas<\/span>\n  ctx.reset();\n  ctx.drawElementImage(content, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>);\n\n  <span class=\"hljs-comment\">\/\/ Then read the pixels from the canvas<\/span>\n  <span class=\"hljs-keyword\">const<\/span> imageData = ctx.getImageData(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, canvas.width, canvas.height);\n  <span class=\"hljs-keyword\">const<\/span> data = imageData.data;\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-group learn-more\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<p>Remember! <code>data<\/code> is now an array where each pixel is represented by 4 cells: red, green, blue, and alpha (opacity). So the array length is 4 times the number of pixels in the canvas.<\/p>\n\n\n\n<p><code>data.length === canvas.width * canvas.height * 4<\/code><\/p>\n<\/div><\/div>\n\n\n\n<p>Now we can loop through all pixels. Notice that we increment by 4 each time, so every iteration handles one full pixel.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt; data.length; i += <span class=\"hljs-number\">4<\/span>) {\n  ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Inside that loop, we can do anything. For this example, we&#8217;ll check each of the RGB values to see if the pixel is pure green (<code>#0f0<\/code>), and if it is, we will calculate a new color based on the pixel&#8217;s position, and create a colorful gradient.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt; data.length; i += <span class=\"hljs-number\">4<\/span>) {\n\n  <span class=\"hljs-comment\">\/\/ Check if the pixel is pure green (#0f0)<\/span>\n  <span class=\"hljs-keyword\">if<\/span> (data&#91;i] === <span class=\"hljs-number\">0<\/span> &amp;&amp; data&#91;i + <span class=\"hljs-number\">1<\/span>] === <span class=\"hljs-number\">255<\/span> &amp;&amp; data&#91;i + <span class=\"hljs-number\">2<\/span>] === <span class=\"hljs-number\">0<\/span>) {\n\n    <span class=\"hljs-comment\">\/\/ Calculate the pixel's position<\/span>\n    <span class=\"hljs-keyword\">const<\/span> pixelX = (i \/ <span class=\"hljs-number\">4<\/span>) % canvas.width;\n    <span class=\"hljs-keyword\">const<\/span> pixelY = <span class=\"hljs-built_in\">Math<\/span>.floor((i \/ <span class=\"hljs-number\">4<\/span>) \/ canvas.width);\n  \n    <span class=\"hljs-comment\">\/\/ Calculate a hue based on the pixel's position and convert it to RGB<\/span>\n    <span class=\"hljs-keyword\">const<\/span> hue = (pixelX + pixelY) % <span class=\"hljs-number\">360<\/span>;\n    <span class=\"hljs-keyword\">const<\/span> &#91;r, g, b] = hslToRgb(hue);\n  \n    <span class=\"hljs-comment\">\/\/ Replace the green pixel with the new color<\/span>\n    data&#91;i] = r;\n    data&#91;i + <span class=\"hljs-number\">1<\/span>] = g;\n    data&#91;i + <span class=\"hljs-number\">2<\/span>] = b;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And after we finish updating the values in <code>data<\/code>, we need to draw the updated pixels back into the canvas with <code>putImageData<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ Draw the modified pixels into the canvas<\/span>\nctx.putImageData(imageData, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And here is the result. Note that input and button are still at their original color, because we didn&#8217;t change those elements&#8217; color.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ogzQoNY\/6ea679c8ea5e624d056a8fb04fcc0548\" src=\"\/\/codepen.io\/anon\/embed\/ogzQoNY\/6ea679c8ea5e624d056a8fb04fcc0548?height=450&amp;theme-id=1&amp;slug-hash=ogzQoNY\/6ea679c8ea5e624d056a8fb04fcc0548&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ogzQoNY\/6ea679c8ea5e624d056a8fb04fcc0548\" title=\"CodePen Embed ogzQoNY\/6ea679c8ea5e624d056a8fb04fcc0548\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>If you inspect the elements, you will see that the text and borders are still just green in the DOM, but on the canvas, they have been replaced with a colorful gradient.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Moving Pixels Around<\/h2>\n\n\n\n<p>Okay, so we changed the color of each pixel, and that\u2019s cool, but when we talk about pixel manipulation, we usually want to see them move.<\/p>\n\n\n\n<p>The key issue here is that pixels are not elements. They are just values in an array. You cannot call <code>translate()<\/code> on them and move them somewhere else. To &#8220;move&#8221; a pixel, you simply draw its value into a different pixel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Buffer Zone<\/h3>\n\n\n\n<p>Because JavaScript is asynchronous and the loop iterates over pixels one by one, we don&#8217;t want to mutate the source while we are still reading from it. A common best practice is to keep a buffer copy of the original data as a stable reference, then write the new values into <code>data<\/code>.<\/p>\n\n\n\n<p>We save that buffer in the <code>paint<\/code> event listener, right after reading from canvas:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ Read the pixels from the canvas<\/span>\n<span class=\"hljs-keyword\">const<\/span> imageData = ctx.getImageData(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, canvas.width, canvas.height);\n<span class=\"hljs-keyword\">const<\/span> data = imageData.data;\n\n<span class=\"hljs-comment\">\/\/ Save the original pixel data as a source of reference<\/span>\n<span class=\"hljs-keyword\">const<\/span> originalPixelData = &#91;...data];<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we can redraw any pixel anywhere. For example, here is a simple X\/Y distortion pass: for each pixel, we compute its coordinates, calculate an offset on each axis, use that offset to find a source pixel, and copy the source RGBA into the current pixel.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ Loop through the pixels<\/span>\n<span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt; data.length; i += <span class=\"hljs-number\">4<\/span>) {\n\n  <span class=\"hljs-comment\">\/\/ Calculate the pixel's coordinates<\/span>\n  <span class=\"hljs-keyword\">const<\/span> pixelX = (i \/ <span class=\"hljs-number\">4<\/span>) % canvas.width;\n  <span class=\"hljs-keyword\">const<\/span> pixelY = <span class=\"hljs-built_in\">Math<\/span>.floor((i \/ <span class=\"hljs-number\">4<\/span>) \/ canvas.width);\n\n  <span class=\"hljs-comment\">\/\/ Calculate the wave offset for this pixel based on its coordinates<\/span>\n  <span class=\"hljs-keyword\">const<\/span> offsetX = waveSize&#91;<span class=\"hljs-number\">0<\/span>] * <span class=\"hljs-built_in\">Math<\/span>.cos(((pixelX % waveSpacing&#91;<span class=\"hljs-number\">0<\/span>]) \/ waveSpacing&#91;<span class=\"hljs-number\">0<\/span>]) * <span class=\"hljs-number\">2<\/span> * <span class=\"hljs-built_in\">Math<\/span>.PI);\n  <span class=\"hljs-keyword\">const<\/span> offsetY = waveSize&#91;<span class=\"hljs-number\">1<\/span>] * <span class=\"hljs-built_in\">Math<\/span>.sin(((pixelY % waveSpacing&#91;<span class=\"hljs-number\">1<\/span>]) \/ waveSpacing&#91;<span class=\"hljs-number\">1<\/span>]) * <span class=\"hljs-number\">2<\/span> * <span class=\"hljs-built_in\">Math<\/span>.PI);\n\n  <span class=\"hljs-comment\">\/\/ Calculate the coordinates of the source pixel<\/span>\n  <span class=\"hljs-keyword\">const<\/span> newX = <span class=\"hljs-built_in\">Math<\/span>.max(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-built_in\">Math<\/span>.min(canvas.width - <span class=\"hljs-number\">1<\/span>, pixelX + <span class=\"hljs-built_in\">Math<\/span>.round(offsetX)));\n  <span class=\"hljs-keyword\">const<\/span> newY = <span class=\"hljs-built_in\">Math<\/span>.max(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-built_in\">Math<\/span>.min(canvas.height - <span class=\"hljs-number\">1<\/span>, pixelY + <span class=\"hljs-built_in\">Math<\/span>.round(offsetY)));\n\n  <span class=\"hljs-comment\">\/\/ Calculate the index of the source pixel in the original pixel data array<\/span>\n  <span class=\"hljs-keyword\">const<\/span> newIndex = (newY * canvas.width + newX) * <span class=\"hljs-number\">4<\/span>;\n\n  <span class=\"hljs-comment\">\/\/ Save the offset pixel values from the original pixel data<\/span>\n  data&#91;i] = originalPixelData&#91;newIndex];\n  data&#91;i + <span class=\"hljs-number\">1<\/span>] = originalPixelData&#91;newIndex + <span class=\"hljs-number\">1<\/span>];\n  data&#91;i + <span class=\"hljs-number\">2<\/span>] = originalPixelData&#91;newIndex + <span class=\"hljs-number\">2<\/span>];\n  data&#91;i + <span class=\"hljs-number\">3<\/span>] = originalPixelData&#91;newIndex + <span class=\"hljs-number\">3<\/span>];\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And here is the result:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_Kwgryqj\/630bdabade0956d21074c2e57eac0429\" src=\"\/\/codepen.io\/anon\/embed\/Kwgryqj\/630bdabade0956d21074c2e57eac0429?height=450&amp;theme-id=1&amp;slug-hash=Kwgryqj\/630bdabade0956d21074c2e57eac0429&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed Kwgryqj\/630bdabade0956d21074c2e57eac0429\" title=\"CodePen Embed Kwgryqj\/630bdabade0956d21074c2e57eac0429\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Mouse Interaction<\/h2>\n\n\n\n<p>One of the most common things to do once you start moving pixels around, is to add some mouse interaction. This is pretty straightforward: listen to <code>mousemove<\/code>, get the mouse position relative to the canvas, and use that data however you like.<\/p>\n\n\n\n<p>The big difference here is that the render loop is no longer driven directly inside the <code>paint<\/code> event listener. When canvas content changes, we still capture the source data into <code>originalPixelData<\/code> (same as before), but for the actual pixel remap we now need live mouse coordinates, so that part runs inside the <code>mousemove<\/code> callback.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">canvas.onpaint = <span class=\"hljs-function\">(<span class=\"hljs-params\">event<\/span>) =&gt;<\/span> {\n    <span class=\"hljs-comment\">\/\/ Draw the elements and read the pixels from the canvas<\/span>\n    <span class=\"hljs-comment\">\/\/ Save the original pixel data as a source of reference<\/span>\n}\n\ncanvas.addEventListener(<span class=\"hljs-string\">'mousemove'<\/span>, (e) =&gt; {\n    <span class=\"hljs-comment\">\/\/ Loop through the pixels<\/span>\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt; data.length; i += <span class=\"hljs-number\">4<\/span>) {\n      <span class=\"hljs-comment\">\/\/ Do stuff based on `e.clientX` and `e.clientY`<\/span>\n    }\n    <span class=\"hljs-comment\">\/\/ Draw the modified pixels into the canvas<\/span>\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here is a simple example that calculates the distance of each pixel from the mouse. It is basically the same idea as the previous example, but now we compute the offset relative to the mouse position. If a pixel is outside the effect radius, it keeps its original value.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ Calculate the distance (d) from the mouse to the pixel<\/span>\n<span class=\"hljs-keyword\">const<\/span> dx = pixelX - e.pageX + <span class=\"hljs-number\">32<\/span>;\n<span class=\"hljs-keyword\">const<\/span> dy = pixelY - e.pageY + <span class=\"hljs-number\">32<\/span>;\n<span class=\"hljs-keyword\">const<\/span> d = <span class=\"hljs-built_in\">Math<\/span>.sqrt(dx * dx + dy * dy);\n\n<span class=\"hljs-keyword\">if<\/span> (d &lt; effectSize) {\n\n  <span class=\"hljs-comment\">\/\/ Calculate the offset for this pixel<\/span>\n  <span class=\"hljs-keyword\">const<\/span> offset = <span class=\"hljs-built_in\">Math<\/span>.sin(<span class=\"hljs-built_in\">Math<\/span>.sqrt(d \/ effectSize) * <span class=\"hljs-built_in\">Math<\/span>.PI) * <span class=\"hljs-number\">-10<\/span>;\n\n  <span class=\"hljs-comment\">\/\/ Calculate the coordinates of the source pixel<\/span>\n  <span class=\"hljs-keyword\">const<\/span> newX = clamp(pixelX + <span class=\"hljs-built_in\">Math<\/span>.round(offset * (dx \/ d)), <span class=\"hljs-number\">0<\/span>, canvas.width - <span class=\"hljs-number\">1<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> newY = clamp(pixelY + <span class=\"hljs-built_in\">Math<\/span>.round(offset * (dy \/ d)), <span class=\"hljs-number\">0<\/span>, canvas.height - <span class=\"hljs-number\">1<\/span>);\n\n<span class=\"hljs-comment\">\/\/ Calculate the index of the source pixel in the original pixel data array<\/span>\nnewIndex = (newY * canvas.width + newX) * <span class=\"hljs-number\">4<\/span>;\n\n} <span class=\"hljs-keyword\">else<\/span> {\n  <span class=\"hljs-comment\">\/\/ If the pixel is outside the wave radius, keep its original color<\/span>\n  newIndex = i;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_GgjwyXR\/3697eeeb173fa8b909f3dd69ae85af99\" src=\"\/\/codepen.io\/anon\/embed\/GgjwyXR\/3697eeeb173fa8b909f3dd69ae85af99?height=450&amp;theme-id=1&amp;slug-hash=GgjwyXR\/3697eeeb173fa8b909f3dd69ae85af99&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed GgjwyXR\/3697eeeb173fa8b909f3dd69ae85af99\" title=\"CodePen Embed GgjwyXR\/3697eeeb173fa8b909f3dd69ae85af99\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Video<\/summary>\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='243' src='https:\/\/videopress.com\/embed\/7pGz8kaz?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=1&amp;persistVolume=0&amp;playsinline=1&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p><\/p>\n<\/details>\n\n\n\n<h2 class=\"wp-block-heading\">Render Loop<\/h2>\n\n\n\n<p>So far, we have moved things with the mouse, sliders, and even by typing in an input field. But in many cases, we just need continuous motion. For that, we need a render loop that re-calculates pixel values and redraws the canvas every frame.<\/p>\n\n\n\n<p>The basic structure looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ Start the animation loop<\/span>\nrender(performance.now());\n\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">render<\/span>(<span class=\"hljs-params\">nowMs<\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ Loop through the pixels and calculate new values<\/span>\n  <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt; data.length; i += <span class=\"hljs-number\">4<\/span>) {\n          ...\n  }\n\n  <span class=\"hljs-comment\">\/\/ Then draw the modified pixels into the canvas<\/span>\n  ctx.putImageData(imageData, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>);\n\n  <span class=\"hljs-comment\">\/\/ Request the next animation frame to keep the animation going<\/span>\n  requestAnimationFrame(render);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>nowMs<\/code> is the current timestamp (in milliseconds) passed automatically by <code>requestAnimationFrame<\/code> into <code>render(nowMs)<\/code>, and we can use that value to create time-based animations.<\/p>\n\n\n\n<p>As a simple example, I took the rainbow demo from before, but this time the color is recalculated on every frame based on time.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ Calculate a hue based on the pixel's position and the current time<\/span>\n<span class=\"hljs-keyword\">const<\/span> hue = (pixelX + pixelY + nowMs * effectSpeed) % <span class=\"hljs-number\">360<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And here is the result:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_VYKVyNJ\/684cae02f76e096c0ae6903870ee2ed4\" src=\"\/\/codepen.io\/anon\/embed\/VYKVyNJ\/684cae02f76e096c0ae6903870ee2ed4?height=450&amp;theme-id=1&amp;slug-hash=VYKVyNJ\/684cae02f76e096c0ae6903870ee2ed4&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed VYKVyNJ\/684cae02f76e096c0ae6903870ee2ed4\" title=\"CodePen Embed VYKVyNJ\/684cae02f76e096c0ae6903870ee2ed4\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Of course, these are still just small demos to explain the core ideas behind pixel manipulation. But once the basics click, it becomes very easy to extend them and build original effects on top of them, and I hope this also sparks your own creative itch to experiment and build something weird and wonderful.<\/p>\n\n\n\n<p>When I started playing with this API, it reminded me of an old Daniel Shiffman (Coding train) video where he used pixel manipulation to create a fire effect, and I wondered what it would feel like to do that on real DOM elements.<\/p>\n\n\n\n<p>If you are curious, here is <a href=\"https:\/\/www.youtube.com\/watch?v=X0kjv0MozuY\">Daniel&#8217;s great video<\/a>, and this is how I implemented the effect on a real text input, utilizing the same pixel manipulation techniques we covered in this post.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_jEMQZMY\/8037bb33bb9233be0974cc60cd3bea5f\" src=\"\/\/codepen.io\/anon\/embed\/jEMQZMY\/8037bb33bb9233be0974cc60cd3bea5f?height=450&amp;theme-id=1&amp;slug-hash=jEMQZMY\/8037bb33bb9233be0974cc60cd3bea5f&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed jEMQZMY\/8037bb33bb9233be0974cc60cd3bea5f\" title=\"CodePen Embed jEMQZMY\/8037bb33bb9233be0974cc60cd3bea5f\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Now for the Serious Stuff: Shaders<\/h2>\n\n\n\n<p>Until now, we were playing. Transforms and pixel manipulation are great up to a certain point, but when you really want to go wild, you call shaders.<\/p>\n\n\n\n<p>Turning native elements into pixels opens the door to the GPU&#8217;s raw power via WebGL and WebGPU. These technologies can push visual effects to a completely different level without sacrificing performance.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">It\u2019s Not About Shaders<\/h3>\n\n\n\n<p>Explaining shaders in depth is a full article on its own, but at a high level, a shader is just a tiny program that runs on the graphics card and determines how things should be drawn. Instead of manually editing pixels one by one on the CPU, you describe a visual rule, and the GPU applies that rule across huge amounts of pixels in parallel.<\/p>\n\n\n\n<p>If you want a fun visual reference for this idea, there is an old but great Mythbusters demo showing the difference between CPU-style processing (one pixel after another, like we did so far) and GPU-style processing (many pixels in parallel).<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"NVIDIA GPU vs CPU. &#039;Mythbusters&#039; stars paint the Mona Lisa in 80 Milliseconds!\" width=\"500\" height=\"281\" src=\"https:\/\/www.youtube.com\/embed\/8_ZTvG1WQxM?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n<\/div><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">The Smallest Shader Pipeline<\/h2>\n\n\n\n<p>To understand how to use WebGL with the HTML in Canvas API, let&#8217;s start with a minimal setup to get the first shader-based frame on screen. We will build the most basic shader possible, which creates a tinted gradient across the canvas, and then we can extend it with more complex effects.<\/p>\n\n\n\n<p>The flow: capture HTML, feed it into a GPU texture, run a shader, and render the result back to the canvas. Once this flow works, we can experiment with all the wild effects that shader programming allows.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1) Get the canvas and a WebGL context<\/h3>\n\n\n\n<p>At this stage, we are no longer working with a 2D canvas context. Instead of using <code>ctx<\/code>, we move to a WebGL context (<code>gl<\/code>) so the rendering path runs through the GPU.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> canvas = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'canvas'<\/span>);\n<span class=\"hljs-keyword\">const<\/span> content = canvas.querySelector(<span class=\"hljs-string\">'.content'<\/span>);\n<span class=\"hljs-keyword\">const<\/span> gl = canvas.getContext(<span class=\"hljs-string\">'webgl2'<\/span>, { <span class=\"hljs-attr\">alpha<\/span>: <span class=\"hljs-literal\">true<\/span>, <span class=\"hljs-attr\">premultipliedAlpha<\/span>: <span class=\"hljs-literal\">true<\/span> });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">2) Create a Tiny Shader Program<\/h3>\n\n\n\n<p>A basic shader setup consists of four main ingredients: First, we define the &#8220;rules&#8221; that tell the computer exactly where to place our content and how to color it (Shaders). Second, we translate these rules into a language the graphics card can actually understand (Compile &amp; Link). Third, we set up a flat, invisible surface across the screen to project our image onto (Geometry). Finally, we prepare the image itself so the system knows exactly how to smoothly read and display its pixels on that surface (Texture).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-comment\">\/\/ 1. Shaders (Shortened)<\/span>\n<span class=\"hljs-keyword\">const<\/span> vsSource = <span class=\"hljs-string\">`\n  attribute vec2 p; varying vec2 v;\n  void main() { v = vec2(p.x, -p.y) * 0.5 + 0.5; gl_Position = vec4(p, 0, 1); }\n`<\/span>;\n<span class=\"hljs-keyword\">const<\/span> fsSource = <span class=\"hljs-string\">`\n  precision mediump float; varying vec2 v; uniform sampler2D u;\n  void main() { \n    vec4 c = texture2D(u, v);\n    vec3 tint = mix(vec3(1.5, 0.5, 0.5), vec3(0.5, 0.5, 1.5), v.x);\n    gl_FragColor = vec4(c.rgb * tint * c.a, c.a); \n  }\n`<\/span>;\n\n<span class=\"hljs-comment\">\/\/ 2. Compile &amp; Link<\/span>\n<span class=\"hljs-keyword\">const<\/span> program = gl.createProgram();\n&#91;vsSource, fsSource].forEach(<span class=\"hljs-function\">(<span class=\"hljs-params\">src, i<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> s = gl.createShader(i ? gl.FRAGMENT_SHADER : gl.VERTEX_SHADER);\n  gl.shaderSource(s, src);\n  gl.compileShader(s);\n  gl.attachShader(program, s);\n});\ngl.linkProgram(program);\ngl.useProgram(program);\n\n<span class=\"hljs-comment\">\/\/ 3. Geometry<\/span>\ngl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\ngl.bufferData(gl.ARRAY_BUFFER, <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Float32Array<\/span>(&#91;<span class=\"hljs-number\">-1<\/span>,<span class=\"hljs-number\">-1<\/span>, <span class=\"hljs-number\">1<\/span>,<span class=\"hljs-number\">-1<\/span>, <span class=\"hljs-number\">-1<\/span>,<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">1<\/span>,<span class=\"hljs-number\">1<\/span>]), gl.STATIC_DRAW);\n<span class=\"hljs-keyword\">const<\/span> pos = gl.getAttribLocation(program, <span class=\"hljs-string\">'p'<\/span>);\ngl.enableVertexAttribArray(pos);\ngl.vertexAttribPointer(pos, <span class=\"hljs-number\">2<\/span>, gl.FLOAT, <span class=\"hljs-literal\">false<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>);\n\n<span class=\"hljs-comment\">\/\/ 4. Texture<\/span>\ngl.bindTexture(gl.TEXTURE_2D, gl.createTexture());\ngl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); <span class=\"hljs-comment\">\/\/ Required (no mipmaps)<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">3) Create the Render Function<\/h3>\n\n\n\n<p>Now that the shader is ready, we can copy the HTML into the canvas on each frame, but this time through the WebGL path. Instead of <code>ctx.drawElementImage(...)<\/code> (which is a 2D-canvas direct draw call), we upload the element snapshot into a GPU texture with <code>gl.texElementImage2D(...)<\/code>, and then render that texture with <code>gl.drawArrays(...)<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">render<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n    gl.texElementImage2D(gl.TEXTURE_2D, <span class=\"hljs-number\">0<\/span>, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, content);\n    gl.drawArrays(gl.TRIANGLE_STRIP, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">4<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">4) Draw Your Shader<\/h3>\n\n\n\n<p>At this point, if we want a continuous animation, we can schedule the next frame from inside <code>render()<\/code> by adding <code>requestAnimationFrame(render)<\/code> at the end of the function. When the output is static, like in this example, calling <code>render<\/code> from the <code>paint<\/code> callback is enough.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">canvas.addEventListener(<span class=\"hljs-string\">'paint'<\/span>, () =&gt; requestAnimationFrame(render));\ncanvas.requestPaint();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Notice we still schedule rendering for the next frame instead of calling <code>render()<\/code> directly, to avoid loop-related issues. Also, prefer <code>requestPaint<\/code>-driven flow and avoid direct render calls when possible, as calling render without a valid paint snapshot can throw errors like &#8220;no cached paint record for element&#8221;.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_KwgrQqR\/a486464a688794e777aa0a0ecdd72b36\" src=\"\/\/codepen.io\/anon\/embed\/KwgrQqR\/a486464a688794e777aa0a0ecdd72b36?height=450&amp;theme-id=1&amp;slug-hash=KwgrQqR\/a486464a688794e777aa0a0ecdd72b36&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed KwgrQqR\/a486464a688794e777aa0a0ecdd72b36\" title=\"CodePen Embed KwgrQqR\/a486464a688794e777aa0a0ecdd72b36\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Same Pipeline, Wildly Different Shaders<\/h2>\n\n\n\n<p>Now it\u2019s the fun part: once the pipeline is in place, everything opens up. The structure is the same, the tools are the same, and the building blocks are the same, but from there, you can create the wildest shaders you can imagine.<\/p>\n\n\n\n<p>You probably noticed the classic trail ripple demo at the beginning of this post. I have a feeling we are going to see that pattern a lot soon. It is already a great baseline effect, and in a similar style, you can build a much more expressive cursor treatment.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_NPREyzy\/1d5636998f0a98a5b2af02c89484789b\" src=\"\/\/codepen.io\/anon\/embed\/NPREyzy\/1d5636998f0a98a5b2af02c89484789b?height=450&amp;theme-id=1&amp;slug-hash=NPREyzy\/1d5636998f0a98a5b2af02c89484789b&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed NPREyzy\/1d5636998f0a98a5b2af02c89484789b\" title=\"CodePen Embed NPREyzy\/1d5636998f0a98a5b2af02c89484789b\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Of course, we are not limited to mouse movement only. We can also react to clicks themselves, like in this demo.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZYpmxYd\/597373bb1d295feb751ae42bcfa890cc\" src=\"\/\/codepen.io\/anon\/embed\/ZYpmxYd\/597373bb1d295feb751ae42bcfa890cc?height=450&amp;theme-id=1&amp;slug-hash=ZYpmxYd\/597373bb1d295feb751ae42bcfa890cc&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZYpmxYd\/597373bb1d295feb751ae42bcfa890cc\" title=\"CodePen Embed ZYpmxYd\/597373bb1d295feb751ae42bcfa890cc\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Video<\/summary>\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='322' src='https:\/\/videopress.com\/embed\/Vkr6onDa?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p><\/p>\n<\/details>\n\n\n\n<p>And we can even react to drag gestures, then animate the content as if it has physical tension.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_NPREYqL\/0a167f6402718992470aa377211551a2\" src=\"\/\/codepen.io\/anon\/embed\/NPREYqL\/0a167f6402718992470aa377211551a2?height=450&amp;theme-id=1&amp;slug-hash=NPREYqL\/0a167f6402718992470aa377211551a2&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed NPREYqL\/0a167f6402718992470aa377211551a2\" title=\"CodePen Embed NPREYqL\/0a167f6402718992470aa377211551a2\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>Video<\/summary>\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='289' src='https:\/\/videopress.com\/embed\/NITseV7t?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p><\/p>\n<\/details>\n\n\n\n<p>We can also go in a softer direction and use subtle ambient effects for atmosphere.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_MYjzVaM\/eb4501f32f9d025cf56b8667dd84baf6\" src=\"\/\/codepen.io\/anon\/embed\/MYjzVaM\/eb4501f32f9d025cf56b8667dd84baf6?height=450&amp;theme-id=1&amp;slug-hash=MYjzVaM\/eb4501f32f9d025cf56b8667dd84baf6&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed MYjzVaM\/eb4501f32f9d025cf56b8667dd84baf6\" title=\"CodePen Embed MYjzVaM\/eb4501f32f9d025cf56b8667dd84baf6\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>And here is one more, just because I really liked it.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_dPpQmGv\/42adf6e00961d1c6e5b890a30ef6e5ae\" src=\"\/\/codepen.io\/anon\/embed\/dPpQmGv\/42adf6e00961d1c6e5b890a30ef6e5ae?height=450&amp;theme-id=1&amp;slug-hash=dPpQmGv\/42adf6e00961d1c6e5b890a30ef6e5ae&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed dPpQmGv\/42adf6e00961d1c6e5b890a30ef6e5ae\" title=\"CodePen Embed dPpQmGv\/42adf6e00961d1c6e5b890a30ef6e5ae\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Final Thoughts<\/strong><\/h2>\n\n\n\n<p>If there is one takeaway from all of this, it is that HTML in Canvas is not just a new API; it is a new workflow mindset. We keep real HTML, semantics, forms, and interactions, but we can render the final output as pure pixels and treat the UI as a visual playground.<\/p>\n\n\n\n<p>What excites me most is the range this technology unlocks. It can serve playful demos, expressive interactions, visual storytelling, and many practical UI ideas that were previously awkward to build with traditional rendering paths.<\/p>\n\n\n\n<p>We are still at the beginning, which is exactly why this is the right time to experiment, push boundaries, and publish weird ideas that might become tomorrow&#8217;s standard patterns.<\/p>\n\n\n\n<p>So, what is your weird idea?<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Also, check out the HiC (HTML-in-Canvas) Showroom with many more effects!<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/hicshowroom.com\/\">Explore the Showroom \u27a1\ufe0f<\/a><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>An experimental API let&#8217;s us put HTML within those opening and closing canvas tags and render it to the canvas, while remaining interactive. Lots of possibility here! <\/p>\n","protected":false},"author":27,"featured_media":9417,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[131,31,473,3],"class_list":["post-9400","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-canvas","tag-html","tag-html-in-canvas","tag-javascript"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/html-in-canvas.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9400","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/27"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=9400"}],"version-history":[{"count":13,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9400\/revisions"}],"predecessor-version":[{"id":9445,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9400\/revisions\/9445"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/9417"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=9400"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=9400"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=9400"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}