bandarra.me 2025-12-06T18:53:50Z https://bandarrame.me André Cipriani Bandarra andreban@gmail.com Smarter Filters: Empowering Users with AI-Driven Search 2025-08-15T13:00:00Z https://bandarra.me/posts/ai-smart-filters <p><img src="/images/SmartFilters.png" alt="" /> Over a decade ago, I worked on a travel meta-search website. We discovered through UX research that only the most savvy users could effectively utilize the myriad filter options available for flight results. Acknowledging this, we dedicated significant time to optimizing the user experience in that area, but never found a solution that really made filters significantly easier to use.</p> <p>Now, imagine if instead of having to figure out how the filters work and select the ones that reflect what they are looking for, users could simply express what they want using their own language, maybe even using their voice as the input.</p> <p>It turns out that, with AI's significant development in the last few years, this is possible today, and there are already sites out there implementing this pattern, like Kayak's Smart Filter feature, or Redbus's Eazzy filter.</p> <div style="display: flex; flex-direction: row; gap: 4px; font-size: 16px; justify-content: space-between;"> <div style="display: flex; flex-direction: column;align-items: center;max-width:50%"> <a href="/images/KayakSmartFilters.png"> <img style="margin-bottom: 0; height: 250px;" src="/images/KayakSmartFilters.png"/></a> <div>Kayak's Smart Filter</div> </div> <div style="display: flex; flex-direction: column;align-items: center;max-width:50%"> <a href="/images/RedbusEazzyFilter.png"> <img style="margin-bottom: 0; height: 250px;" src="/images/RedbusEazzyFilter.png"/></a> <div>Redbus' Eazzy Filter</div> </div> </div> <p>And even better, with the help of the <a href="https://developer.chrome.com/docs/ai/built-in-apis">Built-in AI APIs</a> the entire process can run on the user's device, with zero cost, without the user's voice or text prompt ever leaving the user's device, and even works offline! Here's a demo application implementing a smart filtering experience that runs on the client-side, and with voice input:</p> <p><iframe width="800" height="450" style="width:100%;" src="https://www.youtube.com/embed/Vldmo2DFoqc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p> <p>You can also try out the <a href="https://bandarra.me/apps/flyby/">live demo</a>.</p> <blockquote class="markdown-alert-important"> <p>To try out the demo above, you will need to enable the Prompt API for Gemini Nano by pointing tour browser to <code>chrome://flags/#prompt-api-for-gemini-nano</code> and setting the flag to <code>Enabled</code>. To enable the audio input you will also need to set the <code>chrome://flags/#prompt-api-for-gemini-nano-multimodal-input</code> flag to <code>Enabled</code>.</p> </blockquote> <p>But how does this work!?</p> <p>The core functionality of transforming the user's input in natural language into filter settings uses generative AI in the format of a Large Language Model (LLM), with a feature called structured output, which helps the model generate output in an specific format. The audio input feature also uses a multimodal LLM to transcribe the user's voice into text that is then passed to the core functionality for processing. Let's take a deeper look into how those work.</p> <h2>Transforming natural language input into structured filter configuration</h2> <p>As described in the previous section, the solution utilizes an LLM to build the the core functionality, which transforms the user's query in natural language into a structured filter configuration use.</p> <p>More specifically, this implementation uses the <a href="https://developer.chrome.com/docs/ai/prompt-api">Built-in Prompt API</a>, which runs on top of a browser provided state of the art LLM, Gemini Nano in Chrome's case. Using an LLM via the Prompt API has the advantage that the model is managed by the browser and, because once it's downloaded the first time it's available to any sites, it may be immediately available on the user machine, avoiding hefty downloads.</p> <blockquote class="markdown-alert-note"> <p>While the Built-in Prompt API is generally available on Chrome Extensions, it's currently only available a Chrome Origin Trial for the web on MacOS, Windows and Linux, and Chrome is currently the only browser that provides the API. This means that features that lean on this API should either be <em>Progressive Enhancement</em> or use a hybrid solution, like the <a href="https://developer.chrome.com/docs/ai/firebase-ai-logic">Firebase AI Logic</a>.</p> </blockquote> <p>It's possible to break the process to transform the user's input into a filter configuration in three components:</p> <ul> <li>A <strong>structured output schema</strong> which describes the format the model should use to output information, as well as constraints to the output and field descriptions.</li> <li>A <strong>system prompt</strong> describing what the model's goal is, a set of rules and examples to help the model understand how to process the information and, finally, any additional information it may need.</li> <li>Handling the <strong>user's query</strong>, that is, taking the user input and prompting the large language model to extract the information and return it in the required format.</li> </ul> <h3>Definining the structured output</h3> <p>The structured output is the glue between the output of the LLM and the filters in the application. The application includes a method where a set of filters can be applied by passing an object with the filter definitions to a method:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">interface FilterState { </span><span style="color:#f8f8f2;"> minPrice: number; </span><span style="color:#f8f8f2;"> maxPrice: number; </span><span style="color:#f8f8f2;"> departureAirports: string[]; </span><span style="color:#f8f8f2;"> arrivalAirports: string[]; </span><span style="color:#f8f8f2;"> stops: number[]; </span><span style="color:#f8f8f2;"> airlines: string[]; </span><span style="color:#f8f8f2;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">const handleSmartFilterChange = (newFilters: FilterState) =&gt; { </span><span style="color:#f8f8f2;"> // Filter results providing the state and update the UI. </span><span style="color:#f8f8f2;">} </span></pre> <p>The <a href="https://json-schema.org/">JSON Schema</a> is used to describe the target output for the LLM. In this example, it should describe the <code>FilterState</code> object above, which is the input to <code>handleSmartFilterChange()</code>. This example from <a href="https://developer.chrome.com/docs/ai/structured-output-for-prompt-api">the Chrome documentation for using Structured Ouput with the Prompt API</a> explains the steps needed:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">session </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await LanguageModel</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">create</span><span style="color:#f8f8f2;">(); </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">schema </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;type&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&quot;boolean&quot; </span><span style="color:#f8f8f2;">}; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">post </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;Mugs and ramen bowls, both a bit smaller than intended- but that&#39;s</span><span style="background-color:#ff79c6;color:#f8f8f0;"> </span><span style="color:#ffffff;">how it goes </span><span style="color:#ff79c6;">with </span><span style="color:#ffffff;">reclaim</span><span style="color:#ff79c6;">. </span><span style="color:#ffffff;">Glaze crawled the first time around</span><span style="color:#f8f8f2;">, </span><span style="color:#ffffff;">but pretty happy </span><span style="color:#ff79c6;">with </span><span style="color:#ffffff;">it after refiring</span><span style="color:#ff79c6;">.</span><span style="color:#f1fa8c;">&quot;;</span><span style="background-color:#ff79c6;color:#f8f8f0;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">result </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await session</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">prompt</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">`Is this post about pottery?</span><span style="color:#ff79c6;">\n\n</span><span style="color:#f8f8f2;">${</span><span style="color:#ffffff;">post</span><span style="color:#f8f8f2;">}</span><span style="color:#f1fa8c;">`</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> { </span><span style="color:#f8f8f2;"> responseConstraint: </span><span style="color:#ffffff;">schema</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;">); </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">JSON</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">parse</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">result</span><span style="color:#f8f8f2;">)); </span></pre> <blockquote class="markdown-alert-tip"> <p>The <code>responseConstraint</code> is defined with a JSON Schema, which is a quite powerful format. Make sure to check the documentation for it at <a href="https://json-schema.org/">jsonschema.org</a>.</p> </blockquote> <p>The schema for the filter object is much larger than this example, and you can read <a href="https://github.com/andreban/flyby-results-explorer/blob/main/src/lib/ai.ts#L171-L229">the whole definition on the project's repository</a>. The following are some tips and best practices identified while writing the schema for this demo application:</p> <ul> <li><strong>Required fields:</strong> in the initial version, the choice was to hide fields when no information about them was available in the user's query, so there were no required fields. Through experimentation, it was identified that the results from the LLM were most consistent when requiring those fields, with default values provided for when they didn't exist in the user's query. For numeric fields, a value of <code>-1</code> was used as the default value, and the code for handling filters was adapted to handle that.</li> <li><strong>Field descriptions:</strong> field descriptions help the LLM understand the context of each field and add the correct information into them. It's generally better to provide this information in the schema itself, rather then in the prompt engineering.</li> <li><strong>Regex fields:</strong> the airline are 2 letter strings and airport fields are 3 letter strings. Without providing the pattern description, the LLM would eventually return the airline or airport full names, rather than the 2 or 3 letter codes.</li> </ul> <h3>Engineering the system prompt</h3> <p>If the structured output defines what the model output should look like, the system prompt defines how the model should interpret the user's query. This is the step where most of time was spent optimizing, to maximize the cases where the model would interpret the results correctly.</p> <p>In the Prompt API, the system prompt can be passed to the model when creating a new instance of <code>LanguageModel</code>:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">systemPrompt </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&#39;...&#39;</span><span style="color:#f8f8f2;">; </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">session </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await LanguageModel</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">create</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> initialPrompts: [{ </span><span style="color:#f8f8f2;"> role: </span><span style="color:#f1fa8c;">&quot;system&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> content: </span><span style="color:#ffffff;">systemPrompt</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> }] </span><span style="color:#f8f8f2;">}); </span></pre> <blockquote class="markdown-alert-tip"> <p>The system prompt can also be passed as a parameter when calling <code>prompt()</code> or <code>promptStreaming()</code>. However, using it in the session makes it easier and results in more performance when prompting with the same system prompt.</p> </blockquote> <p>Given prompt engineering is an area where a lot of the time building the application is spent, it's worth using a tool to track progress and regression across different changes and iterations. For this application, <a href="https://bandarra.me/apps/structured-output-eval/">a tool to test user queries against prompts and model configuration</a> was created, specifically for the Prompt API and structured output. You can see the output of the the in the screenshot below:</p> <p><img src="/images/structured-output-eval.png" alt="A screenshot of the structured output evaluation tool" /></p> <p>The following notes and recommendations were derived from the prompt engineering work on this application:</p> <ol> <li>The initial implementation focused on providing a set of rules for the model to follow in the system prompt. But when adding example inputs and outputs (multi-shot prompting), the accuracy of the output increased significantly, going from <code>~56%</code> to <code>~90%</code>. At that point, it was possible to completely remove the rules and focus on examples, without a penalty in the model's accuracy, resulting in a short and simple base prompt:</li> </ol> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">You are a helpful assistant that generates structured data for flight search filters. </span></pre> <p>While the base prompt was short, the system prompt includes a total of 12 examples that help the LLM understand different user queries and expected results. Note that the examples output include all the required fields, which helps the LLM understand when the default values should be used.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">&lt;example&gt; </span><span style="color:#f8f8f2;">&lt;query&gt; </span><span style="color:#f8f8f2;">Flights under $800 </span><span style="color:#f8f8f2;">&lt;/query&gt; </span><span style="color:#f8f8f2;">&lt;output&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> &quot;minPrice&quot;: -1, </span><span style="color:#f8f8f2;"> &quot;maxPrice&quot;: 500, </span><span style="color:#f8f8f2;"> &quot;nonstop&quot;: false, </span><span style="color:#f8f8f2;"> &quot;onestop&quot;: false, </span><span style="color:#f8f8f2;"> &quot;twostop&quot;: false, </span><span style="color:#f8f8f2;"> &quot;departureAirports&quot;: [], </span><span style="color:#f8f8f2;"> &quot;arrivalAirports&quot;: [], </span><span style="color:#f8f8f2;"> &quot;airlines&quot;: [] </span><span style="color:#f8f8f2;">} </span><span style="color:#f8f8f2;">&lt;/output&gt; </span><span style="color:#f8f8f2;">&lt;/example&gt; </span></pre> <ol start="2"> <li>The model would eventually return the wrong code for airlines. Including a list of available airlines and codes into the system made it much more accurate to return airline codes. The following is how the list of airlines were included into the system prompt.</li> </ol> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">This is a list of airlines and codes available to filter: </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">[ </span><span style="color:#f8f8f2;"> { code: &quot;UA&quot;, name: &quot;United Airlines&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;DL&quot;, name: &quot;Delta Air Lines&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;AA&quot;, name: &quot;American Airlines&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;WN&quot;, name: &quot;Southwest Airlines&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;B6&quot;, name: &quot;JetBlue Airways&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;NK&quot;, name: &quot;Spirit Airlines&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;AS&quot;, name: &quot;Alaska Airlines&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;F9&quot;, name: &quot;Frontier Airlines&quot; }, </span><span style="color:#f8f8f2;"> { code: &quot;QF&quot;, name: &quot;Quantas Airlines&quot; }, </span><span style="color:#f8f8f2;">] </span></pre> <p>While the same solution could be used for airport codes and names, the model's has been accurate in extracting airport codes from user queries, so the same approach wasn't necessary.</p> <h3>Transform user queries into filter configuration</h3> <p>With all the pieces in place, it now becomes possible to add the code that glues the system prompt, schema and the user query to transform the user's input into a filter configuration. It's important to note that the configuration includes a value of <code>0.5</code> for the <strong>temperature</strong> and <code>1</code> for the <strong>top-K</strong>, which significantly reduces the randomness of the model and causes it to return more consistent results.</p> <blockquote class="markdown-alert-tip"> <p>Read <a href="/posts/understand-temperature-topk">Understand the Effects of Temperature on Large Language Model Output</a> for considerations on the impact of changing temperature and top-K values on an LLM output.</p> </blockquote> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">systemPrompt </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;...&quot;</span><span style="color:#f8f8f2;">; </span><span style="color:#6272a4;">// content elided for brevity. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">schema </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">{...}; </span><span style="color:#6272a4;">// content elided for brevity. </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Create the model session. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">session </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await LanguageModel</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">create</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> temperature: </span><span style="color:#bd93f9;">0.5</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> topK: </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> initialPrompts: [{ </span><span style="color:#f8f8f2;"> role: </span><span style="color:#f1fa8c;">&quot;system&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> content: </span><span style="color:#ffffff;">systemPrompt</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> }] </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Execute the user&#39;s on the session, passing the structured output schema as a parameter. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">result </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await session</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">prompt</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">query</span><span style="color:#f8f8f2;">, { </span><span style="color:#f8f8f2;"> responseConstraint: </span><span style="color:#ffffff;">schema</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">filterState </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">JSON</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">parse</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">result</span><span style="color:#f8f8f2;">); </span></pre> <p>With the AI generated result ready, all that is left to apply the AI generated filter configuration to the application is invoking the code that handles filtering:</p> <pre style="background-color:#282a36;"> <span style="color:#50fa7b;">handleSmartFilterChange</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">filterState</span><span style="color:#f8f8f2;">) </span></pre> <h2>Handling voice input with multimodal</h2> <p>Multimodal models are capable of handing image, audio and sometimes video in their input or output. One potential use-case for those models is to transcribe the user's voice input into text, that can then be plugged in into other parts of the application.</p> <p>The implementation with the Prompt API is similar to before, with the key differences being that the API is told to expect audio inputs, so it can ensure the right models that support this modality are created, and that the audio blob is handed over to the model as part of the prompt call.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">async function </span><span style="color:#50fa7b;">getTranscriptionFromAudio</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">audioBlob</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">session </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await LanguageModel</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">create</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> expectedInputs: [{ type: </span><span style="color:#f1fa8c;">&#39;audio&#39; </span><span style="color:#f8f8f2;">}], </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">result </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await session</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">prompt</span><span style="color:#f8f8f2;">([{ </span><span style="color:#f8f8f2;"> role: </span><span style="color:#f1fa8c;">&#39;user&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> content: [ </span><span style="color:#f8f8f2;"> { type: </span><span style="color:#f1fa8c;">&#39;text&#39;</span><span style="color:#f8f8f2;">, value: </span><span style="color:#f1fa8c;">&#39;Transcribe this audio&#39; </span><span style="color:#f8f8f2;">}, </span><span style="color:#f8f8f2;"> { type: </span><span style="color:#f1fa8c;">&#39;audio&#39;</span><span style="color:#f8f8f2;">, value: </span><span style="color:#ffffff;">audioBlob </span><span style="color:#f8f8f2;">}, </span><span style="color:#f8f8f2;"> ] </span><span style="color:#f8f8f2;"> }]); </span><span style="color:#f8f8f2;">} </span></pre> <blockquote class="markdown-alert-important"> <p>The Multimodal functionality for the Prompt API is behind a different flag. To try out the filter, make sure to enable the flags mentioned previously in this article and additionally set <code>chrome://flags/#prompt-api-for-gemini-nano-multimodal-input</code> to <code>Enabled</code>.</p> </blockquote> <p>Finally, recording the user's voice input can be implemented via the MediaRecorder:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#f8f8f2;">stream </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await </span><span style="font-style:italic;color:#66d9ef;">navigator</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">mediaDevices</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getUserMedia</span><span style="color:#f8f8f2;">({ audio: </span><span style="color:#bd93f9;">true </span><span style="color:#f8f8f2;">}); </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">mediaRecorder </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">MediaRecorder(stream); </span><span style="color:#ffffff;">mediaRecorderRef</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">current </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">mediaRecorder</span><span style="color:#f8f8f2;">; </span><span style="color:#ffffff;">audioChunksRef</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">current </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">mediaRecorder</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">ondataavailable </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">event</span><span style="color:#f8f8f2;">) </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">audioChunksRef</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">current</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">event</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data); </span><span style="color:#f8f8f2;">}; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">mediaRecorder</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">onstop </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">async </span><span style="color:#f8f8f2;">() </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">audioBlob </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">Blob(</span><span style="color:#ffffff;">audioChunksRef</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">current, { type: </span><span style="color:#f1fa8c;">&quot;audio/webm&quot; </span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">try </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">transcription </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await </span><span style="color:#50fa7b;">getTranscriptionFromAudio</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">audioBlob</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">setQuery</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">transcription</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#50fa7b;">handleFilter</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">transcription</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> } </span><span style="color:#ff79c6;">catch </span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">error</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">error</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;Error getting transcription:&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#ffffff;">error</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;">}; </span><span style="color:#ffffff;">mediaRecorder</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">start</span><span style="color:#f8f8f2;">(); </span></pre> <h2>Conclusion</h2> <p>This article demonstrated how a developer can use generative AI to build a better filtering experience, using the Built-in Prompt.</p> <p>Result filters have been an advanced user feature for a long time, and not only for the travel vertical, where it seems to be getting more traction, but across e-commerces, auction sites, or any other user interface where users need to apply filters to get to the result they want.</p> <p>Generative AI can streamline this user journey, allowing user to explain the results they want in their own language, instead of having to figure out how to use the controls provided by the site, allowing them to get to reach their goals when visiting your site faster.</p> Discover how AI is revolutionizing search filters! Learn how to transform natural language queries into structured filters using Chrome's Built-in Prompt API and Gemini Nano. Explore techniques like structured output schemas and prompt engineering to create intuitive, voice-enabled filtering experiences that run offline, enhancing user experience on travel and e-commerce sites. Try the live demo! Building with Lovable: A Low-Code Experiment 2025-07-14T10:00:00Z https://bandarra.me/posts/building-with-lovable <p><img src="https://bandarra.me/images/LovableLove.png" alt="A vibe code writing code" /> My buddy <a href="https://www.linkedin.com/in/thiagocarneiro/">Thiago Carneiro</a> showed me some projects he's been building with <a href="https://lovable.dev/">Lovable</a>, like this <a href="https://texturewiz.com/">Texture Wizard</a> or this <a href="https://ytpreview.thiagocarneiro.com/">YouTube Preview tool</a>. But the really really cool thing about this is what Thiago told me - his background is as Designer and Game Artist and, while he does understand well what goes behind the scenes, he says he wouldn't have the know-how to build those applications himself, and those tools unlocked the possibility for him to build a number of tools that make his life easier - and share with others. It's amazing to see how those tools are unlocking the potential for more people to build.</p> <p>As often is the case, there are limitations to those tools. Integrating with a 3rd party API might be hard, or even implementing a sign-in system or payments, and that might require someone - maybe the vibe coder or another skilled developer, to dive deeper into the code the AI generated</p> <p>Now, a common concern from developers around low-code tools like Lovable is the code quality, and how easy it is to maintain the code, since it can be an AI generated code base with little to no human supervision. Thiago was kind enough to share the source code of his tools with me, and checking out the code for his various applications was interesting.</p> <p>What I found were applications that are quite consistent across each other on the tech stack, which uses <a href="https://vite.dev/">Vite</a>, <a href="https://react.dev/">React</a> and <a href="https://tailwindcss.com/">Tailwind</a>, with a directory structure and patterns that are consistent across applications, and programming patterns that are componentized, clean, and easy to read. In short, getting up to speed with those projects wasn't only easy, but because they are similar, moving o the next one got easier.</p> <p>Over this week, I made an experiment - I bootstrapped a demo application with Lovable (stay tuned for more), then moved the application o VS Code and continued developing it (with the help of Cline). Because Lovable has an integration with <a href="https://github.com/">GitHub</a>, saving the project and running it locally was straightforward, and building up the application from that point, given how well organized the project was was easy.</p> <p>I'm excited by how tools like <a href="https://lovable.dev/">Lovable</a>, <a href="https://bolt.new/">Bolt</a>, and others are enabling non-coders to build the tools and applications they need on the web, creating a new wave of excitement on the platform. After looking at the code generated by Lovable, I'm also confident that there's an "upgrade path" for when the limits of those tools are reached, and growing beyond that requires hands-on coding.</p> Discover how Lovable, a low-code tool, empowers designers like Thiago Carneiro to build impressive applications like Texture Wizard and YouTube Preview tool, despite limited coding experience. This review explores the code quality of Lovable-generated projects, revealing a consistent tech stack (Vite, React, Tailwind) and clean, componentized code. See how easily you can extend Lovable projects with tools like VS Code and GitHub, as demonstrated by the Flyby demo, which experiments with natural language filtering using Chrome's Built-in Prompt API. Try the Flyby demo and see for yourself. AI-Generated Code: Ownership and Developer Responsibility 2025-07-07T16:37:00Z https://bandarra.me/posts/ai-generated-code-ownership <p>A few weeks ago I was discussing recommendations for organizations adopting AI developer tooling with a friend and, one of the points we agreed with, is that developers should treat code generated by AI as their own, and thoroughly review it themselves before submitting for review by their wider team.</p> <p>Later in that week, I learned about the <a href="https://arxiv.org/abs/2506.08872">Your Brain on ChatGPT: Accumulation of Cognitive Debt when Using an AI Assistant for Essay Writing Task</a> study, which triggered loads of discussions online. One of the takeaways from the study was the potential impact on the feeling of ownership of the work produced:</p> <blockquote> <p>This trade-off highlights an important educational concern: AI tools, while valuable for supporting performance, may unintentionally hinder deep cognitive processing, retention, and authentic engagement with written material. If users rely heavily on AI tools, they may achieve superficial fluency but fail to internalize the knowledge <strong>or feel a sense of ownership over it</strong>.</p> </blockquote> <p>This made me question the idea that developers should have ownership of the AI generated code. While this is, indeed, the best scenario, it may just not be in line with human nature.</p> <p>But if developers are unable to feel ownership of the code generated by AI, what do they feel ownership of? <a href="https://x.com/karpathy">Andrej Karpathy</a> hinted on what that is <a href="https://x.com/karpathy/status/1617979122625712128">in one of his tweets</a>:</p> <blockquote> <p>“The hottest new programming language is English”</p> </blockquote> <p>What developers can feel ownership of are the prompts given to an AI to generate code, but does this make AI a programming language? Not really, and at least now how I’d like it to be.</p> <p>Maybe, for prompts to be considered a programming language, they should work like a higher level language. That is, similar to how a compiler transforms C++ code into machine code, I’d expect AI to transform the prompts into C++ code (or any other language).</p> <p>But this fails in a couple ways. The same set of prompts would need to generate the same output. If that was the case, developers would be able to commit the sequence of prompts, which they feel ownership of, to their GitHub repository and reproduce the entire application from them. But that’s not how AI works, the same set of prompts can produce widely different results.</p> <p>Additionally, looking at the compiler analogy, when the C++ code is correct and the compiler produces the incorrect output, this is not a developer issue, but a compiler issue. A compiler that doesn’t always produce the correct, or even the same output is considered to be broken. But, with AI, the solution is to go back and tweak the prompt until it works.</p> <p>Another question is that, if code becomes English, it's likely that submitting the full specification rather than the sequence of prompts that led to that specification is easier for the AI to reproduce, and for humans to maintain (even if maintenance is assisted by AI).</p> <p>Maybe, over time, coding AI systems will get better at correctness and reproducibility, and developers will be able to build their work on top of what they will feel ownership of, the prompts.</p> <p>In the meantime, while developers may not feel the same level of ownership over AI generated code, learning how to effectively review it is becoming an important part of the developer’s skillset.</p> Explore the evolving role of AI in software development and the critical question of code ownership. Is AI-generated code truly "owned" by developers, or is the focus shifting to prompt engineering? This article delves into the challenges of reproducibility, the need for rigorous review, and how AI's impact on coding skills is reshaping the developer landscape. From PyTorch to Browser: a full client-side solution with ONNX and Transformers.js 2025-05-06T13:16:00Z https://bandarra.me/posts/from-pytorch-to-browser-a-full-client-side-solution-with-onnx-and-transformers-js <p>In the <a href="https://bandarra.me/posts/from-pytorch-to-browser-creating-a-web-friendly-ai-model">previous article</a>, I wrote about using an <a href="https://huggingface.co/tasks/feature-extraction">feature extraction model</a> to generate embeddings from text, then train a custom classification model for sentiment analysis, using the embeddings as the input for the model, and finally <a href="https://ai.google.dev/edge/litert/models/pytorch_to_tflite">exported the model to run in the browser with LiteRT</a> and <a href="https://www.npmjs.com/package/@tensorflow/tfjs-tflite">Tensorflow Lite</a>.</p> <p>While the classification model in the previous solution runs on the client side, the solution uses <a href="https://ai.google.dev/gemini-api/docs/embeddings">Google AI's embedding API</a> to generate text embeds - a Cloud API, so the solution is not entirely client-side.</p> <p>In this article, I'll explore a full client-side solution for toxicity detection using <a href="https://www.kaggle.com/competitions/jigsaw-toxic-comment-classification-challenge/">Kaggle's Toxic Comment Classification Challenge</a> dataset and <a href="https://huggingface.co/docs/transformers.js/en/api/pipelines#module_pipelines.FeatureExtractionPipeline">Transformers.js's feature extraction pipeline</a>, and running it in the browser with the <a href="https://www.npmjs.com/package/onnxruntime-web">ONNX web runtime</a>.</p> <h2>Choose the tools and libraries</h2> <p>PyTorch was the ML framework used in the previous article, and there's no reason to choose a different approach.</p> <p>Since the goal is to enable a full client-side solution, the embedding model needs to run on both the training pipeline and in the browser, for inference. The <code>all-MiniLM-L6-v2</code> model is <a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2">supported in Python with Sentence Transformers</a> and <a href="https://huggingface.co/Xenova/all-MiniLM-L6-v2">in the browser with Transformers.js</a>, making it a great choice.</p> <p><a href="https://huggingface.co/docs/transformers.js/en/index">Transformers.js</a> is a great library for running off-the-shelf AI models in the browser. Because Transformers.js uses <a href="https://onnxruntime.ai/">ONNX Runtime</a> as the underlying AI library to run models, it makes <a href="https://www.npmjs.com/package/onnxruntime-web">ONNX Runtime web</a> a great to choice to run the custom model in the browser, creating synergy between Transformers.js and the custom model, and avoiding increasing the number of dependencies on the web application.</p> <h2>Data pre processing</h2> <p>The data pre processing step consists of transforming the original dataset containing text comments and labels into a new dataset containing the embeddings generated by the feature extraction model and labels:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">from </span><span style="color:#f8f8f2;">sentence_transformers </span><span style="color:#ff79c6;">import </span><span style="color:#f8f8f2;">SentenceTransformer </span><span style="color:#f8f8f2;">model </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">SentenceTransformer</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;sentence-transformers/all-MiniLM-L6-v2&#39;</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">with </span><span style="color:#8be9fd;">open</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;data/train.csv&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;r&quot;</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">encoding</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">&quot;utf-8&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">as </span><span style="color:#f8f8f2;">dataset_file: </span><span style="color:#f8f8f2;"> dataset_csv </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">csv</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">DictReader</span><span style="color:#f8f8f2;">(dataset_file) </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">with </span><span style="color:#8be9fd;">open</span><span style="color:#f8f8f2;">(output_file, </span><span style="color:#f1fa8c;">&quot;a&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">as </span><span style="color:#f8f8f2;">output_file: </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">entry </span><span style="color:#ff79c6;">in </span><span style="color:#f8f8f2;">dataset_csv: </span><span style="color:#f8f8f2;"> embeddings </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">encode</span><span style="color:#f8f8f2;">([entry[</span><span style="color:#f1fa8c;">&#39;comment_text&#39;</span><span style="color:#f8f8f2;">]]) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> result </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;id&#39;</span><span style="color:#f8f8f2;">: entry[</span><span style="color:#f1fa8c;">&#39;id&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;embeddings&#39;</span><span style="color:#f8f8f2;">: embeddings[</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">]</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">tolist</span><span style="color:#f8f8f2;">(), </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;toxic&#39;</span><span style="color:#f8f8f2;">: entry[</span><span style="color:#f1fa8c;">&#39;toxic&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;severe_toxic&#39;</span><span style="color:#f8f8f2;">: entry[</span><span style="color:#f1fa8c;">&#39;severe_toxic&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;obscene&#39;</span><span style="color:#f8f8f2;">: entry[</span><span style="color:#f1fa8c;">&#39;obscene&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;threat&#39;</span><span style="color:#f8f8f2;">: entry[</span><span style="color:#f1fa8c;">&#39;threat&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;insult&#39;</span><span style="color:#f8f8f2;">: entry[</span><span style="color:#f1fa8c;">&#39;insult&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;identity_hate&#39;</span><span style="color:#f8f8f2;">: entry[</span><span style="color:#f1fa8c;">&#39;identity_hate&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> json_result </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">json</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">dumps</span><span style="color:#f8f8f2;">(result) </span><span style="color:#f8f8f2;"> output_file</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">write</span><span style="color:#f8f8f2;">(json_result </span><span style="color:#ff79c6;">+ </span><span style="color:#f1fa8c;">&quot;</span><span style="color:#ff79c6;">\n</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> output_file</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">flush</span><span style="color:#f8f8f2;">() </span></pre> <h2>Model architecture and training</h2> <p>The model architecture is similar to the one used on the previous article. In this case, the <code>all-MiniLM-L6-v2</code> feature extraction model generates embeddings as an array of 384 float values, so the model needs to be changed to reflect that.</p> <p>A normalization layer was also introduced in each layer of the model, as that has demonstrated to slightly improve the performance on the validation set, as well as reducing the number of epochs required for the model to converge.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#ff79c6;">class </span><span style="text-decoration:underline;color:#8be9fd;">ToxicityModel</span><span style="color:#f8f8f2;">(</span><span style="text-decoration:underline;font-style:italic;color:#8be9fd;">nn</span><span style="text-decoration:underline;font-style:italic;color:#ff79c6;">.</span><span style="text-decoration:underline;font-style:italic;color:#8be9fd;">Module</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">def </span><span style="color:#8be9fd;">__init__</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="color:#8be9fd;">super</span><span style="color:#f8f8f2;">()</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">__init__</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">linear0 </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Linear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">384</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">128</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">norm0 </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">BatchNorm1d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">128</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">linear1 </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Linear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">128</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">32</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">norm1 </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">BatchNorm1d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">32</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">linear_out </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Linear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">32</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">6</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">def </span><span style="color:#50fa7b;">forward</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">x</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">linear0</span><span style="color:#f8f8f2;">(x) </span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">norm0</span><span style="color:#f8f8f2;">(x) </span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">F</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">relu</span><span style="color:#f8f8f2;">(x) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">linear1</span><span style="color:#f8f8f2;">(x) </span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">norm1</span><span style="color:#f8f8f2;">(x) </span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">F</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">relu</span><span style="color:#f8f8f2;">(x) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">linear_out</span><span style="color:#f8f8f2;">(x) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return </span><span style="color:#f8f8f2;">x </span></pre> <p>Binary Cross Entropy (BCE) is used as the loss function, through <a href="https://docs.pytorch.org/docs/stable/generated/torch.nn.BCEWithLogitsLoss.html">BCEWithLogitsLoss</a>, which also combines the output with a <a href="https://en.wikipedia.org/wiki/Sigmoid_function">sigmoid function</a>, which allows comparing the results from the model with the labels from the training set.</p> <p>An accuracy of 98% is achieved with this model on the validation set.</p> <h2>Model conversion from PyTorch to ONNX</h2> <p><a href="https://pytorch.org/tutorials/beginner/onnx/export_simple_model_to_onnx_tutorial.html">Converting from the PyTorch format to ONNX</a> requires the installation of the <code>onnx</code> and <code>onnxscript</code> dependencies, with a straightforward implementation.</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">import </span><span style="color:#f8f8f2;">torch </span><span style="color:#ff79c6;">from </span><span style="color:#f8f8f2;">all_minilm_l6</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">toxicity_model </span><span style="color:#ff79c6;">import </span><span style="color:#f8f8f2;">ToxicityModel </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">torch_model </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">ToxicityModel</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;">torch_model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">load_state_dict</span><span style="color:#f8f8f2;">(torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">load</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;SP-all-MiniLM-L6-v2.safetensors&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">weights_only</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">True</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">map_location</span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">device</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;cpu&#39;</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;">)) </span><span style="color:#f8f8f2;">torch_model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">eval</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">example_inputs </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">(torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">randn</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">384</span><span style="color:#f8f8f2;">),) </span><span style="color:#f8f8f2;">onnx_program </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">onnx</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">export</span><span style="color:#f8f8f2;">(torch_model, example_inputs, </span><span style="font-style:italic;color:#ffb86c;">dynamo</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">True</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;">onnx_program</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">optimize</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">onnx_program</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">save</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;SP-all-MiniLM-L6-v2.onnx&quot;</span><span style="color:#f8f8f2;">) </span></pre> <p>Similar to the LiteRT conversion, the model requires passing an example input when being converted, that can be randomly generated.</p> <h2>Running the ONNX model in the browser</h2> <p>Running ONNX models in the browser is achieved with the <a href="https://www.npmjs.com/package/onnxruntime-web"><code>onnxruntime-web</code> library</a>.</p> <p>Because the model takes embeddings as input, generated with the <code>all-MiniLM-L6-v2</code>, <a href="https://huggingface.co/docs/transformers.js/en/index">Transformers.js</a> is required for the pre processing step, to transform the user input into embeddings.</p> <p>Finally, the model output logits, which can be transformed into probabilities with a <a href="https://en.wikipedia.org/wiki/Sigmoid_function">sigmoid function</a>. ONNX Runtime doesn't provide the function out of the box, but the implementation is a one line function:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">function </span><span style="color:#50fa7b;">sigmoid</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">xs</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return </span><span style="color:#ffffff;">xs</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">map</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">x</span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#bd93f9;">1 </span><span style="color:#ff79c6;">/ </span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1 </span><span style="color:#ff79c6;">+ </span><span style="font-style:italic;color:#66d9ef;">Math</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">exp</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">-</span><span style="color:#ffffff;">x</span><span style="color:#f8f8f2;">))) </span><span style="color:#f8f8f2;">} </span></pre> <p>Finally putting everything together becomes a matter of importing the required libraries, transforming the user's input into embeddings, calling the custom model with those embeddings, and then applying the sigmoid function to the model results, generating a probability for each toxicity type:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">import </span><span style="color:#f8f8f2;">{ </span><span style="color:#ffffff;">pipeline </span><span style="color:#f8f8f2;">} </span><span style="color:#ff79c6;">from </span><span style="color:#f1fa8c;">&#39;@huggingface/transformers&#39;</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="color:#bd93f9;">* </span><span style="color:#ff79c6;">as </span><span style="color:#ffffff;">ort </span><span style="color:#ff79c6;">from </span><span style="color:#f1fa8c;">&#39;onnxruntime-web&#39;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Create the Transformers.js pipeline using the all-MiniLM-L6-v2 feature </span><span style="color:#6272a4;">// extractionmodel. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">extractor </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await </span><span style="color:#50fa7b;">pipeline</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;feature-extraction&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;Xenova/all-MiniLM-L6-v2&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Instantiate the custom ONNX runtime model. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">model </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await ort</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">InferenceSession</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">create</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;SP-all-MiniLM-L6-v2.onnx&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">sentences </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[</span><span style="color:#f1fa8c;">&quot;This is an example input&quot;</span><span style="color:#f8f8f2;">]; </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Generate embeddings from the user input. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">output </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await </span><span style="color:#50fa7b;">extractor</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">sentences</span><span style="color:#f8f8f2;">, { pooling: </span><span style="color:#f1fa8c;">&#39;mean&#39;</span><span style="color:#f8f8f2;">, normalize: </span><span style="color:#bd93f9;">true </span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Classify the embeddings. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">outputTensor </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">run</span><span style="color:#f8f8f2;">({x: </span><span style="color:#ffffff;">output</span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Transform the embeddings into probabilities using the sigmoid function. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">probabilities </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">sigmoid</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">outputTensor</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">linear_2</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data); </span></pre> <h2>Conclusion</h2> <p>The combination of a feature extraction model with a classification pattern looks like a promising pattern. With the model hitting 98% accuracy, and the size of both models together under 5mb, and inference that is almost instant, this looks like a good use case to run on the client side.</p> <p>As a next step, make sure to <a href="https://bandarra.me/apps/toxicity-detection-onnx/">check the model in action</a> or <a href="https://github.com/andreban/jigsaw-toxic-comment-classification-challenge">take a look at source code for the model and web application</a></p> Build a full client-side toxicity detection solution using Transformers.js, ONNX Runtime, and the `all-MiniLM-L6-v2` model. Train a custom model on the Kaggle Toxic Comment Classification Challenge dataset, convert it to ONNX, and run it in the browser for fast, private text analysis. Get 98% accuracy with this approach, see the code, and try the live demo. From PyTorch to Browser: Creating a Web-Friendly AI Model 2025-04-16T12:23:00Z https://bandarra.me/posts/from-pytorch-to-browser-creating-a-web-friendly-ai-model <h1>Motivation</h1> <p>There's an immense availability of AI models for a wide range of use cases, readily available to web developers via tools like <a href="https://ai.google.dev/edge/mediapipe/solutions/guide">Mediapipe</a> and <a href="https://huggingface.co/docs/transformers.js/en/index">Transformers.js</a>, and more recently via the <a href="https://huggingface.co/docs/transformers.js/en/index">Built-in AI APIs</a>, with friendly APIs. Those tools provide models for a wide range of common tasks, ranging from text texts like text classification or language detection, vision tasks like image segmentation, and even large language models.</p> <p>However, developers will sometimes have specific needs that are not covered by readily available models, or those models might not be available in a web friendly format.</p> <p>In this article, I explore building a model from scratch using <a href="https://pytorch.org/">PyTorch</a>, and exporting it to a browser friendly format that is compatible with Google's LiteRT library.</p> <p><em>Disclaimer: I'm not a Python developer or an AI engineer. This is just an exercise to understand the process of building and deploying a model, end to end.</em></p> <h1>Picking a problem and outlining a solution</h1> <p>Ideally, such an experiment should happen on a problem that is at least adjacent to the real world. The inspiration for this one comes from a colleague trying to understand sentiment of messages from a mailing list - whether the messages were positive, negative or neutral. This is also not too far away from another problem I heard from a developer, where they had their own specific, somewhat more lenient rules, for toxicity detection.</p> <p>The <a href="https://ai.google.dev/gemini-api/docs/embeddings">Google AI Embeddings API</a> has an option that optimizes embeddings for classification, and this looked like a good opportunity to experiment with building a classification model on top of the embeddings generated by that API.</p> <p>The last thing needed to get started is a dataset, as finding good datasets is crucial for building good models. Fortunately, dataset hubs like <a href="https://www.kaggle.com/datasets">Kaggle</a> or <a href="https://huggingface.co/datasets">HuggingFace</a> for various datasets we can use and, for this particular problem, I chose this <a href="https://www.kaggle.com/datasets/atifaliak/youtube-comments-dataset">Kaggle dataset for sentiment analysis on YouTube comments</a>. It contains <code>17872</code> comments, each one classified as <code>positive</code>, <code>negative</code>, or <code>neutral</code>.</p> <h1>Preparing the dataset</h1> <p>With the dataset selected and downloaded, the next step is transforming it into a format that can be used by our model. Most important, in this case, is transforming the comments from the dataset into the embeddings we are going to use as the the input for the model.</p> <p>As this is a time consuming process, the solution is to pre-process the data and save the embeddings into a separate file, using the <a href="https://ai.google.dev/gemini-api/docs/libraries">Google GenAI Python</a> library:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">client </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">genai</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Client</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">api_key</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">&quot;YOUR API KEY&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">with </span><span style="color:#8be9fd;">open</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;YoutubeCommentsDataSet.csv&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;r&quot;</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">encoding</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">&quot;utf-8&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">as </span><span style="color:#f8f8f2;">csvfile: </span><span style="color:#f8f8f2;"> csvreader </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">csv</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">DictReader</span><span style="color:#f8f8f2;">(csvfile) </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">with </span><span style="color:#8be9fd;">open</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;YouTubeCommentsEmbeddings.jsonl&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;a&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">as </span><span style="color:#f8f8f2;">embeddingsfile: </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">row </span><span style="color:#ff79c6;">in </span><span style="color:#f8f8f2;">csvreader: </span><span style="color:#f8f8f2;"> result </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">client</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">models</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">embed_content</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">model</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">&#39;text-embedding-004&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">contents</span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;">row[</span><span style="color:#f1fa8c;">&#39;Comment&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">config</span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;">types</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">EmbedContentConfig</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">task_type</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">&quot;CLASSIFICATION&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> ) </span><span style="color:#f8f8f2;"> result </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;embeddings&quot;</span><span style="color:#f8f8f2;">: result</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">embeddings[</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">]</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">values, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;sentiment&quot;</span><span style="color:#f8f8f2;">: row[</span><span style="color:#f1fa8c;">&#39;Sentiment&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> json_result </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">json</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">dumps</span><span style="color:#f8f8f2;">(result) </span><span style="color:#f8f8f2;"> embeddingsfile</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">write</span><span style="color:#f8f8f2;">(json_result </span><span style="color:#ff79c6;">+ </span><span style="color:#f1fa8c;">&quot;</span><span style="color:#ff79c6;">\n</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> embeddingsfile</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">flush</span><span style="color:#f8f8f2;">() </span></pre> <p>The code above loads all comments from <code>YoutubeCommentsDataset.csv</code>, transforms the comments into embeddings using the Google AI SDK, and saves the embeddings and the sentiment into a new file, <code>YouTubeCommentsEmbeddings.jsonl</code>.</p> <h1>Training the model</h1> <h2>Loading the previously generated data.</h2> <p>Before training the model, the file created in the previous set must be loaded. While doing that, the <code>positive</code>, <code>neutral</code> and <code>negative</code> sentiment values are also mapped to <code>0</code>, <code>1</code>, and <code>2</code>.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">sentiments_dict </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">{</span><span style="color:#f1fa8c;">&#39;positive&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;neutral&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;negative&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">2</span><span style="color:#f8f8f2;">} </span><span style="color:#f8f8f2;">dataset </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[] </span><span style="color:#ff79c6;">with </span><span style="color:#8be9fd;">open</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;YouTubeCommentsEmbeddings.jsonl&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;r&#39;</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">as </span><span style="color:#f8f8f2;">f: </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">line </span><span style="color:#ff79c6;">in </span><span style="color:#f8f8f2;">f: </span><span style="color:#f8f8f2;"> data </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">json</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">loads</span><span style="color:#f8f8f2;">(line) </span><span style="color:#f8f8f2;"> embeddings </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">tensor</span><span style="color:#f8f8f2;">(data[</span><span style="color:#f1fa8c;">&#39;embeddings&#39;</span><span style="color:#f8f8f2;">]) </span><span style="color:#f8f8f2;"> sentiment </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">tensor</span><span style="color:#f8f8f2;">(sentiments_dict[data[</span><span style="color:#f1fa8c;">&#39;sentiment&#39;</span><span style="color:#f8f8f2;">]]) </span><span style="color:#f8f8f2;"> dataset</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">append</span><span style="color:#f8f8f2;">((embeddings, sentiment)) </span></pre> <h2>Splitting into training and validation datasets</h2> <p>With the initial dataset loaded, a good practice is to split the dataset into a <em>training dataset</em> and a <em>validation dataset</em>. While the first is used to train the model, the second is used to check the model accuracy, and that it's not memorizing the training dataset instead, which would lead to poor performance in the real world.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">num_samples </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">len</span><span style="color:#f8f8f2;">(dataset) </span><span style="color:#f8f8f2;">num_validation </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">int</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0.2 </span><span style="color:#ff79c6;">* </span><span style="color:#f8f8f2;">num_samples) </span><span style="color:#f8f8f2;">shuffled_indices </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">randperm</span><span style="color:#f8f8f2;">(num_samples) </span><span style="color:#f8f8f2;">train_indices </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">shuffled_indices[:</span><span style="color:#ff79c6;">-</span><span style="color:#f8f8f2;">num_validation] </span><span style="color:#f8f8f2;">validation_indices </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">shuffled_indices[</span><span style="color:#ff79c6;">-</span><span style="color:#f8f8f2;">num_validation:] </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">training_dataset </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[dataset[i] </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">i </span><span style="color:#ff79c6;">in </span><span style="color:#f8f8f2;">train_indices] </span><span style="color:#f8f8f2;">validation_dataset </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[dataset[i] </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">i </span><span style="color:#ff79c6;">in </span><span style="color:#f8f8f2;">validation_indices] </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">train_loader </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">utils</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">DataLoader</span><span style="color:#f8f8f2;">(training_dataset, </span><span style="font-style:italic;color:#ffb86c;">batch_size</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">64</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">shuffle</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">True</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;">validation_loader </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">utils</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">DataLoader</span><span style="color:#f8f8f2;">(validation_dataset, </span><span style="font-style:italic;color:#ffb86c;">batch_size</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">64</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">shuffle</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">False</span><span style="color:#f8f8f2;">) </span></pre> <p>Dataset loaders are also created, with a batch size of <code>64</code>. The training loader has the <code>shuffle</code> parameter set to <code>True</code>, ensuring that, each training loop, the order of the inputs is different.</p> <h2>The training loop</h2> <p>With the training and validation sets ready, it's now time to train the model. The training loop is fairly standard for training neural networks: the output is calculated by invoking the model with <code>model(x_train)</code>, the loss is calculated from the predicted values and the expected results with <code>loss_fn(y_predicted, y_train)</code>. After any remaining gradients are cleared with <code>optimizer.zero_grad()</code>, new gradients are generated with <code>loss.backwards()</code>, and then model weights are updated with <code>optimizer.step()</code>.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">def </span><span style="color:#50fa7b;">training_loop</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">n_epochs</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">model</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">optimizer</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">loss_fn</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">training_loader</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">validation_loader</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">epoch </span><span style="color:#ff79c6;">in </span><span style="color:#8be9fd;">range</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, n_epochs </span><span style="color:#ff79c6;">+ </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">x_train, y_train </span><span style="color:#ff79c6;">in </span><span style="color:#f8f8f2;">training_loader: </span><span style="color:#f8f8f2;"> y_predicted </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">model</span><span style="color:#f8f8f2;">(x_train) </span><span style="color:#f8f8f2;"> loss </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">loss_fn</span><span style="color:#f8f8f2;">(y_predicted, y_train) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> optimizer</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">zero_grad</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> loss</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">backward</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> optimizer</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">step</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;"># Disable grad for calculating validation metrics, since backpropagation </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;"># is not needed and this should improve performance. </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">with </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">no_grad</span><span style="color:#f8f8f2;">(): </span><span style="color:#f8f8f2;"> correct </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0 </span><span style="color:#f8f8f2;"> total </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0 </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">x_val, y_val </span><span style="color:#ff79c6;">in </span><span style="color:#f8f8f2;">validation_loader: </span><span style="color:#f8f8f2;"> outputs </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">model</span><span style="color:#f8f8f2;">(x_val) </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">_</span><span style="color:#f8f8f2;">, predicted </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">max</span><span style="color:#f8f8f2;">(outputs, </span><span style="font-style:italic;color:#ffb86c;">dim</span><span style="color:#ff79c6;">=-</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> correct </span><span style="color:#ff79c6;">+= </span><span style="font-style:italic;color:#66d9ef;">int</span><span style="color:#f8f8f2;">((predicted </span><span style="color:#ff79c6;">== </span><span style="color:#f8f8f2;">y_val)</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">sum</span><span style="color:#f8f8f2;">()) </span><span style="color:#f8f8f2;"> total </span><span style="color:#ff79c6;">+= </span><span style="color:#f8f8f2;">x_val</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">shape[</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">] </span><span style="color:#f8f8f2;"> </span><span style="color:#8be9fd;">print</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;Epoch: </span><span style="color:#bd93f9;">%d</span><span style="color:#f1fa8c;">, Loss: </span><span style="color:#bd93f9;">%f</span><span style="color:#f1fa8c;">, Accuracy: </span><span style="color:#bd93f9;">%f</span><span style="color:#f1fa8c;">&#39; </span><span style="color:#ff79c6;">% </span><span style="color:#f8f8f2;">(epoch, </span><span style="font-style:italic;color:#66d9ef;">float</span><span style="color:#f8f8f2;">(loss), correct </span><span style="color:#ff79c6;">/ </span><span style="color:#f8f8f2;">total)) </span></pre> <p>After each epoch, the training loop calculates and prints the accuracy using the validation dataset.</p> <h2>The model, optimizer and hyper parameters</h2> <p>The model used has an input of <code>768</code>, which is the size of the embeddings array created by the Google AI embeddings API, and an output <code>3</code>, one for each possible value. The hidden layer size is <code>512</code>. <a href="https://en.wikipedia.org/wiki/Stochastic_gradient_descent">Stochastic Gradient Descent (SGD)</a> is used as the optimizer, and <a href="https://en.wikipedia.org/wiki/Cross-entropy">Cross Entropy as the loss function</a>.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">seq_model </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Sequential</span><span style="color:#f8f8f2;">(</span><span style="color:#50fa7b;">OrderedDict</span><span style="color:#f8f8f2;">([ </span><span style="color:#f8f8f2;"> (</span><span style="color:#f1fa8c;">&#39;hidden_linear_0&#39;</span><span style="color:#f8f8f2;">, nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Linear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">768</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">512</span><span style="color:#f8f8f2;">)), </span><span style="color:#f8f8f2;"> (</span><span style="color:#f1fa8c;">&#39;hidden_activation_0&#39;</span><span style="color:#f8f8f2;">, nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">ReLU</span><span style="color:#f8f8f2;">()), </span><span style="color:#f8f8f2;"> (</span><span style="color:#f1fa8c;">&#39;output_linear&#39;</span><span style="color:#f8f8f2;">, nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Linear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">512</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">)) </span><span style="color:#f8f8f2;">])) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">optimizer </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">optim</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">SGD</span><span style="color:#f8f8f2;">(seq_model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">parameters</span><span style="color:#f8f8f2;">(), </span><span style="font-style:italic;color:#ffb86c;">lr</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">1e-2</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">training_loop</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">n_epochs </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">200</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">model </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">seq_model, </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">optimizer </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">optimizer, </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">loss_fn </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">CrossEntropyLoss</span><span style="color:#f8f8f2;">(), </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">training_loader </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">train_loader, </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#ffb86c;">validation_loader </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">validation_loader, </span><span style="color:#f8f8f2;">) </span></pre> <h2>Training results</h2> <p>The model needs less than <code>200</code> epochs (or training loops) for the accuracy on the validation set to stabilize around <code>83%</code>! While there's probably a lot of space for improvements, this seems to be on the top end of the existing notebooks for the model, shared on Kaggle, which range between <code>65%</code> and <code>85%</code>.</p> <p>Once the training is finished, the weights can be easily saved with <code>torch.save()</code>:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">save</span><span style="color:#f8f8f2;">(seq_model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">state_dict</span><span style="color:#f8f8f2;">(), </span><span style="color:#f1fa8c;">&#39;ytsentiment.safetensors&#39;</span><span style="color:#f8f8f2;">) </span></pre> <h1>Running the model on the web</h1> <p>The model is now trained and the weights saved into <code>ytsentiment.safetensors</code>. Unfortunately, this format cannot be run directly on the web. But not everything is lost - there are formats that are web friendly, and one of the is the TFLite mode, used by <a href="https://ai.google.dev/edge/litert">LiteRT</a>.</p> <p><strong>Note</strong>: <em><a href="https://developers.googleblog.com/en/tensorflow-lite-is-now-litert/">LiteRT is the new name for Tensorflow Lite</a>. At the time this is being writter, the LiteRT documentation doesn't mention web libraries. However, the <a href="https://www.npmjs.com/package/@tensorflow/tfjs-tflite">Tensorflow Lite</a> library is still available on NPM and can handle the TFLite format</em>.</p> <h2>Converting the PyTorch model to TFLite</h2> <p>The LiteRT team <a href="https://ai.google.dev/edge/litert/models/pytorch_to_tflite">provides a library that makes the work to convert PyTorch models to TFLite straightforward</a>. The process consists on creating an instance of the same model used for training and loading the previously trained weights into it, then calling <code>ai_edge_torch.convert()</code>, passing the model and a random input as parameter so the library can understand the model better. Finally, save the model to disk with <code>edge_model.export()</code>.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">model </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Sequential</span><span style="color:#f8f8f2;">(</span><span style="color:#50fa7b;">OrderedDict</span><span style="color:#f8f8f2;">([ </span><span style="color:#f8f8f2;"> (</span><span style="color:#f1fa8c;">&#39;hidden_linear_0&#39;</span><span style="color:#f8f8f2;">, nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Linear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">768</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">512</span><span style="color:#f8f8f2;">)), </span><span style="color:#f8f8f2;"> (</span><span style="color:#f1fa8c;">&#39;hidden_activation_0&#39;</span><span style="color:#f8f8f2;">, nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">ReLU</span><span style="color:#f8f8f2;">()), </span><span style="color:#f8f8f2;"> (</span><span style="color:#f1fa8c;">&#39;output_linear&#39;</span><span style="color:#f8f8f2;">, nn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Linear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">512</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">)) </span><span style="color:#f8f8f2;">])) </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">load_state_dict</span><span style="color:#f8f8f2;">(torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">load</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;ytsentiment.safetensors&quot;</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">weights_only</span><span style="color:#ff79c6;">=</span><span style="color:#bd93f9;">True</span><span style="color:#f8f8f2;">)) </span><span style="color:#f8f8f2;">sample_inputs </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">randn</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">768</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;">edge_model </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">ai_edge_torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">convert</span><span style="color:#f8f8f2;">(model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">eval</span><span style="color:#f8f8f2;">(), (torch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">randn</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">768</span><span style="color:#f8f8f2;">),)) </span><span style="color:#f8f8f2;">edge_model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">export</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;ytsentiment.tflite&#39;</span><span style="color:#f8f8f2;">) </span></pre> <h2>Running the converted model in the browser</h2> <p>The model is now compabitle with the <a href="https://www.npmjs.com/package/@tensorflow/tfjs-tflite">Tensorflow Lite library</a>, can be loaded with <code>tflite.loadTFLiteModel()</code> and inference is executed with <code>model.predict()</code>.</p> <p>We we'll use <em>Hello, your video is amazing</em> as an example input. Because the model was trained on embeddings, rather than on text, it first needs to be converted into embeddings, using the same embedding model as before, but this time via the <a href="https://www.npmjs.com/package/@google/genai">Google Gen AI JavaScript library</a>:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">genAi </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">GoogleGenAI({ apiKey: </span><span style="color:#f1fa8c;">&#39;YOUR API KEY HERE&#39; </span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">sampleInput </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;Hello, your video is amazing!&quot;</span><span style="color:#ff79c6;">. </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">result </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await genAi</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">models</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">embedContent</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> model: </span><span style="color:#f1fa8c;">&#39;text-embedding-004&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> contents: [</span><span style="color:#ffffff;">sampleInput</span><span style="color:#f8f8f2;">] </span><span style="color:#f8f8f2;">}); </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">embeddings </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">embedResult</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">embeddings</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">map</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">embedding </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#ffffff;">embedding</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">values); </span></pre> <p>The embeddings are generated with <code>models.embedContent()</code> which allows to back the generation of embeddings by providing an array of inputs to contents <code>contents</code>. The result object contains a list of embeddings results, one for each input provided as parameter. The embedding array can be accessed with <code>embedding.values</code>. The result is then mapped into a 2D array, representing the list of embeddings for each input, and then the values of the embeddings themselves.</p> <p>Since the model needs a tensor to run inference instead of a JavaScript array, <code>tf.tensor2d()</code>, is used to convert the 2D JavaScript array into a 2D tensor, which is then passed to the model when calling <code>model.predict()</code>:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">embeddingTensor </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">tf</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">tensor2D</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">embeddings</span><span style="color:#f8f8f2;">); </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">outputTensor </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">predict</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">embeddingTensor</span><span style="color:#f8f8f2;">); </span></pre> <p>The output of the prediction is another 2D tensor, containing one element for each input, then another array with the logits, are are the score given by the model for each possible class. The tensor can be converted to a regular JavaScript array by calling <code>.array()</code>:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">`</span><span style="color:#f8f8f2;">${</span><span style="color:#ffffff;">await outputTensor</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">array</span><span style="color:#f8f8f2;">()}</span><span style="color:#f1fa8c;">`</span><span style="color:#f8f8f2;">); </span><span style="color:#6272a4;">// Outputs &quot;[[4.526814937591553,0.1881929636001587,-4.69814395904541]]&quot; </span></pre> <p>As noticed in the output, the array contains only one item in the first level, matching the number of inputs passed to the model, and 3 items on the second level, which are the scores for each class - the 1st item is the score for positive, the 2nd for neutral, and the 3rd for negative, and the highest score is the most likely one, according to the model.</p> <p>A neat trick to convert the array of results into an array of classes to use the <code>tf.argMax()</code> function, which transforms the array of scores into an array with the index of the highest scores:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">labels </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[</span><span style="color:#f1fa8c;">&#39;Positive&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;Neutral&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;Negative&#39;</span><span style="color:#f8f8f2;">]; </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">argmax </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await tf</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">argMax</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">outputTensor</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">)</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">array</span><span style="color:#f8f8f2;">(); </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">results </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">argmax</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">map</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">i </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#ffffff;">labels</span><span style="color:#f8f8f2;">[</span><span style="color:#ffffff;">i</span><span style="color:#f8f8f2;">]); </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">results</span><span style="color:#f8f8f2;">); </span><span style="color:#6272a4;">// Outputs &quot; [&#39;Positive&#39;]&quot; </span></pre> <h2>Viewing results as probabilities</h2> <p>While the logits are enough to find the most likely result for the classification, developers may want to view those scores as probabilities, which is clearly not the case looking at the result numbers right now. This can be solved by feeding the model output into the the <code>tf.softmax()</code> function:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">await tf</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">softmax</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">outputTensor</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">)</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">array</span><span style="color:#f8f8f2;">()); </span><span style="color:#6272a4;">// Outputs &quot;[[0.9870176911354065, 0.012885026633739471, 0.00009726943972054869]]&quot; </span></pre> <p>The model gave a probability of <code>98.7%</code> that <em>Hello, your video is amazing</em> is a <em>Positive</em> comment. Seems to check out.</p> <h2>Putting it all together</h2> <p>This is the code for the JavaScript inference all together:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">input </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&#39;Hello, your video is amazing!&#39;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">embedResult </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await genAi</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">models</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">embedContent</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> model: </span><span style="color:#f1fa8c;">&#39;text-embedding-004&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> contents: [</span><span style="color:#ffffff;">input</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">embeddings </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">embedResult</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">embeddings</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">map</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">embedding </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#ffffff;">embedding</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">values); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">outputTensor </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await model</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">predict</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">tf</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">tensor2d</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">embeddings</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">argmax </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await tf</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">argMax</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">outputTensor</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">)</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">array</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">labels </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[</span><span style="color:#f1fa8c;">&#39;Positive&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;Neutral&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;Negative&#39;</span><span style="color:#f8f8f2;">]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">results </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">argmax</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">map</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">i </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#ffffff;">labels</span><span style="color:#f8f8f2;">[</span><span style="color:#ffffff;">i</span><span style="color:#f8f8f2;">]); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">results</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">probabilities </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await tf</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">softmax</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">outputTensor</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">)</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">array</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">probabilities</span><span style="color:#f8f8f2;">); </span></pre> <h1>Conclusion</h1> <p>Training a custom model, and doing well, requires specialized knowledge, which may not be worth for developers who want to focus on web development. At the same time, having understanding how models work and, more importantly, how to adapt them to the web can be a powerful tool for AI developers who want to get more usage of their model, or for web developers who want to take advantage of off the shelf models that are not immediately available on the web.</p> Learn how to build and deploy a custom sentiment analysis model for the web using PyTorch and Google's LiteRT! This guide walks you through the process of creating a model from scratch, training it on a YouTube comments dataset, converting it to a browser-friendly format, and running it in the browser with TensorFlow Lite and the Google Gen AI JavaScript library. Perfect for web developers looking to leverage custom AI models. Understand the Effects of Temperature on Large Language Model Output 2025-03-24T12:36:00Z https://bandarra.me/posts/understand-temperature-topk <p>When trying to understand the effect changing the temperature parameter on the output of a Large Language Model, the explanation is often that “<em>it makes the model responses more creative</em>” and, if you know that the model works by looping over predictions for the next token, that explanation can feel a bit underwhelming, so here’s a slightly longer one.</p> <p>What happens is that the model generates a score for each possible next token, which is then transformed into probabilities, and the next token is sampled from those probabilities. While the top-k parameters means that the next token will be sampled from the k top tokens by score, temperature changes the probability distribution created from those scores.</p> <p>While a temperature of 1.0 means the probabilities are a direct reflection of the scores, higher temperatures flatten those probabilities, increasing the chances of tokens that would be less likely to be selected, and discretion that of the most likely ones - leading the model to output less common tokens more often, making it more “creative”.</p> <p>Conversely, temperatures below 1.0 makes the chance of those tokens more likely to be selected even larger, making the model more predictable.</p> <p>To help understand how changing temperature and top-k affect probabilities, I’ve put together <a href="https://andreban.github.io/temperature-topk-visualizer/">this visualization</a>.</p> <p><img src="http://bandarra.me/images/temperature-topk-visualizer.png" alt="Screenshot of the visualization application" /></p> <p>The visualization shows the 10 highest score next tokens for a few different prompts and the bars show the probability of each token being selected.</p> <p>When changing the temperature up or down, you can observe how the probabilities are flattened out or become sharper, and how changing top-k drops some of the potential next tokens altogether.</p> <p>Check out the visualizer at <a href="https://andreban.github.io/temperature-topk-visualizer/">https://andreban.github.io/temperature-topk-visualizer/</a>!</p> Understand how temperature affects Large Language Model (LLM) output. Learn how temperature parameter changes probability distribution of next-token predictions, impacting creativity and predictability. Explore a visualization tool demonstrating the effects of temperature and top-k parameters on LLM responses. Balancing AI Assistance and Learning 2025-03-04T15:26:00Z https://bandarra.me/posts/balancing-ai-assistance-for-learning <p><img src="/images/split-brain.png" alt="A brain" /></p> <p>While I'm not completely oblivious about Python as I've read code in the language here and there and wrote some MicroPython, I can't say I'm a Python developer. With lot of what's happening on the AI space uses Python, I decided to brush up my Python skills and I've been going about it same way I've done multiple times in the past: get a good book on the language, and try it out on a simple project.</p> <p>I believe AI is a big productivity booster for developers, from autocomplete functionality that allows writing code faster to agentic experiences where you don't write code at all. However, I found that, for learning a new language, the exact same thing that boosts productivity can slow you down.</p> <p>The reason is that, when learning a language, practicing is a key aspect of the learning process. When writing my practice project with an AI autocomplete enabled, the autocomplete would kick in and finish the code I intented to write, which despite being correct, removed the opportunity for writing the code myself, preventing me from forgetting correct the syntax and patterns for the language and having to looking them up, or making errors and having to fix them. In general, AI prevented me from learning from my own mistakes.</p> <p>Maybe, in the long term, with better and better agentic experiences, that won't matter and forgetting syntax won't be as relevant. For now, I'm disabling the AI autocomplete when practicing a new language.</p> Learn Python for AI development: This hands-on guide details a practical approach to mastering Python, focusing on effective learning techniques and addressing the impact of AI-powered code completion tools on the learning process. Discover how to balance AI assistance with focused practice for optimal skill acquisition in Python programming for AI projects. Building Composite Indexes for Firestore on Windows 2025-01-10T18:46:00Z https://bandarra.me/posts/create-composite-index-on-firestore-on-windows <p>The command on the <a href="https://firebase.google.com/docs/firestore/vector-search?_gl=1*1bnvvnt*_up*MQ..*_ga*NjY3OTU3OTMuMTczNDk1NzM5OQ..*_ga_CW55HF8NVT*MTczNDk1NzM5OS4xLjAuMTczNDk1NzM5OS4wLjAuMA..#create_a_vector_index">Firestore docs</a> to create a composite index that includes a vector embedding doesn't work out of the box on Windows. This seems to be related on how escaping JSON works on Powershell. The solution I found to run the command was to create a JSON file with the index definition, like so:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">[ </span><span style="color:#f8f8f2;"> { </span><span style="color:#f8f8f2;"> </span><span style="color:#eeeeee;">&quot;</span><span style="color:#cfcfc2;">field-path</span><span style="color:#eeeeee;">&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&quot;user_id&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#eeeeee;">&quot;</span><span style="color:#cfcfc2;">order</span><span style="color:#eeeeee;">&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&quot;ASCENDING&quot; </span><span style="color:#f8f8f2;"> }, </span><span style="color:#f8f8f2;"> { </span><span style="color:#f8f8f2;"> </span><span style="color:#eeeeee;">&quot;</span><span style="color:#cfcfc2;">field-path</span><span style="color:#eeeeee;">&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&quot;embedding&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#eeeeee;">&quot;</span><span style="color:#cfcfc2;">vector-config</span><span style="color:#eeeeee;">&quot;</span><span style="color:#f8f8f2;">: { </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;dimension&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">768</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;flat&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&quot;{}&quot; </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;">] </span></pre> <p>Then, create the index with a command that reads the index details from the file, like this:</p> <pre style="background-color:#282a36;"> <span style="color:#50fa7b;">gcloud</span><span style="color:#f8f8f2;"> beta firestore indexes composite create ` </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">--collection-group</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">MyCollection </span><span style="color:#f8f8f2;">` </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">--query-scope</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">COLLECTION </span><span style="color:#f8f8f2;">` </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">--field-config</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">field-config.json </span><span style="color:#f8f8f2;">` </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">--database</span><span style="color:#ff79c6;">=</span><span style="color:#f1fa8c;">mydatabase </span></pre> Troubleshoot creating composite indexes with vector embeddings in Firestore on Windows. This solution uses a JSON file to define the index, bypassing Powershell JSON escaping issues, and provides the corrected `gcloud` command for successful index creation. Learn how to create a functional composite index with vector embeddings. Count tokens with the Gemma 2 Tokenizer in Rust 2024-11-22T20:46:00Z https://bandarra.me/posts/count-tokens-with-the-gemma-2-tokenizer-in-rust <p>For those working with Large Language Models, counting the number of tokens in an input can be a frequent task. As <a href="https://medium.com/google-cloud/a-gemini-and-gemma-tokenizer-in-java-e18831ac9677">Gemini and Gemma share the same tokenizer</a> (at least for now), it is quite useful to be able to be able to count tokens on an input locally, without making network calls <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/count-tokens">to an endpoint</a>, which can be much slower.</p> <p>In rust, this can be achieved with the <a href="https://crates.io/crates/tokenizers"><code>tokenizers</code></a> crate. The sample code below is a minimalistc implementation of <a href="https://github.com/Kamalabot/cratesploring/blob/main/candle_explorer/gemma-tokenizer/src/main.rs">this sample code</a>, removing the need for the <a href="https://crates.io/crates/candle-examples"><code>candle-examples</code></a> create, but still uses the <a href="https://crates.io/crates/candle-examples"><code>hf_hub</code></a> crate to manage model download, but those could be manually downloaded too.</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">use </span><span style="text-decoration:underline;color:#66d9ef;">hf_hub</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#ffffff;">{</span><span style="text-decoration:underline;color:#66d9ef;">api</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">sync</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">ApiBuilder, Repo, RepoType</span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">use </span><span style="text-decoration:underline;color:#66d9ef;">tokenizers</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Tokenizer; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#bd93f9;">HF_TOKEN</span><span style="color:#f8f8f2;">: </span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#8be9fd;">str </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;YOUR_TOKEN_HERE&quot;</span><span style="color:#f8f8f2;">; </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#bd93f9;">MODEL_ID</span><span style="color:#f8f8f2;">: </span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#8be9fd;">str</span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;google/gemma-2-2b&quot;</span><span style="color:#f8f8f2;">; </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#bd93f9;">MODEL_REVISION</span><span style="color:#f8f8f2;">: </span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#8be9fd;">str </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;main&quot;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">-&gt; Result&lt;(), Box&lt;dyn </span><span style="text-decoration:underline;color:#66d9ef;">std</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">error</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#ff79c6;">Error&gt;&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> api </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">ApiBuilder</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new().</span><span style="color:#8be9fd;">with_token</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">HF_TOKEN</span><span style="color:#f8f8f2;">.</span><span style="color:#8be9fd;">to_string</span><span style="color:#f8f8f2;">())).</span><span style="color:#8be9fd;">build</span><span style="color:#f8f8f2;">()</span><span style="color:#ff79c6;">?</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> repo </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> api.</span><span style="color:#8be9fd;">repo</span><span style="color:#f8f8f2;">(</span><span style="text-decoration:underline;color:#66d9ef;">Repo</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">with_revision( </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">MODEL_ID</span><span style="color:#f8f8f2;">.</span><span style="color:#8be9fd;">to_string</span><span style="color:#f8f8f2;">(), </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">RepoType</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Model, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">MODEL_REVISION</span><span style="color:#f8f8f2;">.</span><span style="color:#8be9fd;">to_string</span><span style="color:#f8f8f2;">().</span><span style="color:#8be9fd;">to_string</span><span style="color:#f8f8f2;">(), </span><span style="color:#f8f8f2;"> )); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> tokenizer_filename </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> repo.</span><span style="color:#8be9fd;">get</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;tokenizer.json&quot;</span><span style="color:#f8f8f2;">)</span><span style="color:#ff79c6;">?</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> tokenizer </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">Tokenizer</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">from_file(tokenizer_filename.</span><span style="color:#8be9fd;">clone</span><span style="color:#f8f8f2;">()).</span><span style="color:#8be9fd;">unwrap</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> prompt </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;Why is the sky blue?&quot;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> tokens </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> tokenizer </span><span style="color:#f8f8f2;"> .</span><span style="color:#8be9fd;">encode</span><span style="color:#f8f8f2;">(prompt, </span><span style="color:#bd93f9;">true</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> .</span><span style="color:#8be9fd;">unwrap</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> .</span><span style="color:#8be9fd;">get_ids</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> .</span><span style="color:#8be9fd;">to_vec</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Generated </span><span style="color:#bd93f9;">{}</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#f8f8f2;">, tokens.</span><span style="color:#8be9fd;">len</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Ok</span><span style="color:#f8f8f2;">(()) </span><span style="color:#ffffff;">} </span></pre> <p>The <code>hf_hub</code> crate is smart and caches the model once downloaded. While initializing the model from still takes about 600ms, it should be done only once in the application, counting tokens is quite fast, generally under 1ms.</p> <h2><code>aarch64-pc-windows-msvc</code> issues with <code>candle-examples</code></h2> <p>In the <a href="https://github.com/Kamalabot/cratesploring/blob/main/candle_explorer/gemma-tokenizer/src/main.rs">original example</a>, this code is based on depends on the <a href="https://crates.io/crates/candle-examples"><code>candle-examples</code></a> crate, which fails to build on <code>aarch64</code> architectures. The issue is caused by one of its dependencies, the <a href="https://crates.io/crates/gemm-f16"><code>gemm-f16</code></a> crate. There are workarounds described in <a href="https://github.com/sarah-quinones/gemm/issues/31">this issue</a>.</p> <p>For <code>aarch64-pc-windows-msvc</code>, adding the configuration below to <code>.cargo/config.toml</code> file should do the trick:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">[build] </span><span style="color:#f8f8f2;">rustflags = [ </span><span style="color:#f8f8f2;"> &quot;-Ctarget-feature=+fp16,+fhm&quot; </span><span style="color:#f8f8f2;">] </span></pre> Quickly count tokens in Large Language Models (LLMs) like Gemini and Gemma using Rust. This efficient method avoids slow network calls, leveraging the `tokenizers` crate for local processing. The code example demonstrates token counting with minimal dependencies, even handling `aarch64` architecture challenges. Get started with fast, local token counting now! Rust Markdown Syntax Highlighting: A Practical Guide 2024-09-20T21:15:00Z https://bandarra.me/posts/Rust-Markdown-Syntax-Highlighting-A-Practical-Guide <p>You're a Rust developer, and you love Markdown's simplicity and readability. You might use it to write blog posts, documentation, or even as part of an interactive code editor. However, displaying plain code within Markdown can be tough on the eyes. Enter syntax highlighting, a feature that adds color and structure to your code, making it more visually appealing and easier to understand.</p> <p>This blog post will guide you on combining two powerful Rust libraries –<a href="https://crates.io/crates/pulldown-cmark"> <strong>pulldown-cmark</strong></a> and <a href="https://crates.io/crates/syntect"><strong>syntect</strong></a> – to seamlessly add syntax highlighting to your Markdown files and output the result as a styled HTML file.</p> <p>We'll cover:</p> <ul> <li>How the <strong>pulldown-cmark</strong> library works to parse Markdown.</li> <li>How to leverage <strong>pulldown-cmark</strong> events to specifically target code blocks.</li> <li>How to integrate <strong>syntect</strong> for syntax highlighting your code.</li> <li>Practical examples and best practices to ensure efficient syntax highlighting.</li> </ul> <p>Let's get started!</p> <h2>Understanding Markdown Events with <code>pulldown-cmark</code></h2> <p>You're already familiar with Markdown's simple syntax, but the key to working with it programmatically is understanding how <code>pulldown-cmark</code> represents the parsed content. This library uses events to model the structure of your Markdown document. Think of each event as a signal about what's being encountered while parsing.</p> <p>Let's break down the key events you'll be working with:</p> <ul> <li><strong><code>Event::Start(Tag)</code>:</strong> Indicates the start of a Markdown element. The <code>Tag</code> enum reveals what type of element it is: <ul> <li><code>Tag::Heading</code></li> <li><code>Tag::CodeBlock</code></li> <li><code>Tag::ListItem</code></li> <li>And more.</li> </ul> </li> <li><strong><code>Event::End(TagEnd)</code>:</strong> Signals the end of a Markdown element.</li> <li><strong><code>Event::Text(String)</code>:</strong> Represents the text content within a Markdown element.</li> <li><strong><code>Event::Code(String)</code>:</strong> Indicates a code block and provides the actual code text.</li> </ul> <p>To illustrate how these events work in identifying code blocks, here's a basic example:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">use </span><span style="text-decoration:underline;color:#66d9ef;">pulldown_cmark</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#ffffff;">{</span><span style="color:#f8f8f2;">Event, Parser, Tag, TagEnd</span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> markdown </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">r</span><span style="color:#f1fa8c;">#&quot; </span><span style="color:#f1fa8c;"># Hello, World </span><span style="color:#f1fa8c;"> </span><span style="color:#f1fa8c;">Here&#39;s a code block: </span><span style="color:#f1fa8c;"> </span><span style="color:#f1fa8c;">```rust </span><span style="color:#f1fa8c;">fn main() { </span><span style="color:#f1fa8c;"> println!(&quot;Hello, World&quot;); </span><span style="color:#f1fa8c;">} </span><span style="color:#f1fa8c;">``` </span><span style="color:#f1fa8c;">&quot;#</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> parser </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">Parser</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(markdown); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for</span><span style="color:#f8f8f2;"> event </span><span style="color:#ff79c6;">in</span><span style="color:#f8f8f2;"> parser </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">match</span><span style="color:#f8f8f2;"> event </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Start(</span><span style="text-decoration:underline;color:#66d9ef;">Tag</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">CodeBlock(</span><span style="color:#ff79c6;">_</span><span style="color:#f8f8f2;">)) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Code block start&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">End(</span><span style="text-decoration:underline;color:#66d9ef;">TagEnd</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">CodeBlock) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Code block end&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Text(t) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Text: </span><span style="color:#bd93f9;">{}</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#f8f8f2;">, t); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">_ =&gt; </span><span style="color:#ffffff;">{} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span></pre> <p>In this example, the loop iterates through the events emitted by <code>pulldown-cmark</code>. We are particularly interested in events representing the start and end of code blocks, and also the <code>Text</code> events that appear inside of code blocks.</p> <p>Now that you understand these core concepts, you're ready to move on to incorporating <code>syntect</code> for syntax highlighting!</p> <h2>Highlighting Code with <code>syntect</code></h2> <p>Now that you've learned how to identify code blocks using <code>pulldown-cmark</code> events, let's bring in the powerful syntax highlighting capabilities of <code>syntect</code>. This library makes applying beautiful syntax coloring to your code incredibly straightforward.</p> <h3>What <code>syntect</code> brings to the table</h3> <p>The <code>syntect</code> library shines by providing you with the tools to define and apply custom syntax definitions and color themes. It even leverages Sublime Text's widely popular syntax definitions, enabling you to instantly support a plethora of programming languages.</p> <p>Here's a breakdown of what <code>syntect</code> offers:</p> <ul> <li><strong>Sublime Text Compatibility:</strong> The library utilizes Sublime Text's <code>tmTheme</code> files for creating color themes. There's a wealth of existing themes you can use or customize.</li> <li><strong>Extensive Language Support:</strong> With the default syntax sets included in <code>syntect</code>, you gain immediate support for a vast array of languages.</li> <li><strong>Easy Integration:</strong> Integrating <code>syntect</code> is a breeze. The library provides a clean interface for applying syntax highlighting to code.</li> <li><strong>HTML Output:</strong> <code>syntect</code> can seamlessly generate HTML output, allowing you to embed syntax-highlighted code directly within your web pages or documents.</li> </ul> <h3>Getting Started with <code>syntect</code></h3> <p>Here's a quick demonstration on how to apply syntax highlighting using <code>syntect</code>:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">use </span><span style="text-decoration:underline;color:#66d9ef;">syntect</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#ffffff;">{</span><span style="text-decoration:underline;color:#66d9ef;">highlighting</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">ThemeSet, </span><span style="text-decoration:underline;color:#66d9ef;">html</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">highlighted_html_for_string, </span><span style="text-decoration:underline;color:#66d9ef;">parsing</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">SyntaxSet</span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> code </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">r</span><span style="color:#f1fa8c;">#&quot; </span><span style="color:#f1fa8c;">fn main() { </span><span style="color:#f1fa8c;"> println!(&quot;Hello, World&quot;); </span><span style="color:#f1fa8c;">} </span><span style="color:#f1fa8c;"> &quot;#</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> syntax_set </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">SyntaxSet</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">load_defaults_newlines(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> syntax_reference </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> syntax_set.</span><span style="color:#8be9fd;">find_syntax_by_token</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;rust&quot;</span><span style="color:#f8f8f2;">).</span><span style="color:#8be9fd;">unwrap</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> theme </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">ThemeSet</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">load_defaults().themes[</span><span style="color:#f1fa8c;">&quot;base16-ocean.dark&quot;</span><span style="color:#f8f8f2;">].</span><span style="color:#8be9fd;">clone</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> html </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">highlighted_html_for_string</span><span style="color:#f8f8f2;">(code, </span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">syntax_set, </span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">syntax_reference, </span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">theme).</span><span style="color:#8be9fd;">unwrap</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#bd93f9;">{}</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#f8f8f2;">, html); </span><span style="color:#ffffff;">} </span></pre> <p>In this snippet:</p> <ol> <li><strong><code>SyntaxSet::load_defaults_newlines()</code></strong> loads the default set of syntax definitions, including definitions for Rust, JavaScript, Python, and many other languages.</li> <li><strong><code>syntax_set.find_syntax_by_token("rust")</code></strong> retrieves the specific syntax definition for Rust, which is later used to highlight the code.</li> <li><strong><code>ThemeSet::load_defaults().themes["base16-ocean.dark"].clone()</code></strong> accesses the <code>base16-ocean.dark</code> theme from the default set of themes, offering a clean and modern dark theme.</li> <li><strong><code>highlighted_html_for_string()</code></strong> is the main function responsible for applying highlighting. It takes the code, the syntax set, the theme, and the chosen language, generating a syntax highlighted HTML snippet.</li> <li>The generated <code>html</code> string is then printed to the console.</li> </ol> <p>Let's dive deeper into customization next!</p> <h2>Integrating <code>pulldown-cmark</code> and <code>syntect</code> for Syntax Highlighting</h2> <p>Now you're ready to combine the power of <code>pulldown-cmark</code> and <code>syntect</code> to bring syntax highlighting to your Markdown content. This section walks you through the process, step by step, with code examples to guide you.</p> <p>Let's start by outlining the key steps:</p> <ol> <li><strong>Parse Markdown with <code>pulldown-cmark</code>:</strong> Use <code>pulldown-cmark</code>'s event iterator to extract the relevant data from your Markdown content.</li> <li><strong>Identify Code Blocks:</strong> Specifically look for <code>Event::Start(Tag::CodeBlock)</code> events to pinpoint code sections.</li> <li><strong>Apply Syntax Highlighting with <code>syntect</code>:</strong> For each code block: <ul> <li>Determine the language used (e.g., "rust").</li> <li>Use <code>syntect</code> to apply the appropriate syntax highlighting.</li> <li>Replace the code block content with syntax highlighted HTML.</li> </ul> </li> <li><strong>Render the Final HTML Output:</strong> Stitch the highlighted code blocks back into the <code>pulldown-cmark</code> events stream. Finally, use <code>pulldown-cmark::html::push_html</code> to generate the HTML representation of your Markdown.</li> </ol> <p>Here's how you can implement these steps within a function named <code>markdown_to_html</code>:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">pub </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">markdown_to_html</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">markdown</span><span style="color:#f8f8f2;">: </span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#8be9fd;">str</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">-&gt; String </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">static </span><span style="color:#bd93f9;">SYNTAX_SET</span><span style="color:#f8f8f2;">: LazyLock&lt;SyntaxSet&gt; </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">LazyLock</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(</span><span style="text-decoration:underline;color:#66d9ef;">SyntaxSet</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">load_defaults_newlines); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">static </span><span style="color:#bd93f9;">THEME</span><span style="color:#f8f8f2;">: LazyLock&lt;Theme&gt; </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">LazyLock</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(|| </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> theme_set </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">ThemeSet</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">load_defaults(); </span><span style="color:#f8f8f2;"> theme_set.themes[</span><span style="color:#f1fa8c;">&quot;base16-ocean.dark&quot;</span><span style="color:#f8f8f2;">].</span><span style="color:#8be9fd;">clone</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> sr </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">SYNTAX_SET</span><span style="color:#f8f8f2;">.</span><span style="color:#8be9fd;">find_syntax_plain_text</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> code </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> code_block </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> parser </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">Parser</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(markdown).</span><span style="color:#8be9fd;">filter_map</span><span style="color:#f8f8f2;">(|</span><span style="font-style:italic;color:#ffb86c;">event</span><span style="color:#f8f8f2;">| </span><span style="color:#ff79c6;">match</span><span style="color:#f8f8f2;"> event </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Start(</span><span style="text-decoration:underline;color:#66d9ef;">Tag</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">CodeBlock(</span><span style="text-decoration:underline;color:#66d9ef;">CodeBlockKind</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Fenced(lang))) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> lang </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> lang.</span><span style="color:#8be9fd;">trim</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> sr </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">SYNTAX_SET </span><span style="color:#f8f8f2;"> .</span><span style="color:#8be9fd;">find_syntax_by_token</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">lang) </span><span style="color:#f8f8f2;"> .</span><span style="color:#8be9fd;">unwrap_or_else</span><span style="color:#f8f8f2;">(|| </span><span style="color:#bd93f9;">SYNTAX_SET</span><span style="color:#f8f8f2;">.</span><span style="color:#8be9fd;">find_syntax_plain_text</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> code_block </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">true</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">None </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">End(</span><span style="text-decoration:underline;color:#66d9ef;">TagEnd</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">CodeBlock) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> html </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">highlighted_html_for_string</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">code, </span><span style="color:#ff79c6;">&amp;</span><span style="color:#bd93f9;">SYNTAX_SET</span><span style="color:#f8f8f2;">, </span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">sr, </span><span style="color:#ff79c6;">&amp;</span><span style="color:#bd93f9;">THEME</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> .</span><span style="color:#8be9fd;">unwrap_or</span><span style="color:#f8f8f2;">(code.</span><span style="color:#8be9fd;">clone</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> code.</span><span style="color:#8be9fd;">clear</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> code_block </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(</span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Html(html.</span><span style="color:#8be9fd;">into</span><span style="color:#f8f8f2;">())) </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Text(t) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if</span><span style="color:#f8f8f2;"> code_block </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> code.</span><span style="color:#8be9fd;">push_str</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">t); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return </span><span style="font-style:italic;color:#66d9ef;">None</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(</span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Text(t)) </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">_ =&gt; </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(event), </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> html_output </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(); </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">pulldown_cmark</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">html</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">push_html(</span><span style="color:#ff79c6;">&amp;mut</span><span style="color:#f8f8f2;"> html_output, parser); </span><span style="color:#f8f8f2;"> html_output </span><span style="color:#ffffff;">} </span></pre> <p>Let's examine this code:</p> <ul> <li><strong>Lazy Initialization:</strong> You'll see <code>LazyLock</code> from the <code>lazy_static</code> crate used for both <code>SYNTAX_SET</code> and <code>THEME</code>. This ensures the syntax set and theme are only loaded once during the application's lifetime.</li> <li><strong>Code Block Detection:</strong> We check if we have a code block using <code>Event::Start(Tag::CodeBlock)</code> to track the start of a code block and if a block has ended with <code>Event::End(TagEnd::CodeBlock)</code>.</li> <li><strong>Language Determination:</strong> <code>CodeBlockKind::Fenced</code> will retrieve the fenced code's language (<code>lang</code>). It attempts to locate the matching language within the <code>SYNTAX_SET</code>, falling back to the plain text syntax if no language matches.</li> <li><strong>Syntax Highlighting:</strong> If a code block is found, the code content (<code>code</code>) is highlighted using <code>highlighted_html_for_string</code> and a HTML representation of the code is returned in the Event stream.</li> </ul> <p>Now, this is an essential example of how to use <code>pulldown-cmark</code> and <code>syntect</code>. The core concept is how events are filtered for certain events and replaced with new HTML.</p> <p>We've touched on many ways to apply these ideas. It's up to you to create different tools or applications based on your specific use cases!</p> <h2>Optimization and Performance Best Practices</h2> <p>You've now got a good understanding of how to use <code>pulldown-cmark</code> and <code>syntect</code> for syntax highlighting. However, for real-world use cases, you'll likely want to optimize the process for speed and efficiency, particularly when dealing with large Markdown files. Here are some essential best practices to keep in mind:</p> <h3>Optimizing Syntax Set and Theme Loading</h3> <p>The initial loading of syntax sets and themes is a relatively expensive operation. Since loading these resources can significantly impact performance, it's crucial to load them wisely. You can use <code>LazyLock</code> to ensure these resources are loaded only when needed, rather than upfront:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">static </span><span style="color:#bd93f9;">SYNTAX_SET</span><span style="color:#f8f8f2;">: LazyLock&lt;SyntaxSet&gt; </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">LazyLock</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(</span><span style="text-decoration:underline;color:#66d9ef;">SyntaxSet</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">load_defaults_newlines); </span><span style="font-style:italic;color:#8be9fd;">static </span><span style="color:#bd93f9;">THEME</span><span style="color:#f8f8f2;">: LazyLock&lt;Theme&gt; </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">LazyLock</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(|| </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> theme_set </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">ThemeSet</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">load_defaults(); </span><span style="color:#f8f8f2;"> theme_set.themes[</span><span style="color:#f1fa8c;">&quot;base16-ocean.dark&quot;</span><span style="color:#f8f8f2;">].</span><span style="color:#8be9fd;">clone</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">); </span></pre> <p>This way, <code>SYNTAX_SET</code> and <code>THEME</code> are loaded only once and will be available globally in your project, ensuring that resources are efficiently managed, reducing unnecessary overhead.</p> <h3>Efficient Event Processing Techniques</h3> <p>A naïve approach to handle the events is to use <code>collect()</code> from the <code>pulldown-cmark</code> event iterator, turning it into a <code>Vec</code> of <code>Event</code>s. However, this approach iterates over the entire vector multiple times, creating performance problems for larger Markdown files.</p> <p>Here's how you can rewrite the core loop of the markdown rendering function to use an iterator approach, which optimizes for performance:</p> <pre style="background-color:#282a36;"> <span style="color:#6272a4;">// ... </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> parser </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">Parser</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(markdown).</span><span style="color:#8be9fd;">filter_map</span><span style="color:#f8f8f2;">(|</span><span style="font-style:italic;color:#ffb86c;">event</span><span style="color:#f8f8f2;">| </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">match</span><span style="color:#f8f8f2;"> event </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Start(</span><span style="text-decoration:underline;color:#66d9ef;">Tag</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">CodeBlock(</span><span style="text-decoration:underline;color:#66d9ef;">CodeBlockKind</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Fenced(lang))) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... Handle start of a code block. </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">End(</span><span style="text-decoration:underline;color:#66d9ef;">TagEnd</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">CodeBlock) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... Handle the end of a code block. </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">Event</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Text(t) </span><span style="color:#ff79c6;">=&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... Handle Text within a code block </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">_ =&gt; </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(event), </span><span style="color:#6272a4;">// Return other events to continue the processing </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// This uses a `filter_map`, and the `match` inside creates the output based on the events. </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> html_output </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(); </span><span style="color:#f8f8f2;"> </span><span style="text-decoration:underline;color:#66d9ef;">pulldown_cmark</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">html</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">push_html(</span><span style="color:#ff79c6;">&amp;mut</span><span style="color:#f8f8f2;"> html_output, parser); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... </span></pre> <p>In this revised snippet, we employ a filter and mapping pattern, creating a streamlined and performant code. The idea is that the <code>pulldown-cmark::html::push_html</code> method iterates through each event on the fly, applies the logic and only modifies the needed events.</p> <h4>Summary of Optimizations</h4> <p>By embracing these optimizations, you can significantly improve the performance and efficiency of your syntax highlighting code while reducing the overall memory consumption:</p> <ul> <li>Use <code>LazyLock</code> for delayed loading.</li> <li>Process events iteratively instead of creating intermediate vectors.</li> <li>Use efficient techniques to dynamically load the appropriate language definition, handling unexpected languages gracefully.</li> </ul> <h2>Conclusion: Elevating Markdown Rendering with Syntax Highlighting</h2> <p>Combining the power of <code>pulldown-cmark</code> and <code>syntect</code> allows you to unlock a whole new level of polish and functionality when working with Markdown files in your Rust projects. This approach transforms Markdown rendering into something truly delightful, enhancing your ability to produce visually engaging and easy-to-read content for blogs, documentation, and code editors.</p> <p>Imagine generating your documentation with beautifully highlighted code, creating blog posts with captivating syntax highlighting, or empowering your interactive code editor with the elegance of colored code – this dynamic duo empowers you to achieve all this and more.</p> <p>By mastering these libraries, you not only streamline the process of creating Markdown-based content, but you also infuse it with an enhanced visual experience, ultimately enhancing communication and readability. You can focus on creating clear, structured content, knowing that your code will be presented with the style it deserves.</p> <p>Take the time to experiment with these powerful tools, explore different themes, languages, and use cases. As you become comfortable with the capabilities of <code>pulldown-cmark</code> and <code>syntect</code>, you'll discover new ways to create compelling and engaging content with Markdown.</p> Add syntax highlighting to your Markdown files using Rust's pulldown-cmark and syntect libraries. This tutorial shows you how to parse Markdown, target code blocks, integrate syntect for highlighting, and optimize for performance with practical examples and best practices, resulting in styled HTML output. Optimizing Your Rust Workflow: Mitigating Unnecessary Dependency Recompilation 2024-09-10T08:49:00Z https://bandarra.me/posts/optimizing-your-rust-workflow-mitigating-unnecessary-dependency-recompilation <p><img src="/images/rust-developer-frustrated.jpg" alt="A frustrated Rust developer" /></p> <p>As a Rust developer using Visual Studio Code and <code>rust-analyzer</code>, you might have encountered a common problem: unnecessary recompilation of dependencies upon saving a file. Even changes to files seemingly unrelated to dependencies can trigger a <code>cargo check</code>, causing delays in your development workflow.</p> <p>This article examines the reasons behind this behavior and offers practical solutions to mitigate the issue.</p> <p><strong>Why Does <code>rust-analyzer</code> Trigger Dependency Recompilation?</strong></p> <p><code>rust-analyzer</code> aims to provide a comprehensive and accurate understanding of your project, constantly updating its internal model as you work. Whenever you save a file, <code>rust-analyzer</code> assumes the change might impact dependencies, prompting it to run a <code>cargo check</code> to validate the project's consistency. This validation involves recompiling dependencies.</p> <p><strong>Understanding the Challenge</strong></p> <p>Predicting the precise impact of a change, even on seemingly unrelated dependencies, is difficult. This difficulty arises from factors such as macros, where a change can cascade through your code. <code>rust-analyzer</code>, in its effort to provide the most reliable feedback and code completion, takes a more cautious approach, triggering recompilation for potentially impacted dependencies.</p> <p><strong>Scenarios Where Recompilation is More Noticeable</strong></p> <p>The recompilation issue becomes particularly noticeable in scenarios with a high number of dependencies.</p> <ul> <li> <p><strong>Projects with a Large Dependency Tree:</strong> Large, interconnected dependency trees result in longer compilation times. <code>rust-analyzer</code> analyzes each dependency, adding significant overhead.</p> </li> <li> <p><strong>Dependencies with Build Scripts:</strong> Dependencies involving build scripts can drastically increase compilation time. Build scripts generate code, download external resources, or configure build settings, contributing to the complexity and execution time.</p> </li> <li> <p><strong>Native Dependencies:</strong> Dependencies with native code (like C or C++) add an additional layer of complexity. Native libraries must be compiled and linked, further delaying the build process.</p> </li> </ul> <p><strong>Mitigating the Issue</strong></p> <ul> <li> <p><strong><code>cargo check</code> is faster than <code>cargo test</code> or <code>cargo build</code>.</strong> So, these aren't more effective alternatives to checking your code on save.</p> </li> <li> <p><strong>Disabling <code>rust-analyzer.checkOnSave</code>.</strong> While you can disable code checks on save by setting <code>rust-analyzer.checkOnSave</code> to <code>false</code> in your project workspace settings, it effectively turns the editor into a less efficient development environment by completely disabling checks.</p> </li> </ul> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#eeeeee;">&quot;</span><span style="color:#cfcfc2;">rust-analyzer.checkOnSave</span><span style="color:#eeeeee;">&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;">} </span></pre> <ul> <li><strong><code>rust-analyzer.check.extraArgs</code>.</strong> The most effective solution is to use <code>rust-analyzer.check.extraArgs</code> to configure a separate target directory for <code>cargo check</code> operations:</li> </ul> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#eeeeee;">&quot;</span><span style="color:#cfcfc2;">rust-analyzer.checkOnSave</span><span style="color:#eeeeee;">&quot;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">true</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#eeeeee;">&quot;</span><span style="color:#cfcfc2;">rust-analyzer.check.extraArgs</span><span style="color:#eeeeee;">&quot;</span><span style="color:#f8f8f2;">: [ </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;--target-dir&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;${workspaceFolder}/target/check&quot; </span><span style="color:#f8f8f2;"> ] </span><span style="color:#f8f8f2;">} </span></pre> <p><strong>Conclusion</strong></p> <p>While dependency recompilation is a common challenge faced by Rust developers, the right configuration can significantly improve the development cycle by reducing unnecessary delays. Understanding the reasoning behind <code>rust-analyzer</code>'s approach is essential for effectively managing these issues. You can optimize your workspace configuration to achieve a faster, more responsive coding experience, maximizing your productivity and streamlining your development workflow.</p> Speed up your Rust development workflow in VS Code! This guide tackles the frustrating issue of unnecessary dependency recompilation with `rust-analyzer`, explaining why it happens and offering effective solutions, including configuring a separate target directory for `cargo check` to drastically reduce build times. Learn how to optimize your settings for a smoother coding experience. Heterogeneous collections in Rust 2023-10-03T00:00:00Z https://bandarra.me/posts/Heterogeneous-collections-in-Rust <p>In some occasions, when programming software, developers run into the need of heterogenous collections - that is, a collection that can store objects of different types. In Rust, there are different ways a developer can achieve that, with different tradeoffs. This article will look into a few different ways to achieve this.</p> <h2>Using Enums</h2> <p>Rust <a href="https://doc.rust-lang.org/reference/items/enumerations.html">enums</a> are a great way to achieve this. Provided that all implementations of the objects to be store are known at development time, developers can create an enum that wraps each possible type, then create a collection for those enums.</p> <p>Then, to access the methods and attributes of the inner class, a <a href="https://doc.rust-lang.org/reference/expressions/match-expr.html">match expression</a> can be used to retrieve the inner object.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">enum </span><span style="color:#f8f8f2;">ComponentType </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> FirstComponent(MyFirstComponent), </span><span style="color:#f8f8f2;"> SecondComponent(MySecondComponent), </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_first_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;First Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_second_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Second Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Create a collection of enums; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> components: Vec&lt;ComponentType&gt; </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">Vec</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Add the enums to the collection, wrapping the target type. </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="text-decoration:underline;color:#66d9ef;">ComponentType</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">FirstComponent(MyFirstComponent </span><span style="color:#ffffff;">{}</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="text-decoration:underline;color:#66d9ef;">ComponentType</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">SecondComponent(MySecondComponent </span><span style="color:#ffffff;">{}</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Use match expressions to retrieve the object from the enum and access methods and attributes. </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="text-decoration:underline;color:#66d9ef;">ComponentType</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">FirstComponent(component) </span><span style="color:#ff79c6;">= &amp;</span><span style="color:#f8f8f2;">components[</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">] </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> component.</span><span style="color:#8be9fd;">do_first_component_thing</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span></pre> <p>An advantage of this method is that the implementation is quite simple and idiomatic and, when used inside an Array, for instance, it will allocate all objects on the stack (the <code>Vector</code> used in this example will allocate on the heap, though).</p> <p>On the other hand, a challenge with this approach is that those component types need to be known when writing the code. Think about a library that needs to store objects from different types, but those are only known by the user of that library.</p> <h2>Using Traits</h2> <p>An alternative is using <a href="https://doc.rust-lang.org/reference/items/traits.html">traits</a> as alternate solution, where:</p> <ul> <li>the common methods for Components are defined in the <code>Component</code> trait.</li> <li>each relevant component struct implements the <code>Component</code> trait.</li> <li>since the size of the objects being added to the collection are not know, the objects need to be wrapped in a <a href="https://doc.rust-lang.org/std/boxed/struct.Box.html">Box</a>.</li> </ul> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Declare a trait with common behaviour. </span><span style="font-style:italic;color:#8be9fd;">trait </span><span style="color:#f8f8f2;">Component </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">); </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{} </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Implement the trait for each type. </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">Component </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;First Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">Component </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Second Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> components: Vec&lt;Box&lt;dyn Component&gt;&gt; </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">Vec</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(); </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Box</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(MyFirstComponent </span><span style="color:#ffffff;">{ }</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Box</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(MySecondComponent </span><span style="color:#ffffff;">{ }</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> components[</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">].</span><span style="color:#8be9fd;">do_component_thing</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> components[</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">].</span><span style="color:#8be9fd;">do_component_thing</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>This approach works well when it's only necessary to access the common method in all traits. A disadvantage of this approach is that elements will always be allocated on the heap, and another disadvantage is that it's only possible to access common methods.</p> <h2>Using Any</h2> <p>The Rust documentation describes the <a href="https://doc.rust-lang.org/std/any/trait.Any.html">Any</a> type as <em>A trait to emulate dynamic typing.</em>. It provides a <code>downcast</code> method, which allows typecasting to different types.</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">use </span><span style="text-decoration:underline;color:#66d9ef;">std</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">any</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Any; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_first_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;First Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_second_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Second Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> components: Vec&lt;Box&lt;dyn Any&gt;&gt; </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">Vec</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(); </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Box</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(MyFirstComponent </span><span style="color:#ffffff;">{}</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Box</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(MySecondComponent </span><span style="color:#ffffff;">{}</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(component) </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;"> components[</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">].</span><span style="text-decoration:underline;color:#66d9ef;">downcast_ref</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">&lt;MyFirstComponent&gt;() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> component.</span><span style="color:#8be9fd;">do_first_component_thing</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(component) </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;"> components[</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">].</span><span style="text-decoration:underline;color:#66d9ef;">downcast_ref</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">&lt;MySecondComponent&gt;() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> component.</span><span style="color:#8be9fd;">do_second_component_thing</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span></pre> <p>While this will still always allocate objects on the heap, it's now possible to have different component types inside the data structure, cast them to original types and access component specific attributes and methods.</p> <p>There's one small issue, though - there is no bound to which types can be added to the structure and the line below wokis just fine:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Box</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(</span><span style="color:#f1fa8c;">&quot;I shouldn&#39;t be here&quot;</span><span style="color:#f8f8f2;">).</span><span style="color:#8be9fd;">to_string</span><span style="color:#f8f8f2;">()); </span></pre> <h2>Mixing Any and Traits</h2> <p><code>Any</code> can be used along with <code>Traits</code> to create bounds for the object. The trick is to add a method to the trait that converts the object to <code>Any</code>, which will then be downcasted to other objects. Each structure will then have to implement the trait, and the conversion method:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">use </span><span style="text-decoration:underline;color:#66d9ef;">std</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">any</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">Any; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">trait </span><span style="color:#f8f8f2;">Component </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">as_any</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">-&gt; &amp;dyn Any</span><span style="color:#f8f8f2;">; </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_first_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;First Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">Component </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">as_any</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">-&gt; &amp;dyn Any </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_second_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;Second Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">Component </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">MySecondComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">as_any</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">-&gt; &amp;dyn Any </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> components: Vec&lt;Box&lt;dyn Component&gt;&gt; </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">Vec</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(); </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Box</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(MyFirstComponent </span><span style="color:#ffffff;">{}</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> components.</span><span style="color:#8be9fd;">push</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Box</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(MySecondComponent </span><span style="color:#ffffff;">{}</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(component) </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;"> components[</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">].</span><span style="color:#8be9fd;">as_any</span><span style="color:#f8f8f2;">().</span><span style="text-decoration:underline;color:#66d9ef;">downcast_ref</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">&lt;MyFirstComponent&gt;() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> component.</span><span style="color:#8be9fd;">do_first_component_thing</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="font-style:italic;color:#66d9ef;">Some</span><span style="color:#f8f8f2;">(component) </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;"> components[</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">].</span><span style="color:#8be9fd;">as_any</span><span style="color:#f8f8f2;">().</span><span style="text-decoration:underline;color:#66d9ef;">downcast_ref</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">&lt;MySecondComponent&gt;() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> component.</span><span style="color:#8be9fd;">do_second_component_thing</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span></pre> <p>While, again, this will still allocate objects on the heap, object specific methods and attributes can be used with a downcast, and the collection is bound to objects that implement that trait. One big downside is having to implement the trait for each object, which is just boilerplate.</p> <h3>Using proc-macro-derive to avoid boilerplate</h3> <p>A solution to the boilerplate using using a <a href="https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros">procedural macro</a> to implement the boiler plate:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// A derive macro needs to live in its own crate. </span><span style="color:#f8f8f2;">#[proc_macro_derive(Component)] </span><span style="color:#ff79c6;">pub </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">component_macro_derive</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">input</span><span style="color:#f8f8f2;">: TokenStream) </span><span style="color:#ff79c6;">-&gt; TokenStream </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> ast: DeriveInput </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">syn</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">parse(input).</span><span style="color:#8be9fd;">unwrap</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> name </span><span style="color:#ff79c6;">= &amp;</span><span style="color:#f8f8f2;">ast.ident; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> gen </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">quote! </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">Component </span><span style="color:#ff79c6;">for</span><span style="color:#f8f8f2;"> #name </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">as_any</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">-&gt; &amp;dyn Any </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> gen.</span><span style="color:#8be9fd;">into</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// The component still lives in the project file. </span><span style="color:#f8f8f2;">#[derive(Component)] </span><span style="font-style:italic;color:#8be9fd;">struct </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">impl </span><span style="color:#f8f8f2;">MyFirstComponent </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">do_first_component_thing</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">&amp;</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> println!(</span><span style="color:#f1fa8c;">&quot;First Component&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span></pre> <h1>Conclusion</h1> <p>There are different ways to implement heterogenous collectionos in Rust. While the enums approach seems to be considered the most idiomatic, it's not always possible to be used. In those cases, different approaches are available, with their own tradeoffs.</p> Efficiently manage heterogenous collections in Rust using enums, traits, or the `Any` type. Learn the tradeoffs of each approach, from stack vs. heap allocation to accessing specific methods, and discover how to reduce boilerplate with procedural macros for optimal code structure. A Glimpse into how ChatGPT could be used for coding 2023-01-07T00:00:00Z https://bandarra.me/posts/A-Glimpse-into-how-ChatGPT-could-be-used-for-coding <p>There’s a lot of discussion out there around how AIs such as <a href="https://openai.com/blog/chatgpt/">ChatGPT</a> will impact Software Engineering - from making programmers redundant, to becoming an assistive, to not changing anything. Personally, I lean more towards the assistive side and, this week, I had an experience that might relate to how it could be used.</p> <p>The task was unrelated to software engineering. I was helping someone to get an article translated from English to Brazilian Portuguese - my mother language. Fully automated translation frequently generates awkward results, but I wanted to use <a href="https://translate.google.com/">Google Translate</a> as a tool to help me with the translation.</p> <p>So, my approach was interactive - Some bits I’d translate myself and be confident about it, without bothering to ask AI for help. In other bits, I’d do the translation, then ask the AI to translate and check if what it provided was better than what I came up with - sometimes it was, sometimes it wasn’t. In other cases, when I felt I couldn’t come up with a good translation, I’d ask AI to do it first, then tweak the results.</p> <p>The back and forth between Google Docs and Google Translate was a bit awkward, and a specialized UI for this type of interactive workflow would go a long way to increased productivity and leverage Google Translate for this use-case.</p> <p>Could the interaction with AI for programming look something like that? Maybe yes… maybe no… but I’m looking forward to what will come out of it!</p> AI's role in software engineering is evolving from redundancy fears to assistive capabilities. This account details using AI translation interactively, improving results through a blend of human expertise and AI assistance. The experience suggests a future where programmers collaborate with AI, enhancing productivity and leveraging AI's strengths for optimal outcomes. Play music with a raspberry Pi Pico and Rust 2022-08-02T00:00:00Z https://bandarra.me/posts/Play-Music-with-the-Raspberry-Pi-Pico-and-Rust <p>If you are coming from Micropython or the Arduino IDE, playing a musical note is as straightforward as calling <code>freq()</code> passing the desired note frequency as a parameter, as in the MicroPython example below:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">import </span><span style="color:#f8f8f2;">machine </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> p12 </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">machine</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">Pin</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">12</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> pwm12 </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">machine</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">PWM</span><span style="color:#f8f8f2;">(p12) </span><span style="color:#f8f8f2;"> pwm12</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">freq</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">440</span><span style="color:#f8f8f2;">) </span><span style="color:#6272a4;"># 440Hz is an A4 note. </span><span style="color:#f8f8f2;"> pwm12</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">duty</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">512</span><span style="color:#f8f8f2;">) </span></pre> <p>However, when using the <a href="https://www.raspberrypi.com/documentation/microcontrollers/c_sdk.html#raspberry-pi-pico-cc-sdk">Raspberry Pi Pico C/C++ SDK</a> or the <a href="https://github.com/rp-rs/rp-hal/">Rust's rp-hal</a>, there isn't a method to set the frequency directy and it is necessary to use a lower-level API, the <a href="https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__pwm.html#gad6cf6d9237144234732a50eb6d5e4fe9"><code>pwm_config_set_wrap()</code></a> on C/C++ or <a href="https://docs.rs/rp2040-hal/0.5.0/rp2040_hal/pwm/struct.Slice.html#method.set_top"><code>set_top</code></a> in Rust.</p> <p>In order to use this, understanding how PWM is implemented on the Pico is helpful. Here's what the <a href="https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__pwm.html#gad6cf6d9237144234732a50eb6d5e4fe9:~:text=Detailed%20Description">C/C++ documentation</a> says:</p> <blockquote> <p>The PWM hardware functions by continuously comparing the input value to a free-running counter. This produces a toggling output where the amount of time spent at the high output level is proportional to the input value. The fraction of time spent at the high signal level is known as the duty cycle of the signal.</p> <p>The default behaviour of a PWM slice is to count upward until the wrap value (<code>pwm_config_set_wrap</code>) is reached, and then immediately wrap to 0. PWM slices also offer a phase-correct mode, where the counter starts to count downward after reaching TOP, until it reaches 0 again.</p> </blockquote> <p>The hardware PWM is implemented with a counter that, by default, is incremented at the same rate as the Pico crystal frequency, or 12Mhz, and input that is compared to that counter value, in order to set the voltage to high or low.</p> <p>The input used for comparison is passed to the system using <code>set_duty()</code> and the maximum value for the counter is set via <code>set_top()</code>. In the example below, the counter is set to 1000, creating a frequency of 12Khz (12Mhz divided by 1000). In this case, we're setting the duty cycle to half the value of the counter - or a 50% duty cycle.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> pwm.</span><span style="color:#8be9fd;">set_top</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1000</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> pwm.channel_b.</span><span style="color:#8be9fd;">set_duty</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">500</span><span style="color:#f8f8f2;">); </span></pre> <p>By setting top to 1, a maximum frequency of 12Mhz can be created. The maximum value that can be passed to <code>set_top()</code> is <code>65535</code> (an <code>u16</code>), creating a frequency of 183Hz.</p> <p>Since we are concerned about musical notes and the humans can detect sounds between 20Hz and 20Khz, which doesn't fully overlap with the available frequencies between 183Hz to 12Mhz.</p> <p>This problem can be solved by setting a clock divider, via <code>set_div_int()</code> and <code>set_div_frac()</code>. This causes the counter to be updated at a lower frequency. When setting the divider to 2, for instance, the counter is only incremented every other cycle, decreasing the frequency of updates to 6Mhz (12Mhz / 2).</p> <p>Setting the divider to 40, for instance, would set the maximum update frequency to 300Kh. Combined with <code>set_top()</code>, this allows a minimum frequency of 4.5Hz, which is below the minimum for the human hearing.</p> <p>It is possible to calculate the <code>top</code> value for a particlular note by dividing the 12Mhz by the divider, then by the note frequency. Using a 40 as a divider and the an A4 note as an example, divide 12Mhz by 40, and the result by 440.0. The resulting top value is 681.</p> <p>Below is an example of playing musical notes with rp-hal on the Rasperry Pi Pico:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">fn </span><span style="color:#50fa7b;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">freq</span><span style="color:#f8f8f2;">: </span><span style="font-style:italic;color:#8be9fd;">f32</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">-&gt; </span><span style="font-style:italic;color:#8be9fd;">u16 </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> (</span><span style="color:#bd93f9;">12_000_000 </span><span style="color:#ff79c6;">as </span><span style="font-style:italic;color:#8be9fd;">f32 </span><span style="color:#ff79c6;">/ </span><span style="color:#bd93f9;">40 </span><span style="color:#ff79c6;">as </span><span style="font-style:italic;color:#8be9fd;">f32 </span><span style="color:#ff79c6;">/</span><span style="color:#f8f8f2;"> freq) </span><span style="color:#ff79c6;">as </span><span style="font-style:italic;color:#8be9fd;">u16 </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> pwm_slices </span><span style="color:#ff79c6;">= </span><span style="text-decoration:underline;color:#66d9ef;">hal</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">pwm</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="text-decoration:underline;color:#66d9ef;">Slices</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">new(pac.</span><span style="color:#bd93f9;">PWM</span><span style="color:#f8f8f2;">, </span><span style="color:#ff79c6;">&amp;mut</span><span style="color:#f8f8f2;"> pac.</span><span style="color:#bd93f9;">RESETS</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">mut</span><span style="color:#f8f8f2;"> buzzer </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> pwm_slices.pwm5; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Notes </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> c4 </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">261.63</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> d4 </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">293.66</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> e4 </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">329.63</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> f4 </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">349.23</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> g4 </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">392.00</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> a4 </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">440.00</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> b4 </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">493.88</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> space </span><span style="color:#ff79c6;">= </span><span style="color:#8be9fd;">calc_note</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0.0</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> doremi </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[c4, d4, e4, f4, g4, a4, b4]; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let</span><span style="color:#f8f8f2;"> twinkle_twinkle </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">[ </span><span style="color:#f8f8f2;"> c4, c4, g4, g4, a4, a4, g4, space, </span><span style="color:#f8f8f2;"> f4, f4, e4, e4, d4, d4, c4, space, </span><span style="color:#f8f8f2;"> g4, g4, f4, f4, e4, e4, d4, space, </span><span style="color:#f8f8f2;"> g4, g4, f4, f4, e4, e4, d4, space, </span><span style="color:#f8f8f2;"> c4, c4, g4, g4, a4, a4, g4, space, </span><span style="color:#f8f8f2;"> f4, f4, e4, e4, d4, d4, c4, space, </span><span style="color:#f8f8f2;"> ]; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for</span><span style="color:#f8f8f2;"> top </span><span style="color:#ff79c6;">in</span><span style="color:#f8f8f2;"> twinkle_twinkle </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> buzzer.channel_b.</span><span style="color:#8be9fd;">set_duty</span><span style="color:#f8f8f2;">(top </span><span style="color:#ff79c6;">/ </span><span style="color:#bd93f9;">2</span><span style="color:#f8f8f2;">); </span><span style="color:#6272a4;">// 50% Duty Cycle </span><span style="color:#f8f8f2;"> buzzer.</span><span style="color:#8be9fd;">set_top</span><span style="color:#f8f8f2;">(top); </span><span style="color:#f8f8f2;"> delay.</span><span style="color:#8be9fd;">start</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">500.</span><span style="color:#8be9fd;">milliseconds</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">_ = </span><span style="text-decoration:underline;color:#66d9ef;">nb</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">block</span><span style="color:#ff79c6;">!</span><span style="color:#f8f8f2;">(delay.</span><span style="color:#8be9fd;">wait</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> buzzer.channel_b.</span><span style="color:#8be9fd;">set_duty</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> delay.</span><span style="color:#8be9fd;">start</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">100.</span><span style="color:#8be9fd;">milliseconds</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ff79c6;">_ = </span><span style="text-decoration:underline;color:#66d9ef;">nb</span><span style="text-decoration:underline;color:#ff79c6;">::</span><span style="color:#f8f8f2;">block</span><span style="color:#ff79c6;">!</span><span style="color:#f8f8f2;">(delay.</span><span style="color:#8be9fd;">wait</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span></pre> Generate musical notes on a Raspberry Pi Pico using C/C++ or Rust. Learn to control PWM frequency and duty cycle for precise sound output. Example code demonstrates playing "Twinkle Twinkle Little Star," covering frequency calculation and clock divider techniques for optimal sound reproduction within the human hearing range. Drive a LED grid with a Raspberry Pi Pico and Web Serial - Part 1 2022-02-23T00:00:00Z https://bandarra.me/posts/Driving-a-ledgrid-with-a-Raspberry-Pi-Pico-and-WebSerial-Part-1 <p>Last year, I got one of those <a href="https://www.aliexpress.com/item/1005001659493361.html">LED grids from AliExpress</a> and I wanted to connect it to my computer, while allowing others to control what is displayed from a web page.</p> <p>To achieve that, I used a <a href="https://www.raspberrypi.com/products/raspberry-pi-pico/">Raspberry Pi Pico</a> connected to my computer to control the LED grid, while controlling the Pico itself via <a href="https://web.dev/serial/">Web Serial</a> and using <a href="https://firebase.google.com/products/realtime-database">Firebase Realtime Database</a> to allow others to remotely change what is rendered.</p> <p>This is a 3-part blog post covering:</p> <ul> <li><a href="/2022/02/21/Driving-a-ledgrid-with-a-Raspberry-Pi-Pico-and-WebSerial-Part-1/">Part 1</a>: Control an LED Grid from Pico using the serial port.</li> <li>Part 2: Control the Pico using Web Serial from the computer.</li> <li>Part 3: Remotely control the LED Grid from a web page.</li> </ul> <h1>Part 1 - Control a LED Grid with the Raspberry Pi Pico</h1> <h2>What you will need</h2> <ul> <li><a href="https://www.raspberrypi.com/products/raspberry-pi-pico/">Raspberry Pi Pico</a>.</li> <li>LED Grid like <a href="https://www.aliexpress.com/item/1005001659493361.html">this one</a>.</li> <li>External 5V / 5A power source.</li> <li>Breadboard.</li> <li>Breadboard jumper wires.</li> </ul> <p>The LED Grid has 256 LEDs, distributed in 16 columns and 16 rows. Each LED can consume up to <code>20mA</code> (milliamps) of power, when set to white. On total, the entire LED Grid can consume up to around <code>5A</code>, which is way higher than what can be powered via the USB port powering the Pico, so an external power supply capable of handling that is needed.</p> <p>To power the LED grid, the external power supply is connected to the power rails on the breadboard, and the rails are connected <code>VCC</code> and <code>GND</code> on the LED grid.</p> <p><code>GPIO7</code> (Pin 10) on the Pico is used to to control the LED grid, so Pin 10 on the Pico is connected to <code>DIN</code> on the LED grid, and the circuit is closed by connecting one of the <code>GND</code> pins on the Pico to the ground power rail.</p> <p>This diagram shows how things should look like with everything connected:</p> <p><img src="/img/2022/02/LedGrid_bb.jpg" alt="LED Grid" title="LED grid" /></p> <p>Note: It is possible to power the Pico with the external power source by connecting the positive power rail to <code>VSYS</code> (Pin 39). In this project, since the Pico will be connected to the USB for the serial communication, it can draw power from the USB port and doesn't need to be connected to <code>VSYS</code>.</p> <h2>Getting started with LED strips</h2> <p>There are different models of LED strips out there. Some, like the ones based on <a href="https://cdn-shop.adafruit.com/datasheets/WS2801.pdf">WS2801</a> can be controlled via the SPI bus - this make them ideal to be controlled from computers like the Raspberry Pis.</p> <p>However, the LED controllers used on this LED grid is the <a href="https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf">WS2812B</a>. Instead of using a higher level protocol, like SPI or I2C, sending data to those controllers is achieved by setting pins to <code>HIGH</code> and <code>LOW</code> with specific timings, with a technique called <a href="https://en.wikipedia.org/wiki/Bit_banging">bit banging</a>.</p> <h3>Bit banging and the Pico PIO</h3> <p>Implementing bit banging usually requires very careful programming, due to the interaction of the specific timings required by the protocol, the CPU clock cycle and other parts of the code that also use the CPU clock cycle.</p> <p>The Pico adds a feature called Programmable Input/Output (PIO). It implements a state machine connected to a FIFO queue that exchange data with the main program have access to the board's GPIO, making the code for the protocol and other parts of the code independent, in terms of clock cycles. An explanation of the Raspberry Pi Pico PIO is outside the scope of this article, and has already been covered by a number of online resources, like <a href="https://medium.com/geekculture/raspberry-pico-programming-with-pio-state-machines-e4610e6b0f29">this blog post</a>.</p> <p>The <a href="https://github.com/raspberrypi/pico-examples/">Raspberry Pi Pico Examples</a> repository implements the protocol needed on the <a href="https://github.com/raspberrypi/pico-examples/blob/master/pio/ws2812/ws2812.pio">ws2812 example</a>, with timings adjusted to work with the ws2812b:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">.</span><span style="color:#ffffff;">program</span><span style="color:#f8f8f2;"> ws2812b </span><span style="color:#ff79c6;">.</span><span style="color:#ffffff;">side_set </span><span style="color:#bd93f9;">1 </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">.</span><span style="color:#ffffff;">define</span><span style="color:#f8f8f2;"> public T1 </span><span style="color:#bd93f9;">3 </span><span style="color:#ff79c6;">.</span><span style="color:#ffffff;">define</span><span style="color:#f8f8f2;"> public T2 </span><span style="color:#bd93f9;">4 </span><span style="color:#ff79c6;">.</span><span style="color:#ffffff;">define</span><span style="color:#f8f8f2;"> public T3 </span><span style="color:#bd93f9;">3 </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">... </span></pre> <p>The Raspberry Pi Pico SDK CMake file will take the <code>.pio</code> program and generate the related code, introducing the <code>ws2812b_program_init()</code> method to the application, which allows initialising the state machine with the pio port, the pin it controls and the clock. Data is sent to the LED strip by calling <a href="https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__pio.html#gaee8bfc3409cb8d93cccdeda3961bc377"><code>pio_sm_put_blocking()</code></a>.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">class </span><span style="text-decoration:underline;color:#8be9fd;">LedStrip </span><span style="color:#ffffff;">{ </span><span style="color:#ff79c6;">public</span><span style="color:#f8f8f2;">: </span><span style="color:#f8f8f2;"> PIO pio; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint32_t </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;">buffer; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> pin_tx; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> length; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> sm </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">LedStrip</span><span style="color:#f8f8f2;">(PIO </span><span style="font-style:italic;color:#ffb86c;">pio</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#8be9fd;">int </span><span style="font-style:italic;color:#ffb86c;">pin_tx</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#66d9ef;">uint32_t </span><span style="font-style:italic;color:#ffb86c;">buffer</span><span style="color:#f8f8f2;">[], </span><span style="font-style:italic;color:#8be9fd;">int </span><span style="font-style:italic;color:#ffb86c;">length</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">pio</span><span style="color:#f8f8f2;">(pio), </span><span style="color:#ffffff;">pin_tx</span><span style="color:#f8f8f2;">(pin_tx), </span><span style="color:#ffffff;">buffer</span><span style="color:#f8f8f2;">(buffer), </span><span style="color:#ffffff;">length</span><span style="color:#f8f8f2;">(length) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint</span><span style="color:#f8f8f2;"> offset </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">pio_add_program</span><span style="color:#f8f8f2;">(pio, </span><span style="color:#ff79c6;">&amp;</span><span style="color:#f8f8f2;">ws2812_program); </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">ws2812_program_init</span><span style="color:#f8f8f2;">(pio, sm, offset, pin_tx, </span><span style="color:#bd93f9;">800000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">clear</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> i </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; i </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> length; i</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> buffer[i] </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> i </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; i </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> length; i</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">pio_sm_put_blocking</span><span style="color:#f8f8f2;">(pio, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, buffer[i] </span><span style="color:#ff79c6;">&lt;&lt; </span><span style="color:#bd93f9;">8u</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">; </span></pre> <p>Those details are encapsulated in the <code>LedStrip</code> class. Besides storing information needed to control the PIO the class also stores an array buffer where each index represents the pixels on the LED strip.</p> <h2>From LED strip to LED grid</h2> <p>You may noticed the reference an LED strip instead of a grid a few times in this arcticle so far. This is due to, in reality, the LED grid being a LED strip where the way rows and columns are mapped can be unintuitive.</p> <p><img src="/img/2022/02/ledgrid.svg" alt="LED grid diagram" title="LED grid diagram" /></p> <p>Instead of following a left-right pattern across the whole grid, the LED strip snakes around the board, so that columns on even rows are refenced from left to right and columns on odd rows are referenced from right to left - while it would be expected for <code>(1, 0)</code> to reference the first LED of the second line, it actually points to the last one.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">class </span><span style="text-decoration:underline;color:#8be9fd;">LedGrid</span><span style="color:#f8f8f2;">: </span><span style="color:#ff79c6;">public </span><span style="text-decoration:underline;font-style:italic;color:#8be9fd;">LedStrip </span><span style="color:#ffffff;">{ </span><span style="color:#ff79c6;">public</span><span style="color:#f8f8f2;">: </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> width; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> height; </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">LedGrid</span><span style="color:#f8f8f2;">(PIO </span><span style="font-style:italic;color:#ffb86c;">pio</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#8be9fd;">int </span><span style="font-style:italic;color:#ffb86c;">pin_tx</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#66d9ef;">uint32_t </span><span style="font-style:italic;color:#ffb86c;">buffer</span><span style="color:#f8f8f2;">[], </span><span style="font-style:italic;color:#8be9fd;">int </span><span style="font-style:italic;color:#ffb86c;">width</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#8be9fd;">int </span><span style="font-style:italic;color:#ffb86c;">height</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">LedStrip</span><span style="color:#f8f8f2;">(pio, pin_tx, buffer, width </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> height), </span><span style="color:#ffffff;">width</span><span style="color:#f8f8f2;">(width), </span><span style="color:#ffffff;">height</span><span style="color:#f8f8f2;">(height) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">set_pixel</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int </span><span style="font-style:italic;color:#ffb86c;">x</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#8be9fd;">int </span><span style="font-style:italic;color:#ffb86c;">y</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#66d9ef;">uint32_t </span><span style="font-style:italic;color:#ffb86c;">color</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(x </span><span style="color:#ff79c6;">% </span><span style="color:#bd93f9;">2 </span><span style="color:#ff79c6;">== </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> buffer[x </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> height </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> y] </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> color; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ff79c6;">else </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> buffer[(x </span><span style="color:#ff79c6;">+ </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> height </span><span style="color:#ff79c6;">-</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">] </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> color; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">; </span></pre> <p>The <code>LedStrip</code> implementation can be extended into a <code>LedGrid</code> and implement a <code>set_pixel()</code> method that caters for that difference.</p> <h3>Testing the LED Grid</h3> <p>Before moving forward with enabling the serial port, it is possible to thes the LED grid by hard coding an image into the code:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">const </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> PANEL_WIDTH </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">const </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> PANEL_HEIGHT </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">const </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> PIN_TX </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">7</span><span style="color:#f8f8f2;">; </span><span style="color:#6272a4;">// The GPIO port controlling the LED grid. </span><span style="color:#ff79c6;">const </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> NUM_LEDS </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> PANEL_WIDTH </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> PANEL_HEIGHT; </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// The pixels for the Chrome logo. </span><span style="color:#ff79c6;">const </span><span style="font-style:italic;color:#66d9ef;">uint32_t</span><span style="color:#f8f8f2;"> CHROME_LOGO[</span><span style="color:#bd93f9;">256</span><span style="color:#f8f8f2;">] </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x011500</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x011500</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001100</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001300</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001200</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x011500</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001100</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001300</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001400</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x081500</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x081700</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x091800</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0A1900</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0B1B00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0C1D00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000B00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001200</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0D1E00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0E1E00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0E1E00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0E1F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F1F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000C00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x001100</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F1F00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x0F1F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x101F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x101F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x030200</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x05001C</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F091B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x1B1B1B</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x050001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x070001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x070001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x040001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x101E00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x050001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x070001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x080001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x070001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0F1B00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x070001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x070001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x060001</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x111F00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x0E1B00</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0x000000</span><span style="color:#f8f8f2;">, </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int </span><span style="color:#50fa7b;">main</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">stdio_init_all</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Buffer for holding the values for the LED strip. </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint32_t</span><span style="color:#f8f8f2;"> buffer[NUM_LEDS]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">auto</span><span style="color:#f8f8f2;"> ledGrid </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">LedGrid</span><span style="color:#f8f8f2;">(pio0, PIN_TX, buffer, PANEL_WIDTH, PANEL_HEIGHT); </span><span style="color:#f8f8f2;"> ledGrid</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">clear</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; x </span><span style="color:#ff79c6;">&lt; </span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">; x</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; y </span><span style="color:#ff79c6;">&lt; </span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">; y</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint32_t</span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> CHROME_LOGO[y </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> PANEL_WIDTH </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> x]; </span><span style="color:#f8f8f2;"> ledGrid</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">set_pixel</span><span style="color:#f8f8f2;">(x, y, color); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> ledGrid</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>Now, with everything connected, build the project and copy the <code>uf2</code> file to the Pico. Once it reboots, you shoudl see the Chrome logo rendered.</p> <p><img src="/img/2022/02/chrome-logo.jpg" alt="Chrome logo rendered on the LED grid" title="Chrome logo rendered on the LED grid" /></p> <h2>Enable UART and read data from the USB</h2> <p>The Pi Pico has 2 UART ports and, by default, those are enabled on the GPI pins. To enable UART over the USB port, the following lines need to be added to<code>CMakeLists.txt</code>:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"># enable usb output, disable uart output </span><span style="color:#f8f8f2;">pico_enable_stdio_usb(pico_ledstrip 1) </span><span style="color:#f8f8f2;">pico_enable_stdio_uart(pico_ledstrip 0) </span></pre> <p>The final step to setup the UART is to ensure the program is calling <code>stdio_init_all()</code> - this makes the standard I/O functions, like <code>printf()</code>, send data to the serial port instead.</p> <pre style="background-color:#282a36;"> <span style="color:#50fa7b;">stdio_init_all</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Buffer for reading values from stdinput. </span><span style="font-style:italic;color:#66d9ef;">uint8_t</span><span style="color:#f8f8f2;"> read_buffer[NUM_LEDS </span><span style="color:#ff79c6;">* </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">]; </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Buffer for holding the values for the LED strip. </span><span style="font-style:italic;color:#66d9ef;">uint32_t</span><span style="color:#f8f8f2;"> buffer[NUM_LEDS]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">auto</span><span style="color:#f8f8f2;"> ledGrid </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">LedGrid</span><span style="color:#f8f8f2;">(pio0, PIN_TX, buffer, PANEL_WIDTH, PANEL_HEIGHT); </span><span style="color:#ff79c6;">while </span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">true</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#8be9fd;">printf</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;Waiting for data</span><span style="color:#ff79c6;">\n</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#8be9fd;">fread</span><span style="color:#f8f8f2;">(read_buffer, </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, NUM_LEDS </span><span style="color:#ff79c6;">* </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">, stdin); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; x </span><span style="color:#ff79c6;">&lt; </span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">; x</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; y </span><span style="color:#ff79c6;">&lt; </span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">; y</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> start_index </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">(y </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> PANEL_WIDTH </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> x) </span><span style="color:#ff79c6;">* </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint8_t</span><span style="color:#f8f8f2;"> red </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> read_buffer[start_index]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint8_t</span><span style="color:#f8f8f2;"> green </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> read_buffer[start_index </span><span style="color:#ff79c6;">+ </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint8_t</span><span style="color:#f8f8f2;"> blue </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> read_buffer[start_index </span><span style="color:#ff79c6;">+ </span><span style="color:#bd93f9;">2</span><span style="color:#f8f8f2;">]; </span><span style="color:#f8f8f2;"> ledGrid</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">set_pixel</span><span style="color:#f8f8f2;">(x, y, </span><span style="color:#50fa7b;">urgb_u32</span><span style="color:#f8f8f2;">(red, green, blue)); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> ledGrid</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#8be9fd;">printf</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;Received data</span><span style="color:#ff79c6;">\n</span><span style="color:#f1fa8c;">&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#ffffff;">} </span></pre> <p>A new buffer, <code>read_buffer</code>, is introduced to read data from the serial port. Then, <code>LedGrid</code> is initialized with the PIO port used, the pin connected to the LED grid data line, the buffer, and the width and height for the LED strip.</p> <p>It then enters an infinite loop where the program blocks on the <code>fread()</code> call until <code>read_buffer</code> is full, then sets the colours to the LED strip via <code>set_pixel()</code>. Finally, it updates the display by calling <code>update()</code>.</p> <h2>Next Step</h2> <p>You now have an application that will run on the Raspberry Pi Pico and control the LED Grid. You can <a href="https://github.com/andreban/pico-ledgrid/releases/download/0.1.0/ledtrip_controller.uf2">download a pre-build <code>uf2</code></a> and build your own, then copy the <code>uf2</code> file to the Pico. Then, with the pico connected to your computer, navigate to <a href="https://ledmoji.bandarra.me/">https://ledmoji.bandarra.me/</a> and click <code>Connect</code>. You should be able to select your Pico and control the connected LED grid.</p> <p>On the next part, you will learn how to connect to the Pico using WebSerial! Stay tuned! And, in the meantime, check out the full code for the <a href="https://github.com/andreban/pico-ledgrid">project on Github</a>.</p> Control an LED grid with a Raspberry Pi Pico, Web Serial, and Firebase. This 3-part tutorial shows how to control a 256 LED grid using the Pico's PIO, send data via Web Serial, and enable remote control with Firebase. Learn bit-banging techniques and build a web interface for your LED project. Get started now! Writing Doom Fire for the Raspberry Pi Pico and the Pimoroni Pico Display 2021-02-23T00:00:00Z https://bandarra.me/posts/Doom-Fire-on-the-Raspberry-Pi-Pico <p><img src="/img/2021/02/pico-fire.jpg" alt="Doom Fire running on a Pi Pico" title="Doom Fire running on a Pi Pico" /></p> <h2>Introduction</h2> <p>The <a href="https://fabiensanglard.net/doom_fire_psx/">Doom Fire animation</a> is fire animation used for the PSX port of the original Doom game. This animation is a nice <em>Hello World</em> to implement when learning new graphics APIs, and I recently wrote about a <a href="/2021/01/13/Building-Doom-Fire-using-modern-JavaScript/">modern JavaScript implementation</a>.</p> <p>The <a href="https://www.raspberrypi.org/products/raspberry-pi-pico/">Raspberry Pi Pico</a> is a new board, based on the new RP2040 microcontroller and, along with the <a href="https://shop.pimoroni.com/products/pico-display-pack">Pimoroni Pico Display</a> makes an interesting platform to port the Doom Fire animation to.</p> <h2>Using MicroPython</h2> <p><a href="https://micropython.org/">MicroPython</a> is an implementation of <a href="https://www.python.org/">Python 3</a> that is optimised to run on microcontrollers.</p> <p>The nice thing about MicroPython is how beginner friendly it is, as it only requires flashing a custom image and installing the <a href="https://thonny.org/">Thonny IDE</a>. The details on how to get started have been extensively covered by the <a href="https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-python-sdk.pdf">official documentation</a>, blogposts, and YouTube videos, so I won't repeat those here. I do, however, wonder why the official documentation is only available as PDF file, and not as an HTML page though.</p> <p>Pimoroni has also done a great job and provides a custom firmware that makes it <a href="https://learn.pimoroni.com/tutorial/hel/getting-started-with-pico">a breeze to use the Pico Display from MicroPython</a> and a <a href="https://github.com/pimoroni/pimoroni-pico/tree/main/micropython/examples/pico_display">set of examples for the display</a>.</p> <p>If you interested on the final MicroPython implementation, <a href="https://github.com/andreban/pico-fire/blob/main/pico_fire.py">check out the source code on GitHub</a>.</p> <p>In my first attempt of the implementation, I had created separate methods for updating the fire, with the <code>update()</code> method and rendering the outcome, with the <code>render()</code> method:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">def </span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">y </span><span style="color:#ff79c6;">in </span><span style="color:#8be9fd;">range</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, height): </span><span style="color:#f8f8f2;"> row </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">y </span><span style="color:#ff79c6;">* </span><span style="color:#f8f8f2;">width </span><span style="color:#f8f8f2;"> next_row </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">(y </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">* </span><span style="color:#f8f8f2;">width </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">x </span><span style="color:#ff79c6;">in </span><span style="color:#8be9fd;">range</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, width): </span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">fire[row </span><span style="color:#ff79c6;">+ </span><span style="color:#f8f8f2;">x] </span><span style="color:#f8f8f2;"> pen </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">colorScale[color] </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">x </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">color </span><span style="color:#ff79c6;">&gt; </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">: </span><span style="color:#f8f8f2;"> rand </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">random</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">randint</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">color </span><span style="color:#ff79c6;">- </span><span style="color:#f8f8f2;">(rand </span><span style="color:#ff79c6;">&amp; </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">new_x </span><span style="color:#ff79c6;">+ </span><span style="color:#f8f8f2;">rand </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1 </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">fire[next_row </span><span style="color:#ff79c6;">+ </span><span style="color:#f8f8f2;">new_x] </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">color </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">def </span><span style="color:#50fa7b;">render</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">self</span><span style="color:#f8f8f2;">): </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">y </span><span style="color:#ff79c6;">in </span><span style="color:#8be9fd;">range</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, height): </span><span style="color:#f8f8f2;"> row </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">y </span><span style="color:#ff79c6;">* </span><span style="color:#f8f8f2;">width </span><span style="color:#f8f8f2;"> next_row </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">(y </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">* </span><span style="color:#f8f8f2;">width </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">x </span><span style="color:#ff79c6;">in </span><span style="color:#8be9fd;">range</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, width): </span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">fire[row </span><span style="color:#ff79c6;">+ </span><span style="color:#f8f8f2;">x] </span><span style="color:#f8f8f2;"> pen </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">colorScale[color] </span><span style="color:#f8f8f2;"> display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">set_pen</span><span style="color:#f8f8f2;">(pen) </span><span style="color:#f8f8f2;"> display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">pixel</span><span style="color:#f8f8f2;">(x, y) </span><span style="color:#f8f8f2;"> display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span></pre> <p>This implementation works and was quick to implement, even with almost no experience with Python programming. The problem is that this implementation takes almost <strong>4 seconds</strong> to render each frame. Yes, that's <strong>0.25 frames per second (FPS)</strong>.</p> <p>The most obvious place to optimise is avoid looping over the pixels for the fire twice and implement updating and rendering at the same time, and merge the <code>render()</code> into <code>update()</code>:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">def update(self): </span><span style="color:#f8f8f2;"> for y in range(0, height): </span><span style="color:#f8f8f2;"> row = y * width </span><span style="color:#f8f8f2;"> next_row = (y - 1) * width </span><span style="color:#f8f8f2;"> for x in range(0, width): </span><span style="color:#f8f8f2;"> color = self.fire[row + x] </span><span style="color:#f8f8f2;"> pen = colorScale[color] </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> if y &gt; 0: </span><span style="color:#f8f8f2;"> new_x = x </span><span style="color:#f8f8f2;"> if color &gt; 0: </span><span style="color:#f8f8f2;"> rand = random.randint(0, 3) </span><span style="color:#f8f8f2;"> color = color - (rand &amp; 1) </span><span style="color:#f8f8f2;"> new_x = new_x + rand - 1 </span><span style="color:#f8f8f2;"> self.fire[next_row + new_x] = color </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> display.set_pen(pen) </span><span style="color:#f8f8f2;"> display.pixel(x, y) </span><span style="color:#f8f8f2;"> display.update() </span></pre> <p>This cut the time to render to <strong>2 seconds</strong>. That's a great improvement, but not nearly enough to run at the 27 FPS required by the Doom Fire animation.</p> <p>At this point, I found unlikely that it would be worth working on improving the Python animation, but I also found unlikely that the Pico couldn't run fast enough to implement it. My guess was that MicroPython had a larger overhead than I expected.</p> <h2>Using C++</h2> <p>While the C++ process is also <a href="https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf">well documented</a> (also as a PDF), I can't say it's as easy as getting started with MicroPython and does require installing a toolchain with a small set of tools. The documentation also covers setting up using difference IDEs. In my case, I have used CLion.</p> <p>Rewriting the latest Python code in C++ looks like the following:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">uint32_t </span><span style="font-style:italic;color:#ffb86c;">time</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; y </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">HEIGHT; y</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> row </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> next_row </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">== </span><span style="color:#bd93f9;">0 </span><span style="color:#ff79c6;">? </span><span style="color:#bd93f9;">0 </span><span style="color:#ff79c6;">: </span><span style="color:#f8f8f2;">(y </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; x </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; x</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint8_t</span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> fire[row </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> x]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint16_t</span><span style="color:#f8f8f2;"> pen </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> pallete[color]; </span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setPen</span><span style="color:#f8f8f2;">(pen); </span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setPixel</span><span style="color:#f8f8f2;">(x, y); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(y </span><span style="color:#ff79c6;">&gt; </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> x; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> rand </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">std</span><span style="color:#ff79c6;">::</span><span style="color:#8be9fd;">rand</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">% </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">(new_x </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> rand </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">&gt; </span><span style="color:#bd93f9;">0 </span><span style="color:#ff79c6;">?</span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">- </span><span style="color:#f8f8f2;">(rand </span><span style="color:#ff79c6;">&amp; </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">: </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> fire[next_row </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> new_x] </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> color; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>From the start this code at ~20 FPS, or around 50 ms per frame, which is a huge improvement over MicroPython but still not our 27 FPS target.</p> <p>Since we're not worried with a high quality random number generator, it felt that a faster generator could help. A quick Google search took me to <a href="https://stackoverflow.com/a/26237777/1249994">this StackOverflow answer</a>, which promises being 2x the speed of <code>std:random()</code>:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">void update(uint32_t time) { </span><span style="color:#f8f8f2;"> for (int y = 0; y &lt; pimoroni::PicoDisplay::HEIGHT; y++) { </span><span style="color:#f8f8f2;"> int row = y * pimoroni::PicoDisplay::WIDTH; </span><span style="color:#f8f8f2;"> int next_row = y == 0 ? 0 : (y - 1) * pimoroni::PicoDisplay::WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> for (int x = 0; x &lt; pimoroni::PicoDisplay::WIDTH; x++) { </span><span style="color:#f8f8f2;"> uint8_t color = fire[row + x]; </span><span style="color:#f8f8f2;"> uint16_t pen = pallete[color]; </span><span style="color:#f8f8f2;"> pico_display.setPen(pen); </span><span style="color:#f8f8f2;"> pico_display.setPixel(x, y); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> if (y &gt; 0) { </span><span style="color:#f8f8f2;"> int new_x = x; </span><span style="color:#f8f8f2;"> int rand = fast_rand() % 3; </span><span style="color:#f8f8f2;"> new_x = (new_x + rand - 1); </span><span style="color:#f8f8f2;"> color = color &gt; 0 ? color - (rand &amp; 1) : 0; </span><span style="color:#f8f8f2;"> fire[next_row + new_x] = color; </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> pico_display.update(); </span><span style="color:#f8f8f2;">} </span></pre> <p>And, indeed, it it improved rendering to about 37ms per frame, exacly the 27 FPS we needed.</p> <h3>Adding Wind</h3> <p>The random number generated is an integer number between <code>0</code> and <code>2</code> (inclusive) that controls how the fire in a given cell is spread:</p> <ul> <li><code>0</code> - fire spreads to the cell above and to left of the current cell.</li> <li><code>1</code> - fire spreads to the cell directly above the current cell.</li> <li><code>2</code> - fire spreads to the cell above and to the right of the current cell.</li> </ul> <p>Adding wind means that we want to add a bias to this number. If a negative bias is added, the fire will spread more to the left and if a positive bias is added, the fire will spread more to the right.</p> <p>To control the wind, we are going to use the <code>B</code> button to add wind to the left and the <code>Y</code> button to add wind to the right.</p> <p>Checking if a button is pressed on the Pico Display can be done with a call to <code>pico_display::is_pressed()</code>:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">is_pressed</span><span style="color:#f8f8f2;">(pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">X)) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Add button handler code here. </span><span style="color:#ffffff;">} </span></pre> <p>The problem with this method is that, since we run this every frame, the wind will increase very quickly, even when pressing the button for a short period of time.</p> <p>Instead, what we want, is to increase/decrease the wind when it button gets pressed - more cleary, when it changes state from "not pressed" to "pressed":</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">bool</span><span style="color:#f8f8f2;"> y_pressed </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">; </span><span style="font-style:italic;color:#8be9fd;">bool</span><span style="color:#f8f8f2;"> b_pressed </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">while </span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">true</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">!</span><span style="color:#f8f8f2;">y_pressed </span><span style="color:#ff79c6;">&amp;&amp;</span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">is_pressed</span><span style="color:#f8f8f2;">(pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">Y)) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Button Y changed state from &quot;not pressed&quot; to &quot;pressed&quot;. </span><span style="color:#f8f8f2;"> wind</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> y_pressed </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">is_pressed</span><span style="color:#f8f8f2;">(pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">Y); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">!</span><span style="color:#f8f8f2;">b_pressed </span><span style="color:#ff79c6;">&amp;&amp;</span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">is_pressed</span><span style="color:#f8f8f2;">(pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">B)) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Button B changed state from &quot;not pressed&quot; to &quot;pressed&quot;. </span><span style="color:#f8f8f2;"> wind</span><span style="color:#ff79c6;">--</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> b_pressed </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">is_pressed</span><span style="color:#f8f8f2;">(pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">B); </span><span style="color:#ffffff;">} </span></pre> <p>We can then apply wind to our logic:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">void update(uint32_t time) { </span><span style="color:#f8f8f2;"> for (int y = 0; y &lt; pimoroni::PicoDisplay::HEIGHT; y++) { </span><span style="color:#f8f8f2;"> int row = y * pimoroni::PicoDisplay::WIDTH; </span><span style="color:#f8f8f2;"> int next_row = y == 0 ? 0 : (y - 1) * pimoroni::PicoDisplay::WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> for (int x = 0; x &lt; pimoroni::PicoDisplay::WIDTH; x++) { </span><span style="color:#f8f8f2;"> uint8_t color = fire[row + x]; </span><span style="color:#f8f8f2;"> uint16_t pen = pallete[color]; </span><span style="color:#f8f8f2;"> pico_display.setPen(pen); </span><span style="color:#f8f8f2;"> pico_display.setPixel(x, y); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> if (y &gt; 0) { </span><span style="color:#f8f8f2;"> int new_x = x; </span><span style="color:#f8f8f2;"> int rand = fast_rand() % 3; </span><span style="color:#f8f8f2;"> new_x = (new_x + rand - 1 + wind); </span><span style="color:#f8f8f2;"> if (new_x &gt;= pimoroni::PicoDisplay::WIDTH) { </span><span style="color:#f8f8f2;"> new_x = new_x - pimoroni::PicoDisplay::WIDTH; </span><span style="color:#f8f8f2;"> } else if (new_x &lt; 0) { </span><span style="color:#f8f8f2;"> new_x = new_x + pimoroni::PicoDisplay::WIDTH; </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> color = color &gt; 0 ? color - (rand &amp; 1) : 0; </span><span style="color:#f8f8f2;"> fire[next_row + new_x] = color; </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> pico_display.update(); </span><span style="color:#f8f8f2;">} </span></pre> <p>Another modification is that we now "wrap around" the fire spread: If a pixel at the first column spreads to the left, we teleport that pixel to the last column and if a pixel at the last column spreads to the right, we teleport that to the first column.</p> <h3>More perf improvements</h3> <p>These extra checks mean that our FPS to a hit again, and we're now back to 21 FPS. The next improvement is a trick around the <code>pico_graphics</code> API.</p> <p>When <code>setPixel(x, y)</code> is called, the API will check boundaries to ensure that the values are not written outside the <code>frame_buffer</code> boundaries. In our case, and after implementing the "wrap around" for the wind, we <strong>know</strong> we will never write outside the boundaries.</p> <p>So, instead of calling <code>setPixel(x, y</code>), we invoke the <code>ptr(x, y)</code> function, which allows manipulating the framebuffer directly, skipping the boundary validation:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">uint32_t </span><span style="font-style:italic;color:#ffb86c;">time</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; y </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">HEIGHT; y</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> row </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> next_row </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> y </span><span style="color:#ff79c6;">== </span><span style="color:#bd93f9;">0 </span><span style="color:#ff79c6;">? </span><span style="color:#bd93f9;">0 </span><span style="color:#ff79c6;">: </span><span style="color:#f8f8f2;">(y </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> x </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; x </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; x</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint8_t</span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> fire[row </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> x]; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">uint16_t</span><span style="color:#f8f8f2;"> pen </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> pallete[color]; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">*</span><span style="color:#f8f8f2;">pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">ptr</span><span style="color:#f8f8f2;">(x, y) </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> pen; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(y </span><span style="color:#ff79c6;">&gt; </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> x; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> rand </span><span style="color:#ff79c6;">= </span><span style="color:#50fa7b;">fast_rand</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">% </span><span style="color:#bd93f9;">3</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">(new_x </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> rand </span><span style="color:#ff79c6;">- </span><span style="color:#bd93f9;">1 </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> wind); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(new_x </span><span style="color:#ff79c6;">&gt;=</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">-</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ff79c6;">else if </span><span style="color:#f8f8f2;">(new_x </span><span style="color:#ff79c6;">&lt; </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> new_x </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> pimoroni</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">PicoDisplay</span><span style="color:#ff79c6;">::</span><span style="color:#f8f8f2;">WIDTH; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">&gt; </span><span style="color:#bd93f9;">0 </span><span style="color:#ff79c6;">?</span><span style="color:#f8f8f2;"> color </span><span style="color:#ff79c6;">- </span><span style="color:#f8f8f2;">(rand </span><span style="color:#ff79c6;">&amp; </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">: </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> fire[next_row </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> new_x] </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> color; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> pico_display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>This got us over 40 FPS, which is more than 27 FPS required by doom-fire. Yay!</p> <h2>Conclusion</h2> <p>The Raspberry Pi Pico and the Pico Display are incredibly fun to play with.</p> <p>While MicroPython is easy to get started and prototype, it has a significant performance cost.</p> <p>C/C++ is more complex to setup and probably has a steeper learning curve, but it can payoff if the extra performance is needed.</p> <p>I'm not an expert in Python or C++ but, if you want to check out the code, head over to the <a href="https://github.com/andreban/pico-fire/">Github repo</a> and drop issues or even pull-requests.</p> Run Doom Fire animation on a Raspberry Pi Pico using MicroPython or C++. MicroPython is beginner-friendly but slower; C++ offers significant performance improvements, achieving over 40 FPS. This tutorial details both implementations, optimization techniques, and adding wind effects, providing code examples and addressing performance bottlenecks. webadb - ADB over WebUSB resources. 2021-02-04T00:00:00Z https://bandarra.me/posts/webadb-Adb-Over-Usb-Resources <p>Most Android Developers will be familiar with the <a href="https://developer.android.com/studio/command-line/adb">adb command-line tool</a>. It allows developers to connect their development computer to an Android device and run a variety of actions, like installing, starting or stopping an application, pushing and pulling files, taking screenshots, or recording the screen.</p> <p>The introduction of <a href="https://web.dev/usb/">WebUSB</a> to the Web Platform opens the possibility of using ADB via a web page. This post contains some resources for developers looking for more information on using ADB over USB (or webadb).</p> <h2>Implementations</h2> <ul> <li><a href="https://github.com/GoogleChromeLabs/wadb/">wadb</a>: my own implementation of webadb, in TypeScript. Created as a demo / exploration of implementing ADB over WebUSB. Currently used by <a href="http://screenrecord.bandarra.me/">screenrecord.bandarra.me</a></li> <li><a href="https://github.com/webadb/webadb.js">webadb.js</a>: the oldest implementation of webadb that I'm aware of. The wadb implementation is largely based on this implementation.</li> <li><a href="https://github.com/yume-chan/ya-webadb/">ya-webadb</a>: a newer implementation, also in TypeScript. Powering <a href="https://app.webadb.com/">app.webadb.com</a>. Seems to also have a <a href="https://github.com/yume-chan/ya-webadb/tree/master/packages/adb-backend-ws">WebSocket implementation of the transport layer</a>, but is not enabled on the demo site.</li> </ul> <h2>ADB Protocol Documentation</h2> <p>Besides reading the code and contributing to existing implementations, you may want to check other resources:</p> <ul> <li><a href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/">ADB Internals</a>: the official Android source repository. Contains both the C++ implementation of ADB as well as a set of text files describing various sections of the protocol: <ul> <li><a href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/README.md">README.md</a></li> <li><a href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/OVERVIEW.TXT">OVERVIEW.TXT</a></li> <li><a href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/protocol.txt">protocol.txt</a></li> </ul> </li> <li><a href="https://github.com/cstyan/adbDocumentation">github.com/cstyan/adbDocumentation</a>: an unnoficial documentation of the protocol. May be easier to read than the official documents.</li> </ul> Use ADB over WebUSB! This guide explores using the Android Debug Bridge (ADB) via WebUSB, detailing implementations like wadb and ya-webadb, and providing links to crucial ADB protocol documentation for seamless web-based Android device interaction. Learn how to control your Android device from your browser. Best practices for using the Wake Lock API. 2021-01-25T00:00:00Z https://bandarra.me/posts/Best-practices-for-using-the-Wake-Lock-API <p>The <a href="https://web.dev/wake-lock/">Wake Lock API</a> provides a way to prevent devices from dimming or locking the screen when an application needs to keep running.</p> <p>I used an earlier version of the API on the <a href="https://github.com/GoogleChromeLabs/rowing-monitor">Rowing Monitor</a> project and the final version more recently for <a href="https://doom-fire.com">doom-fire</a>.</p> <p>Those two applications have slightly different implementations from a user perspective, which made me think of a couple of best practices:</p> <h2>Be mindful of the user's battery life</h2> <p>This is <a href="https://web.dev/wake-lock/#best-practices">part of the Wake Lock documentation</a>, but it's never much to repeat: There's a good reason on why devices will dimm or turn off the screen after a few seconds - a device's display is one of the components that draws the largest amount of power from the battery.</p> <p>This means that, to avoid wasting battery, applications should only request a wake lock when there's a clear benefit to the user.</p> <p>Before implementing the API, a good question to ask is: Does the user really need the device to stay awake? Here are some things consider that may help answering this question:</p> <ul> <li>Will the user consume the on-screen content for long periods?</li> <li>Will tapping the screen multiple times just to keep it awake severily degrade the user experience?</li> <li>Is the user unable to tap the screen while using the application?</li> <li>What are the parts of the application where the Wake Lock is required? Restrict the implementation to only those parts.</li> </ul> <h2>Avoid making users think about Wake Lock (when possible)</h2> <p>On the <a href="https://pm5-monitor-c63a2.firebaseapp.com/index.html">rowing monitor</a> application, it's clear that the user will want to keep track of their exercise on the screen once they start and while they are busy doing the exercise.</p> <p>This gives us a hint that we can request the Wake Lock when the user starts the exercise and release it when they stop. The device will stay awake while we know the user needs it, and only while they need it. But the user never has to think about keeping the device awake!</p> <p>A good question to ask is:</p> <ul> <li>Is there a natural moment where users will need to keep the screen awake and the Wake Lock API can be seamlessy integrated into the user experience?</li> </ul> <p>However, this is not always possible. In <a href="https://doom-fire.com">doom-fire</a>, for example, there's no natural way to tell when the user wants to keep the screen awake, so a padlock is used as a control for the user to to request/release the Wake Lock.</p> Optimize Wake Lock API usage for improved user experience and battery life. Learn best practices for implementing the Wake Lock API, including minimizing battery drain and seamlessly integrating wake lock functionality into your app's user experience. Discover when to automatically manage wake lock and when to provide user controls. Integrating in-app-reviews with Trusted Web Activity 2021-01-18T00:00:00Z https://bandarra.me/posts/Integrating-in-app-reviews-with-Trusted-Web-Activity <p>I was reading the questions on the <a href="https://stackoverflow.com/questions/tagged/trusted-web-activity/"><code>trusted-web-activity</code></a> tag on StackOverflow, as I often do, when <a href="https://stackoverflow.com/questions/65752429/how-can-i-extend-twa-application-with-in-app-review">a question</a> asking if it is possible to integrate a <a href="https://developers.google.com/web/android/trusted-web-activity/">Trusted Web Activity</a> with <a href="https://developer.android.com/guide/playcore/in-app-review"><code>in-app-reviews</code></a> caught my attention. In short, the answer is <strong>yes, it’s possible to do it</strong>, but there are caveats.</p> <h1>Ok, tell me how to do it!</h1> <p>Let’s assume an application bootstrapped with <a href="https://www.npmjs.com/package/@bubblewrap/cli">Bubblewrap</a> and go over the changes needed to that application to implement in-app-reviews. I you are new to Trusted Web Activity, I do recommend reading the <a href="https://developers.google.com/web/android/trusted-web-activity/">documentation</a>.</p> <p>The idea is to use a custom schema, like <code>my-app://</code> that is handled by an <a href="https://developer.android.com/reference/android/app/Activity">Android Activity</a>. This Activity will, in turn, launch the review flow and then finish itself.</p> <p>The reason we need a custom schema is that internal URLs will trigger navigation inside the Trusted Web Activity and we want to get the user back to the Android part of the app for the review flow.</p> <h2>Step 1: Add the in-app-reviews dependency</h2> <p>You will need to add the <code>com.google.android.play:core</code> dependency to <code>app/build.gradle</code>. After adding it, the <code>dependencies</code> section should look like the following:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">dependencies { </span><span style="color:#f8f8f2;"> implementation fileTree(</span><span style="color:#bd93f9;">include</span><span style="color:#f8f8f2;">: [</span><span style="color:#f1fa8c;">&#39;*.jar&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#bd93f9;">dir</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&#39;libs&#39;</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> implementation </span><span style="color:#f1fa8c;">&#39;com.google.androidbrowserhelper:androidbrowserhelper:2.1.0&#39; </span><span style="color:#f8f8f2;"> implementation </span><span style="color:#f1fa8c;">&#39;com.google.android.play:core:1.9.0&#39; </span><span style="color:#f8f8f2;">} </span></pre> <h2>Step 2: Create a ReviewActivity</h2> <p>Add a <code>ReviewActivity.java</code> file to the same folder where you will find <code>Application.java</code>, <code>LauncherActivity.java</code> and others, and implement in-app-reviews in this Activity.</p> <p>This Activity won't have any UI and you'll only use its <code>onCreate()</code> method to launch the review flow:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">package </span><span style="color:#f8f8f2;">com.doom_fire.twa; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">android.app</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">Activity</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">android.os</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">Bundle</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">android.util</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">Log</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">androidx.annotation</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">NonNull</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">androidx.annotation</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">Nullable</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">com.google.android.play.core.review</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">ReviewInfo</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">com.google.android.play.core.review</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">ReviewManager</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">com.google.android.play.core.review</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">ReviewManagerFactory</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">com.google.android.play.core.tasks</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">Task</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">class </span><span style="text-decoration:underline;color:#8be9fd;">ReviewActivity </span><span style="color:#ff79c6;">extends </span><span style="text-decoration:underline;font-style:italic;color:#8be9fd;">Activity </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">private static final </span><span style="font-style:italic;color:#66d9ef;">String </span><span style="color:#f8f8f2;">TAG </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;ReviewActivity&quot;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">private </span><span style="font-style:italic;color:#66d9ef;">ReviewManager</span><span style="color:#f8f8f2;"> mReviewManager; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> @Override </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">protected </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">onCreate</span><span style="color:#f8f8f2;">(@Nullable </span><span style="font-style:italic;color:#66d9ef;">Bundle </span><span style="font-style:italic;color:#ffb86c;">savedInstanceState</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">super</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">onCreate</span><span style="color:#f8f8f2;">(savedInstanceState); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Log</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">TAG</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;Review Activity started.&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">startReview</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">startReview</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> mReviewManager </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">ReviewManagerFactory</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">create</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Log</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">TAG</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;Requesting Review flow.&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Task</span><span style="color:#f8f8f2;">&lt;</span><span style="font-style:italic;color:#66d9ef;">ReviewInfo</span><span style="color:#f8f8f2;">&gt; request </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;"> mReviewManager</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">requestReviewFlow</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> request</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">addOnCompleteListener</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">task </span><span style="font-style:italic;color:#8be9fd;">-&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(task</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">isSuccessful</span><span style="color:#f8f8f2;">()) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Log</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">TAG</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;Review Flow request succeeded.&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">launchReviewFlow</span><span style="color:#f8f8f2;">(task</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getResult</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ff79c6;">else </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Log</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">TAG</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;Review Flow request failed. Finishing.&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">finish</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">private </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">launchReviewFlow</span><span style="color:#f8f8f2;">(@NonNull </span><span style="font-style:italic;color:#66d9ef;">ReviewInfo </span><span style="font-style:italic;color:#ffb86c;">reviewInfo</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Log</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">TAG</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;Launching review flow.&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Task</span><span style="color:#f8f8f2;">&lt;</span><span style="font-style:italic;color:#66d9ef;">Void</span><span style="color:#f8f8f2;">&gt; reviewFlow </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;"> mReviewManager</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">launchReviewFlow</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#f8f8f2;">, reviewInfo); </span><span style="color:#f8f8f2;"> reviewFlow</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">addOnCompleteListener</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">task </span><span style="font-style:italic;color:#8be9fd;">-&gt; </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Log</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">d</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">TAG</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&quot;Review flow finished. Finishing Activity.&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">finish</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">}</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span></pre> <h2>Step 3</h2> <p>Add the Activity you just created to <code>AndroidManifest.xml</code>. In the example below, we are using <code>doom-fire</code> as the schema and <code>review</code> as the host. This means that, in our web application, we'll link to <code>doom-fire://review</code> to trigger the ReviewActivity. Make sure to change this to something that suits your app.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> &lt;</span><span style="color:#ff79c6;">activity </span><span style="color:#50fa7b;">android</span><span style="color:#ff79c6;">:</span><span style="color:#50fa7b;">name</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;.ReviewActivity&quot; </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">android</span><span style="color:#ff79c6;">:</span><span style="color:#50fa7b;">theme</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;@android:style/Theme.Translucent.NoTitleBar&quot;</span><span style="color:#f8f8f2;">&gt; </span><span style="color:#f8f8f2;"> &lt;</span><span style="color:#ff79c6;">intent-filter</span><span style="color:#f8f8f2;">&gt; </span><span style="color:#f8f8f2;"> &lt;</span><span style="color:#ff79c6;">action </span><span style="color:#50fa7b;">android</span><span style="color:#ff79c6;">:</span><span style="color:#50fa7b;">name</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;android.intent.action.VIEW&quot;</span><span style="color:#f8f8f2;">/&gt; </span><span style="color:#f8f8f2;"> &lt;</span><span style="color:#ff79c6;">category </span><span style="color:#50fa7b;">android</span><span style="color:#ff79c6;">:</span><span style="color:#50fa7b;">name</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;android.intent.category.DEFAULT&quot; </span><span style="color:#f8f8f2;">/&gt; </span><span style="color:#f8f8f2;"> &lt;</span><span style="color:#ff79c6;">category </span><span style="color:#50fa7b;">android</span><span style="color:#ff79c6;">:</span><span style="color:#50fa7b;">name</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;android.intent.category.BROWSABLE&quot; </span><span style="color:#f8f8f2;">/&gt; </span><span style="color:#f8f8f2;"> &lt;</span><span style="color:#ff79c6;">data </span><span style="color:#50fa7b;">android</span><span style="color:#ff79c6;">:</span><span style="color:#50fa7b;">scheme</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;doom-fire&quot; </span><span style="color:#50fa7b;">android</span><span style="color:#ff79c6;">:</span><span style="color:#50fa7b;">host</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;review&quot; </span><span style="color:#f8f8f2;">/&gt; </span><span style="color:#f8f8f2;"> &lt;/</span><span style="color:#ff79c6;">intent-filter</span><span style="color:#f8f8f2;">&gt; </span><span style="color:#f8f8f2;"> &lt;/</span><span style="color:#ff79c6;">activity</span><span style="color:#f8f8f2;">&gt; </span></pre> <h2>Step 4: Add a link to the web application:</h2> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">&lt;</span><span style="color:#ff79c6;">a </span><span style="color:#50fa7b;">href</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;doom-fire://review&quot;</span><span style="color:#f8f8f2;">&gt;Rate app now!&lt;/</span><span style="color:#ff79c6;">a</span><span style="color:#f8f8f2;">&gt; </span></pre> <h2>The result</h2> <p><video src="/img/2021/01/in-app-review.mp4" controls poster="/img/2021/01/in-app-review-cover.png"></video></p> <h1>Caveats</h1> <ol> <li> <p>The link above will only work when the web app is running inside a Trusted Web Activity. <a href="https://stackoverflow.com/questions/54580414/how-can-i-detect-if-my-website-is-opened-inside-a-trusted-web-actvity">This question</a> explains how to detect this.</p> </li> <li> <p>Due to the way Trusted Web Activities on ChromeOS works, this solution may not work on the platform.</p> </li> <li> <p>The app review flow will only work if the application has been deployed to the Play Store, which can making testing a bit tricky.</p> </li> </ol> <h1>The future</h1> <p>Reviews and ratings have been available to platform-specific developers for a long time and not only allow users to express their happiness (or the lack of it), but are also a tool that shortens the feedback cycle for developers and provides another metric businesses can use to benchmark against their competitors.</p> <p>It’s interesting to think what such tool would mean for the web. If you are interested in the subject, the <a href="https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/RatingsAndReviewsPrompt/explainer.md">Prompt for Rating/Review Explainer</a> proposes how a general API for this use-case could work.</p> Integrate in-app reviews into your Trusted Web Activity (TWA) using a custom schema. This tutorial shows you how to add the necessary Android dependency, create a ReviewActivity to handle the review flow, and add a link in your web app to trigger the review. Learn how to overcome caveats like ChromeOS compatibility and Play Store deployment requirements for testing. Building Doom Fire using Modern JavaScript 2021-01-13T00:00:00Z https://bandarra.me/posts/Building-Doom-Fire-using-modern-JavaScript <p><doom-fire style="width: 100%;height: 20vh;display: block;background-color: black"></doom-fire></p> <p><a href="https://doom-fire.com/">Doom Fire</a> is an animated fire used by some ports of Doom, and documented in <a href="https://fabiensanglard.net/doom_fire_psx/">Fabien Sanglard's blogpost</a>. Despite the animation looking cool, the code is simple and ideal for learning graphic APIs.</p> <p>The <a href="https://developers.google.com/web/updates/2018/08/offscreen-canvas">Offscreen Canvas API</a> allows for moving the animation code to a Worker, allowing the main thread to worry about more important things - like handling user input! I've been looking into trying out the API for a while and the Doom Fire animation sounded simple enough to allow focusing on how Offscreen Canvas works.</p> <p>This post will focus on the Offscreen Canvas and modern JavaScript aspects for the code. I do recommend Fabien's <a href="https://fabiensanglard.net/doom_fire_psx/">blogpost</a> if you just want to learn more about the animation or go straight to the <a href="/static/doom-fire-animation.mjs">source code</a>.</p> <h2>Browser support</h2> <p>As most modern APIs, it's a good idea to start the work by checking browser support. The Offscreen Canvas API has good support and includes Chrome, Samsung Internet, Edge, and a couple other browsers. <del>But it's still missing in Safari and Firefox.</del> <strong>Update:</strong> Since 2023, Offscreen Canvas is supported by all major browsers and a <a href="https://web.dev/baseline">Baseline</a> feature.</p> <p>Thankfully, the API is easy to feature test:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">canvas </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">document</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">querySelector</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;#canvas&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;OffscreenCanvas&quot; </span><span style="color:#ff79c6;">in </span><span style="font-style:italic;color:#66d9ef;">window</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Offscreen Canvas code goes here! </span><span style="color:#f8f8f2;">} </span></pre> <h2>Architecture considerations</h2> <p>Since <code>Offscreen Canvas</code> is not supported by all browsers, we'll want to use <a href="https://en.wikipedia.org/wiki/Progressive_enhancement">progressive enhancement</a> and use the API when it is available - this means that the animation will run on a worker thread when <code>Offscreen Canvas</code> is available and on the main thread when it isn't.</p> <p>With this information, we now know we have to decouple the code that runs the animation from the code that sets up the Canvas and add it to a module that can be used from the main thread or from the worker:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">export default </span><span style="font-style:italic;color:#ff79c6;">class </span><span style="text-decoration:underline;color:#8be9fd;">DoomFireAnimation </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">constructor</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">parent</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">canvas</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">parent </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">parent</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">canvas</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">ctx </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">canvas</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getContext</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;2d&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">... </span><span style="color:#6272a4;">// Finish setting up the animation. </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">start</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">parent</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">requestAnimationFrame</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">_update</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">bind</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">_update</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">... </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Run the Doom Fire animation then render the next frame. </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">parent</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">requestAnimationFrame</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">_update</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">bind</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span></pre> <p>In the snippet above, the constructor gets two variables:</p> <ol> <li>A reference to the context where the code is running, so we can call <code>requestAnimationFrame()</code> to render each frame. This will either be the <code>Window</code> object when the animation is running on the main thread or the <code>Worker</code> object when running off the main thread.</li> <li>A reference to the <code>Canvas</code> object that we will used to draw the animation.</li> </ol> <p>The Worker implementation is straightforward:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">import </span><span style="color:#ffffff;">DoomFireAnimation </span><span style="color:#ff79c6;">from </span><span style="color:#f1fa8c;">&#39;./doom-fire-animation.mjs&#39;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">let </span><span style="color:#ffffff;">doomFireAnimation</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">onmessage </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">function</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">ev</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">ev</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">msg </span><span style="color:#ff79c6;">=== </span><span style="color:#f1fa8c;">&#39;init&#39;</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">doomFireAnimation </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">DoomFireAnimation(</span><span style="color:#bd93f9;">self</span><span style="color:#f8f8f2;">, </span><span style="color:#ffffff;">ev</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">ev</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">msg </span><span style="color:#ff79c6;">=== </span><span style="color:#f1fa8c;">&#39;start&#39;</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">doomFireAnimation</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">doomFireAnimation</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">toggle</span><span style="color:#f8f8f2;">()</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">start</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;">} </span></pre> <p>The <code>Worker</code> can handle two types of messages: one to prepare the animation and another message that starts it. Those messages could be merged into a single one, but having separate events comes handy when wrapping the animation into a web component.</p> <p>Finally, we can put everything together in the application:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">canvas </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">document</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">querySelector</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;#canvas&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;OffscreenCanvas&quot; </span><span style="color:#ff79c6;">in </span><span style="font-style:italic;color:#66d9ef;">window</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">offscreenCanvas </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">transferControlToOffscreen</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">worker </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">Worker(</span><span style="color:#f1fa8c;">&#39;doom-fire-worker.js&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">worker</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">postMessage</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> {msg: </span><span style="color:#f1fa8c;">&#39;init&#39;</span><span style="color:#f8f8f2;">, canvas: </span><span style="color:#ffffff;">offscreenCanvas</span><span style="color:#f8f8f2;">}, [</span><span style="color:#ffffff;">offscreenCanvas</span><span style="color:#f8f8f2;">] </span><span style="color:#f8f8f2;"> ); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">worker</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">postMessage</span><span style="color:#f8f8f2;">({msg: </span><span style="color:#f1fa8c;">&#39;start&#39;</span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;">} </span><span style="color:#ff79c6;">else </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">animation </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">DoomFireAnimation(</span><span style="font-style:italic;color:#66d9ef;">window</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">animation</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">start</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;">} </span></pre> <p>When Offscreen Canvas is available, <code>transferControlToOffscreen()</code> transfers control of the canvas and then is passed to a Worker. We then send an <code>init</code> message with a reference to the Offscreen Canvas and start the animation.</p> <p>When not available, the <code>DoomFireAnimation</code> is created in the main thread, with a reference to the Window object and the canvas.</p> <h2>Wrapping everything in a Web Component</h2> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">export default </span><span style="font-style:italic;color:#ff79c6;">class </span><span style="text-decoration:underline;color:#8be9fd;">DoomFire </span><span style="color:#ff79c6;">extends </span><span style="text-decoration:underline;font-style:italic;color:#8be9fd;">HTMLElement </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">constructor</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="color:#8be9fd;">super</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Create our own Canvas! </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">document</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">createElement</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;canvas&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">offscreen </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&quot;OffscreenCanvas&quot; </span><span style="color:#ff79c6;">in </span><span style="font-style:italic;color:#66d9ef;">window</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Make the canvas use the whole element. </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">style</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">width </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&#39;100%&#39;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">style</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">height </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&#39;100%&#39;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">offscreen) { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;Rendering with Offscreen Canvas.&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">offscreenCanvas </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">transferControlToOffscreen</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">worker </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">Worker(</span><span style="color:#f1fa8c;">&#39;doom-fire-worker.js&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">worker</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">postMessage</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> {msg: </span><span style="color:#f1fa8c;">&#39;init&#39;</span><span style="color:#f8f8f2;">, canvas: </span><span style="color:#ffffff;">offscreenCanvas</span><span style="color:#f8f8f2;">}, [</span><span style="color:#ffffff;">offscreenCanvas</span><span style="color:#f8f8f2;">] </span><span style="color:#f8f8f2;"> ); </span><span style="color:#f8f8f2;"> } </span><span style="color:#ff79c6;">else </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;Rendering with regular Canvas.&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">animation </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">DoomFireAnimation(</span><span style="font-style:italic;color:#66d9ef;">window</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">shadowRoot </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">attachShadow</span><span style="color:#f8f8f2;">({mode: </span><span style="color:#f1fa8c;">&#39;open&#39;</span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">shadowRoot</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">appendChild</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">canvas); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">connectedCallback</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">offscreen) { </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">worker</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">postMessage</span><span style="color:#f8f8f2;">({msg: </span><span style="color:#f1fa8c;">&#39;start&#39;</span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> } </span><span style="color:#ff79c6;">else </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">animation</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">start</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">!</span><span style="color:#ffffff;">customElements</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">get</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;doom-fire&#39;</span><span style="color:#f8f8f2;">)) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">customElements</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">define</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;doom-fire&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#ffffff;">DoomFire</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;">} </span></pre> <p>And this is how the module can be added to the HTML:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">&lt;!</span><span style="color:#ff79c6;">doctype</span><span style="color:#f8f8f2;"> html&gt; </span><span style="color:#f8f8f2;">&lt;</span><span style="color:#ff79c6;">head</span><span style="color:#f8f8f2;">&gt; </span><span style="color:#f8f8f2;"> ... </span><span style="color:#f8f8f2;"> &lt;</span><span style="color:#ff79c6;">script </span><span style="color:#50fa7b;">type</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;module&quot; </span><span style="color:#50fa7b;">src</span><span style="color:#f8f8f2;">=</span><span style="color:#f1fa8c;">&quot;doom-fire.mjs&quot;</span><span style="color:#f8f8f2;">&gt; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">... </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f1fa8c;">/head&gt; </span><span style="color:#f1fa8c;">&lt;body&gt; </span><span style="color:#f1fa8c;"> &lt;doom-fire&gt;&lt;/</span><span style="color:#ffffff;">doom</span><span style="color:#ff79c6;">-</span><span style="color:#ffffff;">fire</span><span style="color:#ff79c6;">&gt; </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f1fa8c;">/body&gt; </span></pre> <h2>Performance</h2> <p>The Doom Fire animation is lightweight enough to run without effort in most devices, as it was originally created to run on devices like the PSX and the Nintendo 64.</p> <p>Still, checking out the fire charts on DevTools (pun intended) shows us how free the main thread gets with Offscreen Canvas:</p> <table><thead><tr><th style="text-align: center">Main Thread</th><th style="text-align: center">Offscreen Canvas</th></tr></thead><tbody> <tr><td style="text-align: center"><img src="/img/2021/01/doom-fire-main-thread.jpg" alt="Main Thread fire chart" title="Main Thread fire chart" /></td><td style="text-align: center"><img src="/img/2021/01/doom-fire-worker.jpg" alt="Worker fire chart" title="Worker fire chart" /></td></tr> </tbody></table> <h2>Where to go next</h2> <p>If you are a fan of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D">Canvas 2D API</a>, you may be interested to know that it is getting updates and improvements! Check out the recent Chrome Dev Summit by Aaron Krajeski talk to learn more!</p> <p><iframe width="800" height="450" style="width:100%;" src="https://www.youtube.com/embed/dfOKFSDG7IM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p> <script type=module src="/static/doom-fire.mjs"></script> Create a mesmerizing Doom Fire animation in your browser using modern JavaScript and the Offscreen Canvas API. This tutorial shows you how to implement the animation efficiently, offloading it to a worker thread for optimal performance and main thread responsiveness. Learn progressive enhancement techniques to ensure broad browser compatibility. Improve your web development skills with this engaging project. Building a Physical AirHorn Button with Web USB 2020-02-22T00:00:00Z https://bandarra.me/posts/Building-a-Physical-AirHorn-Button-with-Web-USB <p>During the holiday season, I decided to experiment with building a physical button for <a href="https://twitter.com/Paul_Kinlan">Paul Kinlan’s</a> <a href="https://airhorner.com/">AirHorn</a>. This blogpost provides the instructions and links so you can also build your own <a href="https://wicg.github.io/webusb/">WebUSB</a> powered AirHorn Button.</p> <p>What you Will Need</p> <ul> <li>An Arduino Device. I used an <a href="https://store.arduino.cc/arduino-nano-33-iot">Arduino Nano 33 IoT</a>.</li> <li>A Momentary button or Switch. I used this <a href="https://shop.pimoroni.com/products/massive-arcade-button-with-led-100mm-red">massive red button</a> from Pimoroni.</li> <li>A 10k Ohm Resistor</li> </ul> <h1>Getting Started</h1> <p>WebUSB is an API that securely provides access to USB devices from Web Pages. The API has been around for a while, and hass been available in Chrome <a href="https://developers.google.com/web/updates/2017/09/nic61">since version 61</a>.</p> <p>I didn’t know anything about the API and the first step was to figure out how hard this would be. Fortunately, Francois Beaufort wrote a handy <a href="https://developers.google.com/web/updates/2016/03/access-usb-devices-on-the-web">getting started guide</a>.</p> <h2>Hooking up a Button on the Arduino</h2> <p>The button was hooked up to the Arduino Board exactly as shown in the <a href="https://www.arduino.cc/en/tutorial/button">Arduino Button Tutorial</a>. The implementation uses the <a href="https://github.com/webusb/arduino">Arduino WebUSB library</a>. Make sure to follow the steps to setup the library on your development computer.</p> <p>The Arduino implementation is a slightly modified verson of the Arduino Button Tutorial to implement something analog to <code>keydown</code> and <code>keyup</code> events.</p> <p>When the pin voltage is <code>HIGH</code>, it means the button is pressed. When it is <code>LOW</code>, it means the button is not pressed. To create the desired behaviour we want to check when the button state changes from <code>LOW</code> to <code>HIGH</code>, meaning the button was pressed, and from <code>HIGH</code> to <code>LOW</code>, meaning the button was released.</p> <p>We send an <code>ON</code> message via the serial bus when the button is pressed and an <code>OFF</code> message when it is released.</p> <p>Here's what the code looks like:</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">#include &lt;WebUSB.h&gt; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">/** </span><span style="color:#f8f8f2;"> * Follow the instructions on https://github.com/webusb/arduino/ to install </span><span style="color:#f8f8f2;"> * the library and get the Arduino IDE to build and install it correctly. </span><span style="color:#f8f8f2;"> */ </span><span style="color:#f8f8f2;">WebUSB WebUSBSerial(1 /* https:// */, &quot;webusb-horn.firebaseapp.com&quot;); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">#define Serial WebUSBSerial </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">const int ledPin = 13; </span><span style="color:#f8f8f2;">const int buttonPin = 2; </span><span style="color:#f8f8f2;">int previousButtonState = 0; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">void setup() { </span><span style="color:#f8f8f2;"> while (!Serial) { </span><span style="color:#f8f8f2;"> ; </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> Serial.begin(9600); </span><span style="color:#f8f8f2;"> Serial.write(&quot;Sketch begins.\r\n&gt; &quot;); </span><span style="color:#f8f8f2;"> Serial.flush(); </span><span style="color:#f8f8f2;"> pinMode(ledPin, OUTPUT); </span><span style="color:#f8f8f2;"> pinMode(buttonPin, INPUT); </span><span style="color:#f8f8f2;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;">void loop() { </span><span style="color:#f8f8f2;"> if (Serial) { </span><span style="color:#f8f8f2;"> int buttonState = digitalRead(buttonPin); </span><span style="color:#f8f8f2;"> if (buttonState != previousButtonState) { </span><span style="color:#f8f8f2;"> if (buttonState == HIGH) { </span><span style="color:#f8f8f2;"> digitalWrite(ledPin, HIGH); </span><span style="color:#f8f8f2;"> Serial.write(&quot;ON\r\n&quot;); </span><span style="color:#f8f8f2;"> } else { </span><span style="color:#f8f8f2;"> digitalWrite(ledPin, LOW); </span><span style="color:#f8f8f2;"> Serial.write(&quot;OFF\r\n&quot;); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> Serial.flush(); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> previousButtonState = buttonState; </span><span style="color:#f8f8f2;"> delay(10); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;">} </span><span style="color:#f8f8f2;"> </span></pre> <p>On the JavaScript side, we need to connect to the Arduino and then listen to messages on the serial interface. When an <code>ON</code> message is received, we start the AirHorn with <code>airhorn.start()</code>. When an <code>OFF</code> message is received, we stop it with <code>airhorn.stop()</code>;</p> <p>This is implemented in the <code>_loopRead</code> method in the code listing below. Check this <a href="https://github.com/andreban/airhorn/commit/299c8c1b4c1fd8a49b8db48a9add4864cc6259a3">commit</a> to see all changes made to AirHorn to make this work.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#50fa7b;">HardwareButton </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">function</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">airhorn</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">airhorn </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">airhorn</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">decoder </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">TextDecoder(); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">connected </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#bd93f9;">self </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">this</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">_loopRead </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">async function</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">!</span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">device) { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;no device&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">try </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">result </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">device</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">transferIn</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">2</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">64</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">command </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">decoder</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">decode</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">result</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">data); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">command</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">trim</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">=== </span><span style="color:#f1fa8c;">&#39;ON&#39;</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">airhorn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">start</span><span style="color:#f8f8f2;">({loop: </span><span style="color:#bd93f9;">true</span><span style="color:#f8f8f2;">}); </span><span style="color:#f8f8f2;"> } </span><span style="color:#ff79c6;">else </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">airhorn</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">stop</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">_loopRead</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> } </span><span style="color:#ff79c6;">catch </span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">e</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;Error reading data&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#ffffff;">e</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> }; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">connect </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">async function</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">try </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">device </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">await </span><span style="font-style:italic;color:#66d9ef;">navigator</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">usb</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">requestDevice</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> filters: [{</span><span style="color:#f1fa8c;">&#39;vendorId&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x2341</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;productId&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x8057</span><span style="color:#f8f8f2;">}] </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">device </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">device</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#ffffff;">device</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">open</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#ffffff;">device</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">selectConfiguration</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#ffffff;">device</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">claimInterface</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#ffffff;">device</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">selectAlternateInterface</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#ffffff;">device</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">controlTransferOut</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;requestType&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&#39;class&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;recipient&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&#39;interface&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;request&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x22</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;value&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x01</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;index&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">self</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">_loopRead</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> } </span><span style="color:#ff79c6;">catch </span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">e</span><span style="color:#f8f8f2;">) { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">console</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">log</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;Failed to Connect: &#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#ffffff;">e</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> }; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">disconnect </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">async function</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">!</span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">device) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">device</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">controlTransferOut</span><span style="color:#f8f8f2;">({ </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;requestType&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&#39;class&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;recipient&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#f1fa8c;">&#39;interface&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;request&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x22</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;value&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;index&#39;</span><span style="color:#f8f8f2;">: </span><span style="color:#bd93f9;">0x00</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">await </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">device</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">close</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">device </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">null</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> }; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">init </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#8be9fd;">function</span><span style="color:#f8f8f2;">() { </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">buttonDiv </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">document</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">querySelector</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;#connect&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">button </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">buttonDiv</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">querySelector</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;button&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">button</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">addEventListener</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;click&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">connect</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">bind</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">this</span><span style="color:#f8f8f2;">)); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">navigator</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">usb) { </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">buttonDiv</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">classList</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">add</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;available&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> } </span><span style="color:#f8f8f2;"> }; </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">this</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">init</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;">}; </span></pre> <p>This is how the result looks like:</p> <video controls loop poster="/img/2020/02/airhorn.jpg"> <source src="/img/2020/02/airhorn.webm" type="video/webm; codecs=vp8"> <source src="/img/2020/02/airhorn_x264.mp4" type="video/mp4; codecs=h264"> </video> <h2>[Optional] Building a case for our button</h2> <p>Finally, to make a nice packaging for the project, we can 3D print a box to fit our button and the required electronics inside. The design I used is <a href="https://www.thingiverse.com/thing:4088197">available at Thingiverse</a>, and it contains both the box and a lid where the button can be fitted.</p> <p>Here's an interesting video of the box being printed:</p> <video controls loop muted poster="/img/2020/02/airhorn_box.jpg"> <source src="/img/2020/02/airhorn_box.webm" type="video/webm; codecs=vp8"> <source src="/img/2020/02/airhorn_box.mp4" type="video/mp4; codecs=h264"> </video> <h2>Final Result</h2> <p>Finally, I used blue acrylic paint to give a nice color for the box. This is what the final result looks like:</p> <p><img src="/img/2020/02/airhorn_final.jpg" alt="Complete AirHorn box" title="Complete AirHorn box" /></p> Build a WebUSB powered AirHorn button! This tutorial shows you how to build a physical button to control Paul Kinlan's AirHorn using an Arduino, a momentary button, and WebUSB. The guide includes code, wiring diagrams, and even 3D printing instructions for a custom case. Make your own fun, interactive project today! Fitness with Web Bluetooth 2017-02-20T11:42:00Z https://bandarra.me/posts/Fitness-Tracking-with-Web-Bluetooth <p><img src="/img/2017/02/monitor.jpg" alt="PM 5 Monitor" title="Connecting to a PM5 Monitor" /></p> <p>With the launch of Chrome 56, web applications are now able to access Bluetooth Low Energy devices directly from the browser, without the need to install a plugin or a native application. This opens the opportunity to create types of web applications that were only available to native platforms.</p> <p>For a great introduction on how to implement applications on the browser using Web Bluetooth, check François Beaufort's <a href="https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web">"Interact with Bluetooth Devices on the Web"</a> article.</p> <h2>Building a Rowing Monitor</h2> <p>Many fitness tracking applications track exercises by connecting to Bluetooth enabled devices, such as Health Monitors and Treadmills. Due to the lack of Bluetooth connectivity on the browser, those applications are, in most cases, developed using native platforms. With Web Bluetooth now being available, it becomes possible to connect and track exercises in real-time, from the browser. So, I decided to give it a try and build such application for my rowing machine.</p> <p>The machine in question is a Concept2 Model D, but the most important part is it's PM5 monitor. The monitor is BLE enabled, and, in fact, Concept2 offers native applications for both <a href="https://play.google.com/store/apps/details?id=com.concept2.ergdata">Android</a> and <a href="https://itunes.apple.com/gb/app/ergdata/id561716382?mt=8">iOS</a>.A quick search on Google took me to the <a href="http://www.concept2.co.uk/files/pdf/us/monitors/PM5_BluetoothSmartInterfaceDefinition.pdf">protocol</a> used by the monitor.</p> <p>The resulting application is hosted <a href="https://rowing-monitor.bandarra.me/">here</a>, and the source code is publicly available on <a href="https://github.com/GoogleChrome/rowing-monitor/">GitHub</a>.</p> <h3>Connecting to the Monitor</h3> <p>The PM5 specification outlines 4 different services on the device: The Information, Discovery, Control and Rowing services. The Discovery Service is the one that announces the device, so we need to pass it as a filter for the connection.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">options </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> filters: [{services: [</span><span style="color:#f1fa8c;">&#39;ce060000-43e5-11e4-916c-0800200c9a66&#39;</span><span style="color:#f8f8f2;">]}], </span><span style="color:#f8f8f2;"> optionalServices: [ </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;ce060010-43e5-11e4-916c-0800200c9a66&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#6272a4;">// Information Service. </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;ce060020-43e5-11e4-916c-0800200c9a66&#39;</span><span style="color:#f8f8f2;">, </span><span style="color:#6272a4;">// Control Service. </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&#39;ce060030-43e5-11e4-916c-0800200c9a66&#39; </span><span style="color:#6272a4;">// Rowing Service. </span><span style="color:#f8f8f2;"> ] </span><span style="color:#f8f8f2;">}; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">navigator</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">bluetooth</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">requestDevice</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">options</span><span style="color:#f8f8f2;">) </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">then</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">device </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... </span><span style="color:#f8f8f2;"> }); </span></pre> <p>The characteristics we want to use are accessed through the other services. In order to have access to those services later, we need to pass them as <code>optionalServices</code> when requesting the device.</p> <h3>Selecting a device</h3> <p>When the application requests the device, the browser will show a native interface, showing the devices that are compatible with the configuration requested. This dialog doubles both as a request for the application to access the bluetooth and a device selection screen!</p> <p><img src="/img/2017/02/pm5.gif" alt="Choosing a PM5 Monitor" title="Choosing a PM5 Monitor" /></p> <p>For developers who have gone through the process of developing a Bluetooth application on native platforms, this is very welcome news: On most platforms, developers have to build an interface that deals with device scans and handles the user selecting the devices by themselves.</p> <h3>Accessing characteristics</h3> <p>There are 2 ways to access characteristics. It's possible to request them at any time for the application and, more interesting to our objectives, sign up for notifications for when a characteristic changes.</p> <p>First, retrieve the Service this characteristic belongs to:</p> <pre style="background-color:#282a36;"> <span style="color:#ffffff;">device</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">gatt</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">connect</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">then</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">server </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">server</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getPrimaryService</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;ce060030-43e5-11e4-916c-0800200c9a66&#39;</span><span style="color:#f8f8f2;">) </span><span style="color:#6272a4;">// Rowing Service. </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">then</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">service </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> }); </span></pre> <p>Then, retrieve the characteristic itself. Call <code>characteristic.startNotifications</code> and then setup an <code>eventListener</code> to get updates on the characteristic.</p> <pre style="background-color:#282a36;"> <span style="color:#ffffff;">service</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getCharacteristic</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;ce060031-43e5-11e4-916c-0800200c9a66&#39;</span><span style="color:#f8f8f2;">) </span><span style="color:#6272a4;">// General Info. </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">then</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">characteristic </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#ffffff;">characteristic</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">startNotifications</span><span style="color:#f8f8f2;">()) </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">then</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#ffb86c;">characteristic </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">characteristic</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">addEventListener</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;characteristicvaluechanged&#39;</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">e </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// Parse characteristic value. </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> }); </span></pre> <p>Some characteristics will have values that are strings. The serialNumber characteristic, from the Information Service is one example. Here's how a developer can parse a characteristic like this.</p> <pre style="background-color:#282a36;"> <span style="color:#ffffff;">characteristic</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">addEventListener</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;characteristicvaluechanged&#39;</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">e </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">decoder </span><span style="color:#ff79c6;">= new </span><span style="color:#f8f8f2;">TextDecoder(</span><span style="color:#f1fa8c;">&#39;utf-8&#39;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">value </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">decoder</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">decode</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">e</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">target</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">value); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... </span><span style="color:#f8f8f2;">}); </span></pre> <p>On the Rowing Monitor, most characteristics are made of data. The specific format for that data is specified on the documentation, and it can can be accessed from the event object, in the form of a DataView. Here's how a developer can parse such data.</p> <pre style="background-color:#282a36;"> <span style="color:#ffffff;">characteristic</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">addEventListener</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;characteristicvaluechanged&#39;</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">e </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">dataView </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">e</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">target</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">value; </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">avgStrokeRate </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">dataView</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getUint8</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">10</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">endingHeartRate </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">dataView</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getUint8</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">11</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">averageHeartRat </span><span style="color:#ff79c6;">= </span><span style="color:#ffffff;">dataView</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getUint8</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">12</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// ... </span><span style="color:#f8f8f2;">}); </span></pre> <h2>Always test on multiple devices</h2> <p>The Bluetooth LE stack doesn't support parallel access to the API. On some operating systems, the bluetooth stack implements a queue and serializes the calls to the API. But on other operating systems, such as Android, the OS does not queue the calls and it's up to the application developer to manage the queue. For more context around this, check this Github <a href="https://github.com/WebBluetoothCG/web-bluetooth/issues/188#issuecomment-255121220">issue</a>, where it's under active discussion.</p> <p>This leads to an interesting problem when developing using Web Bluetooth: An inadvertent developer may create an app and not care about serializing the calls to the API. The application would work flawlessly on a Mac OS, but fails on Android, with an error message that is not so obvious.</p> <p>Developers can implement their own queue to serialize API calls, or carefully design their application so that parallel calls don't happen. It's also important to test the application on multiple Operating Systems to make sure apps behave properly.</p> <h2>Look mom, no internet!</h2> <p>An application without access to the internet can be a dull application. But the Rowing Monitor takes advantages of features such as <a href="https://developers.google.com/web/fundamentals/getting-started/primers/service-workers">ServiceWorkers</a> and <a href="https://developer.mozilla.org/en/docs/Web/API/IndexedDB_API">IndexedDB</a> to create an application that fully works offline: Once the user visits the website for the first time, the service worker is installed and fully caches the application for offline usage. The Rowing Monitor's service worker is generated during build time, using the <a href="https://github.com/GoogleChrome/sw-precache/">sw-precache</a> library. Here's what the service worker generator task looks like in Gulp:</p> <pre style="background-color:#282a36;"> <span style="color:#ffffff;">gulp</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">task</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;generate-service-worker&#39;</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#ffb86c;">callback </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">const </span><span style="color:#ffffff;">rootDir </span><span style="color:#ff79c6;">= </span><span style="color:#f1fa8c;">&#39;dist&#39;</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">swPrecache</span><span style="color:#ff79c6;">.</span><span style="color:#8be9fd;">write</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">path</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">join</span><span style="color:#f8f8f2;">(</span><span style="color:#ffffff;">rootDir</span><span style="color:#f8f8f2;">, </span><span style="color:#f1fa8c;">&#39;service-worker.js&#39;</span><span style="color:#f8f8f2;">), { </span><span style="color:#f8f8f2;"> staticFileGlobs: [</span><span style="color:#ffffff;">rootDir </span><span style="color:#ff79c6;">+ </span><span style="color:#f1fa8c;">&#39;/**/*.{js,html,css,png,jpg,gif,svg,eot,ttf,woff}&#39;</span><span style="color:#f8f8f2;">], </span><span style="color:#f8f8f2;"> stripPrefix: </span><span style="color:#ffffff;">rootDir </span><span style="color:#f8f8f2;"> }, </span><span style="color:#ffffff;">callback</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;">}); </span></pre> <p>The workouts are persisted using IndexedDB, so even if the user doesn't have connection the application is fully functional. The data is only persisted locally, but the application could be easily extended to offer the user the change to login and persist the workout data to a remote server, once it is online.</p> <h2>Almost there!</h2> <p>The Web Bluetooth API offers the functionality needed to connect and gather information from a Bluetooth enabled device, but there's still one thing missing to transform the Rowing Monitor into a fully functional fitness tracker.</p> <p>When using the application to track an exercise, the screen will go to sleep after a few seconds and the bluetooth connection will be lost. This is due to the lack of a Wakelock API. Fortunately, such a Web API is already <a href="https://www.w3.org/TR/wake-lock/">under discussion</a> on W3C.</p> <p>In fact, Chrome already implements an early version of the API, and it can be enabled by activating Chrome's experimental features. To activate it, go to <em>chrome://flags/#enable-experimental-web-platform-features</em> and click <em>Enable</em>.</p> <pre style="background-color:#282a36;"> <span style="color:#6272a4;">// The PM5 API returns a Promise when the connection is established. The WakeLock </span><span style="color:#6272a4;">// is acquired once a connection is established. </span><span style="color:#ffffff;">pm5</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">connect</span><span style="color:#f8f8f2;">() </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">then</span><span style="color:#f8f8f2;">(() </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">screen</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">keepAwake </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">true</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">//... </span><span style="color:#f8f8f2;"> }); </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">// The API also provides a disconnect event. The WakeLock is released once the </span><span style="color:#6272a4;">// application is disconnected from the rowing machine. </span><span style="color:#ffffff;">pm5</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">addEventListener</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&#39;disconnect&#39;</span><span style="color:#f8f8f2;">, () </span><span style="font-style:italic;color:#8be9fd;">=&gt; </span><span style="color:#f8f8f2;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">screen</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">keepAwake </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">//... </span><span style="color:#f8f8f2;">}); </span></pre> <h2>Conclusion</h2> <p>The potential of integrating fitness equipment with Web Bluetooth is amazing. Imagine someone arriving to the gym and instead of having to download a full app to track the exercise, they can just receive the URL through a Physical Web Beacon, scan a NFC tag or a QR code, and the application is made available instantly — no download involved.</p> <p>It also opens the possibility of deeper integrations. The Rowing Monitor application, for instance, could be evolved into an online racing application, a game or a VR experience that lets users row at different parts of the world.</p> <p>Another application worth checking is <a href="https://kinomap.tv/">https://kinomap.tv/</a>, which lets a user sync a YouTube riding video with a smart trainer, using Web Bluetooth.</p> Control your Concept2 PM5 rowing monitor directly from your web browser! This tutorial shows how to build a web app using Web Bluetooth to connect, track real-time workout data (stroke rate, heart rate), and store results locally, all without needing a native app. Learn how to handle device selection, access characteristics, parse data, and implement offline functionality with Service Workers and IndexedDB. Ignoring Corrupted gzip files in Hadoop 2013-12-09T00:00:00Z https://bandarra.me/posts/Ignoring-Corrupted-gzip-files-in-Hadoop <p>I've been analyzing my website traffic using Hadoop and MapReduce. Our logs are recorded hourly on a gzipped file. But, since the server may be restarted while writing to the file, every now and then a file gets corrupted. When this happens the default Hadoop implementation aborts the entire job. So, I had to dive into the Hadoop source code and find a way to make it more lenient towards corrupted files.</p> <p>The trick is to create a LineRecordReader that, instead of raising the EOFException, catches it and tells that there are no more lines to read in the file. As the default TextInputFormat has a hardcoded LineRecordReader, it is necessary to extend the FileInputFormat and override the createRecordReader method to return my version of FileInputFormat.</p> <p>Here's what the code looks like:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">package </span><span style="color:#f8f8f2;">org.bandarra.hadoop; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.commons.compress.utils</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">Charsets</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.hadoop.io</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">LongWritable</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.hadoop.io</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">Text</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.hadoop.mapreduce</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">InputSplit</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.hadoop.mapreduce</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">RecordReader</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.hadoop.mapreduce</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">TaskAttemptContext</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.hadoop.mapreduce.lib.input</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">FileInputFormat</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">org.apache.hadoop.mapreduce.lib.input</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">LineRecordReader</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">java.io</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">EOFException</span><span style="color:#f8f8f2;">; </span><span style="color:#ff79c6;">import </span><span style="font-style:italic;color:#66d9ef;">java.io</span><span style="font-style:italic;color:#ff79c6;">.</span><span style="font-style:italic;color:#66d9ef;">IOException</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">/** </span><span style="color:#6272a4;"> * Created by andreban on 12/9/13. </span><span style="color:#6272a4;"> */ </span><span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">class </span><span style="text-decoration:underline;color:#8be9fd;">LenientTextInputFormat </span><span style="color:#ff79c6;">extends </span><span style="text-decoration:underline;font-style:italic;color:#8be9fd;">FileInputFormat </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">private static </span><span style="font-style:italic;color:#8be9fd;">class </span><span style="text-decoration:underline;color:#8be9fd;">LenientLineRecordReader </span><span style="color:#ff79c6;">extends </span><span style="text-decoration:underline;font-style:italic;color:#8be9fd;">LineRecordReader </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">public </span><span style="color:#50fa7b;">LenientLineRecordReader</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">byte</span><span style="color:#ff79c6;">[] </span><span style="font-style:italic;color:#ffb86c;">recordDelimiter</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">super</span><span style="color:#f8f8f2;">(recordDelimiter); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> @Override </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">boolean </span><span style="color:#50fa7b;">nextKeyValue</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">throws </span><span style="font-style:italic;color:#66d9ef;">IOException </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">try </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return </span><span style="color:#bd93f9;">super</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">nextKeyValue</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ff79c6;">catch</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">EOFException </span><span style="font-style:italic;color:#ffb86c;">ex</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> ex</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">printStackTrace</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> @Override </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#66d9ef;">RecordReader </span><span style="color:#50fa7b;">createRecordReader</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">InputSplit </span><span style="font-style:italic;color:#ffb86c;">split</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#66d9ef;">TaskAttemptContext </span><span style="font-style:italic;color:#ffb86c;">context</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="color:#f8f8f2;"> delimiter </span><span style="color:#ff79c6;">= </span><span style="color:#f8f8f2;"> context</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getConfiguration</span><span style="color:#f8f8f2;">()</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">get</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;textinputformat.record.delimiter&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">byte</span><span style="color:#ff79c6;">[]</span><span style="color:#f8f8f2;"> recordDelimiterBytes </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">null</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">null </span><span style="color:#ff79c6;">!=</span><span style="color:#f8f8f2;"> delimiter) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> recordDelimiterBytes </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> delimiter</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getBytes</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">Charsets</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">UTF_8</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">return new </span><span style="font-style:italic;color:#66d9ef;">LenientLineRecordReader</span><span style="color:#f8f8f2;">(recordDelimiterBytes); </span><span style="color:#f8f8f2;"> </span><span style="color:#ffffff;">} </span><span style="color:#ffffff;">} </span></pre> Handle corrupted Hadoop gzipped log files gracefully. This improved `FileInputFormat` extends Hadoop's default functionality, enabling your MapReduce jobs to continue processing even when encountering corrupted hourly log files, preventing job aborts due to `EOFException`. The solution uses a custom `LineRecordReader` to handle exceptions, ensuring data processing continues uninterrupted. OpenGL SuperBible in Java: Putting things in perspective! 2011-12-19T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-in-Java-Putting-things-in-perspective <p>Until now, all our examples were done using the orthographic mode. This means that we haven't seen any example that gives us a sense of depth. Ortographic mode is actually very useful if you want to make 2D games in OpenGL or draw a HUD for your game. But what most developers really want is to make 3D games, with an awesome sense of depth.</p> <p>The 2 images below picture the same scene rendered in orthographic mode, and then in perspective mode.</p> <p><img src="/img/2011/12/Orthographic.png" alt="Orthographic" title="Orthographic" /> <img src="/img/2011/12/Perspective.png" alt="Perspective" title="Perspective" /></p> <p>For this example, i've included the same scene rendered in orthographic and perspective mode. Check the examples on the example6 package of the source. I won't detail the ortographic mode example, because its not much different from previous examples.</p> <p>The only new class in this example is the <code>GLFrustrum</code> class. Its just a simple wrapper that manipulates the projection matrix with some utility methods. The relevant method for this example is setPerspective method. The image below helps to understand the parameter values. The first parameter is the field of view, followed by the aspect ratio, and then by the value of the near plane and then the far plane.</p> <p>So, using the <code>GLFrustrum</code> is the first difference in this example. The other on is that we have another <code>MatrixStack</code> declared, the perspective matrix. It is build from the <code>GLFrustrum</code> matrix and should be changed everytime he values in the <code>GLFrustrum</code> change. This is how the initGL method looks like now.</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">initGL</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">glClearColor</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> shader </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">GLShaderFactory</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getFlatShader</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> sideWall </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">GLBatchFactory</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">makeCube</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0.2</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.8</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> topWall </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">GLBatchFactory</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">makeCube</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0.8</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.2</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> frustrum </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#66d9ef;">GLFrustrum</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> modelViewMatrix </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#66d9ef;">MatrixStack</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>And this is how the resizeGL method looks like:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">resizeGL</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">glViewport</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">,</span><span style="font-style:italic;color:#66d9ef;">Display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getWidth</span><span style="color:#f8f8f2;">() ,</span><span style="font-style:italic;color:#66d9ef;">Display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getHeight</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> frustrum</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setPerspective</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">45</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="font-style:italic;color:#66d9ef;">Display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getWidth</span><span style="color:#f8f8f2;">()</span><span style="color:#ff79c6;">/ </span><span style="font-style:italic;color:#66d9ef;">Display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getHeight</span><span style="color:#f8f8f2;">(), </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">10.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> projectionMatrix </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#66d9ef;">MatrixStack</span><span style="color:#f8f8f2;">(frustrum</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getProjectionMatrix</span><span style="color:#f8f8f2;">()); </span><span style="color:#ffffff;">} </span></pre> <p>The change in the initGL method is straightforward. The significant change (besides initalizing scene specific stuff) is the addition of the code creating the <code>GLFrustrum</code>. But the <code>resizeGL</code> method packs more interesting stuff. besides calling glViewPort, it configures a <code>45</code> degree fov for the frustrum, with near plane of <code>1.0</code> and far plane of <code>10.0</code>, which means that anything outside those points wont be drawed.</p> <p>The code for drawing the scene is also straightforward. This time, we need to multiply the <code>ModelView</code> matrix by the <code>ProjectionMatrix</code> so as to get the projected scene. The other change you should notice is that when translating the objects in the scene, we use a <code>-2</code> on the <code>Z</code> axis. Thats related to the configuration of the near and far plane we mentioned before.</p> <p>So, that's it for drawing a projected scene! Again, all the code is available at http://code.google.com/p/opengl-superbible-java/</p> Learn to create stunning 3D visuals in OpenGL using perspective projection. This tutorial shows you how to switch from orthographic to perspective mode, leveraging the `GLFrustrum` class and matrix manipulation for depth and realism in your game or application. Master the `setPerspective` method and understand the impact of field of view, aspect ratio, near and far planes. Enhance your OpenGL skills now! OpenGL SuperBible in Java: The GLBatchFactory 2011-12-12T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-in-Java-GLBatchFactory <p><img src="/img/2011/12/GLBatchFactory.png" alt="Different 3D Shapes" title="Different 3D Shapes" /></p> <p>The GLBatchFactory is a helper class that helps the developer to create models. Besides providing a tool to create more complex model, where the developer may chose to add each vertex of the triangle separately, it provides 5 static methods that create some useful shapes.</p> <p>The first shape is the cube, created with the makeCube method. It takes 3 float parameters, the cube width, height and depth. The cube is created with the point 0.0.0 on its center;</p> <p>The second shape is the sphere. It also takes 3 float parameters, but with different meaning. The first is da radius of the sphere. Now, the sphere is constructed of triangles organized in slices and stacks. The second and third parameters determine the number of slices and stacks of the sphere.. The more slices and stacks, the better the sphere will look. But slower to build and render it will be. The sphere is constructed around 0.0.0 too.</p> <p>The third shape is the cylinder (or cone). It takes 5 parametres. The radius of the top, the radius of the bottom, the length, the number of slices and number of stacks.</p> <p>The fourth shape is the disk. A shape that resembles a CD. It takes four parameters, the inner radius (size of the middle of the disk), the outer radius, number of stacks and slices.</p> <p>The fifth shape is the Torus, a shape that looks like a donut. It takes 4 parameters, the inner radius, the outer radius, number of stacks and number of slices.</p> <p>On the OpenGL Superbible C++ code, this code is inside the GLBatch class. I decided to put it on another class, just to make the code more clear.</p> <p>Again, the example is available at the Example5.java, on http://code.google.com/p/opengl-superbible-java/</p> <p>If you want to check out the older tutorials, go to http://www.codemansion.com/p/opengl-superbible-in-java-using-lwjgl.html</p> Learn to create 3D shapes like cubes, spheres, cylinders, disks, and tori using the GLBatchFactory helper class. This tutorial provides five static methods with parameters to easily generate these common shapes in your OpenGL projects, improving your model-building efficiency. Explore the example code and enhance your 3D graphics development skills. OpenGL Superbible in Java, the MatrixStack 2011-12-09T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-in-Java-The-MatrixStack <p>On the last example, we learned how to rotate/translate a triangle manipulating the matrices. There was a lot of code for such a small feature. Imagine if you hat to rotate various objects in different places of the screen. Thats where the <code>MatrixStack</code> class comes to help. It encapsulates the feature of the old OpenGL 1.1 where you could push, pop and manipulate the matrix state, demanding much less code if you want to draw various objects.</p> <p>As the first example, lets modify our RotatingTriangle to use the <code>MatrixStack</code>, on Example3.java. The first step is creating an instance variable to hold our <code>MatrixStack</code>. Let's also move our FloatBuffer from inside the update method to an instance variable;</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">private </span><span style="font-style:italic;color:#66d9ef;">MatrixStack</span><span style="color:#f8f8f2;"> matrixStack </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#66d9ef;">MatrixStack</span><span style="color:#f8f8f2;">(); </span><span style="color:#ff79c6;">private </span><span style="font-style:italic;color:#66d9ef;">FloatBuffer</span><span style="color:#f8f8f2;"> buff </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">BufferUtils</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">createFloatBuffer</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">render</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> angle </span><span style="color:#ff79c6;">+= </span><span style="color:#bd93f9;">1</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">glClear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL_DEPTH_BUFFER_BIT </span><span style="color:#ff79c6;">| </span><span style="color:#bd93f9;">GL_COLOR_BUFFER_BIT</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">useShader</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniform4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;vColor&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">push</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">rotate</span><span style="color:#f8f8f2;">(angle, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">fillBuffer</span><span style="color:#f8f8f2;">(buff); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniformMatrix4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;mvpMatrix&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">, buff); </span><span style="color:#f8f8f2;"> triangleBatch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">draw</span><span style="color:#f8f8f2;">(shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getAttributeLocations</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">pop</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>Now, lets see what happens on the render method. Instead of declaring and multipying the matrices <em>by hand</em>, we just push the matrix, manipulate it as we want by calling the <code>matrix.translate()</code> and <code>matrix.rotate()</code> methods, then we call the <code>fillBuffer()</code> method to put the result matrix in the buff variable and <code>draw()</code>. The last important thing is to call the <code>pop()</code> method so that we end up with the same matrix that we started.</p> <p>Again, the code is available at http://code.google.com/p/opengl-superbible-java/. Just look for Example3.java.</p> <p>Now, to show how simple it is to draw various objects on the screen, each one at its own place and rotation, let's check what happens on Example4.java. The first change is on the <code>initGL()</code> method. We are now using a smaller triangle.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">triangleBatch </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#66d9ef;">SimpleGLBatch</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL11</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_TRIANGLES</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">new </span><span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#f8f8f2;">[]{ </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.3</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">-</span><span style="color:#bd93f9;">0.3</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#ff79c6;">-</span><span style="color:#bd93f9;">0.3</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0.3</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#ff79c6;">-</span><span style="color:#bd93f9;">0.3</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">}, </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">new </span><span style="font-style:italic;color:#8be9fd;">short</span><span style="color:#f8f8f2;">[]{</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">2</span><span style="color:#f8f8f2;">}); </span></pre> <p>Now, lets see what happens on the <code>render()</code> method.</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">render</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> angle </span><span style="color:#ff79c6;">+= </span><span style="color:#bd93f9;">1</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">; </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">glClear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL_DEPTH_BUFFER_BIT </span><span style="color:#ff79c6;">| </span><span style="color:#bd93f9;">GL_COLOR_BUFFER_BIT</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">useShader</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">push</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniform4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;vColor&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">translate</span><span style="color:#f8f8f2;">(</span><span style="color:#ff79c6;">-</span><span style="color:#bd93f9;">0.5</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">rotate</span><span style="color:#f8f8f2;">(angle, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">fillBuffer</span><span style="color:#f8f8f2;">(buff); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniformMatrix4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;mvpMatrix&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">, buff); </span><span style="color:#f8f8f2;"> triangleBatch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">draw</span><span style="color:#f8f8f2;">(shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getAttributeLocations</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">pop</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">push</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniform4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;vColor&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">translate</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0.5</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">rotate</span><span style="color:#f8f8f2;">(angle, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">fillBuffer</span><span style="color:#f8f8f2;">(buff); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniformMatrix4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;mvpMatrix&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">, buff); </span><span style="color:#f8f8f2;"> triangleBatch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">draw</span><span style="color:#f8f8f2;">(shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getAttributeLocations</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> matrixStack</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">pop</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>For each triangle we want to draw, we call matrix.push() before translating and rotating, fill the buffer, draw the triangle and call pop. You could easily have a loop in your code to draw hundreds of objects this way!!</p> Simplify OpenGL rendering with MatrixStack. Learn to efficiently rotate and translate multiple objects using push/pop matrix operations, minimizing code and maximizing performance. See examples and code for easy implementation. OpenGLSuperbible in Java, rotating our triangle! 2011-12-04T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-in-Java-Rotating-our-triangle <p>On the last post of our lwjgl series, we saw how to draw a triangle. Now, lets learn how to make our triangle rotate around the Z axis.</p> <p>The first thing we need is to use a different shader so that we may pass the <code>modelView</code> matrix to the shader and then use the shader to update the vertex positions.</p> <p>While the identity shader just passed on the vertex position, the flat shader multiplies the vertex position as a matrix passed by a unifor, called <code>mvpMatrix</code>. Thats exactly what we need. Let's change the code on the initGL method to do that</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">shader </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">GLShaderFactory</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getFlatShader</span><span style="color:#f8f8f2;">(); </span></pre> <p>Next, on the <code>render()</code> method, we need to pass the modelView matrix as a uniform to the shader. The first step is creating the variables to hold the matrix:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#ff79c6;">[]</span><span style="color:#f8f8f2;"> modelViewMatrix </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#f8f8f2;">[</span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">]; </span><span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#ff79c6;">[]</span><span style="color:#f8f8f2;"> translationMatrix </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#f8f8f2;">[</span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">]; </span><span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#ff79c6;">[]</span><span style="color:#f8f8f2;"> rotationMatrix </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#f8f8f2;">[</span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">]; </span></pre> <p>Now, lets fill the matrix. We don't want to move the triangle in the scene at this moment, so, lets create the translation matrix filled with zeroes</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#66d9ef;">Math3D</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">translationMatrix44f</span><span style="color:#f8f8f2;">(translationMatrix, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span></pre> <p>For the rotation matrix, let's rotate it around the <code>Z</code> axis.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#66d9ef;">Math3D</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">rotationMatrix44</span><span style="color:#f8f8f2;">(rotationMatrix, angle, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span></pre> <p>The angle variable has been created with class scope and is updated every time the <code>render()</code> method runs.</p> <p>Now, we need to multiply our matrices and use it as a uniform in our shade.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#66d9ef;">Math3D</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">matrixMultiply44</span><span style="color:#f8f8f2;">(modelViewMatrix, translationMatrix, rotationMatrix); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">FloatBuffer</span><span style="color:#f8f8f2;"> buff </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">BufferUtils</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">createFloatBuffer</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">16</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;">buff</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">put</span><span style="color:#f8f8f2;">(modelViewMatrix); </span><span style="color:#f8f8f2;">buff</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">flip</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;">shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniformMatrix4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;mvpMatrix&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">, buff); </span></pre> <p>After multiplying, we need to create a <code>FloatBuffer</code> to add the shader as a uniform. Then, all we need is to call the <code>setUniformMatrix4</code> on the shader instances and we are all set.</p> <p>Thats it for rotation a triangle. You can also change this code to rotate it around other axis or move it around the scene.</p> <p>Again, all the code is available at http://code.google.com/p/opengl-superbible-java</p> Learn how to rotate a triangle in Java using LWJGL and shaders. This tutorial shows you how to create and use rotation matrices, pass them to shaders as uniforms, and update vertex positions for smooth rotation around the Z-axis. Improve your 3D graphics skills with this step-by-step guide and example code. OpenGL SuperBible in Java: Your first triangle 2011-11-11T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-in-Java-Your-First-Triangle <p>On the last tutorials, there was a lot of code that we used to build a framework and encapsulate the complexity of the shaders. Today, it's time for that work to start paying off and getting something actually drawn on our screen!</p> <p>Besides using the code on OpenGL Superbible, the code in this article was inspired by the tutorials on <a href="http://lwjgl.org/wiki/index.php?title=Main_Page">LWJGL's wiki page</a> and a few other searches on Google, to figure out the LWJGL specific parts, like initalizing screen and etc.</p> <p>Again, all the code is available on <a href="http://code.google.com/p/opengl-superbible-java/">http://code.google.com/p/opengl-superbible-java/</a></p> <p>First, i won't cover the details on LWJGL's implementation. The code is fairly simple and their wiki and docs should clear any doubts.</p> <p>Taking out the LWJGL initialization code, the example turns out to be very short. There are two instance variables that are important to our example in the Triangle class. The triangleBatch and shader instances. The first is a <code>GLBatch</code> which has the responsability of drawing our triangle. The second is shader used to draw this triangle.</p> <p>Those variables are initialized on the <code>initGL</code> method. Heres a transcription of the code:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">initGL</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">glClearColor</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#f8f8f2;"> shader </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">GLShaderFactory</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getIdentityShader</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> triangleBatch </span><span style="color:#ff79c6;">= new </span><span style="font-style:italic;color:#66d9ef;">SimpleGLBatch</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL11</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_TRIANGLES</span><span style="color:#f8f8f2;">, </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">new </span><span style="font-style:italic;color:#8be9fd;">float</span><span style="color:#f8f8f2;">[]{ </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.5</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#6272a4;">//vertex 1 </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">-</span><span style="color:#bd93f9;">0.5</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#ff79c6;">-</span><span style="color:#bd93f9;">0.5</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#6272a4;">//vertex 2 </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">0.5</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#ff79c6;">-</span><span style="color:#bd93f9;">0.5</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">}, </span><span style="color:#6272a4;">//vertex 3 </span><span style="color:#f8f8f2;"> </span><span style="color:#ff79c6;">new </span><span style="font-style:italic;color:#8be9fd;">short</span><span style="color:#f8f8f2;">[]{</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">2</span><span style="color:#f8f8f2;">}); </span><span style="color:#ffffff;">} </span></pre> <p>The <code>glClearColor</code> call specifies which color to using when clearing the color buffers. You can get more details here.</p> <p>The shader initialization just uses the default Identity Shader from <code>GLShaderFactory</code>. This shader does not make any transformation on the vertex.</p> <p>The next line initializes the GLBatch informing that it should use <code>GL_TRIANGLES</code> to draw the vertices, the vertices values. Each vertex has 4 float values. <code>x</code>, <code>y</code>, <code>z</code> and <code>scale</code>, and, at last, the indexes of the vertices.</p> <p>The next important line of code is:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">resizeGL</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">glViewport</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">DISPLAY_WIDTH </span><span style="color:#f8f8f2;">,</span><span style="color:#bd93f9;">DISPLAY_HEIGHT</span><span style="color:#f8f8f2;">); </span><span style="color:#ffffff;">} </span></pre> <p>This code simply resizes the OpenGL viewport when the window size is changed.</p> <p>Now, we have to actually draw the triangle on the screen, and thats what the render method does:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">render</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#50fa7b;">glClear</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL_DEPTH_BUFFER_BIT </span><span style="color:#ff79c6;">| </span><span style="color:#bd93f9;">GL_COLOR_BUFFER_BIT</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">useShader</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">setUniform4</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;vColor&quot;</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">1.0</span><span style="font-style:italic;color:#8be9fd;">f</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> triangleBatch</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">draw</span><span style="color:#f8f8f2;">(shader</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">getAttributeLocations</span><span style="color:#f8f8f2;">()); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">Display</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">update</span><span style="color:#f8f8f2;">(); </span><span style="color:#ffffff;">} </span></pre> <p>The first step is clearing the screen, with a call to <a href="http://www.opengl.org/sdk/docs/man/xhtml/glClear.xml"><code>glClear</code></a>. Then, we tell the shader we want to use it, with <code>shader.useShader()</code>. The next step is telling the shader which color we want to paint our triangle. The identity shader user a uniform called vColor to do that. The, we can draw the triangleBatch and ask LWJGL to swap the screen buffer with <code>Display.update()</code>. Thats it, we got a triangle on our screen.</p> <p><img src="/img/2011/11/triangle.png" alt="A red triangle" title="The resulting triangle" /></p> <p>You may notice that if you change the screen size, the triangle will change its shape. That's because we are using the identity shader, which maps the viewport to coordinates between <code>-1.0</code> and <code>1.0</code>. On the next tutorials, we will see how to draw our triangle without losing the proportion.</p> Learn to draw a triangle in Java using OpenGL and LWJGL. This tutorial provides a concise code example demonstrating how to set up a basic OpenGL environment, initialize shaders, and render a red triangle to the screen. The code covers clearing the color buffer, using an identity shader, and updating the display. Improve your understanding of OpenGL fundamentals and begin creating your own 2D graphics. OpenGL SuperBible in Java: The GLBatch class 2011-11-01T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-The-GLBatch <p>On the last tutorials, we saw information about the <code>GLShaderManager</code> class. But creating a shader is just the first step to be able to render your scene. The second step is passing geometry data to your shaders. And that's where the GLBatch class comes to help.</p> <p>As i did with the <code>GLShaderManager</code> class, i broke the GLBatch class into more than 1 class. The first one, <code>GLBatch.java</code> is just an interface with a single method, draw, that receives a single paramater, a Map that contains pointers to the attribute locations. The <code>GLBatch</code> does not know the <code>GLShader</code> class, making the design decoupled.</p> <p>The implementation class, where the real business happens is the <code>SimpleGLShader</code>. This class receives the geometry data in its contructors and uses Buffer Objects to hold the data.</p> <p>There are 2 constructors available. The fist one represents the minimal data needed to create a SimpleGLBatch, which are the vertex array, the element index array and the mode that must be used to create the triangles (<code>GL_TRIANGLES</code>, <code>GL_TRIANGLE_FAN</code>, <code>GL_TRIANGLE_STRIP</code>, etc).</p> <p>The second constructor has all the data supported by the class. Besides the mode, vertex array and index array, this constructor may receive, the color array, normal array and texture array. The last 3 ones may null.</p> <p>The first constructor is actually a shortcurt for the 2nd constructor.</p> <p>Half of the magic from the <code>SimpleGLBatch</code> happens in the constructor. The other half happens inside the draw method.</p> <p>For each array of data that is not null in the constructor, we have to build OpenGL buffers using <code>genBuffer</code>. Here's a sample of the code:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(vColorData </span><span style="color:#ff79c6;">!= </span><span style="color:#bd93f9;">null </span><span style="color:#ff79c6;">&amp;&amp;</span><span style="color:#f8f8f2;"> vColorData</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">length </span><span style="color:#ff79c6;">&gt; </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">FloatBuffer</span><span style="color:#f8f8f2;"> colorData </span><span style="color:#ff79c6;">= </span><span style="font-style:italic;color:#66d9ef;">BufferUtils</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">createFloatBuffer</span><span style="color:#f8f8f2;">(vColorData</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">length); </span><span style="color:#f8f8f2;"> colorData</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">put</span><span style="color:#f8f8f2;">(vColorData); </span><span style="color:#f8f8f2;"> colorData</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">flip</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> colorBuffer </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGenBuffers</span><span style="color:#f8f8f2;">(); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glBindBuffer</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ARRAY_BUFFER</span><span style="color:#f8f8f2;">, colorBuffer); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glBufferData</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ARRAY_BUFFER</span><span style="color:#f8f8f2;">, colorData, </span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_STATIC_DRAW</span><span style="color:#f8f8f2;">); </span><span style="color:#ffffff;">} </span></pre> <p>This method creates a <code>FloatBuffer</code> from the the array, then generates a gl buffer. The last step is filling the GL Buffer with data. Repeat this code for the <code>vertexData</code>, <code>normalData</code> and <code>textureData</code>.</p> <p>The only difference is the index array, which has a similar code, but instead of binding to the <code>GL_ARRAY_BUFFER</code>, binds to the <code>GL_ELEMENT_ARRAY_BUFFER</code>.</p> <p>In the draw method, we draw using the buffers created in the constructor. Heres the code:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(attributeLocations</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">containsKey</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;inColor&quot;</span><span style="color:#f8f8f2;">) </span><span style="color:#ff79c6;">&amp;&amp;</span><span style="color:#f8f8f2;"> colorBuffer </span><span style="color:#ff79c6;">&gt;= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glBindBuffer</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ARRAY_BUFFER</span><span style="color:#f8f8f2;">, colorBuffer); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> colorLocation </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> attributeLocations</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">get</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;inColor&quot;</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glVertexAttribPointer</span><span style="color:#f8f8f2;">(colorLocation, </span><span style="color:#bd93f9;">4</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">GL11</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_FLOAT</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">false</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">4 </span><span style="color:#ff79c6;">* </span><span style="color:#bd93f9;">4</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glEnableVertexAttribArray</span><span style="color:#f8f8f2;">(colorLocation); </span><span style="color:#ffffff;">} </span></pre> <p>Again, the only difference is for the index array:</p> <pre style="background-color:#282a36;"> <span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glBindBuffer</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL15</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ELEMENT_ARRAY_BUFFER</span><span style="color:#f8f8f2;">, indexBuffer); </span></pre> <p>The last step is drawing the elements with a call to <code>glDrawElements</code></p> <pre style="background-color:#282a36;"> <span style="color:#bd93f9;">GL11</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glDrawElements</span><span style="color:#f8f8f2;">(mode, numElements, </span><span style="color:#bd93f9;">GL11</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_UNSIGNED_SHORT</span><span style="color:#f8f8f2;">, </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">); </span></pre> <p>An observation is that the shaders must have a standard name for the attributes. inVe<code>rtex, </code>inColor<code>, </code>inNormal<code>and</code>inTexCoord` for the vertex position, color, normal and texture coordinate.</p> <p>Again, all the code is available at <a href="http://code.google.com/p/opengl-superbible-java/">http://code.google.com/p/opengl-superbible-java/</a></p> Learn how to use the GLBatch class in OpenGL ES 2.0 to efficiently pass geometry data to your shaders. This tutorial covers creating vertex, color, normal, and texture buffers, explains the two-constructor approach for flexible data handling, and provides Java code examples demonstrating buffer creation and drawing using `glDrawElements`. Master efficient OpenGL rendering techniques now! OpenGL SuperBible in Java: The GLShaderManager class - Part2 2011-10-25T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-in-Java-The-GLShaderManager-Part-2 <p>In the <a href="/2011/10/24/OpenGL-Superbible-in-Java-The-GLShaderManager/">last post</a>, we saw how to adapt the <code>GLShaderManager</code> from the C code to Java. But the C code from the book actually has 2 responsabilities, the first is actually being a GLShader, and the second is being a factory to create several default Shaders.</p> <p>In the Java implementation, I decided to separate those responsabilities and created a class <code>GLShaderFactory</code> that has method to create the default shaders. Theres no need to go in detail. The shaders are the same from the book.</p> <p>The code for the <code>GLShaderFactory.java</code> is available <a href="http://code.google.com/p/opengl-superbible-java/source/browse/OpenGLSuperBible/src/openglsuperbible/glutils/GLShaderFactory.java">here</a></p> Learn how to separate GLShader and GLShaderFactory responsibilities in Java for efficient OpenGL programming. This tutorial builds upon a previous post adapting C code to Java, offering a cleaner, more organized approach to creating default shaders. Get the source code now! OpenGL SuperBible in Java: The GLShaderManager class 2011-10-24T00:00:00Z https://bandarra.me/posts/OpenGL-Superbible-in-Java-The-GLShaderManager <p>When I started using the OpenGL SuperBible book to learn OpenGL, I noticed that the first examples from the book used some classes made by the author to encapsulate some complexity that would be explained later. As I was translating the examples do Java code, I had to figure out what those classes did before reading and testing the examples from the book.</p> <p>One of those classes is the GLShaderManager, which I actually transformed in 2 classes. The first is the <code>GLShader.java</code>. The objective of <code>GLShader.java</code> is to receive 2 shader programs: a vertex program and a fragment program, and compile the programs, link them and extract the uniform and attribute ids into a Map on the client side for easier use later.</p> <p>The code in the example is available at: <a href="http://code.google.com/p/opengl-superbible-java/">http://code.google.com/p/opengl-superbible-java/</a></p> <p>I rewrote the examples using <a href="https://www.lwjgl.org/">LWJGL</a>.</p> <p>The main tricks of the class are on the constructor. It receives 2 strings as parameters, the <code>vertexShaderSource</code> and the <code>fragmentShaderSource</code>.</p> <p>So, the first step is compiling the shader sources. The code for compiling the Vertex Shader is as follows:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> vertexShader </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glCreateShader</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_VERTEX_SHADER</span><span style="color:#f8f8f2;">); </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glShaderSource</span><span style="color:#f8f8f2;">(vertexShader, vertexShaderSource); </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glCompileShader</span><span style="color:#f8f8f2;">(vertexShader); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="color:#f8f8f2;"> vertexShaderErrorLog </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetShaderInfoLog</span><span style="color:#f8f8f2;">(vertexShader, </span><span style="color:#bd93f9;">65536</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(vertexShaderErrorLog</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">length</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">!= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">System</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">err</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">println</span><span style="color:#f8f8f2;">( </span><span style="color:#f8f8f2;"> </span><span style="color:#f1fa8c;">&quot;Vertex shader compile log: </span><span style="color:#ff79c6;">\n</span><span style="color:#f1fa8c;">&quot; </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> vertexShaderErrorLog); </span><span style="color:#ffffff;">} </span></pre> <p>First, a shader pointer is created with <code>glCreateShader</code>. The parameter indicates which type of shader to create. The second line attaches the shader source the the shader id. And, on the 3rd line, the shader is compiled.</p> <p>The rest of the code checks if the compilation of the shader was OK and displays a message if something went wrong. (May be a good place to throw an exception).</p> <p>Next thing to do is compiling the Fragment Shader.</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> fragmentShader </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glCreateShader</span><span style="color:#f8f8f2;">(</span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_FRAGMENT_SHADER</span><span style="color:#f8f8f2;">); </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glShaderSource</span><span style="color:#f8f8f2;">(fragmentShader, fragmentShaderSource); </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glCompileShader</span><span style="color:#f8f8f2;">(fragmentShader); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="color:#f8f8f2;"> fragmentShaderErrorLog </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetShaderInfoLog</span><span style="color:#f8f8f2;">(fragmentShader, </span><span style="color:#bd93f9;">65536</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(fragmentShaderErrorLog</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">length</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">!= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">System</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">err</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">println</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;Fragment shader compile log: </span><span style="color:#ff79c6;">\n</span><span style="color:#f1fa8c;">&quot; </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> fragmentShaderErrorLog); </span><span style="color:#ffffff;">} </span></pre> <p>The code is almost the same as compiling the vertex shader, but passing <code>GL_FRAGMENT_SHADER</code> as a parameter to <code>glCreateShader</code>.</p> <p>Now that we've compiled both the vertex shader and fragment shader, we have to link them together in a program.</p> <pre style="background-color:#282a36;"> <span style="color:#f8f8f2;">program </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glCreateProgram</span><span style="color:#f8f8f2;">(); </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glAttachShader</span><span style="color:#f8f8f2;">(program, vertexShader); </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glAttachShader</span><span style="color:#f8f8f2;">(program, fragmentShader); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glLinkProgram</span><span style="color:#f8f8f2;">(program); </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="color:#f8f8f2;"> log </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetProgramInfoLog</span><span style="color:#f8f8f2;">(program, </span><span style="color:#bd93f9;">65536</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">if </span><span style="color:#f8f8f2;">(log</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">length</span><span style="color:#f8f8f2;">() </span><span style="color:#ff79c6;">!= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">System</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">err</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">println</span><span style="color:#f8f8f2;">(</span><span style="color:#f1fa8c;">&quot;Program link log:</span><span style="color:#ff79c6;">\n</span><span style="color:#f1fa8c;">&quot; </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> log); </span><span style="color:#ffffff;">} </span><span style="color:#f8f8f2;"> </span></pre> <p>The first line creates a pointer to the program. Then, both the vertex shader and fragment shaders are attached to the program. The last step is to link them. Again, after linking the program we check if something went wrong.</p> <p>By now, your shaders are compiled, linked and ready to use. But we may do something else. Each uniform or attribute created on your vertex and shader programs receives a pointer to be use the client code. But you have to find out which is which. Heres the code to identify the attribute ids:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> numAttributes </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetProgram</span><span style="color:#f8f8f2;">(program, </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ACTIVE_ATTRIBUTES</span><span style="color:#f8f8f2;">); </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> maxAttributeLength </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetProgram</span><span style="color:#f8f8f2;">(program, </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ACTIVE_ATTRIBUTE_MAX_LENGTH</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> i </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; i </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> numAttributes; i</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="color:#f8f8f2;"> name </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetActiveAttrib</span><span style="color:#f8f8f2;">(program, i, maxAttributeLength); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> location </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetAttribLocation</span><span style="color:#f8f8f2;">(program, name); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">System</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">out</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">println</span><span style="color:#f8f8f2;">(name </span><span style="color:#ff79c6;">+ </span><span style="color:#f1fa8c;">&quot;:&quot; </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> location); </span><span style="color:#f8f8f2;"> attributeLocations</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">put</span><span style="color:#f8f8f2;">(name, location); </span><span style="color:#ffffff;">} </span></pre> <p>First, we find out how many attributes we have. Then the size of the larges attribute name. Then, we loop for each attribute, get its name and its pointer and put it inside a map. The code for the uniform IDs is almost the same:</p> <pre style="background-color:#282a36;"> <span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> numUniforms </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetProgram</span><span style="color:#f8f8f2;">(program, </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ACTIVE_UNIFORMS</span><span style="color:#f8f8f2;">); </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> maxUniformLength </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetProgram</span><span style="color:#f8f8f2;">(program, </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#bd93f9;">GL_ACTIVE_UNIFORM_MAX_LENGTH</span><span style="color:#f8f8f2;">); </span><span style="color:#ff79c6;">for </span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> i </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">0</span><span style="color:#f8f8f2;">; i </span><span style="color:#ff79c6;">&lt;</span><span style="color:#f8f8f2;"> numUniforms; i</span><span style="color:#ff79c6;">++</span><span style="color:#f8f8f2;">) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">String</span><span style="color:#f8f8f2;"> name </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetActiveUniform</span><span style="color:#f8f8f2;">(program, i, maxUniformLength); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> location </span><span style="color:#ff79c6;">= </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glGetUniformLocation</span><span style="color:#f8f8f2;">(program, name); </span><span style="color:#f8f8f2;"> uniformLocations</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">put</span><span style="color:#f8f8f2;">(name, location); </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#66d9ef;">System</span><span style="color:#ff79c6;">.</span><span style="color:#f8f8f2;">out</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">println</span><span style="color:#f8f8f2;">(name </span><span style="color:#ff79c6;">+ </span><span style="color:#f1fa8c;">&quot;:&quot; </span><span style="color:#ff79c6;">+</span><span style="color:#f8f8f2;"> location); </span><span style="color:#ffffff;">} </span></pre> <p>The code to activate the shader is pretty simple:</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">useShader</span><span style="color:#f8f8f2;">() </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="color:#6272a4;">//Enable shader </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glUseProgram</span><span style="color:#f8f8f2;">(program); </span><span style="color:#ffffff;">} </span></pre> <p>And this is how we encapsulate setting a uniform value. There, we can see the Map we made at the constructor in action. We could opt to not creating the map and getting the location on the fly on this method.</p> <pre style="background-color:#282a36;"> <span style="color:#ff79c6;">public </span><span style="font-style:italic;color:#8be9fd;">void </span><span style="color:#50fa7b;">setUniformMatrix4</span><span style="color:#f8f8f2;">(</span><span style="font-style:italic;color:#66d9ef;">String</span><span style="color:#f8f8f2;"> uniformName, </span><span style="font-style:italic;color:#8be9fd;">boolean</span><span style="color:#f8f8f2;"> traverse, </span><span style="font-style:italic;color:#66d9ef;">FloatBuffer</span><span style="color:#f8f8f2;"> matrixdata) </span><span style="color:#ffffff;">{ </span><span style="color:#f8f8f2;"> </span><span style="font-style:italic;color:#8be9fd;">int</span><span style="color:#f8f8f2;"> location </span><span style="color:#ff79c6;">=</span><span style="color:#f8f8f2;"> uniformLocations</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">get</span><span style="color:#f8f8f2;">(uniformName); </span><span style="color:#f8f8f2;"> </span><span style="color:#bd93f9;">GL20</span><span style="color:#ff79c6;">.</span><span style="color:#50fa7b;">glUniformMatrix4</span><span style="color:#f8f8f2;">(location, traverse, matrixdata); </span><span style="color:#ffffff;">} </span></pre> <p>Thats it. Thats the first part of my port of the GLShaderManager class to Java using lwjgl. I've also ported this code to Android (2.2+) with minimal changes.</p> Learn to create and manage shaders in Java using LWJGL. This tutorial shows you how to compile vertex and fragment shaders, link them into a program, and retrieve attribute and uniform locations for efficient use in your OpenGL applications. The code examples cover shader compilation, linking, and utilizing a map for easy access to shader locations. Improve your OpenGL development skills today! VBOs on Android 2.2 2011-10-13T00:00:00Z https://bandarra.me/posts/VBOs-on-Android-2-2 <p>Android 2.2 was the first Android version to support OpenGL ES 2.0. The problem is that it came out with a big flaw. It does not support VBOs. The advantage of using VBOs is that besides having improved performance, the memory used for those objects do not count for the app used heap size. So, the developer is able to have more and more complex models in the app.</p> <p>The issue has been fixed on Android 2.3+, but for devs who still want this feature on 2.2, i found this post: <a href="http://code.google.com/p/android/issues/detail?id=8931">http://code.google.com/p/android/issues/detail?id=8931</a></p> <p>Basically, you can use JNI to access the function that is not available on Android!</p> Bypass Android 2.2 OpenGL ES 2.0 VBO limitations using JNI for enhanced performance and increased model complexity in your apps. Learn how to overcome this flaw and unlock access to features unavailable in older Android versions. Books on Java Game Programming 2011-10-09T14:37:00Z https://bandarra.me/posts/Books-on-Java-Game-Programming <p>I started to learn about game programming as probably most people do: by using google and finding information about it. It was enough for me to put a few games together and be happy with them. But when i decided that i wanted to get more deep on the subject, i started to search for books.</p> <p>I had a preference for books that were coded in Java for mainly two reasons. The first is that i'm well experienced in Java. The second, and most important, i wanted to port the knowledge gained on those books to the Android Platform, which also used Java.</p> <p>My main source search was amazon.com and the book reviews there. I bought two books on the subject and got very satisfied with them.</p> <p><img src="https://lh5.googleusercontent.com/proxy/IX4EgILWbQqGXUWe5rZ7QW6ghdRiG_wXFXRilGizyt5wnjfIEoTo9YmUey5XiIUhCT8rSCjONgopFZRPj1U-G-p7VjK5Kas3NsHFMotUPFKNlwG6NQCeqXxKk_D0aaWfqmbGvS4OjN3ANUEYVH4CyXsW_pmXkZeeXLNtOYiBSmpwBtTONHiUZrhQI66zNSJI7uyitixQjg=s0-d" alt="Killer Game Programming in Java" title="Killer Game Programming in Java book cover" /></p> <p>The first book I got was <a href="http://www.amazon.com/Killer-Game-Programming-Andrew-Davison/dp/0596007302/ref=sr_1_1?ie=UTF8&amp;qid=1318079333&amp;sr=8-1">Killer Java Game Programming in Java</a>, by Andrew Davison, published by O'Reilly. Although I had the impression that the book is not the best example on OO programming and code reuse, the book has a lot of great examples of how to do things in Java. The chapter about coding the game loop alone is worth the entire book. Actually, it was the base for my own game loop code. There are many cool examples on Java 2D. The downside of the book is that the chapters on 3D rely heavily on Java 3D, which is OK if you want do use that API on your code. But i wanted things on a "lower level" so that i could port to Android. The chapters on AI and Pathfinding are great too!</p> <p><img src="https://lh6.googleusercontent.com/proxy/VGC7XpKk3SYwe0ErFf92hio6hzB-XZZcSTWyjuIn-4tpxeWfOrnf4ba9pO4UniONbsfqYgXlWGDGZWSEPicV95ioQi2Sj6YD5tLTdGvxx9kR5Oe0uW8_=s0-d" alt="Developing Games in Java" title="Developing Games in Java book cover" /></p> <p>The second book was <a href="http://www.amazon.com/Developing-Games-Java-David-Brackeen/dp/1592730051/ref=sr_1_1?ie=UTF8&amp;qid=1318079346&amp;sr=8-1">Developing Games in Java</a>, by David Brackeen. This book is somewhat different from the first one. So, i wouldnt recommend buying one or the other, they actually complement each other. This one is more OO friendly, so it will give you more insights in actually building a reusable game engine, which is, actually, the stronger point of this book. The 3D chapters are awesome. The author actually teaches the reader how to code a 3D software renderer, from scratch, in Java. Although the software renderer is not much useful in the real world, it gives the reader a lot of knowledge on how hardware renderers actually work, making much easier to learn and work with them.</p> Learn Java game programming with these two recommended books. "Killer Game Programming in Java" excels at practical examples and game loop coding, while "Developing Games in Java" focuses on object-oriented design and building a 3D software renderer from scratch, providing valuable insights into game engine architecture and hardware rendering. Both books are great resources for Android game development. OpenGL 2011-09-25T10:02:00Z https://bandarra.me/posts/OpenGL <p><img src="https://www.opengl.org/archives/resources/features/KilgardTechniques/LensFlare/glflare1.gif" alt="OpenGL" title="The OpenGL logo" /></p> <p>Since i started coding games for Android, I wanted to learn OpenGL. Besides enabling the development of 3D games, OpenGL is supposed to give the app a performance boost on 2D games.</p> <p>Android has OpenGL ES bindings for OpenGL ES 1.0 and OpenGL ES 2.0 (if you are on Android 2.2+). Since i didn't know much about OpenGL coding, I searched for good books one the subject.</p> <p>I found <a href="http://www.amazon.com/OpenGL-SuperBible-Comprehensive-Tutorial-Reference/dp/0321712617/ref=sr_1_1?ie=UTF8&amp;qid=1316983191&amp;sr=8-1">OpenGL SuperBible</a>, which covers OpenGL 3.3 Core Profile and a bit about OpenGL ES 2.0 and <a href="http://www.amazon.com/OpenGL-ES-2-0-Programming-Guide/dp/0321502795/ref=sr_1_1?ie=UTF8&amp;qid=1316983205&amp;sr=8-1">OpenGL ES 2.0 Programming</a> Guide which covers OpenGL ES 2.0 in depth.</p> <p>The downside for both books is that none of them cover Android OpenGL coding and both books have code examples in C. So, I decided to break my learning curve into a few parts:</p> <ol> <li>Review my C/C++ knowledge so I can have a better understanding of the books.</li> <li>Understand OpenGL programming in general (Code and ES) and use this knowledge on Java Programming</li> <li>Use the OpenGL + Java programming skills on Android.</li> </ol> <p>The starting place for my research was <a href="http://www.opengl.org/resources/bindings/">this page</a> on <a href="http://opengl.org/">opengl.org</a>, When checking out those links, I found out that a many of them had not been updated for OpenGL 3.<em>, or even OpenGL 2.</em>. Java 3D is more than an OpenGL Binding and is more like a <a href="http://en.wikipedia.org/wiki/Scene_graph">SceneGraph API</a> filled with lots of bells and whistles. The real contenders where <a href="http://jogamp.org/jogl/www/">JOGL</a> and <a href="http://lwjgl.org/">lwjgl</a>. So, I researched both API's in more depath and found that lwjg is leaner than JOGL and gives me access to all the methods I need to implement the code from the OpenGL SuperBible book and would make an easier port to the Android OpenGL API too, which is nothing more than bindings to the OpenGL ES 2 API.</p> <p>Of course, nothing comes for free. I had to (and I'm still) translating o lot of Math code from the C++ book to Java. My intention is to get some tutorials done and help people who have the same goals!</p> <p>Stay tuned for updates soon!</p> Learn OpenGL for Android game development using LWJGL. This guide covers transitioning from C/C++ OpenGL knowledge to Java, focusing on LWJGL as a leaner, efficient binding for OpenGL ES 2.0 on Android, perfect for porting examples from OpenGL SuperBible. Start building high-performance 2D and 3D games today!