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

1,436 lines (1,022 loc) 294 kB
// # Scrawl-canvas filter engine // All Scrawl-canvas filters-related image manipulation work happens in this engine code. Note that this functionality is entirely separate from the &lt;canvas> element's context engine's native `filter` functionality, which allows us to add CSS/SVG-based filters to the canvas context // + Note that prior to v8.5.0 most of this code lived in an (asynchronous) web worker. Web worker functionality has now been removed from Scrawl-canvas as it was not adding sufficient efficiency to rendering speed import { constructors, filter, filternames, styles, stylesnames } from '../core/library.js'; import { seededRandomNumberGenerator } from './random-seed.js'; import { correctAngle, doCreate, easeEngines, isa_fn } from './utilities.js'; import { getOrAddWorkstoreItem, getWorkstoreItem, setAndReturnWorkstoreItem, setWorkstoreItem } from './workstore.js'; import { colorEngine } from './color-engine.js'; import { releaseArray, requestArray } from './array-pool.js'; import { makeAnimation } from '../factory/animation.js'; import { releaseCell, requestCell } from '../untracked-factory/cell-fragment.js'; import { releaseCoordinate, requestCoordinate } from '../untracked-factory/coordinate.js'; import { bluenoise } from './filter-engine-bluenoise-data.js'; // Shared constants import { _abs, _atan2, _ceil, _cos, _floor, _isArray, _isFinite, _max, _min, _piHalf, _pow, _radian, _round, _sin, _sqrt, ALPHA_TO_CHANNELS, ALPHA_TO_LUMINANCE, AREA_ALPHA, ARG_SPLITTER, AVERAGE_CHANNELS, BLACK_WHITE, BLEND, BLUENOISE, BLUR, CHANNELS_TO_ALPHA, CHROMA, CLAMP_CHANNELS, CLAMP_VALUES, CLEAR, COLOR, COLORS_TO_ALPHA, COMPOSE, CORRODE, DECONVOLUTE, DEFAULT_SEED, DESTINATION_OUT, DESTINATION_OVER, DISPLACE, DOWN, EMBOSS, FLOOD, GAUSSIAN_BLUR, GLITCH, GRAYSCALE, GREEN, INVERT_CHANNELS, LOCK_CHANNELS_TO_LEVELS, LUMINANCE_TO_ALPHA, MAP_TO_GRADIENT, MATRIX, MEAN, MODIFY_OK_CHANNELS, MODULATE_CHANNELS, MODULATE_OK_CHANNELS, MULTIPLY, NEGATIVE, NEWSPRINT, OFFSET, OK_PERCEPTUAL_CURVES, PIXELATE, PROCESS_IMAGE, RANDOM, RANDOM_NOISE, RED, REDUCE_PALETTE, ROTATE_HUE, ROUND, SET_CHANNEL_TO_LEVEL, SOURCE, SOURCE_IN, SOURCE_OUT, SOURCE_OVER, STEP_CHANNELS, SWIRL, THRESHOLD, TILES, TINT_CHANNELS, UP, UNSHARP, USER_DEFINED_LEGACY, VARY_CHANNELS_BY_WEIGHTS, ZERO_STR, ZOOM_BLUR } from './shared-vars.js'; // Local constants const _256 = 256, _256_SQUARE = 256 * 256, _exp = Math.exp, BLUE = 'blue', CHROMA_MATCH = 'chroma-match', COLOR_BURN = 'color-burn', COLOR_DODGE = 'color-dodge', CURRENT = 'current', DARKEN = 'darken', DESTINATION_ATOP = 'destination-atop', DESTINATION_IN = 'destination-in', DESTINATION_ONLY = 'destination-only', DIFFERENCE = 'difference', EXCLUSION = 'exclusion', GRAY_PALETTES = ['black-white', 'monochrome-4', 'monochrome-8', 'monochrome-16'], HARD_LIGHT = 'hard-light', HEX = 'hex', HUE = 'hue', HUE_MATCH = 'hue-match', LIGHTEN = 'lighten', LIGHTER = 'lighter', LUMINOSITY = 'luminosity', MONOCHROME_16 = 'monochrome-16', MONOCHROME_4 = 'monochrome-4', MONOCHROME_8 = 'monochrome-8', NAIVE_GRAY_LUT = 'naive-gray-lut', ORDERED = 'ordered', OVERLAY = 'overlay', POINTS = 'points', RECT = 'rect', SATURATION = 'saturation', SCREEN = 'screen', SOFT_LIGHT = 'soft-light', SOURCE_ALPHA = 'source-alpha', SOURCE_ATOP = 'source-atop', SOURCE_ONLY = 'source-only', T_FILTER_ENGINE = 'FilterEngine', XOR = 'xor'; const OK_BLENDS = [HUE, SATURATION, LUMINOSITY, COLOR, HUE_MATCH, CHROMA_MATCH]; const orderedNoise = new Float32Array([0.00,0.50,0.13,0.63,0.03,0.53,0.16,0.66,0.75,0.25,0.88,0.38,0.78,0.28,0.91,0.41,0.19,0.69,0.06,0.56,0.22,0.72,0.09,0.59,0.94,0.44,0.81,0.31,0.97,0.47,0.84,0.34,0.05,0.55,0.17,0.67,0.02,0.52,0.14,0.64,0.80,0.30,0.92,0.42,0.77,0.27,0.89,0.39,0.23,0.73,0.11,0.61,0.20,0.70,0.08,0.58,0.98,0.48,0.86,0.36,0.95,0.45,0.83,0.33]); const newspaperPatterns = [ new Uint8Array([0,0,0,0]), new Uint8Array([0,0,0,180]), new Uint8Array([180,0,0,0]), new Uint8Array([180,0,0,180]), new Uint8Array([0,180,180,180]), new Uint8Array([180,180,180,0]), new Uint8Array([180,180,180,180]), new Uint8Array([180,180,180,255]), new Uint8Array([255,180,180,180]), new Uint8Array([255,180,180,255]), new Uint8Array([180,255,255,255]), new Uint8Array([255,255,255,180]), new Uint8Array([255,255,255,255]) ]; const predefinedPalette = { [BLACK_WHITE]: [255, 0], [MONOCHROME_4]: [255, 187, 102, 0], [MONOCHROME_8]: [255, 221, 187, 153, 119, 85, 51, 0], [MONOCHROME_16]: [255, 238, 221, 204, 187, 170, 153, 136, 119, 102, 85, 68, 51, 34, 17, 0], } // A backdoor to retrieve the last palette used by the `reduce-palette` filter // + We use this in Demo filters-027 to report the colors used in the commonest colors palette let lastUsedReducePalette = 'black-white'; const setLastUsedReducePalette = (val) => lastUsedReducePalette = val; export const getLastUsedReducePalette = () => lastUsedReducePalette; // __cache__ - an Object consisting of `key:Object` pairs where the key is the named input of a `process-image` action or the output of any action object. This object is cleared and re-initialized each time the `engine.action` function is invoked let cache = null; // #### FilterEngine constructor const FilterEngine = function () { // __actions__ - the Array of action objects that the engine needs to process. this.actions = []; return this; }; // #### FilterEngine prototype const P = FilterEngine.prototype = doCreate(); P.type = T_FILTER_ENGINE; P.action = function (packet) { const { identifier, filters, image } = packet; const { actions, theBigActionsObject } = this; let i, iz, actData, a; const itemInWorkstore = getWorkstoreItem(identifier); if (itemInWorkstore) return itemInWorkstore; actions.length = 0; for (i = 0, iz = filters.length; i < iz; i++) { actions.push(...filters[i].actions); } const actionsLen = actions.length; if (actionsLen) { this.unknit(image); for (i = 0; i < actionsLen; i++) { actData = actions[i]; a = theBigActionsObject[actData.action]; if (a) a.call(this, actData); } if (identifier) setWorkstoreItem(identifier, cache.work); return cache.work; } return image; }; // ### Permanent variables // `unknit` - called at the start of each new message action chain. Creates and populates the __source__ and __work__ objects from the image data supplied in the message P.unknit = function (image) { cache = {}; const { width, height, data } = image; cache.source = new ImageData(new Uint8ClampedArray(data), width, height); cache.work = new ImageData(new Uint8ClampedArray(data), width, height); }; // ### Functions invoked by a range of different action functions // const getRandomNumbers = function (items = {}) { const { seed = DEFAULT_SEED, length = 0, imgWidth = 0, type = RANDOM, } = items; const name = `random-${seed}-${length}-${type}`, itemInWorkstore = getWorkstoreItem(name); if (itemInWorkstore) return itemInWorkstore; if ((type === BLUENOISE || type === ORDERED) && imgWidth) { const base = (type === BLUENOISE) ? bluenoise : orderedNoise, dim = (_sqrt(base.length) | 0), imgH = ((length / imgWidth) | 0), out = new Float32Array(length); let p = 0, y, y0, x; for (y = 0; y < imgH && p < length; y++) { y0 = (y % dim) * dim; for (x = 0; x < imgWidth && p < length; x++) { out[p++] = base[y0 + (x % dim)]; } } setWorkstoreItem(name, out); return out; } else { const engine = seededRandomNumberGenerator(seed), out = new Float32Array(length); for (let i = 0; i < length; i++) { out[i] = engine.random(); } setWorkstoreItem(name, out); return out; } }; // Build compact tile rectangles (no per-pixel arrays). // + Returns an Int32Array laid out as [x0, y0, x1, y1, x0, y0, x1, y1, ...] const buildTileRects = function (tileWidth, tileHeight, offsetX, offsetY, image) { if (!image) image = cache.source; const iWidth = image.width | 0, iHeight = image.height | 0; if (!iWidth || !iHeight) return new Int32Array(0); let tW = (_isFinite(tileWidth) ? tileWidth : 1) | 0, tH = (_isFinite(tileHeight) ? tileHeight : 1) | 0, offX = (_isFinite(offsetX) ? offsetX : 0) | 0, offY = (_isFinite(offsetY) ? offsetY : 0) | 0; if (tW < 1) tW = 1; if (tW >= iWidth) tW = iWidth - 1; if (tH < 1) tH = 1; if (tH >= iHeight) tH = iHeight - 1; if (offX < 0) offX = 0; else if (offX >= tW) offX = tW - 1; if (offY < 0) offY = 0; else if (offY >= tH) offY = tH - 1; const name = `simple-tileset-rects-${iWidth}-${iHeight}-${tW}-${tH}-${offX}-${offY}`; const cached = getWorkstoreItem(name); if (cached) return cached; const rects = requestArray(); let j, y0, y1, yEnd, i, x0, x1, xEnd; for (j = offY - tH; j < iHeight; j += tH) { y0 = (j < 0 ? 0 : j); y1 = j + tH; if (y0 >= iHeight) break; yEnd = (y1 > iHeight ? iHeight : y1); for (i = offX - tW; i < iWidth; i += tW) { x0 = (i < 0 ? 0 : i); x1 = i + tW; if (x0 >= iWidth) break; xEnd = (x1 > iWidth ? iWidth : x1); if (x0 < xEnd && y0 < yEnd) rects.push(x0, y0, xEnd, yEnd); } } const out = new Int32Array(rects); setWorkstoreItem(name, out); releaseArray(rects); return out; }; // `getInputAndOutputLines` - determine, and return, the appropriate results object for the lineIn, lineMix and lineOut values supplied to each action function when it gets invoked const getInputAndOutputLines = function (requirements) { const getAlphaData = function (image) { const { width, height, data:iData } = image, aImg = new ImageData(width, height), aData = aImg.data; for (let i = 3, len = iData.length; i < len; i += 4) { aData[i] = (iData[i] > 0) ? 255 : 0; } return aImg; }; const sourceData = cache.source; let lineIn = cache.work, lineMix = false, alphaData = false; if (requirements.lineIn === SOURCE_ALPHA || requirements.lineMix === SOURCE_ALPHA) alphaData = getAlphaData(sourceData); if (requirements.lineIn) { if (requirements.lineIn === SOURCE) lineIn = sourceData; else if (requirements.lineIn === SOURCE_ALPHA) lineIn = alphaData; else if (cache[requirements.lineIn]) lineIn = cache[requirements.lineIn]; } if (requirements.lineMix) { if (requirements.lineMix === SOURCE) lineMix = sourceData; else if (requirements.lineMix === SOURCE_ALPHA) lineMix = alphaData; else if (requirements.lineMix === CURRENT) lineMix = cache.work; else if (cache[requirements.lineMix]) lineMix = cache[requirements.lineMix]; } let lineOut; if (!requirements.lineOut || !cache[requirements.lineOut]) { lineOut = new ImageData(lineIn.width, lineIn.height); if (requirements.lineOut) cache[requirements.lineOut] = lineOut; } else lineOut = cache[requirements.lineOut]; return [lineIn, lineOut, lineMix]; }; // `processResults` - at the conclusion of each action function, combine the results of the function's manipulations back into the data supplied for manipulation, in line with the value of the action object's `opacity` attribute const processResults = function (store, incoming, ratio) { const sData = store.data, iData = incoming.data; // Clamp ratio defensively if (ratio <= 0) return; if (ratio >= 1) { sData.set(iData); return; } // If source and destination are literally the same bytes, nothing to do. if (sData.buffer === iData.buffer && sData.byteOffset === iData.byteOffset && sData.byteLength === iData.byteLength) return; // Convert to fixed-point [0..255] const k = (ratio * 255 + 0.5) | 0, ak = 255 - k; // Blend 4 channels at a time via 32-bit views const nPixels = sData.byteLength >>> 2, s32 = new Uint32Array(sData.buffer, sData.byteOffset, nPixels), i32 = new Uint32Array(iData.buffer, iData.byteOffset, nPixels); // Lane mask: operate on (R,B) in low 16s and (G,A) in high 16s separately // + M selects bytes 0 and 2 in each 32-bit word // + ROUND is per-lane rounding before >> 8 const M = 0x00FF00FF, ROUND = 0x00800080; let sv, iv, s_lo, s_hi, i_lo, i_hi, o_lo, o_hi; for (let p = 0, pz = s32.length | 0; p < pz; p++) { sv = s32[p]; iv = i32[p]; // Split into two 16-bit lanes: low bytes (R,B), high bytes (G,A) s_lo = sv & M; s_hi = (sv >>> 8) & M; i_lo = iv & M; i_hi = (iv >>> 8) & M; // Per-lane blend with fixed-point 8.8 o_lo = (((s_lo * ak) + (i_lo * k) + ROUND) >>> 8) & M; o_hi = (((s_hi * ak) + (i_hi * k) + ROUND) >>> 8) & M; // Repack lanes back to RGBA s32[p] = ((o_hi << 8) & 0xFF00FF00) | o_lo; } }; const transferDataUnchanged = function (oData, iData, len) { if (len === iData.length) oData.set(iData); else oData.set(iData.subarray(0, len)); }; const getGaussianCoeffCache = () => { const COEFFS_KEY = 'gaussian-blur::coeffs'; let m = getWorkstoreItem(COEFFS_KEY); if (!m) { m = new Map(); setWorkstoreItem(COEFFS_KEY, m); } return m; }; const gaussCoefRawFloat = (sigmaIn) => { let sigma = sigmaIn; if (sigma < 0.5) sigma = 0.5; const a = _exp(0.726 * 0.726) / sigma, g1 = _exp(-a), g2 = _exp(-2 * a); const a0 = (1 - g1) * (1 - g1) / (1 + 2 * a * g1 - g2), a1 = a0 * (a - 1) * g1, a2 = a0 * (a + 1) * g1, a3 = -a0 * g2, b1 = 2 * g1, b2 = -g2; const left_corner = (a0 + a1) / (1 - b1 - b2), right_corner = (a2 + a3) / (1 - b1 - b2); return new Float32Array([a0, a1, a2, a3, b1, b2, left_corner, right_corner]); }; const getGaussianCoeffsFloat = (sigma) => { const s = (sigma > 0 ? sigma : 0) || 0, key = s < 0.5 ? 0.5 : Math.round(s * 1024) / 1024, cache = getGaussianCoeffCache(); let c = cache.get(key); if (!c) { c = gaussCoefRawFloat(key); cache.set(key, c); } return c; }; // ## Filter action functions // Each function is held in the `theBigActionsObject` object, for convenience P.theBigActionsObject = { // __alpha-to-channels__ - Copies the alpha channel value over to the selected value or, alternatively, sets that channel's value to zero, or leaves the channel's value unchanged. Setting the appropriate "includeChannel" flags will copy the alpha channel value to that channel; when that flag is false, setting the appropriate "excludeChannel" flag will set that channel's value to zero. // __alpha-to-channels__ (32-bit view + byte masks) [ALPHA_TO_CHANNELS]: function (requirements) { const [input, output] = getInputAndOutputLines(requirements); const iData = input.data, oData = output.data; const src32 = new Uint32Array(iData.buffer, iData.byteOffset, iData.byteLength >>> 2), out32 = new Uint32Array(oData.buffer, oData.byteOffset, oData.byteLength >>> 2); const { opacity = 1, includeRed = true, includeGreen = true, includeBlue = true, excludeRed = true, excludeGreen = true, excludeBlue = true, lineOut, } = requirements; const Rb = 0x000000FF, Gb = 0x0000FF00, Bb = 0x00FF0000; // Channels to receive alpha const incMask = (includeRed ? Rb : 0) | (includeGreen ? Gb : 0) | (includeBlue ? Bb : 0); // Channels to zero (only when NOT included) const zeroMask = (!includeRed && excludeRed ? Rb : 0) | (!includeGreen && excludeGreen ? Gb : 0) | (!includeBlue && excludeBlue ? Bb : 0); // Fast path: if we’re not changing RGB at all, only set A=255 for nonzero A const onlyAlphaTo255 = (incMask | zeroMask) === 0; if (onlyAlphaTo255) { let p, pz, s, a; for (p = 0, pz = src32.length | 0; p < pz; p++) { s = src32[p]; a = (s >>> 24) & 0xFF; if (a === 0) continue; out32[p] = (s & 0x00FFFFFF) | 0xFF000000; } } else { const rgbMask = 0x00FFFFFF; let p, pz, s, a, rgb, aRGB; for (p = 0, pz = src32.length | 0; p < pz; p++) { s = src32[p]; a = (s >>> 24) & 0xFF; if (a === 0) continue; rgb = s & rgbMask; if (zeroMask) rgb &= ~zeroMask; if (incMask) { aRGB = (a * 0x00010101) & rgbMask; rgb = (rgb & ~incMask) | (aRGB & incMask); } out32[p] = 0xFF000000 | rgb; } } if (lineOut) processResults(output, input, 1 - opacity); else processResults(cache.work, output, opacity); }, // __alpha-to-luminance__ - Sets the OKLAB luminance channel to the value of the alpha channel, then sets the alpha channel to opaque and the A and B channels to 0 (gray) [ALPHA_TO_LUMINANCE]: function (requirements) { // A small LUT for oklab gray const LUMINANCE_OKLAB_GRAY_LUT = 'alpha-to-luminance-oklab-gray-lut-256'; const getOklabGrayLut = () => { let lut = getWorkstoreItem(LUMINANCE_OKLAB_GRAY_LUT); if (lut != null) return lut; else { lut = new Uint32Array(256); const libs = colorEngine.getRgbOkCache(); let a, L, r, g, b; for (a = 0; a < 256; a++) { L = a / 256; if (L > 1) L = 1; else if (L < 0) L = 0; [r, g, b] = colorEngine.getRgbValsForOklab(L, 0, 0, libs); lut[a] = (255 << 24) | (b << 16) | (g << 8) | r; } setWorkstoreItem(LUMINANCE_OKLAB_GRAY_LUT, lut); return lut; } }; const [input, output] = getInputAndOutputLines(requirements); const iData = input.data, oData = output.data; const src32 = new Uint32Array(iData.buffer, iData.byteOffset, iData.byteLength >>> 2), out32 = new Uint32Array(oData.buffer, oData.byteOffset, oData.byteLength >>> 2); const { opacity = 1, lineOut, } = requirements; const lut = getOklabGrayLut(); const RGB_MASK = 0x00FFFFFF; let p, pz, s, a; for (p = 0, pz = src32.length | 0; p < pz; p++) { s = src32[p]; a = (s >>> 24) & 0xFF; if (a === 0) out32[p] = s & RGB_MASK; else out32[p] = lut[a]; } if (lineOut) processResults(output, input, 1 - opacity); else processResults(cache.work, output, opacity); }, // __area-alpha__ - Places a tile schema across the input, quarters each tile and then sets the alpha channels of the pixels in selected quarters of each tile to zero. Can be used to create horizontal or vertical bars, or chequerboard effects. [AREA_ALPHA]: function (requirements) { const [input, output] = getInputAndOutputLines(requirements); const iData = input.data, oData = output.data, len = iData.length, width = input.width, height = input.height; const { opacity = 1, tileWidth = 1, tileHeight = 1, offsetX = 0, offsetY = 0, gutterWidth = 1, gutterHeight = 1, // [top-left, bottom-left, top-right, bottom-right] areaAlphaLevels = [255, 0, 0, 0], lineOut, } = requirements; transferDataUnchanged(oData, iData, len); let tW = (_isFinite(tileWidth) ? tileWidth : 1) | 0, tH = (_isFinite(tileHeight) ? tileHeight : 1) | 0, gW = (_isFinite(gutterWidth) ? gutterWidth : 1) | 0, gH = (_isFinite(gutterHeight) ? gutterHeight : 1) | 0; if (tW < 1) tW = 1; if (tH < 1) tH = 1; if (tW + gW >= width) { tW = _max(1, width - gW - 1); gW = _max(1, width - tW - 1); } if (tH + gH >= height) { tH = _max(1, height - gH - 1); gH = _max(1, height - tH - 1); } const aW = tW + gW, aH = tH + gH; let offX = (_isFinite(offsetX) ? offsetX : 0) | 0, offY = (_isFinite(offsetY) ? offsetY : 0) | 0; if (offX < 0) offX = 0; else if (offX >= aW) offX = aW - 1; if (offY < 0) offY = 0; else if (offY >= aH) offY = aH - 1; const mod = (a, m) => { const r = a % m; return r < 0 ? r + m : r; }; let y, localY, inCoreY, localX, x, inCoreX, idx, a, segmentSpan, runLen, remain, p, k; for (y = 0; y < height; y++) { localY = mod(y - offY, aH); inCoreY = (localY < tH); localX = mod(0 - offX, aW); x = 0; while (x < width) { inCoreX = (localX < tW); idx = inCoreY ? (inCoreX ? 0 : 2) : (inCoreX ? 1 : 3); a = areaAlphaLevels[idx] | 0; segmentSpan = inCoreX ? (tW - localX) : (aW - localX); runLen = segmentSpan; remain = width - x; if (runLen > remain) runLen = remain; p = ((y * width) + x) * 4 + 3; for (k = 0; k < runLen; k++) { if (iData[p]) oData[p] = a; p += 4; } x += runLen; localX = (localX + runLen) % aW; } } if (lineOut) processResults(output, input, 1 - opacity); else processResults(cache.work, output, opacity); }, // __average-channels__ - Calculates an average value from each pixel's included channels and applies that value to all channels that have not been specifically excluded; excluded channels have their values set to 0. [AVERAGE_CHANNELS]: function (requirements) { const [input, output] = getInputAndOutputLines(requirements); const iData = input.data, oData = output.data; const src32 = new Uint32Array(iData.buffer, iData.byteOffset, iData.byteLength >>> 2), out32 = new Uint32Array(oData.buffer, oData.byteOffset, oData.byteLength >>> 2); const { opacity = 1, includeRed = true, includeGreen = true, includeBlue = true, excludeRed = false, excludeGreen = false, excludeBlue = false, lineOut, } = requirements; // Precompute divisor (how many channels contribute to the average) const divisor = (includeRed ? 1 : 0) + (includeGreen ? 1 : 0) + (includeBlue ? 1 : 0); // Fast path flags (turned into ints to help JIT) const incR = includeRed | 0, incG = includeGreen | 0, incB = includeBlue | 0, excR = excludeRed | 0, excG = excludeGreen | 0, excB = excludeBlue | 0; // Walk one pixel per iteration let p, rgba, r, g, b, a, rOut, gOut, bOut, sum, avg; for (p = 0; p < src32.length; p++) { rgba = src32[p]; r = rgba & 0xff; g = (rgba >>> 8) & 0xff; b = (rgba >>> 16) & 0xff; a = (rgba >>> 24) & 0xff; if (a === 0) { out32[p] = rgba; continue; } if (divisor) { sum = (incR ? r : 0) + (incG ? g : 0) + (incB ? b : 0); avg = (sum / divisor) | 0; rOut = excR ? 0 : avg; gOut = excG ? 0 : avg; bOut = excB ? 0 : avg; } else { rOut = excR ? 0 : r; gOut = excG ? 0 : g; bOut = excB ? 0 : b; } out32[p] = ((a << 24) | (bOut << 16) | (gOut << 8) | (rOut << 0)) >>> 0; } if (lineOut) processResults(output, input, 1 - opacity); else processResults(cache.work, output, opacity); }, // __blend__ - Using two source images (from the "lineIn" and "lineMix" arguments), combine their color information using various separable and non-separable blend modes (as defined by the W3C Compositing and Blending Level 1 recommendations). // + The blending method is determined by the String value supplied in the "blend" argument; permitted values are: 'color-burn', 'color-dodge', 'darken', 'difference', 'exclusion', 'hard-light', 'lighten', 'lighter', 'multiply', 'overlay', 'screen', 'soft-light', 'color', 'hue', 'luminosity', and 'saturation'. // + Scrawl-canvas uses the OKLCH color space to calculate color, hue, luminosity and saturation blends, which may lead to unexpecgted results for users coming from other products. SC also includes the "missing" combinations: 'hue-match', and 'chroma-match'. // + Note that the source images may be of different sizes: the output (lineOut) image size will be the same as the source (NOT lineIn) image; the lineMix image can be moved relative to the lineIn image using the "offsetX" and "offsetY" arguments. [BLEND]: function (requirements) { const [input, output, mix] = getInputAndOutputLines(requirements); const iWidth = input.width | 0, iHeight = input.height | 0, iData = input.data, mWidth = mix.width | 0, mHeight = mix.height | 0, mData = mix.data, oData = output.data; const { opacity = 1, blend = ZERO_STR, offsetX = 0, offsetY = 0, lineOut, } = requirements || {}; if (!iWidth || !iHeight) { if (lineOut) processResults(output, input, 1 - opacity); else processResults(cache.work, output, opacity); return; } oData.set(iData); const nPixInput = (iWidth * iHeight) | 0, nPixMix = (mWidth * mHeight) | 0; const i32 = new Uint32Array(iData.buffer, iData.byteOffset, nPixInput), m32 = new Uint32Array(mData.buffer, mData.byteOffset, nPixMix), o32 = new Uint32Array(oData.buffer, oData.byteOffset, nPixInput); const x0 = (offsetX > 0 ? offsetX : 0) | 0, y0 = (offsetY > 0 ? offsetY : 0) | 0, x1 = _min(iWidth, offsetX + mWidth) | 0, y1 = _min(iHeight, offsetY + mHeight) | 0; const hasOverlap = (x1 > x0) && (y1 > y0); if (!hasOverlap) { if (lineOut) processResults(output, input, 1 - opacity); else processResults(cache.work, output, opacity); return; } const inv255 = 1 / 255; const libs = colorEngine.getRgbOkCache(); const isOkBlend = OK_BLENDS.includes(blend); const mx0 = (x0 - offsetX) | 0, my0 = (y0 - offsetY) | 0; let y, my, x, mx, iPix, mPix, ip, mp, ia8, ma8, ir, ig, ib, mr, mg, mb, As, Ab, br, bg, bb, Fr, Fg, Fb, Sr, Sg, Sb, Br, Bg, Bb, k, oneMinusAs, oneMinusAb, R, G, B, A, outR, outG, outB, outA, IL, IC, IH, ML, MC, MH, okSrc, okMix, tmp; for (y = y0, my = my0; y < y1; y++, my++) { for (x = x0, mx = mx0; x < x1; x++, mx++) { iPix = (y * iWidth + x) | 0; mPix = (my * mWidth + mx) | 0; ip = i32[iPix]; mp = m32[mPix]; ia8 = ip >>> 24; ma8 = mp >>> 24; if (ia8 === 0 || ma8 === 0) continue; ir = ip & 0xFF; ig = (ip >>> 8) & 0xFF; ib = (ip >>> 16) & 0xFF; mr = mp & 0xFF; mg = (mp >>> 8) & 0xFF; mb = (mp >>> 16) & 0xFF; As = ia8 * inv255; Ab = ma8 * inv255; if (isOkBlend) { okSrc = colorEngine.getOkValsForRgb(ir, ig, ib, libs); okMix = colorEngine.getOkValsForRgb(mr, mg, mb, libs); IL = okSrc[0]; IC = okSrc[3]; IH = okSrc[4]; ML = okMix[0]; MC = okMix[3]; MH = okMix[4]; switch (blend) { case COLOR: [br, bg, bb] = colorEngine.getRgbValsForOklch(ML, IC, IH, libs); break; case HUE_MATCH: [br, bg, bb] = colorEngine.getRgbValsForOklch(IL, MC, IH, libs); break; case CHROMA_MATCH: [br, bg, bb] = colorEngine.getRgbValsForOklch(IL, IC, MH, libs); break; case HUE: [br, bg, bb] = colorEngine.getRgbValsForOklch(ML, MC, IH, libs); break; case SATURATION: [br, bg, bb] = colorEngine.getRgbValsForOklch(ML, IC, MH, libs); break; case LUMINOSITY: [br, bg, bb] = colorEngine.getRgbValsForOklch(IL, MC, MH, libs); break; } Fr = br * inv255; Fg = bg * inv255; Fb = bb * inv255; Sr = ir * inv255; Sg = ig * inv255; Sb = ib * inv255; Br = mr * inv255; Bg = mg * inv255; Bb = mb * inv255; k = As * Ab; oneMinusAs = 1 - As; oneMinusAb = 1 - Ab; R = Sr * oneMinusAb + Br * oneMinusAs + Fr * k; G = Sg * oneMinusAb + Bg * oneMinusAs + Fg * k; B = Sb * oneMinusAb + Bb * oneMinusAs + Fb * k; A = As + Ab - As * Ab; } else { Sr = ir * inv255; Sg = ig * inv255; Sb = ib * inv255; Br = mr * inv255; Bg = mg * inv255; Bb = mb * inv255; switch (blend) { case COLOR_BURN: if (Sr === 0) Fr = 0; else if (Br === 1) Fr = 1; else { tmp = (1 - Br) / Sr; if (tmp > 1) tmp = 1; Fr = 1 - tmp; } if (Sg === 0) Fg = 0; else if (Bg === 1) Fg = 1; else { tmp = (1 - Bg) / Sg; if (tmp > 1) tmp = 1; Fg = 1 - tmp; } if (Sb === 0) Fb = 0; else if (Bb === 1) Fb = 1; else { tmp = (1 - Bb) / Sb; if (tmp > 1) tmp = 1; Fb = 1 - tmp; } break; case COLOR_DODGE: if (Sr === 1) Fr = 1; else if (Br === 0) Fr = 0; else { tmp = Br / (1 - Sr); Fr = tmp > 1 ? 1 : tmp; } if (Sg === 1) Fg = 1; else if (Bg === 0) Fg = 0; else { tmp = Bg / (1 - Sg); Fg = tmp > 1 ? 1 : tmp; } if (Sb === 1) Fb = 1; else if (Bb === 0) Fb = 0; else { tmp = Bb / (1 - Sb); Fb = tmp > 1 ? 1 : tmp; } break; case DARKEN: Fr = _min(Sr, Br); Fg = _min(Sg, Bg); Fb = _min(Sb, Bb); break; case LIGHTEN: Fr = _max(Sr, Br); Fg = _max(Sg, Bg); Fb = _max(Sb, Bb); break; case LIGHTER: Fr = _min(1, Sr + Br); Fg = _min(1, Sg + Bg); Fb = _min(1, Sb + Bb); break; case MULTIPLY: Fr = Sr * Br; Fg = Sg * Bg; Fb = Sb * Bb; break; case SCREEN: Fr = Br + Sr - Br * Sr; Fg = Bg + Sg - Bg * Sg; Fb = Bb + Sb - Bb * Sb; break; case DIFFERENCE: Fr = _abs(Sr - Br); Fg = _abs(Sg - Bg); Fb = _abs(Sb - Bb); break; case EXCLUSION: Fr = Sr + Br - 2 * Sr * Br; Fg = Sg + Bg - 2 * Sg * Bg; Fb = Sb + Bb - 2 * Sb * Bb; break; case OVERLAY: Fr = (Br <= 0.5 ? 2 * Sr * Br : 1 - 2 * (1 - Sr) * (1 - Br)); Fg = (Bg <= 0.5 ? 2 * Sg * Bg : 1 - 2 * (1 - Sg) * (1 - Bg)); Fb = (Bb <= 0.5 ? 2 * Sb * Bb : 1 - 2 * (1 - Sb) * (1 - Bb)); break; case HARD_LIGHT: Fr = (Sr <= 0.5 ? 2 * Sr * Br : 1 - 2 * (1 - Sr) * (1 - Br)); Fg = (Sg <= 0.5 ? 2 * Sg * Bg : 1 - 2 * (1 - Sg) * (1 - Bg)); Fb = (Sb <= 0.5 ? 2 * Sb * Bb : 1 - 2 * (1 - Sb) * (1 - Bb)); break; case SOFT_LIGHT: { const DBr = (Br <= 0.25) ? (((16 * Br - 12) * Br) + 4) * Br : _sqrt(Br); const DBg = (Bg <= 0.25) ? (((16 * Bg - 12) * Bg) + 4) * Bg : _sqrt(Bg); const DBb = (Bb <= 0.25) ? (((16 * Bb - 12) * Bb) + 4) * Bb : _sqrt(Bb); Fr = (Sr <= 0.5) ? (Br - (1 - 2 * Sr) * Br * (1 - Br)) : (Br + (2 * Sr - 1) * (DBr - Br)); Fg = (Sg <= 0.5) ? (Bg - (1 - 2 * Sg) * Bg * (1 - Bg)) : (Bg + (2 * Sg - 1) * (DBg - Bg)); Fb = (Sb <= 0.5) ? (Bb - (1 - 2 * Sb) * Bb * (1 - Bb)) : (Bb + (2 * Sb - 1) * (DBb - Bb)); break; } default: Fr = Sr; Fg = Sg; Fb = Sb; } k = As * Ab; oneMinusAs = 1 - As; oneMinusAb = 1 - Ab; R = Sr * oneMinusAb + Br * oneMinusAs + Fr * k; G = Sg * oneMinusAb + Bg * oneMinusAs + Fg * k; B = Sb * oneMinusAb + Bb * oneMinusAs + Fb * k; A = As + Ab - As * Ab; } outR = (R * 255) | 0; outG = (G * 255) | 0; outB = (B * 255) | 0; outA = (A * 255) | 0; o32[iPix] = (outA << 24) | (outB << 16) | (outG << 8) | outR; } } if (lineOut) processResults(output, input, 1 - opacity); else processResults(cache.work, output, opacity); }, // __blur__ - Performs a multi-loop, two-step 'horizontal-then-vertical averaging sweep' calculation across all pixels to create a blur effect. [BLUR]: function (requirements) { // `getBlurPrefixBuffers` Prefix buffers for blur filter (inclusive prefix sums). const getBlurPrefixBuffers = function (len, axisKey) { const name = `blur-prefix-${axisKey}-${len}`; let obj = getWorkstoreItem(name); if (obj) return obj; const n = (len + 1), bytes = n * 4 * 4, buf = new ArrayBuffer(bytes); const r = new Uint32Array(buf, 0, n), g = new Uint32Array(buf, n * 4, n), b = new Uint32Array(buf, n * 8, n), a = new Uint32Array(buf, n * 12, n); obj = { r, g, b, a }; setWorkstoreItem(name, obj); return obj; }; // `buildHorizontalBlur` - creates an Array of Arrays detailing which pixels contribute to the horizontal part of each pixel's blur calculation. Resulting object will be cached in the store const buildHorizontalBlur = function (gridWidth, gridHeight, radius) { if (!_isFinite(radius)) radius = 0; const name = `blur-h-${gridWidth}-${gridHeight}-${radius}`, itemInWorkstore = getWorkstoreItem(name); if (itemInWorkstore) return itemInWorkstore; const startX = new Uint16Array(gridWidth * gridHeight); const endX = new Uint16Array(gridWidth * gridHeight); let x, y, p, sx, ex; for (y = 0; y < gridHeight; y++) { for (x = 0; x < gridWidth; x++) { p = (y * gridWidth) + x; sx = x - radius; ex = x + radius; if (sx < 0) sx = 0; if (ex >= gridWidth) ex = gridWidth - 1; startX[p] = sx; endX[p] = ex; } } const horizontalRanges = { startX, endX, width: gridWidth, height: gridHeight, kind: 'range-h' }; setWorkstoreItem(name, horizontalRanges); return horizontalRanges; }; // `buildVerticalBlur` - creates an Array of Arrays detailing which pixels contribute to the vertical part of each pixel's blur calculation. Resulting object will be cached in the store const buildVerticalBlur = function (gridWidth, gridHeight, radius) { if (!_isFinite(radius)) radius = 0; const name = `blur-v-${gridWidth}-${gridHeight}-${radius}`, itemInWorkstore = getWorkstoreItem(name); if (itemInWorkstore) return itemInWorkstore; const startY = new Uint16Array(gridWidth * gridHeight); const endY = new Uint16Array(gridWidth * gridHeight); let x, y, p, sy, ey; for (y = 0; y < gridHeight; y++) { for (x = 0; x < gridWidth; x++) { p = (y * gridWidth) + x; sy = y - radius; ey = y + radius; if (sy < 0) sy = 0; if (ey >= gridHeight) ey = gridHeight - 1; startY[p] = sy; endY[p] = ey; } } const verticalRanges = { startY, endY, width: gridWidth, height: gridHeight, kind: 'range-v' }; setWorkstoreItem(name, verticalRanges); return verticalRanges; }; const [input, output] = getInputAndOutputLines(requirements); const iData = input.data, oData = output.data, len = iData.length, pixelLen = _floor(len / 4); const { opacity = 1, processVertical = true, radiusVertical = 0, passesVertical = 1, stepVertical = 1, processHorizontal = true, radiusHorizontal = 0, passesHorizontal = 1, stepHorizontal = 1, includeRed = true, includeGreen = true, includeBlue = true, includeAlpha = false, excludeTransparentPixels = false, lineOut, } = requirements; if ((!processVertical && !processHorizontal) || (!includeRed && !includeGreen && !includeBlue && !includeAlpha)) transferDataUnchanged(oData, iData, len); else { const gridWidth = input.width, gridHeight = input.height; let horizontalBlurGrid, verticalBlurGrid; if (processHorizontal || processVertical) { if (processHorizontal) horizontalBlurGrid = buildHorizontalBlur(gridWidth, gridHeight, radiusHorizontal); if (processVertical) verticalBlurGrid = buildVerticalBlur(gridWidth, gridHeight, radiusVertical); } oData.set(iData); const hold = new Uint8ClampedArray(iData); let pass, counter, rIdx, gIdx, bIdx, aIdx, startX, endX, width, height, sx, ex, y, rowBase, step4, sumR, sumG, sumB, sumA, countRGB, totalCount, idx, c, aVal, startY, endY, sy, ey, x, stepRow4, pr, pg, pb, pa, base, pos, count; const canFastH = (stepHorizontal === 1) && !excludeTransparentPixels, canFastV = (stepVertical === 1) && !excludeTransparentPixels; if (processHorizontal) { for (pass = 0; pass < passesHorizontal; pass++) { if (canFastH) { ({ startX, endX, width, height } = horizontalBlurGrid); ({ r: pr, g: pg, b: pb, a: pa } = getBlurPrefixBuffers(width, 'h')); for (y = 0; y < height; y++) { base = (y * width) << 2; if (includeRed) pr[0] = 0; if (includeGreen) pg[0] = 0; if (includeBlue) pb[0] = 0; if (includeAlpha) pa[0] = 0; for (let x = 0; x < width; x++) { idx = base + (x << 2); if (includeRed) pr[x + 1] = pr[x] + hold[idx]; if (includeGreen) pg[x + 1] = pg[x] + hold[idx + 1]; if (includeBlue) pb[x + 1] = pb[x] + hold[idx + 2]; if (includeAlpha) pa[x + 1] = pa[x] + hold[idx + 3]; } for (let x = 0; x < width; x++) { pos = (y * width) + x; sx = startX[pos]; ex = endX[pos]; count = (ex - sx + 1); idx = base + (x << 2); if (includeRed) oData[idx] = (pr[ex + 1] - pr[sx]) / count; else oData[idx] = hold[idx]; if (includeGreen) oData[idx + 1] = (pg[ex + 1] - pg[sx]) / count; else oData[idx + 1] = hold[idx + 1]; if (includeBlue) oData[idx + 2] = (pb[ex + 1] - pb[sx]) / count; else oData[idx + 2] = hold[idx + 2]; if (includeAlpha) oData[idx + 3] = (pa[ex + 1] - pa[sx]) / count; else oData[idx + 3] = hold[idx + 3]; } } } else { for (counter = 0; counter < pixelLen; counter++) { rIdx = counter * 4; gIdx = rIdx + 1; bIdx = gIdx + 1; aIdx = bIdx + 1; if (includeAlpha || hold[aIdx]) { ({ startX, endX, width } = horizontalBlurGrid); sx = startX[counter]; ex = endX[counter]; y = (counter / width) | 0; rowBase = (y * width) * 4; step4 = stepHorizontal << 2; sumR = 0; sumG = 0; sumB = 0; sumA = 0; countRGB = 0; totalCount = ((ex - sx) / stepHorizontal | 0) + 1; idx = rowBase + (sx << 2); if (!excludeTransparentPixels) { for (c = sx; c <= ex; c += stepHorizontal) { if (includeRed) sumR += hold[idx]; if (includeGreen) sumG += hold[idx + 1]; if (includeBlue) sumB += hold[idx + 2]; if (includeAlpha) sumA += hold[idx + 3]; idx += step4; } if (includeRed) oData[rIdx] = sumR / totalCount; else oData[rIdx] = hold[rIdx]; if (includeGreen) oData[gIdx] = sumG / totalCount; else oData[gIdx] = hold[gIdx]; if (includeBlue) oData[bIdx] = sumB / totalCount; else oData[bIdx] = hold[bIdx]; if (includeAlpha) oData[aIdx] = sumA / totalCount; else oData[aIdx] = hold[aIdx]; } else { for (c = sx; c <= ex; c += stepHorizontal) { aVal = hold[idx + 3]; if (aVal) { if (includeRed) sumR += hold[idx]; if (includeGreen) sumG += hold[idx + 1]; if (includeBlue) sumB += hold[idx + 2]; countRGB++; } if (includeAlpha) sumA += aVal; idx += step4; } if (includeRed) oData[rIdx] = countRGB ? (sumR / countRGB) : hold[rIdx]; else oData[rIdx] = hold[rIdx]; if (includeGreen) oData[gIdx] = countRGB ? (sumG / countRGB) : hold[gIdx]; else oData[gIdx] = hold[gIdx]; if (includeBlue) oData[bIdx] = countRGB ? (sumB / countRGB) : hold[bIdx]; else oData[bIdx] = hold[bIdx]; if (includeAlpha) oData[aIdx] = sumA / totalCount; else oData[aIdx] = hold[aIdx]; } } } } if (processVertical || pass < passesHorizontal - 1) hold.set(oData); } } if (processVertical) { for (pass = 0; pass < passesVertical; pass++) { if (canFastV) { ({ startY, endY, width, height } = verticalBlurGrid); ({ r: pr, g: pg, b: pb, a: pa } = getBlurPrefixBuffers(height, 'v')); for (x = 0; x < width; x++) { if (includeRed) pr[0] = 0; if (includeGreen) pg[0] = 0; if (includeBlue) pb[0] = 0; if (includeAlpha) pa[0] = 0; for (y = 0; y < height; y++) {