How to handle making a demo in javascript

Cyril Pereira
8 min readJul 30, 2021

--

When you only code on the web

Introduction

Hey, if you are here you know what is a demo, you maybe know me as Med or MedCg (Mister Electric Demon) member of Mandarine.
I also did demos with Condense, Mankind, and music for Demoscene labels as Analogik and Jecoute !

Some of my musics are available here : https://misterelectricdemon.bandcamp.com/

From some years now, i started to code demo as a coder and not as musician with javascript, here are my productions :

I choose to code them in javascript because i like it ;) And i think we can do really good stuff with it today !

Let’s talk about my last demo : Shadow-Party 2021 intro invit’

The project

I wanted to create a demo with some 80’s design, i composed the base of the music at first and i wanted to do tribute to Prince with something which can sound a bit like his period of Cream

So demo followed the music, and the effect was added all long during the creation step by step.

The Music

I always used Tracker Software to compose music. Today i’m using Renoise.

Renoise is a software really close to another one called Fasttracker II i used for quiet long time.

Fasttracker II sound like this

For my past demos i used FT2 because i wanted to make tinies demos in weight of octets.

The Visuals

For this demo i used threejs as 3D library.

The demo is a one scene one camera with a 3D environnement. Everything inside the scene is really simple, only plane !

Every 2D image displayed with a plane placed in front of the camera with a material which container the bitmap or vectorial images.

The demo contain a big Plane Mesh with a vertex shader on it.

The vertex shader is animate with a phase which give the effect that it’s a road and we are moving forward on it.

Some texture are animated in real time, like the vumeter or the oscilloscope, it’s generated from the music.

The background is a simple shader, it container some entrypoint to control the visual.

The sparks are simple plane turned in front of the camera with a texture in additif. All the sparks are recycled, and displayed from the very fare background of the scene to the front. A random position is defined at the start.

All the flash, grain camera effects are done with shaders in post rendering.

I had the help of Callisto which create a nice logo for the demo, and i used the logo from Nytrik/Cocoon for the end part.

Code

Loading the code

First, we load all the assets (libraries, images, music). To do so we use javascript and HTML.Then, we define the scene with threejs :

  • the scene
  • the camera
  • the 3D objects
  • we create the sparks
  • the background

In the HTML file i wrote all the dependencies, and i created a main.js which contains everything to start the demo by using this event.

window.addEventListener('load', function() {
// Start the demo
});

Because the modern browser block the audio, we need to display a button on the page and add an event on it to start the demo and initialize the audio.

var audio;
var source = "./music/cool-me-down-unfinished.mp3";
audio = document.createElement("audio");
audio.preload = "auto";
audio.src = source;
startButton.addEventListener('click', function() {
audio.play();
});

The demo start when the audio start. Why ? Because the demo is sync to the music and i will explain that just after.

The rendering

The rendering is done with threejs and use requestAnimationFrame

function render() {
requestAnimationFrame(render);
}
startButton.addEventListener('click', function() {
audio.play();
render();
});

the function render will be called to 60fps, if your computer is not enough fast the request will adapt the number of fps to continue to render the maximum of image.

All the demo is rendered in multiple post-processors with threejs and shader.

Inside the render function we compute all the visual modification, and all the textures rendered.

it looks like this

var renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "low-power",
});
// Post processors
var renderScene = new THREE.RenderPass(scene, camera);
var effectFXAA = new THREE.ShaderPass(THREE.FXAAShader);
// Chain processors into composer
var composer = new THREE.EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(effectFXAA);
function render() {
requestAnimationFrame(render);
renderer.autoClear = false;
renderer.clear();
composer.render();
}

As you see it’s really basic:

  • Generate scene
  • Prepare shader
  • Load and prepare 3D and 2D objects
  • Way the viewer to be ready
  • When he is ready start the music and the main loop of code

Syncs & animations

I create the sync key with Reaper.

I create all the sync with Reaper and then use it in my demo

To sync and animate the demo, i wrote 2 librairies.

  • MNDRN.Animate for animations and control the objects
  • Timeliner for syncs to do in the correct time with the music

Animations

MNDRN.Animate control and change any values of css or javascript properties.

It use some little basic of ease and tween and it’s only 2ko.

