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
478 lines (329 loc) • 13.1 kB
JavaScript
// # Crescent factory
// Crescent entitys are formed by the intersection of two circles, rendered onto a DOM <canvas> element using the Canvas 2D API's [Path2D interface](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) - specifically the arc() method.
// #### Imports
import { constructors } from '../core/library.js';
import { addStrings, doCreate, mergeOver, xta, xto, Ωempty } from '../helper/utilities.js';
import { releaseCoordinate, requestCoordinate } from '../untracked-factory/coordinate.js';
import { releaseCell, requestCell } from '../untracked-factory/cell-fragment.js';
import baseMix from '../mixin/base.js';
import entityMix from '../mixin/entity.js';
// Shared constants
import { _radian, DESTINATION_OUT, ENTITY } from '../helper/shared-vars.js';
// Local constants
const T_CRESCENT = 'Crescent';
// #### Crescent constructor
const Crescent = function (items = Ωempty) {
if (!xto(items.dimensions, items.width, items.height, items.radius)) items.radius = 5;
this.currentOuterRadius = 5;
this.currentInnerRadius = 5;
this.currentDisplacement = 0;
this.currentDimensions = [];
this.outerCircleStart = 0;
this.outerCircleEnd = 0;
this.innerCircleStart = 0;
this.innerCircleEnd = 0;
this.drawOuterCircle = false;
this.drawDonut = false;
this.pathObjectOuter = null;
this.pathObjectInner = null;
this.entityInit(items);
return this;
};
// #### Crescent prototype
const P = Crescent.prototype = doCreate();
P.type = T_CRESCENT;
P.lib = ENTITY;
P.isArtefact = true;
P.isAsset = false;
// #### Mixins
baseMix(P);
entityMix(P);
// #### Crescent attributes
const defaultAttributes = {
// __outerRadius__, __innerRadius__ - the circles' radius measured in Number pixels, or as a String% - `'10%'` - of the Cell's width
outerRadius: 20,
innerRadius: 10,
// __displacement__ - the displacement (rightwards) of the inner circle relative to the outer circle. Measured in positive Number pixels; negative values will be set to `0`
displacement: 0,
// __displayIntersect__ - a Boolean flag determining which of the two parts of the combined circles to display. Defaults to `false`, which shows the portion of the outer circle not covered by the inner circle; set to `true`, will display the intersection of the inner and outer circles
displayIntersect: false,
};
P.defs = mergeOver(P.defs, defaultAttributes);
// #### Packet management
// No additional packet functionality required
// #### Clone management
// No additional clone functionality required
// #### Kill management
// No additional kill functionality required
// #### Get, Set, deltaSet
const S = P.setters,
D = P.deltaSetters;
// __width__ and __height__ (and dimensions) values are largely irrelevant to Crescent entitys; they get used internally purely as part of the Display cycle stamp functionality.
// If they are used to (delta)set the entity's values then outerRadius will be set to the supplied width value with String% values calculated from the entity's host's width, while innerRadius will be set to the supplied height value with String% values calculated from the entity's host's height
S.outerRadius = function (val) {
if (val != null) {
this.outerRadius = val;
this.dirtyDimensions = true;
this.dirtyFilterIdentifier = true;
}
};
D.outerRadius = function (val) {
if (val != null) {
this.outerRadius = addStrings(this.outerRadius, val);
this.dirtyDimensions = true;
this.dirtyFilterIdentifier = true;
}
};
S.innerRadius = function (val) {
if (val != null) {
this.innerRadius = val;
this.dirtyDimensions = true;
this.dirtyFilterIdentifier = true;
}
};
D.innerRadius = function (val) {
if (val != null) {
this.innerRadius = addStrings(this.innerRadius, val);
this.dirtyDimensions = true;
this.dirtyFilterIdentifier = true;
}
};
S.width = S.outerRadius;
D.width = D.outerRadius;
S.height = S.innerRadius;
D.height = D.innerRadius;
S.displacement = function (val) {
if (val != null && val.toFixed && val >= 0) {
let d = val;
if (d < 0) d = 0;
this.displacement = d;
this.dirtyDimensions = true;
this.dirtyFilterIdentifier = true;
}
};
D.displacement = function (val) {
if (val != null && val.toFixed) {
let d = addStrings(this.displacement, val);
if (d.toFixed && d < 0) d = 0;
this.displacement = d;
this.dirtyDimensions = true;
this.dirtyFilterIdentifier = true;
}
};
S.displayIntersect = function (val) {
this.displayIntersect = val;
this.dirtyPathObject = true;
this.dirtyFilterIdentifier = true;
};
// #### Prototype functions
// Dimensions calculations - overwrites mixin/position.js function
P.cleanDimensionsAdditionalActions = function () {
const { outerRadius:oR, innerRadius:iR, displacement:disp } = this;
const host = this.getHost();
let hostDims;
if (host) hostDims = (host.currentDimensions) ? host.currentDimensions : [host.w, host.h];
else hostDims = [300, 150];
const [w, h] = hostDims;
this.currentOuterRadius = (oR.substring) ? (parseFloat(oR) / 100) * w : oR;
this.currentInnerRadius = (iR.substring) ? (parseFloat(iR) / 100) * h : iR;
this.currentDisplacement = (disp.substring) ? (parseFloat(disp) / 100) * w : disp;
this.currentDimensions[0] = this.currentDimensions[1] = this.currentOuterRadius * 2;
this.dirtyPathObject = true;
};
// `calculateInterception` - internal function to calculate how the two circles interact
P.calculateInterception = function () {
if (!xta(this.currentOuterRadius, this.currentInnerRadius, this.currentDisplacement)) this.cleanDimensionsAdditionalActions();
const { currentOuterRadius:oR, currentInnerRadius:iR, currentDisplacement:disp } = this;
this.outerCircleStart = 0;
this.outerCircleEnd = 360 * _radian;
this.innerCircleStart = 0;
this.innerCircleEnd = 360 * _radian;
this.drawOuterCircle = false;
this.drawDonut = false;
const dMax = oR + iR,
dMin = oR - iR;
const equalCircles = (!dMin) ? true : false;
if (equalCircles && !disp) this.drawOuterCircle = true;
else {
if (disp >= dMax) this.drawOuterCircle = true;
else if (disp < dMax && disp > dMin) {
const cell = requestCell();
const {engine, element} = cell;
const v = requestCoordinate();
let a, b;
// Decided to calculate the start/end angles for each circle through brute force
// + Trigonometry is the proper answer, but I can't get the equations to stay still and play nicely
// + So instead we draw circles on a canvas and rotate a vector to see when it enters/leaves the circles
/* eslint-disable-next-line */
element.width = element.width;
engine.fillStyle = 'black';
engine.save();
engine.beginPath();
engine.arc(0, 0, oR, 0, Math.PI * 2);
v.setFromArray([iR, 0]);
for (a = 0; a < 360; a += 0.5) {
v.rotate(0.5);
if(engine.isPointInPath(v[0] + disp, v[1])) break;
}
engine.restore();
engine.save();
engine.beginPath();
engine.arc(disp, 0, iR, 0, Math.PI * 2);
v.setFromArray([oR, 0]);
for (b = 0; b < 360; b += 0.5) {
v.rotate(0.5);
if(!engine.isPointInPath(...v)) break;
}
engine.restore();
this.outerCircleStart = -b * _radian;
this.outerCircleEnd = b * _radian;
this.innerCircleStart = a * _radian;
this.innerCircleEnd = -a * _radian;
releaseCoordinate(v);
releaseCell(cell);
}
else this.drawDonut = true;
}
};
// Calculate the Crescent entity's __Path2D object__
P.cleanPathObject = function () {
this.dirtyPathObject = false;
if (!this.noPathUpdates || !this.pathObject) {
this.calculateInterception();
const { currentStampHandlePosition:handle, currentScale:scale, outerCircleStart:ocs, outerCircleEnd:oce, innerCircleStart:ics, innerCircleEnd:ice, drawOuterCircle, displayIntersect } = this;
let { currentOuterRadius:oR, currentInnerRadius:iR, currentDisplacement:disp } = this;
const p = this.pathObject = new Path2D();
oR *= scale;
iR *= scale;
disp *= scale;
const x = oR - (handle[0] * scale),
y = oR - (handle[1] * scale);
if (drawOuterCircle) {
p.arc(x, y, oR, ocs, oce);
p.closePath();
this.pathObjectOuter = false;
this.pathObjectInner = false;
}
else {
const pOuter = this.pathObjectOuter = new Path2D();
const pInner = this.pathObjectInner = new Path2D();
if (displayIntersect) p.arc(x, y, oR, ocs, oce);
else p.arc(x, y, oR, ocs, oce, true);
p.arc(x + disp, y, iR, ics, ice);
p.closePath();
pOuter.arc(x, y, oR, ocs, oce, true);
pOuter.closePath();
pInner.arc(x + disp, y, iR, ics, ice);
pInner.closePath();
}
}
};
// `draw` - stroke the entity outline with the entity's `strokeStyle` color, gradient or pattern - including shadow
P.draw = function (engine) {
if (!this.drawDonut) engine.stroke(this.pathObject);
else {
engine.stroke(this.pathObjectOuter);
engine.stroke(this.pathObjectInner);
}
};
// `fill` - fill the entity with the entity's `fillStyle` color, gradient or pattern - including shadow
P.fill = function (engine) {
engine.fill(this.pathObject, this.winding);
};
// `drawAndFill` - stamp the entity stroke, then fill, then remove shadow and repeat
P.drawAndFill = function (engine) {
if (!this.drawDonut) {
const p = this.pathObject;
engine.stroke(p);
engine.fill(p, this.winding);
this.currentHost.clearShadow();
engine.stroke(p);
engine.fill(p, this.winding);
}
else {
const p = this.pathObject,
pOuter = this.pathObjectOuter,
pInner = this.pathObjectInner;
engine.stroke(pOuter);
engine.stroke(pInner);
engine.fill(p, this.winding);
this.currentHost.clearShadow();
engine.stroke(pOuter);
engine.stroke(pInner);
engine.fill(p, this.winding);
}
};
// `drawAndFill` - stamp the entity fill, then stroke, then remove shadow and repeat
P.fillAndDraw = function (engine) {
if (!this.drawDonut) {
const p = this.pathObject;
engine.fill(p, this.winding);
engine.stroke(p);
this.currentHost.clearShadow();
engine.fill(p, this.winding);
engine.stroke(p);
}
else {
const p = this.pathObject,
pOuter = this.pathObjectOuter,
pInner = this.pathObjectInner;
engine.fill(p, this.winding);
engine.stroke(pOuter);
engine.stroke(pInner);
this.currentHost.clearShadow();
engine.fill(p, this.winding);
engine.stroke(pOuter);
engine.stroke(pInner);
}
};
// `drawThenFill` - stroke the entity's outline, then fill it (shadow applied twice)
P.drawThenFill = function (engine) {
if (!this.drawDonut) {
const p = this.pathObject;
engine.stroke(p);
engine.fill(p, this.winding);
}
else {
const p = this.pathObject,
pOuter = this.pathObjectOuter,
pInner = this.pathObjectInner;
engine.stroke(pOuter);
engine.stroke(pInner);
engine.fill(p, this.winding);
}
};
// `fillThenDraw` - fill the entity's outline, then stroke it (shadow applied twice)
P.fillThenDraw = function (engine) {
if (!this.drawDonut) {
const p = this.pathObject;
engine.fill(p, this.winding);
engine.stroke(p);
}
else {
const p = this.pathObject,
pOuter = this.pathObjectOuter,
pInner = this.pathObjectInner;
engine.fill(p, this.winding);
engine.stroke(pOuter);
engine.stroke(pInner);
}
};
// `clip` - restrict drawing activities to the entity's enclosed area
P.clip = function (engine) {
engine.clip(this.pathObject, this.winding);
};
// `clear` - remove everything that would have been covered if the entity had performed fill (including shadow)
P.clear = function (engine) {
const gco = engine.globalCompositeOperation;
engine.globalCompositeOperation = DESTINATION_OUT;
engine.fill(this.pathObject, this.winding);
engine.globalCompositeOperation = gco;
};
// #### Factory
// ```
// ```
export const makeCrescent = function (items) {
if (!items) return false;
return new Crescent(items);
};
constructors.Crescent = Crescent;