q5 2.0.17 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,29 @@
1
- Q5.modules.q2d_image = ($, p) => {
1
+ Q5.renderers.q2d.image = ($, q) => {
2
+ class Q5Image {
3
+ constructor(w, h, opt) {
4
+ let $ = this;
5
+ $._scope = 'image';
6
+ $.canvas = $.ctx = $.drawingContext = null;
7
+ $.pixels = [];
8
+ Q5.modules.canvas($, $);
9
+ let r = Q5.renderers.q2d;
10
+ for (let m of ['canvas', 'image', 'soft_filters']) {
11
+ if (r[m]) r[m]($, $);
12
+ }
13
+ $.createCanvas(w, h, opt);
14
+ delete $.createCanvas;
15
+ $._loop = false;
16
+ }
17
+ get w() {
18
+ return this.width;
19
+ }
20
+ get h() {
21
+ return this.height;
22
+ }
23
+ }
24
+
25
+ Q5.Image ??= Q5Image;
26
+
2
27
  $.createImage = (w, h, opt) => {
3
28
  opt ??= {};
4
29
  opt.alpha ??= true;
@@ -6,115 +31,145 @@ Q5.modules.q2d_image = ($, p) => {
6
31
  return new Q5.Image(w, h, opt);
7
32
  };
8
33
 
9
- $._tint = null;
10
- let imgData = null;
11
- let tmpCtx = null;
12
- let tmpCt2 = null;
34
+ $.loadImage = function (url, cb, opt) {
35
+ if (url.canvas) return url;
36
+ if (url.slice(-3).toLowerCase() == 'gif') {
37
+ throw new Error(`q5 doesn't support GIFs due to their impact on performance. Use a video or animation instead.`);
38
+ }
39
+ q._preloadCount++;
40
+ let last = [...arguments].at(-1);
41
+ opt = typeof last == 'object' ? last : null;
13
42
 
14
- function makeTmpCtx(w, h) {
15
- h ??= w || $.canvas.height;
16
- w ??= $.canvas.width;
17
- if (tmpCtx == null) {
18
- tmpCtx = new $._OffscreenCanvas(w, h).getContext('2d', {
19
- colorSpace: $.canvas.colorSpace
20
- });
43
+ let g = $.createImage(1, 1, opt);
44
+
45
+ function loaded(img) {
46
+ g.resize(img.naturalWidth || img.width, img.naturalHeight || img.height);
47
+ g.ctx.drawImage(img, 0, 0);
48
+ q._preloadCount--;
49
+ if (cb) cb(g);
21
50
  }
22
- if (tmpCtx.canvas.width != w || tmpCtx.canvas.height != h) {
23
- tmpCtx.canvas.width = w;
24
- tmpCtx.canvas.height = h;
51
+
52
+ if (Q5._nodejs && global.CairoCanvas) {
53
+ global.CairoCanvas.loadImage(url)
54
+ .then(loaded)
55
+ .catch((e) => {
56
+ q._preloadCount--;
57
+ throw e;
58
+ });
59
+ } else {
60
+ let img = new window.Image();
61
+ img.src = url;
62
+ img.crossOrigin = 'Anonymous';
63
+ img._pixelDensity = 1;
64
+ img.onload = () => loaded(img);
65
+ img.onerror = (e) => {
66
+ q._preloadCount--;
67
+ throw e;
68
+ };
25
69
  }
26
- }
70
+ return g;
71
+ };
27
72
 
28
- function makeTmpCt2(w, h) {
29
- h ??= w || $.canvas.height;
30
- w ??= $.canvas.width;
31
- if (tmpCt2 == null) {
32
- tmpCt2 = new $._OffscreenCanvas(w, h).getContext('2d', {
33
- colorSpace: $.canvas.colorSpace
34
- });
73
+ $.imageMode = (mode) => ($._imageMode = mode);
74
+ $.image = (img, dx, dy, dw, dh, sx = 0, sy = 0, sw, sh) => {
75
+ let drawable = img.canvas || img;
76
+ if (Q5._createNodeJSCanvas) {
77
+ drawable = drawable.context.canvas;
35
78
  }
36
- if (tmpCt2.canvas.width != w || tmpCt2.canvas.height != h) {
37
- tmpCt2.canvas.width = w;
38
- tmpCt2.canvas.height = h;
79
+ dw ??= img.width || img.videoWidth;
80
+ dh ??= img.height || img.videoHeight;
81
+ if ($._imageMode == 'center') {
82
+ dx -= dw * 0.5;
83
+ dy -= dh * 0.5;
39
84
  }
40
- }
85
+ if ($._da) {
86
+ dx *= $._da;
87
+ dy *= $._da;
88
+ dw *= $._da;
89
+ dh *= $._da;
90
+ sx *= $._da;
91
+ sy *= $._da;
92
+ sw *= $._da;
93
+ sh *= $._da;
94
+ }
95
+ let pd = img._pixelDensity || 1;
96
+ if (!sw) {
97
+ sw = drawable.width || drawable.videoWidth;
98
+ } else sw *= pd;
99
+ if (!sh) {
100
+ sh = drawable.height || drawable.videoHeight;
101
+ } else sh *= pd;
102
+ $.ctx.drawImage(drawable, sx * pd, sy * pd, sw, sh, dx, dy, dw, dh);
103
+
104
+ if ($._tint) {
105
+ $.ctx.globalCompositeOperation = 'multiply';
106
+ $.ctx.fillStyle = $._tint.toString();
107
+ $.ctx.fillRect(dx, dy, dw, dh);
108
+ $.ctx.globalCompositeOperation = 'source-over';
109
+ }
110
+ };
111
+
112
+ $._tint = null;
113
+ let imgData = null;
41
114
 
42
115
  $._softFilter = () => {
43
- throw 'Load q5-2d-soft-filters.js to use software filters.';
116
+ throw new Error('Load q5-2d-soft-filters.js to use software filters.');
44
117
  };
45
118
 
46
- function nativeFilter(filt) {
47
- tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
48
- tmpCtx.filter = filt;
49
- tmpCtx.drawImage($.canvas, 0, 0);
50
- $.ctx.save();
51
- $.ctx.resetTransform();
52
- $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
53
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
54
- $.ctx.restore();
55
- }
119
+ $.filter = (type, x) => {
120
+ if (!$.ctx.filter) return $._softFilter(type, x);
56
121
 
57
- $.filter = (typ, x) => {
58
- if (!$.ctx.filter) return $._softFilter(typ, x);
59
- makeTmpCtx();
60
- if (typeof typ == 'string') {
61
- nativeFilter(typ);
62
- } else if (typ == Q5.THRESHOLD) {
122
+ if (typeof type == 'string') f = type;
123
+ else if (type == Q5.GRAY) f = `saturate(0%)`;
124
+ else if (type == Q5.INVERT) f = `invert(100%)`;
125
+ else if (type == Q5.BLUR) {
126
+ let r = Math.ceil(x * $._pixelDensity) || 1;
127
+ f = `blur(${r}px)`;
128
+ } else if (type == Q5.THRESHOLD) {
63
129
  x ??= 0.5;
64
- x = Math.max(x, 0.00001);
65
- let b = Math.floor((0.5 / x) * 100);
66
- nativeFilter(`saturate(0%) brightness(${b}%) contrast(1000000%)`);
67
- } else if (typ == Q5.GRAY) {
68
- nativeFilter(`saturate(0%)`);
69
- } else if (typ == Q5.OPAQUE) {
70
- tmpCtx.fillStyle = 'black';
71
- tmpCtx.fillRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
72
- tmpCtx.drawImage($.canvas, 0, 0);
73
- $.ctx.save();
74
- $.ctx.resetTransform();
75
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
76
- $.ctx.restore();
77
- } else if (typ == Q5.INVERT) {
78
- nativeFilter(`invert(100%)`);
79
- } else if (typ == Q5.BLUR) {
80
- nativeFilter(`blur(${Math.ceil((x * $._pixelDensity) / 1) || 1}px)`);
81
- } else {
82
- $._softFilter(typ, x);
83
- }
84
- };
130
+ let b = Math.floor((0.5 / Math.max(x, 0.00001)) * 100);
131
+ f = `saturate(0%) brightness(${b}%) contrast(1000000%)`;
132
+ } else return $._softFilter(type, x);
85
133
 
86
- $.resize = (w, h) => {
87
- makeTmpCtx();
88
- tmpCtx.drawImage($.canvas, 0, 0);
89
- p.width = w;
90
- p.height = h;
91
- $.canvas.width = w * $._pixelDensity;
92
- $.canvas.height = h * $._pixelDensity;
93
- $.ctx.save();
94
- $.ctx.resetTransform();
95
- $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
96
- $.ctx.drawImage(tmpCtx.canvas, 0, 0, $.canvas.width, $.canvas.height);
97
- $.ctx.restore();
134
+ $.ctx.filter = f;
135
+ $.ctx.drawImage($.canvas, 0, 0, $.canvas.w, $.canvas.h);
136
+ $.ctx.filter = 'none';
98
137
  };
99
138
 
139
+ if ($._scope == 'image') {
140
+ $.resize = (w, h) => {
141
+ let o = new $._OffscreenCanvas($.canvas.width, $.canvas.height);
142
+ let tmpCtx = o.getContext('2d', {
143
+ colorSpace: $.canvas.colorSpace
144
+ });
145
+ tmpCtx.drawImage($.canvas, 0, 0);
146
+ $._setCanvasSize(w, h);
147
+
148
+ $.ctx.clearRect(0, 0, $.canvas.width, $.canvas.height);
149
+ $.ctx.drawImage(o, 0, 0, $.canvas.width, $.canvas.height);
150
+ };
151
+ }
152
+
100
153
  $.trim = () => {
101
154
  let pd = $._pixelDensity || 1;
102
- let imgData = $.ctx.getImageData(0, 0, $.width * pd, $.height * pd);
103
- let data = imgData.data;
104
- let left = $.width,
155
+ let w = $.canvas.width;
156
+ let h = $.canvas.height;
157
+ let data = $.ctx.getImageData(0, 0, w, h).data;
158
+ let left = w,
105
159
  right = 0,
106
- top = $.height,
160
+ top = h,
107
161
  bottom = 0;
108
162
 
109
- for (let y = 0; y < $.height * pd; y++) {
110
- for (let x = 0; x < $.width * pd; x++) {
111
- let index = (y * $.width * pd + x) * 4;
112
- if (data[index + 3] !== 0) {
163
+ let i = 3;
164
+ for (let y = 0; y < h; y++) {
165
+ for (let x = 0; x < w; x++) {
166
+ if (data[i] !== 0) {
113
167
  if (x < left) left = x;
114
168
  if (x > right) right = x;
115
169
  if (y < top) top = y;
116
170
  if (y > bottom) bottom = y;
117
171
  }
172
+ i += 4;
118
173
  }
119
174
  }
120
175
  top = Math.floor(top / pd);
@@ -135,48 +190,6 @@ Q5.modules.q2d_image = ($, p) => {
135
190
  $.ctx.restore();
136
191
  };
137
192
 
138
- $._save = async (data, name, ext) => {
139
- name = name || 'untitled';
140
- ext = ext || 'png';
141
- if (ext == 'jpg' || ext == 'png' || ext == 'webp') {
142
- if (data instanceof OffscreenCanvas) {
143
- const blob = await data.convertToBlob({ type: 'image/' + ext });
144
- data = await new Promise((resolve) => {
145
- const reader = new FileReader();
146
- reader.onloadend = () => resolve(reader.result);
147
- reader.readAsDataURL(blob);
148
- });
149
- } else {
150
- data = data.toDataURL('image/' + ext);
151
- }
152
- } else {
153
- let type = 'text/plain';
154
- if (ext == 'json') {
155
- if (typeof data != 'string') data = JSON.stringify(data);
156
- type = 'text/json';
157
- }
158
- data = new Blob([data], { type });
159
- data = URL.createObjectURL(data);
160
- }
161
- let a = document.createElement('a');
162
- a.href = data;
163
- a.download = name + '.' + ext;
164
- a.click();
165
- URL.revokeObjectURL(a.href);
166
- };
167
- $.save = (a, b, c) => {
168
- if (!a || (typeof a == 'string' && (!b || (!c && b.length < 5)))) {
169
- c = b;
170
- b = a;
171
- a = $.canvas;
172
- }
173
- if (c) return $._save(a, b, c);
174
- if (b) {
175
- b = b.split('.');
176
- $._save(a, b[0], b.at(-1));
177
- } else $._save(a);
178
- };
179
-
180
193
  $.get = (x, y, w, h) => {
181
194
  let pd = $._pixelDensity || 1;
182
195
  if (x !== undefined && w === undefined) {
@@ -221,174 +234,23 @@ Q5.modules.q2d_image = ($, p) => {
221
234
 
222
235
  $.loadPixels = () => {
223
236
  imgData = $.ctx.getImageData(0, 0, $.canvas.width, $.canvas.height);
224
- p.pixels = imgData.data;
237
+ q.pixels = imgData.data;
225
238
  };
226
239
  $.updatePixels = () => {
227
240
  if (imgData != null) $.ctx.putImageData(imgData, 0, 0);
228
241
  };
229
242
 
230
- $._tinted = function (col) {
231
- let alpha = col.a;
232
- col.a = 255;
233
- makeTmpCtx();
234
- tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
235
- tmpCtx.fillStyle = col.toString();
236
- tmpCtx.fillRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
237
- tmpCtx.globalCompositeOperation = 'multiply';
238
- tmpCtx.drawImage($.ctx.canvas, 0, 0);
239
- tmpCtx.globalCompositeOperation = 'source-over';
240
-
241
- $.ctx.save();
242
- $.ctx.resetTransform();
243
- let old = $.ctx.globalCompositeOperation;
244
- $.ctx.globalCompositeOperation = 'source-in';
245
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
246
- $.ctx.globalCompositeOperation = old;
247
- $.ctx.restore();
248
-
249
- tmpCtx.globalAlpha = alpha / 255;
250
- tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
251
- tmpCtx.drawImage($.ctx.canvas, 0, 0);
252
- tmpCtx.globalAlpha = 1;
253
-
254
- $.ctx.save();
255
- $.ctx.resetTransform();
256
- $.ctx.clearRect(0, 0, $.ctx.canvas.width, $.ctx.canvas.height);
257
- $.ctx.drawImage(tmpCtx.canvas, 0, 0);
258
- $.ctx.restore();
259
- };
260
-
261
243
  $.smooth = () => ($.ctx.imageSmoothingEnabled = true);
262
244
  $.noSmooth = () => ($.ctx.imageSmoothingEnabled = false);
263
245
 
264
246
  if ($._scope == 'image') return;
265
247
 
266
- $.saveCanvas = $.canvas.save = $.save;
267
-
268
248
  $.tint = function (c) {
269
249
  $._tint = c._q5Color ? c : $.color(...arguments);
270
250
  };
271
251
  $.noTint = () => ($._tint = null);
272
-
273
- // IMAGING
274
-
275
- $.imageMode = (mode) => ($._imageMode = mode);
276
- $.image = (img, dx, dy, dWidth, dHeight, sx = 0, sy = 0, sWidth, sHeight) => {
277
- if ($._da) {
278
- dx *= $._da;
279
- dy *= $._da;
280
- dWidth *= $._da;
281
- dHeight *= $._da;
282
- sx *= $._da;
283
- sy *= $._da;
284
- sWidth *= $._da;
285
- sHeight *= $._da;
286
- }
287
- let drawable = img.canvas || img;
288
- if (Q5._createNodeJSCanvas) {
289
- drawable = drawable.context.canvas;
290
- }
291
- function reset() {
292
- if (!img._q5 || !$._tint) return;
293
- let c = img.ctx;
294
- c.save();
295
- c.resetTransform();
296
- c.clearRect(0, 0, c.canvas.width, c.canvas.height);
297
- c.drawImage(tmpCt2.canvas, 0, 0);
298
- c.restore();
299
- }
300
- if (img.canvas && $._tint != null) {
301
- makeTmpCt2(img.canvas.width, img.canvas.height);
302
- tmpCt2.drawImage(img.canvas, 0, 0);
303
- img._tinted($._tint);
304
- }
305
- dWidth ??= img.width || img.videoWidth;
306
- dHeight ??= img.height || img.videoHeight;
307
- if ($._imageMode == 'center') {
308
- dx -= dWidth * 0.5;
309
- dy -= dHeight * 0.5;
310
- }
311
- let pd = img._pixelDensity || 1;
312
- if (!sWidth) {
313
- sWidth = drawable.width || drawable.videoWidth;
314
- } else sWidth *= pd;
315
- if (!sHeight) {
316
- sHeight = drawable.height || drawable.videoHeight;
317
- } else sHeight *= pd;
318
- $.ctx.drawImage(drawable, sx * pd, sy * pd, sWidth, sHeight, dx, dy, dWidth, dHeight);
319
- reset();
320
- };
321
-
322
- $.loadImage = function (url, cb, opt) {
323
- if (url.canvas) return url;
324
- if (url.slice(-3).toLowerCase() == 'gif') {
325
- throw new Error(`q5 doesn't support GIFs due to their impact on performance. Use a video or animation instead.`);
326
- }
327
- p._preloadCount++;
328
- let last = [...arguments].at(-1);
329
- opt = typeof last == 'object' ? last : null;
330
-
331
- let g = $.createImage(1, 1, opt);
332
-
333
- function loaded(img) {
334
- let c = g.ctx;
335
- g.width = c.canvas.width = img.naturalWidth || img.width;
336
- g.height = c.canvas.height = img.naturalHeight || img.height;
337
- c.drawImage(img, 0, 0);
338
- p._preloadCount--;
339
- if (cb) cb(g);
340
- }
341
-
342
- if (Q5._nodejs && global.CairoCanvas) {
343
- global.CairoCanvas.loadImage(url)
344
- .then(loaded)
345
- .catch((e) => {
346
- p._preloadCount--;
347
- throw e;
348
- });
349
- } else {
350
- let img = new window.Image();
351
- img.src = url;
352
- img.crossOrigin = 'Anonymous';
353
- img._pixelDensity = 1;
354
- img.onload = () => loaded(img);
355
- img.onerror = (e) => {
356
- p._preloadCount--;
357
- throw e;
358
- };
359
- }
360
- return g;
361
- };
362
252
  };
363
253
 
364
- // IMAGE CLASS
365
-
366
- Q5.imageModules = ['q2d_canvas', 'q2d_image'];
367
-
368
- class _Q5Image {
369
- constructor(w, h, opt) {
370
- let $ = this;
371
- $._scope = 'image';
372
- $.canvas = $.ctx = $.drawingContext = null;
373
- $.pixels = [];
374
- for (let m of Q5.imageModules) {
375
- Q5.modules[m]($, $);
376
- }
377
- delete this.createCanvas;
378
-
379
- this._createCanvas(w, h, '2d', opt);
380
- this._loop = false;
381
- }
382
- get w() {
383
- return this.width;
384
- }
385
- get h() {
386
- return this.height;
387
- }
388
- }
389
-
390
- Q5.Image ??= _Q5Image;
391
-
392
254
  Q5.THRESHOLD = 1;
393
255
  Q5.GRAY = 2;
394
256
  Q5.OPAQUE = 3;
@@ -1,10 +1,10 @@
1
1
  /* software implementation of image filters */
2
- Q5.modules.q2d_soft_filters = ($) => {
2
+ Q5.renderers.q2d.soft_filters = ($) => {
3
3
  let tmpBuf = null;
4
4
 
5
- function makeTmpBuf() {
5
+ function ensureTmpBuf() {
6
6
  let l = $.canvas.width * $.canvas.height * 4;
7
- if (!tmpBuf || l != tmpBuf.length) {
7
+ if (!tmpBuf || tmpBuf.length != l) {
8
8
  tmpBuf = new Uint8ClampedArray(l);
9
9
  }
10
10
  }
@@ -46,7 +46,7 @@ Q5.modules.q2d_soft_filters = ($) => {
46
46
  }
47
47
  };
48
48
  $._filters[Q5.DILATE] = (data) => {
49
- makeTmpBuf();
49
+ ensureTmpBuf();
50
50
  tmpBuf.set(data);
51
51
  let [w, h] = [$.canvas.width, $.canvas.height];
52
52
  for (let i = 0; i < h; i++) {
@@ -73,7 +73,7 @@ Q5.modules.q2d_soft_filters = ($) => {
73
73
  }
74
74
  };
75
75
  $._filters[Q5.ERODE] = (data) => {
76
- makeTmpBuf();
76
+ ensureTmpBuf();
77
77
  tmpBuf.set(data);
78
78
  let [w, h] = [$.canvas.width, $.canvas.height];
79
79
  for (let i = 0; i < h; i++) {
@@ -102,7 +102,7 @@ Q5.modules.q2d_soft_filters = ($) => {
102
102
  $._filters[Q5.BLUR] = (data, rad) => {
103
103
  rad = rad || 1;
104
104
  rad = Math.floor(rad * $._pixelDensity);
105
- makeTmpBuf();
105
+ ensureTmpBuf();
106
106
  tmpBuf.set(data);
107
107
 
108
108
  let ksize = rad * 2 + 1;
package/src/q5-2d-text.js CHANGED
@@ -1,4 +1,4 @@
1
- Q5.modules.q2d_text = ($, p) => {
1
+ Q5.renderers.q2d.text = ($, q) => {
2
2
  $.NORMAL = 'normal';
3
3
  $.ITALIC = 'italic';
4
4
  $.BOLD = 'bold';
@@ -18,12 +18,12 @@ Q5.modules.q2d_text = ($, p) => {
18
18
  $._textStyle = 'normal';
19
19
 
20
20
  $.loadFont = (url, cb) => {
21
- p._preloadCount++;
21
+ q._preloadCount++;
22
22
  let name = url.split('/').pop().split('.')[0].replace(' ', '');
23
23
  let f = new FontFace(name, `url(${url})`);
24
24
  document.fonts.add(f);
25
25
  f.load().then(() => {
26
- p._preloadCount--;
26
+ q._preloadCount--;
27
27
  if (cb) cb(name);
28
28
  });
29
29
  return name;
package/src/q5-ai.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Q5.modules.ai = ($) => {
2
- $.askAI = (q = '') => {
3
- throw Error('Ask AI ✨ ' + q);
2
+ $.askAI = (question = '') => {
3
+ throw Error('Ask AI ✨ ' + question);
4
4
  };
5
5
 
6
6
  $._aiErrorAssistance = async (e) => {