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

755 lines (540 loc) 19.8 kB
// # Picture factory // Picture entitys are image, video or canvas-based rectangles rendered onto a DOM &lt;canvas> element using the Canvas API's [CanvasRenderingContext2D interface](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) - in particular the [drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) method. // #### Imports import { artefact, constructors } from '../core/library.js'; import { addStrings, doCreate, isa_obj, mergeOver, pushUnique, removeItem, xta, Ωempty } from '../helper/utilities.js'; import { gettableVideoAssetAtributes, settableVideoAssetAtributes } from '../asset-management/video-asset.js'; import { gettableImageAssetAtributes, settableImageAssetAtributes } from '../asset-management/image-asset.js'; import { makeCoordinate } from '../untracked-factory/coordinate.js'; import baseMix from '../mixin/base.js'; import entityMix from '../mixin/entity.js'; import assetConsumerMix from '../mixin/asset-consumer.js'; // Shared constants import { $IMAGE, $VIDEO, _keys, ENTITY, MOUSE, NAME, PARTICLE, STATE_KEYS, T_PICTURE, T_SPRITE, UNDEF } from '../helper/shared-vars.js'; // Local constants const COPY_DIMENSIONS = 'copyDimensions', COPY_START = 'copyStart'; // #### Picture constructor const Picture = function (items = Ωempty) { this.copyStart = makeCoordinate(); this.currentCopyStart = makeCoordinate(); this.copyDimensions = makeCoordinate(); this.currentCopyDimensions = makeCoordinate(); this.copyArray = []; this.pasteArray = []; this.dirtyPaste = true; this.source = null; this.sourceNaturalWidth = 0; this.sourceNaturalHeight = 0; this.sourceNaturalDimensions = []; this.sourceLoaded = false; this.entityInit(items); if (!items.copyStart) { if (!items.copyStartX) this.copyStart[0] = 0; if (!items.copyStartY) this.copyStart[1] = 0; } if (!items.copyDimensions) { if (!items.copyWidth) this.copyDimensions[0] = 1; if (!items.copyHeight) this.copyDimensions[1] = 1; } this.imageSubscribers = []; this.dirtyCopyStart = true; this.dirtyCopyDimensions = true; this.dirtyImage = true; return this; }; // #### Picture prototype const P = Picture.prototype = doCreate(); P.type = T_PICTURE; P.lib = ENTITY; P.isArtefact = true; P.isAsset = false; // #### Mixins baseMix(P); entityMix(P); assetConsumerMix(P); // #### Picture attributes const defaultAttributes = { // __copyStart__ - Coordinate array // + We can use the pseudo-attributes __copyStartX__ and __copyStartY__ to make working with the Coordinate easier. copyStart: null, // __copyDimensions__ - Coordinate array // + We can use the pseudo-attributes __copyWidth__ and __copyHeight__ to make working with the Coordinate easier. copyDimensions: null, // __checkHitIgnoreTransparency__ - Boolean - when set, will check the stashedImage data to return whether a coordinate is hitting the image; otherwise checkHit will use the Picture entity's dimensions to calculate the hit checkHitIgnoreTransparency: false, // ___Additional attributes and pseudo-attributes___ are defined in the [assetConsumer mixin](../mixin/assetConsumer.html) }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management P.packetCoordinates = pushUnique(P.packetCoordinates, ['copyStart', 'copyDimensions']); P.packetObjects = pushUnique(P.packetObjects, ['asset']); // #### Clone management // No additional clone functionality required // #### Kill management P.factoryKill = function () { const { asset, removeAssetOnKill } = this; if (isa_obj(asset)) { asset.unsubscribe(this); // Cascade kill invocation to the asset object, if required if (removeAssetOnKill) asset.kill(true); } }; // #### Get, Set, deltaSet const G = P.getters, S = P.setters, D = P.deltaSetters; // __copyStart__ // + Including __copyStartX__, __copyStartY__ G.copyStart = function () { return [].concat(this.currentCopyStart); }; G.copyStartX = function () { return this.currentCopyStart[0]; }; G.copyStartY = function () { return this.currentCopyStart[1]; }; S.copyStartX = function (coord) { if (coord != null) { this.copyStart[0] = coord; this.dirtyCopyStart = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; } }; S.copyStartY = function (coord) { if (coord != null) { this.copyStart[1] = coord; this.dirtyCopyStart = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; } }; S.copyStart = function (x, y) { this.setCoordinateHelper(COPY_START, x, y); this.dirtyCopyStart = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; }; D.copyStartX = function (coord) { const c = this.copyStart; c[0] = addStrings(c[0], coord); this.dirtyCopyStart = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; }; D.copyStartY = function (coord) { const c = this.copyStart; c[1] = addStrings(c[1], coord); this.dirtyCopyStart = true; this.dirtyFilterIdentifier = true; }; D.copyStart = function (x, y) { this.setDeltaCoordinateHelper(COPY_START, x, y); this.dirtyCopyStart = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; }; // __copyDimensions__ // + Including __copyWidth__, __copyHeight__ G.copyWidth = function () { return this.currentCopyDimensions[0]; }; G.copyHeight = function () { return this.currentCopyDimensions[1]; }; G.copyDimensions = function () { return [].concat(this.currentCopyDimensions); }; S.copyWidth = function (val) { if (val != null) { this.copyDimensions[0] = val; this.dirtyCopyDimensions = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; } }; S.copyHeight = function (val) { if (val != null) { this.copyDimensions[1] = val; this.dirtyCopyDimensions = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; } }; S.copyDimensions = function (w, h) { this.setCoordinateHelper(COPY_DIMENSIONS, w, h); this.dirtyCopyDimensions = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; }; D.copyWidth = function (val) { const c = this.copyDimensions; c[0] = addStrings(c[0], val); this.dirtyCopyDimensions = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; }; D.copyHeight = function (val) { const c = this.copyDimensions; c[1] = addStrings(c[1], val); this.dirtyCopyDimensions = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; }; D.copyDimensions = function (w, h) { this.setDeltaCoordinateHelper(COPY_DIMENSIONS, w, h); this.dirtyCopyDimensions = true; this.dirtyImageSubscribers = true; this.dirtyFilterIdentifier = true; }; // Picture `get` and `set` (but not `deltaSet`) functions need to take into account their current source, whose attributes can be retrieved/amended directly on the Picture object // `get` P.get = function (item) { const source = this.source; if ((item.indexOf($VIDEO) === 0 || item.indexOf($IMAGE) === 0) && source) { if (gettableVideoAssetAtributes.includes(item)) return source[item.substring(6)]; else if (gettableImageAssetAtributes.includes(item)) return source[item.substring(6)]; } else { const getter = this.getters[item]; if (getter) return getter.call(this); else { const state = this.state; let def = this.defs[item], val; if (typeof def !== UNDEF) { val = this[item]; return (typeof val !== UNDEF) ? val : def; } def = state.defs[item]; if (typeof def !== UNDEF) { val = state[item]; return (typeof val !== UNDEF) ? val : def; } return undefined; } } }; // `set` P.set = function (items = Ωempty) { const keys = _keys(items), keysLen = keys.length; if (keysLen) { const setters = this.setters, defs = this.defs, source = this.source, state = this.state; const stateSetters = (state) ? state.setters : Ωempty; const stateDefs = (state) ? state.defs : Ωempty; let fn, i, key, value; for (i = 0; i < keysLen; i++) { key = keys[i]; value = items[key]; if ((key.indexOf($VIDEO) === 0 || key.indexOf($IMAGE) === 0) && source) { if (settableVideoAssetAtributes.includes(key)) source[key.substring(6)] = value else if (settableImageAssetAtributes.includes(key)) source[key.substring(6)] = value } else if (key && key !== NAME && value != null) { if (!STATE_KEYS.includes(key)) { fn = setters[key]; if (fn) fn.call(this, value); else if (typeof defs[key] !== UNDEF) this[key] = value; } else { fn = stateSetters[key]; if (fn) fn.call(state, value); else if (typeof stateDefs[key] !== UNDEF) state[key] = value; } } } } return this; }; // #### Subscriber management // `updateImageSubscribers` P.updateImageSubscribers = function () { this.dirtyImageSubscribers = false; if (this.imageSubscribers.length) { this.imageSubscribers.forEach(name => { const instance = artefact[name]; if (instance) instance.dirtyInput = true; }); } }; // `imageSubscribe` P.imageSubscribe = function (name) { if (name && name.substring) pushUnique(this.imageSubscribers, name); }; // `imageUnsubscribe` P.imageUnsubscribe = function (name) { if (name && name.substring) removeItem(this.imageSubscribers, name); }; // #### Display cycle functionality // `cleanImage` P.cleanImage = function () { const natWidth = this.sourceNaturalWidth, natHeight = this.sourceNaturalHeight; if (xta(natWidth, natHeight) && natWidth > 0 && natHeight > 0) { this.dirtyImage = false; const start = this.currentCopyStart, x = start[0], y = start[1]; const dims = this.currentCopyDimensions, w = dims[0], h = dims[1]; if (x + w > natWidth) start[0] = natWidth - w; if (y + h > natHeight) start[1] = natHeight - h; const copyArray = this.copyArray; copyArray.length = 0; copyArray.push(~~start[0], ~~start[1], ~~w, ~~h); } }; // `cleanCopyStart` P.cleanCopyStart = function () { const width = this.sourceNaturalWidth, height = this.sourceNaturalHeight; if (xta(width, height) && width > 0 && height > 0) { this.dirtyCopyStart = false; this.cleanPosition(this.currentCopyStart, this.copyStart, [width, height]); const current = this.currentCopyStart, x = current[0], y = current[1]; if (x < 0 || x > width) { if (x < 0) current[0] = 0; else current[0] = width - 1; } if (y < 0 || y > height) { if (y < 0) current[1] = 0; else current[1] = height - 1; } this.dirtyImage = true; } }; // `cleanCopyDimensions` P.cleanCopyDimensions = function () { const natWidth = this.sourceNaturalWidth, natHeight = this.sourceNaturalHeight; if (xta(natWidth, natHeight) && natWidth > 0 && natHeight > 0) { this.dirtyCopyDimensions = false; const dims = this.copyDimensions, currentDims = this.currentCopyDimensions, width = dims[0], height = dims[1]; if (width.substring) currentDims[0] = (parseFloat(width) / 100) * natWidth; else currentDims[0] = width; if (height.substring) currentDims[1] = (parseFloat(height) / 100) * natHeight; else currentDims[1] = height; const currentWidth = currentDims[0], currentHeight = currentDims[1]; if (currentWidth <= 0 || currentWidth > natWidth) { if (currentWidth <= 0) currentDims[0] = 1; else currentDims[0] = natWidth; } if (currentHeight <= 0 || currentHeight > natHeight) { if (currentHeight <= 0) currentDims[1] = 1; else currentDims[1] = natHeight; } this.dirtyImage = true; } }; // `prepareStamp` P.prepareStamp = function() { // The asset itself will update the Picture entity object when changes occur, by setting the entity's `dirtyAsset` flag if (this.dirtyAsset) this.cleanAsset(); // Not content with the dirty flag, the entity now interrogates its asset via its `checkSource` to trigger it to directly rewrite key information if it has changed - particularly dimensional data if (this.asset) { if (this.asset.type === T_SPRITE) this.checkSpriteFrame(this); else { if (this.asset.checkSource) this.asset.checkSource(this.sourceNaturalWidth, this.sourceNaturalHeight); else this.dirtyAsset = true; } } // See the [entity mixin function](http://localhost:8080/docs/source/mixin/entity.html#section-31) for details on the following checks and actions if (this.dirtyDimensions || this.dirtyHandle || this.dirtyScale) this.dirtyPaste = true; if (this.dirtyScale || this.dirtyDimensions || this.dirtyStart || this.dirtyOffset || this.dirtyHandle) this.dirtyPathObject = true; if (this.dirtyScale) this.cleanScale(); if (this.dirtyDimensions) this.cleanDimensions(); if (this.dirtyLock) this.cleanLock(); if (this.dirtyStart) this.cleanStart(); if (this.dirtyOffset) this.cleanOffset(); if (this.dirtyHandle) this.cleanHandle(); if (this.dirtyRotation) this.cleanRotation(); if (this.isBeingDragged || this.lockTo.includes(MOUSE) || this.lockTo.includes(PARTICLE)) { this.dirtyStampPositions = true; this.dirtyStampHandlePositions = true; } if (this.dirtyStampPositions) this.cleanStampPositions(); if (this.dirtyStampHandlePositions) this.cleanStampHandlePositions(); if (this.dirtyCopyStart) this.cleanCopyStart(); if (this.dirtyCopyDimensions) this.cleanCopyDimensions(); if (this.dirtyImage) this.cleanImage(); if (this.dirtyPaste) this.preparePasteObject(); if (this.dirtyPathObject) this.cleanPathObject(); // Update artefacts subscribed to this artefact (using it as their pivot or mimic source), if required if (this.dirtyPositionSubscribers) this.updatePositionSubscribers(); // Specifically for Loom entitys if (this.dirtyImageSubscribers) this.updateImageSubscribers(); // `prepareStampTabsHelper` is defined in the `mixin/hidden-dom-elements.js` file - handles updates to anchor and button objects this.prepareStampTabsHelper(); }; // `preparePasteObject` - internal function // + the __pasteArray__ is a convenience Array containing start coordinate and dimensions data, which we can quickly add to the render engine's drawImage function (which gets called many times) P.preparePasteObject = function () { this.dirtyPaste = false; const handle = this.currentStampHandlePosition, dims = this.currentDimensions, scale = this.currentScale; const x = -handle[0] * scale, y = -handle[1] * scale, w = dims[0] * scale, h = dims[1] * scale; const pasteArray = this.pasteArray; pasteArray.length = 0; pasteArray.push(~~x, ~~y, ~~w, ~~h); this.dirtyPathObject = true; }; // `cleanPathObject` - internal function // + For Picture entitys, the pathObject is a rectangle P.cleanPathObject = function () { this.dirtyPathObject = false; if (!this.noPathUpdates || !this.pathObject) { if (!this.pasteArray || this.pasteArray.length !== 4) this.preparePasteObject(); if (this.pasteArray.length !== 4) this.dirtyPathObject = true; else { const p = this.pathObject = new Path2D(); p.rect(...this.pasteArray); } } }; // ##### Stamp methods // `draw` P.draw = function (engine) { engine.stroke(this.pathObject); }; // `fill` P.fill = function (engine) { const [x, y, w, h] = this.copyArray; if (this.source && w && h) engine.drawImage(this.source, x, y, w, h, ...this.pasteArray); }; // `drawAndFill` P.drawAndFill = function (engine) { const [x, y, w, h] = this.copyArray; const [_x, _y, _w, _h] = this.pasteArray; if (this.source && w && h) { engine.stroke(this.pathObject); engine.drawImage(this.source, x, y, w, h, _x, _y, _w, _h); this.currentHost.clearShadow(); engine.stroke(this.pathObject); engine.drawImage(this.source, x, y, w, h, _x, _y, _w, _h); } }; // `fillAndDraw` P.fillAndDraw = function (engine) { const [x, y, w, h] = this.copyArray; const [_x, _y, _w, _h] = this.pasteArray; if (this.source && w && h) { engine.drawImage(this.source, x, y, w, h, _x, _y, _w, _h); engine.stroke(this.pathObject); this.currentHost.clearShadow(); engine.drawImage(this.source, x, y, w, h, _x, _y, _w, _h); engine.stroke(this.pathObject); } engine.stroke(this.pathObject); }; // `drawThenFill` P.drawThenFill = function (engine) { const [x, y, w, h] = this.copyArray; if (this.source && w && h) { engine.stroke(this.pathObject); engine.drawImage(this.source, x, y, w, h, ...this.pasteArray); } }; // `fillThenDraw` P.fillThenDraw = function (engine) { const [x, y, w, h] = this.copyArray; if (this.source && w && h) { engine.drawImage(this.source, x, y, w, h, ...this.pasteArray); engine.stroke(this.pathObject); } }; // `checkHitReturn` - overwrites mixin/position.js function P.checkHitReturn = function (x, y) { if (this.checkHitIgnoreTransparency) { const img = this.stashedImageData; if (img) { const index = (((y * img.width) + x) * 4) + 3; if (img.data[index]) { return { x: x, y: y, artefact: this }; } } return false; } else { return { x: x, y: y, artefact: this }; } }; // #### Factory // ``` // scrawl.importDomImage('.flowers'); // // scrawl.makePicture({ // // name: 'myFlower', // asset: 'iris', // // width: 200, // height: 200, // // startX: 300, // startY: 200, // handleX: 100, // handleY: 100, // // copyWidth: 200, // copyHeight: 200, // copyStartX: 100, // copyStartY: 100, // // lineWidth: 10, // strokeStyle: 'gold', // // order: 1, // method: 'drawAndFill', // // }).clone({ // // name: 'myFactory', // imageSource: 'img/canalFactory-800.png', // // width: 600, // height: 400, // // startX: 0, // startY: 0, // handleX: 0, // handleY: 0, // // copyWidth: 600, // copyHeight: 400, // copyStartX: 150, // copyStartY: 0, // // order: 0, // method: 'fill', // }); // ``` export const makePicture = function (items) { if (!items) return false; return new Picture(items); }; constructors.Picture = Picture;