<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xml:base="https://leozard.com/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Leozard</title>
    <link>https://leozard.com/</link>
    <atom:link href="https://leozard.com/feed.xml" rel="self" type="application/rss+xml" />
    <description>Ramblin</description>
    <language>en</language>
    <item>
      <title>A javascript adventure</title>
      <link>https://leozard.com/posts/demo-1/</link>
      <description>A javascript adventure</description>
      <content:encoded>&lt;!-- import lib for syntax highlights --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://leozard.com/highlight-darcula.min.css&quot;&gt;
&lt;script src=&quot;https://leozard.com/highlight.min.js&quot;&gt;&lt;/script&gt;

&lt;style&gt;
  canvas:has(+ script) {
    display: block;
    border: 1px solid #c3c3c3;
    margin-inline: auto;
  }

  pre {
    overflow-x: auto;
    max-width: 100%;
  }
&lt;/style&gt;

&lt;h3&gt;JavaScript widget game?&lt;/h3&gt;

&lt;h4&gt; How feasible and fun is a super portable mini-format JS game? Let&#39;s find out!&lt;/h4&gt;

&lt;noscript&gt;
  JavaScript is disabled. This content requires JavaScript.
&lt;/noscript&gt;

&lt;p&gt;
  The idea is based around a single portable &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag. It should create the canvas the content
  will
  be drawn
  on. For this we will use &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript&quot;&gt;currentScript&lt;/a&gt;. As a demo let&#39;s
  create a script tag that adds a canvas element above itself and draws some text to it.
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-1&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-1&quot;&gt;
  (function () {
    const me = document.currentScript
    const cv = document.createElement(&quot;canvas&quot;)
    me.parentNode.insertBefore(cv, me)
    const ctx = cv.getContext(&#39;2d&#39;)
    ctx.font = &#39;24px sans-serif&#39;
    ctx.fillText(&#39;Hello, world!&#39;, 50, 50)
  })()
&lt;/script&gt;


&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-1&quot;)
      c1.textContent = document.getElementById(&quot;script-1&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;
  Neat. using &lt;code&gt;currentScript&lt;/code&gt; we get a reference to the current script element. Then we create a canvas
  element and add it just above our script. Then we get the 2D drawing context for that canvas and use it to draw some
  text to it.
&lt;/p&gt;
&lt;p&gt;
  Now let&#39;s add a render loop.
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-2&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-2&quot;&gt;
    (function () {
      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)
      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)
      ctx.font = &#39;24px sans-serif&#39;

      function draw(timestamp) {
        ctx.clearRect(0, 0, cv.width, cv.height)
        const d = Math.sin(timestamp * 0.005) * 10
        ctx.fillText(&#39;Hello, world!&#39;, 50, 50 + d)
        requestAnimationFrame(draw)
      }
      requestAnimationFrame(draw)
    })()
&lt;/script&gt;


&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-2&quot;)
      c1.textContent = document.getElementById(&quot;script-2&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;
  Using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame&quot;&gt;requestAnimationFrame&lt;/a&gt; and
  supplying to it our draw method we get a render loop. Do remember to request another animation frame, otherwise we
  won&#39;t loop.
&lt;/p&gt;

&lt;p&gt;
  The next step is adding user input.
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-3&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-3&quot;&gt;
    (function () {
      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)
      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)
      ctx.font = &#39;24px sans-serif&#39;
      let clicks = 0

      cv.addEventListener(&#39;pointerdown&#39;, (e) =&gt; {
        clicks++
      })

      function draw(timestamp) {
        ctx.clearRect(0, 0, cv.width, cv.height)
        const d = Math.sin(timestamp * 0.005) * 10
        ctx.fillText(`Clicks: ${clicks}`, 50, 50 + d)
        requestAnimationFrame(draw)
      }
      requestAnimationFrame(draw)
    })()
&lt;/script&gt;


