<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ canvas - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ canvas - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 13 Sep 2026 16:32:15 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/canvas/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ High-Frequency Real-Time Data in React: From Ring Buffers to OffscreenCanvas ]]>
                </title>
                <description>
                    <![CDATA[ React is great at many things. But if you've ever tried pushing thousands of data points per second through it, you'll quickly learn that React isn't a firehose. It's more like a garden hose. Try forc ]]>
                </description>
                <link>https://www.freecodecamp.org/news/high-frequency-real-time-data-in-react-from-ring-buffers-to-offscreencanvas/</link>
                <guid isPermaLink="false">6a84d268fa401f6597cd200e</guid>
                
                    <category>
                        <![CDATA[ react js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ canvas ]]>
                    </category>
                
                    <category>
                        <![CDATA[ realtime ]]>
                    </category>
                
                    <category>
                        <![CDATA[ workers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ concurrency ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vineeth Pawar ]]>
                </dc:creator>
                <pubDate>Tue, 18 Aug 2026 21:45:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c3ffaae8-51d5-4add-9a9e-e49443e49746.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>React is great at many things. But if you've ever tried pushing thousands of data points per second through it, you'll quickly learn that React isn't a firehose. It's more like a garden hose.</p>
<p>Try forcing too much through it, and either the lawn floods (your DOM) or the pipe bursts (your app).</p>
<p>There's a second observation that pairs with the first. Your laptop has 8 to 16 CPU cores. Your React app uses 1 of them, almost always. The main thread handles JavaScript, the DOM, layout, and paint setup. The other cores sit idle while the main thread struggles to keep a 60fps frame budget.</p>
<p>Both problems have the same shape: you need to keep React out of the hot path, and you need to use more than one thread. The patterns that get you there also happen to be the patterns behind Figma's canvas engine, Bloomberg's trading dashboards, and every biosignal viewer you've seen.</p>
<p>In one project, I had to visualise 19 EEG (brainwave) channels, each sending about 1,000 data points per second. That's almost 19,000 updates per second. If you feed all of that directly into React, the UI doesn't just slow down. It faints dramatically.</p>
<p>This article is the end-to-end architecture I landed on: the ring buffers, workers, shared memory, off-main rendering, and specific patterns that hold up under sustained multi-hour load.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-whos-already-doing-this">Who's Already Doing This?</a></p>
</li>
<li><p><a href="#heading-the-1khz-math">The 1kHz Math</a></p>
</li>
<li><p><a href="#heading-where-it-usually-goes-wrong">Where it Usually Goes Wrong</a></p>
</li>
<li><p><a href="#heading-the-mental-model-air-traffic-control-plus-a-kitchen-brigade">The Mental Model: Air Traffic Control Plus a Kitchen Brigade</a></p>
</li>
<li><p><a href="#heading-step-1-stop-putting-samples-in-react-state">Step 1: Stop Putting Samples in React State</a></p>
</li>
<li><p><a href="#heading-step-2-separate-shape-from-values">Step 2: Separate Shape from Values</a></p>
</li>
<li><p><a href="#heading-step-3-move-heavy-work-off-the-main-thread">Step 3: Move Heavy Work Off the Main Thread</a></p>
</li>
<li><p><a href="#heading-step-4-render-off-main-with-offscreencanvas">Step 4: Render Off Main with OffscreenCanvas</a></p>
</li>
<li><p><a href="#heading-step-5-decimate-before-you-draw">Step 5: Decimate Before You Draw</a></p>
</li>
<li><p><a href="#heading-step-6-wrap-an-external-renderer">Step 6: Wrap an External Renderer</a></p>
</li>
<li><p><a href="#heading-step-7-when-canvas-isnt-enough-reach-for-webgl">Step 7: When Canvas isn't Enough, Reach for WebGL</a></p>
</li>
<li><p><a href="#heading-step-8-keep-memory-flat">Step 8: Keep Memory Flat</a></p>
</li>
<li><p><a href="#heading-step-9-scheduling-strategies">Step 9: Scheduling Strategies</a></p>
</li>
<li><p><a href="#heading-step-10-measure-sustained-performance">Step 10: Measure Sustained Performance</a></p>
</li>
<li><p><a href="#heading-case-study-19-eeg-channels">Case Study: 19 EEG Channels</a></p>
</li>
<li><p><a href="#heading-benchmarks-single-thread-vs-multi-thread">Benchmarks: Single Thread vs Multi-Thread</a></p>
</li>
<li><p><a href="#heading-the-coopcoep-catch">The COOP/COEP Catch</a></p>
</li>
<li><p><a href="#heading-production-tradeoffs">Production Tradeoffs</a></p>
</li>
<li><p><a href="#heading-should-you-build-like-this">Should You Build Like This?</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most out of this article, you'll want:</p>
<ul>
<li><p><strong>Working knowledge of React 18 or 19.</strong> You should be comfortable with <code>useState</code>, <code>useEffect</code>, <code>useRef</code>, and the difference between mounting and re-rendering.</p>
</li>
<li><p><strong>TypeScript basics.</strong> Most examples are in TypeScript. You should be able to read type annotations without stopping.</p>
</li>
<li><p><strong>A rough sense of the browser main thread and event loop.</strong> You don't need to have written a Web Worker, but knowing what "blocking the main thread" means will make Step 3 easier.</p>
</li>
<li><p><strong>Familiarity with Canvas 2D or a chart library</strong> is a plus, not a requirement. If you've drawn anything on a canvas, you're ready.</p>
</li>
<li><p><strong>A laptop that can run modern Chrome or Edge.</strong> The examples rely on <code>SharedArrayBuffer</code>, <code>OffscreenCanvas</code>, and Atomics, which need a Chromium-based browser and cross-origin isolation (covered later in the article).</p>
</li>
</ul>
<p>You don't need prior experience with Web Workers, ring buffers, or WebGL. This article introduces each in the context of a real problem.</p>
<h2 id="heading-whos-already-doing-this">Who's Already Doing This?</h2>
<p>The patterns in this article aren't experimental. They're the architecture behind production apps that ingest and render high-frequency data:</p>
<ul>
<li><p><strong>Trading and finance dashboards</strong> (Bloomberg, Hyperliquid, dYdX, every serious market viewer) push thousands of price ticks per second through canvas-rendered grids.</p>
</li>
<li><p><strong>Figma</strong> runs its entire canvas engine in WebAssembly inside a worker. The main thread renders React for the chrome only.</p>
</li>
<li><p><strong>Google Docs and Microsoft Loop</strong> run their document models in workers, with the DOM as the projection.</p>
</li>
<li><p><strong>Charting libraries</strong> like LightningChart, uPlot, Plotly, and ECharts draw on Canvas or WebGL and treat React as a wrapper.</p>
</li>
<li><p><strong>Biosignal, ECG, EEG, and motion-capture apps</strong> routinely process samples at 1kHz or higher and stream them to live plots.</p>
</li>
<li><p><strong>Observability and APM tools</strong> (Datadog live tail, Grafana real-time panels) decouple ingestion from render to keep tabs responsive.</p>
</li>
<li><p><strong>Audio editors and visualisers</strong>, plus anything using the Web Audio API with a waveform display.</p>
</li>
<li><p><strong>transformers.js and ONNX Runtime Web</strong> place ML inference in workers by default.</p>
</li>
</ul>
<p>Different domains, same trick: React owns what changes rarely, something else owns what changes at refresh rate, and heavy work happens on threads that aren't the main one.</p>
<h2 id="heading-the-1khz-math">The 1kHz Math</h2>
<p>Some numbers to make the problem concrete.</p>
<ul>
<li><p>A sample arrives every 1ms.</p>
</li>
<li><p>A 60Hz display refreshes every 16.67ms.</p>
</li>
<li><p>So in one frame, you'll receive roughly <strong>16 to 17 samples per stream</strong>.</p>
</li>
<li><p>With 19 active streams (the EEG case), that's <strong>300 to 320 samples per frame</strong>.</p>
</li>
</ul>
<p>If you <code>setState</code> on each sample, React tries to do around 19,000 renders per second. It can't, so it skips frames. The UI stutters and your laptop fans take off.</p>
<p>If you <code>setState</code> once per frame with the batch of ~320 samples, React does 60 renders per second, which is easy.</p>
<p>That single reframe is the entire trick, and it will echo through every step below.</p>
<h2 id="heading-where-it-usually-goes-wrong">Where it Usually Goes Wrong</h2>
<p>Here's the version of the code I see in most real-time React apps the first time they try this. It looks reasonable. It's also the source of every bit of jank the team will spend the next two weeks tracking down.</p>
<pre><code class="language-tsx">import { useEffect, useState } from "react";

export default function NaiveChart({ socket }) {
  const [data, setData] = useState&lt;number[]&gt;([]);

  useEffect(() =&gt; {
    socket.on("newPoint", (point: number) =&gt; {
      setData((prev) =&gt; [...prev, point]); // re-renders every time
    });
  }, [socket]);

  return &lt;div&gt;{data.length} points&lt;/div&gt;;
}
</code></pre>
<p>Three problems baked into seven lines:</p>
<ol>
<li><p><strong>Every incoming sample triggers a re-render:</strong> At 1kHz that's 1,000 renders per second. React was never going to be happy about that.</p>
</li>
<li><p><code>[...prev, point]</code> <strong>allocates a new array on every push:</strong> At 1kHz that's a new array per millisecond, all of which the garbage collector has to clean up. The heap climbs, GC pauses lengthen, the fan kicks in.</p>
</li>
<li><p><strong>There's no upper bound on the array:</strong> Run this for an hour and you have 3.6 million numbers in memory, all of which React has to consider on every render.</p>
</li>
</ol>
<p>The fix isn't one trick. It's a stack of small ones, each addressing one of those failure modes and, eventually, the deeper problem of the main thread being the only thread.</p>
<h2 id="heading-the-mental-model-air-traffic-control-plus-a-kitchen-brigade">The Mental Model: Air Traffic Control Plus a Kitchen Brigade</h2>
<p>Before the code, two metaphors that will keep the pieces straight.</p>
<p><strong>First, air traffic control:</strong> Three roles, one airport.</p>
<ul>
<li><p>The <strong>control tower</strong> (your store) sees every plane (every sample) and tracks where it is.</p>
</li>
<li><p>The <strong>ground crew</strong> (React) sets up the runways and gates: the layout that planes use.</p>
</li>
<li><p>The <strong>pilots</strong> (the draw loop) actually fly. They look at the tower for clearance and act every few seconds.</p>
</li>
</ul>
<p>The control tower doesn't pull a gate from the ground crew every time a plane moves. It just holds the data. The ground crew rearranges gates when the schedule changes, which is rare. The pilots act constantly, at their own rate, off the tower's data.</p>
<p><strong>Second, a kitchen brigade:</strong> A restaurant kitchen at peak hour. One head chef can't make every dish. The classical brigade has stations: sauce, fish, pastry, garde manger, plating, and service. Each station owns a slice of the meal. The expediter coordinates timing.</p>
<ul>
<li><p>The <strong>expediter</strong> is your main thread.</p>
</li>
<li><p>The <strong>stations</strong> are your workers.</p>
</li>
<li><p>The <strong>plating window</strong> is your shared memory.</p>
</li>
<li><p>The <strong>dishes going to tables</strong> are your rendered frames.</p>
</li>
</ul>
<p>A single chef trying to do everything serially is your main-thread-only frontend. A brigade is your worker-based one. The brigade is faster because cuts, sauces, and sears happen in parallel, not because any one cook is faster than the soloist. The expediter doesn't cook, they orchestrate.</p>
<p>Everything below is a specific application of these two ideas.</p>
<h2 id="heading-step-1-stop-putting-samples-in-react-state">Step 1: Stop Putting Samples in React State</h2>
<p>The single biggest mistake in real-time React apps is treating every sample as state, which it isn't. State is what determines <em>which components exist and how they're arranged</em>. A live plot is one component. The 60,000 samples scrolling across it aren't 60,000 pieces of state. They're one buffer.</p>
<p>You want a store that lives outside React. A ring buffer over a typed array gives you constant-time inserts and a bounded heap:</p>
<pre><code class="language-ts">type Listener = () =&gt; void;

