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

825 lines (580 loc) 22.9 kB
// # Grid factory // The Grid entity is a graphical representation of __a grid with equal width columns and equal height rows__, separated by gutters. Each tile within the grid can be filled with a different color, or a different gradient, or a different Picture entity output. // #### Imports import { constructors, entity } from '../core/library.js'; import { doCreate, isa_number, isa_obj, mergeOver, pushUnique, xt, xta, λnull, Ωempty } from '../helper/utilities.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 { _isArray, _isFinite, _parse, BLACK, COLOR, ENTITY, FILL, SOURCE_IN, SOURCE_OVER, WHITE } from '../helper/shared-vars.js'; // Local constants const _isInteger = Number.isSafeInteger || Number.isInteger, CELL_GRADIENT = 'cellGradient', GRAY = 'rgb(127 127 127 / 1)', GRID_GRADIENT = 'gridGradient', GRID_PICTURE = 'gridPicture', T_GRID = 'Grid', TILE_PICTURE = 'tilePicture'; // #### Grid constructor const Grid = function (items = Ωempty) { this.tileFill = []; this.tileSources = []; this.rowLines = null; this.columnLines = null; this.currentTileWidth = 0; this.currentTileHeight = 0; this.entityInit(items); if (!items.tileSources) { this.tileSources = [].concat([{ type: COLOR, source: BLACK, }, { type: COLOR, source: WHITE, }]); } if (!items.tileFill) { this.tileFill.length = this.columns * this.rows; this.tileFill.fill(0); } else if (_isArray(items.tileFill) && this.tileFill.length === items.tileFill.length) { this.tileFill = items.tileFill; } this.tilePaths = []; this.tileRealCoordinates = []; this.tileVirtualCoordinates = []; if (!items.dimensions) { if (!items.width) this.currentDimensions[0] = this.dimensions[0] = 20; if (!items.height) this.currentDimensions[1] = this.dimensions[1] = 20; } return this; }; // #### Block prototype const P = Grid.prototype = doCreate(); P.type = T_GRID; P.lib = ENTITY; P.isArtefact = true; P.isAsset = false; // #### Mixins baseMix(P); entityMix(P); // #### Grid attributes const defaultAttributes = { // __columns__, __rows__ - integer Numbers representing the number of columns and rows in the Grid columns: 2, rows: 2, // __columnGutterWidth__, __rowGutterWidth__ - float Number distances (measured in px) between tiles in the grid. columnGutterWidth: 1, rowGutterWidth: 1, // __tileSources__ - Array of Javascript Objects // + Each Object describes a source which can be used to fill tiles // + Available fills include: `color`, `cellGradient`, `gridGradient`, `gridPicture`, `tilePicture` tileSources: null, // __tileFill__ - Array of integer Numbers // + Length of the Array will be `rows * columns` // + Tiles are arranged left-to-right, top-to-bottom with tileFill[0] being the top left tile in the grid // + Each Number represents the index of the tileSource object to be used to fill this tile tileFill: null, // __gutterColor__ - can accept the following sources: // + A valid CSS color String // + The name-String of a Scrawl-canvas Gradient or RadialGradient object, or the object itself // + An integer Number representing the index of a tileSource object gutterColor: GRAY, }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management P.packetExclusions = pushUnique(P.packetExclusions, ['tileSources']); P.finalizePacketOut = function (copy, items) { const cSources = copy.tileSources = [], tSources = this.tileSources; tSources.forEach(item => { cSources.push({ type: item.type, source: (isa_obj(item.source)) ? item.source.name : item.source }); }); if (isa_obj(copy.gutterColor)) copy.gutterColor = copy.gutterColor.name; const stateCopy = _parse(this.state.saveAsPacket(items))[3]; copy = mergeOver(copy, stateCopy); copy = this.handlePacketAnchor(copy, items); return copy; }; // #### Clone management // No additional clone functionality required // #### Kill management // No additional kill functionality required // #### Get, Set, deltaSet const S = P.setters, D = P.deltaSetters; // __columns__ S.columns = function (item) { if (isa_number(item)) { if (!_isInteger(item)) item = parseInt(item, 10); if (item !== this.columns) { let i, iz, j; const currentFill = this.tileFill, currentCols = this.columns, newFill = []; this.columns = item; for (i = 0, iz = this.rows; i < iz; i++) { for (j = 0; j < item; j++) { if (j < currentCols) newFill.push(currentFill[(i * currentCols) + j]); else newFill.push(0); } } this.tileFill = newFill; } } this.dirtyPathObject = true; this.dirtyFilterIdentifier = true; }; D.columns = λnull; // __rows__ S.rows = function (item) { if (isa_number(item)) { if (!_isInteger(item)) item = parseInt(item, 10); if (item !== this.rows) { const currentRows = this.rows; this.rows = item; this.tileFill.length = this.columns * item; if (currentRows < item) this.tileFill.fill(0, currentRows * this.columns); } } this.dirtyPathObject = true; this.dirtyFilterIdentifier = true; }; D.rows = λnull; // #### Tile management // `setAllTilesTo` - change the fill for all tiles in a Grid // + Argument is an integer Number representing the index of a tileSource object P.setAllTilesTo = function (val) { if (isa_number(val)) { if (!_isInteger(val)) val = parseInt(val, 10); this.tileFill.fill(val); this.dirtyFilterIdentifier = true; } return this; }; // `setTileFill` - update the tileFill array // + The array supplied as an argument must be the same length as current rows * columns P.setTileFill = function (item) { const { columns, rows } = this; if (_isArray(item) && item.length === columns * rows) { this.tileFill = item; this.dirtyFilterIdentifier = true; } return this; }; // `setTilesTo` - change the fill for a (set of) tile(s) in a Grid - requires two arguments: // + First argument is an integer Number representing the position of a tile in the tileFill Array, or an Array of such Numbers // + Second argument is an integer Number representing the index of a tileSource object P.setTilesTo = function (tiles, val) { const tileFill = this.tileFill; if (xt(tiles) && isa_number(val)) { if (!_isInteger(val)) val = parseInt(val, 10); if (isa_number(tiles)) tileFill[tiles] = val; else if (_isArray(tiles)) { tiles.forEach(tile => { if (isa_number(tile)) tileFill[tile] = val; }); } this.dirtyFilterIdentifier = true; } return this; }; // `setTileSourceTo` - update or replace a tileSource object - requires two arguments: // + First argument is the integer Number index of the tileSource object to be replaced // + Second argument is the new or updated object P.setTileSourceTo = function (index, obj) { if (isa_number(index) && isa_obj(obj)) { if (obj.type && obj.source) this.tileSources[index] = obj; } return this; }; // `removeTileSource` - remove a tileSource object // + Argument is an integer Number representing the index of a tileSource object // + Object will be replaced with `null` P.removeTileSource = function (index) { if (isa_number(index) && index) { this.tileSources[index] = null; this.tileFill = this.tileFill.map(item => item === index ? 0 : item); } return this; }; // `getTileSource` - returns the tileSource index Number for the given tile. Function is overloaded: // + One argument: Number representing the position of a tile in the tileFill Array. // + Two arguments: The `(row, column)` position of the tile in the Grid - both values start from `0` P.getTileSource = function (row, col) { if (isa_number(row)) { if (!isa_number(col)) return this.tileFill[row]; else return this.tileFill[(row * this.rows) + col]; } }; // `getTilesUsingSource` - returns an Array of tileFill index Numbers representing tiles that are currently using the tileSource Object at the given tileSource index. P.getTilesUsingSource = function (key) { const res = []; if (isa_number(key)) this.tileFill.forEach((val, index) => val === key && res.push(index)); return res; }; // `cleanPathObject` - internal - used for entity stamping (Display cycle), and collision detection P.cleanPathObject = function () { this.dirtyPathObject = false; if (!this.noPathUpdates || !this.pathObject) { const p = this.pathObject = new Path2D(), rowLines = new Path2D(), colLines = new Path2D(); const handle = this.currentStampHandlePosition, scale = this.currentScale, dims = this.currentDimensions; const x = -handle[0] * scale, y = -handle[1] * scale, w = dims[0] * scale, h = dims[1] * scale; p.rect(x, y, w, h); const cols = this.columns, rows = this.rows, colWidth = w / cols, rowHeight = h / rows, paths = this.tilePaths, real = this.tileRealCoordinates, virtual = this.tileVirtualCoordinates; let i, j, cx, cy; rowLines.moveTo(x, y); rowLines.lineTo(x + w, y); for (i = 1; i <= rows; i++) { const ry = y + (i * rowHeight); rowLines.moveTo(x, ry); rowLines.lineTo(x + w, ry); } this.rowLines = rowLines; colLines.moveTo(x, y); colLines.lineTo(x, y + h); for (j = 1; j <= cols; j++) { cx = x + (j * colWidth); colLines.moveTo(cx, y); colLines.lineTo(cx, y + h); } this.columnLines = colLines; paths.length = 0; real.length = 0; virtual.length = 0; for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { const path = new Path2D(); cx = j * colWidth; cy = i * rowHeight; path.rect(x + cx, y + cy, colWidth, rowHeight); paths.push(path); virtual.push([cx, cy]); real.push([x + cx, y + cy]); } } this.currentTileWidth = colWidth; this.currentTileHeight = rowHeight; } }; // ##### Stamp methods // `performFill` - internal stamp method helper function // + If you are not a fan of long, complex functions ... look away now! P.performFill = function (engine) { if (this.scale > 0) { // Grab the current engine values for various things engine.save(); const composer = requestCell(), compEngine = composer.engine, compCanvas = composer.element; const tileSources = this.tileSources, tileFill = this.tileFill, tilePaths = this.tilePaths, tileRealCoords = this.tileRealCoordinates, tileVirtualCoords = this.tileVirtualCoordinates, winding = this.winding, tileWidth = this.currentTileWidth, tileHeight = this.currentTileHeight, scale = this.scale; const dims = this.currentDimensions; let currentPicture; // Iterate through the grid's tileSources tileSources.forEach((obj, index) => { // Set up the engine fillStyle value (where required) if (obj && obj.type) { switch (obj.type) { case COLOR : engine.fillStyle = obj.source; break; case CELL_GRADIENT : this.lockFillStyleToEntity = false; engine.fillStyle = obj.source.getData(this, this.currentHost); break; case GRID_GRADIENT : this.lockFillStyleToEntity = true; engine.fillStyle = obj.source.getData(this, this.currentHost); break; } } // Get an map of tiles using this source const validTiles = tileFill.map(item => item === index ? true : false); if (validTiles.length) { switch (obj.type) { // Use pool canvas to compose the output case GRID_PICTURE : currentPicture = (obj.source.substring) ? entity[obj.source] : obj.source; if (currentPicture.simpleStamp) { compCanvas.width = dims[0] * scale; compCanvas.height = dims[1] * scale; compEngine.globalCompositeOperation = SOURCE_OVER; compEngine.fillStyle = BLACK; validTiles.forEach((tile, pos) => { if (tile) compEngine.fillRect(tileVirtualCoords[pos][0], tileVirtualCoords[pos][1], tileWidth, tileHeight); }); compEngine.globalCompositeOperation = SOURCE_IN; currentPicture.simpleStamp(composer, { startX: 0, startY: 0, width: dims[0] * scale, height: dims[1] * scale, method: FILL, }); engine.drawImage(compCanvas, ~~tileRealCoords[0][0], ~~tileRealCoords[0][1]); } break; case TILE_PICTURE : currentPicture = (obj.source.substring) ? entity[obj.source] : obj.source; if (currentPicture.simpleStamp) { compCanvas.width = tileWidth; compCanvas.height = tileHeight; compEngine.globalCompositeOperation = SOURCE_OVER; currentPicture.simpleStamp(composer, { startX: 0, startY: 0, width: tileWidth, height: tileHeight, method: FILL, }); validTiles.forEach((tile, pos) => tile && engine.drawImage(compCanvas, ~~tileRealCoords[pos][0], ~~tileRealCoords[pos][1])); } break; default : validTiles.forEach((tile, pos) => tile && engine.fill(tilePaths[pos], winding)); } } }); const gColor = this.gutterColor, gRow = this.rowGutterWidth, gCol = this.columnGutterWidth; let gObject; if(xt(gColor)) { // Assign (or construct) the appropriate object to gObject if (gColor.substring) { gObject = { type: COLOR, source: this.gutterColor }; } else if (isa_obj(gColor)) gObject = gColor; else if (isa_number(gColor) && isa_obj(tileSources[gColor])) gObject = tileSources[gColor]; // Set the engine's strokeStyle to the appropriate value (if needed) switch (gObject.type) { case CELL_GRADIENT : this.lockFillStyleToEntity = false; engine.strokeStyle = gObject.source.getData(this, this.currentHost); break; case GRID_GRADIENT : this.lockFillStyleToEntity = true; engine.strokeStyle = gObject.source.getData(this, this.currentHost); break; case COLOR : engine.strokeStyle = gObject.source; break; } switch (gObject.type) { // Use pool canvas to compose the output // + gridPicture and tilePicture both treated the same case GRID_PICTURE : case TILE_PICTURE : if(gRow || gCol) { currentPicture = (gObject.source.substring) ? entity[gObject.source] : gObject.source; if (currentPicture.simpleStamp) { const handle = this.currentStampHandlePosition, scale = this.currentScale, x = handle[0] * scale, y = handle[1] * scale; compCanvas.width = dims[0] * scale; compCanvas.height = dims[1] * scale; compEngine.globalCompositeOperation = SOURCE_OVER; compEngine.strokeStyle = BLACK; compEngine.translate(x, y); if (gRow) { compEngine.lineWidth = gRow; compEngine.stroke(this.rowLines); } if (gCol) { compEngine.lineWidth = gCol; compEngine.stroke(this.columnLines); } compEngine.globalCompositeOperation = SOURCE_IN; currentPicture.simpleStamp(composer, { startX: 0, startY: 0, width: dims[0] * scale, height: dims[1] * scale, method: FILL, }); engine.drawImage(compCanvas, ~~tileRealCoords[0][0], ~~tileRealCoords[0][1]); compEngine.translate(0, 0); } } break; // We have a color/gradient all set up - stroke the lines directly onto grid default : if (gRow) { engine.lineWidth = gRow; engine.stroke(this.rowLines); } if (gCol) { engine.lineWidth = gCol; engine.stroke(this.columnLines); } } } releaseCell(composer); engine.restore(); } }; // `fill` P.fill = function (engine) { this.performFill(engine); }; // `drawAndFill` P.drawAndFill = function (engine) { const p = this.pathObject; engine.stroke(p); this.currentHost.clearShadow(); this.performFill(engine); }; // `fillAndDraw` P.fillAndDraw = function (engine) { const p = this.pathObject; engine.stroke(p); this.currentHost.clearShadow(); this.performFill(engine); engine.stroke(p); }; // `drawThenFill` P.drawThenFill = function (engine) { const p = this.pathObject; engine.stroke(p); this.performFill(engine); }; // `fillThenDraw` P.fillThenDraw = function (engine) { const p = this.pathObject; this.performFill(engine); engine.stroke(p); }; // `checkHit` - overrides position mixin function // + Grid entitys need to return ALL of the successful hit coordinates, not just the first // + They also need to include the tile index(es) of where the hit(s) took place within them // // Returns an object with the following attributes // ``` // { // x: x-coordinate of the _last_ successful hit, // y: y-coordinate of the _last_ successful hit, // tiles: Array of tile index Numbers representing each tile reporting a hit, // artefact: the Grid entity object // } // ``` P.checkHit = function (items = []) { if (this.noUserInteraction) return false; if (!this.pathObject || this.dirtyPathObject) { this.cleanPathObject(); } const tests = (!_isArray(items)) ? [items] : items; const mycell = requestCell(), engine = mycell.engine, stamp = this.currentStampPosition, x = stamp[0], y = stamp[1], tiles = new Set(), tilePaths = this.tilePaths; let isGood, tx, ty; const getCoords = (coords) => { let x, y; if (_isArray(coords)) { x = coords[0]; y = coords[1]; } else if (xta(coords, coords.x, coords.y)) { x = coords.x; y = coords.y; } else return [false]; if (!_isFinite(x) || !_isFinite(y)) return [false]; return [true, x, y]; } mycell.rotateDestination(engine, x, y, this); if (tests.some(test => { [isGood, tx, ty] = getCoords(test); if (!isGood) return false; else return engine.isPointInPath(this.pathObject, tx, ty, this.winding); }, this)) { tests.forEach(test => { [isGood, tx, ty] = getCoords(test); if (isGood) { tilePaths.some((path, index) => { if (engine.isPointInPath(path, tx, ty, this.winding)) { tiles.add(index); return true; } return false; }) } }); releaseCell(mycell); return { x: tx, y: ty, tiles: [...tiles], artefact: this }; } releaseCell(mycell); return false; }; // #### Factory // ``` // let blueSource = { // type: 'color', // source: 'aliceblue', // }; // // let myGrid = scrawl.makeGrid({ // // name: 'test-grid', // // startX: 'center', // startY: 'center', // // handleX: 'center', // handleY: 'center', // // width: 300, // height: 200, // // columns: 6, // rows: 6, // // tileSources: [blueSource, { // type: 'color', // source: 'red', // }], // }); // ``` export const makeGrid = function (items) { if (!items) return false; return new Grid(items); }; constructors.Grid = Grid;