&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-3&quot;)
      c1.textContent = document.getElementById(&quot;script-3&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;And we can get the canvas relative click position by:&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot;&gt;
    cv.addEventListener(&#39;pointerdown&#39;, (e) =&gt; {
    &amp;Tab;const rect = cv.getBoundingClientRect()
    &amp;Tab;const x = e.clientX - rect.left
    &amp;Tab;const y = e.clientY - rect.top
    &amp;Tab;console.log(x, y)
    })
  &lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;
  This subtracts the click event&#39;s position with the canvas element&#39;s position, giving us x, y relative to the top left
  of
  the canvas. Thus x and y increases towards the bottom right corner. And using that we can do:
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-4&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-4&quot;&gt;
    (function () {
      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)
      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)
      ctx.font = &#39;24px sans-serif&#39;
      const radius = 4;
      const dots = []

      cv.addEventListener(&#39;pointerdown&#39;, (e) =&gt; {
        const rect = cv.getBoundingClientRect()
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        dots.push({x, y})
      })

      function draw(timestamp) {
        ctx.clearRect(0, 0, cv.width, cv.height)
        ctx.fillText(&quot;Click me!&quot;, 50, 50)
        dots.forEach((d) =&gt; {
          ctx.beginPath()
          ctx.arc(d.x, d.y, radius, 0, Math.PI * 2)
          ctx.fill()
        })
        requestAnimationFrame(draw)
      }
      requestAnimationFrame(draw)
    })()
&lt;/script&gt;


&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-4&quot;)
      c1.textContent = document.getElementById(&quot;script-4&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;
  Now let&#39;s explore keyboard input
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-5&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-5&quot;&gt;
    (function () {
      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)
      cv.setAttribute(&quot;tabindex&quot;, 0)
      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)
      ctx.font = &#39;24px sans-serif&#39;
      const radius = 4;
      const dots = []
      let lastKey = &quot;N/A&quot;

      cv.addEventListener(&#39;pointerdown&#39;, (e) =&gt; {
        const rect = cv.getBoundingClientRect()
        const x = e.clientX - rect.left
        const y = e.clientY - rect.top
        dots.push({x, y})
      })

      cv.addEventListener(&#39;keydown&#39;, (e) =&gt; {
        lastKey = e.key
      });

      function draw(timestamp) {
        ctx.clearRect(0, 0, cv.width, cv.height)
        ctx.fillText(`User pressed: ${lastKey}`, 50, 50)
        dots.forEach((d) =&gt; {
          ctx.beginPath()
          ctx.arc(d.x, d.y, radius, 0, Math.PI * 2)
          ctx.fill()
        })
        requestAnimationFrame(draw)
      }
      requestAnimationFrame(draw)

      cv.addEventListener(&#39;focus&#39;, () =&gt; {
        cv.style.outline = &#39;2px solid #2d2d2d&#39;
      })
      cv.addEventListener(&#39;blur&#39;, () =&gt; {
        cv.style.outline = &#39;none&#39;
      })
    })()
&lt;/script&gt;


&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-5&quot;)
      c1.textContent = document.getElementById(&quot;script-5&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;Here we do a small bit of trickery in order to make sure that the canvas can grab input focus and that we give the
  user some indication that the canvas has focus. Click the canvas and it starts accepting keyboard input. Click outside
  it or tab away and it stops accepting that input.&lt;/p&gt;
&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot;&gt;
    cv.setAttribute(&quot;tabindex&quot;, 0) // This allows the canvas element to be focusable

    cv.addEventListener(&#39;keydown&#39;, (e) =&gt; {
    &amp;Tab;// key handle code
    });

    // Add a border when the canvas has focus. Remove it when focus is lost: blurred
    cv.addEventListener(&#39;focus&#39;, () =&gt; {
    &amp;Tab;cv.style.outline = &#39;2px solid #2d2d2d&#39;
    })
    cv.addEventListener(&#39;blur&#39;, () =&gt; {
    &amp;Tab;cv.style.outline = &#39;none&#39;
    })

  &lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;
  This brings us to a quite good place to start building a game. Some helper functions to make drawing graphics easier
  would be great. But one thing I really think would make the &quot;widget&quot; or game more portable is the ability to change
  its
  size. I think that the widget itself should determine aspect ratio, but the final scale should be determined in situ.
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-6&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-6&quot;&gt;
    (function () {
      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)
      cv.width = 200
      cv.height = 200
      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)
      ctx.font = &#39;24px sans-serif&#39;

      ctx.beginPath()
      ctx.arc(50, 50, 20, 0, Math.PI * 2)
      ctx.fill()
      ctx.fillText(&quot;Some text&quot;, 10, 50)
    })()