export function createSampleStore(capacity: number) {
  const buf = new Float32Array(capacity);
  let head = 0;
  let size = 0;
  const listeners = new Set&lt;Listener&gt;();

  return {
    push(sample: number) {
      buf[head] = sample;
      head = (head + 1) % capacity;
      if (size &lt; capacity) size++;
    },
    pushBatch(samples: Float32Array) {
      for (let i = 0; i &lt; samples.length; i++) {
        buf[head] = samples[i];
        head = (head + 1) % capacity;
        if (size &lt; capacity) size++;
      }
    },
    read(): Float32Array {
      if (size &lt; capacity) return buf.subarray(0, size);
      const out = new Float32Array(capacity);
      out.set(buf.subarray(head));
      out.set(buf.subarray(0, head), capacity - head);
      return out;
    },
    subscribe(l: Listener) {
      listeners.add(l);
      return () =&gt; listeners.delete(l);
    },
  };
}
</code></pre>
<p>Notice what's not here: no <code>useState</code>, no setter, no React anything. It's just plain JavaScript. The store can accept a million pushes per second and React won't care, because React isn't subscribed.</p>
<p>Here are the ring buffer mechanics, visualised:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779361162753/2374fc6e-429c-4a8a-8b31-30fb51c6052c.png" alt="Ring buffer with eight indexed slots, a head pointer advancing each push and wrapping around when it reaches the end, providing constant-time inserts and a bounded heap" style="display: block;" width="1600" height="463" loading="lazy">

<p>We have head advances on every push and wraps at capacity. Reads pull a window of the last N samples in chronological order. The result is constant time and a bounded heap.</p>
<p>A simpler stop-gap, when you don't want a typed buffer yet, is to put the rolling window in a ref instead of state:</p>
<pre><code class="language-tsx">import { useEffect, useRef } from "react";