The use of it looks like this :

new MNDRN.Animate({
delay: 1,
item: material,
to: {"opacity": 0},
duration: duration,
delta: MNDRN.Animate.EASE.EaseOut(MNDRN.Animate.EASE.Quad)
}).start();

As you see, i can add a delay, set the object you want to manipulate (here a material on a mesh), the duration and the change to do, here the opacity turn to 0, and the ease to use.

This tool animate everything in the demo.

I can do a flash, change a color, hide or display something, move it, with or without a bounce or elastic effect…

Syncs

I wrote a little library Timeliner which help me a lot on doing demos https://www.npmjs.com/package/funkymed-timeliner

This tool contain a set of interface object and tool which can manage to control a story in time.

To create a timeline we define a Scenario, and it scenario receive every Scenes of the demo. Every Scenes contain information a start, an end, and we can redefine every function of the interface :

  • init : fire when the time arrive to the start time
  • resize : fire if the window is resized
  • render : fire in requestAnimation loop after the init
  • and clear : fire to stop the rendering and clear the Scene

Here is the source of the Scene interface.

function Scene(start, end) {
this.obj = [];
this.assets = {};
this.start = start;
this.end = end;
this.visible = false;
this.init = function () {
this.visible = true;
};
this.resize = function () {
};
this.render = function () {
};
this.addObj = function (o) {
this.obj.push(o);
scene.add(o);
};
this.clear = function () {
this.obj.forEach(function (b) {
scene.remove(b);
});
for (var b in this.obj) {
delete this.obj[b];
}
for (var b in this.assets) {
delete this.assets[b];
}
this.visible = false;
};
}

So imagine you want to do a flash, you will do something like this

function sceneFlash(st, power, duration) {
var _scene = new Scene(st, st + 1);
_scene.init = function () {
this.visible = true;
var currentBloom = bloomPass.copyUniforms["opacity"].value;
bloomTo(parseInt(currentBloom) + power, 50);
setTimeout(function () {
bloomTo(currentBloom, duration ? duration : 500);
}, 50);
};
return _scene;
}

And that function will be part of the Timeline in the Scenario like this :

var s = new Scenario();
var timeline = [
sceneFlash(14, 3, 900) // start at 14s, power of 3 duration 900
];
timeline.forEach(function (b) {
s.add(b);
});

After you know that, you can imagine you can add a lots of other scene to the demo, here is the beginning of the demo.

var timeline = [
//Default config
setBgColor(0, 1, 0.1, 1.8),
setBgPower(0, 3, 500),
setBgLine(0, 150, 500),
// Fade in
setOpacity(0, renderer.domElement.style, 1, 2000),
sceneScrollText(11, 35),
// Intro
setOpacity(2.004, planeMat, .95, 8000),
setOpacity(3.127, sunMat, 1, 1000),
setOpacity(4.275, starMaterial, 1, 5000),
setBgPower(5.137, 6, 5000),
sceneText(5.603, 6, "## Mandarine %%", false, false, false, "left", "zoomOut"),
sceneText(5.603, 6, "presents", false, -40, false, "right", "zoomIn", 1),
// ...
];

To control the scenario, and sync with the audio, we just have to add to the render loop this : s.check(audio.currentTime)

function render() {
requestAnimationFrame(render);
s.check(audio.currentTime); // Send the current time to the scenario
renderer.autoClear = false;
renderer.clear();
composer.render();
}

audio.currentTime is a value inside the Audio HTML5 object, but you can use something else to sync your demo.

Compression and Packaging

PNG

I wrote a compiler to compile all the javascript files and librairies in one file, i wrote it in php.

