<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sebastián Rojas Ricaurte</title>
    <description>The latest articles on DEV Community by Sebastián Rojas Ricaurte (@quedicesebas).</description>
    <link>https://dev.to/quedicesebas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F368638%2Fac9bec82-5fdb-408d-88ff-ce4c04fde837.jpg</url>
      <title>DEV Community: Sebastián Rojas Ricaurte</title>
      <link>https://dev.to/quedicesebas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quedicesebas"/>
    <language>en</language>
    <item>
      <title>Integrate Google Gemini AI to your Angular 17 app</title>
      <dc:creator>Sebastián Rojas Ricaurte</dc:creator>
      <pubDate>Thu, 14 Mar 2024 01:25:51 +0000</pubDate>
      <link>https://dev.to/quedicesebas/integrate-google-gemini-ai-to-your-angular-17-app-3b6</link>
      <guid>https://dev.to/quedicesebas/integrate-google-gemini-ai-to-your-angular-17-app-3b6</guid>
      <description>&lt;p&gt;A step-by-step guide: build a simple application to test Gemini Pro and Gemini Pro Visual via the official client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is Google Gemini?&lt;/li&gt;
&lt;li&gt;Google AI Studio API key&lt;/li&gt;
&lt;li&gt;Create the Angular Application&lt;/li&gt;
&lt;li&gt;Logic code

&lt;ul&gt;
&lt;li&gt;Initialize model&lt;/li&gt;
&lt;li&gt;Generate text from text-only input (text)&lt;/li&gt;
&lt;li&gt;Generate text from text-and-images input (multimodal)&lt;/li&gt;
&lt;li&gt;Build multi-turn conversations (chat)&lt;/li&gt;
&lt;li&gt;Generate content using streaming (stream)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Template code&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Google Gemini?
&lt;/h2&gt;

&lt;p&gt;Gemini is a multimodal AI model that can understand and generate text, as well as other types of information like audio, images, videos, and code. Gemini is Google's most capable AI model, and is the first to outperform human experts on MMLU (Massive Multitask Language).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ultra. 1.0 (preview): Most capable model for large-scale, highly complex text and image reasoning tasks coming in early 2024. Used by Gemini Advanced (formerly Bard):&lt;/li&gt;
&lt;li&gt;Pro. 1.01.5 (available): The best performing model with features for a wide variety of text and image reasoning tasks.&lt;/li&gt;
&lt;li&gt;Nano. 1.0 (preview): The most efficient model built for on-device experiences, enabling offline use cases. Leverages device processing power at no cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition to Gemini models, exists the Gemma open models.&lt;br&gt;
Gemma is a family of lightweight, state-of-the-art open models built from the same research and technology used to create the Gemini models.&lt;/p&gt;
&lt;h2&gt;
  
  
  Google AI Studio API key
&lt;/h2&gt;

&lt;p&gt;Go to &lt;a href="https://aistudio.google.com/app/apikey" rel="noopener noreferrer"&gt;aistudio.google.com&lt;/a&gt; and create an API key.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxq264i9s76lvv1uokmco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxq264i9s76lvv1uokmco.png" alt="Google AHI JavaScript SDK" width="720" height="360"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Create the Angular Application
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;ng new google-gemini-angular-demo --ssr=false&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setting up
&lt;/h3&gt;

&lt;p&gt;Once done, let's create an environment variable to store the just generated API key. Go to the created folder and run &lt;code&gt;ng g environments&lt;/code&gt;. Then add the &lt;code&gt;googleAiApiKey&lt;/code&gt; to both environment files.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;src/environments/environment.ts&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const environment = { googleAiApiKey: '' };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;src/environments/environment.development.ts&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const environment = {
  googleAiApiKey: 'AIzaSyD-XZY53eApU74AkLfUZPrWfv49geU1dfw',
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the official client to access Gemini models. Run &lt;code&gt;npm i @google/generative-ai&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logic code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Initialize model
&lt;/h3&gt;