export default function RefChart({ socket }) {
  const bufferRef = useRef&lt;number[]&gt;([]);

  useEffect(() =&gt; {
    socket.on("newPoint", (point: number) =&gt; {
      bufferRef.current.push(point);
      if (bufferRef.current.length &gt; 1000) {
        bufferRef.current.shift();
      }
    });
  }, [socket]);

  return &lt;div&gt;Streaming {bufferRef.current.length} points&lt;/div&gt;;
}
</code></pre>
<p>This is the "10x faster than <code>NaiveChart</code>, still not great" version. The render doesn't trigger on every push, but you also won't see updates unless something else re-renders. For real plotting, pair the ref with a <code>requestAnimationFrame</code> draw loop (see Step 2).</p>
<h2 id="heading-step-2-separate-shape-from-values">Step 2: Separate Shape from Values</h2>
<p>React renders when the <em>shape</em> of the UI changes. New plot? Re-render. Removed plot? Re-render. Switched from line to bar? Re-render. None of those happen at 1000Hz. They happen a few times a minute, when the user clicks something.</p>
<p>The <em>values</em> inside each plot change at the data rate. Those should never touch React.</p>
<p>Concretely:</p>
<pre><code class="language-tsx">function LivePlot({ store }: { store: SampleStore }) {
  const canvasRef = useRef&lt;HTMLCanvasElement&gt;(null);

  useEffect(() =&gt; {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d")!;

    let raf = 0;
    const draw = () =&gt; {
      const samples = store.read();
      drawSeries(ctx, samples);
      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () =&gt; cancelAnimationFrame(raf);
  }, [store]);

  return &lt;canvas ref={canvasRef} width={1200} height={300} /&gt;;
}
</code></pre>
<p>React renders this component once. The <code>useEffect</code> runs once. The <code>requestAnimationFrame</code> loop reads from the store on every frame.</p>
<p>The store can be pushed to at any rate the source can manage. The user sees a smooth 60fps line whether the source is sending 100 samples per second or 100,000. The instinct to wire <code>samples</code> into <code>useState</code> is wrong, so resist it.</p>
<h3 id="heading-selective-subscriptions-when-you-do-need-react-in-the-loop">Selective Subscriptions When You Do Need React in the Loop</h3>
<p>Sometimes a component genuinely depends on the data (summary stats, axis labels, or a live value badge). For that, use a store with selective subscriptions so only the components that care re-render. Zustand makes this trivial:</p>
<pre><code class="language-ts">import { create } from "zustand";

const useDataStore = create&lt;{ latest: number | null; setLatest: (v: number) =&gt; void }&gt;((set) =&gt; ({
  latest: null,
  setLatest: (v) =&gt; set({ latest: v }),
}));

function LatestBadge() {
  // Only re-renders when latest changes, not when other store fields do.
  const latest = useDataStore((state) =&gt; state.latest);
  return &lt;div&gt;Latest: {latest?.toFixed(2)}&lt;/div&gt;;
}
</code></pre>
<p>Pair this with RAF coalescing on the writes (call <code>setLatest</code> once per frame, not once per sample) and you get a React component that updates smoothly at 60fps no matter what the data rate is.</p>
<p><code>useSyncExternalStore</code> is the native equivalent and works against any pub-sub store, including the ring buffer above. Use whichever feels lighter for your team.</p>
<h2 id="heading-step-3-move-heavy-work-off-the-main-thread">Step 3: Move Heavy Work Off the Main Thread</h2>
<p>The store and imperative draw loop handle React's contribution to the bottleneck. The main thread itself is still doing all the ingest, parsing, and math. Everything past ~50,000 samples per second per stream needs more.</p>
<p>The browser gives you four escape hatches: Web Workers, transferable objects, <code>SharedArrayBuffer</code> + Atomics, and <code>OffscreenCanvas</code>. Each solves a specific problem.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369113008/d1d608c9-8078-4339-91e1-c9ee221bf014.png" alt="Browser process model with the renderer process containing the main thread, dedicated workers, shared workers, a service worker, and a compositor thread; the GPU process containing the GPU thread for WebGL and WebGPU; and the network process containing the network thread for fetch and WebSocket, with communication between main and worker types happening via postMessage" style="display: block;" width="1600" height="1006" loading="lazy">

<p>The main thread runs JavaScript, the DOM, layout, and paint setup. Workers are isolated JavaScript contexts with their own event loops. They can't touch the DOM, they can't share memory by default, and every cross-thread message is async and copied unless you transfer it.</p>
<p>Three properties matter as you design against this model:</p>
<ul>
<li><p><strong>Workers can't touch the DOM:</strong> That's the point. They run pure JavaScript. Perfect for data work, network parsing, math, codecs, ML inference.</p>
</li>
<li><p><strong>Communication is async:</strong> No shared variable to read in the middle of a function. Plan APIs around requests and events.</p>
</li>
<li><p><strong>Data copies, unless you transfer or share it:</strong> Structured clone is the default, transferable objects skip the copy, and <code>SharedArrayBuffer</code> skips it permanently.</p>
</li>
</ul>
<h3 id="heading-dedicated-workers-in-practice">Dedicated Workers in Practice</h3>
<p>Here's a minimal worker for ingest:</p>
<pre><code class="language-ts">// worker.ts
self.onmessage = (event) =&gt; {
  const { samples } = event.data;
  // Decode, filter, decimate. The main thread doesn't see any of this.
  const summary = computeStats(samples);
  postMessage(summary);
};
</code></pre>
<pre><code class="language-tsx">// main thread
import { useEffect, useRef } from "react";

export default function WorkerChart({ socket, store }) {
  const workerRef = useRef&lt;Worker&gt;();

  useEffect(() =&gt; {
    workerRef.current = new Worker(new URL("./worker.ts", import.meta.url), {
      type: "module",
    });

    workerRef.current.onmessage = (event) =&gt; {
      store.pushBatch(event.data); // pre-processed, cheap to ingest
    };

    socket.on("newPoint", (point: number) =&gt; {
      workerRef.current?.postMessage({ point });
    });

    return () =&gt; workerRef.current?.terminate();
  }, [socket, store]);

  return &lt;LivePlot store={store} /&gt;;
}
</code></pre>
<p>Three things to internalise about workers.</p>
<p>First, workers are real processes from the runtime's perspective. Each has its own heap, event loop, and <code>globalThis</code>. Starting one costs about 1 to 5ms. Don't spin them up inside hot paths.</p>
<p>Second, module workers are the modern default. <code>type: "module"</code> enables ES modules inside the worker, including <code>import</code>. The legacy <code>importScripts</code> is for classic workers, so avoid it for new code.</p>
<p>Finally, bundlers know about workers. Vite, Webpack, esbuild, and Rspack all detect the <code>new Worker(new URL("./x.ts", import.meta.url))</code> pattern and produce a separate chunk for the worker.</p>
<h3 id="heading-a-worker-pool-that-scales-with-cores">A Worker Pool That Scales with Cores</h3>
<p>For CPU-bound work (parsing binary frames, decoding audio, computing FFTs), a pool spreads jobs across all available cores:</p>
<pre><code class="language-ts">class WorkerPool {
  private workers: Worker[];
  private next = 0;
  private pending = new Map&lt;string, (result: unknown) =&gt; void&gt;();

  constructor(scriptUrl: URL, size = Math.max(1, navigator.hardwareConcurrency - 1)) {
    this.workers = Array.from({ length: size }, () =&gt; {
      const w = new Worker(scriptUrl, { type: "module" });
      w.onmessage = (event) =&gt; {
        const cb = this.pending.get(event.data.id);
        if (!cb) return;
        this.pending.delete(event.data.id);
        cb(event.data.result);
      };
      return w;
    });
  }

  async run&lt;T&gt;(kind: string, payload: unknown, transferables: Transferable[] = []): Promise&lt;T&gt; {
    const id = crypto.randomUUID();
    const result = new Promise&lt;T&gt;((resolve) =&gt; this.pending.set(id, resolve as (r: unknown) =&gt; void));
    const worker = this.workers[this.next];
    this.next = (this.next + 1) % this.workers.length;
    worker.postMessage({ id, kind, payload }, transferables);
    return result;
  }
}

export const pool = new WorkerPool(new URL("./decoder.worker.ts", import.meta.url));
</code></pre>
<p>Round-robin distribution with one worker per core minus one. Pending requests resolve when the matching response arrives. It's cheap, predictable, and scales linearly until you hit memory bandwidth limits.</p>
<h3 id="heading-transferable-objects-the-no-copy-path">Transferable Objects, the No-copy Path</h3>
<p><code>postMessage</code> clones the payload by default. Cloning a 10MB buffer takes milliseconds and doubles your memory use. The fix is to <strong>transfer</strong> the buffer instead.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369124743/0a53c521-618c-48ec-a5c5-935e1acdbf1a.png" alt="Side-by-side comparison of postMessage modes: default postMessage clones the buffer with a copy landing in both the main heap and the worker heap via structured clone, while Transferable transfers ownership so the same buffer exists once with ownership moving from main to worker" style="display: block;" width="1600" height="4546" loading="lazy">

<p>The semantics: when you transfer a buffer, the sender loses access. The receiver gains it, with no copy and an O(1) handoff.</p>
<pre><code class="language-ts">const buf = new ArrayBuffer(10 * 1024 * 1024); // 10 MB
new Uint8Array(buf).set(somePayload);

worker.postMessage({ buf }, [buf]); // second arg = list of transferables
// `buf` is now detached on this side. Accessing it throws.
</code></pre>
<p>The list of transferable types in 2026 includes <code>ArrayBuffer</code> (and any typed-array view backed by one), <code>MessagePort</code>, <code>ImageBitmap</code>, <code>OffscreenCanvas</code>, the stream types (<code>ReadableStream</code>, <code>WritableStream</code>, <code>TransformStream</code>), <code>RTCDataChannel</code>, <code>VideoFrame</code>, <code>AudioData</code>, and the WebTransport streams. The most useful for React plus real-time work are <code>ArrayBuffer</code>, <code>OffscreenCanvas</code>, and <code>MessagePort</code>.</p>
<p>A common pitfall is that forgetting to transfer creates silent slowness. The app works, but it copies every message. Profile worker <code>postMessage</code> calls. If they're showing milliseconds for "small" payloads, you're cloning when you should be transferring.</p>
<pre><code class="language-ts">// Bad: copies every frame.
worker.postMessage({ samples: float32Array });

// Good: transfers the underlying buffer.
worker.postMessage({ samples: float32Array }, [float32Array.buffer]);
</code></pre>
<p>The buffer is detached after transfer, so the sender needs to re-allocate (or pull from a pool of pre-allocated buffers) if it wants to keep producing.</p>
<h3 id="heading-sharedarraybuffer-and-atomics">SharedArrayBuffer and Atomics</h3>
<p>Transfer hands a buffer off. <strong>Sharing</strong> lets both threads see the same memory simultaneously.</p>
<pre><code class="language-ts">const sab = new SharedArrayBuffer(1024 * 1024); // 1 MB shared
worker.postMessage({ sab });

// Both main and worker now hold references to the same memory.
const viewMain = new Int32Array(sab);
// Inside worker:
// const viewWorker = new Int32Array(event.data.sab);
</code></pre>
<p>Three properties matter.</p>
<p><code>SharedArrayBuffer</code> needs cross-origin isolation. Your page must be served with <code>Cross-Origin-Opener-Policy: same-origin</code> and <code>Cross-Origin-Embedder-Policy: require-corp</code>. Without these headers, <code>SharedArrayBuffer</code> is undefined in the browser. Covered in more detail later in the article.</p>
<p>You also need <code>Atomics</code> for synchronisation. Multiple threads writing to the same memory without coordination produces undefined results. <code>Atomics</code> gives you compare-and-swap, load, store, add, sub, wait, and notify operations on typed array views.</p>
<pre><code class="language-ts">const sab = new SharedArrayBuffer(8);
const view = new Int32Array(sab);

// Main thread: wake any worker that's waiting on slot 0.
Atomics.store(view, 0, 1);
Atomics.notify(view, 0, 1);

// Worker: block until slot 0 changes from 0.
const result = Atomics.wait(view, 0, 0); // "ok", "not-equal", or "timed-out"
</code></pre>
<p>And lock-free ring buffers are the killer app. A producer-consumer queue between two threads with no locking, no <code>postMessage</code> round-trip, and no GC pressure. The data sits in the shared buffer. Atomics coordinate read/write positions.</p>
<p>Here's a minimal SPSC (single-producer, single-consumer) ring buffer:</p>
<pre><code class="language-ts">type SharedRing = {
  data: Float32Array;          // payload
  control: Int32Array;         // [head, tail]
};

function createSharedRing(capacity: number): SharedRing {
  const sab = new SharedArrayBuffer(capacity * 4 + 16);
  const control = new Int32Array(sab, 0, 4);   // [head, tail, ...]
  const data = new Float32Array(sab, 16, capacity);
  return { data, control };
}

function push(ring: SharedRing, value: number): boolean {
  const head = Atomics.load(ring.control, 0);
  const tail = Atomics.load(ring.control, 1);
  const next = (head + 1) % ring.data.length;
  if (next === tail) return false; // full
  ring.data[head] = value;
  Atomics.store(ring.control, 0, next);
  return true;
}

function pop(ring: SharedRing): number | null {
  const tail = Atomics.load(ring.control, 1);
  const head = Atomics.load(ring.control, 0);
  if (tail === head) return null; // empty
  const value = ring.data[tail];
  Atomics.store(ring.control, 1, (tail + 1) % ring.data.length);
  return value;
}
</code></pre>
<p>The producer writes from one thread. The consumer reads from another, neither blocks, and there's no <code>postMessage</code> between them. At signal rates, this is the only architecture that scales.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369132294/fcefe558-040e-41d3-b759-a8c10c5d5502.png" alt="Single-producer single-consumer ring buffer over SharedArrayBuffer: the producer (ingest worker) writes at the head pointer and increments it, the consumer (render thread) reads from the tail pointer and increments it, both pointers stored in the shared buffer and updated with atomic operations" style="display: block;" width="1600" height="308" loading="lazy">

<p>For multi-producer or multi-consumer queues, you need compare-and-swap loops (<code>Atomics.compareExchange</code>). They get fiddly fast. Most production setups use SPSC where they can and fall back to message-passing where they can't.</p>
<h3 id="heading-in-electron-ingest-in-the-main-process">In Electron, Ingest in the Main Process</h3>
<p>The same idea applies one level up. With Electron and a native SDK, you can ingest samples in the <strong>main process</strong>, buffer there, and forward batches across IPC at the renderer's refresh cadence. One IPC message per sample at 1kHz will saturate IPC. One IPC message per frame with a batch of 16 samples is trivial.</p>
<pre><code class="language-ts">// electron main: buffer in Node, flush at 60Hz
let pending: number[] = [];

device.on("sample", (value) =&gt; pending.push(value));

setInterval(() =&gt; {
  if (pending.length === 0) return;
  const batch = new Float32Array(pending);
  pending = [];
  // Transferable to avoid the structured-clone copy.
  mainWindow.webContents.send("device:samples", batch.buffer, [batch.buffer]);
}, 1000 / 60);
</code></pre>
<pre><code class="language-ts">// preload: expose a thin subscription API to the renderer
contextBridge.exposeInMainWorld("device", {
  onSamples: (cb: (samples: Float32Array) =&gt; void) =&gt; {
    const handler = (_: unknown, buf: ArrayBuffer) =&gt; cb(new Float32Array(buf));
    ipcRenderer.on("device:samples", handler);
    return () =&gt; ipcRenderer.removeListener("device:samples", handler);
  },
});
</code></pre>
<pre><code class="language-tsx">// renderer: feed the store from the bridge
useEffect(() =&gt; {
  return window.device.onSamples((batch) =&gt; store.pushBatch(batch));
}, []);
</code></pre>
<p>The renderer thread of the renderer touches only one batch per frame, no matter how fast the device is. The data work happens upstream.</p>
<h2 id="heading-step-4-render-off-main-with-offscreencanvas">Step 4: Render Off Main with <code>OffscreenCanvas</code></h2>
<p>The DOM is single-threaded. The Canvas API used to be too. <code>OffscreenCanvas</code> breaks that: a canvas you can transfer to a worker, where it draws independently of main.</p>
<pre><code class="language-ts">// Main thread
const canvas = canvasRef.current!;
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
</code></pre>
<pre><code class="language-ts">// Worker
let ctx: OffscreenCanvasRenderingContext2D | null = null;

self.onmessage = (event) =&gt; {
  if (event.data.canvas) {
    ctx = event.data.canvas.getContext("2d");
    return;
  }
  if (event.data.samples &amp;&amp; ctx) {
    drawSeries(ctx, event.data.samples);
  }
};
</code></pre>
<p>The main thread is now free to handle clicks, hover, and other interaction without competing with the draw loop. The worker draws at its own rate, against whatever data it has.</p>
<p>Combined with <code>SharedArrayBuffer</code>, you get the cleanest real-time rendering pipeline available in the browser:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369135331/1a205236-981d-4df7-8b21-698d15b84a63.png" alt="Rendering pipeline off the main thread: a data source feeds an ingest worker which writes into a SharedArrayBuffer, which a render worker owning an OffscreenCanvas reads to issue GPU commands that update the DOM, while the main thread reads a summary from the SharedArrayBuffer for React UI" style="display: block;" width="1600" height="260" loading="lazy">

<p>The ingest worker writes samples into the shared buffer. The render worker reads them and draws. Main only reads summaries (FPS, latest value, channel labels) and renders chrome. No data ever crosses through main's event loop.</p>
<p>For 19-channel signal at 1kHz, this is the only architecture that keeps a guaranteed 60fps on a laptop, with headroom.</p>
<h2 id="heading-step-5-decimate-before-you-draw">Step 5: Decimate Before You Draw</h2>
<p>A 1200-pixel-wide canvas can show, at best, 1200 distinct X positions. If you have 60,000 samples in your window and you draw all of them, you do 50 times more work than the user can see.</p>
<p>Pick the right point per pixel column. The "min-max" pattern works well for signals: for each pixel column, find the minimum and maximum value in that range and draw a vertical line between them. This is visually identical to drawing every point, but it's much cheaper.</p>
<pre><code class="language-ts">function drawDecimated(
  ctx: CanvasRenderingContext2D,
  samples: Float32Array,
  width: number,
) {
  const samplesPerPixel = samples.length / width;
  ctx.beginPath();
  for (let x = 0; x &lt; width; x++) {
    const start = Math.floor(x * samplesPerPixel);
    const end = Math.floor((x + 1) * samplesPerPixel);
    let min = Infinity;
    let max = -Infinity;
    for (let i = start; i &lt; end; i++) {
      const v = samples[i];
      if (v &lt; min) min = v;
      if (v &gt; max) max = v;
    }
    ctx.moveTo(x, scale(min));
    ctx.lineTo(x, scale(max));
  }
  ctx.stroke();
}
</code></pre>
<p><a href="https://en.wikipedia.org/wiki/Downsampling_(signal_processing)">Decimation</a> is the single biggest CPU win in real-time visualisation, and almost nobody does it.</p>
<p>A more visually faithful variant is <strong>LTTB (Largest Triangle Three Buckets)</strong>, an algorithm that picks one representative sample per bucket while preserving the visual shape better than min-max. It's worth the read if you're plotting non-signal data like stock charts where peaks and dips matter individually. Most good chart libraries (uPlot, Plotly, ECharts) include decimation out of the box.</p>
<h2 id="heading-step-6-wrap-an-external-renderer">Step 6: Wrap an External Renderer</h2>
<p>You don't always need to write the draw loop yourself. React is great at lifecycle management. Imperative chart libraries are great at raw performance. The right move is often to let React mount and unmount the chart while the library handles the fast inner loop.</p>
<pre><code class="language-tsx">import Uplot from "uplot";
import "uplot/dist/uPlot.min.css";
import { useEffect, useRef } from "react";

export default function UPlotChart({ data }: { data: AlignedData }) {
  const ref = useRef&lt;HTMLDivElement&gt;(null);
  const plotRef = useRef&lt;Uplot | null&gt;(null);

  useEffect(() =&gt; {
    const opts = {
      title: "Realtime Chart",
      width: 600,
      height: 300,
      series: [{}, { label: "Signal" }],
    };
    plotRef.current = new Uplot(opts, data, ref.current!);
    return () =&gt; plotRef.current?.destroy();
  }, []);

  useEffect(() =&gt; {
    plotRef.current?.setData(data); // imperative update, no React render
  }, [data]);

  return &lt;div ref={ref} /&gt;;
}
</code></pre>
<p>uPlot, TimeChart, ECharts, Plotly, LightningChart, and the others all follow this shape: instantiate inside <code>useEffect</code>, call <code>setData</code> imperatively, and destroy on unmount. React orchestrates and the library renders.</p>
<p>The general pattern, for any imperative renderer, looks like this:</p>
<pre><code class="language-tsx">function ChartWrapper({ config, data }) {
  const ref = useRef&lt;HTMLDivElement&gt;(null);

  useEffect(() =&gt; {
    const chart = new SomeFastChartLib(ref.current, config);
    chart.setData(data);
    return () =&gt; chart.destroy();
  }, [config, data]);

  return &lt;div ref={ref} /&gt;;
}
</code></pre>
<p>This is the highest-ROI move if you're not in a domain where you need pixel-level control over the drawing. Reach for a library first, and only write your own draw loop when no library fits.</p>
<h2 id="heading-step-7-when-canvas-isnt-enough-reach-for-webgl">Step 7: When Canvas isn't Enough, Reach for WebGL</h2>
<p>Canvas 2D handles a few thousand line segments per frame comfortably. Past that, you start spending milliseconds in <code>stroke()</code> itself.</p>
<p>WebGL (or its higher-level wrappers like <code>regl</code>, <code>twgl</code>, <code>pixi.js</code>, <code>deck.gl</code>) moves the rendering to the GPU. You pay an upfront cost (writing shaders and managing buffers) for the ability to draw millions of points without breaking a sweat.</p>
<p>Here's a minimal WebGL "line strip with a single vertex shader" sketch:</p>
<pre><code class="language-ts">const gl = canvas.getContext("webgl2")!;

const program = gl.createProgram()!;
// ... compile vertex + fragment shaders, link, get attribute location ...

const samplesBuffer = gl.createBuffer();
function drawWebGL(samples: Float32Array) {
  gl.bindBuffer(gl.ARRAY_BUFFER, samplesBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, samples, gl.STREAM_DRAW);
  gl.useProgram(program);
  gl.drawArrays(gl.LINE_STRIP, 0, samples.length);
}
</code></pre>
<p>The pattern is the same as Canvas 2D: an imperative draw call inside the RAF loop. The difference is the GPU does the actual rasterising. Most real-time charting libraries that claim "millions of points" are doing exactly this under the hood.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779361166770/22241c5c-446b-4026-88fb-33e3caad3831.png" alt="Decision tree comparing rendering options: Canvas 2D for up to a few thousand points, WebGL for tens of thousands or more, and WebGPU for 3D, custom shaders, or compute once stable" style="display: block;" width="1600" height="1351" loading="lazy">

<p>Default to Canvas 2D, and escalate to WebGL only when you can prove Canvas is the bottleneck on the Profiler.</p>
<h2 id="heading-step-8-keep-memory-flat">Step 8: Keep Memory Flat</h2>
<p>Real-time apps die slowly. They run smoothly for an hour and then the laptop fan kicks in. The cause is almost always memory.</p>
<p>There are three rules that keep the heap calm.</p>
<p>First, use typed arrays for numeric data. A <code>Float32Array</code> of 60,000 floats is 240KB. A plain JavaScript array of the same size is around 1.4MB and creates GC pressure on every push.</p>
<p>Second, bound everything. Use ring buffers, not growing arrays. Put caps on history, queues, and pending work. Anything that <em>can</em> grow unbounded eventually will.</p>
<p>Third, reuse buffers. Allocate once in setup, and reuse across frames. Allocating a fresh array per frame at 60fps is 60 allocations per second per plot. Multiply by plot count.</p>
<pre><code class="language-ts">// Bad: fresh array every frame.
function drawFrame() {
  const snapshot = store.read(); // returns a new Float32Array
  drawDecimated(ctx, snapshot, width);
}

// Better: reuse a draw buffer.
const drawBuf = new Float32Array(WINDOW_SIZE);

function drawFrame() {
  store.readInto(drawBuf); // writes into the existing buffer
  drawDecimated(ctx, drawBuf, width);
}
</code></pre>
<p>These rules aren't React-specific. They're the rules any real-time system follows. The reason they need stating is that React-shaped thinking ("derive a new array each render") is the opposite of what real-time wants.</p>
<h2 id="heading-step-9-scheduling-strategies">Step 9: Scheduling Strategies</h2>
<p>A worker pool with no scheduling is just a queue. Sometimes you want priority: a user-driven action should jump ahead of a background sweep.</p>
<h3 id="heading-priority-queues">Priority Queues</h3>
<p>A small priority scheduler:</p>
<pre><code class="language-ts">type Priority = "high" | "normal" | "low";

class PriorityPool {
  private queues: Record&lt;Priority, Job[]&gt; = { high: [], normal: [], low: [] };
  private idle: Worker[];

  enqueue(job: Job, priority: Priority = "normal") {
    if (this.idle.length &gt; 0) {
      const worker = this.idle.pop()!;
      this.dispatch(worker, job);
    } else {
      this.queues[priority].push(job);
    }
  }

  private next(): Job | null {
    for (const p of ["high", "normal", "low"] as const) {
      const j = this.queues[p].shift();
      if (j) return j;
    }
    return null;
  }

  private dispatch(worker: Worker, job: Job) {
    worker.postMessage(job.payload, job.transferables);
    worker.onmessage = (event) =&gt; {
      job.resolve(event.data);
      const next = this.next();
      if (next) this.dispatch(worker, next);
      else this.idle.push(worker);
    };
  }
}
</code></pre>
<p>Three buckets are usually enough: high (user-initiated), normal (steady-state work), low (background sweeps, prefetch, telemetry flushing).</p>
<h3 id="heading-chunkable-work">Chunkable Work</h3>
<p>A job that takes 2 seconds blocks a worker for 2 seconds. If you want to keep workers responsive to higher-priority jobs, the job has to be chunkable.</p>
<pre><code class="language-ts">// In the worker:
self.onmessage = async (event) =&gt; {
  const { id, kind, payload } = event.data;
  if (kind === "decode_large") {
    const total = payload.byteLength;
    for (let i = 0; i &lt; total; i += CHUNK) {
      const chunk = decodeChunk(payload, i, Math.min(i + CHUNK, total));
      self.postMessage({ id, kind: "progress", chunk, offset: i });
      // Yield so the worker can check its message queue.
      await new Promise((r) =&gt; setTimeout(r, 0));
    }
    self.postMessage({ id, kind: "done" });
  }
  if (kind === "cancel") {
    // ... abort the current job
  }
};
</code></pre>
<p>Yielding inside a worker isn't free, but it lets the worker process cancellation messages or higher-priority jobs interleaved with the big task. For long jobs (like a firmware flash, large file decode, or big render), this is essential.</p>
<h3 id="heading-fan-out-and-fan-in">Fan-out and Fan-in</h3>
<p>Split a big job into N pieces, dispatch each to a different worker, and gather the results.</p>
<pre><code class="language-ts">async function decodeFile(buffer: ArrayBuffer): Promise&lt;DecodedFrame[]&gt; {
  const chunks = splitBuffer(buffer, 8);
  const results = await Promise.all(
    chunks.map((chunk) =&gt; pool.run&lt;DecodedFrame[]&gt;("decode", chunk, [chunk])),
  );
  return results.flat();
}
</code></pre>
<p>This gives you linear speedup on parallelisable workloads, up to the worker count. It's critical for batch operations like decoding a recording, summarising a long document, or computing embeddings for a folder.</p>
<h2 id="heading-step-10-measure-sustained-performance">Step 10: Measure Sustained Performance</h2>
<p>A spike on the Profiler is one thing. A slow heap creep over an hour is another. For real-time apps, you measure two things.</p>
<p>First, frame consistency. Are you holding 60fps consistently, or dropping occasional frames? A simple FPS meter:</p>
<pre><code class="language-ts">let lastTime = performance.now();
let frames = 0;

function rafLoop(now: number) {
  frames++;
  if (now - lastTime &gt;= 1000) {
    const fps = (frames * 1000) / (now - lastTime);
    frames = 0;
    lastTime = now;
    console.log(`fps: ${fps.toFixed(1)}`);
  }
  requestAnimationFrame(rafLoop);
}
requestAnimationFrame(rafLoop);
</code></pre>
<p>A more honest measure is the worst frame in a window. Average frame time hides the jank.</p>
<p>Second, heap stability. Open DevTools Memory tab, take a heap snapshot, run the app for ten minutes, take another snapshot, and then compare. The diff should be flat. If it's growing, you have a leak: retained listeners, growing arrays, or closures holding onto large objects.</p>
<p>For real apps, wire heap usage and FPS into your analytics so you spot regressions before users complain.</p>
<h2 id="heading-case-study-19-eeg-channels">Case Study: 19 EEG Channels</h2>
<p>Back to the project that started this article: we have nineteen channels, each at 1kHz, all rendered simultaneously, and all needing to stay smooth across multi-hour recording sessions.</p>
<p>The first build used <strong>LightningChart</strong>. It's powerful, capable, and beautiful. But it's also heavy. Memory usage climbed noticeably, the chart's own internals were doing a lot of work for our use case (which was 19 simple line plots, not full multi-axis financial charts), and the licensing was a friction point.</p>
<p>We switched to <strong>uPlot</strong>. It's tiny, fast, and written specifically for time-series. Memory usage dropped, render time per frame went from "occasionally over budget" to "always under," and my machine stopped sounding like it was about to take off. The chart library change alone bought us most of the headroom we needed.</p>
<p>The architecture around the chart did the rest. The pipeline runs on four threads:</p>
<ul>
<li><p><strong>One ingest worker</strong> reads from the device SDK over IPC (Electron main process to renderer).</p>
</li>
<li><p><strong>One</strong> <code>SharedArrayBuffer</code> holds the rolling window for all 19 channels.</p>
</li>
<li><p><strong>One render worker</strong> reads the SAB and draws to an <code>OffscreenCanvas</code>.</p>
</li>
<li><p><strong>Main thread</strong> renders chrome: channel labels, controls, and the FPS meter.</p>
</li>
</ul>
<p>The shared buffer layout is one big <code>Float32Array</code> indexed as <code>[channel * samplesPerChannel + sampleIndex]</code>. The ingest worker writes new samples and advances per-channel head pointers (stored in an <code>Int32Array</code> slice of the SAB). The render worker reads the latest window each frame.</p>
<pre><code class="language-ts">const CHANNELS = 19;
const WINDOW_SAMPLES = 60_000;

const sab = new SharedArrayBuffer(CHANNELS * WINDOW_SAMPLES * 4 + CHANNELS * 4);
const heads = new Int32Array(sab, 0, CHANNELS);
const samples = new Float32Array(sab, CHANNELS * 4, CHANNELS * WINDOW_SAMPLES);

function writeSample(channel: number, value: number) {
  const head = Atomics.load(heads, channel);
  samples[channel * WINDOW_SAMPLES + head] = value;
  Atomics.store(heads, channel, (head + 1) % WINDOW_SAMPLES);
}
</code></pre>
<p>The renderer reads the channel buffers, decimates per pixel column, and draws:</p>
<pre><code class="language-ts">// render.worker.ts
function drawFrame() {
  const ctx = offscreenCtx;
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (let ch = 0; ch &lt; CHANNELS; ch++) {
    const start = ch * WINDOW_SAMPLES;
    drawDecimatedRow(ctx, samples.subarray(start, start + WINDOW_SAMPLES), ch);
  }
  requestAnimationFrame(drawFrame);
}
</code></pre>
<p>On a 2024 M3 MacBook Pro, this holds 60fps with 19 channels at 1kHz, and 144fps if the display supports it. The main thread stays under 1% utilisation. Workers consume the spare cores. The user feels something that used to require a native app.</p>
<p>The lesson, in short: most of the work is choosing the right tool for the inner loop, and getting out of its way.</p>
<h2 id="heading-benchmarks-single-thread-vs-multi-thread">Benchmarks: Single Thread vs Multi-Thread</h2>
<p>Here are numbers from running comparable workloads on a 2024 M3 MacBook Pro. They're indicative, not promissory.</p>
<table>
<thead>
<tr>
<th>Workload</th>
<th>Main thread only</th>
<th>Worker pool</th>
<th>Workers + SAB</th>
<th>Workers + SAB + OffscreenCanvas</th>
</tr>
</thead>
<tbody><tr>
<td>Parse 100MB binary file</td>
<td>4.2s (UI frozen)</td>
<td>1.1s</td>
<td>1.0s</td>
<td>1.0s</td>
</tr>
<tr>
<td>Decode 1,000 frames</td>
<td>920ms</td>
<td>280ms</td>
<td>240ms</td>
<td>240ms</td>
</tr>
<tr>
<td>Render 1M-point chart</td>
<td>24fps</td>
<td>24fps</td>
<td>28fps</td>
<td>60fps</td>
</tr>
<tr>
<td>Telemetry: 4 streams x 1000Hz</td>
<td>22fps</td>
<td>38fps</td>
<td>55fps</td>
<td>60fps</td>
</tr>
<tr>
<td>EEG: 19 channels x 1kHz</td>
<td>12fps</td>
<td>25fps</td>
<td>48fps</td>
<td>60fps (144fps possible)</td>
</tr>
<tr>
<td>Main-thread JS time per frame</td>
<td>22ms</td>
<td>8ms</td>
<td>4ms</td>
<td>&lt; 1ms</td>
</tr>
<tr>
<td>Memory overhead</td>
<td>baseline</td>
<td>+50MB</td>
<td>+20MB</td>
<td>+20MB</td>
</tr>
<tr>
<td>Worker spin-up latency (first call)</td>
<td>0</td>
<td>2-5ms</td>
<td>2-5ms</td>
<td>5-10ms</td>
</tr>
</tbody></table>
<p>The pattern: workers alone help, and workers plus shared memory help more. Workers plus shared memory plus <code>OffscreenCanvas</code> is what gets you to "the main thread is doing nothing and the chart is still smooth."</p>
<p>For chart-heavy apps, the leap from "workers" to "workers plus <code>OffscreenCanvas</code>" is the biggest single architectural improvement available without leaving the browser.</p>
<h2 id="heading-the-coopcoep-catch">The COOP/COEP Catch</h2>
<p><code>SharedArrayBuffer</code> and high-resolution timers were tightened in 2020 after Spectre/Meltdown. To use them, your page must be served with two HTTP headers:</p>
<pre><code class="language-plaintext">Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
</code></pre>
<p>This opts your page into "cross-origin isolation." Within an isolated context, <code>SharedArrayBuffer</code> exists, <code>performance.now()</code> is high-resolution, and various other restricted APIs work.</p>
<p>Outside an isolated context, <code>SharedArrayBuffer</code> is undefined, <code>performance.now()</code> is throttled to about 1ms precision, and Atomics throw.</p>
<p>The cost is significant. <code>require-corp</code> means every cross-origin resource (images from a CDN, embedded YouTube videos, third-party fonts, analytics scripts) must explicitly opt in by setting <code>Cross-Origin-Resource-Policy: cross-origin</code> or <code>Cross-Origin-Embedder-Policy: credentialless</code>. Many third-party services don't, which breaks their embedding.</p>
<p>There are two practical options:</p>
<ul>
<li><p><strong>For an Electron app:</strong> the renderer process can be configured to use these headers easily. Most production Electron apps that want real-time visualisation enable them by default.</p>
</li>
<li><p><strong>For a browser app:</strong> weigh the embeds you'd lose against the performance you'd gain. If your app is the main attraction (Figma, Google Docs), opt in. If you depend on third-party widgets, the cost is real.</p>
</li>
</ul>
<p>For chart-heavy or signal-heavy apps that need SAB, the Electron path is usually cleaner.</p>
<h2 id="heading-production-tradeoffs">Production Tradeoffs</h2>
<p>Here are the five real costs of everything above.</p>
<ul>
<li><p><strong>Code complexity:</strong> A worker-driven app has 2 to 4 times the source files of a single-threaded one (main + workers + shared types). It's worth it for the right scale, but painful for a trivial app.</p>
</li>
<li><p><strong>Debugging:</strong> Stack traces split across threads. Chrome DevTools handles this well in 2026 (each worker has its own debugger panel), but it's still more work than a single-thread bug.</p>
</li>
<li><p><strong>Bundle size:</strong> Each worker is a separate chunk. Tree-shaking inside workers is sometimes worse than in main (less mature). Audit worker bundles separately.</p>
</li>
<li><p><strong>Startup latency:</strong> Spinning up workers at app start adds 50 to 200ms. Pre-warm them during the splash screen, or accept the first-frame delay.</p>
</li>
<li><p><strong>Browser API gaps:</strong> <code>localStorage</code>, <code>document</code>, and most DOM APIs aren't available in workers. Some libraries silently rely on them and break. Test in a worker context before bundling a library you haven't tried there.</p>
</li>
</ul>
<p>There are three trade-offs specific to the imperative rendering pattern:</p>
<ul>
<li><p><strong>Declarative animation of the data:</strong> The chart frame, labels, and controls all stay declarative. The data inside the chart becomes imperative.</p>
</li>
<li><p><strong>Easy snapshot testing of the rendered output:</strong> A canvas has no DOM you can query. Test the data path separately from the draw path. Snapshot the store output, not the pixels.</p>
</li>
<li><p><strong>React's component story for the inner loop:</strong> The draw loop is a closure. Composing draw loops is harder than composing components. Pick your component boundary carefully so each canvas does one thing.</p>
</li>
</ul>
<p>For most apps these costs aren't worth paying. For an app that has to render 1kHz data smoothly, they're the price of admission.</p>
<h2 id="heading-should-you-build-like-this">Should You Build Like This?</h2>
<p>If your data rate is below 30 updates per second per stream, none of this is needed. Naïve <code>setState</code> per batch will work. Profile first, optimise second.</p>
<p>This architecture earns its keep when:</p>
<ul>
<li><p>You have many streams or sensors</p>
</li>
<li><p>Ingestion is sustained, not bursty</p>
</li>
<li><p>The UX promise is smooth motion for hours, not seconds</p>
</li>
<li><p>You'd rather not rewrite the UI in a native language to get there</p>
</li>
</ul>
<p>The boring rule: start with <code>requestAnimationFrame</code> coalescing and external stores. Promote to workers when those aren't enough. Promote to shared memory when worker <code>postMessage</code> is the bottleneck. Promote to <code>OffscreenCanvas</code> when the render loop itself becomes the bottleneck. Each step is a real architectural investment. Take them in order.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>React can handle real-time visualisation if you use it the right way. Instead of pushing React to do everything, use it as the conductor. Let specialised libraries and workers handle the heavy lifting.</p>
<p>These are the three rules that hold up across every high-frequency React app I've built:</p>
<ol>
<li><p><strong>Workers do the work, while main does the UI.</strong> If main is doing math, you've put the math in the wrong place.</p>
</li>
<li><p><strong>Transfer if you can, share if you must.</strong> Both beat cloning. Sharing is more complex than transferring.</p>
</li>
<li><p><strong>Let React orchestrate. Let specialised tools render.</strong> The store owns the data, the draw loop owns the values, and React owns the shape.</p>
</li>
</ol>
<p>Get those right and your React app stops being a one-core system that flinches at high-frequency data. It becomes a real multi-core system that scales with the hardware, ingests without dropping, and renders without stuttering.</p>
<p>The cores are right there. Use them.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a href="https://github.com/leeoniya/uPlot">uPlot</a>: tiny, fast, focused on time-series.</p>
</li>
<li><p><a href="https://github.com/huww98/TimeChart">TimeChart</a>: high-performance real-time chart on WebGL.</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">Using Web Workers in React</a>: MDN reference.</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer">SharedArrayBuffer on MDN</a>: the shared-memory primitive.</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas">OffscreenCanvas on MDN</a>: rendering off the main thread.</p>
</li>
<li><p><a href="https://web.dev/articles/coop-coep">COOP and COEP explainer</a>: what cross-origin isolation buys you.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to use the Fullscreen API in JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ How do you run a game created for the web in fullscreen? In this quick tutorial, you'll see how to display a game or any other HTML element in fullscreen, how to exit fullscreen, and how to make a nice fullscreen toggle button in SVG. Recently I publ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-use-full-screen-api-in-js/</link>
                <guid isPermaLink="false">66c4c81129f446a67a4197ee</guid>
                
                    <category>
                        <![CDATA[ Fullscreen API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ canvas ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SVG ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hunor Márton Borbély ]]>
                </dc:creator>
                <pubDate>Thu, 22 Feb 2024 14:17:32 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/Untitled.022.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>How do you run a game created for the web in fullscreen? In this quick tutorial, you'll see how to display a game or any other HTML element in fullscreen, how to exit fullscreen, and how to make a nice fullscreen toggle button in SVG.</p>
<p>Recently I published a long <a target="_blank" href="https://www.freecodecamp.org/news/how-to-draw-a-gorilla-with-javascript-on-html-canvas/">JavaScript game tutorial</a>. While it was a very packed guide, there were still a few things we could not cover in it: how to display the game in fullscreen.</p>
<p>When you watch a video on YouTube, you have the option to also watch it on fullscreen. But did you know that the fullscreen feature isn't only for video elements?</p>
<p>In JavaScript, there’s a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API">Fullscreen API</a>. And it’s surprisingly simple to use. Here's a quick demo of what we're about to implement. Let's see how it works.</p>
<div class="embed-wrapper">
        <iframe width="100%" height="350" src="https://codepen.io/HunorMarton/embed/QWoRLXM" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="CodePen embed" scrolling="no" allowtransparency="true" allowfullscreen="true" loading="lazy"></iframe></div>
<p>You can also <a target="_blank" href="https://www.youtube.com/watch?v=jX3mIQdQQ2w&amp;t=15s">watch this article as a video</a> on YouTube.</p>
<h2 id="heading-table-of-contents">Table of Contents:</h2>
<ul>
<li><a class="post-section-overview" href="#heading-how-to-enter-fullscreen-mode">How to Enter Fullscreen Mode</a></li>
<li><a class="post-section-overview" href="#heading-how-to-style-the-fullscreen">How to Style the Fullscreen</a></li>
<li><a class="post-section-overview" href="#heading-how-to-display-games-with-the-canvas-element-in-fullscreen">How to Display Games with the Canvas Element in Fullscreen</a></li>
<li><a class="post-section-overview" href="#heading-how-to-exit-fullscreen">How to Exit Fullscreen</a></li>
<li><a class="post-section-overview" href="#heading-how-to-code-a-fullscreen-icon-with-svg">How to Code a Fullscreen Icon with SVG</a></li>
<li><a class="post-section-overview" href="#heading-learn-more">Learn More</a></li>
</ul>
<h2 id="heading-how-to-enter-fullscreen-mode">How to Enter Fullscreen Mode</h2>
<p>Let’s say we have a simple website with some text. And at the bottom, we have a button that will display the text in full screen. We are going to refine the look of this button, but first, let’s get work on the main logic.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-21-at-18.20.51.png" alt="Image" width="600" height="400" loading="lazy">
<em>A simple website with some text and a Toggle Fullscreen button</em></p>
<pre><code class="lang-css"><span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">font-family</span>: Montserrat;
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">50px</span>;
  <span class="hljs-attribute">max-width</span>: <span class="hljs-number">500px</span>;
}
</code></pre>
<p>In the code above, we attached an event handler to the button in HTML. We can then implement the <code>toggleFullscreen</code> function logic in JavaScript.</p>
<p>In this function, all we have to do is call the <code>requestFullScreen</code> method on the <code>document</code>’s <code>documentElement</code> property. And that’s it:</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toggleFullscreen</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">document</span>.documentElement.requestFullscreen();
}
</code></pre>
<p>If you click the button, your website will pop into fullscreen.</p>
<h2 id="heading-how-to-style-the-fullscreen">How to Style the Fullscreen</h2>
<p>Before we cover how to exit full screen and create a nice-looking toggle button, let’s see a few other things.</p>
<p>What you might notice right away is that, with more space, your content might get a bit lost on a full screen. Make sure you have a responsive styling that looks good on every screen size.</p>
<p>You can even style the layout specifically for fullscreen. In CSS you can set a media query that only applies the styling in case the <code>display-mode</code> is <code>fullscreen</code>. </p>
<p>For instance, you can change the font-size, or change the <code>background-color</code> to have a distinct look on full screen.</p>
<pre><code class="lang-css"><span class="hljs-keyword">@media</span> (<span class="hljs-attribute">display-mode:</span> fullscreen) {
  <span class="hljs-selector-tag">body</span> {
    <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#f9bb86</span>;
    <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.2em</span>;
  }
}
</code></pre>
<h2 id="heading-how-to-display-games-with-the-canvas-element-in-fullscreen">How to Display Games with the Canvas Element in Fullscreen</h2>
<p>In this case, we want to make a game that uses the <code>canvas</code> element to be fullscreen – like the <a target="_blank" href="https://www.freecodecamp.org/news/how-to-draw-a-gorilla-with-javascript-on-html-canvas/">Gorillas</a> game – we also need to resize the <code>canvas</code> element to fit the whole screen.</p>
<p>In this case, we can use the <code>windows</code>’s <code>resize</code> event. The event is triggered both when we simply resize the browser window, and when we enter or exit fullscreen mode. </p>
<p>With the <code>resize</code> event, we can resize the <code>canvas</code> element to fit the whole screen, update the scaling, adjust any other properties we need to change on resize and redraw the whole scene.</p>
<pre><code class="lang-js"><span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">"resize"</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// Resize canvas element</span>
  canvas.width = <span class="hljs-built_in">window</span>.innerWidth;
  canvas.height = <span class="hljs-built_in">window</span>.innerHeight;

  <span class="hljs-comment">// Update scaling</span>
  <span class="hljs-comment">// . . .</span>

  <span class="hljs-comment">// Adjust size dependent properties</span>
  <span class="hljs-comment">// . . .</span>

  <span class="hljs-comment">// Redraw canvas</span>
  draw();
});
</code></pre>
<p>If you check the source code of the <a target="_blank" href="https://codepen.io/HunorMarton/pen/jOJZqvp">Gorillas game on CodePen</a>, you'll find similar steps.</p>
<h2 id="heading-how-to-exit-fullscreen">How to Exit Fullscreen</h2>
<p>Now that we know how to enter full screen, how do we exit from it?</p>
<p>By default, if you press the <code>Escape</code> key, the browser switches back to the normal view. In Google Chrome, you even get a notification at the top of the screen about this when you enter fullscreen mode.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-21-at-17.47.03-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>Google Chrome shows a notification on top of the screen once you enter full screen</em></p>
<p>What if you want to exit fullscreen mode when you click the HTML button? Let’s change our button’s behavior to toggle fullscreen on or off.</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toggleFullscreen</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">document</span>.fullscreenElement) {
    <span class="hljs-built_in">document</span>.documentElement.requestFullscreen();
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">document</span>.exitFullscreen();
  }
}
</code></pre>
<p>First, we start by checking if we are in fullscreen mode already. We can do this by checking the <code>document</code>’s <code>fullscreenElement</code> property. If it is undefined, then we enter fullscreen mode the same way we did before. And if we are already in fullscreen mode, then we can exit by calling the document’s <code>exitFullscreen</code> method. </p>
<p>It is really that simple. With a few lines of code, we can implement the logic for a fullscreen toggle button.</p>
<h2 id="heading-how-to-code-a-fullscreen-icon-with-svg">How to Code a Fullscreen Icon with SVG</h2>
<p>If you follow <a target="_blank" href="https://www.freecodecamp.org/news/author/hunor/">my tutorials</a>, you know I love creative coding, and <a target="_blank" href="https://www.freecodecamp.org/news/svg-tutorial-learn-to-code-images/">drawing from code</a>. So let’s update the look of our button, to make it look similar to what we have on YouTube.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-21-at-18.06.07.png" alt="Image" width="600" height="400" loading="lazy">
<em>Fullscreen icon</em></p>
<p>Let’s create an SVG image within our button. If you check the source code of YouTube, you will see that they also use an SVG.</p>
<p>Let’s define an SVG element within HTML. We'll set its size to 30 x 30 and define a <code>path</code> element:</p>
<pre><code class="lang-html">. . .

