Designing and building a web app from scratch: a case study

This article is a summary of a collaborative and iterative design process by two designers on a tool for a lamp design company.

Written by Vincent Desclaux and Louis Eveillard.
Project: https://acrobatesbuilder.fr/

What is an Acrobates de Gras lamp?

At the beginning of February 2017, we were contacted by DCW éditions for a new project they had in mind: a conception module development for their Acrobates model. DCW is a company that specializes in lamp models production, reissues and design. Their best-seller and most emblematic models are the Gras geometric and functional shades. In their catalog, the specificity of The Acrobates is that they are a suspended lamp model edited as 6 sub-models (n°322, 323, 324, 325, 326 & 327). To sum it up, an Acrobates lamp is composed of 2 (or more) ceiling casts, 2 (or more) electrified cables, a light socket and different shades to customize the desired model.

The brief

The initial brief we received included a basic description of features that had to be included in the app. Most specifically, DCW wanted a tool that would display models in a 3D space, work with pointer and touch devices, and include actual photos and materials of the lamp models. These constraints were difficult to manage, even more so with the short timeframe we had: the project had to be done in about 6 weeks, with the final deadline being the Salone del Mobile Milano at the beginning of April 2017.

The first few meetings with the client included talking about their product catalogue, sketching ideas for layouts and talking about interactions principles. At this point, we tried to simplify the intended brief while maximizing the details and parameters to be sure we had the right amount of informations before designing the app and exploring possible solutions.

“It was important for us that the app would be both appealing and intuitive. Because of this, a website with the look and feel of a native app (usually called webapp) was a no-brainer: no install time, shareable with just a link, and responsive to all device sizes and inputs.”

Methodology

Every project is based on previous experiences, pedagogies and methodologies. We started by defining the vocabulary we would use to build the project and set a common ground with the client. The following concepts were essential to the exchanges:

  • Presets: defines the pre-compilation of a lamp model, as the design of Acrobates Lamp is based on modularity. Ergo 6 Acrobates lamp models equals 6 presets to interact with.
  • Bricks: a part of a model and an implementation system to anticipate the code structure as well as the production of the assets. We can consider a brick as a div-tag or a block, structurally speaking.
  • Sliders (or cursor): corresponds to a parameter, which most of the time is connected to a specific brick and defines the interactive inputs/outputs of the app. Some sliders are integrated as first layer action, others as secondary layer, accessible by a sub-menu or pop-up window.
  • Views: the different representation that data can have. At this point it was important to show the logic of such a system to the client, to explain how the previous listed inputs were going to generate a model (basically, a code representation) of the scene and how the app architecture would be using bricks to display this information as different views in the app.
Sketches of the “bricks” and “sliders” model.
Sketch of the “presets” definition.

Questioning space representation

Space dimensions and its representation was the real dilemma of the design process. First, because of short deadlines and the aim for an intuitive experience, we focused the discussion on a 2D representation of a room. We wanted to avoid the 3D-factory engine that would place this app amongst interior design complex software or branded furniture conception tool such as the famous Swedish Online Kitchen planner. This project’s needs could be handled by a simpler and more visually appealing 2D system. Working with this constraint generated more creative designs and code solution.

“To solve the problem of mapping 3D objects in 2D, we explored architectural and object representation paradigms to find key solutions in matter of depth, parallax or axonometric projection.”

The first visual tests were simple, minimal and straightforward. Our visual references were based on two main notions:

  • For the functional and technical representation of the scene (where the lamp would be set), we referred to the effective designs produced by world-famous designer Dieter Rams for Braun (simplified color swatches, primary-shaped buttons and functions, economy of (visual) material and basic minimalism).
Layouts and posters of Dieter Rams Braun’s interfaces design
  • For the theatrical scene, the frame of the action, we had to find a visual trick to tilt the room to give a feeling of perspective. This trick can be found in architectural front view of collage from the work of Dogma agency, or the graphic perspective plan of Bow-wow atelier (above). Just a hint of perspective brought by a ceiling headband of a contrasted color between floor, furnitures and background would be enough in this case.
References by Atelier Bow-wow and Dogma Agency.

With these notions in mind, we produced the following sketches and perspective mockups:

Space projection prototypes with simulated shadows.
Space projection application on the ceiling design

Mapping light

