UNPKG

scrawl-canvas

Version:

Responsive, interactive and more accessible HTML5 canvas elements. Scrawl-canvas is a JavaScript library designed to make using the HTML5 canvas element easier, and more fun

776 lines (589 loc) 25.6 kB
// # RdAsset factory // [Reaction-diffusion systems](https://en.wikipedia.org/wiki/Reaction%E2%80%93diffusion_system) are mathematical models which can be used to explore various physical phenomena. The Scrawl-canvas version is an interpretation of a _two-component activator-inhibitor system_. // // This factory was inspired by Jason Webb's [Reaction-Diffusion Playground](https://jasonwebb.github.io/reaction-diffusion-playground/) // #### Imports import { constructors, entity } from '../core/library.js'; import { seededRandomNumberGenerator } from '../helper/random-seed.js'; import { doCreate, mergeOver, λnull, λcloneError, Ωempty } from '../helper/utilities.js'; import { releaseCell, requestCell } from '../untracked-factory/cell-fragment.js'; import baseMix from '../mixin/base.js'; import assetMix from '../mixin/asset.js'; import assetAdvancedMix from '../mixin/asset-advanced-functionality.js'; import patternMix from '../mixin/pattern.js'; // Shared constants import { ASSET, DEFAULT_SEED, ENTITY, RANDOM, T_RD_ASSET, WHITE, ZERO_STR } from '../helper/shared-vars.js'; // Local constants const RD_SETTINGS_PREF_VALS = ['random', 'entity']; // #### RdAsset constructor const RdAsset = function (items = Ωempty) { this.makeName(items.name); this.register(); this.installElement(this.name); this.subscribers = []; this.set(this.defs); this.currentSource = null; this.outputA = null; this.outputB = null; this.initialSettingEntity = null; this.rowAbove = null; this.rowBelow = null; this.colLeft = null; this.colRight = null; this.set(items); if (items.subscribe) this.subscribers.push(items.subscribe); this.currentGeneration = 0; this.dataArrays = []; this.dirtyScene = true; this.dirtyOutput = true; return this; }; // #### RdAsset prototype const P = RdAsset.prototype = doCreate(); P.type = T_RD_ASSET; P.lib = ASSET; P.isArtefact = false; P.isAsset = true; // #### Mixins baseMix(P); assetMix(P); assetAdvancedMix(P); patternMix(P); // #### RdAsset attributes const defaultAttributes = { // The offscreen canvas dimensions, within which the reaction-diffusion scene will be generated, is set using the __width__ and __height__ attributes. These take Number values. width: 300, height: 150, // The reaction-diffusion scene results will differ based on the initial settings for the __feedRate__, __killRate__, __diffusionRateA__ and __diffusionRateB__ attributes. These attributes are all float numbers in restricted ranges between 0.0 and 1.0 // + In general, the diffusion rate of the A reagent should be roughly double that of the B reagent // + Given the above, the relative settings of the feed and kill rates will determine the stability and eventual outcome of the scene. To find more interesting values, check out the online interactive[Reaction-Diffusion Playground](https://jasonwebb.github.io/reaction-diffusion-playground/app.html) diffusionRateA: 0.2097, diffusionRateB: 0.105, feedRate: 0.054, killRate: 0.062, // The Scrawl-canvas implementation allows users to initialize the scene using pseudo-random values scattered across the canvas, or alternatively use a regular Scrawl-canvas entity to supply a more regular initial shape. // + Demo [Canvas-053](../../demo/canvas-053.html) includes Block, Wheel and Spiral entitys which can be used for this purpose // // __initialSettingPreference__ - String - either 'random' (default) or 'entity' initialSettingPreference: RANDOM, // __randomEngineSeed__ - String acting as the random engine's seed value. different String values will create different scene outputs, all other attributes not changing. randomEngineSeed: DEFAULT_SEED, // __initialRandomSeedLevel__ - Number between 0.0 and 1.0 representing the density of reagent A across the scene's dimensions initialRandomSeedLevel: 0.0045, // __initialSettingEntity__ - String name of the entity to be used for setting up the scene initialSettingEntity: null, // __drawEvery__ - positive integer Number - the speed of evolution is affected by how often we draw the current state of the scene onto the canvas. drawEvery: 10, // __maxGenerations__ - positive integer Number - some interesting outputs can be found as a given scene develops over time. We can halt development to allow these outputs to be used by Picture entitys and Pattern styles by setting this attribute to the appropriate value // + Halting the calculations when the scene reaches a desired output is also more efficient - allowing a scene to develop forever may cause the entire animation to slow down // + To allow the scene to develop forever (for instance, to show a more chaotic output) set te value of this attribute to `0` maxGenerations: 4000, }; P.defs = mergeOver(P.defs, defaultAttributes); delete P.defs.source; delete P.defs.sourceLoaded; // #### Packet management // This functionality is disabled for Cell objects P.stringifyFunction = λnull; P.processPacketOut = λnull; P.finalizePacketOut = λnull; P.saveAsPacket = function () { return `[${this.name}, ${this.type}, ${this.lib}, {}]` }; // #### Clone management P.clone = λcloneError; // #### Kill management // No additional kill functionality required // #### Get, Set, deltaSet const S = P.setters, G = P.getters; // __subscribers__ - we disable the ability to set the subscribers Array directly. Picture entitys and Pattern styles will manage their subscription to the asset using their subscribe() and unsubscribe() functions. Filters will check for updates every time they run S.subscribers = λnull; // Setting any of the following attributes will cause the scene to re-initialize itself S.width = function (item) { if (item.toFixed) { this.width = item; this.sourceNaturalWidth = item; this.dirtyScene = true; } }; S.height = function (item) { if (item.toFixed) { this.height = item; this.sourceNaturalHeight = item; this.dirtyScene = true; } }; S.initialRandomSeedLevel = function (item) { if (item.toFixed) { this.initialRandomSeedLevel = item; this.dirtyScene = true; } }; S.diffusionRateA = function (item) { if (item.toFixed) { this.diffusionRateA = item; this.dirtyScene = true; } }; S.diffusionRateB = function (item) { if (item.toFixed) { this.diffusionRateB = item; this.dirtyScene = true; } }; S.feedRate = function (item) { if (item.toFixed) { this.feedRate = item; this.dirtyScene = true; } }; S.killRate = function (item) { if (item.toFixed) { this.killRate = item; this.dirtyScene = true; } }; S.drawEvery = function (item) { if (item.toFixed) { this.drawEvery = item; this.dirtyOutput = true; } }; S.maxGenerations = function (item) { if (item.toFixed) { this.maxGenerations = item; this.dirtyOutput = true; } }; S.initialSettingPreference = function (item) { if (item.substring && RD_SETTINGS_PREF_VALS.includes(item)) { this.initialSettingPreference = item; this.dirtyScene = true; } }; S.randomEngineSeed = function (item) { if (item.substring) { this.randomEngineSeed = item; this.dirtyScene = true; } }; S.initialSettingEntity = function (item) { if (item) { if (!item.substring) item = item.name || ZERO_STR; if (item) { this.initialSettingEntity = item; this.dirtyScene = true; } } }; G.generation = function () { return this.currentGeneration; }; // This asset comes with a set of predefined effects built in, to make using it easier // + Reaction-diffusion simulations are very sensitive to their initial conditions and, in most cases, will quickly evolve into all-black or all-white (monochrome) displays // + These presets pick out some of the more stable initial conditions // + ___WARNING! The chaos-related presets will generate strobe-lighting effects which may trigger epileptic fits in people who are sensitive to such displays___ S.preset = function (item) { if (item.substring) { switch (item) { case 'negativeBubbles': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.098; this.killRate = 0.0555; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.05; break; case 'positiveBubbles': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.098; this.killRate = 0.057; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.1; break; case 'precriticalBubbles': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.082; this.killRate = 0.059; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.08; break; case 'wormsAndLoops': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.082; this.killRate = 0.06; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.08; break; case 'stableSolitons': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.074; this.killRate = 0.064; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.15; break; case 'uSkateWorld': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.062; this.killRate = 0.0609; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'worms': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.058; this.killRate = 0.065; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.1; break; case 'wormsJoinIntoMazes': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.046; this.killRate = 0.063; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'negatons': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.046; this.killRate = 0.0594; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'turingPatterns': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.042; this.killRate = 0.059; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'chaosToTuringNegatons': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.039; this.killRate = 0.058; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'fingerprints': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.037; this.killRate = 0.06; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'chaosWithNegatons': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.0353; this.killRate = 0.0566; this.maxGenerations = 0; this.initialRandomSeedLevel = 0.0045; break; case 'spotsAndWorms': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.034; this.killRate = 0.0618; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'selfReplicatingSpots': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.03; this.killRate = 0.063; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'superResonantMazes': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.03; this.killRate = 0.0565; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'mazes': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.029; this.killRate = 0.057; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; break; case 'mazesWithSomeChaos': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.026; this.killRate = 0.055; this.maxGenerations = 0; this.initialRandomSeedLevel = 0.0045; break; case 'chaos': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.026; this.killRate = 0.051; this.maxGenerations = 0; this.initialRandomSeedLevel = 0.009; break; case 'warringMicrobes': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.022; this.killRate = 0.059; this.maxGenerations = 0; this.initialRandomSeedLevel = 0.0045; break; case 'spotsAndLoops': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.018; this.killRate = 0.051; this.maxGenerations = 0; this.initialRandomSeedLevel = 0.0045; break; case 'movingSpots': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.014; this.killRate = 0.054; this.maxGenerations = 0; this.initialRandomSeedLevel = 0.0045; break; case 'waves': this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.014; this.killRate = 0.045; this.maxGenerations = 0; this.initialRandomSeedLevel = 0.0045; break; default: this.diffusionRateA = 0.2097; this.diffusionRateB = 0.105; this.feedRate = 0.054; this.killRate = 0.062; this.maxGenerations = 4000; this.initialRandomSeedLevel = 0.0045; } this.dirtyScene = true; } }; // #### Prototype functions // `update` - this function needs to be called once during each Display cycle. // + Reaction-diffusion systems are, like particle systems, dynamic and time based - the more stable scenes need around 4000 iterations to settle down into their final states. P.update = function () { this.dirtyOutput = true; }; // `cleanOutput` - internal function called by the `notifySubscribers` function // + All reaction-diffusion calculations are coordinated from this function // + Function also checks to see if scene needs to be (re)initialized - initialization will take place after any of the initial parameters are set to new values P.cleanOutput = function () { let iterations = 0; if (this.dirtyScene) this.cleanScene(); if (!this.dirtyScene) { const { dataArrays, diffusionRateA, diffusionRateB, feedRate, killRate, currentSource, drawEvery, maxGenerations, currentGeneration, width, height, rowAbove, rowBelow, colLeft, colRight, } = this; let sourceA, destA, sourceB, destB; if (!maxGenerations || currentGeneration < maxGenerations) { let generation = currentGeneration, sourceFlag = currentSource; while (iterations < drawEvery) { if (sourceFlag) [destA, sourceA, destB, sourceB] = dataArrays; else [sourceA, destA, sourceB, destB] = dataArrays; const killPlusFeed = killRate + feedRate; let index, rowAboveIndex, rowHere, rowBelowIndex, colLeftIndex, colRightIndex, topLeft, top, topRight, left, right, bottomLeft, bottom, bottomRight, a, b, bb, reaction, lapA, lapB, da, db; for (let row = 0; row < height; row++) { rowAboveIndex = rowAbove[row]; rowHere = row * width; rowBelowIndex = rowBelow[row]; for (let col = 0; col < width; col++) { index = rowHere + col; colLeftIndex = colLeft[col]; colRightIndex = colRight[col]; topLeft = rowAboveIndex + colLeftIndex; top = rowAboveIndex + col; topRight = rowAboveIndex + colRightIndex; left = rowHere + colLeftIndex; right = rowHere + colRightIndex; bottomLeft = rowBelowIndex + colLeftIndex; bottom = rowBelowIndex + col; bottomRight = rowBelowIndex + colRightIndex; a = sourceA[index]; b = sourceB[index]; bb = b * b; reaction = a * bb; lapA = (sourceA[index] * -1) + (sourceA[topLeft] * 0.05) + (sourceA[topRight] * 0.05) + (sourceA[bottomLeft] * 0.05) + (sourceA[bottomRight] * 0.05) + (sourceA[top] * 0.2) + (sourceA[right] * 0.2) + (sourceA[left] * 0.2) + (sourceA[bottom] * 0.2); lapB = (sourceB[index] * -1) + (sourceB[topLeft] * 0.05) + (sourceB[topRight] * 0.05) + (sourceB[bottomLeft] * 0.05) + (sourceB[bottomRight] * 0.05) + (sourceB[top] * 0.2) + (sourceB[right] * 0.2) + (sourceB[left] * 0.2) + (sourceB[bottom] * 0.2); da = a + (diffusionRateA * lapA) - reaction + (feedRate * (1 - a)); db = b + (diffusionRateB * lapB) + reaction - (killPlusFeed * b); destA[index] = da < 0 ? 0 : da > 1 ? 1 : da; destB[index] = db < 0 ? 0 : db > 1 ? 1 : db; } } sourceFlag = sourceFlag ? 0 : 1; generation++; iterations++; if (maxGenerations && generation >= maxGenerations) break; } this.currentSource = sourceFlag; this.currentGeneration = generation; if (sourceFlag) { this.outputA = dataArrays[0]; this.outputB = dataArrays[2]; } else { this.outputA = dataArrays[1]; this.outputB = dataArrays[3]; } this.paintCanvas(); } else if (this.dirtyOutput) this.paintCanvas(); } }; // `cleanScene` - internal function called by the `cleanOutput` function // + Resets the scene to its initial conditions P.cleanScene = function () { const { element, width, height, dataArrays, initialRandomSeedLevel, initialSettingPreference, randomEngineSeed, initialSettingEntity } = this; if (width && height) { if (this.dirtyScene) { this.dirtyScene = false; element.width = width; element.height = height; const len = width * height, rowAbove = new Int32Array(height), rowBelow = new Int32Array(height), colLeft = new Int32Array(width), colRight = new Int32Array(width); for (let row = 0; row < height; row++) { rowAbove[row] = ((row > 0) ? row - 1 : height - 1) * width; rowBelow[row] = ((row < height - 1) ? row + 1 : 0) * width; } for (let col = 0; col < width; col++) { colLeft[col] = (col > 0) ? col - 1 : width - 1; colRight[col] = (col < width - 1) ? col + 1 : 0; } this.rowAbove = rowAbove; this.rowBelow = rowBelow; this.colLeft = colLeft; this.colRight = colRight; dataArrays.length = 0; // We use four arrays to contain the current and next state of the scene for (let i = 0; i < 4; i++) { dataArrays.push(new Float32Array(len)); } this.currentSource = 0; const [sourceA, destA, sourceB, destB] = dataArrays; this.outputA = destA; this.outputB = destB; sourceA.fill(1); destA.fill(1); sourceB.fill(0); destB.fill(0); // The scene can be set up to include either random values, or we can use Scrawl-canvas entitys to supply the initial state for the second reagent if (ENTITY === initialSettingPreference && entity[initialSettingEntity]) { const ent = entity[initialSettingEntity], mycell = requestCell(); const {engine:cellEngine, element:cellElement} = mycell; cellElement.width = width; cellElement.height = height; ent.simpleStamp(mycell, { fillStyle: WHITE, strokeStyle: WHITE, }); const initImg = cellEngine.getImageData(0, 0, width, height), initData = initImg.data; let counter = 0; // get data from alpha channel for (let i = 3, iz = initData.length; i < iz; i += 4) { sourceB[counter] = initData[i] / 255; counter++; } releaseCell(mycell); } else { // We use a semi-random generator to supply random values to the scene // + This ensures that we can discover and recreate particular patterns from a of 'randomized' initial state, as the pattern will be tied to the canvas dimensions, the feed and kill rates, the diffusion rates for each reagent, and the String value supplied to the `randomEngineSeed` attribute. const rndEngine = seededRandomNumberGenerator(randomEngineSeed); for (let index = 0; index < len; index++) { sourceB[index] = (rndEngine.random() < initialRandomSeedLevel) ? 1 : 0; } } this.currentGeneration = 0; } } }; // `checkOutputValuesExist` and `getOutputValue` are internal variables that must be defined by any asset that makes use of the _assetAdvancedFunctionality.js_ mixin and its `paintCanvas` function P.checkOutputValuesExist = function () { return (this.dataArrays.length) ? true : false; }; P.getOutputValue = function (index) { return (1 + (this.outputA[index] - this.outputB[index])) / 2; }; // #### Factory // ``` // scrawl.makeReactionDiffusionAsset({ // name: 'my-rd-generator', // width: 50, // height: 50, // }); // ``` export const makeReactionDiffusionAsset = function (items) { if (!items) return false; return new RdAsset(items); }; constructors.RdAsset = RdAsset;