<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"toggleFullscreen()"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"30"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"30"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">path</span>
      <span class="hljs-attr">stroke</span>=<span class="hljs-string">"black"</span>
      <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"3"</span>
      <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span>
      <span class="hljs-attr">d</span>=<span class="hljs-string">"
        M 10, 2 L 2,2 L 2, 10
        M 20, 2 L 28,2 L 28, 10
        M 28, 20 L 28,28 L 20, 28
        M 10, 28 L 2,28 L 2, 20"</span>
    /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

. . .
</code></pre>
<p>To style the path, we set its color with the <code>stroke</code> property, set its <code>stroke-width</code>, and made sure that we didn't end up with a filled shape. SVG paths by default are filled, so we need to set explicitly that we don’t want to <code>fill</code> this shape.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-21-at-18.02.10.png" alt="Image" width="600" height="400" loading="lazy">
<em>Using the move-to and line-to commands within an SVG path</em></p>
<p>Then we defined the path with a few move-to and line-to commands. We can set these commands as a string in the <code>d</code> attribute of the path element.</p>
<p>We started with a move-to command: <code>M 10, 2</code>. The letter <code>M</code> signifies that we have a move-to command, and the 10 and 2 are the <code>x</code> and <code>y</code> coordinates of this command. We moved to the start of one of the four lines.</p>
<p>Then we continued the path with a line-to command that moves to the corner, and then with another line-to command. The line-to command works in a similar way. It starts with the letter <code>L</code>, then we set an <code>x, y</code> coordinates where the line should go to.</p>
<p>Then we did the same with the other corners. We move to the next line segment with another move-to command and draw a line with two more line-to commands.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Untitled.021.png" alt="Image" width="600" height="400" loading="lazy">
<em>Drawing a path on a Canvas element with JavaScript has some similarities to defining a path in SVG</em></p>
<p><strong>Note</strong>: If you read my <a target="_blank" href="https://www.freecodecamp.org/news/how-to-draw-a-gorilla-with-javascript-on-html-canvas/">previous tutorial</a> on how to make the gorillas game, then you might have noticed that we had something similar there. We also drew paths with move to and line to. Except that there we were drawn on a <code>canvas</code> element with JavaScript, and now we have the commands as a string within the HTML file.</p>
<h3 id="heading-how-to-toggle-the-icons-appearance">How to Toggle the Icon's Appearance</h3>
<p>Now the SVG is looking great, but what if we want to have a different look when we are in fullscreen mode? On YouTube, when we enter fullscreen mode, the button switches to a different icon.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-21-at-18.06.33.png" alt="Image" width="600" height="400" loading="lazy">
<em>The two faces of the fullscreen icon</em></p>
<p>You can do this in different ways. The easiest way is probably to define another path, within the same SVG element with a different look. Then make this path transparent by default. We are going to toggle the visibility of these two paths in JavaScript.</p>
<pre><code class="lang-html">. . .

