scrawl-canvas
Version:
Version 8.9.4 - 19 Nov 2022
394 lines (294 loc) • 15.2 kB
JavaScript
// # Demo Particles 001
// Emitter entity, and Particle World, basic functionality
// [Run code](../../demo/particles-001.html)
import {
addNativeListener,
library as L,
makeEmitter,
makeRender,
makeWorld,
observeAndUpdate,
} from '../source/scrawl.js'
import { reportSpeed } from './utilities.js';
// #### Scene setup
let canvas = L.artefact.mycanvas;
// Initial canvas background color - we will also allow the user to control this attribute's value
canvas.setBase({
backgroundColor: '#000040',
});
// #### Particle physics animation scene
// Create a World object which we can then assign to the Emitter entity
let myWorld = makeWorld({
name: 'demo-world',
// `tickMultiplier` controls the speed of the Emitter's animation
tickMultiplier: 2,
// We can define additional attributes for the World object, including their setter and getter functions (if required). We can also initialize the attribute as a Scrawl-canvas Coordinate, Vector or Quaternion object.
userAttributes: [
// These first two new attributes are purely for testing - we will get their values and log them to the console
{
key: 'hello',
defaultValue: 'Hello World',
setter: function (item) { this.hello = `Hello ${item}!`},
},
{
key: 'testCoordinate',
type: 'Coordinate',
// @ts-expect-error
getter: function () { return [].concat(this.testCoordinate) },
// @ts-expect-error
setter: function (item) { this.testCoordinate.set(item) },
},
// We will store a user-updatable Number value - `alphaDecay` - which we will use in the `stampAction` function to tweak the particle effect that we are trying to achieve
{
key: 'alphaDecay',
defaultValue: 6,
},
],
// Overwrite our user-defined attributes' default values with new data, for testing.
// @ts-expect-error
hello: 'Wonderful Person',
testCoordinate: [100, 100],
});
// Test the World object's user-defined attributes
console.log(myWorld.get('hello'));
// @ts-expect-error
myWorld.set({ testCoordinate: ['center', 'center'] });
console.log(myWorld.get('testCoordinate'));
// Define an Emitter entity
const myEmitter = makeEmitter({
name: 'use-raw-2d-context',
// Every emitter __must__ be associated with a World object. The attribute's value can be the World object's String name value, or the object itself
world: myWorld,
// The Emitter is a normal Scrawl-canvas entity. It can be positioned absolutely/relatively - as here, by setting the `start` (`startX`, `startY`) coordinates. Or it can be positioned by reference to other Scrawl-canvas artefacts using the `pivot`, `mimic`, `path`, `mouse` and/or Net `particle` functionality.
start: ['center', 'center'],
// Emitter entitys use ___ephemeral particles___ to produce their visual effects, generating a steady stream of particles over time and then killing them off in various ways. The `generationRate` attribute _sets the number of particles that the Emitter will generate every second_.
generationRate: 60,
// A common way to kill off generated particles is to give them a lifetime limit (measured in seconds). We can set that value using the `killAfterTime` attribute. We can also add in a measure of variability using the `killAfterTimeVariation` attribute.
// + The Emitter being defined here, for instance, will generate a regular stream of 60 particles every second, with each of the particles having a lifetime limit of between 4.9 and 5.1 seconds
killAfterTime: 5,
killAfterTimeVariation: 0.1,
// For every Display cycle tick (which in optimal conditions will be around 17 milliseconds after the previous tick), a particle will update its position and record the new position using a ParticleHistory array. This data is then added to the entity's `history` array. We can limit the number of ParticleHistory arrays stored in the history array by setting the `historyLength` attribute to a suitable integer Number value.
historyLength: 100,
// The key functionality of a particle is that it moves.
// + Particles will move as a consequence of the forces and spring constraints applied to them.
// + When the particle is generated, we can give it an initial ___velocity___; with no other force or spring applied to the particle, it will move at a constant speed over time to match this initial velocity value.
// + We set the particle's initial velocity using a set of six __range__ attributes, which represent the distance travelled in the x, y and z directions, as measured in pixels-per-second.
// + The `rangeFrom` attributes represent the lowest value in that dimension that will be generated. This value is ___local to the particle___ thus negative values are to the left (x) or above (y) or behind (z) the particles initial position.
// + The `range` attribute is the maximum random value which will be added to the rangeFrom value.
// + All particles are assigned a (constrained) random velocity in this manner when they are generated. The values given below will ensure that every particle generated by this Emitter will have a vector velocity of somewhere between -20 to +20 pixels/second in both the x (left-right) and 'y' (above-below) directions, and between -0.2 and -1.2 pixels/second in the z (behind-infront) direction.
rangeX: 40,
rangeFromX: -20,
rangeY: 40,
rangeFromY: -20,
rangeZ: -1,
rangeFromZ: -0.2,
// We can assign a range of colors to our particle - we'll start the demo with the minimum and maximum fillStyle colors set to the same color
fillMinimumColor: '#f0f8ff',
fillMaximumColor: '#f0f8ff',
// The `stampAction` function describes the steps that our Emitter will take to draw each of its particles onto the host canvas screen.
// + In this instance, we have not supplied the Emitter with an `artefact`; instead we will draw directly on the host object's <canvas> element.
stampAction: function (artefact, particle, host) {
// We obtain the [canvas element's 2D rendering context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) - which we will call the ___engine___ - from the function's `host` argument.
let engine = host.engine,
history = particle.history,
len = history.length,
remaining, radius, alpha,
// @ts-expect-error
alphaDecay = myWorld.alphaDecay,
colorRange, x, y, z,
endRad = Math.PI * 2;
// @ts-expect-error
let colorFactory = this.fillColorFactory;
// Start by saving the engine's current state.
engine.save();
engine.setTransform(1, 0, 0, 1, 0, 0);
// // We are using the same color for all of the Emitter's particles, which we've stored in a user-defined attribute in the World obvject.
// engine.fillStyle = myWorld.get('particleColor');
// We are going to display all of the particle's most recent tick positions, as saved in their `history` array
history.forEach((p, index) => {
// Every ParticleHistory Array stores its data in the following manner:
// ```
// [
// How much time the particle has to live, recorded in float Number seconds
// The particle's `z` position at that moment in time, recorded in pixel Number values
// The particle's `x` position at that moment in time, recorded in pixel Number values
// The particle's `y` position at that moment in time, recorded in pixel Number values
// ]
// ```
[remaining, z, x, y] = p;
// We can change the size of the particle circle, based on its given z direction value - the more distant it is from us, the smaller its radius should be.
radius = 6 * (1 + (z / 3));
// As the particle ages, we want it to appear to be more transparent - note that the _remaining_ value represents time remaining before the particle dies, not how long the particle has been alive.
alpha = remaining / alphaDecay;
// Another ageing mecahnism can be constructed using the index value vs the history array's length.
colorRange = index / len;
// Only draw this historical instance of the particle if it will be visible
if (radius > 0 && alpha > 0) {
// Start a new path
engine.beginPath();
// Move the path to the correct position
engine.moveTo(x, y);
// Define the circle to be drawn at those coordinates
engine.arc(x, y, radius, 0, endRad);
// Set the engine's globalAlpha attribute
engine.globalAlpha = alpha;
// Set the engine's fillStyle attribute - we're using a range color here
// + We request the color from the emitter's fillColorFactory using the `get` function
// + When `alpha == 1` the color factory will return the maximum color string
// + When `alpha == 0` the color factory will return the minimum color
// + values between 0 and 1 return a ranged color between the minimum and maximum colors
engine.fillStyle = colorFactory.get(colorRange);
// Perform the fill for this particle
engine.fill();
}
});
// Restore the engine's state.
engine.restore();
},
});
// #### Scene animation
// Function to display frames-per-second data, and other information relevant to the demo
const particlenames = L.particlenames,
particle = L.particle;
const report = reportSpeed('#reportmessage', function () {
// ParticleHistory arrays are not saved in the Scrawl-canvas library; instead we need to count them in each particle
let historyCount = 0;
particlenames.forEach(n => {
let p = particle[n];
if (p) historyCount += p.history.length;
});
// @ts-expect-error
return ` Particles: ${particlenames.length}, generationRate: ${generationRate.value}, historyLength: ${historyLength.value}\n Stamps per display: ${historyCount}\n\n backgroundColor: ${background.value}, tickMultiplier: ${worldSpeed.value}\n maxColor: ${maxColorController.value}, minColor: ${minColorController.value}, alphaDecay: ${colorAlpha.value}\n\n killAfterTime: ${killAfterTime.value}, killAfterTimeVariation: ${killAfterTimeVariation.value}\n\n Range - X: from ${rangeFromX.value} to ${parseFloat(rangeFromX.value) + parseFloat(rangeX.value)}\n Range - Y: from ${rangeFromY.value} to ${parseFloat(rangeFromY.value) + parseFloat(rangeY.value)}\n Range - Z: from ${rangeFromZ.value} to ${parseFloat(rangeFromZ.value) + parseFloat(rangeZ.value)}`;
});
// We want the Emitter to attach itself to the mouse cursor whenever it is active over the <canvas> element
let mouseCheck = function () {
let active = false;
return function () {
if (canvas.here.active !== active) {
active = canvas.here.active;
myEmitter.set({
lockTo: (active) ? 'mouse' : 'start'
});
}
};
}();
// Create the Display cycle animation
makeRender({
name: 'demo-animation',
target: canvas,
commence: mouseCheck,
afterShow: report,
});
// #### User interaction
// For this demo we will suppress touchmove functionality over the canvas
addNativeListener('touchmove', (e) => {
e.preventDefault();
e.returnValue = false;
}, canvas.domElement);
// Setup form observer functionality
observeAndUpdate({
event: ['input', 'change'],
origin: '.controlItem',
target: myWorld,
useNativeListener: true,
preventDefault: true,
updates: {
'world-speed': ['tickMultiplier', 'float'],
'color-alpha': ['alphaDecay', 'float'],
},
});
observeAndUpdate({
event: ['input', 'change'],
origin: '.controlItem',
target: myEmitter,
useNativeListener: true,
preventDefault: true,
updates: {
'maxcolor-controller': ['fillMaximumColor', 'raw'],
'mincolor-controller': ['fillMinimumColor', 'raw'],
generationRate: ['generationRate', 'int'],
historyLength: ['historyLength', 'int'],
killAfterTime: ['killAfterTime', 'float'],
killAfterTimeVariation: ['killAfterTimeVariation', 'float'],
range_x: ['rangeX', 'float'],
rangefrom_x: ['rangeFromX', 'float'],
range_y: ['rangeY', 'float'],
rangefrom_y: ['rangeFromY', 'float'],
range_z: ['rangeZ', 'float'],
rangefrom_z: ['rangeFromZ', 'float'],
},
});
observeAndUpdate({
event: ['input', 'change'],
origin: '.controlItem',
target: canvas,
useNativeListener: true,
preventDefault: true,
updates: {
background: ['backgroundColor', 'raw'],
},
});
const useGravity = function () {
const selector = document.querySelector('#gravity');
return function () {
// @ts-expect-error
if (selector.value === "yes") {
myEmitter.set({
forces: ['gravity'],
});
}
else {
myEmitter.set({
forces: [],
});
}
}
}();
addNativeListener(['input', 'change'], useGravity, '#gravity');
const worldSpeed = document.querySelector('#world-speed'),
maxColorController = document.querySelector('#maxcolor-controller'),
minColorController = document.querySelector('#mincolor-controller'),
colorAlpha = document.querySelector('#color-alpha'),
background = document.querySelector('#background'),
rangeX = document.querySelector('#range_x'),
rangeFromX = document.querySelector('#rangefrom_x'),
rangeY = document.querySelector('#range_y'),
rangeFromY = document.querySelector('#rangefrom_y'),
rangeZ = document.querySelector('#range_z'),
rangeFromZ = document.querySelector('#rangefrom_z'),
historyLength = document.querySelector('#historyLength'),
killAfterTime = document.querySelector('#killAfterTime'),
killAfterTimeVariation = document.querySelector('#killAfterTimeVariation'),
generationRate = document.querySelector('#generationRate');
// @ts-expect-error
maxColorController.value = '#F0F8FF';
// @ts-expect-error
minColorController.value = '#F0F8FF';
// @ts-expect-error
worldSpeed.value = 2;
// @ts-expect-error
colorAlpha.value = 6;
// @ts-expect-error
document.querySelector('#gravity').value = 'no';
// @ts-expect-error
rangeX.value = 40;
// @ts-expect-error
rangeFromX.value = -20;
// @ts-expect-error
rangeY.value = 40;
// @ts-expect-error
rangeFromY.value = -20;
// @ts-expect-error
rangeZ.value = -1;
// @ts-expect-error
rangeFromZ.value = -0.2;
// @ts-expect-error
generationRate.value = 60;
// @ts-expect-error
historyLength.value = 100;
// @ts-expect-error
killAfterTime.value = 5;
// @ts-expect-error
killAfterTimeVariation.value = 0.1;
// #### Development and testing
console.log(L);