{
    "version": "https://jsonfeed.org/version/1.1",
    "title": "Stelabouras Blog",
    "icon": "https://www.stelabouras.com/static/avatar.jpg",
    "home_page_url": "https://www.stelabouras.com/",
    "feed_url": "https://www.stelabouras.com/feed.json",
    "user_comment": "This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL -- https://www.stelabouras.com/feed.json -- and add it your reader.",
    "description": "JSON Feed of blog posts from stelabouras.com",
    "authors": [
        {
            "name": "Stelios Petrakis",
            "url": "https://www.stelabouras.com/",
            "avatar": "https://www.stelabouras.com/static/avatar.jpg"
        }
    ],
    "items": [
        
        {
            "id" : "https://www.stelabouras.com/blog/itchio-webgl/",
            "url": "https://www.stelabouras.com/blog/itchio-webgl/",
            "title": "Distributing your mobile WebGL Unity game via Itch.io",
            "content_html": "<p>The following is a collection of improvements that can be implemented on a Unity WebGL game released in <a href=\"https://itch.io/\">Itch.io</a>, that allows developers to distribute their game on mobile devices without going through Apple's App Store or Google Play.</p><p>This post serves both as a cleaned-up version of my notes for future reference, but it can also help other developers having a similar interest on the subject. Also, while the post focuses on WebGL games created using the Unity engine, the techniques described below can be easily ported to any other engine that supports this in-between JS layer for allowing communication between the game and the hosted webpage.</p><p>Implementing what is being described in this post will allow players to install your WebGL game as a Progressive Web App (PWA), creating an icon on their home screen and launching it without opening their browser. Players can also be validated for their purchase via Itch API and can even be notified when an update is available!</p><p>Minor disclaimer: I haven't coded professionally in JavaScript for more than a decade, so the JS code you will see below can be quite crude. On the upside, it's just pure vanilla JS so it will be easy to port to any framework you are familiar with.</p><p>OK, let's begin!</p><p>Before we delve deeper into the technical stuff, let's address one main issue: Itch does not allow you to sell/set a price on a WebGL game. Don't fret though, as there is a way to check whether a certain Itch user has access to your game via download keys.</p><p>In the \"Distribute\" tab of your WebGL game, you can generate a number of download keys and either distribute them freely, or unlock them after users have paid for the game via your website. You can also bundle them together as a free upgrade when users purchase the desktop version of your game right within Itch!</p><p>To achieve the latter, you will have to generate a number of download keys for your WebGL game, download them locally and then upload them as \"External keys\" in the \"Distribute\" tab of your desktop game (with Key Type set to \"Other\"). This way, when a user purchases the desktop version of the game, they will be granted with a download key for the WebGL build which they can claim.</p><p>Now that we have addressed that, let's proceed!</p><p>The first thing we need to establish is that every time you push a new WebGL build, Itch generates a new webpage for that build, for cache related reasons. The existing webpage will continue to work for some time but it will get invalidated a while after the new build becomes available. This means that we need a way of always pointing the player to the latest and greatest build. Although this is not as straightforward, as it requires server-side API call(s), it's definitely feasible.</p><p>What we can do is create a webpage hosted on the game's website that serves multiple purposes:</p><ol><li>It's the entry point for the player to install the game as a PWA.</li><li>It always fetches the latest build and also notifies the player if an update is available.</li><li>(Optional) It can validate the player on whether they have purchased the game or not, in order to allow access to the build.</li></ol><p>Let's go through the list one-by-one.</p><h3>1. Generate the PWA page</h3><p>There is a lot of online documentation for creating a PWA, but here are the tags that need to be included the <code>&lt;head&gt;</code> of the webpage:</p><pre><code class=\"lang-html\">&lt;meta content=&quot;width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover&quot; name=&quot;viewport&quot;/&gt;&lt;meta content=&quot;yes&quot; name=&quot;mobile-web-app-capable&quot;/&gt;&lt;meta content=&quot;yes&quot; name=&quot;apple-mobile-web-app-capable&quot;/&gt;&lt;meta content=&quot;black-translucent&quot; name=&quot;apple-mobile-web-app-status-bar-style&quot;/&gt;&lt;meta content=&quot;&amp;lt;!-- GAME NAME --&amp;gt;&quot; name=&quot;apple-mobile-web-app-title&quot;/&gt;&lt;link href=&quot;&amp;lt;!-- link to app icon --&amp;gt;&quot; rel=&quot;apple-touch-icon&quot;/&gt;&lt;link href=&quot;&amp;lt;!-- link to the manifest.json file --&amp;gt;&quot; rel=&quot;manifest&quot;/&gt;</code></pre><p>The <code>manifest.json</code> file is needed (although not required), in order for the game to be detected as an installable PWA. Most of <code>manifest.json</code> properties are self-explanatory and <a href=\"https://developer.mozilla.org/en-US/docs/Web/Manifest\">extensively documented</a>. The only note I need to make here is to use <code>fullscreen</code> instead of <code>standalone</code> for the <a href=\"https://developer.mozilla.org/en-US/docs/Web/Manifest/display\"><code>display</code></a> property as this will actually make the web view extend outside the screen safe area when the game is installed. The <a href=\"https://developer.mozilla.org/en-US/docs/Web/Manafest/orientation\"><code>orientation</code></a> and <a href=\"https://developer.mozilla.org/en-US/docs/Web/Manifest/start_url\"><code>start_url</code></a> ones are also important.</p><h3>2. Fetch the latest build</h3><p>In order to always fetch the latest build url, we need to use Itch's API. You can generate an API key on the <a href=\"https://itch.io/user/settings/api-keys\">Developer &gt; API keys</a> section of Itch user settings. The issue here is that the <a href=\"https://itch.io/docs/api/serverside\">API documentation on Itch</a> does not include all of the endpoints: there are endpoints that are not listed there and one of them is the one we need: <code>/api/1/{api_key}/game/{id}/uploads</code>.</p><p>After fetching the ID of the WebGL game from the url of the edit page of the game, we can perform a simple API request on the server that hosts the webpage of our game. Here's a simple snippet that uses server-side JavaScript to perform the request:</p><pre><code class=\"lang-js\">const uploadsURL = `https://itch.io/api/1/${process.env.ITCH_API_KEY}/game/${process.env.ITCH_GAME_ID}/uploads`try {    const response = await fetch(uploadsURL, {        method: &#39;GET&#39;    });    if (response.ok) {        const uploads = await response.json();        if (uploads == undefined             || uploads.uploads == undefined            || uploads.uploads.length == 0) {            return {                statusCode: 404,                body: JSON.stringify({ message: `Upload not found!` })            };        }        const upload = uploads.uploads[0];        const buildID = upload.build.id;        const uploadID = upload.build.upload_id;        const buildURL = &#39;https://html-classic.itch.zone/html/&#39; + uploadID + &#39;-&#39; + buildID + &#39;/index.html&#39;;        return {            statusCode: 200,            body: buildURL        };    } else {        return {            statusCode: response.status,            body: JSON.stringify({ message: `Error fetching uploads ${response.status}` })        };    }}</code></pre><p>This is the main logic of the build url generator: It requests the uploads list from the API for our game, fetches the latest upload object and generates the URL based on the values of the upload and build IDs.</p><h3>3. Optional: Validating the user purchase</h3><p>You can go a step further and only allow the user to fetch the latest build url after checking whether they have actually purchased the game, in order to do that you would need to create an <a href=\"https://itch.io/user/settings/oauth-apps\">OAuth application</a>, allow user to login to Itch using this application and then query two more API endpoints using their access token:</p><p>As soon as the user grants permission to the OAuth application and Itch redirects back to our server with the access token, we can perform the following two API requests before trying to generate the build URL: <code>/api/1/{access_token}/me</code>, where the access token is used as an API key in order to fetch the user ID of that user and <code>/api/1/{api_key}/game/{game_id}/download_keys?user_id={user_id}</code>, where we expect that a download key will exist for that user, if they have claimed a key for the WebGL game (as we have described above).</p><p>And there you have it: We have essentially built a really simple mechanism for checking whether a player has purchased the game and a way to always fetch the latest build url for that game!</p><p>With this url we can create an <code>&lt;iframe&gt;</code> HTML object that hosts this game and load it via JavaScript, as soon as the build url is ready!</p><p>The bonus thing here is that we can communicate with the hosted <code>&lt;iframe&gt;</code>, check when the game is on its 'Main Menu' scene and request the build url again, comparing it with the current one and presenting a pop-up to the user to refresh the website as a new build is available! We will look into how we can communicate with the hosted <code>&lt;iframe&gt;</code> using the <code>postMessage()</code> and <code>message</code> event of HTML below.</p><h3>Communicating with the webpage</h3><p>There are multiple cases where the game logic might need to communicate with the hosting page, either to query certain properties of the device (is it a mobile device?), change certain properties of the DOM or even receive events from the webpage.</p><p>In order to achieve that we need to create a JavaScript bridge. You can follow <a href=\"https://docs.unity3d.com/6000.0/Documentation/Manual/web-interacting-browser-js.html\">Unity's guide for that</a>. You can create a <code>WebGL</code> folder under the <code>Plugins</code> directory (<code>Assets/Plugins/WebGL/</code>) and create two files there:</p><ul><li><code>WebGLPluginJS.cs</code></li><li><code>WebGLPluginJS.jslib</code></li></ul><p>The <code>.cs</code> file holds all the <code>[DllImport(\"__Internal\")]</code> function signatures and the <code>.jslib</code> contains the JavaScript implementation.</p><h3>Passing events from game to the PWA webpage</h3><p>Let's say that we want to inform our webpage that the game's main menu is visible or not.</p><ul><li><code>WebGLPluginJS.cs</code></li></ul><pre><code class=\"lang-cs\">[DllImport(&quot;__Internal&quot;)]public static extern void MainMenuShown();[DllImport(&quot;__Internal&quot;)]public static extern void MainMenuHidden();</code></pre><p>which we can call from our Main Menu's in-game logic like that:</p><ul><li><code>MainMenu.cs</code></li></ul><pre><code class=\"lang-cs\">public class MainMenuLogic : MonoBehaviour {    void Start () {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR        WebGLPluginJS.MainMenuShown();#endif    }    void OnDestroy() {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR        WebGLPluginJS.MainMenuHidden();#endif    }}</code></pre><p>The actual JavaScript methods can take advantage of the <code>postMessage</code> HTML API:</p><ul><li><code>WebGLPluginJS.jslib</code></li></ul><pre><code class=\"lang-js\">MainMenuShown: function() {    parent.postMessage(&quot;MainMenuShown&quot;, &quot;*&quot;);},MainMenuHidden: function() {    parent.postMessage(&quot;MainMenuHidden&quot;, &quot;*&quot;);},</code></pre><p>Here we post a message to the parent webpage which is the page the build URL is hosted as an iframe, namely our PWA webpage. On the PWA webpage JavaScript we can have something like that:</p><pre><code class=\"lang-js\">window.addEventListener(&quot;message&quot;, (e) =&amp;gt; {    const data = e.data;    if (data == &quot;MainMenuShown&quot;) {        // Execute the check for updates logic and        // present the &quot;Update available&quot; popup in HTML    }    else if (data == &quot;MainMenuHidden&quot;) {        // Hide the &quot;Update available&quot; popup in HTML    }},false);</code></pre><p>Alternatively, we can reverse the logic and have the game query (using the JS bridge) the hosting page on whether a new build is available and then present an in-game popup.</p><h3>Visibility change</h3><p>An easy way to pause the game when user taps outside the WebGL container or navigates to another website, is to use the <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API\">Page Visibility API</a>.</p><ul><li><code>WebGLPluginJS.cs</code></li></ul><pre><code class=\"lang-cs\">[DllImport(&quot;__Internal&quot;)]public static extern void RegisterVisibilityChangeEvent();</code></pre><ul><li><code>WebGLPluginJS.jslib</code></li></ul><pre><code class=\"lang-js\">RegisterVisibilityChangeEvent: function () {    document.addEventListener(&quot;visibilitychange&quot;, function () {        SendMessage(&quot;GameLogic&quot;, &quot;OnVisibilityChange&quot;, document.visibilityState);    });    if (document.visibilityState != &quot;visible&quot;) {        SendMessage(&quot;GameLogic&quot;, &quot;OnVisibilityChange&quot;, document.visibilityState);    }},</code></pre><p>In the Unity scene, create a GameObject named \"GameLogic\" (or change that to whatever you want) and in one of its attached scripts declare the \"OnVisibilityChange\" method. Additionally call the <code>RegisterVisibilityChangeEvent()</code> from its <code>Start()</code> method.</p><ul><li><code>GameLogic.cs</code></li></ul><pre><code class=\"lang-cs\">void Start() {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR    WebGLPluginJS.RegisterVisibilityChangeEvent();#endif}void OnVisibilityChange(string visibilityState) {    // Custom logic}</code></pre><p>If the game already reacts to the <code>OnApplicationFocus</code> event by Unity (for example by displaying a pause screen), the implemented <code>OnVisibilityChange()</code> method can simple call <code>OnApplicationFocus()</code> directly which will execute the same logic.</p><ul><li><code>GameLogic.cs</code></li></ul><pre><code class=\"lang-cs\">void OnVisibilityChange(string visibilityState) {    OnApplicationFocus(visibilityState == &quot;visible&quot;);}</code></pre><h3>Device orientation detection</h3><p>Generally speaking, if the game has been created with the option to react correctly to resolution changes and more specifically designed to support both vertical and horizontal orientations of mobile devices, then everything might already be operating correctly.</p><p>If, on the other hand, the game supports only a specific orientation, then locking the screen orientation of the screen sounds like the way to go.</p><p>Sadly, even though a method for locking the orientation exists in the <a href=\"https://caniuse.com/mdn-api_screen_lockorientation\">Screen API</a>, it's not supported yet by mobile browsers. In iOS, for example, this feature is hidden behind the Safari Feature Flags (as seen below), so the only option left here is to inform the player than only a certain device orientation is supported, by detecting and reacting to screen orientation changes.</p><figure class=\"img-container\"><img alt=\"iOS Safari Feature Flags\" src=\"https://www.stelabouras.com/blog/itchio-webgl/ios-screen-orientation-lock.png\"/><figcaption>Even though the Screen Orientation API is enabled, the Locking/Unlock part is disabled by default.</figcaption></figure><p>The following snippet on our PWA webpage assumes that the game only supports portrait orientation on mobile devices:</p><pre><code class=\"lang-js\">isMobile = () =&amp;gt; {    return /mobile/.test(navigator.userAgent.toLowerCase());};isLandscape = () =&amp;gt; {    return isMobile() &amp;amp;&amp;amp; /landscape/.test(screen.orientation.type.toLowerCase());};toggleLandscapePrompt = (enable) =&amp;gt; {    // Toggles landscape prompt visibility based on the value of the    // `enable` flag.};loadGame = () =&amp;gt; {    // Queries our server for the build url and creates the `iframe`    // containing the link to the Itch.io page hosting the Unity WebGL    // game.};window.addEventListener(&quot;DOMContentLoaded&quot;, () =&amp;gt; {    // Listen to orientation events    screen.orientation.addEventListener(&quot;change&quot;, (e) =&amp;gt; {        let landscape = isLandscape();        toggleLandscapePrompt(landscape);        if (!landscape) {            loadGame();        }    });    // If the game only supports portrait orientations, do not allow    // it to be loaded if player&#39;s device is initially held sideways.    if (isLandscape()) {        toggleLandscapePrompt(true);    }    else {        loadGame();    }}</code></pre><h3>Persistent data saving</h3><p>In WebGL, storing data in <code>PlayerPrefs</code> will not persist over page reloads. For this to happen, <code>PlayerPrefs</code> must be replaced with a different way of storing persistent information, which in most cases is the <code>localStorage</code> API. There's a caveat here: For the approach we are exploring in this article where we create a page that generates an iframe which points to the WebGL build on Itch.io, the <code>localStorage</code> API approach won't be enough, as it will store the data on the Itch.io page and if player has added the page as an web app to their home screen, this data will not persist over web app relaunches.</p><p>Firstly, let's tackle the simple scenario where a PWA webpage like the one we have above does not exist and we only aim to store player preferences persistently when Itch generates a new url because of a new build: For this to happen we use the <code>localStorage</code> API and we prefix our <code>PlayerPrefs</code> keys with a unique (to our game) string (<code>PREFIX_KEY</code>), which will allow our keys to co-exist with any other <code>localStorage</code> keys that might already exist for the Itch.io domain.</p><p>Assuming there's a centralized logic responsible for storing information for your game (for example: high scores, currency etc), the logic can me modified to work differently for WebGL builds.</p><ul><li><code>PlayerPrefsManager.cs</code></li></ul><pre><code class=\"lang-cs\">public static class PlayerPrefsManager {    private const string PREFIX_KEY = &quot;&lt;unique_key&gt;&quot;;    public static string PrefixKey(string key) {        return PREFIX_KEY + key;    }    public static void SetString(string key, string data) {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR        WebGLPluginJS.SaveData(PrefixKey(key), data);#else        PlayerPrefs.SetString(key, data);#endif    }    public static string GetString(string key, string defaultValue = &quot;&quot;) {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR        return WebGLPluginJS.LoadData(PrefixKey(key));#else        return PlayerPrefs.GetString(key);#endif    }    // NOTE: This can be extended to `GetFloat` / `SetFloat`, `GetInt` /    // `SetInt` methods.    public static void Save() {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR        // no-op#else        PlayerPrefs.Save();#endif    }    public static bool HasKey(string key) {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR        return WebGLPluginJS.LoadData(PrefixKey(key)) != string.Empty;#else        return PlayerPrefs.HasKey(key);#endif    }}</code></pre><ul><li><code>WebGLPluginJS.cs</code></li></ul><pre><code class=\"lang-cs\">[DllImport(&quot;__Internal&quot;)]public static extern void SaveData(string key, string data);[DllImport(&quot;__Internal&quot;)]public static extern string LoadData(string key);</code></pre><ul><li><code>WebGLPluginJS.jslib</code></li></ul><pre><code class=\"lang-js\">LoadData: function(yourkey){    var returnStr = &quot;&quot;;    if (localStorage.getItem(UTF8ToString(yourkey)) !== null) {        returnStr = localStorage.getItem(UTF8ToString(yourkey));    }    var bufferSize = lengthBytesUTF8(returnStr) + 1;    var buffer = _malloc(bufferSize);    stringToUTF8(returnStr, buffer, bufferSize);    return buffer;},SaveData: function(yourkey, yourdata){    localStorage.setItem(UTF8ToString(yourkey), UTF8ToString(yourdata));}</code></pre><p>Now, if we want to ensure that the <code>PlayerPrefs</code> persist not only across builds but also be synced on our PWA webpage, then we need to perform an extra step: When players install the PWA app, whatever is stored in the <code>localStorage</code> of the hosted <code>&lt;iframe&gt;</code> of that webpage will not persist, which means that we will want to store the information to the <code>localStorage</code> of the PWA webpage and not of the iframe. In order to do that we can use <code>postMessage</code> again to sync the state between the build webpage and our PWA webpage. We can start by storing the <code>PlayerPrefs</code> on a custom object and when the <code>PlayerPrefsManager.Save();</code> method is called, sync this object via <code>postMesage</code>, to the parent PWA webpage. We can also add an optional <code>#installed</code> hash in our generated build url which can differentiate between the webpage being hosted on the game website on Itch or on our PWA webpage where we fully control the logic.</p><p>Here is how the code can be modified to account for that:</p><ul><li><code>PlayerPrefsManager.cs</code></li></ul><pre><code class=\"lang-cs\">public static class PlayerPrefsManager {    public static void Save() {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR        WebGLPluginJS.SyncToParent();#else        PlayerPrefs.Save();#endif    }}</code></pre><ul><li><code>WebGLPluginJS.cs</code></li></ul><pre><code class=\"lang-cs\">[DllImport(&quot;__Internal&quot;)]public static extern void SyncToParent();</code></pre><ul><li><code>WebGLPluginJS.jslib</code></li></ul><pre><code class=\"lang-js\">LoadData: function(yourkey){    var returnStr = &quot;&quot;;    if (window.location.hash == &quot;#installed&quot;) {        if (window.parentLocalStorage[UTF8ToString(yourkey)] != undefined) {            returnStr = window.parentLocalStorage[UTF8ToString(yourkey)];        }    }    else {        if (localStorage.getItem(UTF8ToString(yourkey)) !== null) {            returnStr = localStorage.getItem(UTF8ToString(yourkey));        }    }    var bufferSize = lengthBytesUTF8(returnStr) + 1;    var buffer = _malloc(bufferSize);    stringToUTF8(returnStr, buffer, bufferSize);    return buffer;},SaveData: function(yourkey, yourdata){    if (window.location.hash == &quot;#installed&quot;) {        window.parentLocalStorage[UTF8ToString(yourkey)] = UTF8ToString(yourdata);    }    else {        localStorage.setItem(UTF8ToString(yourkey), UTF8ToString(yourdata));    }},SyncToParent: function() {    if (window.location.hash != &quot;#installed&quot;) {        return;    }    parent.postMessage({ &#39;localStorage&#39;: window.parentLocalStorage }, &quot;*&quot;);}</code></pre><p>and on our PWA webpage we can modify the <code>message</code> listener to also listen for the 'localstorage' messages and update its own <code>localStorage</code> accordingly.</p><pre><code class=\"lang-js\">window.addEventListener(&quot;message&quot;, (e) =&amp;gt; {    var data = e.data;    // ...    else if (typeof data == &quot;object&quot; &amp;amp;&amp;amp; data.localStorage != undefined) {        for (const [k, v] of Object.entries(data.localStorage)) {            localStorage.setItem(k, v);        }    }},false);</code></pre><p>We also want our PWA webpage to sync its contents with the game from its <code>localStorage</code> when the game loads. We can add the following logic when constructing the <code>&lt;iframe&gt;</code> to be added to our webpage and before inject it:</p><pre><code class=\"lang-js\">// assuming that we have constructed the build_urlvar iframe = document.createElement(&#39;iframe&#39;);iframe.setAttribute(&#39;allowtransparency&#39;, &#39;true&#39;);iframe.setAttribute(&#39;webkitallowfullscreen&#39;, &#39;true&#39;);iframe.setAttribute(&#39;mozallowfullscreen&#39;, &#39;true&#39;);iframe.setAttribute(&#39;msallowfullscreen&#39;, &#39;true&#39;);iframe.setAttribute(&#39;allowfullscreen&#39;, &#39;true&#39;);iframe.setAttribute(&#39;frameborder&#39;, &#39;0&#39;);iframe.setAttribute(&#39;scrolling&#39;, &#39;no&#39;);iframe.setAttribute(&#39;allow&#39;, &#39;autoplay; fullscreen *; geolocation; microphone; camera; midi; monetization; xr-spatial-tracking; gamepad; gyroscope; accelerometer; xr; cross-origin-isolated; web-share&#39;);iframe.id = &#39;game&#39;;iframe.src = build_url + &#39;#installed&#39;;iframe.onload = () =&amp;gt; {    let storageDict = JSON.parse(JSON.stringify(localStorage));    document.querySelector(&#39;iframe&#39;).contentWindow.postMessage({        &#39;localStorage&#39;: storageDict    }, &#39;*&#39;);};document.body.appendChild(iframe);</code></pre><p>On the HTML file that Unity generates that actually hosts our game we can add the following:</p><pre><code class=\"lang-js\">// Sync localStorage from parent framedocument.addEventListener(&quot;DOMContentLoaded&quot;, () =&amp;gt; {    window.parentLocalStorage = {};    window.addEventListener(&quot;message&quot;, (e) =&amp;gt; {        let data = e.data        if (typeof data == &quot;object&quot; &amp;amp;&amp;amp; data.localStorage != undefined) {            for (const [k, v] of Object.entries(data.localStorage)) {                window.parentLocalStorage[k] = v;            }        }    });});</code></pre><h4>Bonus: Update the background color of the hosted webpage</h4><p>Given that we now have an easy JS bridge between our game and the build / PWA webpages, we can have some fun as well! One cool thing we can implement is a way to change the background color of the hosted webpage in order to match the color of the scene! You can just change the background color of the build webpage or propagate the change all the way to the PWA webpage.</p><ul><li><code>GameLogic.cs</code></li></ul><pre><code class=\"lang-cs\">public void ChangeBackgroundColor(Color color) {#if UNITY_WEBGL &amp;amp;&amp;amp; !UNITY_EDITOR    WebGLPluginJS.ChangeBackgroundColor(ColorUtility.ToHtmlStringRGB(color));#endif}</code></pre><ul><li><code>WebGLPluginJS.cs</code></li></ul><pre><code class=\"lang-cs\">[DllImport(&quot;__Internal&quot;)]public static extern void ChangeBackgroundColor(string hexCode);</code></pre><ul><li><code>WebGLPluginJS.jslib</code></li></ul><pre><code class=\"lang-js\">ChangeBackgroundColor: function (hexCode) {    var hexCodeString = UTF8ToString(hexCode);    document.documentElement.style.backgroundColor = &quot;#&quot; + hexCodeString;    document.body.style.backgroundColor = &quot;#&quot; + hexCodeString;},</code></pre><p>and there you have it!</p><h3>More ideas</h3><p>Given that a PWA is essentially a webpage, we can leverage more APIs to improve the user experience. Things like service workers to perform tasks in the background, offline support and more, can be just the tip of the iceberg!</p><h3>Further reading</h3><p>Below are references I have collected during my research that you might find useful:</p><ul><li><a href=\"https://www.patrykgalach.com/2020/04/27/unity-js-plugin/\">https://www.patrykgalach.com/2020/04/27/unity-js-plugin/</a></li><li><a href=\"https://itch.io/t/140214/persistent-data-in-updatable-webgl-games\">https://itch.io/t/140214/persistent-data-in-updatable-webgl-games</a></li><li><a href=\"https://itch.io/t/635029/enable-a-way-for-browser-games-to-access-the-username-of-current-player\">https://itch.io/t/635029/enable-a-way-for-browser-games-to-access-the-username-of-current-player</a></li><li><a href=\"https://itch.io/t/1841689/unity3d-playerprefs-problem-on-updating-build\">https://itch.io/t/1841689/unity3d-playerprefs-problem-on-updating-build</a></li><li><a href=\"https://github.com/giantlight-matt/unity-webgl-itch-playerprefs/\">https://github.com/giantlight-matt/unity-webgl-itch-playerprefs/</a></li><li><a href=\"https://discussions.unity.com/t/onpause-events-for-webgl-builds/640800/14\">https://discussions.unity.com/t/onpause-events-for-webgl-builds/640800/14</a></li><li><a href=\"https://www.heltweg.org/posts/checklist-issues-progressive-web-apps-how-to-fix/\">https://www.heltweg.org/posts/checklist-issues-progressive-web-apps-how-to-fix/</a></li><li><a href=\"https://medium.com/appscope/changing-the-ios-status-bar-of-your-progressive-web-app-9fc8fbe8e6ab\">https://medium.com/appscope/changing-the-ios-status-bar-of-your-progressive-web-app-9fc8fbe8e6ab</a>&lt;/iframe&gt;&lt;/iframe&gt;&lt;/unique_key&gt;&lt;/iframe&gt;&lt;/iframe&gt;&lt;/iframe&gt;&lt;/head&gt;</li></ul>",
            "summary": "The following is a collection of improvements that can be implemented on a Unity WebGL game relea...Continue reading \u2192",
            "date_published": "2024-12-17T00:00:00Z",
            "date_modified": "2024-12-17T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/is-the-sky-falling-on-my-head-shortcut/",
            "url": "https://www.stelabouras.com/blog/is-the-sky-falling-on-my-head-shortcut/",
            "title": "The &#34;Is the sky falling on my head?&#34; iOS shortcut",
            "content_html": "<p>Every now and then we learn about a new asteroid passing really close to Earth, usually within a couple of hours of it being detected. And right after all those 'the end is nigh' click-bait news articles, we <a href=\"https://x.com/NASA/status/1807412526977544683\">are reassured by NASA</a> that there is no need to panic, with a link to their <a href=\"https://eyes.nasa.gov/\">Eyes on Asteroids</a> website.</p><p>News like that, made me think of ways we can have a simple up-to-date and easy to access view for those... close encounters.</p><p>Fortunately, NASA already offers an API that we can use in order to retrieve information on close-approach asteroids and comets: The Small-Body Database (aka SBDB) Close-Approach Data API <sup class=\"footnote-ref\" id=\"fnref-1\"><a href=\"#fn-1\">1</a></sup>. Yup, it's a mouthful.</p><p>The API is well-documented and offers a plethora of query parameters we can use, like filtering by the distance of the small bodies in relation to Earth (or any other body <sup class=\"footnote-ref\" id=\"fnref-2\"><a href=\"#fn-2\">2</a></sup>), by date, as well as controlling which parameters are going to be available on the results, like the full name of the approaching small body.</p><p>Given that <a href=\"https://www.stelabouras.com/blog/youtube-redirect-shortcut/\">I have been using the iOS shortcuts a lot lately</a>, I have created a small \"Is the sky falling on my head?\" iOS shortcut that upon being triggered, displays the closest upcoming small body, offering three links for more information:</p><ul><li>The closest approach information of the small body on the <a href=\"https://eyes.nasa.gov/apps/asteroids/#/watch\">Asteroid Watch</a> section of the Eyes on Asteroids NASA website.</li><li>Its more detailed information view (Essential stats, Orbital path, Close approach) on the Eyes on Asteroids NASA website (e.g. <a href=\"https://eyes.nasa.gov/apps/asteroids/#/2024_lh\">2024 LH</a>).</li><li>The associated entry of the small body on the Small-Body Database of NASA's JPL (e.g. <a href=\"https://ssd.jpl.nasa.gov/tools/sbdb_lookup.html#/?sstr=2024%20LH\">2024 LH</a>).</li></ul><p>I have also created an automation and extra helper shortcut: The automation fires once a day (when I wake up) and triggers the helper shortcut that just returns the distance of the closest small body.</p><p>If that distance is lower than a threshold I have already set (I found that 1.000.000 km is a nice threshold), then the automation runs the main 'Is the sky falling on my head?' shortcut that reports the information, otherwise I get a message that our planet is safe...for now!</p><p>I admit that it is really tempting to try and cram all the information reported by NASA's API to a single report (parameters like the body's velocity, diameter, sigma are all interesting) but I have intentionally tried to keep the report as simple as possible, while offering links for more information.</p><p>Here's an example report from the other day:</p><p class=\"img-container\"><img alt=\"An example of the report generated by the 'Is the sky falling on my head?' iOS shortcut\" src=\"https://www.stelabouras.com/blog/is-the-sky-falling-on-my-head-shortcut/shortcut.jpg\"/></p><p>Looks like we are safe!</p><p>You can download and tinker with those shortcuts below.</p><p class=\"centered\"><a href=\"https://www.icloud.com/shortcuts/e738edfa3ba6455084323cf80d8ffe85\" title=\"Download 'Is the sky falling on my head?' Shortcut\">Download 'Is the sky falling on my head?' Shortcut</a></p><p class=\"centered\"><a href=\"https://www.icloud.com/shortcuts/562746b11e044b0f8c78a5c1d461b3cb\" title=\"Download 'Is the sky falling on my head? [HELPER]' Shortcut\">Download 'Is the sky falling on my head? [HELPER]' Shortcut</a></p><div class=\"footnotes\"><hr><ol><li id=\"fn-1\"><p><a href=\"https://ssd-api.jpl.nasa.gov/doc/cad.html\">https://ssd-api.jpl.nasa.gov/doc/cad.html</a><a href=\"#fnref-1\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-2\"><p><a href=\"https://ssd-api.jpl.nasa.gov/doc/cad.html#close-approach-bodies\">https://ssd-api.jpl.nasa.gov/doc/cad.html#close-approach-bodies</a><a href=\"#fnref-2\" class=\"footnote\">&#8617;</a></p></li></ol></div>",
            "summary": "Every now and then we learn about a new asteroid passing really close to Earth, usually within a ...Continue reading \u2192",
            "date_published": "2024-07-04T00:00:00Z",
            "date_modified": "2024-07-04T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/ios-shortcuts/",
            "url": "https://www.stelabouras.com/blog/ios-shortcuts/",
            "title": "On iOS home screen shortcut items",
            "content_html": "<p>How many times have you tried accessing the settings of an iOS application on your device, but just the thought of scrolling through this really long list of installed applications in the Settings app makes you abandon all hope?</p><p>Maybe I am the only one answering positively to the above rhetorical question, but I am surprised that this particular user experience still feels like a chore.</p><p>Although I haven't been using Android as much as I do iOS, I find it really easy that on Android you can access the application settings (called \"App info\") just by long tapping on the application icon on the Android home screen.</p><p class=\"img-container\"><img alt=\"The App info option when long tapping on an Android application\" src=\"https://www.stelabouras.com/blog/ios-shortcuts/android-app-info.jpg\"/></p><p>It's so liberating that it made me wonder why there is no such option on iOS, when force-tapping on an application icon on the iOS Home Screen.</p><p>Fortunately, UIKit provides us with the APIs necessary in order to build such an interaction ourselves. So below you can find a simple way to implement this interaction in the applications you develop (I had to clarify that so that the post doesn't read like an end-user guide where anyone can 'hack' the iOS to do something it cannot).</p><p>There are only three key components we need in order to build this kind of interaction:</p><ul><li>The <code>shortcutItems</code> <sup class=\"footnote-ref\" id=\"fnref-1\"><a href=\"#fn-1\">1</a></sup> property (iOS 9.0+), that allows us to define custom home screen shortcuts programmatically.</li><li>The <code>openSettingsURLString</code> <sup class=\"footnote-ref\" id=\"fnref-2\"><a href=\"#fn-2\">2</a></sup> (iOS 8.0+) and <code>openNotificationSettingsURLString</code> <sup class=\"footnote-ref\" id=\"fnref-3\"><a href=\"#fn-3\">3</a></sup> (iOS 16.0+) properties, that allows us to navigate to the respective view of the native Settings application.</li><li>The <code>gear</code> (iOS 13.0+) and <code>bell.badge</code> (iOS 14.0+) SF Symbols <sup class=\"footnote-ref\" id=\"fnref-4\"><a href=\"#fn-4\">4</a></sup>, allowing us to set the proper icon to those shortcuts that 100% match the iOS style.</li></ul><p>Based on the above, if we only want to provide a shortcut to the main settings of the application, the application can just target iOS 13.0+ but if we also want to provide a separate shortcut to the notification settings, then the application must target iOS 16.0+. In any case, given that the iOS 18.0 release is just around the corner, I don't think this is a limiting factor.</p><p>Using those components, I have created <a href=\"https://github.com/stelabouras/shortcuts-sample\">a simple Swift sample application on GitHub</a> that showcases this interaction, producing the following contextual menu when force-tapping the application icon on the iOS Home Screen:</p><p class=\"img-container\"><img alt=\"The new home shortcuts implemented using the APIs mentioned in this blogpost\" src=\"https://www.stelabouras.com/blog/ios-shortcuts/ios-shortcuts.jpg\"/></p><p>I will start implementing this interaction to my released applications soon, as I find it really helpful to be able to access the application's settings and notification views with just one tap!</p><p>Some (minor) caveats:</p><ul><li>As I have already mentioned, this has to be implemented by the developer of each application separately, so there is a cost associated to that.</li><li>If the application is installed but never launched, those shortcuts will not be available to the user.</li><li>There are some rare cases where even though the shortcut is processed by the logic and the Settings application is launched, the application settings view is not pushed at all.</li></ul><div class=\"footnotes\"><hr><ol><li id=\"fn-1\"><p><a href=\"https://developer.apple.com/documentation/uikit/uiapplication/1623033-shortcutitems\">https://developer.apple.com/documentation/uikit/uiapplication/1623033-shortcutitems</a><a href=\"#fnref-1\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-2\"><p><a href=\"https://developer.apple.com/documentation/uikit/uiapplication/1623042-opensettingsurlstring/\">https://developer.apple.com/documentation/uikit/uiapplication/1623042-opensettingsurlstring/</a><a href=\"#fnref-2\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-3\"><p><a href=\"https://developer.apple.com/documentation/uikit/uiapplication/4013180-opennotificationsettingsurlstrin/\">https://developer.apple.com/documentation/uikit/uiapplication/4013180-opennotificationsettingsurlstrin/</a><a href=\"#fnref-3\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-4\"><p><a href=\"https://developer.apple.com/sf-symbols/\">https://developer.apple.com/sf-symbols/</a><a href=\"#fnref-4\" class=\"footnote\">&#8617;</a></p></li></ol></div>",
            "summary": "How many times have you tried accessing the settings of an iOS application on your device, but ju...Continue reading \u2192",
            "date_published": "2024-06-30T00:00:00Z",
            "date_modified": "2024-06-30T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/youtube-redirect-shortcut/",
            "url": "https://www.stelabouras.com/blog/youtube-redirect-shortcut/",
            "title": "The YouTube Redirect Shortcut",
            "content_html": "<p>It has been a while since I have started the multi-year process of de-Googling myself. It is nowhere near close to the finish line, but I have taken a few steps towards it.</p><p>The past few months I have been trying to find a good YouTube alternative. I am already a subscriber on Nebula <sup class=\"footnote-ref\" id=\"fnref-1\"><a href=\"#fn-1\">1</a></sup> (a good, albeit pretty light on content competitor to YouTube) and I am trying my best to support some of my favorite YouTube creators over on Patreon <sup class=\"footnote-ref\" id=\"fnref-2\"><a href=\"#fn-2\">2</a></sup>.</p><p>For YouTube content, I have recently discovered Invidious <sup class=\"footnote-ref\" id=\"fnref-3\"><a href=\"#fn-3\">3</a></sup>, which is a front-end alternative, meaning that the videos are still hosted on and served by YouTube (note: there are also alternatives to Invidious, like Piped <sup class=\"footnote-ref\" id=\"fnref-9\"><a href=\"#fn-9\">4</a></sup>). Invidious is based on public instances that other people have already set up, and given the nature of its implementation, it is locked on a cat &amp; mouse game with YouTube, where the logic can break everytime Google changes something on their website <sup class=\"footnote-ref\" id=\"fnref-4\"><a href=\"#fn-4\">5</a></sup>. That said it is a rather good alternative: It is stable (minus a few hick-ups when something breaks by Google), it has an active community, there are a lot of different integrations (browser extensions <sup class=\"footnote-ref\" id=\"fnref-5\"><a href=\"#fn-5\">6</a></sup>, applications <sup class=\"footnote-ref\" id=\"fnref-6\"><a href=\"#fn-6\">7</a></sup> etc) and you can always spin up your own private instance if you so desire!</p><p>In the spirit of trying to move away from Google products, I have built this small Shortcut for MacOS / iOS devices <sup class=\"footnote-ref\" id=\"fnref-7\"><a href=\"#fn-7\">8</a></sup>. Its purpose is simple: Every time you try to watch a YouTube video either on the YouTube website or the YouTube mobile application, you have the option to get redirected to the embedded version of the same video on an Invidious instance of your choice.</p><p>On the Shortcut setup (Apple calls that 'Import Questions'), I have added the option to specify the Invidious instance of your choice as well as change the auto-play flag. If you leave the Invidious instance field blank, you will be redirected to the GDPR compliant \"youtube-nocookie\" embedded URL that contains no ads and no tracking. The auto-play flag controls whether the video will start auto-playing (muted, to comply with the auto-play policies <sup class=\"footnote-ref\" id=\"fnref-8\"><a href=\"#fn-8\">9</a></sup>) as soon as it loads, or not.</p><p>On top of that, by viewing the videos this way you can use the Picture-in-Picture (PiP) feature of iOS without being a YouTube Premium subscriber! 😉</p><p>You can find a link to the shortcut below. After adding it to your MacOS / iOS device you can answer the Import Questions and you are ready to go!</p><p class=\"centered\"><a href=\"https://www.icloud.com/shortcuts/381cc098713c462aa72113b31f3562ac\">Download YouTube Redirect Shortcut</a></p><div class=\"footnotes\"><hr><ol><li id=\"fn-1\"><p><a href=\"https://nebula.tv\">https://nebula.tv</a><a href=\"#fnref-1\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-2\"><p><a href=\"https://www.patreon.com\">https://www.patreon.com</a><a href=\"#fnref-2\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-3\"><p><a href=\"https://invidious.io\">https://invidious.io</a><a href=\"#fnref-3\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-9\"><p><a href=\"https://github.com/TeamPiped/Piped\">https://github.com/TeamPiped/Piped</a><a href=\"#fnref-9\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-4\"><p><a href=\"https://github.com/iv-org/invidious/issues/4498\">https://github.com/iv-org/invidious/issues/4498</a><a href=\"#fnref-4\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-5\"><p><a href=\"https://github.com/SimonBrazell/privacy-redirect\">https://github.com/SimonBrazell/privacy-redirect</a><a href=\"#fnref-5\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-6\"><p><a href=\"https://freetubeapp.io\">https://freetubeapp.io</a><a href=\"#fnref-6\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-7\"><p><a href=\"https://support.apple.com/guide/shortcuts/intro-to-shortcuts-apdf22b0444c/7.0/ios/17.0\">https://support.apple.com/guide/shortcuts/intro-to-shortcuts-apdf22b0444c/7.0/ios/17.0</a><a href=\"#fnref-7\" class=\"footnote\">&#8617;</a></p></li><li id=\"fn-8\"><p><a href=\"https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability\">https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability</a><a href=\"#fnref-8\" class=\"footnote\">&#8617;</a></p></li></ol></div>",
            "summary": "It has been a while since I have started the multi-year process of de-Googling myself. It is nowh...Continue reading \u2192",
            "date_published": "2024-04-07T00:00:00Z",
            "date_modified": "2024-04-07T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/the-unity-library/",
            "url": "https://www.stelabouras.com/blog/the-unity-library/",
            "title": "The Unity Library",
            "content_html": "<p>...or how to write a simple static website generator in a day.</p><p>Ten days ago I launched a simple project called '<a href=\"https://unity.stelabouras.com\">The Unity Library</a>'.</p><p>The Unity Library is a small, curated collection of Unity related tutorials, posts and tips that I kept for personal reference, which I decided to make public as a means of giving back to this amazing community.</p><p>Here's the announcement <a href=\"https://x.com/stelabouras/status/1235638832760008704\">originally posted on Twitter</a>:</p><hr><p>Over the past few months, I have been h̶o̶a̶r̶d̶i̶n̶g̶ collecting and organizing Unity related posts, shaders, tutorials and tips from game developers and designers I follow, respect and look up to.</p><p>Although this was created just as a personal reference, I have decided to build a public website &amp; host this information for anyone interested in taking a deeper dive in subjects like Raymarching, Lava or Snow rendering, Tool shading, Sea and Water shaders and many more.</p><p>Let me emphasize this: This project wouldn't be possible without creators like <a href=\"https://x.com/AlanZucconi\">AlanZucconi</a>, <a href=\"https://x.com/catlikecoding\">catlikecoding</a>, <a href=\"https://x.com/Cyanilux\">Cyanilux</a>, <a href=\"https://x.com/febucci\">febucci</a>, <a href=\"https://x.com/HarryAlisavakis\">HarryAlisavakis</a>, <a href=\"https://x.com/TheAllenChou\">TheAllenChou</a>, <a href=\"https://x.com/minionsart\">minionsart</a>, <a href=\"https://x.com/andre_mc\">andre_mc</a>, <a href=\"https://x.com/totallyRonja\">totallyRonja</a>, <a href=\"https://x.com/ManuelaXibanya\">ManuelaXibanya</a>, <a href=\"https://x.com/DanielJMoran\">DanielJMoran</a> and so many others.</p><p>If you happen to be a Unity game developer, I would strongly suggest following the aforementioned accounts; I have learned so much from those people over the past few months and I really want to thank them for sharing their knowledge.</p><p>Here's the link to the project: <a href=\"https://unity.stelabouras.com\">https://unity.stelabouras.com</a></p><p>I hope you like it. This library will be updated with new links every time I find something interesting and useful!</p><p>Finally, I want to dedicate this project to my beloved grandpa, who recently passed away. ♥️</p><hr><p>The response was quite overwhelming and definitely disproportionate to the amount of time I spent building it! 🥰</p><p>The whole idea started when I noticed that a collection of Unity-related links I was maintaing, was growing significantly in number on my <a href=\"https://linkpack.io\">Linkpack</a> folder and I needed a separate searchable interface which I could consult often without having to go through my files. So I began grouping those files into folders and ended up creating a simple parser script to generate the library structure.</p><p>I reused some simple logic already implemented in Python on the server-side of Linkpack, so that I could extract the necessary information from the file structure into a JSON format. The script was aptly named the ...\"Librarian\", as it was tasked with traversing the whole folder, parsing the files, extracting the links, checking the <a href=\"https://ogp.me/\">Open Graph</a> tags of the website, storing the url in the <a href=\"https://archive.org/help/wayback_api.php\">Wayback Machine</a>, and finally storing the whole library JSON structure into a separate file.</p><p>After that, I created a simple generator script that was responsible for creating the whole static website from that JSON script, by generating the proper folder structure, building the HTML files, copying the static CSS and JS I have coded to style the pages correctly and getting the whole JSON structure loaded in Javascript so that the whole website is searchable.</p><p>For hosting the project, I picked the trusted <a href=\"https://netlify.com/\">Netlify service</a>, the same service this very blog is hosted at!</p><p>Overall 'The Unity Library' was a rather fun little project and something that I keep improving those days whenever I have a chance.</p><p><strong>Update!</strong> The Unity Library is now open source under a MIT license! <a href=\"https://gitlab.com/stelabouras/unity-library/\">You can find the project at Gitlab</a> where you can file an MR if you have an interesting link that can be included at the library!</p>",
            "summary": "...or how to write a simple static website generator in a day. Ten days ago I launched a simple p...Continue reading \u2192",
            "date_published": "2020-03-15T00:00:00Z",
            "date_modified": "2020-03-15T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/airplane-safety/",
            "url": "https://www.stelabouras.com/blog/airplane-safety/",
            "title": "Airplane Safety - My most personal project",
            "content_html": "<p>This is the story of <em><a href=\"https://airplane-safety.stelabouras.com/\">Airplane Safety</a></em>, one of my dad's personal projects and also his last.</p><p>While discussing the project around June 2017, he asked me how could we communicate this idea to the right people, so I suggested translating and releasing it to the Web.</p><p>I remember saying to him: \"Let's release it once I come back from my holiday vacations\".</p><p>If only I knew.</p><p>My dad passed away suddenly -without any warning or sign of illness- at the age of 59, the day I was returning home from my vacations in August.</p><p>Since then I have tried multiple times to start working on this project, but failed to do so, as grief was taking over of me every time. It is only lately that I have managed to muster enough courage to begin translating it.</p><p>As you will notice by the <a href=\"https://airplane-safety.stelabouras.com/#abstract\">abstract</a>, my dad was a high-school Physics professor and even though he adored his job, he was also interested in expanding his knowledge by attending online courses on advanced Physics concepts, was eager to help students by providing free after-school classes and loved coming up with ideas on how certain technologies could be improved.</p><p>I remember him telling me that he had also drafted some thoughts, way back when, of a system similar to the one implemented by some cars manufacturers today, able to detect movemevent via sensors and stop the car before an accident could occur.</p><p>The idea behind this <a href=\"https://airplane-safety.stelabouras.com/\">project</a>, though, was one of my dad's biggest concerns: How can the aircraft passengers and crew be saved, even under a severe mulfunction event.</p><p>In <em>Airplane Safety</em> you will not find any mathematic formulas or detailed schematics, but a rather high level approach of the issue, which I think is the best way to convey any ideas at an early stage.</p><p>My dad wished to release this document for free for anyone to read, just in case those ideas made it to some airplane safety specialists.</p><p>For this reason, I am <a href=\"https://github.com/stelabouras/airplane-safety\">open sourcing</a> all of his original notes (scanned and in docx format in Greek), as well as the translation in multiple formats (PDF, epub, LaTeX, Markdown) under the MIT License.</p><p>I could go in great lengths about my dad's character, how he was the most beloved teacher, co-worker, husband and father, but I will just finish this post by saying this one last thing:</p><p><strong><a href=\"https://airplane-safety.stelabouras.com/\">This is for you dad</a>, I love you.</strong></p>",
            "summary": "This is the story of Airplane Safety, one of my dad\u0026#39;s personal projects and also his last. While ...Continue reading \u2192",
            "date_published": "2018-03-24T00:00:00Z",
            "date_modified": "2018-03-24T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/hearth/",
            "url": "https://www.stelabouras.com/blog/hearth/",
            "title": "Hearth: Decentralized Website Publishing",
            "content_html": "<p>Last week, me and <a href=\"https://www.stavros.io/\">Stavros</a> introduced our new side-project to the world, <a href=\"https://hearth.eternum.io/\">Hearth</a>.</p><p>Hearth aims to be your go-to website publisher for the decentralized web, harnessing the power of <a href=\"https://ipfs.io/\">IPFS</a>.</p><p>Having built <a href=\"https://www.eternum.io/\">Eternum</a>, an IPFS pinning service, we were trying to find use cases where IPFS could be useful to a larger audience.</p><p>Being an active Dropbox user, I was really disappointed when the <a href=\"https://www.dropbox.com/help/files-folders/public-folder\">Public folder was disabled</a> earlier last year.</p><p>Dropping a small static website into your Dropbox Public folder and being able to send a link to it felt really great for sharing prototypes and experiments. Sadly, by sunsetting this feature, it was no longer possible to publish websites without having to deal with different services and web UIs.</p><p>That's pretty much how the idea for Hearth was born: We wanted to have a simple desktop app that will run IPFS as a daemon and replicate the Dropbox file sharing experience, without a centralized authority. That means, sharing links to your files from your desktop and also serving mini websites while doing so.</p><p>Stavros wrote a <a href=\"https://www.rust-lang.org/en-US/\">Rust</a> console app that interfaces with the IPFS daemon, providing an API for requesting links to your files on the Hearth folder. I wrote a MacOS menu app with a Finder Sync extension in <a href=\"https://developer.apple.com/swift/\">Swift</a> that dealt with the UI layer and interfacing with the Finder app. In case you are interested to take a look under the hood, both of the apps (the <a href=\"https://gitlab.com/stavros/hearth\">Rust daemon</a> and the <a href=\"https://gitlab.com/stelabouras/hearth-mac\">Swift MacOS app</a>) are open source.</p><p>I also had the pleasure of designing the <a href=\"https://hearth.eternum.io/mac_icon.png\">app icon</a> and <a href=\"https://hearth.eternum.io/\">website</a> using <a href=\"https://www.sketchapp.com/\">Sketch</a>, and I am really happy with the results. SVG animations -when used subtly- can be quite pleasing to the eye.</p><p>Although there have been some challenges in every layer involved (Rust being...Rust, MacOS integration issues and IPFS stability), we have come to a point where Hearth is now quite robust and -with its <a href=\"https://hearth.eternum.io/\">latest 0.0.3</a> version- able to auto-update itself using the awesome <a href=\"https://sparkle-project.org/\">Sparkle</a> library.</p><p>Building Hearth was quite an enjoyable experiment! As Hearth is my third Swift project (<a href=\"https://itunes.apple.com/app/pastery-for-xcode/id1214720236\">Pastery for Xcode</a> and <a href=\"https://itunes.apple.com/us/app/wsh-lst/id1323785754?mt=8\">WSH LST</a> being the first and second respectivelly), it now feels great to develop mobile and desktop apps using Apple's new language. On top of that, having to deal with a new technology like IPFS made the whole effort way more attractive.</p><p>We are still in talks with the IPFS team to iron out the timeout issues that may occur when trying to access a generated hash and we are also looking at the rest of the OS releases. If you want to contribute in any way, you can create an <a href=\"https://gitlab.com/stavros/hearth/issues\">issue</a> in the Hearth repo.</p>",
            "summary": "Last week, me and Stavros introduced our new side-project to the world, Hearth. Hearth aims to be...Continue reading \u2192",
            "date_published": "2018-03-15T00:00:00Z",
            "date_modified": "2018-03-15T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/reboot/",
            "url": "https://www.stelabouras.com/blog/reboot/",
            "title": "Reboot.",
            "content_html": "<p>Six years.</p><p>Six freaking years.</p><p>That's how long it took me to write a new post.</p><p>This website was collecting dust for a really long time, but not anymore!</p><p>Lots of things have happened during this period, both professionally and personally, and I will try to keep a -somewhat- up-to-date diary from now on, starting with the reboot of this very website.</p><p>The old Tumblr backend was not as straight-forward of a blogging experience as I was wanting it to be, so I figured it was time to move on.</p><p>This new website is powered by a static CMS called <a href=\"https://www.getlektor.com/\">Lektor</a>, which also powers some of my <a href=\"/projects\">side-projects</a>, and it's hosted in <a href=\"https://netlify.com\">Netlify</a>, which is hands down one of the best services I have used for quite a long time.</p><p>The website is really crude at the moment, so apologies for any weird issues happening here and there. I will also add things like an RSS feed, a Google crawler-friendly sitemap and probably comments, but I believe this first version is a good start.</p><p>Thanks for stopping by!</p>",
            "summary": "Six years. Six freaking years. That\u0026#39;s how long it took me to write a new post. This website was c...Continue reading \u2192",
            "date_published": "2018-03-09T00:00:00Z",
            "date_modified": "2018-03-09T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/2012-resolutions/",
            "url": "https://www.stelabouras.com/blog/2012-resolutions/",
            "title": "2012 resolutions",
            "content_html": "<p>I turn 26 today and it's about time I release my resolutions for 2012.</p><p>But first let's review the <a href=\"http://stelabouras.com/post/2548675445/newyearsresolutions\">last year's resolutions</a>:</p><ul><li>Work out a lot (Kinect &amp;&amp; (Walk || Cycle || Gym))</li><li>Eat Healthy</li><li>Cook more (&amp; healthy)</li></ul><p>The first three were a success. I lost 32kg in 2011 (in 5 months) following the advice of Tim Ferriss (<a href=\"http://www.amazon.com/4-Hour-Body-Uncommon-Incredible-Superhuman/dp/030746363X\">4 Hour body</a>) plus I've been exercising regularly the last 4 months (<a href=\"http://gymn.gr\">gym</a>) and I learned to eat better &amp; healthier.</p><ul><li>Travel (outside Greece for first time hopefully)</li></ul><p>Happy to report that this one was also a success! After visiting Scotland (and High Lands) this summer, I scored another trip abroad in Berlin, <a href=\"http://www.google.com/events/developerday/2011/berlin/\">Germany for Google Developers Day in November</a>.</p><ul><li>Look into this coffee “business” (Marco inspired me)</li></ul><p>I just didn't pursue this one, I think I will never be a \"coffee\" guy, but I must admit that lately I really enjoy drinking tea.</p><ul><li>Play way less (or quit playing) World of Warcraft</li></ul><p>I quit playing WoW! I decided to stop after lots of years, partly due to disappointment from Blizzard's way of releasing content and due its time-sucking nature of it. Also I stopped playing online games (Call of Duty, Battlefield) in general, although this was not intended. From the other hand I am really active in gaming: I finished two big titles in 2011 (Deus Ex and Skyrim), I <a href=\"http://gamesvideos.tv\">keep tabs with everything gaming related</a> and I am really looking forward to play all the other games I have in my library!</p><ul><li>Launch my personal projects &amp; update the existing ones</li></ul><p>Somewhat of a success: In 2011 I developed (and still developing) <a href=\"http://analytiksapp.com\">Analytiks</a>, I launched some mini projects (<a href=\"http://stelabouras.com/post/5157077022/canvasexperiments\">HTML5 oriented</a> and a <a href=\"http://stelabouras.com/post/4000086387/greaderreimagined\">Google Reader one</a>) started a <a href=\"http://gamesvideos.tv\">small gaming blog</a> and I begun contributing on what became <a href=\"http://evilwindowdog.com/soundbeam\">Soundbeam</a>, the first iOS app of <a href=\"http://evilwindowdog.com/\">Evil Window Dog</a> studio we \"founded\" with my buddy <a href=\"http://twitter.com/petrakeas\">Petros</a>.</p><ul><li>Look into interesting and new technologies (iOS, Node.js, Python, WebGL/JS 3D frameworks)</li><li>Get involved into OS projects (Github FTW)</li></ul><p>I can't say I have met my standards in this one: I was really focused on iOS programming in 2011, which totally overshadowed any other language/technology I wanted to get involved with. Hopefully this will be fixed this year.</p><ul><li>Be a better person to everyone around me</li></ul><p>That's an ongoing process :) I will try harder this year, promise!</p><p>Now let's see the new resolutions. I divided the list into to separate sections: <strong>Personal stuff</strong> and <strong>Work related</strong>. Those two blend in many ways but I didn't want to create a huge list and never complete any of my targets. </p><p><strong>Personal Stuff</strong></p><ul><li>Keep on with the healthy diet / exercise</li><li>Try the tea \"business\" instead</li><li>Travel at least in two countries (preferably US + England)</li><li>Socialize more</li></ul><p><strong>Work Related</strong></p><ul><li>Learn new things (WebGL, Python, Unit Testing, Cocos2D, Continuous Integration...and the list goes on)</li><li>Make a game using <a href=\"http://unity3d.com/\">Unity</a> platform</li><li>Open source retired projects, upgrade or retire some others</li><li>Attend at least two dev conferences</li></ul><p>That's it! They may seem a few but rest assured, they are quite a task!</p><p>What's your resolutions for 2012? :)</p>",
            "summary": "I turn 26 today and it\u0026#39;s about time I release my resolutions for 2012. But first let\u0026#39;s review the...Continue reading \u2192",
            "date_published": "2012-01-29T00:00:00Z",
            "date_modified": "2012-01-29T00:00:00Z"
        },
        
        {
            "id" : "https://www.stelabouras.com/blog/minecraft-on-the-cloud/",
            "url": "https://www.stelabouras.com/blog/minecraft-on-the-cloud/",
            "title": "Minecraft on the cloud!",
            "content_html": "<p><strong>Update:</strong> I have found (after posting this guide) that there is already a pretty <a href=\"http://www.minecraftwiki.net/wiki/Tutorials/Saved_Data_Dropbox_Guide_Saves_Only\">well written tutorial in Minecraft Wiki</a>, so you might wanna check this out too!</p><p>Well, sort of! I mean, title is quite catchy, isn’t it?</p><p>The following piece is a quick tutorial of how you can both save and sync through multiple machines, your <a href=\"http://minecraft.net/\">Minecraft</a> worlds on the cloud (<a href=\"https://www.dropbox.com/\">Dropbox</a>). Think of it this way: It will operate almost transparently (except those Dropbox <a href=\"http://growl.info/\">Growl</a> notifications which can be turned off) just like <a href=\"http://steampowered.com/steamworks/ov_cloud.php\">Steam Cloud</a>.</p><p>I have tested the following steps and I can assure you that the process I describe below works like charm between two different MacOSX systems, but I guess it can be easily extended to Windows machines too - giving that the save game architecture is the same for both OSes.</p><p>So, let’s begin with the prerequisites:</p><ul><li>You will need a <a href=\"http://minecraft.net/\">Minecraft beta account</a> (but I assume you already have one if you are still reading this)</li><li>A free <a href=\"https://www.dropbox.com/\">Dropbox basic account</a> (2Gb of space are more than enough for your Minecraft adventures) and</li><li>A basic understanding of Unix command line</li></ul><p><strong>If you are installing Minecraft for the first time</strong></p><ol><li>Install <a href=\"http://minecraft.net/\">Minecraft</a> (drag &amp; drop it in the /Applications/ folder)</li><li>Install &amp; Configure <a href=\"https://www.dropbox.com/\">Dropbox</a> (Create user, specify the Dropbox directory)</li><li>Create a Minecraft folder inside Dropbox folder</li><li>Move your “saves” directory which can be found in /Users/[your MacOSX username]/Library/Application Support/minecraft/saves to /[path to your Dropbox directory]/Minecraft/saves</li><li>If you have done the above step correctly, the “saves” directory will no longer appear in your original “minecraft” folder (just to be clear)</li><li>DON’T OPEN MINECRAFT YET - yeap, we have one more important step</li><li><p>Open the terminal (/Applications/Utilities/Terminal.app) and type the following:</p></li><li><p>Don’t forget to replace [path to your Dropbox directory] and [your MacOSX username] with your information (that’s where your basic understanding of Unix command line is needed)</p></li><li>If terminal won’t respond with any error and you have indeed provided the right paths, you will now see a “saves” directory in your /Users/[your MacOSX username]/Library/Application\/ Support/minecraft/  folder but with a different icon! This actually means that the saves directory will point to your Dropbox folder, which is synced with Dropbox cloud service</li><li>Open Minecraft and enjoy! You won’t have the fear of losing anything in case your Mac fails, plus you can sync those saves with any other Mac (or Windows) machine, following the same proccess!</li><li>There is no step 11, just go explore the caves in your Minecraft world!</li></ol><p><strong>If you already have Minecraft installed and you want to move your save games to the cloud</strong></p><ol><li>Install &amp; Configure <a href=\"https://www.dropbox.com/\">Dropbox</a> (Create user, specify the Dropbox directory)</li><li>Create a Minecraft folder inside Dropbox folder</li><li>Move your “saves” directory which can be found in /Users/[your MacOSX username]/Library/Application Support/minecraft/saves to /[path to your Dropbox directory]/Minecraft/saves</li><li>If you have done the above step correctly, the “saves” directory will no longer appear in your original “minecraft” folder (just to be clear)</li><li>DON’T OPEN MINECRAFT YET - yeap, we have one more important step</li><li><p>Open the terminal (/Applications/Utilities/Terminal.app) and type the following:</p></li><li><p>Don’t forget to replace [path to your Dropbox directory] and [your MacOSX username] with your information (that’s where your basic understanding of Unix command line is needed)</p></li><li>If terminal won’t respond with any error and you have indeed provided the right paths, you will now see a “saves” directory in your /Users/[your MacOSX username]/Library/Application\/ Support/minecraft/  folder but with a different icon! This actually means that the saves directory will point to your Dropbox folder, which is synced with Dropbox cloud service</li><li>Open Minecraft and enjoy! Your worlds will be intact, you won’t lose anything in case your Mac fails, plus you can sync those saves with any other Mac (or Windows) machine, following the same proccess!</li><li>There is no step 10, just go explore the caves in your Minecraft world!</li></ol><p>Thanks makrp for the idea ;)</p>",
            "summary": "Update: I have found (after posting this guide) that there is already a pretty well written tutor...Continue reading \u2192",
            "date_published": "2011-09-17T00:00:00Z",
            "date_modified": "2011-09-17T00:00:00Z"
        }
        
    ]
}