<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"toggleFullscreen()"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"30"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"30"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">path</span>
      <span class="hljs-attr">id</span>=<span class="hljs-string">"enter-fullscreen"</span>
      <span class="hljs-attr">stroke</span>=<span class="hljs-string">"black"</span>
      <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"3"</span>
      <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span>
      <span class="hljs-attr">d</span>=<span class="hljs-string">"
        M 10, 2 L 2,2 L 2, 10
        M 20, 2 L 28,2 L 28, 10
        M 28, 20 L 28,28 L 20, 28
        M 10, 28 L 2,28 L 2, 20"</span>
    /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">path</span>
      <span class="hljs-attr">id</span>=<span class="hljs-string">"exit-fullscreen"</span>
      <span class="hljs-attr">stroke</span>=<span class="hljs-string">"transparent"</span>
      <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"3"</span>
      <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span>
      <span class="hljs-attr">d</span>=<span class="hljs-string">"
        M 10, 2 L 10,10 L 2, 10
        M 20, 2 L 20,10 L 28, 10
        M 28, 20 L 20,20 L 20, 28
        M 10, 28 L 10,20 L 2, 20"</span>
    /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

. . .
</code></pre>
<p>This second path is very similar to the previous one. Except that we used different coordinates for some of the line-to commands.</p>
<p>Then we set unique IDs for both of these paths, and we update our toggle function in JavaScript. In JavaScript, we get a reference to these paths by ID, and then in the toggle button’s event handler, we can switch the visibility of these elements back and forth.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> enterFullscreen = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"enter-fullscreen"</span>);
<span class="hljs-keyword">const</span> exitFullscreen = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"exit-fullscreen"</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toggleFullscreen</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">document</span>.fullscreenElement) {
    <span class="hljs-built_in">document</span>.documentElement.requestFullscreen();
    enterFullscreen.setAttribute(<span class="hljs-string">"stroke"</span>, <span class="hljs-string">"transparent"</span>);
    exitFullscreen.setAttribute(<span class="hljs-string">"stroke"</span>, <span class="hljs-string">"black"</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">document</span>.exitFullscreen();
    enterFullscreen.setAttribute(<span class="hljs-string">"stroke"</span>, <span class="hljs-string">"black"</span>);
    exitFullscreen.setAttribute(<span class="hljs-string">"stroke"</span>, <span class="hljs-string">"transparent"</span>);
  }
}
</code></pre>
<p>Now if you click this button, it toggles the fullscreen mode and changes its own appearance.</p>
<h2 id="heading-learn-more">Learn More</h2>
<p>If you want to learn more about SVGs, check out <a target="_blank" href="http://SVG-Tutorial.com">SVG-Tutorial.com</a> where you can find a lot of examples from the basics to more advanced levels. It’s a free site and you can also find the example that we discussed in this article.</p>
<p>To use the button to run a JavaScript game in full screen, check out the whole JavaScript Game Tutorial on how to remake the classic Gorillas game here on <a target="_blank" href="https://www.freecodecamp.org/news/how-to-draw-a-gorilla-with-javascript-on-html-canvas/">freeCodeCamp</a> or on <a target="_blank" href="https://www.youtube.com/watch?v=2q5EufbUEQk&amp;t=2337s">YouTube</a>. It’s a massive tutorial that covers things from drawing on an HTML Canvas element, to the entire game logic, from event handling, through the animation loop, hit detection, and even AI logic, for the enemy gorilla.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/2q5EufbUEQk" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p>You can subscribe to my channel for more JavaScript game development tutorials:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/undefined" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Draw with JavaScript on an HTML Canvas Element – Gorilla Example ]]>
                </title>
                <description>
                    <![CDATA[ Drawing from code can be fun for many reasons. You can generate art that follows a certain logic. You can create animations by moving only parts of an image. And you can even build up a whole game as I covered in this tutorial. In my last article, we... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-draw-a-gorilla-with-javascript-on-html-canvas/</link>
                <guid isPermaLink="false">66c4c80829f446a67a4197ec</guid>
                
                    <category>
                        <![CDATA[ canvas ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HTML ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Hunor Márton Borbély ]]>
                </dc:creator>
                <pubDate>Thu, 15 Feb 2024 16:30:12 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/Thumbnail.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Drawing from code can be fun for many reasons. You can generate art that follows a certain logic. You can create animations by moving only parts of an image. And you can even build up a whole game as I covered in <a target="_blank" href="https://www.freecodecamp.org/news/gorillas-game-in-javascript/">this tutorial</a>.</p>
<p>In my last article, we focused on the <a target="_blank" href="https://www.freecodecamp.org/news/drawing-on-a-canvas-element-with-javascript/">basics of drawing</a>. Now, let's see a concrete example and explore how to use JavaScript to draw a Gorilla. </p>
<p>You don't need to have any prerequisites for this tutorial. Even if you missed the basics, you can start right away. We are only going to have some simple HTML and a plain JavaScript file that you can run directly in the browser.</p>
<p>By the end, you'll know how to draw a gorilla with JS.‌</p>
<h2 id="heading-table-of-contents">Table of Contents:</h2>
<ol>
<li><a class="post-section-overview" href="#heading-how-to-define-a-canvas">How to Define a Canvas</a></li>
<li><a class="post-section-overview" href="#heading-how-to-turn-the-coordinate-system-upside-down">How to Turn the Coordinate System Upside Down</a></li>
<li><a class="post-section-overview" href="#heading-how-to-draw-the-legs-of-the-gorilla">How to Draw the Body of the Gorilla</a></li>
<li><a class="post-section-overview" href="#heading-how-to-draw-the-arms-of-the-gorilla">How to Draw the Legs of the gorilla</a></li>
<li><a class="post-section-overview" href="#heading-how-to-draw-the-face-of-the-gorilla">How to Draw the Arms of the Gorilla</a></li>
<li><a class="post-section-overview" href="#heading-how-to-draw-the-face-of-the-gorilla">How to Draw the Face of the Gorilla</a></li>
<li><a class="post-section-overview" href="#heading-next-steps">Next Steps</a></li>
</ol>
<h2 id="heading-how-to-define-a-canvas">How to Define a Canvas</h2>
<p>To draw our gorilla, first let's define a simple HTML file with a Canvas element. Then we'll see how to access it from JavaScript. </p>
<p>In the HTML file, in the header, we'll add our JavaScript file. Note that I’m using the <code>defer</code> keyword to make sure the script only executes once the rest of the document is parsed.</p>
<p>In the body, we'll add a <code>canvas</code> element. We set its size to 500 x 500 and set an ID.</p>
<pre><code class="lang-html"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Gorilla<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"index.js"</span> <span class="hljs-attr">defer</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">canvas</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"gorilla"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"500"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"500"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">canvas</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Then, let's create a JavaScript file. In this file, we'll first get the canvas element by ID. Then, we'll get the rendering context of the canvas element. This is a built-in API with many methods and properties that we can use to draw on the canvas.</p>
<pre><code class="lang-js"><span class="hljs-comment">// The canvas element and its drawing context </span>
<span class="hljs-keyword">const</span> canvas = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"gorilla"</span>); 
<span class="hljs-keyword">const</span> ctx = canvas.getContext(<span class="hljs-string">"2d"</span>);

. . .
</code></pre>
<p>Before we get to drawing, first let's make our life easier by turning the coordinate system upside down.</p>
<h2 id="heading-how-to-turn-the-coordinate-system-upside-down">How to Turn the Coordinate System Upside Down</h2>
<p>When we use canvas, we have a coordinate system with the origin at the top-left corner of the canvas that grows to the right and downwards. This is aligned with how websites work in general. Things go from left to right and top to bottom. </p>
<p>This is the default, but we can change it.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/08/gorilla-with-and-without-transforming-and-scaling.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>The gorilla with and without transforming and scaling the coordinate system</em></p>
<p>In our case, it is more convenient to go from the bottom to the top. Then the gorilla can stand at the bottom, and we don’t have to figure out where the bottom of the canvas is.</p>
<p>We can use the <code>translate</code> method to shift the entire coordinate system to the bottom-middle of the canvas. We'll move the coordinate system down along the Y-axis by the size of the canvas, and to the right along the X-axis by half the size of the canvas.</p>
<p>Once we do this, the Y-coordinate is still growing downwards. We can flip it using the <code>scale</code> method. Setting a negative number for the vertical direction will flip the entire coordinate system upside down.</p>
<pre><code class="lang-js"><span class="hljs-comment">// The canvas element and its drawing context </span>
<span class="hljs-keyword">const</span> canvas = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"gorilla"</span>); 
<span class="hljs-keyword">const</span> ctx = canvas.getContext(<span class="hljs-string">"2d"</span>);

