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