&lt;p&gt;Add this to your &lt;code&gt;app.component.ts&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
/**
   * Creates, configure with defaults, and returns the Google Gemini model from the SDK
   * @param model 'gemini-pro' | 'gemini-pro-vision'
   * @returns
   */
  private initializeModel(model: 'gemini-pro' | 'gemini-pro-vision') {
    const googleGenerativeAI = new GoogleGenerativeAI(
      environment.googleAIApiKey
    );
    const generationConfig = {
      safetySettings: [
        {
          category: HarmCategory.HARM_CATEGORY_HARASSMENT,
          threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
        },
      ],
      temperature: 0.9,
      top_p: 1,
      top_k: 32,
      maxOutputTokens: 100, // limit output
    };
    return googleGenerativeAI.getGenerativeModel({
      model: model,
      ...generationConfig,
    });
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can utilize the &lt;code&gt;https://ai.google.dev/docs/safety_setting_gemini&lt;/code&gt; (block medium or high) or change them to suit your needs. In the example, we raised the harassment level to prohibit outputs with a low or high risk of being dangerous.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://ai.google.dev/models/gemini&lt;/code&gt; that are currently available, along with their default settings. There is a limit of 60 requests per minute. Learn more about model parameters &lt;a href="https://ai.google.dev/docs/concepts#model-parameters" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, let's add the four demo methods to the &lt;code&gt;app.component.ts&lt;/code&gt; file. Fist, add some code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
  history: any;
  prompt?: string;
  multipartPrompt?: Part[];
  stringsToBeTyped: string[] = [];
...
  private clean() {
    this.history = undefined;
    this.prompt = undefined;
    this.multipartPrompt = undefined;
    this.stringsToBeTyped = [];
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Generate text from text-only input (text)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; /**
   * Demonstrates Gemini Pro with a text-only input.
   */
  async textOnlyDemo() {
    this.clean();

    this.prompt =
      "You are a local historian researching the Bennington Triangle disappearances. Write a news report for a national audience detailing your recent findings, including interviews with eyewitnesses (vary details for each response - sightings of strange lights, unusual sounds, personal connection to a missing person). Maintain a neutral tone, presenting the facts while acknowledging the case's lack of resolution.";
    const result = await this.initializeModel('gemini-pro').generateContent(
      this.prompt
    );
    const response = await result.response;
    this.stringsToBeTyped = [response.text()];
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Generate text from text-and-images input (multimodal)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
   * Demonstrates how to use Gemini Pro Vision with text and images as input (using an image in src/assets for convenience).
   */
  async multimodalDemo() {
    this.clean();

    try {
      let imageBase64 = await inject(FileConversionService).convertToBase64(
        'assets/cheesecake.jpg'
      );

      // Check for successful conversion to Base64
      if (typeof imageBase64 !== 'string') {
        console.error('Image conversion to Base64 failed.');
        return;
      }

      this.multipartPrompt = [
        {
          inlineData: {
            mimeType: 'image/jpeg',
            data: imageBase64,
          },
        },
        {
          text: 'Provide a recipe.',
        },
      ];
      const result = await this.initializeModel(
        'gemini-pro-vision'
      ).generateContent(this.multipartPrompt);
      const response = await result.response;
      this.stringsToBeTyped = [response.text()];
    } catch (error) {
      console.error('Error converting file to Base64', error);
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to put a &lt;a href="https://github.com/quedicesebas/google-gemini-angular-demo/blob/main/src/assets/cheesecake.jpg" rel="noopener noreferrer"&gt;cheesecake image&lt;/a&gt; under &lt;code&gt;assets/cheesecake.jpg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Image requirements for Gemini:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MIME types supported include: image/png, image/jpeg, image/webp, image/heic, and image/heif.&lt;/li&gt;
&lt;li&gt;There are a maximum of 16 photos.&lt;/li&gt;
&lt;li&gt;A maximum of 4MB, including photos and text.&lt;/li&gt;
&lt;li&gt;Large photos are scaled down to 3072 by 3072 pixels while maintaining their original aspect ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to convert the input image into Base64, let's create a FileConversion service. Run &lt;code&gt;ng g s FileConversion&lt;/code&gt;. In the &lt;code&gt;file-conversion.service.ts&lt;/code&gt; generated file, replace contents with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class FileConversionService {
  constructor(private http: HttpClient) {}
  async convertToBase64(
    filePath: string
  ): Promise&amp;lt;string | ArrayBuffer | null&amp;gt; {
    const blob = await firstValueFrom(
      this.http.get(filePath, { responseType: 'blob' })
    );
    return new Promise((resolve, reject) =&amp;gt; {
      const reader = new FileReader();
      reader.onloadend = () =&amp;gt; {
        const base64data = reader.result as string;
        resolve(base64data.substring(base64data.indexOf(',') + 1)); // Extract only the Base64 data
      };
      reader.onerror = (error) =&amp;gt; {
        reject(error);
      };
      reader.readAsDataURL(blob);
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be sure to provide the HttpClient service in your app configuration file &lt;code&gt;app.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()],
};

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build multi-turn conversations (chat)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;You can utilize the first user message in the history as a system prompt. Just remember to include a model response that acknowledges the directions. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User: could take on the character of a superhero and write in that style. Do not lose your character. Please respond if you understand these instructions.&lt;/li&gt;
&lt;li&gt;Model: I understand.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
   * Demonstrates how to use Gemini Pro to build a multi-turn conversation
   */
  async chatDemo() {
    this.clean();
    this.history = [
      {
        role: 'user',
        parts: 'Hi, Gemini!',
      },
      {
        role: 'model',
        parts: "It's great to meet you. What do you want to know?",
      },
    ];

    const chat = this.initializeModel('gemini-pro').startChat({
      history: this.history,
      generationConfig: {
        maxOutputTokens: 100,
      },
    });

    this.prompt = 'What is the largest number with a name? Brief answer.';
    const result = await chat.sendMessage(this.prompt);
    const response = await result.response;
    this.stringsToBeTyped = [response.text()];
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Generate content as is created using streaming (stream)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
   * Demonstrates how to use Gemini Pro to generate content using streaming
   */
  async streamDemo() {
    this.clean();
    this.prompt = 'Generate a poem.';

    const prompt = {
      contents: [
        {
          role: 'user',
          parts: [
            {
              text: this.prompt,
            },
          ],
        },
      ],
    };
    const streamingResp = await this.initializeModel(
      'gemini-pro'
    ).generateContentStream(prompt);

    for await (const item of streamingResp.stream) {
      console.log('stream chunk: ' + item.text());
      this.stringsToBeTyped.push('stream chunk:  ' + item.text());
    }
    console.log(
      'aggregated response: ' + (await streamingResp.response).text()
    );
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Template code
&lt;/h2&gt;

&lt;p&gt;Let's add some code to our main component template in order to try this on the browser.&lt;/p&gt;

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

&lt;p&gt;Add the Typed.js wrapper for Angular so we can create the type effect. Run &lt;code&gt;npm i ngx-typed-js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Add this to the &lt;code&gt;app.component.html&lt;/code&gt; file (complete code here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;main class="main"&amp;gt;
  &amp;lt;div class="content"&amp;gt;
...
      &amp;lt;h1&amp;gt;Google Gemini Angular Demo&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;Please choose a demo:&amp;lt;/h2&amp;gt;
      &amp;lt;div class="btn-group"&amp;gt;
        &amp;lt;button (click)="textOnlyDemo()"&amp;gt;Unique prompt&amp;lt;/button&amp;gt;
        &amp;lt;button (click)="multimodalDemo()"&amp;gt;Image prompt&amp;lt;/button&amp;gt;
        &amp;lt;button (click)="chatDemo()"&amp;gt;Prompt with chat history&amp;lt;/button&amp;gt;
        &amp;lt;button (click)="streamDemo()"&amp;gt;Streaming&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
      @if(prompt || multipartPrompt) { @if(history) {
      &amp;lt;h2&amp;gt;Chat history&amp;lt;/h2&amp;gt;
      @for(item of history; track item) {
      &amp;lt;p&amp;gt;
        &amp;lt;b&amp;gt;{{ item.role }}&amp;lt;/b
        &amp;gt;: @if (item.parts[0].length &amp;gt; 1) {
        &amp;lt;i&amp;gt;{{ item.parts.join(", ") }}&amp;lt;/i&amp;gt; }@else {&amp;lt;i&amp;gt;{{ item.parts }}&amp;lt;/i
        &amp;gt;}
      &amp;lt;/p&amp;gt;
      } } @if (multipartPrompt) {
      &amp;lt;h2&amp;gt;Prompt&amp;lt;/h2&amp;gt;
      @for(part of multipartPrompt; track part) { @if(part.inlineData) {
      &amp;lt;img
        src="{{ 'data:{{part.inlineData}};base64,' + part.inlineData.data }}"
        style="max-width: 300px; max-height: 200px"
      /&amp;gt;
      }@else {
      &amp;lt;h3&amp;gt;{{ part.text }}&amp;lt;/h3&amp;gt;
      } } }@else {
      &amp;lt;h2&amp;gt;Prompt: {{ prompt }}&amp;lt;/h2&amp;gt;
      } @for (string of stringsToBeTyped; track string) {
      &amp;lt;ngx-typed-js [strings]="[string]"&amp;gt;
        &amp;lt;pre class="typing"&amp;gt;&amp;lt;/pre&amp;gt;
      &amp;lt;/ngx-typed-js&amp;gt;
      } @empty {
      &amp;lt;p&amp;gt;Waiting for response to start&amp;lt;/p&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;div class="dot-flashing"&amp;gt;&amp;lt;/div&amp;gt;
      } }
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/main&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;Add this to &lt;code&gt;src/style.scss&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.typed-cursor {
    display: none !important;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Andd change the &lt;code&gt;src/app/app.component.scss&lt;/code&gt; contents to &lt;a href="https://github.com/quedicesebas/google-gemini-angular-demo/blob/main/src/app/app.component.scss" rel="noopener noreferrer"&gt;this&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Great! You are now able to use Gemini features. If you get lost along the way, refer to the &lt;a href="https://github.com/quedicesebas/google-gemini-angular-demo" rel="noopener noreferrer"&gt;complete code&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>gemini</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building cross-platform Tetris game from a single codebase with Angular 17</title>
      <dc:creator>Sebastián Rojas Ricaurte</dc:creator>
      <pubDate>Sun, 25 Feb 2024 23:34:57 +0000</pubDate>
      <link>https://dev.to/quedicesebas/building-cross-platform-tetris-game-from-a-single-codebase-with-angular-17-5af9</link>
      <guid>https://dev.to/quedicesebas/building-cross-platform-tetris-game-from-a-single-codebase-with-angular-17-5af9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Learn how to build extensible applications with Angular and reuse your code and abilities to build apps for any deployment target —web, mobile web, iOS, Android, and desktops (macOS, Windows, and Linux).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Based on an &lt;a href="https://angularindepth.com/posts/1420/building-multi-platform-apps-from-a-single-codebase-using-angular" rel="noopener noreferrer"&gt;original article&lt;/a&gt; by &lt;a href="https://twitter.com/sliqric" rel="noopener noreferrer"&gt;Richard Sithole&lt;/a&gt;. Angular and Electron versions updated an text cropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use a single codebase?
&lt;/h2&gt;

&lt;p&gt;Monorepo-style development is an approach where you develop multiple projects in the same repository. Projects can depend on each other and allows code-sharing.&lt;/p&gt;

&lt;p&gt;Code changes in one project do not necessarily force all other projects to be rebuilt. You only rebuild or retest the projects affected by the code change. As a result, your Continuous Integration (CI) pipeline is faster, while maintaining your teams' independence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Angular?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Learn one way to build applications with Angular and reuse your code and abilities to build apps for any deployment target. For web, mobile web, native mobile and native desktop".&lt;br&gt;
— &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2F" rel="noopener noreferrer"&gt;Angular.io docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Angular provides tooling which allows you to build features quickly with simple declarative templates. This article assumes basic knowledge of Angular and its &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2Fguide%2Fstyleguide" rel="noopener noreferrer"&gt;best practices&lt;/a&gt;. If not, &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2Ftutorial" rel="noopener noreferrer"&gt;try it out&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Much has been &lt;a href="https://medium.com/r/?url=https%3A%2F%2Findepth.dev%2Fangular" rel="noopener noreferrer"&gt;written about Angular&lt;/a&gt; and other JavaScript frameworks. As the original article author, I have had a great developer experience with Angular. Check the &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DksVQ68wHR9c" rel="noopener noreferrer"&gt;Richard talk about why Angular has been his framework of choice&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Electron?
&lt;/h2&gt;

&lt;p&gt;Electron is a framework that enables developers to create cross-platform desktop applications with JavaScript, HTML, and CSS. These apps can then be packaged to run directly on the OS or distributed via the Mac App Store or the Microsoft Store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Capacitor?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://medium.com/r/?url=https%3A%2F%2Fcapacitorjs.com%2F" rel="noopener noreferrer"&gt;Capacitor&lt;/a&gt; is an open source native runtime for building web native apps. It allows you to target iOS, Android, and Progressive Web Apps (PWAs) platforms with JavaScript, HTML, and CSS. Also, it provides access to the full Native SDKs on each platform, so you can deploy to the App Stores while still able to target the web.&lt;/p&gt;

&lt;p&gt;If you're familiar with &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fcordova.apache.org%2F" rel="noopener noreferrer"&gt;Cordova&lt;/a&gt;, check the differences according to Capacitor.js creators here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to the sample application (and tools)
&lt;/h2&gt;

&lt;p&gt;We will go through each step of weaving together all the target platforms starting with Electron, then Android and finally iOS. I recommend you to start from scratch, but if you have an existing Angular application, you will see where and how you can add support for other platforms.&lt;/p&gt;

&lt;p&gt;If you just want to see the end result (or fork the project), &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Fquedicesebas%2Fangular-cross-platform-tetris" rel="noopener noreferrer"&gt;here&lt;/a&gt; is the git repo.&lt;/p&gt;

&lt;p&gt;In any case, I encourage every reader to go through the details as it might come in handy down the line when your app is all grown up, and you have to debug it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should I care?
&lt;/h2&gt;

&lt;p&gt;In this article, I will show how we can use our Angular knowledge to build a game application using the Angular framework. As a default, you will be able to play the game on the web. We will add Electron to have the game installable on a computer. Lastly, we will enable our game to be deployable on a mobile device for maximum user reach. All this from a single (and simple) repository.&lt;/p&gt;

&lt;p&gt;As an aside, if you are interested in game development with Angular, you can check out this article, which our game application is based on. I will not dwell in the details of how the game is built, as the article goes in-depth and shows you how to build one from scratch. Our focus will be on how to stitch together disparate technologies to help us, not only to understand all the moving parts, but also enable us to target as many platforms as possible. Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the project
&lt;/h2&gt;

&lt;p&gt;First, let us start by globally installing &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fcli.angular.io%2F" rel="noopener noreferrer"&gt;Angular-CLI&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g @angular/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;‌Since we plan to have multiple applications in the workspace, we create an empty workspace using &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2Fcli%2Fnew" rel="noopener noreferrer"&gt;ng new&lt;/a&gt; and set the —createApplication option to false:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng new cross-platform-monorepo --create-application=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to the new created folder and then add the first Angular application to the workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd cross-platform-monorepo
ng generate application tetris --ssr=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above allows a workspace name to be different from the initial app name, and ensures that all applications (and libraries) reside in the &lt;code&gt;/projects&lt;/code&gt; subfolder, matching the workspace structure of the &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2Fguide%2Fworkspace-config" rel="noopener noreferrer"&gt;configuration file&lt;/a&gt; in angular.json.&lt;/p&gt;

&lt;p&gt;Add the second Angular application (placeholder) to the workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng generate application tetris2 --ssr=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to reuse code in our applications, we can take advantage of the Angular multi-project architecture and create a library project which will hold the game engine logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng generate library game-engine-lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By separating the shell parts (apps) from the logic parts (libs), we ensure that our code is manageable, extendible, and reusable between teams.‌‌&lt;/p&gt;

&lt;h2&gt;
  
  
  Game code
&lt;/h2&gt;

&lt;p&gt;Now that we have the core structure of our game application, the next thing is to add the code for our game.‌‌&lt;/p&gt;

&lt;p&gt;As mentioned above, the game is based on &lt;a href="https://medium.com/angular-in-depth/game-development-tetris-in-angular-64ef96ce56f7" rel="noopener noreferrer"&gt;this article&lt;/a&gt;, which also contains a link to the GitHub repository which you can find &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Fmelcor76%2Fng-tetris" rel="noopener noreferrer"&gt;here&lt;/a&gt;. We have modified the code a bit to demonstrate the use of native APIs (e.g. &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fcapacitorjs.com%2Fdocs%2Fapis%2Ffilesystem" rel="noopener noreferrer"&gt;file system access&lt;/a&gt;) both on desktop and mobile apps, and improving design 🤩. More on this later.‌‌&lt;/p&gt;

&lt;h3&gt;
  
  
  Using libraries in Angular apps‌‌
&lt;/h3&gt;

&lt;p&gt;Angular framework allows us to easily build npm libraries. We do not have to &lt;a href="https://medium.com/r/?url=https%3A%2F%2Findepth.dev%2Fposts%2F1238%2Fcomplete-beginner-guide-to-publishing-an-angular-library-to-npm" rel="noopener noreferrer"&gt;publish&lt;/a&gt; a library to the &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fwww.npmjs.com%2F" rel="noopener noreferrer"&gt;npm package manager&lt;/a&gt; to use it in our own apps, however, we cannot use a library before it is built so let us do that now.‌‌&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: I use the terms "lib" and "library" interchangeably —both refer to an Angular library as described here&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the terminal of your choice, run this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng build game-engine-lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the operation was successful, you should see an output like the one below:&lt;/p&gt;

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

&lt;p&gt;To make our lives a little easier, let's add some scripts to the package.json file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
    "ng": "ng",
    "serve:ssr:tetris": "node dist/tetris/server/server.mjs",
    "start:tetris": "ng serve tetris -o",
    "build:tetris": "ng build tetris",
    "test:tetris": "ng test tetris",
    "lint:tetris": "ng lint tetris",
    "e2e:tetris": "ng e2e tetris",
    "start:tetris2": "ng serve tetris2 -o",
    "build:game-engine-lib": "ng build game-engine-lib --watch",
    "test:game-engine-lib": "ng test game-engine-lib",
    "lint:game-engine-lib": "ng lint game-engine-lib",
    "e2e:game-engine-lib": "ng e2e game-engine-lib"
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, we will use &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2Fguide%2Fcreating-libraries%23use-typescript-path-mapping-for-peer-dependencies" rel="noopener noreferrer"&gt;TypeScript path mapping&lt;/a&gt; for peer dependencies to reference our lib within our apps.‌‌&lt;/p&gt;

&lt;p&gt;In the root &lt;em&gt;tsconfig.json&lt;/em&gt; inside &lt;em&gt;compilerOptions&lt;/em&gt;, modify the code as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"paths": {
      "@game-engine-lib": ["./dist/game-engine-lib"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: I prefer to add "@" in front of the library name to easily distinguish it from local file imports.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the &lt;em&gt;game-engine-lib.service.ts&lt;/em&gt; file, add the following getter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get testing(): string {
    return "GameEngineLibService works!";
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each time we make changes to a lib, we need to rebuild it —alternatively, we can use the &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2Fguide%2Fcreating-libraries%23building-and-rebuilding-your-library" rel="noopener noreferrer"&gt;&lt;em&gt;-watch&lt;/em&gt; flag&lt;/a&gt; to automatically do so on file save.‌‌&lt;/p&gt;

&lt;p&gt;Let's rebuild the lib using one of the scripts we have just added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run build:game-engine-lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let us test whether or not we are able to consume the exports specified in the &lt;em&gt;public-api.ts&lt;/em&gt; file.‌‌&lt;/p&gt;

&lt;p&gt;In the app.componet.ts of the tetris app, import and usethe service (In Angular 17's standalone mode, where the traditional app.module.ts file is not automatically generated, you can organize your application without a central module file. Instead, you can import the necessary modules directly into the components where they are needed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { GameEngineLibService } from '@game-engine-lib';
constructor(private engineService: GameEngineLibService) {
    console.info(engineService.testing);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, in your terminal, serve the tetris app using one of the scripts we added earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run start:tetris
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the app is compiled and the browser window opened, you should see the following:‌‌&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5slo6qcm2u5qyiiu1k8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5slo6qcm2u5qyiiu1k8.png" alt="Image description" width="800" height="449"&gt;&lt;/a&gt;&lt;br&gt;
‌‌‌&lt;br&gt;
Pat yourself on the back, stretch your legs and when you are ready, let us continue to the fun(ky) parts.‌‌‌&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The following section involves moving files. If you feel lost, compare your file structure with that of the &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Fquedicesebas%2Fangular-cross-platform-tetris" rel="noopener noreferrer"&gt;finished project&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Adding game code‌‌
&lt;/h2&gt;

&lt;p&gt;Since we are working in a multi-project repository, we need to &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fangular.io%2Fguide%2Fcreating-libraries%23refactoring-parts-of-an-app-into-a-library" rel="noopener noreferrer"&gt;re-organize&lt;/a&gt; the game code a bit. The "utility" parts of the code will go into the library, and the "shell" will be the application project (tetris folder). We will leave the tetris2 app as is for the time being. ‌‌&lt;/p&gt;

&lt;p&gt;To keep our code well-organized, let's create a &lt;em&gt;components&lt;/em&gt; sub-folder inside the lib folder (i.e., projects/game-engine-lib/src/lib):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng g c components/board --project=game-engine-lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change board.component.ts code to this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {
  Component,
  ViewChild,
  ElementRef,
  OnInit,
  HostListener,
} from '@angular/core';
import {
  COLS,
  BLOCK_SIZE,
  ROWS,
  COLORS,
  COLORSLIGHTER,
  LINES_PER_LEVEL,
  LEVEL,
  POINTS,
  KEY,
  COLORSDARKER,
} from '../../constants';
import { Zoundfx } from 'ng-zzfx';
import { IPiece, PieceComponent } from '../piece/piece.component';
import { GameEngineLibService } from '../../services/game-engine-lib.service';

@Component({
  selector: 'game-board',
  templateUrl: 'board.component.html',
})
export class BoardComponent implements OnInit {
  @ViewChild('board', { static: true })
  canvas!: ElementRef&amp;lt;HTMLCanvasElement&amp;gt;;
  @ViewChild('next', { static: true })
  canvasNext!: ElementRef&amp;lt;HTMLCanvasElement&amp;gt;;
  ctx!: CanvasRenderingContext2D;
  ctxNext!: CanvasRenderingContext2D;
  board!: number[][];
  piece!: PieceComponent;
  next!: PieceComponent;
  requestId!: number;
  paused!: boolean;
  gameStarted!: boolean;
  time!: { start: number; elapsed: number; level: number };
  points!: number;
  highScore!: number;
  lines!: number;
  level!: number;
  moves = {
    [KEY.LEFT as string]: (p: IPiece): IPiece =&amp;gt; ({ ...p, x: p.x - 1 }),
    [KEY.RIGHT as string]: (p: IPiece): IPiece =&amp;gt; ({ ...p, x: p.x + 1 }),
    [KEY.DOWN as string]: (p: IPiece): IPiece =&amp;gt; ({ ...p, y: p.y + 1 }),
    [KEY.SPACE as string]: (p: IPiece): IPiece =&amp;gt; ({ ...p, y: p.y + 1 }),
    [KEY.UP as string]: (p: IPiece): IPiece =&amp;gt; this.service.rotate(p),
  };
  playSoundFn!: Function;

  @HostListener('window:keydown', ['$event'])
  keyEvent(event: KeyboardEvent) {
    if (event.key === KEY.ESC) {
      this.gameOver();
    } else if (this.moves[event.key]) {
      event.preventDefault();
      // Get new state
      let p = this.moves[event.key](this.piece);
      if (event.key === KEY.SPACE) {
        // Hard drop
        while (this.service.valid(p, this.board)) {
          this.points += POINTS.HARD_DROP;
          this.piece.move(p);
          p = this.moves[KEY.DOWN](this.piece);
        }
      } else if (this.service.valid(p, this.board)) {
        this.piece.move(p);
        if (event.key === KEY.DOWN) {
          this.points += POINTS.SOFT_DROP;
        }
      }
    }
  }

  constructor(
    private service: GameEngineLibService) {}

  async ngOnInit() {
    this.initBoard();
    this.initSound();
    this.initNext();
    this.resetGame();
  }

  initSound() {
    this.playSoundFn = Zoundfx.start(0.2);
  }

  initBoard() {
    this.ctx = this.canvas.nativeElement.getContext('2d')!;

    // Calculate size of canvas from constants.
    this.ctx.canvas.width = COLS * BLOCK_SIZE;
    this.ctx.canvas.height = ROWS * BLOCK_SIZE;

    // Scale so we don't need to give size on every draw.
    this.ctx.scale(BLOCK_SIZE, BLOCK_SIZE);
  }

  initNext() {
    this.ctxNext = this.canvasNext.nativeElement.getContext('2d')!;

    // Calculate size of canvas from constants.
    // The + 2 is to allow for space to add the drop shadow to
    // the "next piece"
    this.ctxNext.canvas.width = 4 * BLOCK_SIZE + 2;
    this.ctxNext.canvas.height = 4 * BLOCK_SIZE;

    this.ctxNext.scale(BLOCK_SIZE, BLOCK_SIZE);
  }

  play() {
    this.gameStarted = true;
    this.resetGame();
    this.next = new PieceComponent(this.ctx);
    this.piece = new PieceComponent(this.ctx);
    this.next.drawNext(this.ctxNext);
    this.time.start = performance.now();

    // If we have an old game running a game then cancel the old
    if (this.requestId) {
      cancelAnimationFrame(this.requestId);
    }

    this.animate();
  }

  resetGame() {
    this.points = 0;
    this.lines = 0;
    this.level = 0;
    this.board = this.getEmptyBoard();
    this.time = { start: 0, elapsed: 0, level: LEVEL[this.level] };
    this.paused = false;
    this.addOutlines();
  }

  animate(now = 0) {
    this.time.elapsed = now - this.time.start;
    if (this.time.elapsed &amp;gt; this.time.level) {
      this.time.start = now;
      if (!this.drop()) {
        this.gameOver();
        return;
      }
    }
    this.draw();
    this.requestId = requestAnimationFrame(this.animate.bind(this));
  }

  draw() {
    this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    this.piece.draw();
    this.drawBoard();
  }

  drop(): boolean {
    let p = this.moves[KEY.DOWN](this.piece);
    if (this.service.valid(p, this.board)) {
      this.piece.move(p);
    } else {
      this.freeze();
      this.clearLines();
      if (this.piece.y === 0) {
        // Game over
        return false;
      }
      this.playSoundFn([
        ,
        ,
        224,
        0.02,
        0.02,
        0.08,
        1,
        1.7,
        -13.9,
        ,
        ,
        ,
        ,
        ,
        6.7,
      ]);
      this.piece = this.next;
      this.next = new PieceComponent(this.ctx);
      this.next.drawNext(this.ctxNext);
    }
    return true;
  }

  clearLines() {
    let lines = 0;
    this.board.forEach((row, y) =&amp;gt; {
      if (row.every((value) =&amp;gt; value !== 0)) {
        lines++;
        this.board.splice(y, 1);
        this.board.unshift(Array(COLS).fill(0));
      }
    });
    if (lines &amp;gt; 0) {
      this.points += this.service.getLinesClearedPoints(lines, this.level);
      this.lines += lines;
      if (this.lines &amp;gt;= LINES_PER_LEVEL) {
        this.level++;
        this.lines -= LINES_PER_LEVEL;
        this.time.level = LEVEL[this.level];
      }
    }
  }

  freeze() {
    this.piece.shape.forEach((row, y) =&amp;gt; {
      row.forEach((value, x) =&amp;gt; {
        if (value &amp;gt; 0) {
          this.board[y + this.piece.y][x + this.piece.x] = value;
        }
      });
    });
  }

  private add3D(x: number, y: number, color: number): void {
    //Darker Color
    this.ctx.fillStyle = COLORSDARKER[color];
    // Vertical
    this.ctx.fillRect(x + 0.9, y, 0.1, 1);
    // Horizontal
    this.ctx.fillRect(x, y + 0.9, 1, 0.1);

    //Darker Color - Inner
    // Vertical
    this.ctx.fillRect(x + 0.65, y + 0.3, 0.05, 0.3);
    // Horizontal
    this.ctx.fillRect(x + 0.3, y + 0.6, 0.4, 0.05);

    // Lighter Color - Outer
    this.ctx.fillStyle = COLORSLIGHTER[color];

    // Lighter Color - Inner
    // Vertical
    this.ctx.fillRect(x + 0.3, y + 0.3, 0.05, 0.3);
    // Horizontal
    this.ctx.fillRect(x + 0.3, y + 0.3, 0.4, 0.05);

    // Lighter Color - Outer
    // Vertical
    this.ctx.fillRect(x, y, 0.05, 1);
    this.ctx.fillRect(x, y, 0.1, 0.95);
    // Horizontal
    this.ctx.fillRect(x, y, 1, 0.05);
    this.ctx.fillRect(x, y, 0.95, 0.1);
  }

  private addOutlines() {
    for (let index = 1; index &amp;lt; COLS; index++) {
      this.ctx.fillStyle = 'black';
      this.ctx.fillRect(index, 0, 0.025, this.ctx.canvas.height);
    }

    for (let index = 1; index &amp;lt; ROWS; index++) {
      this.ctx.fillStyle = 'black';
      this.ctx.fillRect(0, index, this.ctx.canvas.width, 0.025);
    }
  }

  drawBoard() {
    this.board.forEach((row, y) =&amp;gt; {
      row.forEach((value, x) =&amp;gt; {
        if (value &amp;gt; 0) {
          this.ctx.fillStyle = COLORS[value];
          this.ctx.fillRect(x, y, 1, 1);
          this.add3D(x, y, value);
        }
      });
    });
    this.addOutlines();
  }

  pause() {
    if (this.gameStarted) {
      if (this.paused) {
        this.animate();
      } else {
        this.ctx.font = '1px Arial';
        this.ctx.fillStyle = 'black';
        this.ctx.fillText('GAME PAUSED', 1.4, 4);
        cancelAnimationFrame(this.requestId);
      }

      this.paused = !this.paused;
    }
  }

  gameOver() {
    this.gameStarted = false;
    cancelAnimationFrame(this.requestId);
    this.ctx.fillStyle = 'black';
    this.ctx.fillRect(1, 3, 8, 1.2);
    this.ctx.font = '1px Arial';
    this.ctx.fillStyle = 'red';
    this.ctx.fillText('GAME OVER', 1.8, 4);
  }

  getEmptyBoard(): number[][] {
    return Array.from({ length: ROWS }, () =&amp;gt; Array(COLS).fill(0));
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, in the same lib directory, create a piece folder. Delete &lt;em&gt;game-engine-lib.component.ts&lt;/em&gt; and &lt;em&gt;game-engine-lib.component.spec.ts&lt;/em&gt; files.&lt;/p&gt;

&lt;p&gt;Copy the contents of &lt;em&gt;board.component.html&lt;/em&gt;, &lt;em&gt;piece.component.ts&lt;/em&gt; and &lt;em&gt;piece.component.html&lt;/em&gt; files from the &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Fquedicesebas%2Fangular-cross-platform-tetris" rel="noopener noreferrer"&gt;finished project&lt;/a&gt; of our monorepo board and piece folders respectively. Grab the &lt;em&gt;constants.ts&lt;/em&gt; file and add it to &lt;code&gt;projects/game-engine-lib/src/lib directory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Move &lt;em&gt;game-engine-lib.service.ts&lt;/em&gt; and &lt;em&gt;game-engine-lib.service.spec.ts&lt;/em&gt; files to a new services folder.&lt;/p&gt;

&lt;p&gt;Install ng-zzfx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install ng-zzfx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expose the Board component in the public API surface of our game-engine-lib so it can be consumed by the apps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export * from './lib/components/board/board.component';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our code structure should now look like this:&lt;/p&gt;

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

&lt;p&gt;Now we are ready to use the game engine logic in the tetris app (or any other app you might decide to add at a later stage).‌‌&lt;/p&gt;

&lt;p&gt;In the tetris app (i.e. /projects/tetris/src/app), replace the placeholder code in app.component.html with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;game-board&amp;gt;&amp;lt;/game-board&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not forget to copy and paste the styles.scss file content to its equivalent as well.‌‌&lt;/p&gt;

&lt;p&gt;Now let us use one of our scripts to build the lib one more time (if not already running with - watch flag) and test if everything works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run build:game-engine-lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then fire up the Tetris game (npm run start:tetris). If all worked out fine, you should see the below when your browser opens, try it out! ☺‌‌:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Integrating Electron into the workspace‌‌
&lt;/h2&gt;

&lt;p&gt;To have an in-depth idea of how to setup a standalone Electron app, have a look &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fwww.electronjs.org%2Fdocs%2Ftutorial%2Fquick-start%23main-and-renderer-processes" rel="noopener noreferrer"&gt;here&lt;/a&gt;.‌‌&lt;/p&gt;

&lt;p&gt;For our tetris game app, we need to first install Electron.js&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install --save-dev electron
npm install @electron/remote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Electron app uses the package.json file as its main entry point (as any other node.js app). So let us modify the package.json file as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
...
"name": "cross-platform-monorepo",
 "version": "0.0.0",
 "description": "Cross-platform monorepo Angular app",
 "author": { 
    "name": "your name",
    "email": "your@email.address"
  },
 "main": "main.js",
...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have written a lot of Angular code or worked in large codebases like I have, you would know how &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fwww.typescriptlang.org%2Fdocs%2Fhandbook%2Fmigrating-from-javascript.html" rel="noopener noreferrer"&gt;indispensable&lt;/a&gt; TypeScript (TS) is, so let us create a main.ts file instead of writing &lt;a href="https://medium.com/r/?url=https%3A%2F%2Fwww.typescriptlang.org%2Fdocs%2Fhandbook%2Fmigrating-from-javascript.html%23early-benefits" rel="noopener noreferrer"&gt;error-prone&lt;/a&gt; pure JavaScript (JS) code. When we build the tetris app, the main.ts code will be transpiled to JS code by the TS compiler (tsc). The output of this process will be the main.js file. This is what gets served to Electron.‌‌&lt;/p&gt;

&lt;p&gt;Create the &lt;em&gt;main.ts&lt;/em&gt; file and fill it with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { app, BrowserWindow, screen } from 'electron';
import * as path from 'path';
import * as url from 'url';

let win: BrowserWindow = null;
const args = process.argv.slice(1),
  serve = args.some((val) =&amp;gt; val === '--serve');

function createWindow(): BrowserWindow {
  const electronScreen = screen;
  const size = electronScreen.getPrimaryDisplay().workAreaSize;

  const remoteMain = require('@electron/remote/main');
  remoteMain.initialize();

  // Create the browser window:
  win = new BrowserWindow({
    x: 0,
    y: 0,
    width: size.width,
    height: size.height,
    webPreferences: {
      nodeIntegration: true,
      allowRunningInsecureContent: serve ? true : false,
      contextIsolation: false, // false if you want to run e2e tests with Spectron
    },
  });

  remoteMain.enable(win.webContents); // if you want to run e2e tests with Spectron or use remote module in renderer context (i.e. Angular apps)

  if (serve) {
    win.webContents.openDevTools();

    require('electron-reload')(__dirname, {
      electron: path.join(__dirname, 'node_modules', '.bin', 'electron'),
    });
    win.loadURL('http://localhost:4200');
  } else {
    win.loadURL(
      url.format({
        pathname: path.join(__dirname, 'dist/index.html'),
        protocol: 'file:',
        slashes: true,
      })
    );
  }

  // Emitted when the window is closed.
  win.on('closed', () =&amp;gt; {
    // Deference from the window object, usually you would store window
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null;
  });

  return win;
}

try {
  // This method will be called when Electron has finished
  // initialization and is ready to create browser windows.
  // Some APIs can only be used after this event occurs.
  // Added 400ms to fix the black background issue while using a transparent window.
  app.on('ready', () =&amp;gt; setTimeout(createWindow, 400));

  // Quit when all windows are closed.
  app.on('window-all-closed', () =&amp;gt; {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
      app.quit();
    }
  });

  app.on('activate', () =&amp;gt; {
    // On OS X it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
      createWindow();
    }
  });
} catch (e) {
  // handle error
}
Let's now add a couple more npm scripts to help us with the compilation and serving of the Electron app:
{
...
"start": "npm-run-all -p electron:serve start:tetris",
"electron:serve-tsc": "tsc -p tsconfig.serve.json",
"electron:serve": "wait-on tcp:4200 &amp;amp;&amp;amp; npm run electron:serve-tsc &amp;amp;&amp;amp; npx electron . --serve"
...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see from the above scripts, there are a few files and packages we need to create for this to work properly. Let's go ahead and do that now.‌‌&lt;/p&gt;

&lt;p&gt;Firstly, add the following npm packages:&lt;br&gt;
npm install wait-on // wait for resources (e.g. http) to become available before proceeding&lt;br&gt;
npm install electron-reload // load contents of all active BrowserWindows when files change&lt;br&gt;
npm install npm-run-all // run multiple npm-scripts in parallel&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Then create a tsconfig.serve.json file in the root directory and top it up with this code:

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

&lt;/div&gt;



&lt;p&gt;{&lt;br&gt;
  "compilerOptions": {&lt;br&gt;
    "sourceMap": true,&lt;br&gt;
    "declaration": false,&lt;br&gt;
    "moduleResolution": "node",&lt;br&gt;
    "emitDecoratorMetadata": true,&lt;br&gt;
    "experimentalDecorators": true,&lt;br&gt;
    "target": "es5",&lt;br&gt;
    "types": [&lt;br&gt;
      "node"&lt;br&gt;
    ],&lt;br&gt;
    "lib": [&lt;br&gt;
      "es2017",&lt;br&gt;
      "es2016",&lt;br&gt;
      "es2015",&lt;br&gt;
      "dom"&lt;br&gt;
    ]&lt;br&gt;
  },&lt;br&gt;
  "files": [&lt;br&gt;
    "main.ts"&lt;br&gt;
  ],&lt;br&gt;
  "exclude": [&lt;br&gt;
    "node_modules",&lt;br&gt;
    "*&lt;em&gt;/&lt;/em&gt;.spec.ts"&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ok, that's it - let us take it for another spin. If all is good, we should be able to play the tetris game, running on desktop.‌‌
Use the script we added earlier:

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

&lt;/div&gt;



&lt;p&gt;npm start‌&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Congratulations! We have an Electron desktop app running with hot module reload!‌‌

Before we jump into the next section, let us tidy up the code and create some helper services to give us a convenient way to communicate between Electron and Angular. Also, we want to package the game into an installable binary for the different operating systems. This is where [electron-builder](https://medium.com/r/?url=https%3A%2F%2Fwww.electron.build%2F) comes into play.‌‌

First, update the Electron _main.ts_ file, line 41 (add "/tetris/browser" to the path of the pathname parameter):

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

&lt;/div&gt;



&lt;p&gt;win.loadURL(&lt;br&gt;
      url.format({&lt;br&gt;
        pathname: path.join(__dirname, 'dist/tetris/browser/index.html'),&lt;br&gt;
        protocol: 'file:',&lt;br&gt;
        slashes: true,&lt;br&gt;
      })&lt;br&gt;
    );&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next, in the root directory, create a electron-builder.json file and add this content:

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

&lt;/div&gt;



&lt;p&gt;{&lt;br&gt;
  "productName": "cross-platform Tetris game",&lt;br&gt;
  "directories": {&lt;br&gt;
    "output": "release/"&lt;br&gt;
  },&lt;br&gt;
  "files": [&lt;br&gt;
    "&lt;strong&gt;/*",&lt;br&gt;
    "!&lt;/strong&gt;/&lt;em&gt;.ts",&lt;br&gt;
    "!&lt;/em&gt;.code-workspace",&lt;br&gt;
    "!LICENSE.md",&lt;br&gt;
    "!package.json",&lt;br&gt;
    "!package-lock.json",&lt;br&gt;
    "!src/",&lt;br&gt;
    "!e2e/",&lt;br&gt;
    "!hooks/",&lt;br&gt;
    "!angular.json",&lt;br&gt;
    "!_config.yml",&lt;br&gt;
    "!karma.conf.js",&lt;br&gt;
    "!tsconfig.json",&lt;br&gt;
    "!tslint.json"&lt;br&gt;
  ],&lt;br&gt;
  "win": {&lt;br&gt;
    "icon": "dist/tetris/assets/icons",&lt;br&gt;
    "target": ["portable"]&lt;br&gt;
  },&lt;br&gt;
  "mac": {&lt;br&gt;
    "icon": " dist/tetris/assets/icons",&lt;br&gt;
    "target": ["dmg"]&lt;br&gt;
  },&lt;br&gt;
  "linux": {&lt;br&gt;
    "icon": " dist/tetris/assets/icons",&lt;br&gt;
    "target": ["AppImage"]&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now let's install electron-builder using the terminal:

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

&lt;/div&gt;



&lt;p&gt;npm i electron-builder -D&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;In the package.json file, add the respective scripts for packaging the game:

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

&lt;/div&gt;



&lt;p&gt;{&lt;br&gt;
...&lt;br&gt;
"postinstall": "electron-builder install-app-deps", &lt;br&gt;
"build": "npm run electron:serve-tsc &amp;amp;&amp;amp; ng build tetris --base-href ./",&lt;br&gt;
"build:prod": "npm run build -- -c production",&lt;br&gt;
"electron:package": "npm run build:prod &amp;amp;&amp;amp; electron-builder build"&lt;br&gt;
...&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;That's it! Build and package the application using npm run electron:package command and depending on your operating system (on Windows, maybe you need to run it from a elevated termial, with Run as administrator), you will get an installer (in the newly created `/release` folder) for Linux, Windows or macOS with "auto update" support out of the box!‌‌

This is what it looks like on Windows 10:‌‌

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y9nnkzi3cyofpg0qwywv.png)



&amp;gt; Tip: remember to add the new output folder release to the .gitignore file

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

&lt;/div&gt;



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

&lt;h1&gt;
  
  
  Compiled output
&lt;/h1&gt;

&lt;p&gt;/release&lt;br&gt;
/dist&lt;br&gt;
/tmp&lt;br&gt;
/out-tsc&lt;br&gt;
/bazel-out&lt;br&gt;
...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Angular-Electron Communication‌‌
We cannot directly access all of Electron's APIs from the Angular app. To easily communicate between Electron and Angular, we need to make use of Inter-Process Communication (IPC). It is a mechanism the operating system provides so that two different processes (i.e. from main process to browser process and vice versa) can communicate with each other. ‌‌

Let's create a service in the `projects/tetris/src/app` directory to facilitate this inter-process communication:

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

&lt;/div&gt;



&lt;p&gt;ng generate module core --project=tetris&lt;br&gt;
ng generate service core/services/electron --project=tetris&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add the following code inside the newly created file (i.e. _electron.service.ts_):

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

&lt;/div&gt;



&lt;p&gt;import { Injectable } from '@angular/core';&lt;br&gt;
import { ipcRenderer, webFrame } from 'electron';&lt;br&gt;
import * as childProcess from 'child_process';&lt;br&gt;
import * as fs from 'fs';&lt;/p&gt;

&lt;p&gt;@Injectable({&lt;br&gt;
  providedIn: 'root',&lt;br&gt;
})&lt;br&gt;
export class ElectronService {&lt;br&gt;
  ipcRenderer!: typeof ipcRenderer;&lt;br&gt;
  webFrame!: typeof webFrame;&lt;br&gt;
  remote: any;&lt;br&gt;
  childProcess!: typeof childProcess;&lt;br&gt;
  fs!: typeof fs;&lt;/p&gt;

&lt;p&gt;get isElectron(): boolean {&lt;br&gt;
    return !!window?.process?.type;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;constructor() {&lt;br&gt;
    if (this.isElectron) {&lt;br&gt;
      this.ipcRenderer = window.require('electron').ipcRenderer;&lt;br&gt;
      this.webFrame = window.require('electron').webFrame;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  this.remote = require('@electron/remote');

  this.childProcess = window.require('child_process');
  this.fs = window.require('fs');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Consume it in the _app.component.ts_ file (or any other file in the projects):

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

&lt;/div&gt;



&lt;p&gt;export class AppComponent {&lt;br&gt;
  title = "tetris";&lt;br&gt;
  constructor(private electronService: ElectronService) {&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (electronService.isElectron) {
  console.log("Run in electron");
  console.log("Electron ipcRenderer", this.electronService.ipcRenderer);
  console.log("NodeJS childProcess", this.electronService.childProcess);
} else {
  console.log("Run in browser");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Whoa that was a mouthful. Look back at what we have done - with the above setup, you have the power to go wild and use your Angular skills to build apps like VS Code, Slack, Twitch, [Superpowers](https://medium.com/r/?url=http%3A%2F%2Fsuperpowers-html5.com%2Findex.en.html) and [all kinds of apps](https://medium.com/r/?url=https%3A%2F%2Fwww.electronjs.org%2Fapps) you can imagine, and distribute them to the most popular desktop platforms.‌‌

With that said, let us jump into the last platform integration - Mobile.‌‌

## Integrating iOS and Android into the workspace‌‌
We first have to install the package. There are other [pre-requisites](https://medium.com/r/?url=https%3A%2F%2Fcapacitorjs.com%2Fdocs%2Fgetting-started%2Fdependencies) that you should comply with before you can proceed.‌‌

Once you have installed the above dependencies, run the following script in the root directory:

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

&lt;/div&gt;



&lt;p&gt;npm i @capacitor/core&lt;br&gt;
npm i -D @capacitor/cli&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Then, initialize Capacitor with our app data (npx is a utility that executes local binaries or scripts to avoid global installs):

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

&lt;/div&gt;



&lt;p&gt;npx cap init&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;As you can see, a capacitor.config.ts was created with the prompts info and some defaults. Replace webDir param with "dist/tetris/browser".

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/seljmnikncmpbzgmp0j1.png)

Lastly, let us add the platforms of our choice. We will first target Android:

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

&lt;/div&gt;



&lt;p&gt;npm i @capacitor/android&lt;br&gt;
npx cap add android&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Capacitor works on a three-step build process: First, your web code is built (if necessary). Next, the built web code is copied to each platform. Finally, the app is compiled using the platform-specific tooling. There is a recommended [developer workflow](https://medium.com/r/?url=https%3A%2F%2Fcapacitorjs.com%2Fdocs%2Fbasics%2Fworkflow) that you should follow.‌‌

After Android has been successfully added, you should see a bunch of android specific files (inside the newly created android folder). **These files should be part of version control**.

Capacitor relies on each platform's IDE, so we need to launch Android Studio (be sure to have the lattest version) to test our game. To do so, simply run:

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

&lt;/div&gt;



&lt;p&gt;npx cap open android&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Once Android Studio opens, you can build, emulate or run your app through the standard [Android Studio workflow](https://medium.com/r/?url=https%3A%2F%2Fdeveloper.android.com%2Fstudio%2Fworkflow) (be awere that yo need at leats).‌

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yhg6dychli0p1zz6sq2l.png)

As a final point, let us do the same as above to add iOS platform support:

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

&lt;/div&gt;



&lt;p&gt;npm install @capacitor/ios&lt;br&gt;
npx cap add ios&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;‌‌Just like we saw when adding Android, after iOS has been successfully added, you should see a bunch of iOS specific files (inside the newly created ios folder). These files should be added to source control.‌‌

To start Xcode and build the app for the emulator, run:

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

&lt;/div&gt;



&lt;p&gt;npx cap open ios&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
‌‌Capacitor will package your app files and hand them over to Xcode. The rest of the development is up to you. ‌‌

The beauty of Capacitor is that it features a native iOS bridge that enables developers to communicate between JavaScript and Native Swift or Objective-C code. This means you have the freedom to author your code by using the various APIs available, Capacitor or Cordova plugins, or custom native code to build out the rest of your app.‌‌

As mentioned in the [Capacitor developer workflow,](https://medium.com/r/?url=https%3A%2F%2Fcapacitorjs.com%2Fdocs%2Fbasics%2Fworkflow) each time we build a project, we need to sync the app assets with platform folders. Yo can use a npx command or the [VSC Ionic extension](https://medium.com/r/?url=https%3A%2F%2Fcapacitorjs.com%2Fdocs%2Fvscode%2Fgetting-started):

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

&lt;/div&gt;



&lt;p&gt;npx cap sync&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
## Cleaning up‌‌
Keeping with the theme of simple and clean multi-project architecture, and since we now have another platform to maintain, it makes sense to create a new Angular library to hold all the services, components, directives, etc. that are common across the platforms:‌. Go ahead and create the lib:

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

&lt;/div&gt;



&lt;p&gt;ng g library shared-lib&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;‌‌You can delete the automatically generated component and service. ‌‌In _tsconfig.json_ under compilerOptions, add the "@" to the name of the path:

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

&lt;/div&gt;



&lt;p&gt;"paths": {&lt;br&gt;
      ...&lt;br&gt;
      "@shared-lib": [&lt;br&gt;
        "dist/shared-lib"&lt;br&gt;
      ]&lt;br&gt;
    }&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Move all services from `projects/tetris/src/app/core/services` to `projects/shared-lib/src/lib/services` and make sure to export the classes via the lib's public API (i.e. _public-api.ts_):

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

&lt;/div&gt;



&lt;p&gt;/*&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public API Surface of shared-lib
*/&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;export * from './lib/services/electron/electron.service';&lt;br&gt;
export * from './lib/services/capacitor/capacitorstorage.service';&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lastly, let's add a new service which we will need in the next section. Run the following command in `projects/shared-lib/src/lib/services` folder:

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

&lt;/div&gt;



&lt;p&gt;ng g s services/capacitor/capacitorstorage --project=shared-lib&lt;br&gt;
npm install @capacitor/preferences&lt;br&gt;
npx cap syn&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;c

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

&lt;/div&gt;



&lt;p&gt;import { Injectable } from '@angular/core';&lt;br&gt;
import { Preferences } from '@capacitor/preferences';&lt;/p&gt;

&lt;p&gt;@Injectable({&lt;br&gt;
  providedIn: 'root',&lt;br&gt;
})&lt;br&gt;
export class CapacitorStorageService {&lt;br&gt;
  constructor() {}&lt;/p&gt;

&lt;p&gt;async set(key: string, value: any): Promise {&lt;br&gt;
    await Preferences.set({&lt;br&gt;
      key: key,&lt;br&gt;
      value: JSON.stringify(value),&lt;br&gt;
    });&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;async get(key: string): Promise {&lt;br&gt;
    const item = await Preferences.get({ key: key });&lt;br&gt;
    return item ? JSON.parse(item.value!) : null;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;async remove(key: string): Promise {&lt;br&gt;
    await Preferences.remove({&lt;br&gt;
      key: key,&lt;br&gt;
    });&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;As with the previous lib, we have to build it before it can be used —add a script to do so and **then run it**:

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

&lt;/div&gt;



&lt;p&gt;{&lt;br&gt;
...&lt;br&gt;
"build:shared-lib": "ng build shared-lib --watch"&lt;br&gt;
...&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;With that done, we are now ready to use the shared-lib anywhere in the projects.‌‌

&amp;gt; Warning: libs can import other libs, however, avoid importing services, modules, directives, etc. defined in the projects' apps into libs. This often leads to circular dependencies which are hard to debug.‌‌

## Tying it together‌‌
We are almost at the finish line. Let us import the _CapacitorStorageService_, specifically, inside the _board.component.ts_ file (_game-engine-lib_ project):

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

&lt;/div&gt;



&lt;p&gt;import { CapacitorStorageService } from "@shared-lib";&lt;br&gt;
...&lt;br&gt;
  constructor(&lt;br&gt;
    private service: GameEngineLibService,&lt;br&gt;
    private capacitorStorageService: CapacitorStorageService&lt;br&gt;
  ) {}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We want to persist the highscore after a webpage refresh or when we reopen the app on mobile phones or desktop, so modify these methods as follows:

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

&lt;/div&gt;



&lt;p&gt;async ngOnInit() {&lt;br&gt;
    const highscore = await this.localStorageGet('highscore');&lt;br&gt;
    highscore ? (this.highScore = highscore) : (this.highScore = 0);&lt;br&gt;
    this.initBoard();&lt;br&gt;
    this.initSound();&lt;br&gt;
    this.initNext();&lt;br&gt;
    this.resetGame();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;gameOver() {&lt;br&gt;
    this.gameStarted = false;&lt;br&gt;
    cancelAnimationFrame(this.requestId);&lt;br&gt;
    this.highScore =&lt;br&gt;
      this.points &amp;gt; this.highScore ? this.points : this.highScore;&lt;br&gt;
    this.localStorageSet('highscore', this.highScore);&lt;br&gt;
    this.ctx.fillStyle = 'black';&lt;br&gt;
    this.ctx.fillStyle = 'black';&lt;br&gt;
    this.ctx.fillRect(1, 3, 8, 1.2);&lt;br&gt;
    this.ctx.font = '1px Arial';&lt;br&gt;
    this.ctx.fillStyle = 'red';&lt;br&gt;
    this.ctx.fillText('GAME OVER', 1.8, 4);&lt;br&gt;
  }&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Also, add the localStorageSet and localStorageGet methods inside the _board.component.ts_ file:

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

&lt;/div&gt;



&lt;p&gt;async localStorageGet(key: string): Promise {&lt;br&gt;
    return await this.capacitorStorageService.get(key);&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;localStorageSet(key: string, value: any): void {&lt;br&gt;
    this.capacitorStorageService.set(key, value);&lt;br&gt;
  }&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Don't forget to rebuild the libraries if not running already:

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

&lt;/div&gt;



&lt;p&gt;npm run build:shared-lib&lt;br&gt;
npm run build:game-engine-lib&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

LocalStorage is considered transient, meaning your app can expect that the data will be lost eventually. The same can be said for IndexedDB at least on iOS. On Android, the [persisted storage API](https://medium.com/r/?url=https%3A%2F%2Fweb.dev%2Fpersistent-storage%2F) is available to mark IndexedDB as persisted.

Capacitor comes with a native [Preferences API](https://medium.com/r/?url=https%3A%2F%2Fcapacitorjs.com%2Fdocs%2Fapis%2Fpreferences) that avoids the eviction issues above, but it is meant for key-value store of simple data. This API will fall back to using localStorage when not running on mobile. Hence localStorage works for our webApp, Electron as well as mobile platforms.‌‌

## End of the road‌‌
The final workspace file structure and npm scripts should look like this —clean and simple:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y40zljct45yr8xp0rwkv.png)

Well done! You have made it to the end of the article. Take a deep breath and marvel at your creations ☺

## Recap‌‌
We have seen why and how to create a monorepo-style workspace using Angular. We have also seen how effortless it is to add support for platforms other than the web. What started out as a seemingly insurmountable amount of work turned out to be quite an enjoyable journey. ‌‌

We have only scratched the surface of [what the web can do today](https://medium.com/r/?url=https%3A%2F%2Fwhatwebcando.today%2F) and by extension, as I have demonstrated in this article - what you as a developer can do to reach as many users as possible on many devices of their choice.‌‌

If you are new to software development, I hope this article has sparked your interest and makes you eager to go out there and discover more. If you are in the veteran club, I also hope this piece has inspired you and that you will share this newly acquired knowledge (and the article) with your fellow developers and teams.‌‌

## Acknowledgements‌‌
Thank you for taking the time to go on this journey with me, I highly appreciate your feedback and comments. I would also like to thank my colleagues who continue to inspire me with their sheer technical skills as well as their humbleness. To the reviewers (Agnieszka, Andrej, Diana, Hartmut, Игорь Кацуба, [Max](https://medium.com/r/?url=https%3A%2F%2Ftwitter.com%2Fmaxkoretskyi), ‌‌Nikola, René, Stacy, Torsten, Wiebke) of this article - a big shout out and thank you for all your input.‌‌

## What is next?‌‌

Try out [Nx devtools](https://medium.com/r/?url=https%3A%2F%2Fnx.dev%2Flatest%2Fangular%2Fgetting-started%2Fgetting-started) (out of the box tooling) for monorepos. There is also[ NestJS](https://medium.com/r/?url=https%3A%2F%2Fnestjs.com%2F)—a backend integration which works well with the current [tech stack](https://medium.com/r/?url=https%3A%2F%2Findepth.dev%2Fposts%2F1247%2Fcode-sharing-made-easy-in-a-full-stack-app-with-nx-angular-and-nestjs) i.e. Angular + Nodejs. Remember we also created a tetris2 project placeholder? Go ahead and fill that out with the next version of tetris i.e. make it look "pretty" and playable, for example, using native key gestures - as they say, the sky is the proverbial limit.‌‌

## About the original article author‌‌
Richard Sithole is a passionate frontend developer at OPTIMAL SYSTEMS Berlin where he leads efforts to build, maintain and extend a feature-rich propriety Enterprise Content Management software called[ enaio® webclient](https://medium.com/r/?url=https%3A%2F%2Fwww.optimal-systems.de%2Fen%2Fenaio%2Fwebclient). Previously he worked for one of the largest banks in Africa where he focused on full-stack development, application architecture, software developer [hiring](https://medium.com/r/?url=https%3A%2F%2Fjobportal.optimal-systems.de%2F) and mentoring. Say "hallo" to him on twitter [@sliqric](https://medium.com/r/?url=https%3A%2F%2Ftwitter.com%2Fsliqric).

## Inspirational sources‌‌
1. [Bootstrap and package your project with Angular and Electron - Maxime Gris](https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Fmaximegris%2Fangular-electron)
2. [Desktop Apps with Electron and Angular - Jeff Delaney](https://medium.com/r/?url=https%3A%2F%2Ffireship.io%2Flessons%2Fdesktop-apps-with-electron-and-angular%2F)
3. [Why we're using a single codebase for GitLab Community and Enterprise editions](https://medium.com/r/?url=https%3A%2F%2Fabout.gitlab.com%2Fblog%2F2019%2F08%2F23%2Fa-single-codebase-for-gitlab-community-and-enterprise-edition%2F)
4. [Angular and Electron - More than just a desktop app with Aristeidis Bampakos](https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D8JHz402bz34)
5. [Give Your Angular App Unlimited Powers with Electron - Stephen Fluin](https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dv8_1lDSDdgM)
6. [Capacitor Workflow for iOS and Android Applications - Joshua Morony‌‌‌‌](https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DoXbRcpsytGQ)

Fin.‌‌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Connect Your Android Device Over WiFi in VSCode to Run and Debug Your Flutter App</title>
      <dc:creator>Sebastián Rojas Ricaurte</dc:creator>
      <pubDate>Fri, 28 Oct 2022 07:01:21 +0000</pubDate>
      <link>https://dev.to/quedicesebas/connect-your-android-device-over-wifi-in-vscode-to-run-and-debug-your-flutter-app-1gbh</link>
      <guid>https://dev.to/quedicesebas/connect-your-android-device-over-wifi-in-vscode-to-run-and-debug-your-flutter-app-1gbh</guid>
      <description>&lt;h2&gt;
  
  
  Stop damaging your phone's battery to test your apps, debug without cables (almost).
&lt;/h2&gt;

&lt;p&gt;This post assumes that you have already configured your development environment to run and debug on your Android device over a USB connection. Tested on an Android 13 device and a Windows 10 computer. You need to have both your computer and your phone connected to the same network.&lt;/p&gt;

&lt;h3&gt;
  
  
  One time IDE and device configuration
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;[Optional] Install the ADB Commands for VSCode extension. If not, you can use adb commands. I prefer to use the extension because I don't need to configure nothing and is more visual.&lt;/li&gt;
&lt;li&gt;Enable Wireless debugging in your device:

&lt;ul&gt;
&lt;li&gt;Go to Settings and search for "debug".&lt;/li&gt;
&lt;li&gt;Tap on "Wireless debugging", then again in the "Wireless debugging" option (not in the toggle, tap the entire item to enter to the details page.&lt;/li&gt;
&lt;li&gt;Check the "Use wireless debugging" toggle. In the "Allow Wireless debugging on this network" popup you should check "Always allow on this network" and tap on "Allow".&lt;/li&gt;
&lt;li&gt;Reserve for later the "IP Address and &amp;amp; Port info" (most tutorials assume that port is 5555 but in my brand new Pixel 7 it was another port number).
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tem7clucpqtgl8e0p9u.png" alt="Image description" width="559" height="975"&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Device conection
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;[Only de first time] Connect your device over USB.&lt;/li&gt;
&lt;li&gt;In VSCode, hit &lt;em&gt;Ctrl+shift+p&lt;/em&gt;, look for and select "ADB:📱 Connect to device IP":

&lt;ul&gt;
&lt;li&gt;Type the IP Address and hit &lt;em&gt;enter&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Type the Port number&lt;/li&gt;
&lt;li&gt;You should see a toast with "Connected to :". &lt;strong&gt;Now you can disconnect the USB cable&lt;/strong&gt;.
*Note: IP Address changes deppending of the used network and router device configuration. Port number could change each time. This can cause "ADB returned null value" or others errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In VSCode, hit &lt;em&gt;F5&lt;/em&gt; or go to &lt;em&gt;Run &amp;gt; Start Debugging&lt;/em&gt;. Enjoy!&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>vscode</category>
    </item>
  </channel>
</rss>