ctx.translate(<span class="hljs-number">250</span>, <span class="hljs-number">500</span>);
ctx.scale(<span class="hljs-number">1</span>, <span class="hljs-number">-1</span>);

. . .
</code></pre>
<p>‌We have to do this before we paint anything on the canvas because the <code>translate</code> and <code>scale</code> methods do not actually move anything that's already on the canvas. But anything we paint after these method calls will be painted according to this new coordinate system.</p>
<h2 id="heading-how-to-draw-the-gorilla">How to Draw the Gorilla</h2>
<p>Now let's look into drawing the gorilla. We'll break this down into multiple steps. First, we'll draw the body, then the legs, the arms, and finally the face of the gorilla.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/08/drawing-gorilla-steps.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>We'll draw the gorilla in multiple steps</em></p>
<h3 id="heading-how-to-draw-the-body-of-the-gorilla">How to draw the body of the gorilla</h3>
<p>We'll draw the body of the gorilla as a path. Paths start with the <code>beginPath</code> method and end with either calling the <code>fill</code> or the <code>stroke</code> method – or both. </p>
<p>In between, we'll build the path by calling path-building methods. In this case, to build up the body of the gorilla we'll use the <code>moveTo</code> and a bunch of <code>lineTo</code> methods.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-12-at-23.53.57.png" alt="Image" width="600" height="400" loading="lazy">
<em>The body of the gorilla. The image shows the path we're filling.</em></p>
<p>We'll set the fill style to black, and then we'll begin a path. Move to a starting position and then draw straight lines to draw the silhouette of the gorilla. Once we're finished, we'll fill the shape with the <code>fill</code> method.</p>
<pre><code class="lang-js">. . .

