1.3 Product - JeanyDeVries/ilojo-bar Wiki

You can look at our final product using this link:

Ilojo Bar live demo

home

For our product we chose a Brazilian style. The bar is namely build by slaves who came back from Brazil. They brought this building style to Nigeria.

We chose to use the windows of the building as a metaphor for the 9 stories. Behind each window is a story of Ilojo bar.

Chosen User Stories

We worked on the following user stories:

1. Reading, listening and look at stories about Ilojo bar
We made a visualisation of the with the windows of the building for the 9 stories. You can read these stories and look at the photo's that were added for each of them.

2. Experience Ilojo bar
On the homepage we made a visual impression on how the building used to look like in the early days. We added the scenery around the building in Lagos. We also added a 3D model so people can have a 360 look of the building.

Homepage

We designed the homepage in a way that it gives a feeling of coming into the building. By combing a picture of earlier with the 3D building of how the building would have looked like, it comes alive and you get a good impression of Ilojo bar and it's surrounding.

We gave the user control to let the building slowly come to live by letting the scrolling animate the page. If you scroll, it zooms into the Ilojo bar.

We did this using a combination of the library from GSAP and a Javascript animation library. By giving the surrounding and the building multiple layers of photo's overlapping each other, you have the ability to animate it nicely with opacity and scale.

HTML

<div class="scroll_cont">
    <picture>
        <source srcset="/images/ilojo_bar_bw_2.webp" type="image/webp">
        <source srcset="/images/ilojo_bar_bw_2.png" type="image/png">
        <img src="/images/ilojo_bar_bw_2.png" alt="Colored Ilojo bar" width="1772px" height="1772px">
    </picture>
    <picture>
        <source srcset="/images/ilojo_bar_bw_1.webp" type="image/webp">
        <source srcset="/images/ilojo_bar_bw_1.jpg" type="image/jpeg">
        <img id="background" src="/images/ilojo_bar_bw_1.jpg" alt="Ilojo bar old with environment" width="1772px" height="1772px">
    </picture>
    <picture>
        <source srcset="/images/ilojo_bar_bw_2.webp" type="image/webp">
        <source srcset="/images/ilojo_bar_bw_2.png" type="image/png">
        <img id="color_building" src="/images/ilojo_bar_bw_2.png" alt="Colored Ilojo bar" width="1772px" height="1772px">
    </picture>
    <picture>
        <source srcset="/images/ilojo_bar_bw_3.webp" type="image/webp">
        <source srcset="/images/ilojo_bar_bw_3.png" type="image/png">
        <img id="overlay" src="/images/ilojo_bar_bw_3.png" alt="Ilojo bar old with environment" width="1772px" height="1772px">
    </picture>
</div>

GSAP JavaScript
With GSAP you animate the HTML elemnents and CSS properties by looking at the amount of 'scroll'. This is possible with the GSAP plugin scrollTrigger.

let scrollTL = gsap.timeline({
    scrollTrigger: {
      trigger: '.img_cont',
      start: 'top top',
      end: 'bottom top',
      scrub: 1,
      pin: true,
      snap: 1
    }
});

scrollTL
 .fromTo('.scroll_btn', {opacity: 1, duration: .3}, {opacity: 0} )
 .from('.scroll_cont', {scale: 1},'<')
 .fromTo('#color_building', {opacity: 0}, {opacity: 1},'<')
 .fromTo('#background', {opacity: 1}, {opacity: 0},'-=50%')
 .fromTo('#overlay', {opacity: 1, scale: 1}, {opacity: 0, scale: 1.05}, '<25%')
 .fromTo('.discover_cont',{opacity: 0, zIndex: 0, scale: 0.9, rotation:-3}, {opacity: 1, zIndex: 2, scale: 1, rotation:0}, '<')
 .from('.see_model_cont',{ translateX: "100%", opacity: "0"}, '<')
 .addLabel('end')

3D model with Three.js

Legacy wanted a visualisation of the building of how it used to be. A great way to show the building was in a 3D visualisation on the web.

175387780-ba8ecb1f-75a1-4daa-875f-20d0b48701a3

We used Three.js to load in our 3D model on the web. To use Three.js we first imported it to the project with npm install, plus we added modules to our static folder (Three.js works client side).

After that it was coding time. The first thing is to create a scene. This is where we display our elements. Here we set a camera perspective camera. This projection mode is designed to mimic the way the human eye sees. It is the most common projection mode used for rendering a 3D scene. After that we added some lighting to make the scene more realistic.