Accordingly to the 2D direction of the project, we had to design the right flat-feel for the shade’s volume. At this point we had in mind to map and animate gradients dynamically, when the lamps are moved across the scene, to give a sense of volume and roundness to those objects. But the clock was ticking and the complexity of such a minimal effect was much too high for something this subtle. Quickly, the gradient solution was abandoned for a simpler alternative with only two colors for each shade. This was clearer and different from the usual approach of 3D mapping software.

First gradient-based light mapping, which was abandoned after a week.

Some strictly illustrative solutions such as the Windows of New-York project (below) brought the intuitive and frontal shading we were looking for: flat shadows and correct usage of colors camaïeu for gleams to describe the volume of each shade model, from glass matter to copper or painted inox. Later on, we added simple textures to materials like raw-copper to add a distinctive touch for the metal models.

Windows of New-York illustration project, a weekly illustrated atlas by José Guízar
View of a light cone emanating from the lamp.

As we were sketching and drawing those assets, we started adding a shadow under each lamp (basically, a dark grey ellipse) at the bottom of the scene to add some grounding and more depth. At the same time, we also had the idea to implement a feature that would allow the user to turn the scene dark (some kind of a night mode). This gave us the idea to add another feature that wasn’t originally planned: lighting up the light bulb in the lamps to simulate the light beam coming out of the shades. In the end, we scraped the shadow idea, but we kept the night mode and the light beams. A great serendipity!

Art Direction, UI design process and assets

The UI design of the app focused on three parts: the lamp models, the furnitures to create a background, and the right interface to navigate and interact between the parameters of the scene. We drew the separate elements on many artboards in Adobe Illustator, and then exported them to a dropbox in a big batch of SVG files that was synced with code prototypes, to test and correct the integration with an iPad and monitor the result in the app in almost realtime.

1.The representation of DCW’s shades catalog, resulting in the flat design orientation explained before. The rendering is based on flat flare and shadows emphasis. Colors are based on the 3 primary colors used for the collection, also with copper, raw-copper and inox finish.

DCW shades catalog in final flat version

2. A basic standard furniture design for the background, which conveys a depth of field to the scene. The furnitures were listed by DCW in their brief to answer common clients needs: where should the lamps be if there is a furniture underneath (bed, sofa, chair, tables, benches and worktops) ?

Basic furniture to make space and scale markers and improve the experience.

3. Dashboard and navigation assets were exported as many SVG sprites for buttons, icons and markers. Active or important items are reinforced by flash green, close or delete actions use a bright red, and all secondary actions use shades of grey. The creation of the UI interface will be detailed in a next point, especially focusing on the main dashboard.

Technology stack

Making a webapp meant that we had to use web technologies for everything: HTML for the structure of the app, CSS for its layout and JavaScript for everything else. The good news is, with HTML5, there is now a way for 95% of browsers to generate and manipulate images in realtime with code. This technique relies on something called the canvas. In a canvas, and with some javascript code, you can draw geometric shapes or images wherever you want, move them around smoothly, get touch/mouse position and react to clicks/taps almost instantaneously. Usually, for complex canvas animations, developer don’t write plain JS/canvas code but instead rely on a JavaScript library that already implements a certain number of complex formulas to accelerate code development.

http://topotopo.io/, a simple and engaging web app to generate and explore terrain data from different places on earth.

Exploring the web for examples of web app similar to what we had in mind and testing those on a tablet, we were convinced that this would be the way to go.

Week 1–2: first prototypes

The first two weeks were spent experimenting with DCW catalog and making simple interactive prototypes to test our sketches and minitial mockups in a real setting. We worked on the user interface and the prototypes in parallel, with integration of visual elements being usually done the day it was drawn. This way, we could work in a two-way feedback loop (UI-wise and code-wise) that helped improve the overall consistence and helped us experiment a lot of possible interaction principles.

To be as close as possible to how the app would be used (that is, in the context of a stand in the busy Salon of Milan), we focused on designing and prototyping for an iPad in landscape.

“Our target resolution was a tiny 1024*768 pixels in a 4:3 aspect ratio. While we kept in mind that a lot of our visitors from the web (and not the Salon) would be using larger screens, it was a good exercice to try and optimize the user experience for such a screen.”

Drawing on the web

There are a lot of different drawing-oriented libraries in JavaScript for the web: some of the most well-known include paper.js, rafael.js, or p5.js. For that project, we didn’t have much time to experiment to find the most appropriate library. Instead, we considered the following factors to decide which one to use:

  • familiarity with the logic and functions,
  • size and importance of the community behind that project
  • quantity and precision of the resources we would need, such as vector manipulation, touch controls and physics-based animation

