UNPKG

d3-cloud

Version:

Generate word clouds in JavaScript.

1,056 lines (1,053 loc) • 35.3 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __objRest = (source, exclude) => { var target = {}; for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop]; if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) { if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop]; } return target; }; // src/sprite.js var RADIANS = Math.PI / 180; var SCRATCH_WIDTH = 1 << 11; var SCRATCH_HEIGHT = 1 << 11; var IMAGE_SLACK = 4; var CREATE = Symbol("CloudSprite"); var MASKS = /* @__PURE__ */ new WeakMap(); var CANVAS_STATES = /* @__PURE__ */ new WeakMap(); var CloudSprite = class { constructor(token, fields) { if (token !== CREATE) { throw new TypeError("CloudSprite instances are created by CloudLayout.getSprite()"); } Object.assign(this, fields); Object.freeze(this); } }; function spriteMask(sprite) { return MASKS.get(sprite); } function spriteFields(source, options) { var _a, _b, _c, _d, _e, _f, _g, _h; const image = isImageSource(source) ? source : null; if (!image && !isTextSource(source)) { throw new TypeError("getSprite() expects text or an image-like source"); } const size = finiteOption((_a = options.size) != null ? _a : options.fontSize, "size", 1); const padding = finiteOption(options.padding, "padding", image ? 0 : 1); if (padding < 0) { throw new RangeError("getSprite() expects a nonnegative padding"); } const fields = { text: String(image ? (_c = (_b = options.text) != null ? _b : source.alt) != null ? _c : "" : source), image, imageWidth: image ? imageDimensionOption(options.width, "width") : null, imageHeight: image ? imageDimensionOption(options.height, "height") : null, font: String((_d = options.font) != null ? _d : "serif"), style: String((_f = (_e = options.style) != null ? _e : options.fontStyle) != null ? _f : "normal"), weight: fontWeightOption((_h = (_g = options.weight) != null ? _g : options.fontWeight) != null ? _h : "normal"), rotate: finiteOption(options.rotate, "rotate", 0), size: Math.trunc(size), padding, datum: options.datum }; const empty = image ? fields.imageWidth === 0 || fields.imageHeight === 0 : fields.size < 1; return empty ? null : fields; } function rasterizeSprite(fields, state) { var _a, _b; if (state.drawing) { throw new Error("getSprite() cannot be called while a sprite is being drawn to the same canvas"); } const font = fields.image ? null : textFont(fields, state); let raster; state.drawing = true; try { raster = fields.image ? rasterizeImageSprite(fields, state) : rasterizeTextSprite(fields, font, state); } catch (error) { state.invalid = true; throw error; } finally { state.drawing = false; } if (!raster) { return null; } const { data, spriteWidth, x0, y0, x1, y1 } = raster; const sprite = new CloudSprite(CREATE, __spreadProps(__spreadValues({}, fields), { imageWidth: (_a = raster.imageWidth) != null ? _a : fields.imageWidth, imageHeight: (_b = raster.imageHeight) != null ? _b : fields.imageHeight, width: raster.width, height: raster.height, x0, y0, x1, y1, spriteWidth, sprite: data.slice() })); MASKS.set(sprite, { data, words: spriteWidth >>> 5, x0, y0, x1, y1, envelope: raster.envelope }); return sprite; } function createSpriteContext(canvas) { let state = CANVAS_STATES.get(canvas); if (state && !state.invalid) { return state; } const context = canvas.getContext("2d", { willReadFrequently: true }); if (!context) { throw new TypeError("canvas() factories must return a canvas with a 2d context"); } canvas.width = SCRATCH_WIDTH; canvas.height = SCRATCH_HEIGHT; if (context.getImageData(0, 0, 1, 1).data.length !== 4) { throw new TypeError("canvas() factories must return a canvas that reads back one pixel per unit"); } context.fillStyle = context.strokeStyle = "red"; state = Object.assign(state || {}, { context, invalid: false, drawing: false, clearWidth: 0, clearHeight: 0 }); CANVAS_STATES.set(canvas, state); return state; } function finiteOption(value, name, defaultValue) { if (value == null) { return defaultValue; } value = +value; if (!Number.isFinite(value)) { throw new TypeError(`getSprite() expects a finite ${name}`); } return value; } function fontWeightOption(value) { return typeof value === "number" ? value : String(value); } function imageDimensionOption(value, name) { value = finiteOption(value, name, null); return value == null ? null : Math.max(0, Math.trunc(value)); } function clearContext(state, width = state.clearWidth, height = state.clearHeight) { const context = state.context; if (width && height) { context.clearRect(0, 0, width, height); } return context; } function setClearExtent(state, width, height) { state.clearWidth = width; state.clearHeight = height; } function drawAtRasterCenter(context, rasterWidth, rasterHeight, rotate, draw) { context.save(); try { context.translate(rasterWidth >> 1, rasterHeight >> 1); if (rotate) { context.rotate(rotate * RADIANS); } draw(); } finally { context.restore(); } } function textFont(sprite, state) { const context = state.context; const font = `${sprite.style} ${sprite.weight} ${sprite.size}px ${sprite.font}`; context.font = `${sprite.size + 1}px serif`; const previousFont = context.font; context.font = font; if (context.font === previousFont) { throw new TypeError(`getSprite() font "${font}" was rejected by the canvas`); } return font; } function rasterizeTextSprite(sprite, font, state) { const context = clearContext(state); context.font = font; context.textAlign = "center"; const { width, height, hasInkBounds } = textRasterSize(sprite, context.measureText(sprite.text)); const overscan = Math.max(4, Math.ceil((sprite.padding + 2) * 2)); const overhang = hasInkBounds ? 0 : 2 * sprite.size; const envelopeWidth = Math.ceil(width + overscan * 2); let rasterWidth = envelopeWidth; let rasterHeight = Math.ceil(height + overscan * 2); let redrawn = false; let pixels; let alphaBounds; for (; ; ) { if (rasterWidth > SCRATCH_WIDTH || rasterHeight > SCRATCH_HEIGHT) { setClearExtent(state, 0, 0); return null; } try { drawAtRasterCenter(context, rasterWidth, rasterHeight, sprite.rotate, () => { context.fillText(sprite.text, 0, 0); if (sprite.padding) { context.lineWidth = 2 * sprite.padding; context.lineJoin = "round"; context.strokeText(sprite.text, 0, 0); } }); } finally { setClearExtent( state, redrawn ? SCRATCH_WIDTH : Math.min(SCRATCH_WIDTH, rasterWidth + overhang), redrawn ? SCRATCH_HEIGHT : Math.min(SCRATCH_HEIGHT, rasterHeight + overhang) ); } pixels = context.getImageData(0, 0, rasterWidth, rasterHeight).data; alphaBounds = findAlphaBounds(pixels, rasterWidth, rasterHeight); if (!alphaBounds) { return null; } const growWidth = alphaBounds.x0 === 0 || alphaBounds.x0 + alphaBounds.width === rasterWidth; const growHeight = alphaBounds.y0 === 0 || alphaBounds.y0 + alphaBounds.height === rasterHeight; if (!growWidth && !growHeight) { break; } const nextWidth = growWidth ? Math.min(SCRATCH_WIDTH, rasterWidth * 2) : rasterWidth; const nextHeight = growHeight ? Math.min(SCRATCH_HEIGHT, rasterHeight * 2) : rasterHeight; if (nextWidth === rasterWidth && nextHeight === rasterHeight) { return null; } redrawn = true; setClearExtent(state, SCRATCH_WIDTH, SCRATCH_HEIGHT); clearContext(state, nextWidth, nextHeight); rasterWidth = nextWidth; rasterHeight = nextHeight; } const raster = extractSpritePixels(pixels, rasterWidth, rasterHeight, alphaBounds); const left = -(envelopeWidth >> 1); raster.envelope = { x0: Math.min(raster.x0, left), y0: raster.y0, x1: Math.max(raster.x1, left + envelopeWidth), y1: raster.y1 }; return raster; } function textRasterSize(sprite, metrics) { let width = metrics.width + 1; let height = sprite.size * 2; const hasInkBounds = Number.isFinite(metrics.actualBoundingBoxLeft) && Number.isFinite(metrics.actualBoundingBoxRight) && Number.isFinite(metrics.actualBoundingBoxAscent) && Number.isFinite(metrics.actualBoundingBoxDescent); if (hasInkBounds) { width = Math.max(width, 2 * (sprite.padding + Math.max( Math.abs(metrics.actualBoundingBoxLeft), Math.abs(metrics.actualBoundingBoxRight) ))); height = Math.max(height, 2 * (sprite.padding + Math.max( Math.abs(metrics.actualBoundingBoxAscent), Math.abs(metrics.actualBoundingBoxDescent) ))); } return __spreadProps(__spreadValues({}, rotatedExtent(width, height, sprite.rotate)), { hasInkBounds }); } function rasterizeImageSprite(sprite, state) { const context = clearContext(state); const imageSize = resolveImageSize(sprite.image, sprite.imageWidth, sprite.imageHeight); if (!imageSize) { setClearExtent(state, 0, 0); return null; } const { drawWidth, drawHeight, intrinsic } = imageSize; const bounds = rotatedExtent(drawWidth, drawHeight, sprite.rotate); const rasterWidth = Math.ceil(bounds.width + 2); const rasterHeight = Math.ceil(bounds.height + 2); if (rasterWidth > SCRATCH_WIDTH || rasterHeight > SCRATCH_HEIGHT) { setClearExtent(state, 0, 0); return null; } try { drawAtRasterCenter(context, rasterWidth, rasterHeight, sprite.rotate, () => { context.drawImage(sprite.image, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight); }); } catch (error) { if (intrinsic) { throw error; } return null; } finally { setClearExtent(state, rasterWidth, rasterHeight); } const pixels = context.getImageData(0, 0, rasterWidth, rasterHeight).data; const alphaBounds = findAlphaBounds(pixels, rasterWidth, rasterHeight); if (!alphaBounds) { return null; } const raster = extractSpritePixels(pixels, rasterWidth, rasterHeight, alphaBounds); raster.imageWidth = drawWidth; raster.imageHeight = drawHeight; raster.envelope = { x0: raster.x0 - IMAGE_SLACK, y0: raster.y0 - IMAGE_SLACK, x1: raster.x1 + IMAGE_SLACK, y1: raster.y1 + IMAGE_SLACK }; return raster; } function extractSpritePixels(pixels, rasterWidth, rasterHeight, alphaBounds) { const width = alphaBounds.width; const height = alphaBounds.height; const spriteWidth = width + 31 >>> 5 << 5; const wordsPerRow = spriteWidth >>> 5; const x0 = alphaBounds.x0 - (rasterWidth >> 1); const y0 = alphaBounds.y0 - (rasterHeight >> 1); const data = new Uint32Array(wordsPerRow * height); for (let row = 0; row < height; row += 1) { const sourceRow = alphaBounds.y0 + row; const rowOffset = row * wordsPerRow; for (let column = 0; column < width; column += 1) { const sourceColumn = alphaBounds.x0 + column; if (pixels[(sourceRow * rasterWidth + sourceColumn << 2) + 3]) { data[rowOffset + (column >>> 5)] |= 1 << 31 - (column & 31); } } } return { data, spriteWidth, width, height, x0, y0, x1: x0 + width, y1: y0 + height }; } function resolveImageSize(image, imageWidth, imageHeight) { const sourceWidth = resolveImageDimension(image, "naturalWidth", "videoWidth", "width"); const sourceHeight = resolveImageDimension(image, "naturalHeight", "videoHeight", "height"); if (!(sourceWidth > 0) || !(sourceHeight > 0)) { if (imageWidth != null && imageHeight != null && typeof image.naturalWidth === "number" && image.complete === true) { return { drawWidth: imageWidth, drawHeight: imageHeight, intrinsic: false }; } return null; } let drawWidth = imageWidth; let drawHeight = imageHeight; if (drawWidth == null && drawHeight == null) { drawWidth = sourceWidth; drawHeight = sourceHeight; } else if (drawWidth == null) { drawWidth = Math.max(1, Math.round(sourceWidth * (drawHeight / sourceHeight))); } else if (drawHeight == null) { drawHeight = Math.max(1, Math.round(sourceHeight * (drawWidth / sourceWidth))); } return { drawWidth, drawHeight, intrinsic: true }; } function resolveImageDimension(image, ...keys) { for (const key of keys) { const value = image == null ? void 0 : image[key]; if (typeof value === "number") { return value > 0 && Number.isFinite(value) ? Math.trunc(value) : 0; } } return 0; } function rotatedExtent(width, height, rotate) { if (!rotate) { return { width, height }; } const sine = Math.sin(rotate * RADIANS); const cosine = Math.cos(rotate * RADIANS); const widthCosine = width * cosine; const widthSine = width * sine; const heightCosine = height * cosine; const heightSine = height * sine; return { width: Math.max(Math.abs(widthCosine + heightSine), Math.abs(widthCosine - heightSine)), height: Math.max(Math.abs(widthSine + heightCosine), Math.abs(widthSine - heightCosine)) }; } function findAlphaBounds(pixels, width, height) { let minX = width; let minY = height; let maxX = -1; let maxY = -1; for (let row = 0; row < height; row += 1) { for (let column = 0; column < width; column += 1) { if (!pixels[(row * width + column << 2) + 3]) { continue; } if (column < minX) minX = column; if (row < minY) minY = row; if (column > maxX) maxX = column; if (row > maxY) maxY = row; } } if (maxX < minX || maxY < minY) { return null; } return { x0: minX, y0: minY, width: maxX - minX + 1, height: maxY - minY + 1 }; } function isTextSource(source) { return typeof source === "string" || typeof source === "number" || typeof source === "boolean" || source instanceof String; } function isImageSource(source) { return !!source && typeof source === "object" && !Array.isArray(source) && (typeof source.width === "number" || typeof source.naturalWidth === "number" || typeof source.videoWidth === "number"); } // src/layout.js var MAX_COORDINATE = 1 << 24; var MAX_BLOCK_SIZE = 4096; var MIN_SEARCH_BUDGET = 1e5; var MAX_SPIRAL_ASPECT_RATIO = 64; var STRATEGIES = { archimedean: archimedeanStrategy, rectangular: rectangularStrategy, none: noneStrategy }; var CloudLayout = class { constructor() { this._size = [256, 256]; this._overflow = true; this._strategy = archimedeanStrategy; this._random = Math.random; this._blockSize = 512; this._canvasFactory = defaultCanvasFactory; this._spriteContext = null; this._bounds = null; this._blocks = createOccupancyBlocks(this._blockSize); this._searches = 0; this._placements = /* @__PURE__ */ new WeakMap(); } canvas(_) { if (!arguments.length) { return this._canvasFactory; } if (typeof _ !== "function") { throw new TypeError("canvas() expects a canvas factory function"); } this._canvasFactory = _; this._spriteContext = null; return this; } clear() { this._assertNotSearching("clear()"); this._placements = /* @__PURE__ */ new WeakMap(); this._bounds = null; this._blocks = createOccupancyBlocks(this._blockSize); return this; } bounds() { return cloneBounds(this._bounds); } size(_) { if (!arguments.length) { return this._size.slice(); } this._size = normalizeSize(_); return this; } overflow(_) { if (!arguments.length) { return this._overflow; } this._overflow = !!_; return this; } strategy(_) { if (!arguments.length) { return this._strategy; } this._strategy = resolveStrategy(_); return this; } random(_) { if (!arguments.length) { return this._random; } if (typeof _ !== "function") { throw new TypeError("random() expects a function"); } this._random = _; return this; } blockSize(_) { if (!arguments.length) { return this._blockSize; } const nextBlockSize = normalizeBlockSize(_); if (nextBlockSize === this._blockSize) { return this; } this._assertNotSearching("blockSize()"); if (this._bounds) { throw new Error("Cannot change blockSize after placement; call clear() first"); } this._blockSize = nextBlockSize; this._blocks = createOccupancyBlocks(this._blockSize); return this; } getSprite(source, options) { const fields = spriteFields(source, optionsObject(options, "getSprite()")); if (!fields) { return null; } return rasterizeSprite(fields, this._getContext()); } erase(word) { const placement = word && typeof word === "object" ? this._placements.get(word) : void 0; if (!placement) { throw new Error("erase() expects a word returned by place() on this layout since it was last cleared"); } this._blocks.remove(placement.mask, placement.x, placement.y); this._placements.delete(word); if (this._blocks.isEmpty()) { this._bounds = null; } } place(sprite, options) { const mask = spriteMask(sprite); if (!mask) { throw new TypeError("place() expects a CloudSprite from getSprite() on a layout from the same copy of d3-cloud"); } const { x: optionX, y: optionY, strategy: optionStrategy } = optionsObject(options, "place()"); const strategy = optionStrategy == null ? this._strategy : resolveStrategy(optionStrategy); const x = optionX == null ? seedCoordinate(this._size[0], this._random) : placeCoordinate(optionX); const y = optionY == null ? seedCoordinate(this._size[1], this._random) : placeCoordinate(optionY); let position; this._searches++; try { position = tryPlaceSprite(this._blocks, mask, x, y, this._bounds, strategy, this._size, this._overflow, this._random); } finally { this._searches--; } if (!position) { return null; } this._bounds = extendBounds(this._bounds, mask, position.x, position.y); const word = createPlacedWord(sprite, position.x, position.y); this._placements.set(word, { mask, x: position.x, y: position.y }); return word; } // A search holds the occupancy blocks it started with. Replacing them // mid-search would leave the word out of the new blocks, where later words // could overlap it. _assertNotSearching(method) { if (this._searches) { throw new Error(`${method} cannot be called while place() is searching`); } } _getContext() { if (!this._spriteContext || this._spriteContext.invalid) { this._spriteContext = createSpriteContext(this._canvasFactory()); } return this._spriteContext; } }; function tryPlaceSprite(blocks, mask, seedX, seedY, bounds, strategy, size, overflow, random) { const range = anchorRange(mask, bounds, size, overflow); if (!range) { return null; } if (withinRange(range, seedX, seedY) && !blocks.collides(mask, seedX, seedY)) { blocks.insert(mask, seedX, seedY); return { x: seedX, y: seedY }; } const startX = Math.min(Math.max(seedX, range.x0), range.x1); const startY = Math.min(Math.max(seedY, range.y0), range.y1); const next = strategy( { x: startX, y: startY }, { size: size.slice(), aspectRatio: sizeAspectRatio(size), bounds: cloneBounds(bounds), overflow, random } ); if (typeof next !== "function") { throw new TypeError("strategy factories must return a candidate generator"); } const envelope = mask.envelope; const limitX = Math.max(startX - range.x0, range.x1 - startX) + envelope.x1 - envelope.x0 + 1; const limitY = Math.max(startY - range.y0, range.y1 - startY) + envelope.y1 - envelope.y0 + 1; const insideLimit = Math.max(MIN_SEARCH_BUDGET, (range.x1 - range.x0 + 1) * (range.y1 - range.y0 + 1)); const outsideLimit = Math.max(MIN_SEARCH_BUDGET, 4 * limitX * limitY); let candidate; let inside = 0; let outside = 0; while (candidate = normalizeStrategyCandidate(next())) { if (!withinRange(range, candidate.x, candidate.y)) { if (Math.abs(candidate.x - startX) >= limitX && Math.abs(candidate.y - startY) >= limitY) break; if (++outside >= outsideLimit) break; continue; } outside = 0; if (++inside > insideLimit) break; if (!blocks.collides(mask, candidate.x, candidate.y)) { blocks.insert(mask, candidate.x, candidate.y); return candidate; } } return null; } function anchorRange(mask, bounds, size, overflow) { let x0 = -MAX_COORDINATE; let y0 = -MAX_COORDINATE; let x1 = MAX_COORDINATE; let y1 = MAX_COORDINATE; const envelope = mask.envelope; if (!overflow) { x0 = Math.max(x0, Math.ceil(-size[0] / 2 - mask.x0)); y0 = Math.max(y0, Math.ceil(-size[1] / 2 - mask.y0)); x1 = Math.min(x1, Math.floor(size[0] / 2 - mask.x1)); y1 = Math.min(y1, Math.floor(size[1] / 2 - mask.y1)); } if (bounds) { x0 = Math.max(x0, Math.floor(bounds[0].x - envelope.x1) + 1); y0 = Math.max(y0, Math.floor(bounds[0].y - envelope.y1) + 1); x1 = Math.min(x1, Math.ceil(bounds[1].x - envelope.x0) - 1); y1 = Math.min(y1, Math.ceil(bounds[1].y - envelope.y0) - 1); } return x0 <= x1 && y0 <= y1 ? { x0, y0, x1, y1 } : null; } function withinRange(range, x, y) { return x >= range.x0 && x <= range.x1 && y >= range.y0 && y <= range.y1; } function createOccupancyBlocks(blockSize) { const blocks = /* @__PURE__ */ new Map(); const blockWordsPerRow = blockSize >>> 5; function visitBlocks(mask, x, y, visit, create, prune) { const data = mask.data; const wordsPerRow = mask.words; const left = x + mask.x0; const top = y + mask.y0; const right = x + mask.x1; const bottom = y + mask.y1; const firstX = Math.floor(left / blockSize); const lastX = Math.floor((right - 1) / blockSize); const firstY = Math.floor(top / blockSize); const lastY = Math.floor((bottom - 1) / blockSize); for (let blockY = firstY; blockY <= lastY; blockY++) { let blockRow = blocks.get(blockY); if (!blockRow) { if (!create) continue; blockRow = /* @__PURE__ */ new Map(); blocks.set(blockY, blockRow); } const blockTop = blockY * blockSize; const overlapTop = Math.max(top, blockTop); const overlapBottom = Math.min(bottom, blockTop + blockSize); for (let blockX = firstX; blockX <= lastX; blockX++) { const blockLeft = blockX * blockSize; const overlapLeft = Math.max(left, blockLeft); const overlapRight = Math.min(right, blockLeft + blockSize); if (overlapLeft >= overlapRight || overlapTop >= overlapBottom) continue; let block = blockRow.get(blockX); if (!block) { if (!create) continue; block = new Uint32Array(blockWordsPerRow * blockSize); blockRow.set(blockX, block); } if (visit( data, wordsPerRow, left, top, block, blockWordsPerRow, blockLeft, blockTop, overlapLeft, overlapTop, overlapRight, overlapBottom )) { return true; } if (prune && isZeroBlock(block)) { blockRow.delete(blockX); } } if (prune && blockRow.size === 0) blocks.delete(blockY); } return false; } return { isEmpty() { return blocks.size === 0; }, collides(mask, x, y) { return visitBlocks(mask, x, y, packedRegionCollides, false, false); }, insert(mask, x, y) { visitBlocks(mask, x, y, stampPackedRegion, true, false); }, remove(mask, x, y) { visitBlocks(mask, x, y, clearPackedRegion, false, true); } }; } function packedRegionCollides(aData, aWordsPerRow, aLeft, aTop, bData, bWordsPerRow, bLeft, bTop, overlapLeft, overlapTop, overlapRight, overlapBottom) { const rows = overlapBottom - overlapTop; const words = overlapRight - overlapLeft + 31 >>> 5; const aStartBit = overlapLeft - aLeft; const bStartBit = overlapLeft - bLeft; const aStartRow = overlapTop - aTop; const bStartRow = overlapTop - bTop; const trailing = overlapRight - overlapLeft & 31; const lastMask = trailing ? ~0 << 32 - trailing >>> 0 : 4294967295; for (let row = 0; row < rows; row++) { const aRowOffset = (aStartRow + row) * aWordsPerRow; const bRowOffset = (bStartRow + row) * bWordsPerRow; for (let word = 0; word < words; word++) { const mask = word === words - 1 ? lastMask : 4294967295; const aWord = readPackedWord(aData, aRowOffset, aWordsPerRow, aStartBit + (word << 5)); const bWord = readPackedWord(bData, bRowOffset, bWordsPerRow, bStartBit + (word << 5)); if ((aWord & bWord & mask) !== 0) { return true; } } } return false; } function stampPackedRegion(sourceData, sourceWordsPerRow, sourceLeft, sourceTop, targetData, targetWordsPerRow, targetLeft, targetTop, overlapLeft, overlapTop, overlapRight, overlapBottom) { const rows = overlapBottom - overlapTop; const words = overlapRight - overlapLeft + 31 >>> 5; const sourceStartBit = overlapLeft - sourceLeft; const targetStartBit = overlapLeft - targetLeft; const sourceStartRow = overlapTop - sourceTop; const targetStartRow = overlapTop - targetTop; const trailing = overlapRight - overlapLeft & 31; const lastMask = trailing ? ~0 << 32 - trailing >>> 0 : 4294967295; for (let row = 0; row < rows; row++) { const sourceRowOffset = (sourceStartRow + row) * sourceWordsPerRow; const targetRowOffset = (targetStartRow + row) * targetWordsPerRow; for (let word = 0; word < words; word++) { const mask = word === words - 1 ? lastMask : 4294967295; const sourceWord = readPackedWord(sourceData, sourceRowOffset, sourceWordsPerRow, sourceStartBit + (word << 5)) & mask; orPackedWord(targetData, targetRowOffset, targetWordsPerRow, targetStartBit + (word << 5), sourceWord); } } } function clearPackedRegion(sourceData, sourceWordsPerRow, sourceLeft, sourceTop, targetData, targetWordsPerRow, targetLeft, targetTop, overlapLeft, overlapTop, overlapRight, overlapBottom) { const rows = overlapBottom - overlapTop; const words = overlapRight - overlapLeft + 31 >>> 5; const sourceStartBit = overlapLeft - sourceLeft; const targetStartBit = overlapLeft - targetLeft; const sourceStartRow = overlapTop - sourceTop; const targetStartRow = overlapTop - targetTop; const trailing = overlapRight - overlapLeft & 31; const lastMask = trailing ? ~0 << 32 - trailing >>> 0 : 4294967295; for (let row = 0; row < rows; row++) { const sourceRowOffset = (sourceStartRow + row) * sourceWordsPerRow; const targetRowOffset = (targetStartRow + row) * targetWordsPerRow; for (let word = 0; word < words; word++) { const mask = word === words - 1 ? lastMask : 4294967295; const sourceWord = readPackedWord(sourceData, sourceRowOffset, sourceWordsPerRow, sourceStartBit + (word << 5)) & mask; clearPackedWord(targetData, targetRowOffset, targetWordsPerRow, targetStartBit + (word << 5), sourceWord); } } } function readPackedWord(sprite, rowOffset, wordsPerRow, bitIndex) { const wordIndex = bitIndex >>> 5; const bitOffset = bitIndex & 31; const current = wordIndex < wordsPerRow ? sprite[rowOffset + wordIndex] : 0; if (!bitOffset) { return current; } const next = wordIndex + 1 < wordsPerRow ? sprite[rowOffset + wordIndex + 1] : 0; return (current << bitOffset | next >>> 32 - bitOffset) >>> 0; } function orPackedWord(target, rowOffset, wordsPerRow, bitIndex, value) { const wordIndex = bitIndex >>> 5; const bitOffset = bitIndex & 31; if (!bitOffset) { target[rowOffset + wordIndex] |= value; return; } target[rowOffset + wordIndex] |= value >>> bitOffset; if (wordIndex + 1 < wordsPerRow) { target[rowOffset + wordIndex + 1] |= value << 32 - bitOffset >>> 0; } } function clearPackedWord(target, rowOffset, wordsPerRow, bitIndex, value) { const wordIndex = bitIndex >>> 5; const bitOffset = bitIndex & 31; if (!bitOffset) { target[rowOffset + wordIndex] &= ~value >>> 0; return; } target[rowOffset + wordIndex] &= ~(value >>> bitOffset) >>> 0; if (wordIndex + 1 < wordsPerRow) { target[rowOffset + wordIndex + 1] &= ~(value << 32 - bitOffset >>> 0) >>> 0; } } function isZeroBlock(block) { for (let index = 0; index < block.length; index++) { if (block[index] !== 0) { return false; } } return true; } function createPlacedWord(sprite, x, y) { const _a = sprite, { sprite: _mask, spriteWidth: _spriteWidth } = _a, fields = __objRest(_a, ["sprite", "spriteWidth"]); return __spreadProps(__spreadValues({}, fields), { x, y }); } function extendBounds(bounds, mask, x, y) { const left = x + mask.x0; const top = y + mask.y0; const right = x + mask.x1; const bottom = y + mask.y1; if (!bounds) { return [{ x: left, y: top }, { x: right, y: bottom }]; } if (left < bounds[0].x) bounds[0].x = left; if (top < bounds[0].y) bounds[0].y = top; if (right > bounds[1].x) bounds[1].x = right; if (bottom > bounds[1].y) bounds[1].y = bottom; return bounds; } function cloneBounds(bounds) { if (!bounds) { return bounds; } return [ { x: bounds[0].x, y: bounds[0].y }, { x: bounds[1].x, y: bounds[1].y } ]; } function archimedeanStrategy(initial, context) { const e = spiralAspectRatio(context.aspectRatio); const dt = context.random() < 0.5 ? 1 : -1; let t = -dt; return function() { const angle = (t += dt) * 0.1; return { x: initial.x + ~~(e * angle * Math.cos(angle)), y: initial.y + ~~(angle * Math.sin(angle)) }; }; } function rectangularStrategy(initial, context) { const e = spiralAspectRatio(context.aspectRatio); const sign = context.random() < 0.5 ? 1 : -1; let a = 0; let b = 0; let runs = []; let run = 0; let x = 0; let y = 0; let dx = 0; let dy = 0; let count = 1; function grow() { const nextX = e >= 1 ? (a + 1) / e : a + 1; const nextY = e >= 1 ? b + 1 : (b + 1) * e; const growX = nextX <= nextY; const growY = nextY <= nextX; if (growX) a++; if (growY) b++; if (growX && growY) { runs = [ -a, -1, 0, -1, b, -a + 1, -b, 1, 0, 2 * a, a, -b + 1, 0, 1, 2 * b, a - 1, b, -1, 0, 2 * a, -a, b - 1, 0, -1, b ]; } else if (growX) { runs = [ -a, -1, 0, -1, b, a, -b, 0, 1, 2 * b + 1, -a, b, 0, -1, b + 1 ]; } else { runs = [ -a, -b, 1, 0, 2 * a + 1, a, b, -1, 0, 2 * a + 1 ]; } run = 0; } return function() { while (!count) { if (run === runs.length) grow(); x = runs[run++]; y = runs[run++]; dx = runs[run++]; dy = runs[run++]; count = runs[run++]; } const candidate = { x: initial.x + 4 * x, y: initial.y + 4 * sign * y }; x += dx; y += dy; count--; return candidate; }; } function noneStrategy() { return function() { return null; }; } function defaultCanvasFactory() { return document.createElement("canvas"); } function normalizeAspectRatio(value) { value = +value; return value > 0 && Number.isFinite(value) ? value : 1; } function spiralAspectRatio(value) { return Math.min(Math.max(normalizeAspectRatio(value), 1 / MAX_SPIRAL_ASPECT_RATIO), MAX_SPIRAL_ASPECT_RATIO); } function normalizeBlockSize(value) { value = +value; if (!(value >= 1 && value <= MAX_BLOCK_SIZE)) { throw new RangeError(`blockSize() expects a size from 1 to ${MAX_BLOCK_SIZE}`); } return Math.ceil(value / 32) * 32; } function placeCoordinate(value) { value = +value; if (!Number.isFinite(value)) { throw new TypeError("place() expects finite x and y coordinates"); } if (Math.abs(value) > MAX_COORDINATE) { throw new RangeError(`place() expects x and y coordinates within \xB1${MAX_COORDINATE}`); } return Math.floor(value); } function normalizeSize(value) { if (!Array.isArray(value)) { value = [value, value]; } return [ normalizeSizeDimension(value[0]), normalizeSizeDimension(value[1]) ]; } function normalizeSizeDimension(value) { value = +value; if (!(value >= 0 && value <= MAX_COORDINATE)) { throw new RangeError(`size() expects dimensions from 0 to ${MAX_COORDINATE}`); } return value; } function sizeAspectRatio(size) { return normalizeAspectRatio(size[0] / size[1]); } function seedCoordinate(size, random) { if (!(size > 0)) { return 0; } const value = random(); if (!(typeof value === "number" && value >= 0 && value < 1)) { throw new RangeError("random() must return a number in [0, 1)"); } return Math.floor((value - 0.5) * (size / 2)); } function optionsObject(options, method) { if (options == null) { return {}; } if (typeof options !== "object" || Array.isArray(options)) { throw new TypeError(`${method} expects an options object`); } return options; } function resolveStrategy(value) { const strategy = typeof value === "string" && Object.prototype.hasOwnProperty.call(STRATEGIES, value) ? STRATEGIES[value] : value; if (typeof strategy !== "function") { throw new TypeError("strategy() expects a built-in strategy name or strategy factory"); } return strategy; } function normalizeStrategyCandidate(value) { if (!value) { return null; } if (typeof value === "object") { const x = +value.x; const y = +value.y; if (!Number.isFinite(x) || !Number.isFinite(y)) { throw new TypeError("strategy candidates must have finite x and y"); } return { x: Math.floor(x), y: Math.floor(y) }; } throw new TypeError("strategy candidates must be {x, y} or null"); } export { CloudLayout, CloudSprite, archimedeanStrategy, CloudLayout as default, noneStrategy, rectangularStrategy };