icon

Shading Effect

GitHub Repository

Shading Effect in High Resolution

Bypass gb.display with gb.tft

Last modification on 4th May 2020 at 00:09 GMT+4

screen

Adding the tiling to the stage

As in the case of the ball, the tiling will be introduced on the game scene by the main controller GameEngine:

GameEngine.h
#ifndef SHADING_EFFECT_GAME_ENGINE
#define SHADING_EFFECT_GAME_ENGINE

// we will define the `Tiling` class just after...
#include "Tiling.h"
#include "Ball.h"

class GameEngine
{
    private:

        // a pointer to the instance of the tiling
        static Tiling* tiling;

        // a pointer to the instance of the ball
        static Ball* ball;

    public:

        // initialization
        static void init();

        // entry point of the main control loop
        static void tick();
};

#endif
GameEngine.cpp
#include "GameEngine.h"
#include "Renderer.h"

// always initialize a pointer to NULL
Tiling* GameEngine::tiling = NULL;
Ball* GameEngine::ball = NULL;

void GameEngine::init() {
    // instantiation of the tiling
    tiling = new Tiling();
    // instantiation of the ball
    ball = new Ball();

    // registration of observers
    // with the rendering engine
    Renderer::subscribe(tiling);
    Renderer::subscribe(ball);
}

void GameEngine::tick() {
    // performs rendering of the game scene
    Renderer::draw();
}

You will notice here that the registration of observers with the rendering engine is done in descending order of the depth of the element on the display stack. This is so that the tiling is drawn first (the deepest in the stack) and then the ball (remember that adding an item to the linked list of observers is done at the end of the list).

Preparation of graphic assets

You will find the tiles.png file in the small ZIP archive you downloaded earlier. You see that this time, it is a small spritesheet with two sprites: a light and a dark tile.

assets

This is how to set up thetranscoding tool:

img2tft settings

Drag and drop the tiles.png file, and you should get this:

// you can declare your sprites like this:

struct Sprite {
    int x, y;
    uint8_t width, height;
    uint8_t frames;
    uint16_t* data;
    uint16_t transparent;
};

// and after spritedata has been set
// you can initialize your sprite like that:
//
// Sprite mySprite = {
//     0, // choose an initial x value
//     0, // choose an initial y value
//     16,
//     16,
//     2,
//     (uint16_t*) &spritedata,
//     0xffff
// };

const uint16_t spritedata[] = {
    0xffff, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0xffff,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0xffff, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0xffff,
    0xffff, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0xffff,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0xffff, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0xffff
};

Definition of the tiling

Let’s start by declaring the Tilingclass:

Tiling.h
#ifndef SHADING_EFFECT_TILING
#define SHADING_EFFECT_TILING

#include "Renderable.h"

// the Tiling class fulfills the contract defined in the `Renderable` interface
class Tiling : public Renderable
{
    private:

        // the descriptive parameters of the sprite
        static const uint8_t TILE_WIDTH;
        static const uint8_t TILE_HEIGHT;
        static const uint16_t TRANSPARENT_COLOR;
        // the pixel map obtained with the transcoding tool
        static const uint16_t BITMAP[];
        
        // we will move the tiling to give an impression of motion
        // to the ball, so we define the coordinates of the displacement vector
        // to take it into account immediately in the calculation of the rendering
        int8_t offsetX;
        int8_t offsetY;

    public:

        // a constructor is declared
        // we will initialize the displacement vector
        // in this constructor
        Tiling();

        // a destructor must be declared here to
        // avoid potential memory leaks
        ~Tiling();

        // the method imposed by the `Renderable` contract
        void draw(uint8_t sliceY, uint8_t sliceHeight, uint16_t* buffer) override;
};

#endif

Let’s now move on to the definition of our Tiling class. I will detail and comment on each of the steps in calculating the rendering directly within the code:

Tiling.cpp
#include <Gamebuino-Meta.h>
#include "Tiling.h"
#include "constants.h"

// we copy the descriptive parameters of our sprite
const uint8_t Tiling::TILE_WIDTH = 16;
const uint8_t Tiling::TILE_HEIGHT = 16;
const uint16_t Tiling::TRANSPARENT_COLOR = 0xffff;

// the value of the variable `spritedata` is copied here
const uint16_t Tiling::BITMAP[] = {
    // first tile
    0xffff, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0xffff,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0x79ce, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x55ad, 0x2842,
    0xffff, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0xffff,
    // second tile
    0xffff, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0x79ce, 0xffff,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0x79ce, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x518c, 0x2842,
    0xffff, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0x2842, 0xffff
};

// then comes the constructor, which initializes the displacement vector
Tiling::Tiling() : offsetX(0), offsetY(0) {}

