import scrawl from '../source/scrawl.js'
import scrawl from '../source/scrawl.js'
let canvas = scrawl.library.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 = scrawl.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',
getter: function () { return [].concat(this.testCoordinate) },
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.
hello: 'Wonderful Person',
testCoordinate: [100, 100],
});
Test the World object’s user-defined attributes
console.log(myWorld.get('hello'));
myWorld.set({ testCoordinate: ['center', 'center'] });
console.log(myWorld.get('testCoordinate'));
Define an Emitter entity
const myEmitter = scrawl.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,
alphaDecay = myWorld.alphaDecay,
colorRange, x, y, z,
endRad = Math.PI * 2;
let colorFactory = this.fillColorFactory;
Start by saving the engine’s current state.
engine.save();
// 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
let report = function () {
let testTicker = Date.now(),
testTime, testNow, dragging,
testMessage = document.querySelector('#reportmessage');
let particlenames = scrawl.library.particlenames,
particle = scrawl.library.particle,
historyCount;
let worldSpeed = document.querySelector('#world-speed'),
maxColorController = document.querySelector('#maxcolor-controller'),
minColorController = document.querySelector('#mincolor-controller'),
colorAlpha = document.querySelector('#color-alpha'),
background = document.querySelector('#background'),
historyLength = document.querySelector('#historyLength'),
killAfterTime = document.querySelector('#killAfterTime'),
killAfterTimeVariation = document.querySelector('#killAfterTimeVariation'),
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'),
generationRate = document.querySelector('#generationRate');
return function () {
testNow = Date.now();
testTime = testNow - testTicker;
testTicker = testNow;
ParticleHistory arrays are not saved in the Scrawl-canvas library; instead we need to count them in each particle
historyCount = 0;
particlenames.forEach(n => {
let p = particle[n];
if (p) historyCount += p.history.length;
});
testMessage.textContent = `Screen refresh: ${Math.ceil(testTime)}ms; fps: ${Math.floor(1000 / testTime)}
Particles: ${particlenames.length}, generationRate: ${generationRate.value}, historyLength: ${historyLength.value}
Stamps per display: ${historyCount}
backgroundColor: ${background.value}, tickMultiplier: ${worldSpeed.value}
maxColor: ${maxColorController.value}, minColor: ${minColorController.value}, alphaDecay: ${colorAlpha.value}
killAfterTime: ${killAfterTime.value}, killAfterTimeVariation: ${killAfterTimeVariation.value}
Range - X: from ${rangeFromX.value} to ${parseFloat(rangeFromX.value) + parseFloat(rangeX.value)}
Range - Y: from ${rangeFromY.value} to ${parseFloat(rangeFromY.value) + parseFloat(rangeY.value)}
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
scrawl.makeRender({
name: 'demo-animation',
target: canvas,
commence: mouseCheck,
afterShow: report,
});
scrawl.observeAndUpdate({
event: ['input', 'change'],
origin: '.controlItem',
target: myWorld,
useNativeListener: true,
preventDefault: true,
updates: {
'world-speed': ['tickMultiplier', 'float'],
'color-alpha': ['alphaDecay', 'float'],
},
});
scrawl.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'],
},
});
scrawl.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 () {
if (selector.value === "yes") {
myEmitter.set({
forces: ['gravity'],
});
}
else {
myEmitter.set({
forces: [],
});
}
}
}();
scrawl.addNativeListener(['input', 'change'], useGravity, '#gravity');
document.querySelector('#maxcolor-controller').value = '#F0F8FF';
document.querySelector('#mincolor-controller').value = '#F0F8FF';
document.querySelector('#world-speed').value = 2;
document.querySelector('#color-alpha').value = 6;
document.querySelector('#gravity').value = 'no';
document.querySelector('#generationRate').value = 60;
document.querySelector('#historyLength').value = 100;
document.querySelector('#killAfterTime').value = 5;
document.querySelector('#killAfterTimeVariation').value = 0.1;
document.querySelector('#range_x').value = 40;
document.querySelector('#rangefrom_x').value = -20;
document.querySelector('#range_y').value = 40;
document.querySelector('#rangefrom_y').value = -20;
document.querySelector('#range_z').value = -1;
document.querySelector('#rangefrom_z').value = -0.2;
console.log(scrawl.library);
Additional error capture events, for debugging work.
window.onerror = function (message, file, line, col, error) {
alert("Trigger 1 (onerror) - Error occurred: " + error.message);
return false;
};
window.addEventListener("error", function (e) {
alert("Trigger 2 (addEventListener - error) - Error occurred: " + e.error.message);
return false;
});
window.addEventListener('unhandledrejection', function (e) {
alert("Trigger 2 (addEventListener - unhandledrejection) - Error occurred: " + e.reason.message);
});