import * as library from '../core/library.js';
import { addStrings, generateUniqueString, isa_boolean, mergeOver, pushUnique, removeItem, xt, xta, λnull, Ωempty } from '../helper/utilities.js';This mixin sets up much of the basic functionality of a Scrawl-canvas Object:
Given the fundamental nature of the above functionality the base mixin should always, when coding a Scrawl-canvas factory, be the first to be applied to that factory function’s prototype.
import * as library from '../core/library.js';
import { addStrings, generateUniqueString, isa_boolean, mergeOver, pushUnique, removeItem, xt, xta, λnull, Ωempty } from '../helper/utilities.js';Shared constants
import { _entries, _isArray, _keys, _parse, ARG_SPLITTER, NAME, UNDEF, ZERO_STR } from '../helper/shared-vars.js';Local constants
const BAD_PACKET_CHECK = '"name":',
HAS_PACKET_CHECK = '[',
NATIVE_CODE = '[native code]',
PACKET_DIVIDER = '~~~',
TYPE_EXCLUSIONS = ['Image', 'Sprite', 'Video', 'Canvas', 'Stack'];export default function (P = Ωempty) {The defs object supplies default values for a Scrawl-canvas object. Setter functions will check to see that a related defs attribute exists before allowing users to update an object attribute. Similarly the getter function will use the defs object to supply default values for an attribute that has not otherwise been set, or has been deleted by a user.
P.defs = {};The getters object holds a suite of functions for given factory object attributes that need to have their values processed before they can be returned to the user.
P.getters = {};The setters object holds a suite of functions for given factory object attributes that need to process a new value before setting it to the attribute.
P.setters = {};The deltaSetters object holds a suite of functions for given factory object attributes that need to process a new value before adding it to the attribute’s existing value.
P.deltaSetters = {};get - Retrieve an attribute value using the get function. While many attributes can be retrieved directly - for example, scrawl.artefact.myelement.scale - some attributes should only ever be retrieved using get:
scrawl.artefact.myelement.get('startX');
-> 200
P.get = function (item) {
let val, fn;
if (xt(item)) {
fn = this.getters[item];
if (fn) return fn.call(this);
else {
const def = this.defs[item];
if (typeof def !== UNDEF) {
val = this[item];
return (typeof val !== UNDEF) ? val : def;
}
}
}
return null;
};set - Set an attribute value using the set function. It is extremely important that all factory object attributes are set using the set function; setting an attribute directly will lead to unexpected behaviour! The set function takes a single object as its argument.
scrawl.artefact.myelement.set({
startX: 50,
startY: 200,
scale: 1.5,
roll: 90,
});
P.set = function (items = Ωempty) {
let i, key, val, fn;
const keys = _keys(items),
keysLen = keys.length;
if (keysLen) {
const setters = this.setters,
defs = this.defs;
for (i = 0; i < keysLen; i++) {
key = keys[i];
val = items[key];
if (key && key !== NAME && val != null) {
fn = setters[key];
if (fn) fn.call(this, val);
else if (typeof defs[key] !== UNDEF) this[key] = val;
}
}
}
return this;
};setDelta - Add a value to an existing attribute value using the setDelta function. It is extremely important that all factory object attributes are updated using the setDelta function; updating an attribute directly will lead to unexpected behaviour! The setDelta function takes a single object as its argument.
scrawl.artefact.myelement.setDelta({
startX: 10,
startY: -20,
scale: 0.05,
roll: 5,
});
P.setDelta = function (items = Ωempty) {
let i, key, val, fn;
const keys = _keys(items),
keysLen = keys.length;
if (keysLen) {
const setters = this.deltaSetters,
defs = this.defs;
for (i = 0; i < keysLen; i++) {
key = keys[i];
val = items[key];
if (key && key !== NAME && val != null) {
fn = setters[key];
if (fn) fn.call(this, val);
else if (typeof defs[key] !== UNDEF) this[key] = addStrings(this[key], val);
}
}
}
return this;
}; const defaultAttributes = {Scrawl-canvas relies on unique name values being present in a factory object for a wide range of functionalities. Most of the library sections store an object by its name value, for example: scrawl.artefact.myelement
name: '',
};
P.defs = mergeOver(P.defs, defaultAttributes);
const G = P.getters;
G.type = function () {
return this.type;
};
G.isArtefact = function () {
return this.isArtefact;
};
G.isAsset = function () {
return this.isAsset;
};Packets are Scrawl-canvas’s way of generating and consuming SC object data, both locally and over a network. A packet is a formatted JSON String which can be captured in a variable or saved to a text file.
The packet format is as follows:
"[object-name, object-type, object's-library-section, {object-data-key:value-pairs}]"
Using JSON.stringify(object), or object.toString(), to generate String representations of SC objects is not recommended as they will forego the pre-processing that saveAsPacket performs to generate the packet String.
Example:
let canvas = scrawl.library.canvas.mycanvas;
canvas.setAsCurrentCanvas();
let box = scrawl.makeBlock({
name: 'my-box',
startX: 10,
startY: 10,
width: 100,
height: 50,
fillStyle: 'red',
onEnter: function () {
box.set({
fillStyle: 'pink',
});
},
onLeave: function () {
box.set({
fillStyle: 'red',
});
}
});
let boxPacket = box.saveAsPacket();
console.log(boxPacket);
| [
| "my-box",
| "Block",
| "entity",
| {
| "name":"my-box",
| "dimensions":[100,50],
| "start":[10,10],
| "delta":{},
| "onEnter":"function () {\n\t\tbox.set({\n\t\t\tfillStyle: 'pink',\n\t\t});\n\t}",
| "onLeave":"function () {\n\t\tbox.set({\n\t\t\tfillStyle: 'red',\n\t\t});\n\t}",
| "group":"mycanvas_base",
| "fillStyle":"black"
| }
| ]
// delete the box
box.kill()
// recreate the entity - we can invoke the function on any handy SC object without affecting that object
let resurrectedBox = canvas.actionPacket(boxPacket);
// recreate the entity - if we had saved the packet to a file on a server
let fetchedBox = canvas.importPacket('https://example.com/path/to/boxPacket.txt');
Internal processing Arrays
P.packetExclusions = [];
P.packetExclusionsByRegex = [];
P.packetCoordinates = [];
P.packetObjects = [];
P.packetFunctions = [];saveAsPacket - accepts an items {key:value} object argument with the following (optional) attributes:
Note: if the argument is supplied as a boolean ‘true’, code will create an items object with an attribute ‘includeDefaults’ set to true.
P.saveAsPacket = function (items = Ωempty) {
if (isa_boolean(items) && items) items = {
includeDefaults: true,
}
const defs = this.defs,
defKeys = _keys(defs),
packetExclusions = this.packetExclusions,
packetExclusionsByRegex = this.packetExclusionsByRegex,
packetCoordinates = this.packetCoordinates,
packetObjects = this.packetObjects,
packetFunctions = this.packetFunctions;
let packetDefaultInclusions = items.includeDefaults || false;
let copy = {};
if (packetDefaultInclusions && !_isArray(packetDefaultInclusions)) {
packetDefaultInclusions = _keys(defs);
}
else if (!packetDefaultInclusions) packetDefaultInclusions = [];
_entries(this).forEach(([key, val]) => {
let flag = true,
test;
if (!defKeys.includes(key)) flag = false;
if (flag && packetExclusions.includes(key)) flag = false;
if (flag) {
test = packetExclusionsByRegex.some(reg => new RegExp(reg).test(key));
if (test) flag = false;
}
if (flag) {
if (packetFunctions.includes(key)) {
if (xt(val) && val != null) {
const func = this.stringifyFunction(val);
if (func && func.length) copy[key] = func;
}
}
else if (packetObjects.includes(key) && this[key] && this[key].name) copy[key] = this[key].name;
else if (packetCoordinates.includes(key)) {
if (packetDefaultInclusions.includes(key)) copy[key] = val;
else if (val[0] || val[1]) copy[key] = val;
}
else {Start cascade down to factory to pick up factory-specific exclusions
test = this.processPacketOut(key, val, packetDefaultInclusions);
if (test) copy[key] = val;
}
}
}, this);Start cascade down to factory to complete the copy object’s build
copy = this.finalizePacketOut(copy, items);Return a JSON string
return JSON.stringify([this.name, this.type, this.lib, copy]);
};Helper functions that get defined in various mixins and factories - localizing the functionality to meet the specific needs of that factory’s object instances
stringifyFunction
P.stringifyFunction = function (val) {The dotAll /s regex flag currently not supported by Firefox let matches = val.toString().match(/((.?)).?{(.*)}/s);
const matches = val.toString().match(/\(([\s\S]*?)\)[\s\S]*?\{([\s\S]*)\}/),
vars = matches[1],
func = matches[2];
return (xta(vars, func)) ? `${vars}${PACKET_DIVIDER}${func}` : false;
};processPacketOut
P.processPacketOut = function (key, value, incl) {
let result = true;
if (!incl.includes(key) && value === this.defs[key]) result = false;
return result;
};finalizePacketOut
P.finalizePacketOut = function (copy) {
return copy;
};importPacket - Import and unpack a string representation of a factory object serialized using the saveAsPacket function.
P.importPacket = function (items) {
const self = this;
const getPacket = function(url) {
return new Promise((resolve, reject) => {
let report;
if (!url.substring) reject(new Error('Packet url supplied for import is not a string'));
if (url[0] === HAS_PACKET_CHECK) {Looks like we already have a packet for processing
report = self.actionPacket(url);
if (report && report.lib) resolve(report);
else reject(report);
}This is not much of a test …
else if (url.includes(BAD_PACKET_CHECK)) {Looks like we have a packet for processing, but it’s malformed
reject(new Error('Bad packet supplied for import'));
}
else {Attempt to fetch the packet from a remote server
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`Packet import from server failed - ${res.status}: ${res.statusText} - ${res.url}`);
return res.text();
})
.then(packet => {
report = self.actionPacket(packet);
if (report && report.lib) resolve(report);
else throw report;
})
.catch(error => reject(error));
}
});
};
if (_isArray(items)) {
const promises = [];
items.forEach(item => promises.push(getPacket(item)));
return new Promise((resolve, reject) => {
Promise.all(promises)
.then(res => resolve(res))
.catch(error => reject(error));
});
}
else if (items.substring) return getPacket(items);
else Promise.reject(new Error('Argument supplied for packet import is not a string or array of strings'));
};actionPacket - This function:
The function can be called directly on any Scrawl-canvas object that uses the base.js mixin - which means that all differing functionality for various types of object have to remain here, in base.js
P.actionPacket = function (packet) {
try {
if (packet && packet.substring) {
if (packet[0] === HAS_PACKET_CHECK) {
let name, type, lib, update;
try {
[name, type, lib, update] = _parse(packet);
}
catch (e) {
throw new Error(`Failed to process packet due to JSON parsing error - ${e.message}`);
}
if (xta(name, type, lib, update)) {
if (TYPE_EXCLUSIONS.includes(type)) {
throw new Error(`Failed to process packet - Stacks, Canvases and visual assets are excluded from the packet system`);
}
let obj = library[lib][name];
if (obj) obj.set(update);
else {Stack-based artefacts need a DOM element that they can pass into the factory
if (update.outerHTML && update.host) {
const myParent = document.querySelector(`#${update.host}`);
if (myParent) {
const tempEl = document.createElement('div');
tempEl.innerHTML = update.outerHTML;
const myEl = tempEl.firstElementChild;
if (myEl) {
myEl.id = name;
myParent.appendChild(myEl);
update.domElement = myEl;
}
}
}
obj = new library.constructors[type](update);
if (!obj) throw new Error('Failed to create Scrawl-canvas object from supplied packet');
}For the main object
obj.packetFunctions.forEach(item => this.actionPacketFunctions(obj, item));For artefact anchors - I know that anchors only have the one function to worry about, but doing it this way so I don’t forget how to approach it eg for SC sub-objects that have more than one user-settable function (eg timeline actions)
if (update.anchor && obj.anchor) {
obj.anchor.packetFunctions.forEach(item => {Anchor.setters.clickAction(arg) explicitly checks that the supplied arg is a function - if it isn’t (like in packet cases) then the attribute doesn’t get updated when we invoke obj.set(update); earlier in this function.
obj.anchor[item] = update.anchor[item];
this.actionPacketFunctions(obj.anchor, item)Anchors are a bit of an exception case because they add a user-interactive and yet otherwise untracked DOM element to the page, which has to be updated in its own sweet, special way…
obj.anchor.build();
});
}Same thing as anchors for artefact buttons
if (update.button && obj.button) {
obj.button.packetFunctions.forEach(item => {
obj.button[item] = update.button[item];
this.actionPacketFunctions(obj.button, item)
obj.button.build();
});
}
if (obj) return obj;
else throw new Error('Failed to process supplied packet');
}
else throw new Error('Failed to process packet - JSON string holds incomplete data');
}
else throw new Error('Failed to process packet - JSON string does not represent an array');
}
else throw new Error('Failed to process packet - not a JSON string');
}
catch (e) { console.log(e); return e }
};actionPacketFunctions - internal helper function - creates functions from Strings
P.actionPacketFunctions = function(obj, item) {
const fItem = obj[item];
if (xt(fItem) && fItem != null && fItem.substring) {
if (fItem === PACKET_DIVIDER) obj[item] = λnull;
else {
let args, func, f;
/* eslint-disable-next-line */
[args, func] = fItem.split(PACKET_DIVIDER);
args = args.split(ARG_SPLITTER);
args = args.map(a => a.trim());Native code raises non-terminal errors (because it is native code!) - so we dodge that bullet.
if (!func.includes(NATIVE_CODE)) {
f = new Function(...args, func);
obj[item] = f.bind(obj);
}
else obj[item] = λnull;
}
}
};Most Scrawl-canvas factory objects can be copied using the clone function. The result will be an exact copy of the original, additionally set with values supplied in the argument object.
scrawl.artefact.myelement.clone({
name: 'myclonedelement',
startY: 60,
});
clone
P.clone = function (items = Ωempty) {
const myName = this.name;
let myPacket, myTicker, myAnchor, myButton;
this.name = items.name || ZERO_STR;Tickers are specific to Tween and Action objects
if (items.useNewTicker) {
myTicker = this.ticker;
this.ticker = null;
myPacket = this.saveAsPacket();
this.ticker = myTicker;
}Everything else
else {Anchors and Buttons are SC artefacts in their own right
if (this.anchor) {
myAnchor = this.anchor;
this.anchor = null;
}
if (this.button) {
myButton = this.button;
this.button = null;
}
myPacket = this.saveAsPacket();
if (myAnchor) this.anchor = myAnchor;
if (myButton) this.button = myButton;
}
this.name = myName;
let clone = this.actionPacket(myPacket);
this.packetFunctions.forEach(func => {
if (this[func]) clone[func] = this[func];
});
clone = this.postCloneAction(clone, items);
clone.set(items);
return clone;
};postCloneAction - overwritten by a variety of mixins and factories
P.postCloneAction = function (clone) {
return clone;
};kill - overwritten by various mixins and factories
P.kill = function () {
return this.deregister();
}makeName - If the developer doesn’t supply a name value for a factory function, then Scrawl-canvas will generate a random name for the object to use.
P.makeName = function (item) {
if (item && item.substring && !library[`${this.lib}names`].includes(item)) this.name = item;
else this.name = generateUniqueString();
return this;
};register - Many (but not all) factory functions will register their result objects in the scrawl lobrary. The section where the object is stored is dependent on the factory function’s type value.
P.register = function () {
if (!xt(this.name)) throw new Error(`core/base error - register() name not set: ${this}`);
const arr = library[`${this.lib}names`],
mylib = library[this.lib];
if(this.isArtefact){
pushUnique(library.artefactnames, this.name);
library.artefact[this.name] = this;
}
if(this.isAsset){
pushUnique(library.assetnames, this.name);
library.asset[this.name] = this;
}
pushUnique(arr, this.name);
mylib[this.name] = this;
return this;
};deregister - Reverse what register() does
P.deregister = function () {
if (!xt(this.name)) throw new Error(`core/base error - deregister() name not set: ${this}`);
const arr = library[`${this.lib}names`],
mylib = library[this.lib];
if(this.isArtefact){
removeItem(library.artefactnames, this.name);
delete library.artefact[this.name];
}
if(this.isAsset){
removeItem(library.assetnames, this.name);
delete library.asset[this.name];
}
removeItem(arr, this.name);
delete mylib[this.name];
return this;
};
}