Forum Discussion
Buckle Up
With the additional functionality exposed by the JavaScript API, I've been wanting to implement more specific mechanics of design, e.g., movement/traversal, action/combat, puzzles/problem-solving, etc. With objects (and their coordinates) being so easy to reference now, I wanted to demonstrate movement/traversal being implementable with only some modest code.
In this particular design, I wanted to explore how sliding textured images horizontally from one side of the screen to the other could simulate motion, especially with a simple, red rectangle standing in as a car controlled by you.
Note: The Up/Down and Left/Right controls cancel each other out accidentally. My intent in this project was on animating graphics and connecting audio, not so much exploring controls, so I left it as it is. I do recognize though that additional learner control would likely improve that aspect of the experience.
Graphics
For graphics, I constrained myself to most every visual asset being either a basic shape styled a particular way or an image sourced from the content library so that the bulk of the emergence would come from how those simple objects were manipulated in complex ways.
Below is a reduced version of the slide's main update loop:
update(() => {
// Calculates deltaTime (dt), a variable common in frame-rate dependent content like games to constrain the content from rushing or dragging unintentionally.
const now = performance.now();
const dt = (now - lastTime) / 1000; // seconds since last frame
lastTime = now;
const elapsedSeconds = now - lastRecordedTime;
// Flipping this flag to false naturally stops the road from moving.
if (isMoving) {
// Applies/simulates friction against the speed variable, increased elsewhere (not here) in other code related the keyboard keys.
speed = Math.max(1, getVar('speed') - (getVar('isOffroad')? 2.5*gsap.ticker.deltaRatio(60) : .2*gsap.ticker.deltaRatio(60)));
turnSpeed = Math.ceil(speed / 150) + (getVar('isOffroad')? 2 : 0);
// The gsp.ticker calls above allow for gradual changes in the speed over time (as opposed to sudden changes), better simulating friction. In practice, increase and decrease speed however you want so long as its final value makes it down below to whichever lines change the x-position of the image.
// Move roads to the left every frame
road1.x -= speed * dt;
road2.x -= speed * dt;
// Reset to right side once entirely off-screen, leap-frogging one road image over the other.
if (road1.x < (slideWidth() * -1)) { road1.x = road2.x + road1.width; }
if (road2.x < (slideWidth() * -1)) { road2.x = road1.x + road2.width; }
};
Audio
The "car" features three sound emitters:
- A repetitive sound emulating exhaust coming out a car's muffler
- A slower, lower repetitive thump that plays when "driving" on grass
- A horn (press Spacebar or click the car)
All three "emitted" sounds are generated at runtime (not sample-based) using the Tone.js library.
The horn is instrumental and silly by design, demonstrating a simple "sine wave" from a default synth that one can use to play music. The other two emitters demonstrate a more complex understanding of waveform modulation.
The offroad rumble and muffler synthesizers demonstrate the contextual simulation of simple rhythmic, mechanical sounds. The offroad sound is produced with a Metalsynth, a synthesizer considered clangy and dissonant, but adding a low-pass filter (LPF) at a warm 90hz softens its clang into a warm rumble:
const rumbleLPF = new Tone.Filter(90, "lowpass");
const rumblePanner = new Tone.Panner();
if(!window.tireRumble) {
window.tireRumble = new Tone.MembraneSynth({
envelope: {
attack: 0.1,
decay: 0.0,
sustain: 1,
release: 0.1
}
})
.connect(rumbleLPF)
.toDestination();
}
The muffler exhaust sound was a bit more nuanced. Being personally cognizant of the importance of harmonics in vehicle vibrations, both desirable and not, the FM synth seemed a fitting choice:
const exhaustHPF = new Tone.Filter(45, "highpass");
const exhaustLPF = new Tone.Filter(125, "lowpass");
const exhaustPanner = new Tone.Panner();
if (!window.exhaust) {
window.exhaust = new Tone.FMSynth({
envelope: {
attack: 0.001,
decay: 0,
sustain: 1,
release: 0.005
},
modulationIndex: 40,
harmonicity: 3,
volume: -6
})
.connect(exhaustHPF)
.connect(exhaustLPF)
.connect(exhaustPanner)
.toDestination();
}
Both of these synths run in Storyline's update() loop with some logic dictating when and how they activate. Of particular note (indeed, my solitary seed of this whole project) is scaling the pitch played by the synthesizer with the speed variable:
// triggerAttackRelease(note, duration, time?, velocity?): this
window.exhaust.triggerAttackRelease(5 + getVar('speed')/100, "8n", now);
The minimum of 5 guarantees a lovely purr. "8n" refers to eighth notes, the type of note played over and over again by Storyline. A gentle logarithmic function might better emulate a gear nearing a maximum, and a few conditional ones stair-cased together could emulate the sound of changing gears…
The explosion sound effect and music are stock I found online, though I did adjust the song's stereo field to carve more space in the center for the sound effects. Naturally both the music and the explosion can also be crafted in Tone.js--I entertained the idea of the off-road sound effect only ever being out-of-sync with the music to further layer in negative feedback--but I did what I aimed to.
I've wanted for a very long time to procedurally synthesize responsive audio at runtime and this proof-of-concept hopefully helps demonstrate such utility.
Related Content
- 3 years ago
- 3 years ago