UNPKG

scrawl-canvas

Version:
842 lines (598 loc) 25.1 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. // + Each grid requires a mimimum of one column and one row; columns will divide into the Grid width evenly, rows divide evenly into the Grid height. // + The __tiles__ can display as many different colors, or gradients, or Pictures, as are required for the display. // + The __gutters__ between columns, and between rows, can be different values; gutter fills can also be colors, gradients or Picture entity output. // + The display for each tile, and the gutters, can be easily updated. // + Grid entity __hit reports__ include an extra attribute - an array of tiles that report being hit. // + Grids will accept __Filter__ objects - filters are applied to the entire Grid, not to individual tiles. // + They can use __Anchor__ objects for user navigation. // + They can be rendered to the canvas by including them in a __Cell__ object's __Group__. // + They can be __animated__ directly, or using delta animation, or act as the target for __Tween__ animations. // + Grids can be cloned, and killed. // #### Demos: // + [Canvas-022](../../demo/canvas-022.html) - Grid entity - basic functionality (color, gradients) // + [Canvas-023](../../demo/canvas-023.html) - Grid entity - using picture-based assets (image, video, sprite) // + [Packets-002](../../demo/packets-002.html) - Scrawl-canvas packets - save and load a range of different entitys // #### Imports import { constructors, entity } from '../core/library.js'; import { mergeOver, pushUnique, isa_number, isa_obj, λnull, xt, xta, Ωempty } from '../core/utilities.js'; import { requestCell, releaseCell } from './cell.js'; import baseMix from '../mixin/base.js'; import entityMix from '../mixin/entity.js'; // #### Grid constructor const Grid = function (items = Ωempty) { this.tileFill = []; this.tileSources = []; this.entityInit(items); if (!items.tileSources) { this.tileSources = [].concat([{ type: 'color', source: '#000000', }, { type: 'color', source: '#ffffff', }]); } if (!items.tileFill) { this.tileFill.length = this.columns * this.rows; this.tileFill.fill(0); } else if (Array.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 let P = Grid.prototype = Object.create(Object.prototype); P.type = 'Grid'; P.lib = 'entity'; P.isArtefact = true; P.isAsset = false; // #### Mixins // + [base](../mixin/base.html) // + [entity](../mixin/entity.html) P = baseMix(P); P = entityMix(P); // #### Grid attributes // + Attributes defined in the [base mixin](../mixin/base.html): __name__. // + Attributes defined in the [position mixin](../mixin/position.html): __group, visibility, order, calculateOrder, stampOrder, start, _startX_, _startY_, handle, _handleX_, _handleY_, offset, _offsetX_, _offsetY_, dimensions, _width_, _height_, pivoted, mimicked, lockTo, _lockXTo_, _lockYTo_, scale, roll, noUserInteraction, noPositionDependencies, noCanvasEngineUpdates, noFilters, noPathUpdates, purge, bringToFrontOnDrag__. // + Attributes defined in the [delta mixin](../mixin/delta.html): __delta, noDeltaUpdates__. // + Attributes defined in the [pivot mixin](../mixin/pivot.html): __pivot, pivotCorner, addPivotHandle, addPivotOffset, addPivotRotation__. // + Attributes defined in the [mimic mixin](../mixin/mimic.html): __mimic, useMimicDimensions, useMimicScale, useMimicStart, useMimicHandle, useMimicOffset, useMimicRotation, useMimicFlip, addOwnDimensionsToMimic, addOwnScaleToMimic, addOwnStartToMimic, addOwnHandleToMimic, addOwnOffsetToMimic, addOwnRotationToMimic__. // + Attributes defined in the [path mixin](../mixin/path.html): __path, pathPosition, addPathHandle, addPathOffset, addPathRotation, constantPathSpeed__. // + Attributes defined in the [entity mixin](../mixin/entity.html): __method, pathObject, winding, flipReverse, flipUpend, scaleOutline, lockFillStyleToEntity, lockStrokeStyleToEntity, onEnter, onLeave, onDown, onUp, _fillStyle, strokeStyle, globalAlpha, globalCompositeOperation, lineWidth, lineCap, lineJoin, lineDash, lineDashOffset, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, filter___. // + Attributes defined in the [anchor mixin](../mixin/anchor.html): __anchor__. // + Attributes defined in the [filter mixin](../mixin/filter.html): __filters, isStencil__. let 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: '#808080', }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management P.packetExclusions = pushUnique(P.packetExclusions, ['tileSources']); P.finalizePacketOut = function (copy, items) { let 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; let stateCopy = JSON.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 let G = P.getters, S = P.setters, D = P.deltaSetters; // __columns__ S.columns = function (item) { if (isa_number(item)) { if (!Number.isInteger(item)) item = parseInt(item, 10); if (item !== this.columns) { let i, iz, j, 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 (!Number.isInteger(item)) item = parseInt(item, 10); if (item !== this.rows) { let 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 (!Number.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 (Array.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) { let tileFill = this.tileFill; if (xt(tiles) && isa_number(val)) { if (!Number.isInteger(val)) val = parseInt(val, 10); if (isa_number(tiles)) tileFill[tiles] = val; else if (Array.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) { let 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) { let p = this.pathObject = new Path2D(), rowLines = new Path2D(), colLines = new Path2D(); let handle = this.currentStampHandlePosition, scale = this.currentScale, dims = this.currentDimensions; let x = -handle[0] * scale, y = -handle[1] * scale, w = dims[0] * scale, h = dims[1] * scale; p.rect(x, y, w, h); let cols = this.columns, rows = this.rows, colWidth = w / cols, rowHeight = h / rows, paths = this.tilePaths, real = this.tileRealCoordinates, virtual = this.tileVirtualCoordinates, i, j, cx, cy; rowLines.moveTo(x, y); rowLines.lineTo(x + w, y); for (i = 1; i <= rows; i++) { let 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++) { let 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++) { let 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) { // Grab the current engine values for various things engine.save(); let composer = requestCell(), compEngine = composer.engine, compCanvas = composer.element; let 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; let 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 'cellGradient' : this.lockFillStyleToEntity = false; engine.fillStyle = obj.source.getData(this, this.currentHost); break; case 'gridGradient' : this.lockFillStyleToEntity = true; engine.fillStyle = obj.source.getData(this, this.currentHost); break; } } // Get an map of tiles using this source let validTiles = tileFill.map(item => item === index ? true : false); if (validTiles.length) { switch (obj.type) { // Use pool canvas to compose the output case 'gridPicture' : 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 = '#000000'; 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 'tilePicture' : 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)); } } }); let gColor = this.gutterColor, gRow = this.rowGutterWidth, gCol = this.columnGutterWidth, 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 'cellGradient' : this.lockFillStyleToEntity = false; engine.strokeStyle = gObject.source.getData(this, this.currentHost); break; case 'gridGradient' : 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 'gridPicture' : case 'tilePicture' : if(gRow || gCol) { currentPicture = (gObject.source.substring) ? entity[gObject.source] : gObject.source; if (currentPicture.simpleStamp) { let 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 = '#000000'; 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) { let p = this.pathObject; engine.stroke(p); this.currentHost.clearShadow(); this.performFill(engine); }; // `fillAndDraw` P.fillAndDraw = function (engine) { let p = this.pathObject; engine.stroke(p); this.currentHost.clearShadow(); this.performFill(engine); engine.stroke(p); }; // `drawThenFill` P.drawThenFill = function (engine) { let p = this.pathObject; engine.stroke(p); this.performFill(engine); }; // `fillThenDraw` P.fillThenDraw = function (engine) { let 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 = [], mycell) { if (this.noUserInteraction) return false; if (!this.pathObject || this.dirtyPathObject) { this.cleanPathObject(); } let tests = (!Array.isArray(items)) ? [items] : items, poolCellFlag = false; if (!mycell) { mycell = requestCell(); poolCellFlag = true; } let engine = mycell.engine, stamp = this.currentStampPosition, x = stamp[0], y = stamp[1], isGood, tx, ty, tiles = new Set(), tilePaths = this.tilePaths; const getCoords = (coords) => { let x, y; if (Array.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 (!x.toFixed || !y.toFixed || isNaN(x) || isNaN(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; }) } }) if (poolCellFlag) releaseCell(mycell); return { x: tx, y: ty, tiles: [...tiles], artefact: this }; } if (poolCellFlag) 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', // }], // }); // ``` const makeGrid = function (items) { if (!items) return false; return new Grid(items); }; constructors.Grid = Grid; // #### Exports export { makeGrid, };