&lt;/script&gt;

&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-6&quot;)
      c1.textContent = document.getElementById(&quot;script-6&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;
  Here we set the canvas size to 200x200 and draw to it.
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-7&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-7&quot; data-scale=&quot;2&quot;&gt;
    (function () {
      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)

      cv.width = 200
      cv.height = 200
      const scale = Number(me.dataset.scale || 1)
      cv.style.width = (cv.width * scale) + &quot;px&quot;
      cv.style.height = (cv.height * scale) + &quot;px&quot;
      cv.style.imageRendering = &quot;pixelated&quot;

      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)
      ctx.font = &#39;24px sans-serif&#39;

      ctx.fillText(&quot;Some text&quot;, 10, 50)
      ctx.beginPath()
      ctx.arc(50, 50, 20, 0, Math.PI * 2)
      ctx.fill()

    })()
&lt;/script&gt;

&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-7&quot;)
      c1.textContent = document.getElementById(&quot;script-7&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;
  Now we have added the ability to scale the widget by supplying a &lt;code&gt;data-scale&lt;/code&gt; attribute to the scale
  element. The canvas buffer, the drawing surface, is still 200 by 200 pixels but we have increased the output size to
  400 by 400 by scaling it up to 2x. We have also added &lt;code&gt;imageRendering = &quot;pixelated&quot;&lt;/code&gt; to ensure that when
  the content is scaled we get that pixel graphics sharpness. Do note that when scaling text drawn using
  &lt;code&gt;fillText&lt;/code&gt; in this manner we can get some artefacts. That is because these fonts might have anti-aliasing,
  which we then scale up to become more noticeable. This can maybe be solved using a bitmap font.
&lt;/p&gt;

&lt;p&gt;
  Here we have all this put together.
&lt;/p&gt;

&lt;pre&gt;
  &lt;code class=&quot;language-javascript&quot; id=&quot;code-showcase-8&quot;&gt;&lt;/code&gt;
&lt;/pre&gt;

&lt;script id=&quot;script-8&quot; data-scale=&quot;2&quot;&gt;
    (function () {
      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)

      cv.width = 200
      cv.height = 200
      const scale = Number(me.dataset.scale || 1)
      cv.style.width = (cv.width * scale) + &quot;px&quot;
      cv.style.height = (cv.height * scale) + &quot;px&quot;
      cv.style.imageRendering = &quot;pixelated&quot;

      cv.setAttribute(&quot;tabindex&quot;, 0)
      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)
      ctx.font = &#39;24px sans-serif&#39;
      const radius = 4;
      const dots = []
      let lastKey = &quot;N/A&quot;

      cv.addEventListener(&#39;pointerdown&#39;, (e) =&gt; {
        const rect = cv.getBoundingClientRect()
        const scaleX = cv.width / rect.width
        const scaleY = cv.height / rect.height
        const x = (e.clientX - rect.left) * scaleX
        const y = (e.clientY - rect.top) * scaleY
        dots.push({x, y})
      })

      cv.addEventListener(&#39;keydown&#39;, (e) =&gt; {
        lastKey = e.key
      });

      function draw(timestamp) {
        ctx.clearRect(0, 0, cv.width, cv.height)
        ctx.fillText(`User pressed: ${lastKey}`, 10, 50)
        dots.forEach((d) =&gt; {
          ctx.beginPath()
          ctx.arc(d.x, d.y, radius, 0, Math.PI * 2)
          ctx.fill()
        })
        requestAnimationFrame(draw)
      }
      requestAnimationFrame(draw)

      cv.addEventListener(&#39;focus&#39;, () =&gt; {
        cv.style.outline = &#39;2px solid #2d2d2d&#39;
      })
      cv.addEventListener(&#39;blur&#39;, () =&gt; {
        cv.style.outline = &#39;none&#39;
      })
    })()
