q5
Version:
Beginner friendly graphics powered by WebGPU and optimized for interactive art!
2,151 lines (1,837 loc) • 201 kB
JavaScript
/**
* 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