ctx.fillStyle = <span class="hljs-string">"black"</span>;

<span class="hljs-comment">// Draw the Body of the Gorilla</span>
ctx.beginPath();
ctx.moveTo(<span class="hljs-number">-68</span>, <span class="hljs-number">72</span>);
ctx.lineTo(<span class="hljs-number">-80</span>, <span class="hljs-number">176</span>);

ctx.lineTo(<span class="hljs-number">-44</span>, <span class="hljs-number">308</span>);
ctx.lineTo(<span class="hljs-number">0</span>, <span class="hljs-number">336</span>);
ctx.lineTo(+<span class="hljs-number">44</span>, <span class="hljs-number">308</span>);

ctx.lineTo(+<span class="hljs-number">80</span>, <span class="hljs-number">176</span>);
ctx.lineTo(+<span class="hljs-number">68</span>, <span class="hljs-number">72</span>);
ctx.fill();

. . .
</code></pre>
<p>In case you are wondering how I came up with these coordinates, I actually started with an initial sketch with pen and paper. I tried to estimate the coordinates, tried them with code, and then adjusted them until they started getting the right shape. Of course, you might have other methods as well.</p>
<h3 id="heading-how-to-draw-the-legs-of-the-gorilla">How to draw the legs of the gorilla</h3>
<p>We'll draw the legs the same way. We could even continue the same path we used before, but it might be easier to see what's happening if we break this down into separate paths.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-12-at-23.09.59.png" alt="Image" width="600" height="400" loading="lazy">
<em>Drawing the legs as two separate paths</em></p>
<pre><code class="lang-js">. . .

<span class="hljs-comment">// Draw the Left Leg</span>
ctx.beginPath();
ctx.moveTo(<span class="hljs-number">0</span>, <span class="hljs-number">72</span>);
ctx.lineTo(<span class="hljs-number">-28</span>, <span class="hljs-number">0</span>);
ctx.lineTo(<span class="hljs-number">-80</span>, <span class="hljs-number">0</span>);
ctx.lineTo(<span class="hljs-number">-68</span>, <span class="hljs-number">72</span>);
ctx.fill();

<span class="hljs-comment">// Draw the Right Leg</span>
ctx.beginPath();
ctx.moveTo(<span class="hljs-number">0</span>, <span class="hljs-number">72</span>);
ctx.lineTo(+<span class="hljs-number">28</span>, <span class="hljs-number">0</span>);
ctx.lineTo(+<span class="hljs-number">80</span>, <span class="hljs-number">0</span>);
ctx.lineTo(+<span class="hljs-number">68</span>, <span class="hljs-number">72</span>);
ctx.fill();

