UNPKG

q5

Version:

Beginner friendly graphics powered by WebGPU and optimized for interactive art!

2,151 lines (1,837 loc) 201 kB
/** * q5.js * @version 3.7 * @author quinton-ashley * @contributors evanalulu, Tezumie, ormaq, Dukemz, LingDong- * @license LGPL-3.0 * @class Q5 */ function Q5(scope, parent, renderer) { let $ = this; $._isQ5 = $._q5 = true; $._parent = parent; let readyResolve; $.ready = new Promise((res) => { readyResolve = res; }); if (renderer == 'webgpu-fallback') { $._renderer = 'c2d'; $._webgpu = $._webgpuFallback = true; } else { $._renderer = renderer || 'c2d'; $['_' + $._renderer] = true; } let autoLoaded = scope == 'auto'; scope ??= 'global'; if (scope == 'auto') { if (!(window.setup || window.update || Q5.update || window.draw || Q5.draw)) return; scope = 'global'; } let globalScope; if (scope == 'global') { Q5._hasGlobal = $._isGlobal = true; globalScope = Q5._esm ? globalThis : !Q5._server ? window : global; } if (scope == 'graphics') $._isGraphics = true; if (scope == 'image') $._isImage = true; let q = new Proxy($, { set: (t, p, v) => { $[p] = v; if ($._isGlobal) globalScope[p] = v; return true; } }); $.canvas = $.ctx = $.drawingContext = null; $.pixels = []; let looper = null, useRAF = true; $.frameCount = 0; $.deltaTime = 16; $._targetFrameRate = 0; $._targetFrameDuration = 16.666666666666668; $._frameRate = $._fps = 60; $._loop = true; async function runHooks(name) { for (let hook of Q5.hooks[name]) { await hook.call($, q); } } let millisStart = 0; $.millis = () => performance.now() - millisStart; $.noCanvas = () => { if ($.canvas?.remove) $.canvas.remove(); $.canvas = 0; q.ctx = q.drawingContext = 0; }; if (window) { $.windowWidth = window.innerWidth; $.windowHeight = window.innerHeight; $.deviceOrientation = window.screen?.orientation?.type; } $._loaders = []; $.loadAll = () => { let loaders = [...$._loaders]; $._loaders = []; if ($._g) { loaders = loaders.concat($._g._loaders); $._g._loaders = []; } return Promise.all(loaders); }; $.isPreloadSupported = () => true; $.disablePreload = () => ($._disablePreload = true); const resolvers = []; $._incrementPreload = () => { $._loaders.push(new Promise((resolve) => resolvers.push(resolve))); }; $._decrementPreload = () => { if (resolvers.length) resolvers.pop()(); }; async function _draw(timestamp) { let ts = timestamp || performance.now(); if ($._didResize) { $.windowResized(); $._didResize = false; } if ($._loop) { if (useRAF) looper = raf(_draw); else { let nextTS = ts + $._targetFrameDuration; let frameDelay = nextTS - performance.now(); while (frameDelay < 0) frameDelay += $._targetFrameDuration; looper = setTimeout(() => _draw(nextTS), frameDelay); } } else if ($.frameCount && !$._redraw) return; if ($.frameCount && useRAF && !$._redraw) { let timeSinceLast = ts - $._lastFrameTime; if (timeSinceLast < $._targetFrameDuration - 4) return; } q.deltaTime = ts - $._lastFrameTime; $._frameRate = 1000 / $.deltaTime; q.frameCount++; let pre = performance.now(); $.resetMatrix(); if ($._beginRender) $._beginRender(); await runHooks('predraw'); try { await $.draw(); } catch (e) { if (!Q5.errorTolerant) $.noLoop(); if ($._fes) $._fes(e); throw e; } await runHooks('postdraw'); await $.postProcess(); if ($._render) $._render(); if ($._finishRender) $._finishRender(); q.pmouseX = $.mouseX; q.pmouseY = $.mouseY; q.moveX = q.moveY = 0; $._lastFrameTime = ts; let post = performance.now(); $._fps = Math.round(1000 / (post - pre)); } $.noLoop = () => { $._loop = false; if (looper != null) { if (useRAF && window.cancelAnimationFrame) cancelAnimationFrame(looper); else clearTimeout(looper); } looper = null; }; $.loop = () => { $._loop = true; if ($._setupDone && looper == null) _draw(); }; $.isLooping = () => $._loop; $.redraw = async (n = 1) => { $._redraw = true; for (let i = 0; i < n; i++) { await _draw(); } $._redraw = false; }; $.remove = async () => { $.noLoop(); $.canvas.remove(); await runHooks('remove'); }; $.frameRate = (hz) => { if (hz && hz != $._targetFrameRate) { $._targetFrameRate = hz; $._targetFrameDuration = 1000 / hz; if ($._loop && looper != null) { if (useRAF && window.cancelAnimationFrame) cancelAnimationFrame(looper); else clearTimeout(looper); looper = null; } useRAF = hz <= 60; if ($._setupDone) { if (useRAF) looper = raf(_draw); else looper = setTimeout(() => _draw(), $._targetFrameDuration); } } return $._frameRate; }; $.getTargetFrameRate = () => $._targetFrameRate || 60; $.getFPS = () => $._fps; // shims for compatibility with p5.js libraries $.Element = function (a) { this.elt = a; }; $._elements = []; $.describe = () => {}; $.log = $.print = console.log; for (let m in Q5.modules) { Q5.modules[m]($, q); } let r = Q5.renderers[$._renderer]; for (let m in r) { r[m]($, q); } // INIT for (let k in Q5) { if (k[1] != '_' && k[1] == k[1].toUpperCase()) { $[k] = Q5[k]; } } if ($._isGraphics) return; if ($._isGlobal) { let tmp = Object.assign({}, $); delete tmp.Color; Object.assign(Q5, tmp); delete Q5.Q5; } for (let hook of Q5.hooks.init) { hook.call($, q); } for (let [n, fn] of Object.entries(Q5.prototype)) { if (n[0] != '_' && typeof $[n] == 'function') $[n] = fn.bind($); } for (let [n, fn] of Object.entries(Q5.preloadMethods)) { $[n] = function () { $._incrementPreload(); return fn.apply($, arguments); // the addon function is responsible for calling $._decrementPreload }; } if ($._isGlobal) { let props = Object.getOwnPropertyNames($); for (let p of props) { if (p[0] != '_') globalScope[p] = $[p]; } // to support p5.sound for (let p of ['_incrementPreload', '_decrementPreload']) { globalScope[p] = $[p]; } } if (typeof scope == 'function') scope($); Q5._instanceCount++; let raf = window.requestAnimationFrame || function (cb) { const idealFrameTime = $._lastFrameTime + $._targetFrameDuration; return setTimeout(() => { cb(idealFrameTime); }, idealFrameTime - performance.now()); }; let t = globalScope || $; let userFns = Q5._userFns.slice(0, 15); // shim if undefined for (let name of userFns) $[name] ??= () => {}; if ($._isGlobal) { let allUserFns = Q5._userFns.slice(0, 17); for (let name of allUserFns) { if (Q5[name]) $[name] = Q5[name]; else { Object.defineProperty(Q5, name, { get: () => $[name], set: (fn) => ($[name] = fn) }); } } } function wrapWithFES(name) { const fn = t[name] || $[name]; $[name] = (event) => { try { return fn(event); } catch (e) { if ($._fes) $._fes(e); throw e; } }; } async function start() { await runHooks('presetup'); readyResolve(); if (t.preload || $.preload) { wrapWithFES('preload'); $.preload(); } // wait for the user to define setup, update, or draw await Promise.race([ new Promise((resolve) => { function checkUserFns() { if ($.setup || $.update || $.draw || t.setup || t.update || t.draw) { resolve(); } else if (!$._setupDone) { // render during loading if ($.canvas?.ready && $._render) { $._beginRender(); $._render(); $._finishRender(); } raf(checkUserFns); } } checkUserFns(); }), new Promise((resolve) => { setTimeout(() => { // if not loading if (!$._loaders.length) resolve(); }, 500); }) ]); if (!$._disablePreload) { await $.loadAll(); } $.setup ??= t.setup || (() => {}); wrapWithFES('setup'); for (let name of userFns) wrapWithFES(name); $.draw ??= t.draw || (() => {}); millisStart = performance.now(); await $.setup(); $._setupDone = true; if ($.ctx === null) $.createCanvas(200, 200); await runHooks('postsetup'); if ($.frameCount) return; $._lastFrameTime = performance.now() - 15; raf(_draw); } Q5.instances.push($); if (autoLoaded || Q5._esm) start(); else setTimeout(start, 32); } Q5.renderers = {}; Q5.modules = {}; Q5._server = typeof process == 'object'; Q5._esm = this === undefined; Q5._instanceCount = 0; Q5.instances = []; Q5._friendlyError = (msg, func) => { if (!Q5.disableFriendlyErrors) console.error(func + ': ' + msg); }; Q5._validateParameters = () => true; Q5._userFns = [ 'postProcess', 'mouseMoved', 'mousePressed', 'mouseReleased', 'mouseDragged', 'mouseClicked', 'doubleClicked', 'mouseWheel', 'keyPressed', 'keyReleased', 'keyTyped', 'touchStarted', 'touchMoved', 'touchEnded', 'windowResized', 'update', 'draw' ]; Q5.hooks = { init: [], presetup: [], postsetup: [], predraw: [], postdraw: [], remove: [] }; Q5.addHook = (lifecycle, fn) => Q5.hooks[lifecycle].push(fn); // p5 v2 compat Q5.registerAddon = (addon) => { let lifecycles = {}; addon(Q5, Q5.prototype, lifecycles); for (let l in lifecycles) { Q5.hooks[l].push(lifecycles[l]); } }; // p5 v1 compat Q5.prototype.registerMethod = (m, fn) => { if (m == 'beforeSetup' || m.includes('Preload')) m = 'presetup'; if (m == 'afterSetup') m = 'postsetup'; if (m == 'pre') m = 'predraw'; if (m == 'post') m = 'postdraw'; Q5.hooks[m].push(fn); }; Q5.preloadMethods = {}; Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.preloadMethods[n] = fn[n]); function createCanvas(w, h, opt) { if (Q5._hasGlobal) return; let useC2D = w == 'c2d' || h == 'c2d' || opt == 'c2d' || opt?.renderer == 'c2d' || !Q5._esm; if (useC2D) { let q = new Q5(); let c = q.createCanvas(w, h, opt); return q.ready.then(() => c); } else { return Q5.WebGPU().then((q) => q.createCanvas(w, h, opt)); } } if (Q5._server) { global.q5 = global.Q5 = Q5; global.p5 ??= Q5; } if (typeof window == 'object') { window.q5 = window.Q5 = Q5; window.p5 ??= Q5; window.createCanvas = createCanvas; window.C2D = 'c2d'; window.WEBGPU = 'webgpu'; } else global.window = 0; Q5.version = Q5.VERSION = '3.7'; if (typeof document == 'object') { document.addEventListener('DOMContentLoaded', () => { if (!Q5._hasGlobal) { if (Q5.update || Q5.draw) { Q5.WebGPU(); } else { new Q5('auto'); } } }); } Q5.modules.canvas = ($, q) => { $._Canvas = window.OffscreenCanvas || function () { return document.createElement('canvas'); }; if (Q5._server) { if (Q5._createServerCanvas) { q.canvas = Q5._createServerCanvas(200, 200); } } else if ($._isImage || $._isGraphics) { q.canvas = new $._Canvas(200, 200); } if (!$.canvas) { if (typeof document == 'object') { q.canvas = document.createElement('canvas'); $.canvas.id = 'q5Canvas' + Q5._instanceCount; $.canvas.classList.add('q5Canvas'); } else $.noCanvas(); } $.displayDensity = () => window.devicePixelRatio || 1; $.width = 200; $.height = 200; $._pixelDensity = 1; let c = $.canvas; if (c) { c.width = 200; c.height = 200; c.colorSpace = Q5.canvasOptions.colorSpace; if (!$._isImage) { c.renderer = $._renderer; c[$._renderer] = true; $._pixelDensity = Math.ceil($.displayDensity()); } } $._adjustDisplay = (forced) => { let s = c.style; if (s && forced) { s.width = c.w + 'px'; s.height = c.h + 'px'; } }; $.createCanvas = function (w, h, options) { if (isNaN(w) || (typeof w == 'string' && !w.includes(':'))) { options = w; w = null; } if (typeof h != 'number') { options = h; h = null; } options ??= arguments[3]; if (typeof options == 'string') options = { renderer: options }; let opt = Object.assign({}, Q5.canvasOptions); if (typeof options == 'object') Object.assign(opt, options); if (!$._isImage) { if ($._isGraphics) $._pixelDensity = this._pixelDensity; else if (!Q5._server) { // the canvas can become detached from the DOM // if the innerHTML of one of its parents is edited // check if canvas is still attached to the DOM let el = c, root = document.body || document.documentElement; while (el && el.parentElement != root) { el = el.parentElement; } if (!el) { // reattach canvas to the DOM document.getElementById(c.id)?.remove(); addCanvas(); } if (window.IntersectionObserver) { let wasObserved = false; new IntersectionObserver((e) => { let isIntersecting = e[0].isIntersecting; if (!isIntersecting) { // the canvas might still be onscreen, just behind other elements let r = c.getBoundingClientRect(); c.visible = r.top < window.innerHeight && r.bottom > 0 && r.left < window.innerWidth && r.right > 0; } else c.visible = true; if (!wasObserved) { $._wasLooping = $._loop; wasObserved = true; } if (c.visible) { if ($._wasLooping && !$._loop) $.loop(); } else { $._wasLooping = $._loop; $.noLoop(); } }).observe(c); } } else c.visible = true; } $._setCanvasSize(w, h); Object.assign(c, opt); let rend = $._createCanvas(c.w, c.h, opt); if ($._addEventMethods) $._addEventMethods(c); $.canvas.ready = true; return rend; }; $.createGraphics = function (w, h, opt = {}) { if (typeof opt == 'string') opt = { renderer: opt }; let g = new Q5('graphics', undefined, opt.renderer || ($._webgpuFallback ? 'webgpu-fallback' : $._renderer)); opt.alpha ??= true; opt.colorSpace ??= $.canvas.colorSpace; opt.pixelDensity ??= $._pixelDensity; g._defaultImageScale = $._defaultImageScale; g.createCanvas.call($, w, h, opt); let scale = g._pixelDensity * $._defaultImageScale; g.defaultWidth = w * scale; g.defaultHeight = h * scale; return g; }; $._setCanvasSize = (w, h) => { if (w == undefined) h ??= window.innerHeight; else h ??= w; w ??= window.innerWidth; c.w = w = Math.ceil(w); c.h = h = Math.ceil(h); q.halfWidth = c.hw = w / 2; q.halfHeight = c.hh = h / 2; // changes the actual size of the canvas c.width = Math.ceil(w * $._pixelDensity); c.height = Math.ceil(h * $._pixelDensity); q.width = w; q.height = h; if ($.displayMode && !c.displayMode) $.displayMode(); else $._adjustDisplay(true); }; $._setImageSize = (w, h) => { q.width = c.w = w; q.height = c.h = h; q.halfWidth = c.hw = w / 2; q.halfHeight = c.hh = h / 2; // changes the actual size of the canvas c.width = Math.ceil(w * $._pixelDensity); c.height = Math.ceil(h * $._pixelDensity); }; $.defaultImageScale = (scale) => { if (!scale) return $._defaultImageScale; if ($._g) $._g._defaultImageScale = scale; return ($._defaultImageScale = scale); }; $.defaultImageScale(0.5); if ($._isImage) return; if (c && !$._isGraphics) { c.parent = (el) => { if (c.parentElement) c.parentElement.removeChild(c); if (typeof el == 'string') el = document.getElementById(el); el.append(c); function parentResized() { if ($.frameCount > 1) { $._didResize = true; $._adjustDisplay(); } } if (typeof ResizeObserver == 'function') { if ($._ro) $._ro.disconnect(); $._ro = new ResizeObserver(parentResized); $._ro.observe(el); } else if (!$.frameCount) { window.addEventListener('resize', parentResized); } }; function addCanvas() { let el = $._parent; el ??= document.getElementsByTagName('main')[0]; if (!el) { el = document.createElement('main'); let root = document.body || document.documentElement; root.appendChild(el); } c.parent(el); if (!document.body) { document.addEventListener('DOMContentLoaded', () => { if (document.body) document.body.appendChild(el); }); } } addCanvas(); } $.resizeCanvas = (w, h) => { if (!$.ctx) return $.createCanvas(w, h); if (w == c.w && h == c.h) return; $._resizeCanvas(w, h); }; if (c && !Q5._createServerCanvas) c.resize = $.resizeCanvas; $.pixelDensity = (v) => { if (!v || v == $._pixelDensity) return $._pixelDensity; $._pixelDensity = v; $._resizeCanvas(c.w, c.h); if ($._g) $._g.pixelDensity(v); return v; }; if (window && !$._isGraphics) { window.addEventListener('resize', () => { $._didResize = true; q.windowWidth = window.innerWidth; q.windowHeight = window.innerHeight; q.deviceOrientation = window.screen?.orientation?.type; }); } }; Q5.CENTER = 'center'; Q5.LEFT = 'left'; Q5.RIGHT = 'right'; Q5.TOP = 'top'; Q5.BOTTOM = 'bottom'; Q5.BASELINE = 'alphabetic'; Q5.MIDDLE = 'middle'; Q5.NORMAL = 'normal'; Q5.ITALIC = 'italic'; Q5.BOLD = 'bold'; Q5.BOLDITALIC = 'italic bold'; Q5.ROUND = 'round'; Q5.SQUARE = 'butt'; Q5.PROJECT = 'square'; Q5.MITER = 'miter'; Q5.BEVEL = 'bevel'; Q5.NONE = 'none'; Q5.SIMPLE = 'simple'; Q5.CHORD_OPEN = 0; Q5.PIE_OPEN = 1; Q5.PIE = 2; Q5.CHORD = 3; Q5.RADIUS = 'radius'; Q5.CORNER = 'corner'; Q5.CORNERS = 'corners'; Q5.OPEN = 0; Q5.CLOSE = 1; Q5.VIDEO = 'video'; Q5.AUDIO = 'audio'; Q5.LANDSCAPE = 'landscape'; Q5.PORTRAIT = 'portrait'; Q5.BLEND = 'source-over'; Q5.REMOVE = 'destination-out'; Q5.ADD = 'lighter'; Q5.DARKEST = 'darken'; Q5.LIGHTEST = 'lighten'; Q5.DIFFERENCE = 'difference'; Q5.SUBTRACT = 'subtract'; Q5.EXCLUSION = 'exclusion'; Q5.MULTIPLY = 'multiply'; Q5.SCREEN = 'screen'; Q5.REPLACE = 'copy'; Q5.OVERLAY = 'overlay'; Q5.HARD_LIGHT = 'hard-light'; Q5.SOFT_LIGHT = 'soft-light'; Q5.DODGE = 'color-dodge'; Q5.BURN = 'color-burn'; Q5.THRESHOLD = 1; Q5.GRAY = 2; Q5.OPAQUE = 3; Q5.INVERT = 4; Q5.POSTERIZE = 5; Q5.DILATE = 6; Q5.ERODE = 7; Q5.BLUR = 8; Q5.SEPIA = 9; Q5.BRIGHTNESS = 10; Q5.SATURATION = 11; Q5.CONTRAST = 12; Q5.HUE_ROTATE = 13; Q5.C2D = Q5.P2D = Q5.P2DHDR = 'c2d'; Q5.WEBGL = 'webgl'; Q5.GPU = Q5.WEBGPU = 'webgpu'; Q5.canvasOptions = { alpha: false, colorSpace: 'display-p3' }; if (!window.matchMedia || !matchMedia('(dynamic-range: high) and (color-gamut: p3)').matches) { Q5.canvasOptions.colorSpace = 'srgb'; } else Q5.supportsHDR = true; Q5.renderers.c2d = {}; Q5.renderers.c2d.canvas = ($, q) => { let c = $.canvas; if ($.colorMode) $.colorMode('rgb', $._webgpu ? 1 : 255); $._createCanvas = function (w, h, options) { if (!c) { console.error('q5 canvas could not be created. skia-canvas and jsdom packages not found.'); return; } q.ctx = q.drawingContext = c.getContext('2d', options); if (!$._isImage) { // default styles $.ctx.fillStyle = $._fill = 'white'; $.ctx.strokeStyle = $._stroke = 'black'; $.ctx.lineCap = 'round'; $.ctx.lineJoin = 'miter'; $.ctx.textAlign = 'left'; $._strokeWeight = 1; } $.ctx.scale($._pixelDensity, $._pixelDensity); $.ctx.save(); return c; }; $.clear = () => { $.ctx.save(); $.ctx.resetTransform(); $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height); $.ctx.restore(); }; if ($._isImage) return; $.background = function (c) { $.ctx.save(); $.ctx.resetTransform(); $.ctx.globalAlpha = 1; if (c.canvas) $.image(c, 0, 0, $.canvas.width, $.canvas.height); else { if (Q5.Color && !c._isColor) c = $.color(...arguments); $.ctx.fillStyle = c.toString(); $.ctx.fillRect(0, 0, $.canvas.width, $.canvas.height); } $.ctx.restore(); }; $._resizeCanvas = (w, h) => { let t = {}; for (let prop in $.ctx) { if (typeof $.ctx[prop] != 'function') t[prop] = $.ctx[prop]; } delete t.canvas; let o; if ($.frameCount > 1) { o = new $._Canvas(c.width, c.height); o.w = c.w; o.h = c.h; let oCtx = o.getContext('2d'); oCtx.drawImage(c, 0, 0); } $._setCanvasSize(w, h); for (let prop in t) $.ctx[prop] = t[prop]; $.scale($._pixelDensity); if (o) $.ctx.drawImage(o, 0, 0, o.w, o.h); }; $.fill = function (c) { $._doFill = $._fillSet = true; if (Q5.Color) { if (!c._isColor && (typeof c != 'string' || $._namedColors[c])) { c = $.color(...arguments); } if (c.a <= 0) return ($._doFill = false); } $.ctx.fillStyle = $._fill = c.toString(); }; $.stroke = function (c) { $._doStroke = $._strokeSet = true; if (Q5.Color) { if (!c._isColor && (typeof c != 'string' || $._namedColors[c])) { c = $.color(...arguments); } if (c.a <= 0) return ($._doStroke = false); } $.ctx.strokeStyle = $._stroke = c.toString(); }; $.strokeWeight = (n) => { if (!n) $._doStroke = false; $.ctx.lineWidth = $._strokeWeight = n || 0.0001; }; $.noFill = () => ($._doFill = false); $.noStroke = () => ($._doStroke = false); $.opacity = (a) => ($.ctx.globalAlpha = a); // polyfill for q5 WebGPU functions (used by q5play) $._getFillIdx = () => $._fill; $._setFillIdx = (v) => ($._fill = v); $._getStrokeIdx = () => $._stroke; $._setStrokeIdx = (v) => ($._stroke = v); $._doShadow = false; $._shadowOffsetX = $._shadowOffsetY = $._shadowBlur = 10; $.shadow = function (c) { if (Q5.Color) { if (!c._isColor && (typeof c != 'string' || $._namedColors[c])) { c = $.color(...arguments); } if (c.a <= 0) return ($._doShadow = false); } $.ctx.shadowColor = $._shadow = c.toString(); $._doShadow = true; $.ctx.shadowOffsetX ||= $._shadowOffsetX; $.ctx.shadowOffsetY ||= $._shadowOffsetY; $.ctx.shadowBlur ||= $._shadowBlur; }; $.shadowBox = (offsetX, offsetY, blur) => { $.ctx.shadowOffsetX = $._shadowOffsetX = offsetX; $.ctx.shadowOffsetY = $._shadowOffsetY = offsetY || offsetX; $.ctx.shadowBlur = $._shadowBlur = blur || 0; }; $.noShadow = () => { $._doShadow = false; $.ctx.shadowOffsetX = $.ctx.shadowOffsetY = $.ctx.shadowBlur = 0; }; // DRAWING MATRIX $.translate = (x, y) => { if (x.x) { y = x.y; x = x.x; } $.ctx.translate(x, y); }; $.rotate = (r) => { if ($._angleMode) r = $.radians(r); $.ctx.rotate(r); }; $.scale = (x, y) => { if (x.x) { y = x.y; x = x.x; } y ??= x; $.ctx.scale(x, y); }; $.applyMatrix = (a, b, c, d, e, f) => $.ctx.transform(a, b, c, d, e, f); $.shearX = (ang) => $.ctx.transform(1, 0, $.tan(ang), 1, 0, 0); $.shearY = (ang) => $.ctx.transform(1, $.tan(ang), 0, 1, 0, 0); $.resetMatrix = () => { if ($.ctx) { $.ctx.resetTransform(); $.scale($._pixelDensity); if ($._webgpu) $.translate($.halfWidth, $.halfHeight); } }; $._styleNames = [ '_fill', '_stroke', '_strokeWeight', '_doFill', '_doStroke', '_fillSet', '_strokeSet', '_shadow', '_doShadow', '_shadowOffsetX', '_shadowOffsetY', '_shadowBlur', '_tint', '_textSize', '_textAlign', '_textBaseline', '_imageMode', '_rectMode', '_ellipseMode', '_colorMode', '_colorFormat', 'Color' ]; $._styles = []; $.pushStyles = () => { let styles = {}; for (let s of $._styleNames) styles[s] = $[s]; $._styles.push(styles); if ($._fontMod) $._updateFont(); }; function popStyles() { let styles = $._styles.pop(); for (let s of $._styleNames) $[s] = styles[s]; } $.popStyles = () => { popStyles(); $.ctx.fillStyle = $._fill; $.ctx.strokeStyle = $._stroke; $.ctx.lineWidth = $._strokeWeight; $.ctx.shadowColor = $._shadow; $.ctx.shadowOffsetX = $._doShadow ? $._shadowOffsetX : 0; $.ctx.shadowOffsetY = $._doShadow ? $._shadowOffsetY : 0; $.ctx.shadowBlur = $._doShadow ? $._shadowBlur : 0; }; $.pushMatrix = () => $.ctx.save(); $.popMatrix = () => $.ctx.restore(); $.push = () => { $.pushStyles(); $.ctx.save(); }; $.pop = () => { $.ctx.restore(); popStyles(); }; }; Q5.renderers.c2d.shapes = ($) => { $._doStroke = true; $._doFill = true; $._strokeSet = false; $._fillSet = false; $._ellipseMode = Q5.CENTER; $._rectMode = Q5.CORNER; let firstVertex = true; let curveBuff = []; function ink() { if ($._doFill) $.ctx.fill(); if ($._doStroke) $.ctx.stroke(); } // DRAWING SETTINGS $.blendMode = (x) => ($.ctx.globalCompositeOperation = x); $.strokeCap = (x) => ($.ctx.lineCap = x); $.strokeJoin = (x) => ($.ctx.lineJoin = x); $.ellipseMode = (x) => ($._ellipseMode = x); $.rectMode = (x) => ($._rectMode = x); $.curveDetail = () => {}; // DRAWING $.line = (x0, y0, x1, y1) => { if ($._doStroke) { $.ctx.beginPath(); $.ctx.moveTo(x0, y0); $.ctx.lineTo(x1, y1); $.ctx.stroke(); } }; const TAU = Math.PI * 2; function arc(x, y, w, h, lo, hi, mode) { if ($._angleMode) { lo = $.radians(lo); hi = $.radians(hi); } lo %= TAU; hi %= TAU; if (lo < 0) lo += TAU; if (hi < 0) hi += TAU; if (lo > hi) hi += TAU; if (lo == hi) return $.ellipse(x, y, w, h); w /= 2; h /= 2; w = Math.abs(w); h = Math.abs(h); if (!$._doFill && mode == $.PIE_OPEN) mode = $.CHORD_OPEN; $.ctx.beginPath(); $.ctx.ellipse(x, y, w, h, 0, lo, hi); if (mode == $.PIE || mode == $.PIE_OPEN) $.ctx.lineTo(x, y); if ($._doFill) $.ctx.fill(); if ($._doStroke) { if (mode == $.PIE || mode == $.CHORD) $.ctx.closePath(); if (mode != $.PIE_OPEN) return $.ctx.stroke(); $.ctx.beginPath(); $.ctx.ellipse(x, y, w, h, 0, lo, hi); $.ctx.stroke(); } } $.arc = (x, y, w, h, start, stop, mode) => { if (start == stop) return $.ellipse(x, y, w, h); mode ??= $.PIE_OPEN; if ($._ellipseMode == $.CENTER) { arc(x, y, w, h, start, stop, mode); } else if ($._ellipseMode == $.RADIUS) { arc(x, y, w * 2, h * 2, start, stop, mode); } else if ($._ellipseMode == $.CORNER) { arc(x + w / 2, y + h / 2, w, h, start, stop, mode); } else if ($._ellipseMode == $.CORNERS) { arc((x + w) / 2, (y + h) / 2, w - x, h - y, start, stop, mode); } }; function ellipse(x, y, w, h) { $.ctx.beginPath(); $.ctx.ellipse(x, y, Math.abs(w / 2), Math.abs(h / 2), 0, 0, TAU); ink(); } $.ellipse = (x, y, w, h) => { h ??= w; if ($._ellipseMode == $.CENTER) { ellipse(x, y, w, h); } else if ($._ellipseMode == $.RADIUS) { ellipse(x, y, w * 2, h * 2); } else if ($._ellipseMode == $.CORNER) { ellipse(x + w / 2, y + h / 2, w, h); } else if ($._ellipseMode == $.CORNERS) { ellipse((x + w) / 2, (y + h) / 2, w - x, h - y); } }; $.circle = (x, y, d) => { if ($._ellipseMode == $.CENTER) { $.ctx.beginPath(); $.ctx.arc(x, y, Math.abs(d / 2), 0, TAU); ink(); } else $.ellipse(x, y, d, d); }; $.point = (x, y) => { if ($._doStroke) { if (x.x) { y = x.y; x = x.x; } $.ctx.beginPath(); $.ctx.moveTo(x, y); $.ctx.lineTo(x, y); $.ctx.stroke(); } }; function rect(x, y, w, h) { $.ctx.beginPath(); $.ctx.rect(x, y, w, h); ink(); } function roundedRect(x, y, w, h, tl, tr, br, bl) { if (tl === undefined) { return rect(x, y, w, h); } if (tr === undefined) { return roundedRect(x, y, w, h, tl, tl, tl, tl); } $.ctx.beginPath(); $.ctx.roundRect(x, y, w, h, [tl, tr, br, bl]); ink(); } $.rect = (x, y, w, h = w, tl, tr, br, bl) => { if ($._rectMode == $.CENTER) { roundedRect(x - w / 2, y - h / 2, w, h, tl, tr, br, bl); } else if ($._rectMode == $.RADIUS) { roundedRect(x - w, y - h, w * 2, h * 2, tl, tr, br, bl); } else if ($._rectMode == $.CORNER) { roundedRect(x, y, w, h, tl, tr, br, bl); } else if ($._rectMode == $.CORNERS) { roundedRect(x, y, w - x, h - y, tl, tr, br, bl); } }; $.square = (x, y, s, tl, tr, br, bl) => { return $.rect(x, y, s, s, tl, tr, br, bl); }; $.capsule = (x1, y1, x2, y2, r) => { const dx = x2 - x1, dy = y2 - y1, len = Math.hypot(dx, dy); if (len === 0) return $.circle(x1, y1, r * 2); const angle = Math.atan2(dy, dx), px = (-dy / len) * r, py = (dx / len) * r; $.ctx.beginPath(); $.ctx.moveTo(x1 - px, y1 - py); $.ctx.arc(x1, y1, r, angle - $.HALF_PI, angle + $.HALF_PI, true); $.ctx.lineTo(x2 + px, y2 + py); $.ctx.arc(x2, y2, r, angle + $.HALF_PI, angle - $.HALF_PI, true); $.ctx.closePath(); ink(); }; $.beginShape = () => { curveBuff = []; $.ctx.beginPath(); firstVertex = true; }; $.beginContour = () => { $.ctx.closePath(); curveBuff = []; firstVertex = true; }; $.endContour = () => { curveBuff = []; firstVertex = true; }; $.vertex = (x, y) => { curveBuff = []; if (firstVertex) { $.ctx.moveTo(x, y); } else { $.ctx.lineTo(x, y); } firstVertex = false; }; $.bezierVertex = (cp1x, cp1y, cp2x, cp2y, x, y) => { curveBuff = []; $.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); }; $.quadraticVertex = (cp1x, cp1y, x, y) => { curveBuff = []; $.ctx.quadraticCurveTo(cp1x, cp1y, x, y); }; $.bezier = (x1, y1, x2, y2, x3, y3, x4, y4) => { $.beginShape(); $.vertex(x1, y1); $.bezierVertex(x2, y2, x3, y3, x4, y4); $.endShape(); }; $.triangle = (x1, y1, x2, y2, x3, y3) => { $.beginShape(); $.vertex(x1, y1); $.vertex(x2, y2); $.vertex(x3, y3); $.endShape($.CLOSE); }; $.quad = (x1, y1, x2, y2, x3, y3, x4, y4) => { $.beginShape(); $.vertex(x1, y1); $.vertex(x2, y2); $.vertex(x3, y3); $.vertex(x4, y4); $.endShape($.CLOSE); }; $.endShape = (close) => { curveBuff = []; if (close) $.ctx.closePath(); ink(); }; $.curveVertex = (x, y) => { curveBuff.push([x, y]); if (curveBuff.length < 4) return; let p0 = curveBuff.at(-4), p1 = curveBuff.at(-3), p2 = curveBuff.at(-2), p3 = curveBuff.at(-1); let cp1x = p1[0] + (p2[0] - p0[0]) / 6, cp1y = p1[1] + (p2[1] - p0[1]) / 6, cp2x = p2[0] - (p3[0] - p1[0]) / 6, cp2y = p2[1] - (p3[1] - p1[1]) / 6; if (firstVertex) { $.ctx.moveTo(p1[0], p1[1]); firstVertex = false; } $.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2[0], p2[1]); }; $.curve = (x1, y1, x2, y2, x3, y3, x4, y4) => { $.beginShape(); $.curveVertex(x1, y1); $.curveVertex(x2, y2); $.curveVertex(x3, y3); $.curveVertex(x4, y4); $.endShape(); }; $.curvePoint = (a, b, c, d, t) => { const t3 = t * t * t, t2 = t * t, f1 = -0.5 * t3 + t2 - 0.5 * t, f2 = 1.5 * t3 - 2.5 * t2 + 1.0, f3 = -1.5 * t3 + 2.0 * t2 + 0.5 * t, f4 = 0.5 * t3 - 0.5 * t2; return a * f1 + b * f2 + c * f3 + d * f4; }; $.bezierPoint = (a, b, c, d, t) => { const adjustedT = 1 - t; return ( Math.pow(adjustedT, 3) * a + 3 * Math.pow(adjustedT, 2) * t * b + 3 * adjustedT * Math.pow(t, 2) * c + Math.pow(t, 3) * d ); }; $.curveTangent = (a, b, c, d, t) => { const t2 = t * t, f1 = (-3 * t2) / 2 + 2 * t - 0.5, f2 = (9 * t2) / 2 - 5 * t, f3 = (-9 * t2) / 2 + 4 * t + 0.5, f4 = (3 * t2) / 2 - t; return a * f1 + b * f2 + c * f3 + d * f4; }; $.bezierTangent = (a, b, c, d, t) => { const adjustedT = 1 - t; return ( 3 * d * Math.pow(t, 2) - 3 * c * Math.pow(t, 2) + 6 * c * adjustedT * t - 6 * b * adjustedT * t + 3 * b * Math.pow(adjustedT, 2) - 3 * a * Math.pow(adjustedT, 2) ); }; $.erase = function (fillAlpha, strokeAlpha) { if ($._colorFormat == 255) { if (fillAlpha) fillAlpha /= 255; if (strokeAlpha) strokeAlpha /= 255; } $.ctx.save(); $.ctx.globalCompositeOperation = 'destination-out'; $.ctx.fillStyle = `rgb(0 0 0 / ${fillAlpha || 1})`; $.ctx.strokeStyle = `rgb(0 0 0 / ${strokeAlpha || 1})`; }; $.noErase = function () { $.ctx.globalCompositeOperation = 'source-over'; $.ctx.restore(); }; $.inFill = (x, y) => { const pd = $._pixelDensity; return $.ctx.isPointInPath(x * pd, y * pd); }; $.inStroke = (x, y) => { const pd = $._pixelDensity; return $.ctx.isPointInStroke(x * pd, y * pd); }; }; Q5.renderers.c2d.image = ($, q) => { const c = $.canvas; if (c) { // polyfill for HTMLCanvasElement c.convertToBlob ??= (opt) => new Promise((resolve) => { c.toBlob((blob) => resolve(blob), opt.type, opt.quality); }); } $._tint = null; let imgData = null, pixels = null; $.createImage = (w, h, opt = {}) => { opt.colorSpace ??= $.canvas.colorSpace; opt.defaultImageScale ??= $._defaultImageScale; return new Q5.Image(w, h, opt); }; $.loadImage = function (url, cb, opt) { if (url.canvas) return url; if (url.slice(-3).toLowerCase() == 'gif') { throw new Error( `q5 doesn't support GIFs. Use a video or p5play animation instead. https://github.com/q5js/q5.js/issues/84` ); } let last = [...arguments].at(-1); if (typeof last == 'object') { opt = last; cb = null; } else opt = undefined; let g = $.createImage(1, 1, opt); let pd = g._pixelDensity; let img = new window.Image(); img.crossOrigin = 'Anonymous'; g.promise = new Promise((resolve, reject) => { img.onload = () => { delete g.then; if (g._usedAwait) g = $.createImage(1, 1, opt); img._pixelDensity = pd; g.defaultWidth = img.width * $._defaultImageScale; g.defaultHeight = img.height * $._defaultImageScale; g.naturalWidth = img.naturalWidth || img.width; g.naturalHeight = img.naturalHeight || img.height; g._setImageSize(Math.ceil(g.naturalWidth / pd), Math.ceil(g.naturalHeight / pd)); g.ctx.drawImage(img, 0, 0); if (cb) cb(g); resolve(g); }; img.onerror = reject; }); $._loaders.push(g.promise); // then only runs when the user awaits the instance g.then = (resolve, reject) => { g._usedAwait = true; return g.promise.then(resolve, reject); }; g.src = img.src = url; return g; }; $._imageMode = Q5.CORNER; $.imageMode = (mode) => ($._imageMode = mode); $.image = (img, dx, dy, dw, dh, sx = 0, sy = 0, sw, sh) => { if (!img) return; let drawable = img.canvas || img; dw ??= img.defaultWidth || drawable.width || img.videoWidth; dh ??= img.defaultHeight || drawable.height || img.videoHeight; if ($._imageMode == 'center') { dx -= dw * 0.5; dy -= dh * 0.5; } let pd = img._pixelDensity || 1; if (!sw) { sw = drawable.width || drawable.videoWidth; } else sw *= pd; if (!sh) { sh = drawable.height || drawable.videoHeight; } else sh *= pd; if ($._tint) { if (img._retint || img._tint != $._tint) { img._tintImg ??= $.createImage(img.w, img.h, { pixelDensity: pd }); if (img._tintImg.width != img.width || img._tintImg.height != img.height) { img._tintImg.resize(img.w, img.h); } let tnt = img._tintImg.ctx; tnt.globalCompositeOperation = 'copy'; tnt.fillStyle = $._tint; tnt.fillRect(0, 0, img.width, img.height); if (img?.canvas?.alpha) { tnt.globalCompositeOperation = 'destination-in'; tnt.drawImage(drawable, 0, 0, img.width, img.height); } tnt.globalCompositeOperation = 'multiply'; tnt.drawImage(drawable, 0, 0, img.width, img.height); img._tint = $._tint; img._retint = false; } drawable = img._tintImg.canvas; } if (img.flipped) { $.ctx.save(); $.ctx.translate(dx + dw, 0); $.ctx.scale(-1, 1); dx = 0; } $.ctx.drawImage(drawable, sx * pd, sy * pd, sw, sh, dx, dy, dw, dh); if (img.flipped) $.ctx.restore(); }; $.filter = (type, value) => { $.ctx.save(); let f = ''; if ($.ctx.filter) { if (typeof type == 'string') { f = type; } else if (type == Q5.GRAY) { f = `saturate(0%)`; } else if (type == Q5.INVERT) { f = `invert(100%)`; } else if (type == Q5.BLUR) { let r = Math.ceil(value * $._pixelDensity) || 1; f = `blur(${r}px)`; } else if (type == Q5.THRESHOLD) { value ??= 0.5; let b = Math.floor((0.5 / Math.max(value, 0.00001)) * 100); f = `saturate(0%) brightness(${b}%) contrast(1000000%)`; } else if (type == Q5.SEPIA) { f = `sepia(${value ?? 1})`; } else if (type == Q5.BRIGHTNESS) { f = `brightness(${value ?? 1})`; } else if (type == Q5.SATURATION) { f = `saturate(${value ?? 1})`; } else if (type == Q5.CONTRAST) { f = `contrast(${value ?? 1})`; } else if (type == Q5.HUE_ROTATE) { let unit = $._angleMode == 0 ? 'rad' : 'deg'; f = `hue-rotate(${value}${unit})`; } if (f) { $.ctx.filter = f; if ($.ctx.filter == 'none') { throw new Error(`Invalid filter format: ${type}`); } } } if (!f) $._softFilter(type, value); $.ctx.globalCompositeOperation = 'source-over'; $.ctx.drawImage(c, 0, 0, c.w, c.h); $.ctx.restore(); $.modified = $._retint = true; }; if ($._isImage) { $.resize = (w, h) => { let o = new $._Canvas(c.width, c.height); let tmpCtx = o.getContext('2d', { colorSpace: c.colorSpace }); tmpCtx.drawImage(c, 0, 0); $._setImageSize(w, h); $.defaultWidth = c.width * $._defaultImageScale; $.defaultHeight = c.height * $._defaultImageScale; $.ctx.clearRect(0, 0, c.width, c.height); $.ctx.drawImage(o, 0, 0, c.width, c.height); $.modified = $._retint = true; }; } $._getImageData = (x, y, w, h) => { return $.ctx.getImageData(x, y, w, h, { colorSpace: c.colorSpace }); }; $.trim = () => { let pd = $._pixelDensity || 1; let w = c.width; let h = c.height; let data = $._getImageData(0, 0, w, h).data; let left = w, right = 0, top = h, bottom = 0; let i = 3; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { if (data[i] !== 0) { if (x < left) left = x; if (x > right) right = x; if (y < top) top = y; if (y > bottom) bottom = y; } i += 4; } } top = Math.floor(top / pd); bottom = Math.floor(bottom / pd); left = Math.floor(left / pd); right = Math.floor(right / pd); return $.get(left, top, right - left + 1, bottom - top + 1); }; $.mask = (img) => { $.ctx.save(); $.ctx.resetTransform(); let old = $.ctx.globalCompositeOperation; $.ctx.globalCompositeOperation = 'destination-in'; $.ctx.drawImage(img.canvas, 0, 0); $.ctx.globalCompositeOperation = old; $.ctx.restore(); $.modified = $._retint = true; }; $.inset = (x, y, w, h, dx, dy, dw, dh) => { let pd = $._pixelDensity || 1; $.ctx.drawImage(c, x * pd, y * pd, w * pd, h * pd, dx, dy, dw, dh); $.modified = $._retint = true; }; $.copy = () => { let img = $.get(); for (let prop in $) { if (typeof $[prop] != 'function' && !/(canvas|ctx|texture)/.test(prop)) { img[prop] = $[prop]; } } return img; }; $.get = (x, y, w, h) => { let pd = $._pixelDensity || 1; if (x !== undefined && w === undefined) { if (!pixels) $.loadPixels(); let px = Math.floor(x * pd), py = Math.floor(y * pd), idx = 4 * (py * c.width + px); return [pixels[idx], pixels[idx + 1], pixels[idx + 2], pixels[idx + 3]]; } x = Math.floor(x || 0) * pd; y = Math.floor(y || 0) * pd; w ??= $.width; h ??= $.height; let img = $.createImage(w, h, { pixelDensity: pd }); img.ctx.drawImage(c, x, y, w * pd, h * pd, 0, 0, w, h); img.width = w; img.height = h; if ($._owner?._makeDrawable) $._owner._makeDrawable(img); return img; }; $.set = (x, y, val) => { x = Math.floor(x); y = Math.floor(y); $.modified = $._retint = true; if (val.canvas) { let old = $._tint; $._tint = null; $.image(val, x, y); $._tint = old; return; } if (!pixels) $.loadPixels(); let mod = $._pixelDensity || 1, r = val.r, g = val.g, b = val.b, a = val.a; if (($._colorFormat || $._owner?._colorFormat) == 1) { r *= 255; g *= 255; b *= 255; a *= 255; } for (let i = 0; i < mod; i++) { for (let j = 0; j < mod; j++) { let idx = 4 * ((y * mod + i) * c.width + x * mod + j); pixels[idx] = r; pixels[idx + 1] = g; pixels[idx + 2] = b; pixels[idx + 3] = a; } } }; $.loadPixels = () => { imgData = $._getImageData(0, 0, c.width, c.height); q.pixels = pixels = imgData.data; }; $.updatePixels = () => { if (imgData != null) { $.ctx.putImageData(imgData, 0, 0); $.modified = $._retint = true; } }; $.smooth = () => ($.ctx.imageSmoothingEnabled = true); $.noSmooth = () => ($.ctx.imageSmoothingEnabled = false); if ($._isImage) return; $.tint = function (c) { $._tint = (c._isColor ? c : $.color(...arguments)).toString(); }; $.noTint = () => ($._tint = null); }; Q5.Image = class { constructor(w, h, opt = {}) { opt.alpha ??= true; opt.colorSpace ??= Q5.canvasOptions.colorSpace; let $ = this; $._isImage = true; $.canvas = $.ctx = $.drawingContext = null; $.pixels = []; Q5.modules.canvas($, $); let r = Q5.renderers.c2d; for (let m of ['canvas', 'image', 'softFilters']) { if (r[m]) r[m]($, $); } $._pixelDensity = opt.pixelDensity || 1; $._defaultImageScale = opt.defaultImageScale || 2; $.createCanvas(w, h, opt); let scale = $._pixelDensity * $._defaultImageScale; $.defaultWidth = w * scale; $.defaultHeight = h * scale; delete $.createCanvas; $._loop = false; } get w() { return this.width; } get h() { return this.height; } }; /* software implementation of image filters */ Q5.renderers.c2d.softFilters = ($) => { let u = null; // uint8 temporary buffer function ensureBuf() { let l = $.canvas.width * $.canvas.height * 4; if (!u || u.length != l) u = new Uint8ClampedArray(l); } function initSoftFilters() { $._filters = []; $._filters[Q5.THRESHOLD] = (d, thresh) => { if (thresh === undefined) thresh = 127.5; else thresh *= 255; for (let i = 0; i < d.length; i += 4) { const gray = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; d[i] = d[i + 1] = d[i + 2] = gray >= thresh ? 255 : 0; } }; $._filters[Q5.GRAY] = (d) => { for (let i = 0; i < d.length; i += 4) { const gray = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; d[i] = d[i + 1] = d[i + 2] = gray; } }; $._filters[Q5.OPAQUE] = (d) => { for (let i = 0; i < d.length; i += 4) { d[i + 3] = 255; } }; $._filters[Q5.INVERT] = (d) => { for (let i = 0; i < d.length; i += 4) { d[i] = 255 - d[i]; d[i + 1] = 255 - d[i + 1]; d[i + 2] = 255 - d[i + 2]; } }; $._filters[Q5.POSTERIZE] = (d, lvl = 4) => { let lvl1 = lvl - 1; for (let i = 0; i < d.length; i += 4) { d[i] = (((d[i] * lvl) >> 8) * 255) / lvl1; d[i + 1] = (((d[i + 1] * lvl) >> 8) * 255) / lvl1; d[i + 2] = (((d[i + 2] * lvl) >> 8) * 255) / lvl1; } }; $._filters[Q5.DILATE] = (d, func) => { func ??= Math.max; ensureBuf(); u.set(d); let [w, h] = [$.canvas.width, $.canvas.height]; for (let i = 0; i < h; i++) { for (let j = 0; j < w; j++) { let l = 4 * Math.max(j - 1, 0); let r = 4 * Math.min(j + 1, w - 1); let t = 4 * Math.max(i - 1, 0) * w; let b = 4 * Math.min(i + 1, h - 1) * w; let oi = 4 * i * w; let oj = 4 * j; for (let k = 0; k < 4; k++) { let kt = k + t; let kb = k + b; let ko = k + oi; d[oi + oj + k] = func(u[kt + oj], u[ko + l], u[ko + oj], u[ko + r], u[kb + oj]); } } } }; $._filters[Q5.ERODE] = (d) => { $._filters[Q5.DILATE](d, Math.min); }; $._filters[Q5.BLUR] = (d, r) => { r = r || 1; r = Math.floor(r * $._pixelDensity); ensureBuf(); u.set(d); let ksize = r * 2 + 1; function gauss(ksize) { let im = new Float32Array(ksize); let sigma = 0.3 * r + 0.8; let ss2 = sigma * sigma * 2; for (let i = 0; i < ksize; i++) { let x = i - ksize / 2; let z = Math.exp(-(x * x) / ss2) / (2.5066282746 * sigma); im[i] = z; } return im; } let kern = gauss(ksize); let [w, h] = [$.canvas.width, $.canvas.height]; for (let i = 0; i < h; i++) { for (let j = 0; j < w; j++) { let s0 = 0, s1 = 0, s2 = 0, s3 = 0; for (let k = 0; k < ksize; k++) { let jk = Math.min(Math.max(j - r + k, 0), w - 1); let idx = 4 * (i * w + jk); s0 += u[idx] * kern[k]; s1 += u[idx + 1] * kern[k]; s2 += u[idx + 2] * kern[k]; s3 += u[idx + 3] * kern[k]; } let idx = 4 * (i * w + j); d[idx] = s0; d[idx + 1] = s1; d[idx + 2] = s2; d[idx + 3] = s3; } } u.set(d); for (let i = 0; i < h; i++) { for (let j = 0; j < w; j++) { let s0 = 0, s1 = 0, s2 = 0, s3 = 0; for (let k = 0; k < ksize; k++) { let ik = Math.min(Math.max(i - r + k, 0), h - 1); let idx = 4 * (ik * w + j); s0 += u[idx] * kern[k]; s1 += u[idx + 1] * kern[k]; s2 += u[idx + 2] * kern[k]; s3 += u[idx + 3] * kern[k]; } let idx = 4 * (i * w + j); d[idx] = s0; d[idx + 1] = s1; d[idx + 2] = s2; d[idx + 3] = s3; } } }; } $._softFilter = (typ, x) => { if (!$._filters) initSoftFilters(); let imgData = $._getImageData(0, 0, $.canvas.width, $.canvas.height); $._filters[typ](imgData.data, x); $.ctx.putImageData(imgData, 0, 0); }; }; Q5.renderers.c2d.text = ($, q) => { $._textAlign = 'left'; $._textBaseline = 'alphabetic'; $._textSize = 12; let font = 'sans-serif', leadingSet = false, leading = 15, leadDiff = 3, emphasis = 'normal', weight = 'normal', styleHash = 0, genTextImage = false, cacheSize = 0; $._fontMod = false; let cache = ($._textCache = {}); $.loadFont = (url, cb) => { let f; if (url.includes('fonts.googleapis.com/css')) { f = loadGoogleFont(url, cb); } else { let name = url.split('/').pop().split('.')[0].replace(' ', ''); f = { family: name }; let ff = new FontFace(name, `url(${encodeURI(url)})`); document.fonts.add(ff); f.promise = new Promise((resolve, reject) => { ff.load() .then(() => { delete f.then; if (cb) cb(ff); resolve(ff); }) .catch((err) => { reject(err); }); }); } $._loaders.push(f.promise); $.textFont(f.family); f.then = (resolve, reject) => { f._usedAwait = true; return f.promise.then(resolve, reject); }; return f; }; function loadGoogleFont(url, cb) { if (!url.startsWith('http')) url = 'https://' + url; const urlParams = new URL(url).searchParams; const familyParam = urlParams.get('family'); if (!familyParam) { console.error('Invalid Google Fonts URL: missing family parameter'); return null; } const fontFamily = familyParam.split(':')[0]; let f = { family: fontFamily }; f.promise = (async () => { try { const res = await fetch(url); if (!res.ok) { throw new Error(`Failed to fetch Google Font: ${res.status} ${res.statusText}`); } let css = await res.text(); let fontFaceRegex = /@font-face\s*{([^}]*)}/g; let srcRegex = /src:\s*url\(([^)]+)\)[^;]*;/; let fontFamilyRegex = /font-family:\s*['"]([^'"]+)['"]/; let fontWeightRegex = /font-weight:\s*([^;]+);/; let fontStyleRegex = /font-style:\s*([^;]+);/; let fontFaceMatch; let loadedFaces = []; while ((fontFaceMatch = fontFaceRegex.exec(css)) !== null) { let fontFaceCSS = fontFaceMatch[1]; let srcMatch = srcRegex.exec(fontFaceCSS); if (!srcMatch) continue; let fontUrl = srcMatch[1]; let familyMatch = fontFamilyRegex.exec(fontFaceCSS); if (!familyMatch) continue; let family = familyMatch[1]; let weightMatch = fontWeightRegex.exec(fontFaceCSS); let weight = weightMatch ? weightMatch[1] : '400'; let styleMatch = fontStyleRegex.exec(fontFaceCSS); let style = styleMatch ? styleMatch[1] : 'normal'; let faceName = `${family}-${weight}-${style}`.replace(/\s+/g, '-'); let fontFace = new FontFace(family, `url(${fontUrl})`, { weight, style }); document.fonts.add(fontFace); try { await fontFace.load(); loadedFaces.push(fontFace); } catch (e) { console.error(`Failed to load font face: ${faceName}`, e); } } if (f._usedAwait) { f = { family: fontFamily }; } f.faces = loadedFaces; delete f.then; if (cb) cb(f); return f; } catch (e) { console.error('Error loading Google Font:', e); throw e; } })(); return f; } $.textFont = (x) => { if (x && typeof x != 'string') x = x.family; if (!x || x == font) return font; font = x; $._fontMod = true; styleHash = -1; }; $.textSize = (x) => { if (x == undefined) return $._textSize; $._textSize = x; $._fontMod = true; styleHash = -1; if (!leadingSet) { leading = x * 1.25; leadDiff = leading - x; } }; $.textStyle = (x) => { if (!x) return emphasis; emphasis = x; $._fontMod = true; styleHash = -1; }; $.textWeight = (x) => { if (!x) return weight; weight = x; $._fontMod = true; styleHash = -1; }; $.textLeading = (x) => { if (x == undefined) return leading || $._textSize * 1.25; leadingSet = true; if (x == leading) return leading; leading = x; leadDiff = x - $._textSize; styleHash = -1; }; $.textAlign = (horiz, vert) => { $.ctx.textAlign = $._textAlign = horiz; if (vert) { $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert; } }; $._updateFont = () => { $.ctx.font = `${emphasis} ${weight} ${$._textSize}px ${font}`; $._fontMod = false; }; $.textWidth = (str) => { if ($._fontMod) $._updateFont(); return $.ctx.measureText(str).width; }; $.textAscent = (str) => { if ($._fontMod) $._updateFont(); return $.ctx.measureText(str).actualBoundingBoxAscent; }; $.textDescent = (str) => { if ($._fontMod) $._updateFont(); return $.ctx.measureText(str).actualBoundingBoxDescent; }; $.textFill = $.fill; $.textStroke = $.stroke; let updateStyleHash = () => { let styleString = font + $._textSize + emphasis + leading; let hash = 5381; for (let i = 0; i < styleString.length; i++) { hash = (hash * 33) ^ styleString.charCodeAt(i); } styleHash = hash >>> 0; }; $.createTextImage = (str, w, h) => { genTextImage = true; let img = $.text(str, 0, 0, w, h); genTextImage = false; return img; }; let lines = []; $.text = (str, x, y, w, h) => { if (str === undefined || (!$._doFill && !$._doStroke)) return; str = str.toString(); let ctx = $.ctx; let img, colorStyle, styleCache, colorCache, recycling; if ($._fontMod) $._updateFont(); if (genTextImage) { if (styleHash == -1) updateStyleHash(); colorStyle = $._fill + $._stroke + $._strokeWeight; styleCache = cache[str]; if (styleCache) colorCache = styleCache[styleHash]; else styleCache = cache[str] = {}; if (colorCache) { img = colorCache[colorStyle]; if (img) return img; if (colorCache.size >= 4) { for (let recycleKey in colorCache) { img = colorCache[recycleKey]; delete colorCache[recycleKey]; bre