// a destructor must be defined here to
// avoid potential memory leaks
Tiling::~Tiling() {
    // he's not doing anything special here,
    // but it's important to think about it!
}

// and we define the method for calculating the rendering of the tiling
void Tiling::draw(uint8_t sliceY, uint8_t sliceHeight, uint16_t* buffer) {
    // we will pre-calculate some parameters
    // to optimize the processing time....

    // we will try to number the tiles according to the two axes X and Y
    // to associate indices that could be named `tx` and `ty`.
    // and then we will calculate a parity flag on these two indices:
    //   txodd = true when `tx` is odd and false otherwise
    //   tyodd = true when `ty` is odd and false otherwise
    bool txodd,tyodd;

    // it will then be sufficient to test jointly the parity of `tx` and `ty`
    // to know if you should display a light or dark tile
    // in other words, if you have to make a "jump" in the spritesheet
    // to reach the dark tile... that's the role of the `jump` flag
    bool jump;

    // and here is the offset to apply in the spritesheet
    // to access the colors of the dark tile
    uint16_t nfo = TILE_WIDTH * TILE_HEIGHT;

    // all pixels (sx,sy) of the current slice will be examined...
    // it will then be necessary to switch from the (sx,sy) local coordinate
    // system to the (x,y) global coordinate system of the screen...
    // and `sx` is actually equivalent to `x` since the slice covers the entire
    // width of the screen:
    //   x = sx
    //   y = sy + sliceY
    uint8_t sy,x,y;

    // don't forget that we have to take into account the displacement vector
    // (offsetX, offsetY) that will be applied to the tiling to give an impression
    // of motion at the ball...
    // we will therefore have to transpose our global coordinate system (x,y):
    //   xo = x + offsetX
    //   yo = y + offsetY
    uint8_t xo,yo;

    // when we have to write in the buffer, we will have to deal with
    // a one-dimensional array... so we will have to project the
    // global coordinates (x,y) on the corresponding index in the buffer :
    //   buffer_index = x + (sy * SCREEN_WIDTH)
    //                       -------syw-------
    uint16_t syw;

    // when we have to go pick the `value` color from the spritesheet
    // which will then have to be copied into the buffer, we will have to
    // calculate to which index to look for this color in the BITMAP array
    uint16_t index, value;

    // the calculation of this index can be broken down into two parts:
    //   index = index_x + index_y, where :
    //   index_x = (xo % TILE_WIDTH) + (jump * nfo)
    //   index_y = (yo % TILE_HEIGHT) * TILE_WIDTH
    uint16_t index_y;

    // scanning of each pixel of the slice (here the Y component)
    for (sy = 0; sy < sliceHeight; sy++) {

        // transition from the two-dimensional system of the slice
        // to the one-dimensional system of the buffer
        syw = sy * SCREEN_WIDTH;

        // transition from the local coordinate system of the slice
        // to the global coordinate system of the screen
        y = sliceY + sy;

        // the Y component of the displacement vector is applied
        yo = y + this->offsetY;

        // the parity indicator of the tile is calculated along the Y axis
        tyodd = (yo / TILE_HEIGHT) % 2;

        // then the Y component of the reading index in the spritesheet
        index_y = (yo % TILE_HEIGHT) * TILE_WIDTH;

        // scanning of each pixel of the slice (here the X component)
        for (x = 0; x < SCREEN_WIDTH; x++) {

            // the X component of the displacement vector is applied
            xo = x + this->offsetX;

            // the parity indicator of the tile is calculated along the X axis
            txodd = (xo / TILE_WIDTH) % 2;

            // then we determine if we should pick the color code
            // in a light or dark tile
            jump = txodd ^ tyodd;

            // the reading index in the spritesheet can now
            // be fully determined
            index = index_y + (xo % TILE_WIDTH) + (jump * nfo);

            // all that remains is to pick the color code of the pixel
            value = BITMAP[index];

            // and to copy it into the stamp if it is not a
            // transparent pixel, otherwise the black color is fixed
            buffer[x + syw] = value != TRANSPARENT_COLOR ? value : 0;
        }
    }
}

Here, I hope I have been clear enough on the details of all the calculation steps necessary to render the tiling… It wasn’t a piece of cake!

New rendering

It’s time to contemplate the result… compile… upload to the META… and this is what it looks like now :

repeated ball

Awesome! We finally have a game scene that looks like something. You might as well admit right away that we did the hardest part!… Yes, believe me… the rest is cream compared to this! And we have not yet really tackled the targeted subject of this tutorial, namely shading… Yet all these elements were necessary. All this had to be put in place so that things would be easier to digest afterwards. You’ll see that it’s not complicated after all…

But let’s keep a little suspense before we reveal everything! First we’re going to add a little interactivity and motion to all this. Let’s jump to the next chapter…

© 2020 Stéphane Calderoni