<?xml version="1.0" encoding="utf-8"?>
<rss xmlns="http://www.w3.org/2005/Atom" xml:base="https://bralri.net/" version="2.0">
    <channel>
        <title>bralri blog</title>
        <link>https://bralri.net/</link>
        <description>bralri.net/blog/</description>
        <language>en-gb</language>
        <image>
          <title>bralri blog</title>
          <link>https://bralri.net/</link>
          <url>http://www.bralri.net/assets/bralri.webp</url>
          <width>406</width>
          <height>406</height>
        </image>
        <lastBuildDate>Sat, 13 May 2023 00:00:00 +0000</lastBuildDate>
        <link href="https://bralri.net/blog.xml" rel="self" type="application/rss+xml"/>
        <author>
          <name>Bryan Ridpath</name>
          <email>bryanridpath@gmail.com</email>
        </author>
            <item>
              <title>How I made Build-A-Vessel - Part 1</title>
              <link>https://bralri.net/blog/how-i-made-build-a-vessel/</link>
              <pubDate>Sat, 13 May 2023 00:00:00 +0000</pubDate>
              <guid>https://bralri.net/blog/how-i-made-build-a-vessel/</guid>
              <description><![CDATA[&lt;section id=&quot;what-is-build-a-vessel&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#what-is-build-a-vessel&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; What is Build-A-Vessel?&lt;/h4&gt;
&lt;br /&gt;
&lt;p&gt;&lt;a href=&quot;https://bralri.net/works/build-a-vessel/&quot;&gt;Build-A-Vessel&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; is a creative application I started to create in 2022 and have recently finished. It is an online application that enables users to create their own &lt;i&gt;Vessel&lt;/i&gt; artworks from a series of randomly picked 3D models which have been broken apart into individual parts and pieces. The application gamifies the art experience, reminiscnet of classic flash games I used to play after school as a kid where you would be given a selection of items or clothes that you could drag around the screen and drop to create an outfit or a object. Build-A-Vessel allows users to piece together these broken fragments, re-forming the objects into something new and unique.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;The models used in Build-A-Vessel originally came from a project where I was experimenting with paper collage and 3D modelling, I was imagining these future objects which could either be human sized or hand-held. The functionality and utility of these future objects was left intentioanlly ambiguous so the audience could impart their own function onto these fabricated objects. You can see the collages &lt;a href=&quot;https://bralri.net/works/works-on-paper/metamorphosis-collage-2/&quot;&gt;here&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; and &lt;a href=&quot;https://bralri.net/works/works-on-paper/metamorphosis-collage-1/&quot;&gt;here&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Similar to the way objects and artifacts found through archaeological digs are considered artifacts of the past, these vessels are artifacts of the future. However, when they are broken up into spliced objects, their purpose and design becomes further obscured, and they become mere fragments of something that once existed or will exist. By arranging these pieces into a grid, similar to how archaeological finds are categorized and identified, Build-A-Vessel puts you in charge of the task of reassembling them!&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;The inspiration for Build-A-Vessel came to me while working on a project that I ultimately lost interest in. As someone who is easily distracted by shiny new ideas, I found myself exploring ideas for incorporating &lt;a href=&quot;https://threejs.org/&quot;&gt;Three.js&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; into my project. Three.js is a JavaScript 3D library that expands on the WebGL API to provide a wider range of options for creating 3D experiences. While browsing the online documentation and examples, I discovered the &lt;a href=&quot;https://threejs.org/examples/?q=controls#misc_controls_drag&quot;&gt;DragControls.js&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; plugin, which allows for the manipulation of objects and models within a Three.js &quot;Scene&quot;. I realized that I could use this plugin to showcase my own models in an interesting way, but soon discovered that my models were too large and unwieldy. To solve this problem, I decided to cut them into smaller sections, and so Build-A-Vessel was born!&lt;/p&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;section id=&quot;initializing-the-project&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#initializing-the-project&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; Initializing the project&lt;/h4&gt;
&lt;br /&gt;
&lt;figure class=&quot;main-article__figure&quot;&gt;
    &lt;img src=&quot;https://bralri.net/assets/img/blog/how-i-made-build-a-vessel/loading-in-models.png&quot; alt=&quot;&quot; title=&quot;Initial loading in of models&quot; loading=&quot;lazy&quot; /&gt;
        &lt;figcaption&gt;
            Initial loading in of all 100 models
        &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;p&gt;To start I began by copying most the drag controls example from the Three.js documentation. However, instead of using cubes, I imported all of the vessel asset paths from an array called vesselAssets in _config.min.js. From there, I set up a basic scene that included the DragControls plugin. Using a for loop, I loaded the .glb models into the scene by looping through all of the models from the vesselAssets array. Once they had been loaded into the scene, the loaded models then get pushed to the main modelArray. This array was then used by the DragControls plugin to set which objects are to be dragged by these controls.&lt;/p&gt;
&lt;br /&gt;
&lt;pre&gt;
&lt;code&gt;
/* _variables.min.js */


export const vesselPaths = {

    vessel0: &quot;/assets/models/vessels/0.glb&quot;,
    vessel1: &quot;/assets/models/vessels/1.glb&quot;,
    vessel2: &quot;/assets/models/vessels/2.glb&quot;,
    vessel3: &quot;/assets/models/vessels/3.glb&quot;,
    vessel4: &quot;/assets/models/vessels/4.glb&quot;,
    vessel5: &quot;/assets/models/vessels/5.glb&quot;,
    /* etc ... */
    
};

&lt;/code&gt;
&lt;/pre&gt;
&lt;pre&gt;
&lt;code&gt;
/* _config_.min.js */


import {vesselPaths as v} from &quot;../js/_variables.min.js&quot;;


export const vesselAssets = [
    [
        {
            src: v.vessel0
        },{
            src: v.vessel1
        },{
            src: v.vessel2
        },{
            src: v.vessel3
        },{
            src: v.vessel4
        },{
            src: v.vessel5
        },
        /* etc ... */
    ]
];

&lt;/code&gt;
&lt;/pre&gt;
&lt;pre&gt;
&lt;code&gt;
/* vessels.min.js */


import * as THREE from &#39;three&#39;;
import {GLTFLoader} from &#39;three/GLTFLoader.js&#39;;
import {DragControls} from &#39;three/DragControls.js&#39;;

import {vesselAssets} from &#39;../js/_config.min.js&#39;;

let scene, camera, renderer, dragControls;
const manager = new THREE.LoadingManager();
let object;
const modelArray = [];
let sceneReady = false;

const urlParams = new URLSearchParams(window.location.search);
let groupNumb = parseInt(urlParams.get(&#39;id&#39;)) &gt; vesselAssets.length ? &#39;0&#39; : 
                parseInt(urlParams.get(&#39;id&#39;)) &lt;= vesselAssets.length ? parseInt(urlParams.get(&#39;id&#39;)) : 
                0;
let currentGroup = vesselAssets[groupNumb];

const loading = document.getElementById(&#39;loading&#39;);

function sceneSetup() {
    /* scene */
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000);
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);

    /* renderer */
    renderer = new THREE.WebGLRenderer({
        alpha: true,
        antialias: true 
    });
    renderer.compile(scene, camera);
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    /* drag controls */
    dragControls = new DragControls(modelArray, camera, renderer.domElement);

    /* lights */
    const ambientLight = new THREE.AmbientLight(0xffffff);
    scene.add(ambientLight);

    window.addEventListener(&#39;resize&#39;, onWindowResize);

    sceneReady = true;
}

function loadAssets() {
    /* load models */
    const loader = new GLTFLoader(manager);
    for (let i = 0; i &lt; currentGroup.length; i++) {

        const obj = currentGroup[i];

        loader.load(obj.src, function (glb) {
            object = glb.scene;

            /* set random rotation */
            object.rotation.y = Math.random() * 4 * Math.PI;

            scene.add(object)
            modelArray.push(object);
        })
    }
}

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);

    render();
}

function animate() {
    requestAnimationFrame(animate);
    render();
}

function render() {
    renderer.render(scene, camera);
}

window.onload = function() {
    sceneSetup();
    loadAssets();
    setTimeout(animate, 1000);
    setTimeout(function() {
        loading.classList.add(&#39;fade&#39;);
    }, 1000);
}

&lt;/code&gt;
&lt;/pre&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;section id=&quot;adding-camera-mobility&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#adding-camera-mobility&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; Adding camera mobility&lt;/h4&gt;
&lt;br /&gt;
&lt;figure class=&quot;main-article__figure&quot;&gt;
    &lt;img src=&quot;https://bralri.net/assets/img/blog/how-i-made-build-a-vessel/orbit-controls.gif&quot; alt=&quot;&quot; title=&quot;Orbit Controls enabling you to pan, spin and zooming in and out of the camera&quot; loading=&quot;lazy&quot; /&gt;
        &lt;figcaption&gt;
            Orbit Controls enabling you to pan, spin and zooming in and out of the camera
        &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;p&gt;After successfully adding all of the models into the scene and enabling drag controls to move them around, I realized that I wanted to have much more camera mobility in the scene. To achieve this, I combined the DragControls with another kind of controls called &lt;a href=&quot;https://threejs.org/examples/?q=orbit#misc_controls_orbit&quot;&gt;OrbitControls.js&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt;. With OrbitControls, you are able pan, spin, and zoom the camera around the scene. This allows users to move the camera around more freely which in my opinion makes the application much easier to use and so users can view their vessel creations from a variety of different angles while building them.&lt;/p&gt;
&lt;br /&gt;
&lt;pre&gt;
&lt;code&gt;
/* vessels.min.js */


/* added OrbitControls import */
import {OrbitControls} from &#39;three/OrbitControls.js&#39;;

let orbitControls;

function sceneSetup() {

    /* controls */
    /* orbit */
    orbitControls = new OrbitControls(camera, renderer.domElement);
    orbitControls.update();
    orbitControls.addEventListener(&#39;change&#39;, render)
    orbitControls.maxDistance = 15;
    orbitControls.enablePan = true;
    orbitControls.panSpeed = 0.5;

    /* drag */
    dragControls = new DragControls(modelArray, camera, renderer.domElement);

    /* added eventListener handlers which enabled the two controls to co-exist, */
    /* when you use the drag controls, orbit controls are disabled, then when */
    /* you stop using drag controls, orbit controls get re-enabled */

    dragControls.addEventListener(&#39;dragstart&#39;, function() {
        orbitControls.enabled = false;
    });
    dragControls.addEventListener(&#39;dragend&#39;, function() {
        orbitControls.enabled = true;
    });

}

&lt;/code&gt;
&lt;/pre&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;section id=&quot;picking-and-shuffling-the-models&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#picking-and-shuffling-the-models&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; Picking and shuffling the models&lt;/h4&gt;
&lt;br /&gt;
&lt;figure class=&quot;main-article__figure&quot;&gt;
    &lt;img src=&quot;https://bralri.net/assets/img/blog/how-i-made-build-a-vessel/reduced-amount.png&quot; alt=&quot;&quot; title=&quot;Reduced amount of models&quot; loading=&quot;lazy&quot; /&gt;
        &lt;figcaption&gt;
            Loading in a reduced amount of models (15) into the scene
        &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;p&gt;Since there are almost 100 different potential spliced models which can be loaded into the scene, I decided that I wanted to load in a much smaller selection. This makes the application quicker to load and much more manageble. Here&#39;s what I did: I took the currentGroup model array, which contained all of the models, and shuffled the array using the commonly used &lt;a href=&quot;https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle&quot;&gt;Fisher-Yates shuffle algorithm&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt;, which is a technique technique used to randomly shuffle the elements of an array. It iterates over the array in reverse order, and for each element, it selects a random index from the remaining unshuffled portion of the array and swaps the current element with the randomly selected element. I then selected the first 15 models from the shuffled array and shuffled those 15 models again so that their order was fully randomized. Then the chosen 15 models get loaded into the scene and pushed to the modelArray.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;After implementing all the shuffling and randomization in the application, I was curious about the total number of possible permutations that could be created. Taking into account the random rotation on the y-axis, the approximate number of Build-A-Vessel permutations is 6.08173 x 10^42, which is an extremely large number: 608,173,145,511,212,917,289,174,621,788,928. However, if we do not include the random y-axis rotation, the number of possible permutations is closer to 2,854,889,312.&lt;/p&gt;
&lt;br /&gt;
&lt;pre&gt;
&lt;code&gt;
/* vessels.min.js */


let numObjects;

function loadAssets() {
    /* shuffle the array containing all of the models */
    for (let i = currentGroup.length - 1; i &gt; 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [currentGroup[i], currentGroup[j]] = [currentGroup[j], currentGroup[i]];
    }

    /* pick the first 15 models from that array */
    currentGroup = currentGroup.slice(0, 15);
    numObjects = currentGroup.length;


    /* shuffle the picked 15 models so their order and position is fully randomized */
    for (let i = numObjects - 1; i &gt; 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [currentGroup[i], currentGroup[j]] = [currentGroup[j], currentGroup[i]];
    }

    /* load models */
    const loader = new GLTFLoader(manager);
    for (let i = 0; i &lt; numObjects; i++) {

        const obj = currentGroup[i];

        loader.load(obj.src, function (glb) {
            object = glb.scene;

            /* Set random rotation */
            object.rotation.y = Math.random() * 4 * Math.PI;

            scene.add(object)
            modelArray.push(object);
        })
    }
}

&lt;/code&gt;
&lt;/pre&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;section id=&quot;grid-layouts-and-responsive-design&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#grid-layouts-and-responsive-design&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; Grid layouts and responsive design&lt;/h4&gt;
&lt;br /&gt;
&lt;figure class=&quot;main-article__figure&quot;&gt;
    &lt;img src=&quot;https://bralri.net/assets/img/blog/how-i-made-build-a-vessel/landscape-grid.png&quot; alt=&quot;&quot; title=&quot;Vessel parts loaded in a grid landscape pattern&quot; loading=&quot;lazy&quot; /&gt;
        &lt;figcaption&gt;
            Vessel parts loaded in a landscape grid pattern
        &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;figure class=&quot;main-article__figure-vert&quot;&gt;
    &lt;img src=&quot;https://bralri.net/assets/img/blog/how-i-made-build-a-vessel/vertical-grid.png&quot; alt=&quot;&quot; title=&quot;Vessel parts loaded in a grid vertical pattern&quot; loading=&quot;lazy&quot; /&gt;
        &lt;figcaption&gt;
            Vessel parts loaded in a vertical grid pattern
        &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;p&gt;To ensure a better user experience on both desktop and mobile devices, I modified the application to display the 15 random models in an even grid that can adjust to screen size and orientation. Upon loading the models, I aligned their x and z axis positions to a grid. On desktop and landscape monitors, I set the grid rows to 5, and the columns are set to 3. However, if the window.innerWidth is less than or equal to 800 pixels wide, the application automatically adjusted the grid to 3 rows and 5 columns, transforming the grid from landscape to portrait mode. Finally, I adjusted the camera&#39;s position to fit the new portrait grid. This modification allowed for a seamless experience across different devices and screen sizes.&lt;/p&gt;
&lt;br /&gt;
&lt;pre&gt;
&lt;code&gt;
/* vessels.min.js */


let numRows, numCols;

function sceneSetup() {

    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
    /* adjust camera position on mobile */
    if (window.innerWidth &lt;= 800) {
        camera.position.z = 12;
    } else {
        camera.position.z = 10;
    }
}

function loadAssets() {

    /* define spacing between models */
    let spacing = 3;

    for (let i = currentGroup.length - 1; i &gt; 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [currentGroup[i], currentGroup[j]] = [currentGroup[j], currentGroup[i]];
    }

    currentGroup = currentGroup.slice(0, 15);
    numObjects = currentGroup.length;

    /* adjust grid parameters for mobile screens */
    if (window.innerWidth &lt; 800) {
        numRows = 5;
        numCols = 3;
    } else {
        numRows = 3;
        numCols = 5;
    }

    /* shuffle array */
    for (let i = numObjects - 1; i &gt; 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [currentGroup[i], currentGroup[j]] = [currentGroup[j], currentGroup[i]];
    }

    /* load models */
    const loader = new GLTFLoader(manager);
    for (let i = 0; i &lt; numObjects; i++) {

        const obj = currentGroup[i];

        loader.load(obj.src, function (glb) {
            object = glb.scene;

            /* set grid pattern */
            const row = Math.floor(i / numCols);
            const col = i % numCols;

            /* set position based on grid index */
            const x = (col - (numCols - 1) / 2) * spacing;
            const y = (row - (numRows - 1) / 2) * spacing;

            /* adjust the y position if on mobile */
            if (window.innerWidth &lt;= 800) {
                object.position.set(x, y + 1, 0);
            } else {
                object.position.set(x, y, 0);
            }

            /* set random rotation */
            object.rotation.y = Math.random() * 4 * Math.PI;

            scene.add(object)
            modelArray.push(object);
        })
    }
}

&lt;/code&gt;
&lt;/pre&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;section id=&quot;adding-download-functionality&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#adding-download-functionality&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; Adding download functionality&lt;/h4&gt;
&lt;br /&gt;
&lt;figure class=&quot;main-article__figure&quot;&gt;
    &lt;img src=&quot;https://bralri.net/assets/img/blog/how-i-made-build-a-vessel/downloaded-vessels.png&quot; alt=&quot;&quot; title=&quot;Downloaded Vessels open in opened in Windows 3D Viewer&quot; loading=&quot;lazy&quot; /&gt;
        &lt;figcaption&gt;
            Downloaded Vessels (vessel-37841.glb) opened in Windows 3D Viewer
        &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;p&gt;After loading the models into the scene and making them visible on both desktop and mobile, the next step was to provide users with a way to save their creations. To achieve this, the &lt;a href=&quot;https://threejs.org/examples/?q=exporter#misc_exporter_gltf&quot;&gt;GLTFExporter.js&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; plugin, which is a native plugin of Three.js, was used. This plugin enables users to download any objects or models created in the scene. By adding this feature, users are able to save their Vessels at any stage of their creation.&lt;/p&gt;
&lt;br /&gt;
&lt;pre&gt;
&lt;code&gt;
/* build-a-vessel.njk */


&amp;lt;div id=&amp;quot;buttons&amp;quot;&amp;gt;
    &amp;lt;p&amp;gt;
        /* add button to the HTML which will be accessed in the javascript */
        &amp;lt;button id=&amp;quot;download-glb&amp;quot; title=&amp;quot;Save Vessel&amp;quot;&amp;gt;&amp;lt;i class=&amp;quot;fa-solid fa-cloud-arrow-down&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/button&amp;gt;
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&lt;/code&gt;
&lt;/pre&gt;
&lt;pre&gt;
&lt;code&gt;
/* vessels.min.js */


/* import GLTFExporter */
import {GLTFExporter} from &#39;three/GLTFExporter.js&#39;;

/* get the HTML download button */
const btn = document.getElementById(&#39;download-glb&#39;);

function sceneSetup() {
    
    /* buttons */
    /* add event listeer to the download vessel button */
    btn.addEventListener(&#39;click&#39;, downloadVessel);
}

/* gltf exporter */
function downloadVessel() {
    const exporter = new GLTFExporter();
    const options = {
        onlyVisible: true,
        binary: true
    };
    exporter.parse(
        scene,
        function(result) {
            saveArrayBuffer(result, `vessel.glb`)
        },
        function (error) {
            console.log(&#39;An error happened during parsing&#39;, error);
        },
        options
    )
}

const link = document.createElement(&#39;a&#39;);
link.style.display = &#39;none&#39;;
document.body.appendChild(link);

function save(blob, fileName) {
    link.href = URL.createObjectURL(blob);
    link.download = fileName;
    link.click();
}

function saveArrayBuffer(buffer, fileName) {
    save(new Blob([buffer], {type: &#39;application/octet-stream&#39;}), fileName);
}

&lt;/code&gt;
&lt;/pre&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;section id=&quot;finishing-touches&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#finishing-touches&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; Finishing touches&lt;/h4&gt;
&lt;br /&gt;
&lt;figure class=&quot;main-article__figure&quot;&gt;
    &lt;img src=&quot;https://bralri.net/assets/img/blog/how-i-made-build-a-vessel/shuffle-on-load.gif&quot; alt=&quot;&quot; title=&quot;Shuffling models on page load&quot; loading=&quot;lazy&quot; /&gt;
        &lt;figcaption&gt;
            Shuffle button when clicked reloads the application and shuffles which models get displayed.
        &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br /&gt;
&lt;p&gt;To finish the application, I included several quality-of-life improvements. I included a shuffle button that enables users to reload the application with a new set of models, this button makes it convienient to refresh the application and gives you a new set of vessel parts to work with. As well as this, I also added a reset camera button, which allows users to reset the camera position to its original position.&lt;/p&gt;
&lt;br /&gt;
&lt;pre&gt;
&lt;code&gt;
/* build-a-vessel.njk */


&amp;lt;div id=&amp;quot;buttons&amp;quot;&amp;gt;
    &amp;lt;p&amp;gt;
        /* add buttons to the HTML which will be accessed in the javascript */
        &amp;lt;button id=&amp;quot;shuffle&amp;quot; title=&amp;quot;Shuffle&amp;quot;&amp;gt;&amp;lt;i class=&amp;quot;fa-solid fa-shuffle&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/button&amp;gt;
        &amp;lt;button id=&amp;quot;reset&amp;quot; title=&amp;quot;Reset Camera&amp;quot;&amp;gt;&amp;lt;i class=&amp;quot;fa-solid fa-camera-rotate&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/button&amp;gt;
        /**/

        &amp;lt;button id=&amp;quot;download-glb&amp;quot; title=&amp;quot;Save Vessel&amp;quot;&amp;gt;&amp;lt;i class=&amp;quot;fa-solid fa-cloud-arrow-down&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/button&amp;gt;
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&lt;/code&gt;
&lt;/pre&gt;
&lt;pre&gt;
&lt;code&gt;
/* vessels.min.js */


const uuidArray = [];

/* get the reset and shuffle buttons */
const resetCameraButton = document.getElementById(&#39;reset&#39;);
const shuffleButton = document.getElementById(&#39;shuffle&#39;);

function sceneSetup() {
    
    /* buttons */
    /* add event listeners to reset button */
    document.body.addEventListener(&#39;keydown&#39;, function(event) {
        if (event.which == 82) { // R
            resetCamera();
        }
    }, false);
    resetCameraButton.addEventListener(&#39;click&#39;, resetCamera, false);

    /* add functionality to shuffle button */
    shuffleButton.onclick = function() {
        loading.classList.remove(&#39;fade&#39;);
        setTimeout(function () {
            /* on window reload append &#39;?id=${uuidArray[0]}&#39; */
            /* uuidArray[0] will be a unique random string of numbers */
            window.location.href = `?id=${uuidArray[0]}`;
        }, 1200)
    }
}

function generateUUID() {
    /* generate a random number */
    const uuid = Math.floor(Math.random() * 100000);
    /* push the unique number to the array */
    /* this number will be used when downloading the model */
    /* and when appending a unique id to the url */
    uuidArray.push(uuid);
    return uuid;
}

function resetCamera() {
    /* when the reset button is clicked the orbit controls reset to their original position */
    orbitControls.reset();
}

&lt;/code&gt;
&lt;/pre&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;section id=&quot;whats-next&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#whats-next&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; What&#39;s next?&lt;/h4&gt;
&lt;br /&gt;
&lt;p&gt;Add model grouping:&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;I would like to add functionality which is in the original DragControls example which allows you to select multiple objects and add them to a group. This means that when you add models to join into a group, when you drag them they move all move together as one. I think this would improve the usabilty of the application and make it easier to position in-progress or finished vessels.&lt;/p&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;p style=&quot;text-decoration: line-through;&quot;&gt;Add camera screenshots:&lt;/p&gt;
&lt;br /&gt;
&lt;p style=&quot;text-decoration: line-through;&quot;&gt;For users who are on mobile or are not able to view the downloaded .glb vessel models, I would like to add a button which takes a screenshot image of the current camera view and saves it. This is so that all users can have the option to save their Build-A-Vessel creations regardless of which device they are on!&lt;/p&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;p&gt;Add accessibility options:&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;I would like to make Build-A-Vessel and any other applications I might make in the future to be as fully accessible as I can. To do this I would use something like &lt;a href=&quot;https://github.com/pmndrs/react-three-a11y&quot;&gt;@react-three/a11y&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; which is a WebGL accessibility plugin that can add components like: Focus and focus indication, tab indexing and keyboard navigation, screen reader support and alt-text, roles, cursor shapes, and descriptive links.&lt;/p&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;p&gt;Exhibiting:&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;I would like to exhibit Build-A-Vessel in some capacity, I would like to develop a way to put the application onto touch screen monitors powered by something like a Raspberry Pi so that the monitors can be portable or included in some kind of multi-media installation.&lt;/p&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;p&gt;Saving models when installed in an exhibition:&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;I would also like to develop some way for users who do use the Build-A-Vessel application at an exhibition to save their models in a way where they get to keep them, using the online application and saving models is very easy as you have your own device. But at an exhibition the application will be on a private device, so I would like to find a way to either save the models to a server which then get uploaded to an archive that can be accessed and downloaded by the public, or for the finished vessels to be sent via email. Or the models can be saved onto a USB with a custom 3D modelled Vessel casing.&lt;/p&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;p&gt;&lt;i class=&quot;fa-solid fa-star-of-life icon-accent&quot;&gt;&lt;/i&gt; Try out &lt;a href=&quot;https://bralri.net/works/build-a-vessel&quot;&gt;Build-A-Vessel&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;i class=&quot;fa-solid fa-star-of-life icon-accent&quot;&gt;&lt;/i&gt; Got some feedback? &lt;a href=&quot;https://bralri.net/contact&quot;&gt;Contact&lt;/a&gt;&lt;/p&gt;
&lt;br /&gt;
&lt;hr /&gt;
&lt;section id=&quot;updates&quot; class=&quot;blog-section&quot;&gt;
&lt;h4&gt;&lt;a href=&quot;https://bralri.net/blog/how-i-made-build-a-vessel/#updates&quot; class=&quot;anchor-tag&quot;&gt;#&lt;/a&gt; Updates&lt;/h4&gt;
&lt;br /&gt;
&lt;p style=&quot;color: grey;&quot;&gt;&lt;span&gt;Update: 15/05/2023&lt;/span&gt; &lt;sub&gt;&lt;i class=&quot;fa-solid fa-arrow-turn-down&quot;&gt;&lt;/i&gt;&lt;/sub&gt;&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Added camera screenshots:&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Added the ability to capture and save a screenshot from the current camera view. This works both on mobile and pc. To save a jpeg screenshot image just adjust the camera to the exact view you want and then press the &quot;Take Camera Screenshot&quot; button! It will download automatically to your device.&lt;/p&gt;
&lt;br /&gt;
&lt;pre&gt;
&lt;code&gt;
/* build-a-vessel.njk */


&amp;lt;div id=&amp;quot;buttons&amp;quot;&amp;gt;
    &amp;lt;p&amp;gt;
        /* add the button to the HTML so we can access this in the javascript and add functionality */
        &amp;lt;button id=&amp;quot;save-img&amp;quot; title=&amp;quot;Take Camera Screenshot&amp;quot;&amp;gt;&amp;lt;i class=&amp;quot;fa-solid fa-image&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/button&amp;gt;
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;
&lt;/pre&gt;
&lt;pre&gt;
&lt;code&gt;
/* vessels.min.js */


/* get the save-img button from HTML */
const imageScreenshotButton = document.getElementById(&#39;save-img&#39;);

function sceneSetup() {

    renderer = new THREE.WebGLRenderer({
        alpha: true,
        antialias: true,

        /* add preserveDrawingBuffer to your renderer, */
        /* this flag will get the base64 encoding of the current frame */
        preserveDrawingBuffer: true
    });

    /* add event listeners to the button and attach savAsImage to run when button is clicked */
    imageScreenshotButton.addEventListener(&#39;click&#39;, saveAsImage);
}

function saveAsImage() {

    let imgData;

    try {
        /* set the dowload image file to be jpeg */
        const strMime = &quot;image/jpeg&quot;;
        const strDownloadMime = &quot;image/octet-stream&quot;;
        imgData = renderer.domElement.toDataURL(strMime);

        /* run the save function */
        saveFile(imgData.replace(strMime, strDownloadMime), `vessel-${groupID}.jpg`);

    } catch (e) {
        console.log(e);
        return;
    }
}

function saveFile(strData, fileName) {

    const link = document.createElement(&#39;a&#39;);

    /* check if jpeg is supported by the browser */
    if (typeof link.download === &#39;string&#39;) {

        /* if it is, append the &#39;link&#39; and set variables */
        document.body.appendChild(link);

        link.download = fileName;
        link.href = strData;
        link.click();

        /* remove anchor tag from body */
        document.body.removeChild(link);

    } else {

        /* if jpeg is not supported by the browser, */
        /* download file data without jpeg extention */
        /* users can then append the file extention themselves */
        /* and convert to whichever file format they want */

        location.replace(uri);
    };
}

&lt;/code&gt;
&lt;/pre&gt;
&lt;/section&gt;
&lt;br /&gt;
&lt;br /&gt;]]></description>
            </item>
            <item>
              <title>Tethered Publics</title>
              <link>https://bralri.net/blog/tethered-publics/</link>
              <pubDate>Thu, 04 May 2023 00:00:00 +0000</pubDate>
              <guid>https://bralri.net/blog/tethered-publics/</guid>
              <description><![CDATA[&lt;p&gt;&lt;i class=&quot;fa-solid fa-star-of-life icon-accent&quot;&gt;&lt;/i&gt; &lt;a href=&quot;https://tetheredpublics.app/exhibitions/MGekceDcjh2nGvKGhF3ETv&quot;&gt;Tethered Publics&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;i class=&quot;fa-solid fa-star-of-life icon-accent&quot;&gt;&lt;/i&gt; &lt;a href=&quot;https://tetheredpublics.app/download&quot;&gt;Download the app&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; to see the show!&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;I am excited to announce that I will be showcasing a new collection of photogrammetry models titled &quot;Amalgamations&quot; in Tethered Public&#39;s latest augmented reality (AR) exhibition. These models represent a technogenic vision of the future, where waste, refuse, and detritus are repurposed and amalgamated into mesmerizing conglomerates of man-made materials.
&lt;/p&gt;&lt;p&gt;Using the process of photogrammetry, I have captured the intricate details of these &amp;quot;fantasized future fossils&amp;quot; made up of an assortment of everyday waste materials that we consume on a daily basis.&lt;/p&gt;
&lt;p&gt;This exhibition will provide a unique opportunity for audiences to experience these models up close and in full detail through augmented reality technology. By using AR, we can bridge the gap between the real and virtual worlds and offer an immersive experience that is both engaging and informative.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Tethered Publics statement:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Step into the captivating world of augmented reality (AR) art through the innovative *Home Collection 2023* exhibition, curated by Tethered Publics. As emerging technologies transform how we perceive and engage with art, this exhibition strives to re-envision concepts of collecting and displaying items within domestic environments, seamlessly connecting physical and digital manifestations.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Available throughout the UK via the Tethered Publics app, this exhibition unites an eclectic array of digital art pieces, drawing inspiration from everyday objects, including artwork, antiques, collectibles, and other ornamental items. Each piece carries its own personal, shared, or historical significance.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Immerse yourself in a diverse collection of works, from enigmatic compositions of fragmented items to stirring reinterpretations of biblical characters. Uncover creations influenced by Chinese architectural roof charms and investigate pieces that probe the etymological origins of economy. Engage with the notion of collecting loss through a fascinating fusion of casino felt and autumnal foliage, and envisage a future inhabited by autonomous robot dogs and more.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;The featured works encompass both digitally native and digitally imported objects, with some static and others animated. This capacity to enhance physical objects digitally in space surpasses the limitations of conventional photography in representing 3D art. Previously, a sculptural object could only exist online as an image; now, it can be explored as a tangible object, ready for re-contextualisation and real-time sharing. In this exhibition, the works are designed to be experienced within the comfort of your own home.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;As you navigate the exhibition and interact with these digital artefacts, unbound by the physical constraints of ownership, witness the app&#39;s ability to bring these creations to life within your personal living space. The intention here is to challenge and disrupt traditional paradigms of collecting and displaying items. Through the app, you can manipulate and position them in any location you prefer. If a piece looks perfect in your kitchen, snap a photo and share it online.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;By harnessing the power of AR technology, Tethered Publics aims to democratise access to cutting-edge technologies for artists and art enthusiasts alike. This exhibition not only eases the transition of artists&#39; work into the virtual 3D domain but also sparks reflection on the future of art collections in the realm of immersive AR experiences.&lt;/p&gt;
&lt;p&gt;We invite you to delve into and enjoy Home Collection 2023.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
&lt;video width=&quot;50%&quot; height=&quot;100%&quot; controls=&quot;&quot; controlsList=&quot;nodownload&quot; poster=&quot;https://bralri.net/assets/img/blog/tethered-publics/video-thumbnail.png&quot;&gt;
    &lt;source src=&quot;https://bralri.net/assets/video/blog/tethered-publics/tethered-publics.mp4&quot; type=&quot;video/mp4&quot; /&gt;
    Your browser does not support the video tag.
&lt;/video&gt;
&lt;figcaption&gt;
    Preview of Tethered Publics: Home Collection.
&lt;/figcaption&gt;
&lt;br /&gt;
&lt;br /&gt;]]></description>
            </item>
            <item>
              <title>Remnants</title>
              <link>https://bralri.net/blog/remnants/</link>
              <pubDate>Sat, 17 Dec 2022 00:00:00 +0000</pubDate>
              <guid>https://bralri.net/blog/remnants/</guid>
              <description><![CDATA[&lt;p&gt;This September I launched Cloud Shelter, a project which I have been working on daily for a few years at this point. Cloud Shelter has gone through many many transformations, from being an app built in Unity which could be downloaded like a game and visited, to now an online virtual experience using various JavaScript libraries. But finally at long last today (17.12.2022) at 18:00 I will be launching Cloud Shelter&#39;s long awaited first show, Remnants!&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Remnants is a group show featuring 8 amazing artists that I am thrilled to be working with: Alex Pearl, Christoph Jones, Molly Erin McCarthy, Speculative Geologies by Jason Urban and Leslie Mutchler, Leifang by Phoebe Bray and Katharine Platts, and myself. The show will feature everthing from dancing video collages, video essays, interactive singing organisms; ruins, structures and folklore, spinning techno minerals and disintegrating landscapes.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;Cloud Shelter statement:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cloud Shelter presents Remnants, an online group exhibition featuring artists Alex Pearl, Christoph Jones, Bryan Ridpath, Speculative Geologies by Jason Urban and Leslie Mutchler, Molly Erin McCarthy, and Leifang a collaboration between artists Katharine Platts and Phoebe Bray.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
&lt;h4&gt;# Artists and artworks:&lt;/h4&gt;
&lt;p&gt;Alex Pearl, Corpse (Long, Picnic, Heat), 2022. Series of digital collages.&lt;/p&gt;
&lt;p&gt;Bryan Ridpath, Untitled, 2022. Series of experimental photogrammetry models of lanscapes.&lt;/p&gt;
&lt;p&gt;Christoph Jones, The Void, Suicide and The Sorrowing Man, 2022. Video Essay.&lt;/p&gt;
&lt;p&gt;Katharine Platts and Phoebe Bray, Leifang, 2022. Series of interactive models with sound.&lt;/p&gt;
&lt;p&gt;Molly Erin McCarthy, Dock, Door, Grave, Pennywort, Trinity, 2022. Series of WIP models.&lt;/p&gt;
&lt;p&gt;Speculative Geologies by Jason Urban and Leslie Mutchler, 2022. Series of animted 3D models.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;&lt;i class=&quot;fa-solid fa-star-of-life icon-accent&quot;&gt;&lt;/i&gt; Visit &lt;a href=&quot;https://www.cloudshelter.space/&quot;&gt;Cloud Shelter&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;i class=&quot;fa-solid fa-star-of-life icon-accent&quot;&gt;&lt;/i&gt; Follow Cloud Shelter on &lt;a href=&quot;https://www.instagram.com/cloud.shelter&quot;&gt;Instagram&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;i class=&quot;fa-solid fa-star-of-life icon-accent&quot;&gt;&lt;/i&gt; &lt;a href=&quot;https://www.artrabbit.com/events/remnants-2022&quot;&gt;ArtRabbit&lt;/a&gt; &lt;sup&gt;&lt;i class=&quot;fa-solid fa-arrow-up-right-from-square icon-grey&quot;&gt;&lt;/i&gt;&lt;/sup&gt; page with all of the details to see the show online!&lt;/p&gt;
&lt;br /&gt;
&lt;br /&gt;]]></description>
            </item>
    </channel>
</rss>