But we now have an empty scene with some lighting. The next step is loading the model. We used a fbs loader, because fbx was a known export in maya and blender. We made the materials double sided, because some materials weren't loading correctly. We added it to the scene and added the canvas to a container in HTML.

fbxLoader.load(
        'models/building.fbx',
        (object) => {
            object.traverse(function (child) {
                if ( child.isMesh ) {
                    if(child.material)
                        child.material.side = THREE.DoubleSide;
                }
            })
            object.scale.set(.02, .02, .02)
            buildingObj = object;
            controls.enablePan = false;
            scene.add(object)
            progressBar.style.display = "none";

After that we wanted some animation. We simply rotated the y rotation of the object in the animate function. We only wanted to users to be able to grab the model and turn it horizontally. So we added orbitcontrols and disabled vertical movement and zooming.

    //disable vertical movement plus zooming
    controls.minPolarAngle = Math.PI/2;
    controls.maxPolarAngle = Math.PI/2;
    controls.enableZoom = false;

Discover stories

We coupled every story to a window in the discover stories page. You can look at the inside of the window, which shows a moment from Ilojo bar. By placing photo's behind the window and letting them open when their are in view (or on hover on desktop), will get people curious about the stories. You get people be triggered to explore the stories.

The different stories are being pulled from a Headless CMS. With server side templating they will be converted to html.

<ol class="storiesList">
    <% discoverList.forEach(story => { %>
        <li class="in-view"> 
            <div class="window_wrap">
                <div class="window_img_cont">
                    <div class="window_img_inner">
                        <img src="<%- story.collage_image.url %>" alt="<%- story.collage_image.alt %>" loading="lazy" width="350px" height="300px">
                    </div>
                </div>
                <img class="window door window_L" srcset="/images/window_L.webp" src="/images/window_L.png" alt="window left side Ilojo bar" width="354px" height="525px" draggable="false" />
                <img class="window door window_R" srcset="/images/window_R.webp" src="/images/window_R.png" alt="window right side Ilojo bar" width="354px" height="525px" draggable="false"/>
                <img class="window window_bg" srcset="/images/window_bg.webp" src="/images/window_bg.png" alt="window Ilojo bar" width="354px" height="525px" draggable="false" />
            </div>
            <h3><%- story.title %></h3>
            <a href="<%- story.url %>" aria-label="<%- story.title %>" class="btn_a">Discover</a>
        </li>
   <% }); %>
</ol>

The windows are made using different layers of photo's on top of each other. There are all made responsive using CSS.

.discover li {
    scroll-snap-align: center;
    height: 100%;
    display: flex;
    color: var(--light);
    flex-direction: column;
    align-items: center;
    position: relative;
    padding-top: .5em;
}

.discover li .window_wrap {
    height: 60vh;
    position: relative;
    display: flex;
    justify-content: center;
}

.discover li img {
    object-fit: contain;
    height: 90%;
    width: auto;
}

.window {
    position: absolute;
}

.door {
    z-index: 2;
    transition: transform 1s ease, filter 1s ease;
}

.window_L {
    transform-origin: 16.28%;
}

.window_R {
    transform-origin: 86.04%;
}

.window_bg {
    -webkit-filter: drop-shadow(0px 0px 10px rgba(0,0,0,.4));
    filter: drop-shadow(0px 0px 10px rgba(0,0,0,.4));
}

.window_img_cont {
    height: 90%;
    display: flex;
    align-items: flex-end;
    justify-content: center;

    mask-image: url(/images/windows.webp);
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: bottom;
    -webkit-mask-image: url(/images/windows.webp);
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: bottom;
}

.window_img_inner {
    height: 56%;
    width: 39vh;
    -webkit-filter: drop-shadow(0px 0px 10px rgba(0,0,0,.4));
    filter: drop-shadow(0px 0px 10px rgba(0,0,0,.4));
}

.discover .window_img_inner > img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

The animating of the windows is also done by using CSS:

175388247-03fcdf3d-c6b9-4377-885e-6df3cb5475fa

li:hover .window_L {
    transform: perspective(1200px) translateZ(0px) translateX(0px) translateY(0px) rotateY(-110deg);
    -webkit-filter: drop-shadow(10px 6px 5px rgba(0,0,0,.2));
    filter: drop-shadow(10px 6px 5px rgba(0,0,0,.2));
}

li:hover .window_R {
    transform: perspective(1200px) translateZ(0px) translateX(0px) translateY(0px) rotateY(110deg);
    -webkit-filter: drop-shadow(-10px 6px 5px rgba(0,0,0,.2));
    filter: drop-shadow(-10px 6px 5px rgba(0,0,0,.2));
}

@media only screen and (max-width: 1220px) {
    .in-view .window_L {
        transform: perspective(1200px) translateZ(0px) translateX(0px) translateY(0px) rotateY(-110deg);
        -webkit-filter: drop-shadow(10px 6px 5px rgba(0,0,0,.2));
        filter: drop-shadow(10px 6px 5px rgba(0,0,0,.2));
    }

    .in-view .window_R {
        transform: perspective(1200px) translateZ(0px) translateX(0px) translateY(0px) rotateY(110deg);
        -webkit-filter: drop-shadow(-10px 6px 5px rgba(0,0,0,.2));
        filter: drop-shadow(-10px 6px 5px rgba(0,0,0,.2));
    }
}

Story pages

The story pages are dynamically being loaded in with the server with the API. This gets the title, body text and the photo's of each story. The photo's give an visualisation of the story.

The API is made in a the Headless CMS Primic. The stories are written in Prismic, as well as the photo's being added.

Every story is based on an id that is being retrieved from the server.

app.get('/discover/:id', async (req, res) => {
    let stories = await client.getAllByType('story', {
        orderings: {
          field: 'my.story.id',
          direction: 'asc'
        }
      })

    let uid = req.params.id
    let story = await client.getByUID('story', uid)
    let storyData = story.data
    let alineas = storyData.body[0].items
    let id = storyData.id - 1;

    let previous = getPreviousStory(id, stories);
    let next = getNextStory(id, stories);

    res.render('story', { 
        storyData,
        alineas,
        previous,
        next
    })
})

The story pages are made responsive by using CSS:

@media only screen and (max-width: 756px) {    
    .story section, .story section:nth-child(2n) { 
        flex-direction: column-reverse;
    }
}

Performance

The site is for the people in Lagos where sadly the internet connection is poorly and many people do not have the newest phones. To still make our site working, we made some changes to optimise our performance.

image

Images

The first thing we did was optimise our images. Some images took a lot of loading time, where one was even 2mb. This was drastic for our loading time. So we made all our images webp, which create smaller, richer images that make the web faster. Sadly webp is not always supported so we added a fallback.

<picture>
   <source srcset="/images/close.webp" type="image/webp" >
   <source srcset="/images/close.png" type="image/png">
   <img src="/images/close.png" width="477px" height="478px" alt="Close button">
</picture>

Compression

We use the compression middleware to improve our performance as well. The middleware will attempt to compress response bodies for all request that traverse through the middleware, based on the given options.

// Compress alle responses
app.use(compression())

Caching

Caching is a great way to improve the performance if you visit the site repeatedly. If you visit the site it caches the core, which is our JS, images, fonts and css, plus it caches the HTML pages you have visited. For the caching strategy we chose 'stale while revalidate'. This strategy means that if a request can be loaded from the caching it will be done this way. Otherwise it will run a fetch in the background to save it in the cache, this way your site wil be up to date with a delay of 1 refresh. This way it will load almost instant if you have visited the page before. Another great thing about caching is that if you are offline, but you have visited a page beforehand when you were online, it will still load. This is because it is cached in your local storage.

self.addEventListener('fetch', function(event) {

    if (isCoreGetRequest(event.request)) {
        //Pakt meteen de cache versie
        event.respondWith(
          caches.open(CORE)
            .then(cache => cache.match(event.request.url))
        )
    } else if (isHtmlGetRequest(event.request)) {
        //Pakt cache als die er is, ondertussen nieuwe versie in cache opslaan
        event.respondWith(
        caches.match(event.request.url)
            .then(response => {
                if (response) {
                    event.waitUntil(
                        fetchAndCache(event.request.url, 'html-cache')
                    )
                    return response
                } else {
                    return fetchAndCache(event.request.url, 'html-cache')
                }
            })
            .catch(e => {
            return caches.open(CORE)
                .then(cache => cache.match('/offline'))
            })
        )
    } else if (isOtherGetRequest(event.request)) {
        event.respondWith(
            fetch(event.request)
                .then(response => {
                    return response;
                })
                .catch(e => {
                    return caches.open(CORE)
                        .then(cache => cache.match('/offline'))
                })
        )

    }
});

We also cache everything besides the HTML pages for a year. This is great for our images or other elements on the site, that do not change often. It does not have to load everytime we load the page again. Plus why not cache it if it doesn't change that often.

app.use(function(req, res, next) {
    if (req.method == "GET" && !(req.rawHeaders.toString().includes("text/html"))) {
        res.set("Cache-control", "public, max-age=31536000")
    }
    next()
})
⚠️ **GitHub.com Fallback** ⚠️