pts 0.12.8 → 1.0.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.
- package/README.md +92 -80
- package/dist/index.d.mts +6208 -1254
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +6208 -1254
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9314 -10680
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +9266 -10611
- package/dist/index.mjs.map +1 -0
- package/dist/pts.js +9446 -10777
- package/dist/pts.js.map +1 -0
- package/dist/pts.min.js +3 -5
- package/dist/pts.min.js.map +1 -0
- package/package.json +91 -31
- package/src/Canvas.ts +1642 -0
- package/src/Color.ts +1109 -0
- package/src/Create.ts +1547 -0
- package/src/Dom.ts +940 -0
- package/src/Form.ts +312 -0
- package/src/Image.ts +722 -0
- package/src/LinearAlgebra.ts +530 -0
- package/src/Num.ts +1091 -0
- package/src/Op.ts +2127 -0
- package/src/Physics.ts +1233 -0
- package/src/Play.ts +861 -0
- package/src/Pt.ts +1303 -0
- package/src/Space.ts +898 -0
- package/src/Svg.ts +1573 -0
- package/src/Types.ts +301 -0
- package/src/Typography.ts +228 -0
- package/src/UI.ts +757 -0
- package/src/Util.ts +454 -0
- package/src/_module.ts +18 -0
- package/src/_script.ts +94 -0
- package/src/_triangulate.ts +884 -0
- package/src/uheprng.ts +153 -0
package/src/Image.ts
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import { CanvasForm, type CanvasSpace } from "./Canvas";
|
|
2
|
+
import { type Bound, Pt } from "./Pt";
|
|
3
|
+
import { Mat } from "./LinearAlgebra";
|
|
4
|
+
import { type PtLike, type CanvasPatternRepetition } from "./Types";
|
|
5
|
+
import { Util } from "./Util";
|
|
6
|
+
import { type RenderingContext2D } from "./Types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for creating an [`Img`](#link).
|
|
10
|
+
*/
|
|
11
|
+
export type ImgOptions = {
|
|
12
|
+
/** Specify if you want to manipulate pixels of this image. Default is `false`. */
|
|
13
|
+
editable?: boolean;
|
|
14
|
+
/** Set the `CanvasSpace` reference so the image's pixelScale matches the canvas. */
|
|
15
|
+
space?: CanvasSpace;
|
|
16
|
+
/** Enable loading cross-domain images. The image server must also allow it. */
|
|
17
|
+
crossOrigin?: boolean;
|
|
18
|
+
/** Set a specific pixel scale, overriding the space's. */
|
|
19
|
+
pixelScale?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Img provides convenient functions to support image operations on HTML Canvas and [`CanvasSpace`](#link). Combine this with other Pts functions to experiment with visual forms that integrate bitmaps and vector graphics.
|
|
24
|
+
*/
|
|
25
|
+
export class Img {
|
|
26
|
+
protected _img!: HTMLImageElement;
|
|
27
|
+
protected _data!: ImageData;
|
|
28
|
+
protected _cv!: HTMLCanvasElement;
|
|
29
|
+
protected _ctx!: RenderingContext2D;
|
|
30
|
+
protected _scale: number = 1;
|
|
31
|
+
|
|
32
|
+
protected _loaded: boolean = false;
|
|
33
|
+
protected _editable: boolean;
|
|
34
|
+
|
|
35
|
+
protected _space: CanvasSpace | undefined;
|
|
36
|
+
protected _patternCtx!: RenderingContext2D; // lazy fallback when no space is set
|
|
37
|
+
protected _objectUrl!: string | null; // tracked for revocation on dispose
|
|
38
|
+
private _pendingLoadReject: ((err: Error) => void) | null = null; // newer loads supersede pending ones
|
|
39
|
+
private _disposed = false;
|
|
40
|
+
protected _dataDirty: boolean = false; // ImageData refreshes lazily on first read
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create an Img
|
|
44
|
+
* @param editable either an [`ImgOptions`](#link) object, or a boolean specifying if you want to manipulate pixels of this image. Default is `false`.
|
|
45
|
+
* @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
|
|
46
|
+
* @param crossOrigin an optional parameter to enable loading cross-domain images if set to true. The image server's configuration must also be set correctly. For more, see [this documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image).
|
|
47
|
+
* @example `new Img(true, space)`, `new Img({ editable: true, pixelScale: 2 })`
|
|
48
|
+
*/
|
|
49
|
+
constructor(
|
|
50
|
+
editable: boolean | ImgOptions = false,
|
|
51
|
+
space?: CanvasSpace,
|
|
52
|
+
crossOrigin?: boolean,
|
|
53
|
+
) {
|
|
54
|
+
const opts: ImgOptions =
|
|
55
|
+
typeof editable === "object"
|
|
56
|
+
? editable
|
|
57
|
+
: { editable, space, crossOrigin };
|
|
58
|
+
this._editable = !!opts.editable;
|
|
59
|
+
this._space = opts.space;
|
|
60
|
+
this._scale = opts.pixelScale ?? (this._space ? this._space.pixelScale : 1);
|
|
61
|
+
this._img = new Image();
|
|
62
|
+
if (opts.crossOrigin) this._img.crossOrigin = "Anonymous";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A static function to load an image, returning a Promise that resolves to the loaded Img.
|
|
67
|
+
* A load failure rejects the Promise.
|
|
68
|
+
* @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
|
|
69
|
+
* @param editable either an [`ImgOptions`](#link) object, or a boolean specifying if you want to manipulate pixels of this image. Default is `false`.
|
|
70
|
+
* @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
|
|
71
|
+
* @param ready An optional callback, invoked with the Img when loading succeeds
|
|
72
|
+
* @example `const img = await Img.load("photo.jpg", true)`
|
|
73
|
+
*/
|
|
74
|
+
static load(
|
|
75
|
+
src: string,
|
|
76
|
+
editable: boolean | ImgOptions = false,
|
|
77
|
+
space?: CanvasSpace,
|
|
78
|
+
ready?: (img: Img) => void,
|
|
79
|
+
): Promise<Img> {
|
|
80
|
+
return new Img(editable, space).load(src).then((res) => {
|
|
81
|
+
if (ready) ready(res);
|
|
82
|
+
return res;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A static method to load an image using async/await.
|
|
88
|
+
* @deprecated Use [`Img.load`](#link), which now returns a Promise.
|
|
89
|
+
* @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
|
|
90
|
+
* @param editable Specify if you want to manipulate pixels of this image. Default is `false`.
|
|
91
|
+
* @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
|
|
92
|
+
*/
|
|
93
|
+
static async loadAsync(
|
|
94
|
+
src: string,
|
|
95
|
+
editable: boolean | ImgOptions = false,
|
|
96
|
+
space?: CanvasSpace,
|
|
97
|
+
): Promise<Img> {
|
|
98
|
+
return Img.load(src, editable, space);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A static method to load an image pattern using async/await.
|
|
103
|
+
* @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
|
|
104
|
+
* @param space Set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
|
|
105
|
+
* @param repeat set how the pattern will repeat fills
|
|
106
|
+
* @param editable Specify if you want to manipulate pixels of this image. Default is `false`.
|
|
107
|
+
* @returns a `CanvasPattern` instance for use in `fill()`
|
|
108
|
+
*/
|
|
109
|
+
static async loadPattern(
|
|
110
|
+
src: string,
|
|
111
|
+
space: CanvasSpace,
|
|
112
|
+
repeat: CanvasPatternRepetition = "repeat",
|
|
113
|
+
editable: boolean = false,
|
|
114
|
+
) {
|
|
115
|
+
const img = await Img.loadAsync(src, editable, space);
|
|
116
|
+
return img.pattern(repeat);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create an editable blank image
|
|
121
|
+
* @param size of image
|
|
122
|
+
* @param space Optionally set the `CanvasSpace` reference. This is optional but will make sure the image's pixelScale match the canvas and set the context for creating pattern.
|
|
123
|
+
* @param scale Optionally set a specific pixel scale (density) of the image canvas.
|
|
124
|
+
*/
|
|
125
|
+
static blank(size: PtLike, space?: CanvasSpace, scale?: number): Img {
|
|
126
|
+
let img = new Img(true, space);
|
|
127
|
+
const s = scale ? scale : space ? space.pixelScale : 1;
|
|
128
|
+
img.initCanvas(size[0], size[1], s);
|
|
129
|
+
return img;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load an image.
|
|
134
|
+
* @param src an url of the image in same domain. Alternatively you can use a base64 string. To load from Blob, use `Img.fromBlob`.
|
|
135
|
+
* @returns a Promise that resolves to an Img
|
|
136
|
+
*/
|
|
137
|
+
load(src: string): Promise<Img> {
|
|
138
|
+
if (this._editable && typeof document === "undefined") {
|
|
139
|
+
return Promise.reject(
|
|
140
|
+
new Error("Cannot create html canvas element. document not found."),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return this._loadImageSrc(src).then(() => {
|
|
145
|
+
if (this._disposed) throw new Error("Img has been disposed");
|
|
146
|
+
if (this._editable) {
|
|
147
|
+
if (!this._cv)
|
|
148
|
+
this._cv = document.createElement("canvas") as HTMLCanvasElement;
|
|
149
|
+
this._drawToScale(this._scale, this._img);
|
|
150
|
+
this._dataDirty = true;
|
|
151
|
+
}
|
|
152
|
+
this._loaded = true;
|
|
153
|
+
return this;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Swap the underlying image's source and await its load — without the editable
|
|
159
|
+
* pipeline. Shared by `load()` and `sync()` so both respect the supersede rule.
|
|
160
|
+
*/
|
|
161
|
+
protected _loadImageSrc(src: string): Promise<void> {
|
|
162
|
+
if (this._disposed)
|
|
163
|
+
return Promise.reject(new Error("Img has been disposed"));
|
|
164
|
+
return new Promise<void>((resolve, reject) => {
|
|
165
|
+
// a newer load replaces this one's handlers on the shared <img>, so a
|
|
166
|
+
// pending previous promise must be rejected proactively
|
|
167
|
+
if (this._pendingLoadReject) {
|
|
168
|
+
this._pendingLoadReject(
|
|
169
|
+
new Error("Img loading superseded by a newer load"),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
this._pendingLoadReject = reject;
|
|
173
|
+
|
|
174
|
+
this._img.onload = () => {
|
|
175
|
+
this._pendingLoadReject = null;
|
|
176
|
+
resolve();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
this._img.onerror = () => {
|
|
180
|
+
this._pendingLoadReject = null;
|
|
181
|
+
reject(new Error(`Img cannot load ${src}`));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
this._img.src = src;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Refresh the cached `ImageData` from the current canvas. */
|
|
189
|
+
protected _refreshData(): void {
|
|
190
|
+
this._data = this._ctx.getImageData(0, 0, this._cv.width, this._cv.height);
|
|
191
|
+
this._dataDirty = false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Materialize the cached `ImageData` lazily, on first read after a change. */
|
|
195
|
+
protected _ensureData(): void {
|
|
196
|
+
if ((this._dataDirty || !this._data) && this._ctx && this._cv) {
|
|
197
|
+
this._refreshData();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Rescale the canvas and draw an image-source on it.
|
|
203
|
+
* @param canvasScale rescale factor for the canvas
|
|
204
|
+
* @param img an image source like Image, Canvas, or ImageBitmap.
|
|
205
|
+
*/
|
|
206
|
+
protected _drawToScale(
|
|
207
|
+
canvasScale: number | PtLike,
|
|
208
|
+
img:
|
|
209
|
+
| HTMLImageElement
|
|
210
|
+
| HTMLCanvasElement
|
|
211
|
+
| ImageBitmap
|
|
212
|
+
| OffscreenCanvas
|
|
213
|
+
| HTMLVideoElement,
|
|
214
|
+
) {
|
|
215
|
+
const nw = img.width as number;
|
|
216
|
+
const nh = img.height as number;
|
|
217
|
+
this._initCanvas(nw, nh, canvasScale);
|
|
218
|
+
if (img)
|
|
219
|
+
this._ctx.drawImage(
|
|
220
|
+
img,
|
|
221
|
+
0,
|
|
222
|
+
0,
|
|
223
|
+
nw,
|
|
224
|
+
nh,
|
|
225
|
+
0,
|
|
226
|
+
0,
|
|
227
|
+
this._cv.width,
|
|
228
|
+
this._cv.height,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Initiate an editable canvas
|
|
234
|
+
* @param width width of canvas
|
|
235
|
+
* @param height height of canvas
|
|
236
|
+
* @param canvasScale pixel scale
|
|
237
|
+
*/
|
|
238
|
+
initCanvas(width: number, height: number, canvasScale: number | PtLike = 1) {
|
|
239
|
+
this._initCanvas(width, height, canvasScale);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Internal canvas setup without the pixel-data refresh — callers that draw
|
|
244
|
+
* immediately afterwards refresh once after their draw instead.
|
|
245
|
+
*/
|
|
246
|
+
protected _initCanvas(
|
|
247
|
+
width: number,
|
|
248
|
+
height: number,
|
|
249
|
+
canvasScale: number | PtLike = 1,
|
|
250
|
+
) {
|
|
251
|
+
if (!this._editable) {
|
|
252
|
+
Util.warn(
|
|
253
|
+
"Cannot initiate canvas because this Img is not set to be editable",
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!this._cv)
|
|
259
|
+
this._cv = document.createElement("canvas") as HTMLCanvasElement;
|
|
260
|
+
|
|
261
|
+
const cms =
|
|
262
|
+
typeof canvasScale === "number"
|
|
263
|
+
? [canvasScale, canvasScale]
|
|
264
|
+
: canvasScale;
|
|
265
|
+
this._cv.width = width * cms[0];
|
|
266
|
+
this._cv.height = height * cms[1];
|
|
267
|
+
// the whole point of an editable Img is repeated readback
|
|
268
|
+
this._ctx = this._cv.getContext("2d", { willReadFrequently: true })!;
|
|
269
|
+
// resizing resets the context state; forget any cached style values so a
|
|
270
|
+
// CanvasForm from getForm() re-applies its styles
|
|
271
|
+
CanvasForm.resetStyleCache(this._ctx);
|
|
272
|
+
// keep the pixel-density field coherent with the actual canvas scaling,
|
|
273
|
+
// which `pixel( p, true )` depends on
|
|
274
|
+
if (typeof canvasScale === "number") this._scale = canvasScale;
|
|
275
|
+
this._dataDirty = true; // pixel reads materialize lazily
|
|
276
|
+
this._loaded = true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get an efficient, readonly bitmap of the current canvas.
|
|
281
|
+
* @param size Optional size to crop
|
|
282
|
+
* @returns a Promise that resolves to an ImageBitmap
|
|
283
|
+
*/
|
|
284
|
+
bitmap(size?: PtLike): Promise<ImageBitmap> {
|
|
285
|
+
const w = size ? size[0] : this._cv.width;
|
|
286
|
+
const h = size ? size[1] : this._cv.height;
|
|
287
|
+
return createImageBitmap(this._cv, 0, 0, w, h);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a canvas pattern for `fill()`
|
|
292
|
+
* @param reptition set how the pattern should repeat-fill
|
|
293
|
+
* @param dynamic If true, use this Img's internal canvas content as pattern fill. This enables the pattern to update dynamically.
|
|
294
|
+
* @returns a `CanvasPattern` instance for use in `fill()`
|
|
295
|
+
*/
|
|
296
|
+
pattern(
|
|
297
|
+
reptition: CanvasPatternRepetition = "repeat",
|
|
298
|
+
dynamic: boolean = false,
|
|
299
|
+
): CanvasPattern {
|
|
300
|
+
// any 2D context can create a pattern; fall back to an internal one so a
|
|
301
|
+
// CanvasSpace reference is optional
|
|
302
|
+
let ctx: RenderingContext2D | undefined = this._space
|
|
303
|
+
? this._space.ctx
|
|
304
|
+
: undefined;
|
|
305
|
+
if (!ctx) {
|
|
306
|
+
if (!this._patternCtx) {
|
|
307
|
+
this._patternCtx = document.createElement("canvas").getContext("2d")!;
|
|
308
|
+
}
|
|
309
|
+
ctx = this._patternCtx;
|
|
310
|
+
}
|
|
311
|
+
return ctx.createPattern(dynamic ? this._cv : this._img, reptition)!;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Replace the image with the current canvas data. For example, you can use CanvasForm's static functions to draw on `this.ctx` and then update the current image.
|
|
316
|
+
* To display the internal canvas, use `form.image( [0, 0], img.current )`.
|
|
317
|
+
*/
|
|
318
|
+
async sync(): Promise<Img> {
|
|
319
|
+
if (this._disposed) throw new Error("Img has been disposed");
|
|
320
|
+
// Blob-blit instead of a base64 round-trip: encode asynchronously, load
|
|
321
|
+
// the result into the image, and leave the working canvas untouched (the
|
|
322
|
+
// canvas is already the source of truth, so no redraw or readback is
|
|
323
|
+
// needed — and the retina canvas is no longer squashed through a lossy
|
|
324
|
+
// reload; a temporary canvas produces the logical-size image instead).
|
|
325
|
+
let source: HTMLCanvasElement = this._cv;
|
|
326
|
+
if (this._scale !== 1) {
|
|
327
|
+
source = document.createElement("canvas");
|
|
328
|
+
source.width = this._cv.width / this._scale;
|
|
329
|
+
source.height = this._cv.height / this._scale;
|
|
330
|
+
source
|
|
331
|
+
.getContext("2d")!
|
|
332
|
+
.drawImage(
|
|
333
|
+
this._cv,
|
|
334
|
+
0,
|
|
335
|
+
0,
|
|
336
|
+
this._cv.width,
|
|
337
|
+
this._cv.height,
|
|
338
|
+
0,
|
|
339
|
+
0,
|
|
340
|
+
source.width,
|
|
341
|
+
source.height,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
346
|
+
source.toBlob((b) =>
|
|
347
|
+
b ? resolve(b) : reject(new Error("Img cannot export canvas to blob")),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const url = URL.createObjectURL(blob);
|
|
352
|
+
this._objectUrl = url;
|
|
353
|
+
try {
|
|
354
|
+
await this._loadImageSrc(url);
|
|
355
|
+
if (this._disposed) throw new Error("Img has been disposed");
|
|
356
|
+
this._loaded = true;
|
|
357
|
+
} finally {
|
|
358
|
+
URL.revokeObjectURL(url);
|
|
359
|
+
this._objectUrl = null;
|
|
360
|
+
}
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get the RGBA values of a pixel in the image
|
|
366
|
+
* @param p position of the pixel
|
|
367
|
+
* @param rescale Specify if the pixel position should be scaled. Usually use rescale when tracking image and don't rescale when tracking canvas. You may also set a custom scale value.
|
|
368
|
+
* @returns [R,G,B,A] values of the pixel at the specific position
|
|
369
|
+
*/
|
|
370
|
+
pixel(p: PtLike, rescale: boolean | number = true): Pt {
|
|
371
|
+
this._ensureData();
|
|
372
|
+
if (!this._data) {
|
|
373
|
+
Util.warn(
|
|
374
|
+
"Img has no pixel data — create it as editable and wait for load",
|
|
375
|
+
);
|
|
376
|
+
return new Pt(0, 0, 0, 0);
|
|
377
|
+
}
|
|
378
|
+
const s = typeof rescale == "number" ? rescale : rescale ? this._scale : 1;
|
|
379
|
+
return Img.getPixel(this._data, [p[0] * s, p[1] * s]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Set the RGBA values of a pixel in the cached `ImageData`. Call [`Img.updatePixels`](#link)
|
|
384
|
+
* to write the changes onto the canvas.
|
|
385
|
+
* @param p position of the pixel
|
|
386
|
+
* @param rgba [R,G,B,A] values, 0-255
|
|
387
|
+
* @param rescale Specify if the pixel position should be scaled, matching [`Img.pixel`](#link)
|
|
388
|
+
*/
|
|
389
|
+
setPixel(p: PtLike, rgba: PtLike, rescale: boolean | number = true): this {
|
|
390
|
+
this._ensureData();
|
|
391
|
+
if (!this._data) {
|
|
392
|
+
Util.warn("Img has no pixel data — create it as editable");
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
const s = typeof rescale == "number" ? rescale : rescale ? this._scale : 1;
|
|
396
|
+
const x = Math.floor(p[0] * s);
|
|
397
|
+
const y = Math.floor(p[1] * s);
|
|
398
|
+
if (x < 0 || y < 0 || x >= this._data.width || y >= this._data.height) {
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
const i = y * this._data.width * 4 + x * 4;
|
|
402
|
+
this._data.data[i] = rgba[0];
|
|
403
|
+
this._data.data[i + 1] = rgba[1];
|
|
404
|
+
this._data.data[i + 2] = rgba[2];
|
|
405
|
+
this._data.data[i + 3] = rgba[3] !== undefined ? rgba[3] : 255;
|
|
406
|
+
return this;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Refresh the cached `ImageData` from the canvas — call this after drawing on the
|
|
411
|
+
* canvas (eg, via [`Img.getForm`](#link)) before reading pixels.
|
|
412
|
+
*/
|
|
413
|
+
loadPixels(): this {
|
|
414
|
+
if (!this._ctx) {
|
|
415
|
+
Util.warn("Img has no canvas — create it as editable");
|
|
416
|
+
return this;
|
|
417
|
+
}
|
|
418
|
+
this._refreshData();
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Write the cached `ImageData` (eg, after [`Img.setPixel`](#link) calls) back onto
|
|
424
|
+
* the canvas.
|
|
425
|
+
*/
|
|
426
|
+
updatePixels(): this {
|
|
427
|
+
if (!this._ctx || !this._data) {
|
|
428
|
+
Util.warn("Img has no canvas — create it as editable");
|
|
429
|
+
return this;
|
|
430
|
+
}
|
|
431
|
+
this._ctx.putImageData(this._data, 0, 0);
|
|
432
|
+
this._dataDirty = false; // canvas now equals the cached data
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Given an ImaegData object and a position, return the RGBA pixel value at that position.
|
|
438
|
+
* @param imgData an ImageData object
|
|
439
|
+
* @param p a position on the image
|
|
440
|
+
* @returns [R,G,B,A] values of the pixel at the specific position
|
|
441
|
+
*/
|
|
442
|
+
static getPixel(imgData: ImageData, p: PtLike): Pt {
|
|
443
|
+
// `new Pt(4)` + element stores is ~8x faster than the 4-argument
|
|
444
|
+
// constructor path, and out-of-bound reads return the zeroed Pt as before
|
|
445
|
+
const out = new Pt(4);
|
|
446
|
+
if (
|
|
447
|
+
p[0] < 0 ||
|
|
448
|
+
p[1] < 0 ||
|
|
449
|
+
p[0] >= imgData.width ||
|
|
450
|
+
p[1] >= imgData.height
|
|
451
|
+
) {
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const i = Math.floor(p[1]) * (imgData.width * 4) + Math.floor(p[0]) * 4;
|
|
456
|
+
const d = imgData.data;
|
|
457
|
+
if (i > d.length - 4) return out;
|
|
458
|
+
|
|
459
|
+
out[0] = d[i];
|
|
460
|
+
out[1] = d[i + 1];
|
|
461
|
+
out[2] = d[i + 2];
|
|
462
|
+
out[3] = d[i + 3];
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Resize the canvas image. The original image is unchanged until `sync()`.
|
|
468
|
+
* @param sizeOrScale A PtLike array specifying either [x, y] scales or [x, y] sizes.
|
|
469
|
+
* @param asScale If true, treat the first parameter as scales. Otherwise, treat it as specific sizes.
|
|
470
|
+
*/
|
|
471
|
+
resize(sizeOrScale: PtLike, asScale: boolean = false): this {
|
|
472
|
+
const hasImage = this._img.naturalWidth > 0;
|
|
473
|
+
// canvas-only images (eg, from `Img.blank`) scale relative to the canvas
|
|
474
|
+
// size, and need a snapshot since `_drawToScale` clears the canvas first
|
|
475
|
+
const refW = hasImage ? this._img.naturalWidth : this._cv.width;
|
|
476
|
+
const refH = hasImage ? this._img.naturalHeight : this._cv.height;
|
|
477
|
+
if (!refW || !refH) {
|
|
478
|
+
Util.warn("Img cannot resize before an image or canvas exists");
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
const s = asScale
|
|
482
|
+
? sizeOrScale
|
|
483
|
+
: [sizeOrScale[0] / refW, sizeOrScale[1] / refH];
|
|
484
|
+
|
|
485
|
+
let source: HTMLImageElement | HTMLCanvasElement = this._img;
|
|
486
|
+
if (!hasImage) {
|
|
487
|
+
const snap = document.createElement("canvas");
|
|
488
|
+
snap.width = this._cv.width;
|
|
489
|
+
snap.height = this._cv.height;
|
|
490
|
+
snap.getContext("2d")!.drawImage(this._cv, 0, 0);
|
|
491
|
+
source = snap;
|
|
492
|
+
}
|
|
493
|
+
this._drawToScale(s, source);
|
|
494
|
+
this._dataDirty = true;
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Crop an area of the image.
|
|
500
|
+
* @param box bounding box
|
|
501
|
+
*/
|
|
502
|
+
crop(box: Bound): ImageData {
|
|
503
|
+
const s = this._scale;
|
|
504
|
+
return this._ctx.getImageData(
|
|
505
|
+
box[0][0] * s,
|
|
506
|
+
box[0][1] * s,
|
|
507
|
+
box.width * s,
|
|
508
|
+
box.height * s,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Apply filters such as blur and grayscale to the canvas image. The original image is unchanged until `sync()`.
|
|
514
|
+
* @param css a css filter string such as "blur(10px) contrast(200%)". See [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter#browser_compatibility) for a list of filter functions.
|
|
515
|
+
*/
|
|
516
|
+
filter(css: string): this {
|
|
517
|
+
// "copy" replaces the canvas with the filtered result (the source is
|
|
518
|
+
// snapshotted before compositing) — plain source-over would blend the
|
|
519
|
+
// filtered copy with the original wherever the filter introduces alpha
|
|
520
|
+
const op = this._ctx.globalCompositeOperation;
|
|
521
|
+
this._ctx.globalCompositeOperation = "copy";
|
|
522
|
+
this._ctx.filter = css;
|
|
523
|
+
this._ctx.drawImage(this._cv, 0, 0);
|
|
524
|
+
this._ctx.filter = "none";
|
|
525
|
+
this._ctx.globalCompositeOperation = op;
|
|
526
|
+
this._dataDirty = true;
|
|
527
|
+
return this;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Dispose of the elements, data, and any object URL associated with this Img. Pending loads reject; the instance should not be reused.
|
|
532
|
+
*/
|
|
533
|
+
dispose(): this {
|
|
534
|
+
this._disposed = true;
|
|
535
|
+
this._pendingLoadReject?.(new Error("Img has been disposed"));
|
|
536
|
+
this._pendingLoadReject = null;
|
|
537
|
+
if (this._img) {
|
|
538
|
+
this._img.onload = null;
|
|
539
|
+
this._img.onerror = null;
|
|
540
|
+
this._img.removeAttribute("src");
|
|
541
|
+
}
|
|
542
|
+
if (this._objectUrl) {
|
|
543
|
+
URL.revokeObjectURL(this._objectUrl);
|
|
544
|
+
this._objectUrl = null;
|
|
545
|
+
}
|
|
546
|
+
if (this._cv) this._cv.remove();
|
|
547
|
+
if (this._img) this._img.remove();
|
|
548
|
+
this._cv = null!;
|
|
549
|
+
this._ctx = null!;
|
|
550
|
+
this._patternCtx = null!;
|
|
551
|
+
this._img = null!;
|
|
552
|
+
this._data = null!;
|
|
553
|
+
this._loaded = false;
|
|
554
|
+
return this;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Remove the elements and data associated with this Img.
|
|
559
|
+
* @deprecated Use [`Img.dispose`](#link).
|
|
560
|
+
*/
|
|
561
|
+
cleanup() {
|
|
562
|
+
this.dispose();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create a blob url that can be passed to `Img.load`
|
|
567
|
+
* @param blob an image blob such as `new Blob([my_Uint8Array], {type: 'image/png'})`
|
|
568
|
+
* @param editable Specify if you want to manipulate pixels of this image. Default is `false`.
|
|
569
|
+
*/
|
|
570
|
+
static fromBlob(
|
|
571
|
+
blob: Blob,
|
|
572
|
+
editable: boolean | ImgOptions = false,
|
|
573
|
+
space?: CanvasSpace,
|
|
574
|
+
): Promise<Img> {
|
|
575
|
+
const url = URL.createObjectURL(blob);
|
|
576
|
+
const img = new Img(editable, space);
|
|
577
|
+
img._objectUrl = url;
|
|
578
|
+
// the decoded image no longer needs the URL once the load settles;
|
|
579
|
+
// dispose() also guards this
|
|
580
|
+
const done = () => {
|
|
581
|
+
URL.revokeObjectURL(url);
|
|
582
|
+
img._objectUrl = null;
|
|
583
|
+
};
|
|
584
|
+
return img.load(url).then(
|
|
585
|
+
(res) => {
|
|
586
|
+
done();
|
|
587
|
+
return res;
|
|
588
|
+
},
|
|
589
|
+
(err) => {
|
|
590
|
+
done();
|
|
591
|
+
throw err;
|
|
592
|
+
},
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Convert ImageData object to a Blob, which you can then create an Img instance via [`Img.fromBlob`](#link). Note that the resulting image's dimensions will not account for pixel density.
|
|
598
|
+
* @param data
|
|
599
|
+
*/
|
|
600
|
+
static imageDataToBlob(data: ImageData): Promise<Blob> {
|
|
601
|
+
return new Promise(function (resolve, reject) {
|
|
602
|
+
if (typeof document === "undefined") {
|
|
603
|
+
reject(
|
|
604
|
+
new Error("Cannot create html canvas element. document not found."),
|
|
605
|
+
);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
let cv = document.createElement("canvas") as HTMLCanvasElement;
|
|
609
|
+
cv.width = data.width;
|
|
610
|
+
cv.height = data.height;
|
|
611
|
+
cv.getContext("2d")!.putImageData(data, 0, 0);
|
|
612
|
+
cv.toBlob((blob) => {
|
|
613
|
+
resolve(blob!);
|
|
614
|
+
cv.remove();
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Export current canvas image as base64 string
|
|
621
|
+
*/
|
|
622
|
+
toBase64(): string {
|
|
623
|
+
return this._cv.toDataURL();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Export current canvas image as a blob
|
|
628
|
+
*/
|
|
629
|
+
toBlob(): Promise<Blob> {
|
|
630
|
+
return new Promise((resolve) => {
|
|
631
|
+
this._cv.toBlob((blob) => resolve(blob!));
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get a CanvasForm for drawing on the internal canvas if this Img is editable
|
|
637
|
+
*/
|
|
638
|
+
getForm(): CanvasForm | undefined {
|
|
639
|
+
if (!this._editable) {
|
|
640
|
+
Util.warn("Cannot get a CanvasForm because this Img is not editable");
|
|
641
|
+
}
|
|
642
|
+
return this._ctx ? new CanvasForm(this._ctx) : undefined;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Get current image source. If editable, this will return the canvas, otherwise it will return the original image.
|
|
647
|
+
*/
|
|
648
|
+
get current(): CanvasImageSource {
|
|
649
|
+
return this._editable ? this._cv : this._img;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Get the original image
|
|
654
|
+
*/
|
|
655
|
+
get image(): HTMLImageElement {
|
|
656
|
+
return this._img;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Get the internal canvas
|
|
661
|
+
*/
|
|
662
|
+
get canvas(): HTMLCanvasElement {
|
|
663
|
+
return this._cv;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Get the internal canvas' ImageData
|
|
668
|
+
*/
|
|
669
|
+
get data(): ImageData {
|
|
670
|
+
this._ensureData();
|
|
671
|
+
return this._data;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get the internal canvas' context. You can use this to draw directly on canvas, or create a new [CanvasForm](#link) instance with it.
|
|
676
|
+
*/
|
|
677
|
+
get ctx(): RenderingContext2D {
|
|
678
|
+
return this._ctx;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Get whether the image is loaded
|
|
683
|
+
*/
|
|
684
|
+
get loaded(): boolean {
|
|
685
|
+
return this._loaded;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Get pixel density scale
|
|
690
|
+
*/
|
|
691
|
+
get pixelScale(): number {
|
|
692
|
+
return this._scale;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Get size of the original image
|
|
697
|
+
*/
|
|
698
|
+
get imageSize(): Pt {
|
|
699
|
+
if (!this._img || !this._img.width || !this._img.height) {
|
|
700
|
+
return this._cv ? this.canvasSize.$divide(this._scale) : new Pt(0, 0);
|
|
701
|
+
} else {
|
|
702
|
+
return new Pt(this._img.width, this._img.height);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Get size of the canvas
|
|
708
|
+
*/
|
|
709
|
+
get canvasSize(): Pt {
|
|
710
|
+
return new Pt(this._cv.width, this._cv.height);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Get a Mat instance with a scale transform based on current `pixelScale`.
|
|
715
|
+
* This can be useful for generating a domMatrix for transforming patterns consistently across different pixel-density screens.
|
|
716
|
+
* @example `img.scaledMatrix.translate2d(...).rotate2D(...).domMatrix`
|
|
717
|
+
*/
|
|
718
|
+
get scaledMatrix(): Mat {
|
|
719
|
+
const s = 1 / this._scale;
|
|
720
|
+
return new Mat().scale2D([s, s]);
|
|
721
|
+
}
|
|
722
|
+
}
|