import {
addNativeListener,
library as L,
makeEmitter,
makeRender,
makeWorld,
observeAndUpdate,
} from '../source/scrawl.js'
import { reportSpeed } from './utilities.js';import {
addNativeListener,
library as L,
makeEmitter,
makeRender,
makeWorld,
observeAndUpdate,
} from '../source/scrawl.js'
import { reportSpeed } from './utilities.js';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',
});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.
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.
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.range attribute is the maximum random value which will be added to the rangeFrom value. 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.
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 - 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
get functionalpha == 1 the color factory will return the maximum color stringalpha == 0 the color factory will return the minimum color engine.fillStyle = colorFactory.get(colorRange);Perform the fill for this particle
engine.fill();
}
});Restore the engine’s state.
engine.restore();
},
});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,
});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;console.log(L);