#!/usr/bin/env php
<?php
include('vendor/autoload.php');
use JShrink\Minifier;
$output = isset($argv[1]) ? $argv[1] : "output";
$filename = 'output/'.$output.".js";
if(!is_dir('output'))
{
mkdir('output');
}
$files = array(
'js/lib/three.min.js',
'js/lib/noise.js',
'js/lib/EffectComposer.js',
'js/lib/RenderPass.js',
'js/lib/LuminosityHighPassShader.js',
'js/lib/UnrealBloomPass.js',
'js/lib/ShaderPass.js',
'js/lib/CopyShader.js',
'js/lib/FXAAShader.js',
'js/lib/BadTVShader.js',
'js/lib/RGBShiftShader.js',
'js/lib/FilmShader.js',
'js/lib/CopyShader.js',
'js/lib/StaticShader.js',
'js/lib/BufferGeometryUtils.js',
'js/lib/SVGLoader.js',
'js/lib/OBJLoader.js',
'js/lib/animation.js',
'js/clearpng.js',
'js/lib/2Dfx.js',
'js/lib/scenario.js',
'js/lib/scene.js',
'js/var.js',
'js/assets.js',
'js/decrunch.js',
'js/scene1.js',
'js/svg.js',
'js/sequences.js',
'js/timeline.js',
'js/music.js',
);
if (file_exists($filename)) {
unlink($filename);
}
foreach($files as $k=>$f) {
$fp1 = fopen($filename, 'a+');
$file2 = file_get_contents($f);
$file2 = Minifier::minify($file2, array('flaggedComments' => false));
$file2 = "// ---- ".$f." \n".$file2."\n";
fwrite($fp1, $file2);
}
exec('ruby pnginator.rb output/'.$output.'.js output/'.$output.'.png.html');

At the end of this file i’m using something to compile the finale javascript file in a png file.

I used pnginator.rb by Gasman available on his github account https://gist.github.com/gasman/2560551

ELECTRON

I could finish the demo with the png package, but i wanted to be part of the demo competition and not only the web competition.

I used Electron to package my demo in executable for Linux, MacOS, and Windows.

After installed the packages i edited my package.json

{
"name": "shadow-party-2021",
"author": {
"name": "med/mandarine",
"email": "cyril.pereira@gmail.com"
},
"homepage": "https://mandarine.planet-d.net",
"description": "Shadow-Party 2021 intro invite by Mandarine",
"license": "UNLICENSED",
"version": "1.0.1",
"main": "main.js",
"devDependencies": {
"electron": "^11.3.0",
"electron-builder": "^22.10.5",
"electron-packager": "^15.2.0"
},
"scripts": {
"postinstall": "./node_modules/.bin/electron-builder install-app-deps",
"start": "./node_modules/.bin/electron .",
"build": "./node_modules/.bin/electron-builder build",
"build-mac": "./node_modules/.bin/electron-builder build --mac",
"build-win": "./node_modules/.bin/electron-builder build --win",
"build-linux": "./node_modules/.bin/electron-builder build --linux",
"pack-mac": "./node_modules/.bin/electron-packager . --platform=darwin",
"pack-win": "./node_modules/.bin/electron-packager . shadow-party-2021 --platform=win32",
"pack-linux": "./node_modules/.bin/electron-packager . shadow-party-2021 --platform=linux"
},
"build": {
"appId": "com.Mandarine.electron",
"productName": "Shadow-Party2021",
"mac": {
"icon": "images/icon-512x512.png"
},
"dmg": {},
"mas": {},
"win": {
"icon": "images/icon-512x512.png",
"publisherName": "Mandarine"
},
"appx": {},
"portable": {},
"linux": {}
},
"files": [
"fonts/*",
"images/**/*",
"js/**/*",
"music/*",
"obj/*",
"index.html"
]
}

Feel free to use my package.json or adapte it.

Here is the main.js file :

const {
app,
BrowserWindow,
Menu,
globalShortcut,
} = require('electron');
let mainWindow;
Menu.setApplicationMenu(null);
// Because demos have to stop by pressing escape
app.on('ready', () => {
globalShortcut.register('Escape', function(){
mainWindow.close();
});
mainWindow = new BrowserWindow({
frame: false,
resizable: false,
transparent: false,
width: 1920,
height: 1080,
webPreferences: {
nodeIntegration: true
}
});
mainWindow.center();
mainWindow.setMenu(null);
mainWindow.setFullScreen(true);
mainWindow.loadURL('file://' + __dirname + '/index.html');
});
app.on('will-quit', function(){ globalShortcut.unregister('Escape');
globalShortcut.unregisterAll();
});

Final Release

Here is the result on Youtube, but you also can found all the executable en web version here https://www.pouet.net/prod.php?which=88403

Enjoy !

--

--