All things considered, we decided to use p5.js. We had no prior experience with this library but a lot with Processing, the langage that inspired it (which helped us build working prototypes in just a few minutes). Also, p5.js’s community is large and has been very active since the beginning of this library. Its issue tracker many hundred issues closed and almost all those that are opened are being discussed.

Also, one of the deciding factor was that Daniel Shiffman, from the youtube series The Coding Train and the book Nature of Code, had published a lot of resources for p5.js, especially on physics. Having watched a bit of his channel already, we were confident these would be precious additions to kickstart our physics engine (which turned out to be the case).

An example of a great video by Daniel Shiffman about physics engine. Excellent way to get to know how they work and how to use them.

In this situation, the riskiest aspect of going with an unknown tool such as a JavaScript library was stability and performance: what we had sketched initially would probably be feasible in p5.js but what could happen was that after a good bit of work on a prototype, we would discover that p5.js can only manage 5 or 10 lamps in a given scene when we expected 30 or 40, or that the app would crash randomly for some unknown reason.

We settled on a reference device to test all our prototypes on (both UI-wise and performance-wise): an iPad 3 from 2012 running an up-to-date iOS and Safari. Since this device is quite old and would probably be amongst the slowest device in terms of performance to connect to our app, we were forced to optimize everything as much as possible.

Testing p5.js

The first few prototypes we made were about getting to know and testing the limits of p5.js. We spent some time reading the tutorials and experimenting with object-oriented programming in p5. We specifically looked at the following example from the p5 docs, and from the Nature of Code Vectors chapter.

This is what the first prototype looks like:

First basic prototype, using just p5.js. Online demo here: http://files.louiseveillard.com/acrobates/proto-01/fr (doesn’t work on Firefox, optimized for Chrome)

It works like this: all the lamp models inside our <canvas> tag are stored in a variable in the main program, empty at the beginning:

var lamps = [];

We then have a simple lamp object, whose code is similar to this (actual code is here):