. . .
</code></pre>
<p>The fill color in this case is also going to be black. Why? Because that's what we set the <code>fillStyle</code> property to, the last time we set its value. Every path that follows this statement will use this color until we change its value.</p>
<h3 id="heading-how-to-draw-the-arms-of-the-gorilla">How to draw the arms of the gorilla</h3>
<p>While the body and the legs are relatively simple parts of the gorilla, the arms are a bit more complicated. We'll draw them as a curve.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-12-at-23.12.03.png" alt="Image" width="600" height="400" loading="lazy">
<em>Drawing the arms as a curve</em></p>
<p>Let’s start with the left arm. The main part of this is actually only two lines of code. We'll use the <code>moveTo</code> method to move to the shoulder of the gorilla, then from there, we'll draw the arm as a quadratic curve with the <code>quadraticCurveTo</code> method.</p>
<p>A quadratic curve is a simple curve with one control point. As the curve goes from the starting point (which we'll set with <code>moveTo</code>), the curve bends towards this control point (set as the first two arguments of the <code>quadraticCurveTo</code> method) as it reaches its end position (set as the last two arguments).</p>
<pre><code class="lang-js">. . .

ctx.strokeStyle = <span class="hljs-string">"black"</span>;
ctx.lineWidth = <span class="hljs-number">70</span>;

<span class="hljs-comment">// Draw the Left Arm</span>
ctx.beginPath();
ctx.moveTo(<span class="hljs-number">-56</span>, <span class="hljs-number">200</span>);
ctx.quadraticCurveTo(<span class="hljs-number">-176</span>, <span class="hljs-number">180</span>, <span class="hljs-number">-112</span>, <span class="hljs-number">48</span>);
ctx.stroke();

<span class="hljs-comment">// Draw the Right Arm</span>
ctx.beginPath();
ctx.moveTo(+<span class="hljs-number">56</span>, <span class="hljs-number">200</span>);
ctx.quadraticCurveTo(+<span class="hljs-number">176</span>, <span class="hljs-number">180</span>, +<span class="hljs-number">112</span>, <span class="hljs-number">48</span>);
ctx.stroke();

. . .
</code></pre>
<p>We'll draw the hands as strokes. Instead of ending the path with the <code>fill</code> method, we'll use the <code>stroke</code> method.</p>
<p>We'll also set up the styling differently. Instead of using the <code>fillStyle</code> property, here we'll set the color with <code>strokeStyle</code> and give thickness to the arms with the <code>lineWidth</code> property.</p>
<p>Drawing the right arm is the same, except the horizontal coordinates are flipped. The negative numbers have a positive sign now.</p>
<p>As a result, our gorillas should start to gain shape. They still don’t have a face, but we have the whole silhouette now.</p>
<h3 id="heading-how-to-draw-the-face-of-the-gorilla">How to draw the face of the gorilla</h3>
<p>The face of the gorilla comes together from multiple different parts. First, we'll draw the facial mask with three circles.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/08/gorilla-facial-mask.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>We draw the facial mask as three circles</em></p>
<p>Unfortunately, we don’t have a simple fill circle method, as we have in the case of rectangles. We have to draw an <code>arc</code> instead.</p>
<p>An <code>arc</code> method can be called as part of a path. We'll start each circle with the <code>beginPath</code> method and end with the <code>fill</code> method.</p>
<pre><code class="lang-js">. . .

ctx.fillStyle = <span class="hljs-string">"lightgray"</span>;

<span class="hljs-comment">// Draw the Facial Mask</span>
ctx.beginPath();
ctx.arc(<span class="hljs-number">0</span>, <span class="hljs-number">252</span>, <span class="hljs-number">36</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span> * <span class="hljs-built_in">Math</span>.PI);
ctx.fill();

ctx.beginPath();
ctx.arc(<span class="hljs-number">-14</span>, <span class="hljs-number">280</span>, <span class="hljs-number">16</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span> * <span class="hljs-built_in">Math</span>.PI);
ctx.fill();

ctx.beginPath();
ctx.arc(+<span class="hljs-number">14</span>, <span class="hljs-number">280</span>, <span class="hljs-number">16</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span> * <span class="hljs-built_in">Math</span>.PI);
ctx.fill();

. . .
</code></pre>
<p>The <code>arc</code> method has a lot of properties. This might look a bit scary, but we only need to focus on the first 3 when drawing circles:</p>
<ul>
<li>The first two arguments are <code>x</code> and <code>y</code>, the center coordinates of the arc.</li>
<li>The third argument is the <code>radius</code>.</li>
<li>Then the last two arguments are the <code>startAngle</code> and the <code>endAngle</code> of the arc in radians. Because here we want to have a full circle and not an arc, we'll start with 0 and end at a full circle. A full circle in radians is two times Pi.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-12-at-23.17.08.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>If these last two properties are confusing, don't worry about it. What's important is that when we draw circles, they are always <code>0</code> and <code>2 * Math.Pi</code>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/08/gorilla-final-steps.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>We draw the eyes, the nostrils, and the mouth</em></p>
<p>Then we draw the eyes of the gorilla as two other circles. Here the center coordinates of the circles are the same as the bigger gray circles around them. Only their radius and their fill color are different.</p>
<pre><code class="lang-js">. . .

ctx.fillStyle = <span class="hljs-string">"black"</span>;

<span class="hljs-comment">// Draw the Left Eye</span>
ctx.beginPath();
ctx.arc(<span class="hljs-number">-14</span>, <span class="hljs-number">280</span>, <span class="hljs-number">6</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span> * <span class="hljs-built_in">Math</span>.PI);
ctx.fill();

<span class="hljs-comment">// Draw the Right Eye</span>
ctx.beginPath();
ctx.arc(+<span class="hljs-number">14</span>, <span class="hljs-number">280</span>, <span class="hljs-number">6</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span> * <span class="hljs-built_in">Math</span>.PI);
ctx.fill();

. . .
</code></pre>
<p>Then for the nose, we'll draw two short lines as nostrils. They are part of the same path, and we'll call the <code>moveTo</code> method in between to get from one side to the other. Before calling stroke at the end of this path, we'll update the <code>lineWidth</code> property. </p>
<pre><code class="lang-js">. . .

ctx.lineWidth = <span class="hljs-number">6</span>;

<span class="hljs-comment">// Draw the Nostrils</span>
ctx.beginPath();
ctx.moveTo(<span class="hljs-number">-14</span>, <span class="hljs-number">266</span>);
ctx.lineTo(<span class="hljs-number">-6</span>, <span class="hljs-number">260</span>);

ctx.moveTo(<span class="hljs-number">14</span>, <span class="hljs-number">266</span>);
ctx.lineTo(+<span class="hljs-number">6</span>, <span class="hljs-number">260</span>);
ctx.stroke();

. . .
</code></pre>
<p>And finally, we'll also add a path for the mouth. This could be part of the same path as the nose, because it has the same line width and color, but might be clearer to have them separate.</p>
<pre><code class="lang-js">. . .

<span class="hljs-comment">// Draw the Mouth</span>
ctx.beginPath();
ctx.moveTo(<span class="hljs-number">-20</span>, <span class="hljs-number">230</span>);
ctx.quadraticCurveTo(<span class="hljs-number">0</span>, <span class="hljs-number">245</span>, <span class="hljs-number">20</span>, <span class="hljs-number">230</span>);
ctx.stroke();
</code></pre>
<p>Let's draw another quadratic curve. This is similar to the one we used for the arms. The start and endpoint of this curve are on the same level, but the control point is a bit higher so the middle of the mouth is higher than the two sides.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>Now that we have a basic gorilla, what can we do with it? We can build a whole game around it. In this <a target="_blank" href="https://www.freecodecamp.org/news/gorillas-game-in-javascript/">JavaScript Game Tutorial</a> we rebuild the 1991 classic game, Gorillas.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-01-19-at-23.49.34.png" alt="Image" width="600" height="400" loading="lazy">
<em>Screenshot from the JavaScript Game Tutorial</em></p>
<p>For a deep dive, read the <a target="_blank" href="https://www.freecodecamp.org/news/gorillas-game-in-javascript/">full tutorial</a> where we build up a complete game with plain JavaScript. In this tutorial, we not only cover how to draw the gorillas and the city skyline but also implement the whole game logic. From event handling, through the animation loop, to hit detection.</p>
<p>For even more, you can also watch the <a target="_blank" href="https://www.youtube.com/watch?v=2q5EufbUEQk">extended tutorial on YouTube</a>. In the YouTube version, we also cover how to make the buildings destructible, how to animate the hand of the gorilla to follow the drag movement while aiming, have nicer graphics, and we add AI logic, so you can play against the computer.</p>
<p>Check it out to learn more:</p>
<p><a target="_blank" href="https://www.youtube.com/embed/2q5EufbUEQk?feature=oembed">Embedded content</a></p>
<p>You can subscribe to my channel for more JavaScript game development tutorials:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/undefined" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to lock an angle when drawing on canvas in JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ By Thang Minh Vu In many drawing tools (Adobe Photoshop, Sketch, and so on), if we hold the SHIFT button when drawing a line, we can create perfectly straight lines horizontally or vertically. Recently, I tried implementing this feature in canvas by ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-lock-an-angle-when-drawing-on-canvas-in-javascript-51938b5abc7c/</link>
                <guid isPermaLink="false">66c35328d58e4fdd567d51ae</guid>
                
                    <category>
                        <![CDATA[ canvas ]]>
                    </category>
                
                    <category>
                        <![CDATA[ geometry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 18 Mar 2019 16:16:31 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*cWcey5rf6AkuNtkVZ7ywwg.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Thang Minh Vu</p>
<p>In many drawing tools (<a target="_blank" href="https://www.adobe.com/products/photoshop.html">Adobe Photoshop</a>, <a target="_blank" href="https://www.sketchapp.com/">Sketch</a>, and so on), if we hold the SHIFT button when drawing a line, we can create perfectly straight lines horizontally or vertically.</p>
<p>Recently, I tried implementing this feature in canvas by JavaScript. The process is really interesting. I would like to share the progress of how I approach it.</p>
<p><strong>Demo</strong>: To easier understand the idea, you can check a demo version at the <a target="_blank" href="https://ittus.github.io/draw-lock-angle/">demo page</a>.</p>
<h3 id="heading-requirements">Requirements</h3>
<p><strong>Input</strong></p>
<ul>
<li>A base point (B)</li>
<li>Current mouse position (M)</li>
</ul>
<p><strong>Output</strong></p>
<ul>
<li>Projection of current mouse position on x-axis or y-axis (P)</li>
</ul>
<p>For convenience, in all graphs, we will mark the base point by a red circle and the current mouse point by a green circle.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/zvt4t9MmiO6Uxc3zAyLDxUOta3S-j4Nl7JvE" alt="Image" width="488" height="458" loading="lazy">
<em>Problem: Decide which projection is better</em></p>
<h3 id="heading-simple-solution">Simple solution</h3>
<p>As I tackle the problem, it’s intuitive to see we can calculate the distance between the current mouse position with the horizontal line and vertical line. If the mouse position is nearer the horizontal line than the vertical line, we will take the projection on the horizontal line, and vice versa.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/F-mQyWvLknInihDnYTgeS2CYyiHljKRB-P1R" alt="Image" width="632" height="495" loading="lazy"></p>
<p>The calculation is quite simple — here is the Javascript code:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/AmQiLZ6chh1YF30QhI6MXs0Qrpwq1SkXAdti" alt="Image" width="800" height="475" loading="lazy"></p>
<h3 id="heading-extended-problem">Extended Problem</h3>
<p>How about if we want to project on the bisector line between the horizontal line and vertical line (similar with <a target="_blank" href="https://www.sketchapp.com/">Sketch</a>)? That means users can project the mouse position on the horizontal line, vertical line, 45-degree angle line, or 135-degree angle line.</p>
<p>The approach is similar. This time we need to calculate the distance between the mouse position to 4 lines: horizontal line, vertical line, and 2 bisector lines (45-degree line and 135-degree line). But the calculation is more complex.</p>
<p>We still can divide it into 2 steps:</p>
<ol>
<li>Determine which line is nearest with mouse position</li>
<li>Calculate the projection of mouse position on the nearest line</li>
</ol>
<p><img src="https://cdn-media-1.freecodecamp.org/images/yRRlpXpZg16PFuGjohkEwMBRTQ074yN4UmvH" alt="Image" width="489" height="460" loading="lazy"></p>
<h4 id="heading-step-1-determine-which-line-is-nearest-with-mouse-position">Step 1: Determine which line is nearest with mouse position</h4>
<p>First, we need to determine line formulation of 4 lines above. Because we already know the base point (x0, y0) and the line angle, it’s easy to figure out the formulation of each line.</p>
<blockquote>
<p>Example: To calculate the formula of the 45-degree bisector, we already know that the line will go through the base point (x0, y0) and (x0 + 1, y0 + 1). Using the <a target="_blank" href="https://www.wikihow.com/Find-the-Equation-of-a-Line">Find-the-Equation-of-a-Line</a> method, we can figure out the line formula.</p>
</blockquote>
<p>Finally, we will have 4 lines’ formulas:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/ngaxjDK6zWtgP74BOXPwMU2jZY014c7L-lu6" alt="Image" width="187" height="82" loading="lazy"></p>
<p>To calculate the distance between the base mouse position to each line, we can use a popular math formula:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/C4Tk7kqRbwGbPtYFwK-HDSRlmVnGTRunxAso" alt="Image" width="359" height="27" loading="lazy">
<em>Distance from a point to a line</em></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/Adk5BQAv15Jy4IuGUBSyKiSxkU68gEgsdh7z" alt="Image" width="800" height="561" loading="lazy">
<em>Finding the nearest line</em></p>
<h4 id="heading-step-2-calculate-the-orthogonal-projection-of-mouse-position-on-the-nearest-line">Step 2: Calculate the orthogonal projection of mouse position on the nearest line</h4>
<p>Now the problem becomes calculating the orthogonal projection of the mouse position (M) to the nearest line with the formula: ax + by + c = 0 (L)</p>
<p>There are multiple ways to solve this problem. I took a simple way: First, calculate the formula of the line which contains mouse position M and perpendicular to line L, called L'. Then, solve the system of equations to get the intersection point between line L and L', which is the projection point which we are finding.</p>
<p>After some calculation, I figured out the formula of L’, which goes through M (x0, y0) and perpendicular to L (ax + by + c = 0):</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/BwlHT2VPl2baMMHQtQYI2vGkCUq0jl0ZpiLz" alt="Image" width="198" height="17" loading="lazy"></p>
<p>Now to find the intersection, we need to solve the system of equations:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/4nJ1rFj7FrXEwZ0KK3MLEGo10DMJDUrchUxB" alt="Image" width="211" height="54" loading="lazy"></p>
<p>Using <a target="_blank" href="https://en.wikipedia.org/wiki/Cramer%27s_rule">Cramer’s rule</a> and matrix determinant, we can solve this equation easily:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/YeHWX9l70xghLPvDkqTZA19RSbRGyRIZq4WJ" alt="Image" width="800" height="526" loading="lazy">
<em>Solve simultaneous equations</em></p>
<h3 id="heading-boundary">Boundary</h3>
<p>There is a situation when we want to limit the boundary of projection.</p>
<p>Example:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/3WNRAcorUIwF9lwihmXCjAna8DovCrhgmUxi" alt="Image" width="379" height="404" loading="lazy">
<em>The projection point is outside of the boundary</em></p>
<p>In this case, we want to limit the projection in the white rectangle area, but using the discussed method, the projection point can be outside of the boundary area.</p>
<p>In this situation, we can simply get the intersection point of line L’ to the boundary (called P’).</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/Zl5D-QVbgmra2XCyD6khtLSFE1sF9VzPz49c" alt="Image" width="800" height="1049" loading="lazy">
<em>Support boundary of the projection</em></p>
<h3 id="heading-full-source-code"><strong>Full source code</strong></h3>
<p>You can check out the demo and source code on <a target="_blank" href="https://github.com/ittus/draw-lock-angle">Github</a>.</p>
<p>Happy Coding!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
