UNPKG

q5

Version:

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

2,177 lines (1,895 loc) 171 kB
/** * q5.js * @version 2.24 * @author quinton-ashley, Tezumie, and LingDong- * @license LGPL-3.0 * @class Q5 */ function Q5(scope, parent, renderer) { let $ = this; $._q5 = true; $._parent = parent; if (renderer == 'webgpu-fallback') { $._renderer = 'c2d'; $._webgpu = $._webgpuFallback = true; } else { $._renderer = renderer || Q5.render; $['_' + $._renderer] = true; } let autoLoaded = scope == 'auto'; scope ??= 'global'; if (scope == 'auto') { if (!(window.setup || window.update || window.draw)) return; scope = 'global'; } $._scope = scope; let globalScope; if (scope == 'global') { Q5._hasGlobal = $._isGlobal = true; globalScope = Q5._esm ? globalThis : !Q5._server ? window : global; } if (scope == 'graphics') $._graphics = 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; $._hooks = { postCanvas: [], preRender: [], postRender: [] }; 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; } $._preloadPromises = []; $._usePreload = true; $.usePreloadSystem = (v) => ($._usePreload = v); $.isPreloadSupported = () => $._usePreload; const resolvers = []; $._incrementPreload = () => { $._preloadPromises.push(new Promise((resolve) => resolvers.push(resolve))); }; $._decrementPreload = () => { if (resolvers.length) resolvers.pop()(); }; $._draw = (timestamp) => { let ts = timestamp || performance.now(); $._lastFrameTime ??= ts - $._targetFrameDuration; 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; log(frameDelay); 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(); for (let m of Q5.methods.pre) m.call($); try { $.draw(); } catch (e) { if (!Q5.errorTolerant) $.noLoop(); if ($._fes) $._fes(e); throw e; } for (let m of Q5.methods.post) m.call($); $.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) cancelAnimationFrame(looper); else clearTimeout(looper); } looper = null; }; $.loop = () => { $._loop = true; if ($._setupDone && looper == null) $._draw(); }; $.isLooping = () => $._loop; $.redraw = (n = 1) => { $._redraw = true; for (let i = 0; i < n; i++) { $._draw(); } $._redraw = false; }; $.remove = () => { $.noLoop(); $.canvas.remove(); }; $.frameRate = (hz) => { if (hz != $._targetFrameRate) { $._targetFrameRate = hz; $._targetFrameDuration = 1000 / hz; if ($._loop && looper != null) { if (useRAF) 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 ($._graphics) return; if (scope == 'global') { let tmp = Object.assign({}, $); delete tmp.Color; Object.assign(Q5, tmp); delete Q5.Q5; } for (let m of Q5.methods.init) { m.call($); } for (let [n, fn] of Object.entries(Q5.prototype)) { if (n[0] != '_' && typeof $[n] == 'function') $[n] = fn.bind($); } if (scope == 'global') { let props = Object.getOwnPropertyNames($); for (let p of props) { if (p[0] != '_') 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 || $; $._isTouchAware = t.touchStarted || t.touchMoved || t.touchEnded; if ($._isGlobal) { $.preload = t.preload; $.setup = t.setup; $.draw = t.draw; $.postProcess = t.postProcess; } $.preload ??= () => {}; $.setup ??= () => {}; $.draw ??= () => {}; $.postProcess ??= () => {}; let userFns = [ 'setup', 'postProcess', 'mouseMoved', 'mousePressed', 'mouseReleased', 'mouseDragged', 'mouseClicked', 'mouseWheel', 'keyPressed', 'keyReleased', 'keyTyped', 'touchStarted', 'touchMoved', 'touchEnded', 'windowResized' ]; for (let k of userFns) { if (!t[k]) $[k] = () => {}; else if ($._isGlobal) { $[k] = (event) => { try { return t[k](event); } catch (e) { if ($._fes) $._fes(e); throw e; } }; } } async function _setup() { $._startDone = true; await Promise.all($._preloadPromises); if ($._g) await Promise.all($._g._preloadPromises); millisStart = performance.now(); await $.setup(); $._setupDone = true; if ($.frameCount) return; if ($.ctx === null) $.createCanvas(200, 200); raf($._draw); } function _start() { try { $.preload(); if (!$._startDone) _setup(); } catch (e) { if ($._fes) $._fes(e); throw e; } } if (autoLoaded) _start(); else setTimeout(_start, 32); } Q5.render = 'c2d'; Q5.renderers = {}; Q5.modules = {}; Q5._server = typeof process == 'object'; Q5._esm = this === undefined; Q5._instanceCount = 0; Q5._friendlyError = (msg, func) => { if (!Q5.disableFriendlyErrors) console.error(func + ': ' + msg); }; Q5._validateParameters = () => true; Q5.methods = { init: [], pre: [], post: [], remove: [] }; Q5.prototype.registerMethod = (m, fn) => Q5.methods[m].push(fn); Q5.prototype.registerPreloadMethod = (n, fn) => (Q5.prototype[n] = fn[n]); if (Q5._server) global.p5 ??= global.Q5 = Q5; if (typeof window == 'object') window.p5 ??= window.Q5 = Q5; else global.window = 0; function createCanvas(w, h, opt) { if (!Q5._hasGlobal) { let q = new Q5(); q.createCanvas(w, h, opt); } } Q5.version = Q5.VERSION = '2.24'; if (typeof document == 'object') { document.addEventListener('DOMContentLoaded', () => { if (!Q5._hasGlobal) 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(100, 100); } } else if ($._scope == 'image' || $._scope == 'graphics') { q.canvas = new $._Canvas(100, 100); } if (!$.canvas) { if (typeof document == 'object') { q.canvas = document.createElement('canvas'); $.canvas.id = 'q5Canvas' + Q5._instanceCount; $.canvas.classList.add('q5Canvas'); } else $.noCanvas(); } let c = $.canvas; $.width = 200; $.height = 200; $._pixelDensity = 1; $.displayDensity = () => window.devicePixelRatio || 1; if (c) { c.width = 200; c.height = 200; if ($._scope != 'image') { c.renderer = $._renderer; c[$._renderer] = true; $._pixelDensity = Math.ceil($.displayDensity()); } } $._adjustDisplay = () => { if (c.style) { c.style.width = c.w + 'px'; c.style.height = c.h + 'px'; } }; $.createCanvas = function (w, h, options) { if (typeof w == 'object') { options = w; w = 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 ($._scope != 'image') { if ($._scope == 'graphics') $._pixelDensity = this._pixelDensity; else if (window.IntersectionObserver) { let wasObserved = false; new IntersectionObserver((e) => { c.visible = e[0].isIntersecting; if (!wasObserved) { $._wasLooping = $._loop; wasObserved = true; } if (c.visible) { if ($._wasLooping && !$._loop) $.loop(); } else { $._wasLooping = $._loop; $.noLoop(); } }).observe(c); } } $._setCanvasSize(w, h); Object.assign(c, opt); let rend = $._createCanvas(c.w, c.h, opt); if ($._hooks) { for (let m of $._hooks.postCanvas) m(); } if ($._addEventMethods) $._addEventMethods(c); 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; 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); if (!$._da) { q.width = w; q.height = h; } else $.flexibleCanvas($._dau); if ($.displayMode && !c.displayMode) $.displayMode(); else $._adjustDisplay(); }; $._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; return ($._defaultImageScale = scale); }; $.defaultImageScale(0.5); if ($._scope == 'image') return; if (c && $._scope != 'graphics') { 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'); document.body.append(el); } c.parent(el); } if (document.body) addCanvas(); else document.addEventListener('DOMContentLoaded', 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); return v; }; $.flexibleCanvas = (unit = 400) => { if (unit) { $._da = c.width / (unit * $._pixelDensity); q.width = $._dau = unit; q.height = (c.h / c.w) * unit; } else $._da = 0; }; $._styleNames = [ '_fill', '_stroke', '_strokeWeight', '_doStroke', '_doFill', '_strokeSet', '_fillSet', '_shadow', '_doShadow', '_shadowOffsetX', '_shadowOffsetY', '_shadowBlur', '_tint', '_colorMode', '_colorFormat', 'Color', '_imageMode', '_rectMode', '_ellipseMode', '_textSize', '_textAlign', '_textBaseline' ]; $._styles = []; $.pushStyles = () => { let styles = {}; for (let s of $._styleNames) styles[s] = $[s]; $._styles.push(styles); }; $.popStyles = () => { let styles = $._styles.pop(); for (let s of $._styleNames) $[s] = styles[s]; q.Color = styles.Color; }; if (window && $._scope != 'graphics') { 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.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', 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 ($._scope != 'image') { // 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 ($._scope == 'image') 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._q5Color) 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._q5Color && (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._q5Color && (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; if ($._da) n *= $._da; $.ctx.lineWidth = $._strokeWeight = n || 0.0001; }; $.noFill = () => ($._doFill = false); $.noStroke = () => ($._doStroke = false); $.opacity = (a) => ($.ctx.globalAlpha = a); $._doShadow = false; $._shadowOffsetX = $._shadowOffsetY = $._shadowBlur = 10; $.shadow = function (c) { if (Q5.Color) { if (!c._q5Color && (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 ($._da) { x *= $._da; y *= $._da; } $.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); } }; $.pushMatrix = () => $.ctx.save(); $.popMatrix = () => $.ctx.restore(); let _popStyles = $.popStyles; $.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; }; $.push = () => { $.ctx.save(); $.pushStyles(); }; $.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) { $._da && ((x0 *= $._da), (y0 *= $._da), (x1 *= $._da), (y1 *= $._da)); $.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; 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); if ($._da) { x *= $._da; y *= $._da; w *= $._da; h *= $._da; } 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, w / 2, h / 2, 0, 0, TAU); ink(); } $.ellipse = (x, y, w, h) => { h ??= w; if ($._da) { x *= $._da; y *= $._da; w *= $._da; h *= $._da; } 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) { if ($._da) { x *= $._da; y *= $._da; d *= $._da; } $.ctx.beginPath(); $.ctx.arc(x, y, 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; } if ($._da) { x *= $._da; y *= $._da; } $.ctx.beginPath(); $.ctx.moveTo(x, y); $.ctx.lineTo(x, y); $.ctx.stroke(); } }; function rect(x, y, w, h) { if ($._da) { x *= $._da; y *= $._da; w *= $._da; h *= $._da; } $.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); } if ($._da) { x *= $._da; y *= $._da; w *= $._da; h *= $._da; tl *= $._da; tr *= $._da; bl *= $._da; br *= $._da; } $.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); }; $.beginShape = () => { curveBuff = []; $.ctx.beginPath(); firstVertex = true; }; $.beginContour = () => { $.ctx.closePath(); curveBuff = []; firstVertex = true; }; $.endContour = () => { curveBuff = []; firstVertex = true; }; $.vertex = (x, y) => { if ($._da) { x *= $._da; y *= $._da; } curveBuff = []; if (firstVertex) { $.ctx.moveTo(x, y); } else { $.ctx.lineTo(x, y); } firstVertex = false; }; $.bezierVertex = (cp1x, cp1y, cp2x, cp2y, x, y) => { if ($._da) { cp1x *= $._da; cp1y *= $._da; cp2x *= $._da; cp2y *= $._da; x *= $._da; y *= $._da; } curveBuff = []; $.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); }; $.quadraticVertex = (cp1x, cp1y, x, y) => { if ($._da) { cp1x *= $._da; cp1y *= $._da; x *= $._da; y *= $._da; } 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) => { if ($._da) { x *= $._da; y *= $._da; } 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) => { class Q5Image { constructor(w, h, opt = {}) { let $ = this; $._scope = 'image'; $.canvas = $.ctx = $.drawingContext = null; $.pixels = []; Q5.modules.canvas($, $); let r = Q5.renderers.c2d; for (let m of ['canvas', 'image', 'soft_filters']) { if (r[m]) r[m]($, $); } $._pixelDensity = opt.pixelDensity || 1; $.createCanvas(w, h, opt); let scale = $._pixelDensity * q._defaultImageScale; $.defaultWidth = w * scale; $.defaultHeight = h * scale; delete $.createCanvas; $._loop = false; } get w() { return this.width; } get h() { return this.height; } } Q5.Image ??= Q5Image; $._tint = null; let imgData = null; $.createImage = (w, h, opt) => { opt ??= {}; opt.alpha ??= true; opt.colorSpace ??= $.canvas.colorSpace || Q5.canvasOptions.colorSpace; 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 = null; let g = $.createImage(1, 1, opt); let pd = g._pixelDensity; let img = new window.Image(); img.crossOrigin = 'Anonymous'; g._loader = new Promise((resolve, reject) => { img.onload = () => { 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); delete g._loader; resolve(g); }; img.onerror = reject; }); $._preloadPromises.push(g._loader); g.src = img.src = url; if (!$._usePreload) return g._loader; return g; }; $.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; } if ($._da) { dx *= $._da; dy *= $._da; dw *= $._da; dh *= $._da; sx *= $._da; sy *= $._da; sw *= $._da; sh *= $._da; } 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($.canvas, 0, 0, $.canvas.w, $.canvas.h); $.ctx.restore(); $.modified = $._retint = true; }; if ($._scope == 'image') { $.resize = (w, h) => { let c = $.canvas; 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: $.canvas.colorSpace }); }; $.trim = () => { let pd = $._pixelDensity || 1; let w = $.canvas.width; let h = $.canvas.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($.canvas, 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|textureIndex)/.test(prop)) { img[prop] = $[prop]; } } return img; }; $.get = (x, y, w, h) => { let pd = $._pixelDensity || 1; if (x !== undefined && w === undefined) { let c = $._getImageData(x * pd, y * pd, 1, 1).data; return [c[0], c[1], c[2], c[3] / 255]; } 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($.canvas, x, y, w * pd, h * pd, 0, 0, w, h); img.width = w; img.height = h; return img; }; $.set = (x, y, c) => { x = Math.floor(x); y = Math.floor(y); $.modified = $._retint = true; if (c.canvas) { let old = $._tint; $._tint = null; $.image(c, x, y); $._tint = old; return; } if (!$.pixels.length) $.loadPixels(); let mod = $._pixelDensity || 1; for (let i = 0; i < mod; i++) { for (let j = 0; j < mod; j++) { let idx = 4 * ((y * mod + i) * $.canvas.width + x * mod + j); $.pixels[idx] = c.r; $.pixels[idx + 1] = c.g; $.pixels[idx + 2] = c.b; $.pixels[idx + 3] = c.a; } } }; $.loadPixels = () => { imgData = $._getImageData(0, 0, $.canvas.width, $.canvas.height); q.pixels = imgData.data; }; $.updatePixels = () => { if (imgData != null) { $.ctx.putImageData(imgData, 0, 0); $.modified = $._retint = true; } }; $.smooth = () => ($.ctx.imageSmoothingEnabled = true); $.noSmooth = () => ($.ctx.imageSmoothingEnabled = false); if ($._scope == 'image') return; $._saveCanvas = async (data, ext) => { data = data.canvas || data; if (data instanceof OffscreenCanvas) { const blob = await data.convertToBlob({ type: 'image/' + ext }); return await new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(blob); }); } return data.toDataURL('image/' + ext); }; $.tint = function (c) { $._tint = (c._q5Color ? c : $.color(...arguments)).toString(); }; $.noTint = () => ($._tint = null); }; /* software implementation of image filters */ Q5.renderers.c2d.soft_filters = ($) => { 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', fontMod = false, styleHash = 0, styleHashes = [], useCache = false, genTextImage = false, cacheSize = 0, cacheMax = 12000; let cache = ($._textCache = {}); $.loadFont = (url, cb) => { let name = url.split('/').pop().split('.')[0].replace(' ', ''); let f = new FontFace(name, `url(${url})`); document.fonts.add(f); f._loader = (async () => { let err; try { await f.load(); } catch (e) { err = e; } delete f._loader; if (err) throw err; if (cb) cb(f); return f; })(); $._preloadPromises.push(f._loader); $.textFont(name); if (!$._usePreload) return f._loader; 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; if ($._da) x *= $._da; $._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; if ($._da) x *= $._da; leading = x; leadDiff = x - $._textSize; styleHash = -1; }; $.textAlign = (horiz, vert) => { $.ctx.textAlign = $._textAlign = horiz; if (vert) { $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert; } }; const 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; }; $.textCache = (enable, maxSize) => { if (maxSize) cacheMax = maxSize; if (enable !== undefined) useCache = enable; return useCache; }; $.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(); if ($._da) { x *= $._da; y *= $._da; } let ctx = $.ctx; let img, tX, tY; if (fontMod) updateFont(); if (useCache || genTextImage) { if (styleHash == -1) updateStyleHash(); img = cache[str]; if (img) img = img[styleHash]; if (img) { if (img._fill == $._fill && img._stroke == $._stroke && img._strokeWeight == $._strokeWeight) { if (genTextImage) return img; return $.textImage(img, x, y); } else img.clear(); } } if (str.indexOf('\n') == -1) lines[0] = str; else lines = str.split('\n'); if (str.length > w) { let wrapped = []; for (let line of lines) { let i = 0; while (i < line.length) { let max = i + w; if (max >= line.length) { wrapped.push(line.slice(i)); break; } let end = line.lastIndexOf(' ', max); if (end === -1 || end < i) end = max; wrapped.push(line.slice(i, end)); i = end + 1; } } lines = wrapped; } if (!useCache && !genTextImage) { tX = x; tY = y; } else { tX = 0; tY = leading * lines.length; if (!img) { let ogBaseline = $.ctx.textBaseline; $.ctx.textBaseline = 'alphabetic'; let measure = ctx.measureText(' '); let ascent = measure.fontBoundingBoxAscent; let descent = measure.fontBoundingBoxDescent; $.ctx.textBaseline = ogBaseline; img = $.createImage.call($, Math.ceil(ctx.measureText(str).width), Math.ceil(tY + descent), { pixelDensity: $._pixelDensity }); img._ascent = ascent; img._descent = descent; img._top = descent + leadDiff; img._middle = img._top + ascent * 0.5; img._bottom = img._top + ascent; img._leading = leading; } else { img.modified = true; } img._fill = $._fill; img._stroke = $._stroke; img._strokeWeight = $._strokeWeight; ctx = img.ctx; ctx.font = $.ctx.font; ctx.fillStyle = $._fill; ctx.strokeStyle = $._stroke; ctx.lineWidth = $.ctx.lineWidth; } let ogFill; if (!$._fillSet) { ogFill = ctx.fillStyle; ctx.fillStyle = 'black'; } let lineAmount = 0; for (let line of lines) { if ($._doStroke && $._strokeSet) ctx.strokeText(line, tX, tY); if ($._doFill) ctx.fillText(line, tX, tY); tY += leading; lineAmount++; if (lineAmount >= h) break; } lines = []; if (!$._fillSet) ctx.fillStyle = ogFill; if (useCache || genTextImage) { styleHashes.push(styleHash); (cache[str] ??= {})[styleHash] = img; cacheSize++; if (cacheSize > cacheMax) { let half = Math.ceil(cacheSize / 2); let hashes = styleHashes.splice(0, half); for (let s in cache) { s = cache[s]; for (let h of hashes) delete s[h]; } cacheSize -= half; } if (genTextImage) return img; $.textImage(img, x, y); } }; $.textImage = (img, x, y) => { if (typeof img == 'string') img = $.createTextImage(img); let og = $._imageMode; $._imageMode = 'corner'; let ta = $._textAlign; if (ta == 'center') x -= img.canvas.hw; else if (ta == 'right') x -= img.width; let bl = $._textBaseline; if (bl == 'alphabetic') y -= img._leading; else if (bl == 'middle') y -= img._middle; else if (bl == 'bottom') y -= img._bottom; else if (bl == 'top') y -= img._top; $.image(img, x, y); $._imageMode = og; }; }; Q5.fonts = []; Q5.modules.color = ($, q) => { $.RGB = $.RGBA = $.RGBHDR = $._colorMode = 'rgb'; $.HSL = 'hsl'; $.HSB = 'hsb'; $.OKLCH = 'oklch'; $.SRGB = 'srgb'; $.DISPLAY_P3 = 'display-p3'; $.colorMode = (mode, format, gamut) => { $._colorMode = mode; let srgb = $.canvas.colorSpace == 'srgb' || gamut == 'srgb'; format ??= mode == 'rgb' ? ($._c2d || srgb ? 255 : 1) : 1; $._colorFormat = format == 'integer' || format == 255 ? 255 : 1; if (mode == 'oklch') { q.Color = Q5.ColorOKLCH; } else if (mode == 'hsl') { q.Color = srgb ? Q5.ColorHSL : Q5.ColorHSL_P3; } else if (mode == 'hsb') { q.Color = srgb ? Q5.ColorHSB : Q5.ColorHSB_P3; } else { if ($._colorFormat == 255) { q.Color = srgb ? Q5.ColorRGB_8 : Q5.ColorRGB_P3_8; } else { q.Color = srgb ? Q5.ColorRGB : Q5.ColorRGB_P3; } $._colorMode = 'rgb'; } }; $._namedColors = { aqua: [0, 255, 255], black: [0, 0, 0], blue: [0, 0, 255], brown: [165, 42, 42], crimson: [220, 20, 60], cyan: [0, 255, 255], darkviolet: [148, 0, 211], gold: [255, 215, 0], green: [0, 128, 0], gray: [128, 128, 128], grey: [128, 128, 128], hotpink: [255, 105, 180], indigo: [75, 0, 130], khaki: [240, 230, 140], lightgreen: [144, 238, 144], lime: [0, 255, 0], magenta: [255, 0, 255], navy: [0, 0, 128], orange: [255, 165, 0], olive: [128, 128, 0], peachpuff: [255, 218, 185], pink: [255, 192, 203], purple: [128, 0, 128], red: [255, 0, 0], skyblue: [135, 206, 235], tan: [210, 180, 140], turquoise: [64, 224, 208], transparent: [0, 0, 0, 0], white: [255, 255, 255], violet: [238, 130, 238], yellow: [255, 255, 0] }; $.color = (c0, c1, c2, c3) => { let C = $.Color; if (c0._q5Color) return new C(...c0.levels); if (c1 == undefined) { if (typeof c0 == 'string') { if (c0[0] == '#') { if (c0.length <= 5) { if (c0.length > 4) c3 = parseInt(c0[4] + c0[4], 16); c2 = parseInt(c0[3] + c0[3], 16); c1 = parseInt(c0[2] + c0[2], 16); c0 = parseInt(c0[1] + c0[1], 16); } else { if (c0.length > 7) c3 = parseInt(c0.slice(7, 9), 16); c2 = parseInt(c0.slice(5, 7), 16); c1 = parseInt(c0.slice(3, 5), 16); c0 = parseInt(c0.slice(1, 3), 16); } } else if ($._namedColors[c0]) { [c0, c1, c2, c3] = $._namedColors[c0]; } else { // css color string not parsed let c = new C(0, 0, 0); c._css = c0; c.toString = function () { return this._css; }; return c; } if ($._colorFormat == 1) { c0 /= 255; if (c1) c1 /= 255; if (c2) c2 /= 255; if (c3) c3 /= 255; } } if (Array.isArray(c0) || c0.constructor == Float32Array) { [c0, c1, c2, c3] = c0; } } if (c2 == undefined) { if ($._colorMode == Q5.OKLCH) return new C(c0, 0, 0, c1); return new C(c0, c0,