function lamp(x,y,cd,shd) {
this.position = createVector(x,y);
this.cast_distance = cd;
this.shade = shd;
this.position_slider = createSlider(20, width, x);
  this.update = function() {
this.position.x = this.position_slider.value();
}
this.display = function() {
image(
this.shade,
this.position.x,
this.position.y+20,
this.shade.width/2,
this.shade.height/2
);
}

To add a lamp to the scene, we just need to call the lamp class for a new object stored in our lamp variable, with specific values that determine its initial x position, y position, cast distance and shade image to be used:

lamps[lamps.length] = new lamp(150, 200, 30, shades[0]);

In the actual sketch, we use random values instead so that each new lamp is different. p5 makes it easy to create a random value between 100 and 150 by calling just random(100, 150);.

Finally, to update all lamps position according to their respective slider value and display them, we call in the draw function:

for (var i = 0; i < lamps.length; i++) {
lamps[i].update();
lamps[i].display();
}

With this setup, we could add more than 50 or 60 lamps before our iPad started slowing down a bit, but it would still run reliably. This gave us the confidence to go forward and test other aspects of this project.

Physics

Physics is an interesting topic for a tool such as this one: since we decided early on to use very simple images in an abstracted 2d representation of a room, the connection to the physical objects that people could buy and use was slimmer than it would have been otherwise.

“To give the lamps some substance and get users to understand that what they were interacting with referred to tangible objects, we relied on physics and simple animations collected from the original object.”

In this prototype, we implemented some simple interpolation between positions: instead of teleporting, lamps subjected to basic forces and could only travel pixel by pixel. This was the start of a lot of fine-tuning to find the settings that would make those pixel lamps act more like their metal and fabric counterparts.

Though better, this was still very rough and far from the experience we had in mind. Amplifying the inertia and adding drag and drop was a huge leap in the right direction. We voluntarily exaggerated all the movements of the lamps to get a more expressive feel while manipulating. At the same time, we redrew the lamps' shades without using gradients in order to reduce the visual clutter and get a more modern look. You can see in the following video both the improved engine and the new shades look.

Third prototype, about a week into the start of the project. The background is dark because at this point we were testing a night mode as an alternative to the default day mode.

We still had a few days before the first presentation to the client, which meant we could still work on improving that system. Since the visual look and interaction principles were very different from what was initially described in the specifications, it was important for us to create a prototype polished enough to convince DCW that we were on the right track. We integrated the newly-drawn assets for the shades and refined their behavior and movement in space.

Experimenting with the lamps movement in space.

More specifically, we spent a lot of time animating the shades themselves so that they would move independently from the rest of the model. Instead of a monobloc object, our lamps would be made up of different parts that would be articulated and react differently to the movement of the lamp object. For example, here’s a drag and drop on a lamp:

Moving a shade around with the debug view activated.

The debugging view was added to simplify working on the physics, it is activated on that gif (the bright lines and squares). The red outline shows the position of the lamp object, while the cable and shade behave with their own logic. In code, this meant that the lamp object now called other objects, which it would update when its own update got called:

function lamp(x,y,cd,shd) {
  […]
  this.cast_pair = new Cast(this.position.x,this.cast_distance);
this.cable = new Cable(this.cast_pair.left.position.x, 10, cables_length);
this.cable2 = new Cable(this.paireAppliques.right.position.x, 10, cables_length);
this.shad = new Shade(this.position.x, this.position.y);
  […]
  this.update = function() {
this.cast_pair.update(this.position.x, this.cast_distance);
this.cable.update(this.cast_pair.left.position.x);
    […]
  }
}

The shade animation itself was the trickiest part to get right. DCW had lend us a few lamps so that we could better get a sense of their weight, their size and the way they moved in space. Since the shades are quite heavy and suspended by flexible cables, the oscillation movement that they would have after being moved around would take a few seconds before stopping. It was clear we needed to highlight this oscillation and make it obvious.

Alas, we had never attempted anything like that in code, let alone in p5.js. However, we had anticipated that situation and found all the resources we needed in the form of the Nature of Code book by Daniel Shiffman.

Preview from Nature of Code by Daniel Shiffman, an immensely useful resource in general and for this project in particular: http://natureofcode.com/book/chapter-3-oscillation/

In the first 3 chapters this book, Daniel Shiffman (a fantastic teacher, writer and caster), explains how vectors work, how to create and apply forces on objects and how to create oscillating systems. We spent around 3 or 4 hours reading and doing the exercices in those chapters, and then finding out how to adapt what we learned to our specific needs.

Though this book was written with Processing in mind, a github repo with p5.js examples exist — the one we used most is this one.

Decomposition of a lamp according to the objects that it uses. JavaScript files are separated by type of object.

In the end, to simulate the rotation of the shade, we had to create a fake weight hanging from a cable suspended from the lamp. This weight has a mass in abstract unit (at this point 4, in the final app 2) and is permanently affected by gravity (a value of 6 down). To position itself, the shade gets the direction of the invisible cable that is suspended from it (in blue on the left) and rotates to match that angle.

While this may seem overly complex, using a suspended weight allowed us to have a number of variables to play with in order to get the interaction right. Later on, we also added a gravity force to the shades themselves, which meant we had even more granular control over their behavior in space.

Assembling all of this gave us the following prototype:

Fourth prototype, with improved animation: http://files.louiseveillard.com/acrobates/proto-04/

Although that prototype worked well on our reference iPad 3, there were a few easy tweaks we could add to make it more polished. We added a colored button to open/close the side panel, and a bunch of toggles to test the day/night mode, furnitures, light projections, grid mode and debug view. We also added disabled buttons in the bottom left corner and surrounded the 1024*768 frame in a slight drop-shadow, to clearly show the size of the frame for an iPad when running in a large desktop window.

A more complex prototype that was used in the first presentation, 2 weeks after the beginning of the project: http://files.louiseveillard.com/acrobates/proto-presentation/fr

Week 3–6: building the app

Almost everything we showed during that presentation was approved, and the mockups and prototype made them feel like we had done already more than half the work. This was of course far from the truth. In the following graph of number of code lines added and removed, we are at the beginning of week 3. There was a lot that remained to build the app that would be used and accessed by people at the Salone and on the web.

Graph of code contribution to the tool, extracted from Github.

The UI mockups changed quite a lot during the following week. We spent a whole week adjusting this 330px wide panel only. As this controller received all the advanced features and parameters, we entered in a week of code-design ping-pong where the UX moved the namings and the imbrication of lamp models resulting in new visual hierarchy in the bricks. We tried several panel display as “folding-item”, “parent-file” structure and “child-models”. The object-oriented denomination of the lamp models are developed in the following chapter.

In between this constant scrapping evolution, a large feature was developed and then abandoned for lack of feedback on how the user will experience this event: the electric point and connexion between models. As a matter of fact, one of the most clever feature of the design of the lamp is the ability to plug many models in a row, using only one electric point in the ceiling or the wall. We designed the Ux and prototyped the chain connexion module that would allow to add adjustable wires on the ceiling. But all the possible combinations and user feedbacks were too unreliable to keep pushing that prototype too far. We decided with DCW to set that feature aside for now.

First evolution of the dashboard, close from a “designed” wireframe
Fine-tuning of the dashboard, the right versions show red dots that introduces the flow of electricity

At this point, almost everything was settled: the main UI principles, the animations/physics and the features. We had about 4 weeks to build everything again, this time in a much more robust way. We scrapped almost everything from the previous prototypes and started over from scratch.

Designing the structure

On a technical note, the main unknown was about finding the right structure for the app. All previous prototypes relied on p5.js to handle scene data: lamps position, type of shades, day or night mode, etc. This was hardly ideal because of the complex user interface we had designed for the right panel: more precise and easier to use sliders, shade selector, furniture selector, etc.

Interaction could happen both via the scene, via the panel, or via code (for loading preset models, for example). Everything had to be updated at the same time, whenever some data would change and whatever the origin of the change. For example:

  • changing a slider must change the position of the corresponding lamp in the canvas,
  • dragging a lamp must update the slider value in the panel, if it is open,
  • adding a lamp via the panel must add it to the scene,

But there was also much more complex scenarii to think about:

  • changing the room size must change the room size in the canvas and adapt all horizontal and cast distance sliders with the right maximum and minimum values,
  • changing the cast distance must change the maximum and minimum values for the horizontal position for that lamp,
  • exporting a scene via URL for later use or sharing on social medias.
“It was clear that what we needed was some kind of a single source for all the data, where both the canvas and the right panel would connect to collect the information they needed.”

The code in p5.js would adapt the room at all time to fit whatever was in that big variable, and the code for the panel would do the same with HTML elements. For this second part, we clearly needed a modern framework such as React or Angular. From experience, we knew that keeping track and updating each HTML node manually could become messy very fast, and it seemed like a good reason to finally experiment with these tools in a real setting. We didn’t have much time to learn the tool we’d select, and it had to be operational in a few days at the most: a few hours worth of research showed us that Vue.js was a good fit for our needs.

Vue.js

Vue.js is a lesser known alternative to React, Ember and Angular. It was created by an experienced developer named Evan You a few years ago and has been gaining a lot of traction recently. It is easy to learn and quite powerful, and has a wide range of components that make developing a complex interface much simpler than it would be otherwise.

We spent about a day doing all the Essentials tutorial on the Vue website. As we read the documentation, we became convinced we had made the right choice: most feature seemed useful (for our app) and easy to use, with expressive and readable syntax. We got Vue.js set-up in about a day and started playing around with some basic input fields.

On the left, the content of the state variable, and on the right the canvas and the panel. Both can edit the content of the state in real-time without conflict.

Storing the scene informations in code

Until that point, we didn’t bother with getting information from the catalog and importing it in the app. That changed when we added Vue.js, which forced us — for good reasons — to create what’s called a store: a large variable that contains the scene information (the state) and also all the gateway functions to accessing or editing it. To put it simply, instead of editing a value directly, a store will give you setters that you call to change this value:

// for example, this
window.store.lamp[0].position.x = 10;
// becomes this
window.store.setLampPositionX(0, 10);

The point of all this is to act on this value before actually changing said value. It means that we can check for the validity of that value before recording it — if it doesn’t make sense, for example if a position is given a negative value, we can just skip it. It makes debugging much easier as well, since we can log what happens and who’s responsible. For more information, see Vue.js/Guide/Advanced/State Management.

At the beginning of the program, the window.store variable only contains those functions but has no content. The scene is empty, it has no size and doesn’t even exist. Then:

  • if a scene is detected and valid in the URL (we’ll talk about this in a minute) then that scene will be loaded,
  • otherwise, the following default informations will be copied to the store.
Default information used when Acrobates Builder starts.

In this file, you can see settings such as the room size in centimeters, scene options (daymode, light cones, debug, grid, measures) and empty variables for mobiliers (furnitures), points_electrique (electric point) and models. Lamps are objects contained by models (a model can be made of up to 6 lamps).

The Acrobates catalog is made of 6 models, each with specific options/sliders:

  1. the 322 is a single shade suspended from a single cast,
  2. the 323 is a single shade with two casts, in the form of a V,
  3. the 324 is two shades with three casts, resembling a W,
  4. the 325 is three shades with a central cast and three peripheral casts,
  5. the 326 is six shades with six casts and a central common to all of them,
  6. the 327 is seven shades, one central and six lateral.
Suspension models for Acrobates lamps from n°322 to n°327 (left to right)

Some model allow for each of the shades’ height to be tweaked independently (the 324 for example) while other do not (such as the 325). Some allow for each shade type to be changed while others (like the 326) force all shades to be identical. This adds a lot of complexity, both in the code and in the user interface. To create a manageable record of all this, we created a big MODELS.js file that contained all the possible models. Here’s a sample for the 322 lamp, comments are in grey:

For a more complex model such as a 324, where horizontal position and cast distance are common between the two lamps, those values are placed at the model level. A setter function checks and apply the position change directly to all the lamps who are inherit_position_x: true and inherit_ecart_cable: true (ecart_cable is cast_distance):

There is a lot of exceptions when dealing with such a varied catalog, whose logic has more to do with marketing than what the lamps allow, but proceeding this way allowed us to keep making changes to the catalog until the very last minute.

What a 322, 324, and 325 look like.
The UI bricks for the above 322, 324 and 325, with all its specificity. Note the variation of Shades/Shade labels and the difference in the way sliders stack.

Working with Vue.js made having exceptions not that big a deal, though. For example, to check in the code for the UI whether the model we are building that brick for is an exception, we could just write:

<lamp-options v-if="model.opus !== 325">
</lamp-options>

This would only display for models that are not 325. Not ideal but not that inconvenient either.

Another benefit of using Vue.js is that all components are literally observing the data that they use and update whenever it changes. For example:

Sliders update on the fly depending on other sliders/values: for example when the x position is changed, the cast distance limits are changed.

Components

There was still a lot to do to match the mockups we made in parallel. To earn some time, we used Vue.js components developed by the community as much as possible. Most of these have great documentation so we won’t detail how they work so much as show how we used them:

vue-slider-component improves a lot of what HTML sliders are by default. Value can be displayed right next to the dot, whose size can be changed.
vee-validate adds tooltip to display hints when values entered in HTML field are out of bounds.
vue-carousel simplify the creation of carousel with a lot of tweaking.

Those components were pretty well documented and adaptable, so we could really change their style and behavior to fit our app. For example, using vue-slider-component, we were able to make a comfortable slider (with a big track button for the thumb) that was also animated to mimic the movement of the lamps.

Adapting vue-slider-component to our needs.
The room size panel with a tooltip that displays when a value is outside the scope. Uses vee-validate.
Two vue-carousel used here side by side. The one on the left updates the one on the right so that color options always exist in the catalog.

Vue.js component approach is really, really convenient and corresponded nicely to our brick metaphor. Essentially, everything that is shown in the panel is part of a component.

Decomposition of Vue components.
Components for a model brick, with subcomponents on three levels.

Week 6: final polish

Overall, the project was almost finished at that moment and what remained was small features that would help polish the overall experience.

Share by URL

Clicking on SAVE pops-up a menu to copy and share a URL to a specific scene. That scene automatically loads on opening Acrobates Builder.

Light cones

We added the right angles on the lights for simulating how objects would receive light from the lamps.

Cone information sent by DCW.
Cone angle implemented in a scene.

Building the tutorial

For this part we used Intro.js

Scores and grid

This functionality is especially used by interior architects for measuring the length of the cables, or test specific settings.

Export

An export option was added, with the possibility to export a PNG of the current display, or with added measures. All the models, lamps, shades and casts are listed as well, with links to 3D files and the corresponding price.

Export menu and exported scene with scores in black and white.

The Acrobates Builder can be tested here: https://acrobatesbuilder.fr/en


We wish to thank the following people: 
- the DCW team,
- Felix Albertini and Emilie Coquard for their precious feedbacks,
- Ferdinand Dervieux for his participation early in this project,
- Arnaud Juracek for inputs on the tech stack,
- Noémie for helping with the lamp oscillation gif in this article