&lt;/script&gt;


&lt;script&gt;
    (function () {
      const c1 = document.getElementById(&quot;code-showcase-8&quot;)
      c1.textContent = document.getElementById(&quot;script-8&quot;).textContent.split(&#39;&#92;n&#39;).slice(2, -2).join(&#39;&#92;n&#39;)
    })()
&lt;/script&gt;

&lt;p&gt;
  Do note that in order to get the correct click event pixels we need to scale them. We divide the canvas buffer size
  with the actual canvas width. This is normally just &lt;code&gt;1/scale&lt;/code&gt; but by calculating it like this we avoid
  issues that can come from CSS styles applied to the canvas by the end user.
&lt;/p&gt;

&lt;script id=&quot;script-9&quot; data-scale=&quot;4&quot;&gt;

    (function () {
      function fillPixelCircle(ctx, cx, cy, r) {
        for (let y = -r; y &lt;= r; y++)
          for (let x = -r; x &lt;= r; x++)
            if (x * x + y * y &lt;= r * r)
              ctx.fillRect(cx + x, cy + y, 1, 1)
      }

      const FONT = {
        &#39;A&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;11111&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;],
        &#39;B&#39;: [&quot;11110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;11110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;11110&quot;],
        &#39;C&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;D&#39;: [&quot;11100&quot;, &quot;10010&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10010&quot;, &quot;11100&quot;],
        &#39;E&#39;: [&quot;11111&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;11110&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;11111&quot;],
        &#39;F&#39;: [&quot;11111&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;11110&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;10000&quot;],
        &#39;G&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10000&quot;, &quot;10111&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;H&#39;: [&quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;11111&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;],
        &#39;I&#39;: [&quot;01110&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;01110&quot;],
        &#39;J&#39;: [&quot;00111&quot;, &quot;00010&quot;, &quot;00010&quot;, &quot;00010&quot;, &quot;00010&quot;, &quot;10010&quot;, &quot;01100&quot;],
        &#39;K&#39;: [&quot;10001&quot;, &quot;10010&quot;, &quot;10100&quot;, &quot;11000&quot;, &quot;10100&quot;, &quot;10010&quot;, &quot;10001&quot;],
        &#39;L&#39;: [&quot;10000&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;11111&quot;],
        &#39;M&#39;: [&quot;10001&quot;, &quot;11011&quot;, &quot;10101&quot;, &quot;10101&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;],
        &#39;N&#39;: [&quot;10001&quot;, &quot;10001&quot;, &quot;11001&quot;, &quot;10101&quot;, &quot;10011&quot;, &quot;10001&quot;, &quot;10001&quot;],
        &#39;O&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;P&#39;: [&quot;11110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;11110&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;10000&quot;],
        &#39;Q&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10101&quot;, &quot;10010&quot;, &quot;01101&quot;],
        &#39;R&#39;: [&quot;11110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;11110&quot;, &quot;10100&quot;, &quot;10010&quot;, &quot;10001&quot;],
        &#39;S&#39;: [&quot;01111&quot;, &quot;10000&quot;, &quot;10000&quot;, &quot;01110&quot;, &quot;00001&quot;, &quot;00001&quot;, &quot;11110&quot;],
        &#39;T&#39;: [&quot;11111&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;],
        &#39;U&#39;: [&quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;V&#39;: [&quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01010&quot;, &quot;00100&quot;],
        &#39;W&#39;: [&quot;10001&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;10101&quot;, &quot;10101&quot;, &quot;11011&quot;, &quot;10001&quot;],
        &#39;X&#39;: [&quot;10001&quot;, &quot;10001&quot;, &quot;01010&quot;, &quot;00100&quot;, &quot;01010&quot;, &quot;10001&quot;, &quot;10001&quot;],
        &#39;Y&#39;: [&quot;10001&quot;, &quot;10001&quot;, &quot;01010&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;],
        &#39;Z&#39;: [&quot;11111&quot;, &quot;00001&quot;, &quot;00010&quot;, &quot;00100&quot;, &quot;01000&quot;, &quot;10000&quot;, &quot;11111&quot;],
        &#39; &#39;: [&quot;00000&quot;, &quot;00000&quot;, &quot;00000&quot;, &quot;00000&quot;, &quot;00000&quot;, &quot;00000&quot;, &quot;00000&quot;],
        &#39;0&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10011&quot;, &quot;10101&quot;, &quot;11001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;1&#39;: [&quot;00100&quot;, &quot;01100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;01110&quot;],
        &#39;2&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;00001&quot;, &quot;00110&quot;, &quot;01000&quot;, &quot;10000&quot;, &quot;11111&quot;],
        &#39;3&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;00001&quot;, &quot;00110&quot;, &quot;00001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;4&#39;: [&quot;00010&quot;, &quot;00110&quot;, &quot;01010&quot;, &quot;10010&quot;, &quot;11111&quot;, &quot;00010&quot;, &quot;00010&quot;],
        &#39;5&#39;: [&quot;11111&quot;, &quot;10000&quot;, &quot;11110&quot;, &quot;00001&quot;, &quot;00001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;6&#39;: [&quot;00110&quot;, &quot;01000&quot;, &quot;10000&quot;, &quot;11110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;7&#39;: [&quot;11111&quot;, &quot;00001&quot;, &quot;00010&quot;, &quot;00100&quot;, &quot;01000&quot;, &quot;01000&quot;, &quot;01000&quot;],
        &#39;8&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01110&quot;],
        &#39;9&#39;: [&quot;01110&quot;, &quot;10001&quot;, &quot;10001&quot;, &quot;01111&quot;, &quot;00001&quot;, &quot;00010&quot;, &quot;01100&quot;],
        &#39;:&#39;: [&quot;00000&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00000&quot;, &quot;00100&quot;, &quot;00100&quot;, &quot;00000&quot;],
      };

      function drawText(ctx, text, x, y, color = &#39;#000&#39;) {
        ctx.fillStyle = color;
        let cx = x;
        for (const ch of text.toUpperCase()) {
          const g = FONT[ch];
          if (g) {
            for (let row = 0; row &lt; g.length; row++)
              for (let col = 0; col &lt; g[row].length; col++)
                if (g[row][col] === &#39;1&#39;) ctx.fillRect(cx + col, y + row, 1, 1);
          }
          cx += 6;
        }
      }

      const me = document.currentScript
      const cv = document.createElement(&quot;canvas&quot;)

      const scale = Number(me.dataset.scale || 1)
      cv.width = 100
      cv.height = 50
      cv.style.width = (cv.width * scale) + &quot;px&quot;
      cv.style.height = (cv.height * scale) + &quot;px&quot;
      cv.style.imageRendering = &quot;pixelated&quot;
      cv.setAttribute(&quot;tabindex&quot;, 0)

      me.parentNode.insertBefore(cv, me)
      const ctx = cv.getContext(&#39;2d&#39;)

      let running = false
      const radius = 5
      const floor_y = 40
      const tokenRadius = Math.round(radius / 2)
      const tokenY = floor_y - radius / 2
      const player_pos = {x: 20, y: floor_y}
      const player_v = {x: 0, y: 0}
      let obst = []
      let timer = 0
      let doubleJumps = 0
      let jumpTokens = []
      let score = 0
      let hiscore = 0

      cv.addEventListener(&#39;pointerdown&#39;, () =&gt; {
        jump()
      })
      cv.addEventListener(&#39;keydown&#39;, () =&gt; {
        jump()
      });

      function jump() {
        if (player_pos.y === floor_y) {
          player_v.y = -3
        } else if (doubleJumps &gt; 0) {
          player_v.y = -3
          doubleJumps--
        }
      }


      ctx.fillStyle = &#39;white&#39;;
      ctx.fillRect(0, 0, cv.width, cv.height);
      drawText(ctx, &quot;Tiny EndlessRunne&quot;, 0, 10)
      drawText(ctx, &quot;R Mini Small Edit&quot;, 0, 20)
      drawText(ctx, &quot;ion&quot;, 0, 30)
      drawText(ctx, &quot;CLICK TO START&quot;, 10, 40)

      function draw(timestamp) {
        ctx.fillStyle = &#39;white&#39;;
        ctx.fillRect(0, 0, cv.width, cv.height);
        drawText(ctx, `SCORE:${score}`, 2, 2)
        drawText(ctx, `2xJMP:${doubleJumps}`, 2, 9)
        drawText(ctx, `HISCR:${hiscore}`, 50, 2)

        ctx.fillRect(0, floor_y + radius, cv.width, 1)

        player_pos.x += player_v.x
        player_pos.y += player_v.y

        player_v.y += 0.2

        if (player_pos.y &gt; floor_y)
          player_pos.y = floor_y

        fillPixelCircle(ctx, player_pos.x, Math.round(player_pos.y), radius)

        obst = obst.filter((o) =&gt; o.x &gt; -radius)
        obst.forEach((o) =&gt; {
          o.x -= 1;
          ctx.fillRect(o.x, radius + floor_y - o.height, radius, o.height)
          if (o.x &lt; player_pos.x + radius) {
            if (o.x + radius &gt; player_pos.x - radius) {
              if (player_pos.y + radius &gt; floor_y - o.height + radius) {
                score = 0
                doubleJumps = 0
                o.scored = true
                return
              }
            }
          }
          if (o.x &lt; player_pos.x &amp;&amp; !o.scored) {
            o.scored = true
            score++
            if (score &gt; hiscore) {
              hiscore = score
            }
          }
        })

        jumpTokens = jumpTokens.filter((t) =&gt; t.x &gt; radius &amp;&amp; !t.scored)
        jumpTokens.forEach((t) =&gt; {
          t.x -= 1
          fillPixelCircle(ctx, t.x, tokenY, tokenRadius)
          if (t.x - tokenRadius &lt; player_pos.x + radius) {
            if (t.x + tokenRadius &gt; player_pos.x - radius) {
              if (player_pos.y - radius &lt; tokenY + tokenRadius &amp;&amp;
                player_pos.y + radius &gt; tokenY - tokenRadius) {
                doubleJumps++;
                t.scored = true
              }
            }
          }

        })

        timer -= 1

        if (timer &lt; 0) {
          if (Math.random() &lt; 0.1) {
            jumpTokens.push({
              x: cv.width,
              scored: false
            })
          } else {
            obst.push({
              x: cv.width,
              height: Math.round(radius + Math.random() * radius),
              scored: false
            })
          }
          timer = Math.random() * 100
        }
        if (running)
          requestAnimationFrame(draw)
      }

      cv.addEventListener(&#39;focus&#39;, () =&gt; {
        cv.style.outline = &#39;2px solid #2d2d2d&#39;
        if (!running) {
          requestAnimationFrame(draw)
          running = true
        }
      })
      cv.addEventListener(&#39;blur&#39;, () =&gt; {
        cv.style.outline = &#39;none&#39;
        running = false
      })

    })()
  /*
  * TinyEndlessRunnerMini - Small Edition © 2026 by Leopold Arreström is licensed under
  * Creative Commons Attribution 4.0 International.
  * To view a copy of this license, visit:
  * https://creativecommons.org/licenses/by/4.0/
  */
&lt;/script&gt;

&lt;p&gt;
  Here is a finished simple example. An endless runner widget you can copy and paste where you&#39;d like on the web.
  Some learnings from this is that in order to keep the graphics as crisp pixels you gotta keep to whole numbers.
  Drawing a square at x=0.5 will give bad results.
&lt;/p&gt;

&lt;!-- add syntax highlights --&gt;
&lt;script&gt;
  hljs.highlightAll()
&lt;/script&gt;
</content:encoded>
      <category>javascript</category>
      <category>tutorial</category>
      <pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate>
      <dc:creator>Leo</dc:creator>
      <guid>https://leozard.com/posts/demo-1/</guid>
    </item>
    <item>
      <title>My first post</title>
      <link>https://leozard.com/posts/first-post/</link>
      <description>This is the first post</description>
      <content:encoded>Hello world!
</content:encoded>
      <category>blog</category>
      <pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate>
      <dc:creator>Leo</dc:creator>
      <guid>https://leozard.com/posts/first-post/</guid>
    </item>
  </channel>
</rss>
