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
1,064 lines (781 loc) • 37 kB
JavaScript
// # Styles mixin
// The styles mixin contains most of the code required for the [Gradient](../factory/gradient.html), [RadialGradient](../factory/radialGradient.html) an [ConicGradient](../factory/conicGradient.html) styles factories. It is not used by the other styles objects ([Color](../factory/color.html), [Pattern](../factory/pattern.html)).
// + the __start__ and __end__ positioning attributes are defined here rather than in the factories
// + gradient-type styles manage their color stops in [Palette factory](../factory/palette.html) objects; that functionality is entirely defined here
//
// The Canvas API CanvasRenderingContext2D interface defines three types of gradient: the widely supported [linear](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient) and [radial](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createRadialGradient) gradients, and the [conic](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createConicGradient) which, while widely supported as a CSS feature, is very much an experimental technology for CanvasRenderingContext2D engines.
// + the `createLinearGradient` method creates a gradient along the line connecting two given coordinates (__start__ and __end__) which are absolute values (measured in pixels) from the <canvas> elements top-left corner
// + the `createRadialGradient` method creates a radial gradient using the size and coordinates of two circles.
// + the `createConicGradient` method creates a conic gradient as a 360° sweep around the gradient's start coordinate. For browsers which don't support this gradient out of the box, the method will apply no gradient to the entity.
//
// Common to linear and radial types of gradient is the idea of a start coordinate and an end coordinate, supplied in pixels.
// + Scrawl-canvas extends this idea so that the coordinates can be supplied as a percentage value (String%) of the host Cell's dimensions.
// + Furthermore Scrawl-canvas allows each entity that uses a Gradient-type style to indicate whether the reference box should be that of the host Cell, or of the entity itself, through their `lockFillStyleToEntity` and `lockStrokeStyleToEntity` attribute flags.
// #### Imports
import { entity, styles, stylesnames } from '../core/library.js';
import { addStrings, isa_boolean, generateIdForArtefact, isa_obj, mergeDiscard, mergeOver, pushUnique, removeItem, xt, λnull, Ωempty } from '../helper/utilities.js';
import { makeAnimation } from '../factory/animation.js';
import { makeCoordinate } from '../untracked-factory/coordinate.js';
import { makePalette } from '../untracked-factory/palette.js';
// Shared constants
import { _floor, _isArray, _isFinite, _keys, _values, ADD_EASE, ADD_MAP_CONTOUR, ADD_MAP_DISPLACE, ADD_MAP_EASE, ADD_MAP_FLOW, ADD_MAP_ROTATE, ADD_MAP_THRESHOLD, ADD_MAP_WARP, ADD_NOISE, ADD_RIPPLE, ADD_WAVE, AFTER_SPREAD, BEFORE_SPREAD, BLACK, BLANK, BOTTOM, CENTER, DRAW, END, FILL, LEFT, LINEAR, NAME, ON_COORDINATES, PAD, PERMITTED_NOISE, REFLECT, REPEAT, RGB, RIGHT, START, T_CONIC_GRADIENT, T_GRADIENT, T_PALETTE, T_RADIAL_GRADIENT, TOP, TRANSPARENT, UNDEF, WHITE, ZERO_STR } from '../helper/shared-vars.js';
// ```
// Available gradient operation shapes
// {
// operation: string - one from: 'add-noise'.
// stage: string - one from: 'before-spread', 'after-spread', on-alpha, 'on-coordinates'.
//. parameters: object containing attributes relevant to the given operation
// }
//
// Thus:
//
// {
// operation: 'add-noise',
// stage: 'after-spread', - before-spread, after-spread
// parameters: {
// noise: 'bluenoise', - random, ordered, bluenoise
// strength: 0.05, - 0.01 to 0.1 for reasonable effect
// seed: '', - any string value
// }
// }
//
// {
// operation: 'add-ease',
// stage: 'after-spread', - before-spread, after-spread
// parameters: {
// ease: 'easeOutIn', - any supported SC easing, or bespoke easing function
// }
// }
//
// {
// operation: 'add-wave',
// stage: 'on-coordinates', - on-coordinates only
// parameters: {
// axis: 'x', - x, y, both
// amplitude: 12,
// frequency: 0.05,
// phase: 0,
// }
// }
//
// {
// operation: 'add-ripple',
// stage: 'on-coordinates', - on-coordinates only
// parameters: {
// amplitude: 12,
// frequency: 0.05,
// phase: 0,
// originX: '50%',
// originY: '50%',
// }
// }
//
// {
// operation: 'add-map-displace',
// stage: 'on-coordinates',
// parameters: {
// axis: 'both', - x, y, both
// strength: 20, - pixels
// offset: 0.5, - map centre point
// linked: false, - true = use same map for x/y
// map: {
// noiseEngine: 'simplex', - 'perlin', 'improved-perlin', 'simplex', 'value', 'worley-euclidean', 'worley-manhattan'
// scale: 50, - positive integer number
// octaves: 3, - positive integer number
// }
// }
// }
// ```
// Local constants
const COLORS = 'colors',
PALETTE_KEYS = ['colors', 'stops'],
SPREAD_VALUES = [PAD, REPEAT, REFLECT, TRANSPARENT],
PERMITTED_OPERATIONS = [ADD_EASE, ADD_MAP_CONTOUR, ADD_MAP_DISPLACE, ADD_MAP_EASE, ADD_MAP_FLOW, ADD_MAP_ROTATE, ADD_MAP_THRESHOLD, ADD_MAP_WARP, ADD_NOISE, ADD_RIPPLE, ADD_WAVE],
PERMITTED_STAGES = [BEFORE_SPREAD, AFTER_SPREAD, ON_COORDINATES],
IDENTIFIED_STYLES = [T_GRADIENT, T_RADIAL_GRADIENT, T_CONIC_GRADIENT];
// Create an animation to handle automated delta gradient animation
makeAnimation({
name: 'SC-core-gradient-delta-animation',
fn: () => {
stylesnames.forEach(name => {
const style = styles[name];
if (style && style.animateByDelta) style.updateByDelta();
});
},
});
// #### Export function
export default function (P = Ωempty) {
// #### Shared attributes
const defaultAttributes = {
// __start__, __end__ - Gradient-type styles use Coordinate factory Arrays to hold details of their start and end coordinates. The following _pseudo-attributes_ can also be used to reference these values:
// + for the start coordinate, __startX__ and __startY__
// + for the end coordinate, __endX__ and __endY__
//
// In all cases, the attribute values can be Numbers, which indicate absolute pixel coordinates, or String% values for coordinates calculated relative to either Cell or entity current dimensions
// + for the `x` coordinate, the Strings __left__, __center__ and __right__ are also supported
// + for the `y` coordinate, the Strings __top__, __center__ and __bottom__ are also supported
start: null,
end: null,
// __palette__ - Every gradient requires a Palette object containing color stop instructions. Generation of the object is automated and should ot be tampered with. Add and remove colors to the gradient using the `updateColor` and `removeColor` functions.
palette: null,
// __paletteStart__, __paletteEnd__ - We don't need to use the entire palette when building a gradient; we can restrict the palette using these start and end attributes.
paletteStart: 0,
paletteEnd: 999,
// The __cyclePalette__ attribute tells the Palette object how to handle situations where the paletteStart value is greater than the paletteEnd value:
// + when false, we reverse the color stops
// + when true, we keep the normal order of color stops and pass through the 1/0 border
cyclePalette: false,
// The __animateByDelta__ attribute, when true, will delta animate the gradient at the start of each Display cycle. When the gradient is used in the `mapToGradient`` filter, setting this attribute to `false` (default) should speed up the filter
animateByDelta: false,
// __spread__ - determines how the gradient behaves beyond the border its other attributes set for it. Default is `pad`, which is the only option available to gradients supplied by the Canvas API. Additional spreads include: `repeat`, `reflect`, `transparent`
spread: PAD,
// __operations__ - an array of operation objects. Operation objects define additional parameters that can act on gradient generation and painting
operations: null,
// The __delta__ object is not stored in the defs object; it acts in a similar way to the artefact delta object - though it is restricted to adding delta values to Number and 'String%' attributes.
// + Unlike artefacts, where delta animation will be applied to artefacts by default as part of each Display cycle, gradient delta animations need to be explicitly invoked: `my_gradient.updateByDelta();`
//
// The __colors__ _pseudo-attribute_ can be used to pass through an array of palette color objects to the Palette object. The data is not retained by the gradient object.
// + A better approach to managing gradient colors is to use the `updateColor` and `removeColor` functions
// The __easing__ _pseudo-attribute_ represents a transformation that will be applied to a copy of the color stops Array - this allows us to create non-linear gradients. Value is passed through to the Palette object
// The __precision__ _pseudo-attribute_ - value is passed through to the Palette object
// The __colorSpace__ - String _pseudo-attribute_ defines the color space to be used by the Palette's Color object for its internal calculations - value is passed through to the Palette object
// + Accepted values from: `'RGB', 'HSL', 'HWB', 'XYZ', 'LAB', 'LCH', 'OKLAB', 'OKLCH'` with `RGB` as the default
//
// The __returnColorAs__ - String _pseudo-attribute_ defines the type of color String the Palette's Color object will return - value is passed through to the Palette object
// + Accepted values from: `'RGB', 'HSL', 'HWB', 'LAB', 'LCH', 'OKLAB', 'OKLCH'` with `RGB` as the default
};
P.defs = mergeOver(P.defs, defaultAttributes);
// #### Packet management
P.packetExclusions = pushUnique(P.packetExclusions, ['fillSubscribers', 'drawSubscribers']);
P.finalizePacketOut = function (copy, items) {
if (items.colors) copy.colors = items.colors;
else if (this.palette) copy.colors = this.palette.get(COLORS);
else copy.colors = [[0, BLACK], [999, WHITE]];
// TODO: don't think this will return bespoke easing functions - need to think further on how to accomplish this
if (items.easing) copy.easing = items.easing;
else if (this.palette && this.palette.easing) copy.easing = this.palette.easing;
else copy.easing = LINEAR;
if (xt(items.precision)) copy.precision = items.precision;
else if (this.palette && xt(this.palette.precision)) copy.precision = this.palette.precision;
else copy.precision = 25;
if (items.colorSpace) copy.colorSpace = items.colorSpace;
else if (this.palette) copy.colorSpace = this.palette.getColorSpace();
else copy.colorSpace = RGB;
if (items.returnColorAs) copy.returnColorAs = items.returnColorAs;
else if (this.palette) copy.returnColorAs = this.palette.getReturnColorAs();
else copy.returnColorAs = RGB;
return copy;
};
// #### Clone management
// No additional actions for cloning
// #### Kill management
P.kill = function () {
const myname = this.name;
if (this.palette && this.palette.kill) this.palette.kill();
// Remove style from all entity state objects
_values(entity).forEach(ent => {
const state = ent.state;
if (state) {
const fill = state.fillStyle,
stroke = state.strokeStyle;
if (isa_obj(fill) && fill.name === myname) state.fillStyle = state.defs.fillStyle;
if (isa_obj(stroke) && stroke.name === myname) state.strokeStyle = state.defs.strokeStyle;
}
});
// Remove style from the Scrawl-canvas library
this.deregister();
return this;
};
// #### Get, Set, deltaSet
const G = P.getters,
S = P.setters,
D = P.deltaSetters;
// `start`, `startX`, `startY`
G.startX = function () {
return this.currentStart[0];
};
G.startY = function () {
return this.currentStart[1];
};
S.startX = function (coord) {
if (coord != null) {
this.start[0] = coord;
this.updateSubscribers();
}
};
S.startY = function (coord) {
if (coord != null) {
this.start[1] = coord;
this.updateSubscribers();
}
};
S.start = function (x, y) {
this.setCoordinateHelper(START, x, y);
this.updateSubscribers();
};
D.startX = function (coord) {
const c = this.start;
c[0] = addStrings(c[0], coord);
this.updateSubscribers();
};
D.startY = function (coord) {
const c = this.start;
c[1] = addStrings(c[1], coord);
this.updateSubscribers();
};
D.start = function (x, y) {
this.setDeltaCoordinateHelper(START, x, y);
this.updateSubscribers();
};
// `end`, `endX`, `endY`
G.endX = function () {
return this.currentEnd[0];
};
G.endY = function () {
return this.currentEnd[1];
};
S.endX = function (coord) {
if (coord != null) {
this.end[0] = coord;
this.updateSubscribers();
}
};
S.endY = function (coord) {
if (coord != null) {
this.end[1] = coord;
this.updateSubscribers();
}
};
S.end = function (x, y) {
this.setCoordinateHelper(END, x, y);
this.updateSubscribers();
};
D.endX = function (coord) {
const c = this.end;
c[0] = addStrings(c[0], coord);
this.updateSubscribers();
};
D.endY = function (coord) {
const c = this.end;
c[1] = addStrings(c[1], coord);
this.updateSubscribers();
};
D.end = function (x, y) {
this.setDeltaCoordinateHelper(END, x, y);
this.updateSubscribers();
};
// `palette` - argument has to be a Palette object
S.palette = function (item = Ωempty) {
if(item.type === T_PALETTE) {
item.dirtyPalette = true;
this.palette = item;
this.updateSubscribers();
}
};
// `paletteStart` - argument must be a positive integer Number in the range 0 - 999
S.paletteStart = function (item) {
if (_isFinite(item) && this.palette) {
let p = _floor(item);
if (p < 0 || p > 999) p = (p > 500) ? 999 : 0;
this.paletteStart = p;
this.palette.updateData();
this.updateSubscribers();
}
};
D.paletteStart = function (item) {
if (_isFinite(item) && this.palette) {
let p = _floor(this.paletteStart + item);
if (p < 0 || p > 999) {
if (this.cyclePalette) p = ((p % 1000) + 1000) % 1000;
else p = (p > 500) ? 999 : 0;
}
this.paletteStart = p;
this.palette.updateData();
this.updateSubscribers();
}
};
// `paletteEnd` - argument must be a positive integer Number in the range 0 - 999
S.paletteEnd = function (item) {
if (_isFinite(item) && this.palette) {
let p = _floor(item);
if (p < 0 || p > 999) p = (p > 500) ? 999 : 0;
this.paletteEnd = p;
this.palette.updateData();
this.updateSubscribers();
}
};
D.paletteEnd = function (item) {
if (_isFinite(item) && this.palette) {
let p = _floor(this.paletteEnd + item);
if (p < 0 || p > 999) {
if (this.cyclePalette) p = ((p % 1000) + 1000) % 1000;
else p = (p > 500) ? 999 : 0;
}
this.paletteEnd = p;
this.palette.updateData();
this.updateSubscribers();
}
};
S.cyclePalette = function (item) {
if (this.palette) {
this.cyclePalette = !!item;
this.palette.updateData();
this.updateSubscribers();
}
};
// `colors` - We can pass through an array of palette color arrays to the Palette object by setting it on the gradient-type styles object. Each palette array is in the form `[Number, String]` where:
// + Number is a positive integer in the range 0-999
// + String is any legitimate CSS color string value
S.colors = function (item) {
if (_isArray(item) && this.palette) {
this.palette.set({ colors: item });
this.updateSubscribers();
}
};
// `easing`, `easingFunction` - the easing to be applied to the gradient
// + Can accept a String value identifying an SC pre-defined easing function (default: `linear`)
// + Can also accept a function accepting a single Number argument (a value between 0-1) and returning an eased Number (again, between 0-1)
S.easing = function (item) {
if (this.palette) {
this.palette.set({ easing: item });
this.updateSubscribers();
}
};
S.easingFunction = S.easing;
// `colorSpace`, `returnColorAs` - Pass through and retrieve a color space String to/from the Palette object
G.colorSpace = function () {
if (this.palette) return this.palette.get('colorSpace');
return null;
};
S.colorSpace = function (item) {
if (this.palette) {
this.palette.set({ colorSpace: item });
this.updateSubscribers();
}
};
G.returnColorAs = function () {
if (this.palette) return this.palette.get('returnColorAs');
return null;
};
S.returnColorAs = function (item) {
if (this.palette) {
this.palette.set({ returnColorAs: item });
this.updateSubscribers();
}
};
// `getColorAtPosition` - a convenience function to retrieve the color at a specified position within the current gradient
P.getColorAtPosition = function (val) {
if (this.palette) return this.palette.getColorAtPosition(val);
return null;
};
// `precision` - Pass through a positive integer Number value between 0 and 50 to the Palette object. If value is `0` (default) no easing will be applied to the gradient; values above 0 apply the easing to the gradient; higher values will give a quicker, but less precise, mapping.
S.precision = function (item) {
if (this.palette) {
this.palette.set({ precision: item });
this.updateSubscribers();
}
};
// `delta` - Gradient-type styles objects support the delta attribute, and can be delta-animated using its attributes
S.delta = function (items = Ωempty) {
if (items) {
this.delta = mergeDiscard(this.delta, items);
this.updateSubscribers();
}
};
// `spread` - The spread option for the gradient
// - Permitted values: 'pad', 'repeat', 'reflect', 'transparent'
S.spread = function (item) {
if (SPREAD_VALUES.includes(item)) this.spread = item;
else this.spread = PAD;
this.updateSubscribers();
};
// `operations` - an array of gradient operation objects
S.operations = function (item) {
if (_isArray(item)) this.replaceGradientOperations(item);
else if (isa_obj(item)) this.replaceGradientOperations([item]);
else if (isa_boolean(item) && !item) {
this.operations.length = 0;
this.updateSubscribers();
}
};
P.checkGradientOperation = function (item) {
if (isa_obj(item)) {
if (!item.stage || !item.operation || !item.parameters) return false;
if (!isa_obj(item.parameters)) return false;
if (!PERMITTED_STAGES.includes(item.stage)) return false;
if (!PERMITTED_OPERATIONS.includes(item.operation)) return false;
if (item.operation === ADD_NOISE) {
if (!PERMITTED_NOISE.includes(item.parameters.noise)) return false;
}
return true;
}
return false;
}
P.addGradientOperation = function (item) {
if (this.checkGradientOperation(item)) {
this.operations.push(item);
}
this.updateSubscribers();
};
P.replaceGradientOperations = function (item) {
if (_isArray(item)) {
this.operations.length = 0;
item.forEach(action => {
if (this.checkGradientOperation(action)) this.operations.push(action);
});
this.updateSubscribers();
}
};
P.removeGradientOperationByIndex = function (index) {
index = _floor(index);
if (_isFinite(index) && index >= 0 && index < this.operations.length) {
this.operations.splice(index, 1);
this.updateSubscribers();
}
};
P.removeGradientOperationsFromStage = function (stage) {
if (PERMITTED_STAGES.includes(stage)) {
const len = this.operations.length;
this.operations = this.operations.filter(op => op.stage !== stage);
if (this.operations.length !== len) this.updateSubscribers();
}
};
P.removeGradientOperationByValues = function (pos) {
console.log('removeGradientOperationByValues', pos)
this.updateSubscribers();
};
P.clearGradientOperations = function () {
if (this.operations.length) {
this.operations.length = 0;
this.updateSubscribers();
}
};
// #### Prototype functions
// `get` - Overwrites function defined in mixin/base.js - takes into account Palette object attributes
P.get = function (item) {
const getter = this.getters[item],
palette = this.palette;
if (getter) return getter.call(this);
else {
let def = this.defs[item],
val;
if (typeof def !== UNDEF) {
val = this[item];
return (typeof val !== UNDEF) ? val : def;
}
if (palette) {
def = palette.defs[item];
if (typeof def !== UNDEF) {
val = palette[item];
return (typeof val !== UNDEF) ? val : def;
}
}
}
return undefined
};
// `set` - Overwrites function defined in mixin/base.js - takes into account Palette object attributes
P.set = function (items = Ωempty) {
const keys = _keys(items),
keysLen = keys.length;
if (keysLen) {
const setters = this.setters,
defs = this.defs,
palette = this.palette;
let paletteSetters, paletteDefs, predefined, i, key, value;
if (palette) {
paletteSetters = palette.setters || Ωempty;
paletteDefs = palette.defs || Ωempty;
}
for (i = 0; i < keysLen; i++) {
key = keys[i];
value = items[key];
if (key && key !== NAME && value != null) {
if (!PALETTE_KEYS.includes(key)) {
predefined = setters[key];
if (predefined) predefined.call(this, value);
else if (typeof defs[key] !== UNDEF) this[key] = value;
}
else {
predefined = paletteSetters[key];
if (predefined) predefined.call(palette, value);
else if (typeof paletteDefs[key] !== UNDEF) palette[key] = value;
}
}
}
this.dirtyFilterIdentifier = true;
}
return this;
};
// `setDelta` - Overwrites function defined in mixin/base.js - takes into account Palette object attributes
P.setDelta = function (items = Ωempty) {
const keys = _keys(items),
keysLen = keys.length;
if (keysLen) {
const setters = this.deltaSetters,
defs = this.defs,
palette = this.palette;
let paletteSetters, paletteDefs, predefined, i, key, value;
if (palette) {
paletteSetters = palette.deltaSetters || Ωempty;
paletteDefs = palette.defs || Ωempty;
}
for (i = 0; i < keysLen; i++) {
key = keys[i];
value = items[key];
if (key && key !== NAME && value != null) {
if (!PALETTE_KEYS.includes(key)) {
predefined = setters[key];
if (predefined) predefined.call(this, value);
else if (typeof defs[key] !== UNDEF) this[key] = addStrings(this[key], value);
}
else {
predefined = paletteSetters[key];
if (predefined) predefined.call(palette, value);
else if (typeof paletteDefs[key] !== UNDEF) palette[key] = addStrings(palette[key], value);
}
}
}
this.dirtyFilterIdentifier = true;
}
return this;
};
// `setCoordinateHelper` - internal helper function
P.setCoordinateHelper = function (label, x, y) {
const c = this[label];
if (_isArray(x)) {
c[0] = x[0];
c[1] = x[1];
}
else {
c[0] = x;
c[1] = y;
}
};
// `setDeltaCoordinateHelper` - internal helper function
P.setDeltaCoordinateHelper = function (label, x, y) {
const c = this[label],
myX = c[0],
myY = c[1];
if (_isArray(x)) {
c[0] = addStrings(myX, x[0]);
c[1] = addStrings(myY, x[1]);
}
else {
c[0] = addStrings(myX, x);
c[1] = addStrings(myY, y);
}
};
// `updateByDelta` - manually force the gradient-type styles object to update its attributes by the values supplied in its delta attribute
P.updateByDelta = function () {
this.setDelta(this.delta);
return this;
};
// `stylesInit` - common functionality invoked by gradient-type factory constructors
P.stylesInit = function (items = Ωempty) {
this.makeName(items.name);
this.register();
this.gradientArgs = [];
this.start = makeCoordinate();
this.end = makeCoordinate();
this.currentStart = makeCoordinate();
this.currentEnd = makeCoordinate();
this.palette = makePalette({
name: `${this.name}_palette`,
});
this.delta = {};
this.fillSubscribers = [];
this.drawSubscribers = [];
this.operations = [];
this.isClassic = true;
this.set(this.defs);
this.set(items);
this.identifier = ZERO_STR;
this.updateIdentifier();
};
// `getData` - Every styles object (Gradient, RadialGradient, ConicGradient, Pattern, Color, Cell) needs to include a __getData__ function. This is invoked by Cell objects during the Display cycle `compile` step, when it takes an entity State object and updates its <canvas> element's context engine to bring it into alignment with requirements.
P.getData = function (myentity, mycell, area) {
// Step 1: recalculate current start and end points
this.cleanStyle(myentity, mycell, area);
// Step 2: finalize the coordinates to use for creating the gradient in relation to the current entity's position and requirements on the canvas
this.finalizeCoordinates(myentity, area);
// Step 3: create, populate and return gradient/pattern object
return this.buildStyle(mycell, myentity, area);
};
// `cleanStyle` - internal function invoked as part of the gradient-type object's `getData` function. The style has to be cleaned every time it is applied to a Cell's engine because it can never know which Cell is invoking it, or for which entity it is to be used.
P.cleanStyle = function (entity = Ωempty, cell = Ωempty, area) {
let dims, w, h, scale;
if (
(area === FILL && entity.lockFillStyleToEntity) ||
(area === DRAW && entity.lockStrokeStyleToEntity)
) {
dims = entity.currentDimensions;
scale = entity.currentScale;
w = dims[0] * scale;
h = dims[1] * scale;
}
else {
dims = cell.currentDimensions || [cell.element.width, cell.element.height];
w = dims[0];
h = dims[1];
}
this.cleanPosition(this.currentStart, this.start, [w, h]);
this.cleanPosition(this.currentEnd, this.end, [w, h]);
this.cleanRadius(w);
};
// `cleanPosition` - internal function invoked as part of the gradient-type object's `cleanStyle` function.
P.cleanPosition = function (current, source, dimensions) {
let val, dim;
for (let i = 0; i < 2; i++) {
val = source[i];
dim = dimensions[i];
if (val.toFixed) current[i] = val;
else if (val === LEFT || val === TOP) current[i] = 0;
else if (val === RIGHT || val === BOTTOM) current[i] = dim;
else if (val === CENTER) current[i] = dim / 2;
else if (!_isFinite(parseFloat(val))) current[i] = 0;
else current[i] = (parseFloat(val) / 100) * dim;
}
};
// `finalizeCoordinates` - internal function invoked as part of the gradient-type object's `getData` function.
P.finalizeCoordinates = function (entity = Ωempty, area) {
const entityStampPosition = entity.currentStampPosition,
entityStampHandlePosition = entity.currentStampHandlePosition,
entityScale = entity.currentScale;
let correctX, correctY;
if (
(area === FILL && entity.lockFillStyleToEntity) ||
(area === DRAW && entity.lockStrokeStyleToEntity)
) {
correctX = -(entityStampHandlePosition[0] * entityScale) || 0;
correctY = -(entityStampHandlePosition[1] * entityScale) || 0;
this.updateGradientArgs(correctX, correctY, 0, entityScale);
}
else {
correctX = -entityStampPosition[0] || 0;
correctY = -entityStampPosition[1] || 0;
this.updateGradientArgs(correctX, correctY, -entity.currentRotation, 1);
}
};
// `cleanRadius` - overwritten by the RadialGradient factory
P.cleanRadius = λnull;
// `buildStyle` - Just in case something went wrong with loading other styles Factory modules, which must overwrite this function, we can return transparent color here
P.buildStyle = function () {
console.log('styles mixin P.buildStyle')
return BLANK;
};
// `addStopsToGradient` - internal function, called by the `buildStyle` function (which is overwritten by the various gradient factories)
P.addStopsToGradient = function (gradient, start, stop, cycle) {
if (this.palette) return this.palette.addStopsToGradient(gradient, start, stop, cycle);
return gradient;
};
// #### Gradients and color stops
// The [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) uses a rather convoluted way to add color data to a [CanvasGradient](https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient) interface object:
// + the object is created first on the <canvas> context engine where it is to be applied, with __start__ and __end__ coordinates,
// + then color stops are _individually_ added to it afterwards.
// + This needs to be done for every gradient applied to a context engine before any fill or stroke operation using that gradient.
// + And only one gradient may be applied to the context engine at any time.
//
// The specificity of the above requirements - in particular relating to position coordinates - and the inability to update the CanvasGradient beyond adding color stops to it, means that storing these objects for future use is not a useful proposition ... especially in a dynamic environment where we want the gradient to move in-step with an entity, or animate its colors in some way.
//
// Scrawl-canvas overcomes this problem through the use of [Palette objects](../factory/palette.html) which separate a gradient-type style's color-stop data from its positioning data. We treat Canvas API `CanvasGradient` objects as use-once-and-dispose objects, generating them in a just-in-time fashion for each entity's `stamp` operation in the Display cycle.
//
// Palette objects store their color data in a `colors` attribute object:
// ```
// {
// name: "mygradient_palette",
// colors: {
// "0 ": [0, 0, 0, 1],
// "350 ": [255, 0, 0, 1],
// "650 ": [0, 0, 255, 1],
// "999 ": [255, 255, 255, 1],
// "index-label-between-0-and-999 ": [redValue, greenValue, blueValue, alphaValue]
// },
// }
// ```
// To `set` the Palette object's `colors` object, either when creating the gradient-type style or at some point afterwards, we can use CSS color Strings instead of an array of values for each color. Note that:
// + the color object attribute labels MUST include a space after the String number; and
// + the number itself must be a positive integer in the range 0-999:
//
// ```
// myGradient.set({
//
// colors: {
//
// '0 ': 'black',
// '350 ': 'red',
// '650 ': 'blue',
// '999 ': 'white'
// },
// });
// ```
//
// The following __convenience functions__ are supplied to make adding, deleting and managing Palette object color stop data easier than the above:
// `updateColor` - add or update a gradient-type style's Palette object with a color.
// + __index__ - positive integer Number between 0 and 999 inclusive
// + __color__ - CSS color String
P.updateColor = function (index, color) {
if (this.palette) {
this.dirtyFilterIdentifier = true;
this.palette.updateColor(index, color);
this.updateSubscribers();
}
return this;
};
// `removeColor` - remove a gradient-type style's Palette object color from a specified index
// + __index__ - positive integer number between 0 and 999 inclusive
P.removeColor = function (index) {
if (this.palette) {
this.dirtyFilterIdentifier = true;
this.palette.removeColor(index);
this.updateSubscribers();
}
return this;
};
// `fillSubscribe`, `fillUnsubscribe`, `drawSubscribe`, `drawUnsubscribe` - we only keep the names of subscribing entitys in these arrays
P.fillSubscribe = function (name) {
if (name.substring) pushUnique(this.fillSubscribers, name);
};
P.fillUnsubscribe = function (name) {
if (name.substring) removeItem(this.fillSubscribers, name);
};
P.drawSubscribe = function (name) {
if (name.substring) pushUnique(this.drawSubscribers, name);
};
P.drawUnsubscribe = function (name) {
if (name.substring) removeItem(this.drawSubscribers, name);
};
// `updateSubscribers`, `updateFillSubscribers`, `updateDrawSubscribers` - signal to subscribers that something about the gradient has changed
P.updateSubscribers = function () {
this.updateFillSubscribers();
this.updateDrawSubscribers();
// We only update subscribers when something has changed
// + So we might as well update the identifier here as well
this.updateIdentifier()
};
P.updateFillSubscribers = function () {
let ent;
this.fillSubscribers.forEach(sub => {
ent = entity[sub];
if (ent) ent.dirtyFillGradient = true;
});
};
P.updateDrawSubscribers = function () {
let ent;
this.drawSubscribers.forEach(sub => {
ent = entity[sub];
if (ent) ent.dirtyDrawGradient = true;
});
};
// P.updateIdentifier = function () {
// if (IDENTIFIED_STYLES.includes(this.type)) {
// if (this.spread === PAD && !this.operations.length) this.identifier = ZERO_STR;
// else this.identifier = generateIdForArtefact(this);
// }
// else this.identifier = ZERO_STR;
// };
P.updateIdentifier = function () {
this.identifier = generateIdForArtefact(this);
if (IDENTIFIED_STYLES.includes(this.type)) this.isClassic = (this.spread === PAD && !this.operations.length);
else this.isClassic = true;
};
}