/
canvasrendertarget.js
executable file
·303 lines (270 loc) · 12.1 KB
/
canvasrendertarget.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import { createCanvas } from "../video.js";
import { setPrefixed } from "../../utils/agent.js";
import { clamp } from "../../math/math.js";
// default canvas settings
let defaultAttributes = {
offscreenCanvas : false,
willReadFrequently : false,
antiAlias : false,
context: "2d",
transparent : false,
premultipliedAlpha: true,
stencil: true,
blendMode : "normal",
failIfMajorPerformanceCaveat : true,
preferWebGL1 : false,
powerPreference : "default"
};
// WebGL version (if a gl context is created)
let WebGLVersion;
// a helper function to create the 2d/webgl context
function createContext(canvas, attributes) {
let context;
if (attributes.context === "2d") {
// 2d/canvas mode
context = canvas.getContext(attributes.context, { willReadFrequently: attributes.willReadFrequently });
} else if (attributes.context === "webgl") {
let attr = {
alpha : attributes.transparent,
antialias : attributes.antiAlias,
depth : attributes.depth,
stencil: true,
preserveDrawingBuffer : false,
premultipliedAlpha: attributes.transparent ? attributes.premultipliedAlpha : false,
powerPreference: attributes.powerPreference,
failIfMajorPerformanceCaveat : attributes.failIfMajorPerformanceCaveat
};
// attempt to create a WebGL2 context unless not requested
if (attributes.preferWebGL1 !== true) {
context = canvas.getContext("webgl2", attr);
if (context) {
WebGLVersion = 2;
}
}
// fallback to WebGL1
if (!context) {
WebGLVersion = 1;
context = canvas.getContext("webgl", attr) || canvas.getContext("experimental-webgl", attr);
}
if (!context) {
throw new Error(
"A WebGL context could not be created."
);
}
} else {
throw new Error(
"Invalid context type. Must be one of '2d' or 'webgl'"
);
}
// set the context size
return context;
}
/**
* CanvasRenderTarget is 2D render target which exposes a Canvas interface.
*/
class CanvasRenderTarget {
/**
* @param {number} width - the desired width of the canvas
* @param {number} height - the desired height of the canvas
* @param {Settings} attributes - The attributes to create both the canvas and context
* @param {boolean} [attributes.context="2d"] - the context type to be created ("2d", "webgl")
* @param {boolean} [attributes.preferWebGL1=false] - set to true for force using WebGL1 instead of WebGL2 (if supported)
* @param {boolean} [attributes.transparent=false] - specify if the canvas contains an alpha channel
* @param {boolean} [attributes.offscreenCanvas=false] - will create an offscreenCanvas if true instead of a standard canvas
* @param {boolean} [attributes.willReadFrequently=false] - Indicates whether or not a lot of read-back operations are planned
* @param {boolean} [attributes.antiAlias=false] - Whether to enable anti-aliasing, use false (default) for a pixelated effect.
*/
constructor(width, height, attributes = defaultAttributes) {
/**
* the canvas created for this CanvasRenderTarget
* @type {HTMLCanvasElement|OffscreenCanvas}
*/
this.canvas;
/**
* the rendering context of this CanvasRenderTarget
* @type {CanvasRenderingContext2D|WebGLRenderingContext}
*/
this.context;
// clean up the given attributes
this.attributes = Object.assign({}, defaultAttributes, attributes);
// make sure context is defined
if (typeof attributes.context === "undefined") {
attributes.context = "2d";
}
// used the given canvas if any
if (typeof attributes.canvas !== "undefined") {
this.canvas = attributes.canvas;
} else {
this.canvas = createCanvas(width, height, this.attributes.offscreenCanvas);
}
// create the context
this.context = createContext(this.canvas, this.attributes);
this.WebGLVersion = WebGLVersion;
// enable or disable antiAlias if specified
this.setAntiAlias(this.attributes.antiAlias);
}
/**
* @ignore
*/
onResetEvent(width, height) {
this.clear();
this.resize(width, height);
}
/**
* Clears the content of the canvas texture
*/
clear() {
this.context.setTransform(1, 0, 0, 1, 0, 0);
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
/**
* enable/disable image smoothing (scaling interpolation)
* @param {boolean} [enable=false] - whether to enable or not image smoothing (scaling interpolation)
*/
setAntiAlias(enable = false) {
let canvas = this.canvas;
// enable/disable antialias on the given Context2d object
setPrefixed("imageSmoothingEnabled", enable, this.context);
// set antialias CSS property on the main canvas
if (typeof canvas.style !== "undefined") {
if (enable !== true) {
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
canvas.style["image-rendering"] = "optimizeSpeed"; // legal fallback
canvas.style["image-rendering"] = "-moz-crisp-edges"; // Firefox
canvas.style["image-rendering"] = "-o-crisp-edges"; // Opera
canvas.style["image-rendering"] = "-webkit-optimize-contrast"; // Safari
canvas.style["image-rendering"] = "optimize-contrast"; // CSS 3
canvas.style["image-rendering"] = "crisp-edges"; // CSS 4
canvas.style["image-rendering"] = "pixelated"; // CSS 4
canvas.style.msInterpolationMode = "nearest-neighbor"; // IE8+
} else {
canvas.style["image-rendering"] = "auto";
}
}
}
/**
* Resizes the canvas texture to the given width and height.
* @param {number} width - the desired width
* @param {number} height - the desired height
*/
resize(width, height) {
this.canvas.width = Math.round(width);
this.canvas.height = Math.round(height);
}
/**
* Returns an ImageData object representing the underlying pixel data for a specified portion of this canvas texture.
* (Note: when using getImageData(), it is highly recommended to use the `willReadFrequently` attribute when creatimg the corresponding canvas texture)
* @param {number} x - The x-axis coordinate of the top-left corner of the rectangle from which the ImageData will be extracted
* @param {number} y - The y-axis coordinate of the top-left corner of the rectangle from which the ImageData will be extracted
* @param {number} width - The width of the rectangle from which the ImageData will be extracted. Positive values are to the right, and negative to the left
* @param {number} height - The height of the rectangle from which the ImageData will be extracted. Positive values are down, and negative are up
* @returns {ImageData} The ImageData extracted from this CanvasRenderTarget.
*/
getImageData(x, y, width, height) {
// clamp values
x = clamp(Math.floor(x), 0, this.canvas.width - 1);
y = clamp(Math.floor(y), 0, this.canvas.height - 1);
width = clamp(width, 1, this.canvas.width - x);
height = clamp(height, 1, this.canvas.height - y);
// return imageData
return this.context.getImageData(x, y, width, height);
}
/**
* creates a Blob object representing the image contained in this canvas texture
* @param {string} [type="image/png"] - A string indicating the image format
* @param {number} [quality] - A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range.
* @returns {Promise} A Promise returning a Blob object representing the image contained in this canvas texture
* @example
* renderTarget.convertToBlob().then((blob) => console.log(blob));
*/
toBlob(type = "image/png", quality) {
if (typeof this.canvas.convertToBlob === "function") {
return this.canvas.convertToBlob(type, quality);
} else {
return new Promise(function(resolve) {
this.canvas.toBlob((blob) => {
resolve(blob);
}, type, quality);
});
}
}
/**
* creates an ImageBitmap object from the most recently rendered image of this canvas texture
* @param {string} [type="image/png"] - A string indicating the image format
* @param {number} [quality] - A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range.
* @returns {Promise} A Promise returning an ImageBitmap.
* @example
* renderTarget.transferToImageBitmap().then((bitmap) => console.log(bitmap));
*/
toImageBitmap(type = "image/png", quality) {
return new Promise((resolve) => {
if (typeof this.canvas.transferToImageBitmap === "function") {
resolve(this.canvas.transferToImageBitmap());
} else {
let image = new Image();
image.src = this.canvas.toDataURL(type, quality);
image.onload = () => {
globalThis.createImageBitmap(image).then((bitmap) => resolve(bitmap));
};
}
});
}
/**
* returns a data URL containing a representation of the most recently rendered image of this canvas texture
* (not supported by OffscreenCanvas)
* @param {string} [type="image/png"] - A string indicating the image format
* @param {number} [quality] - A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as image/jpeg or image/webp). A user agent will use its default quality value if this option is not specified, or if the number is outside the allowed range.
* @returns {Promise} A Promise returning a string containing the requested data URL.
* @example
* renderer.toDataURL().then((dataURL) => console.log(dataURL));
*/
toDataURL(type = "image/png", quality) {
return new Promise((resolve) => {
resolve(this.canvas.toDataURL(type, quality));
});
}
/**
* invalidate the current CanvasRenderTarget, and force a reupload of the corresponding texture
* (call this if you modify the canvas content between two draw calls)
* @param {CanvasRenderer|WebGLRenderer} renderer - the renderer to which this canvas texture is attached
*/
invalidate(renderer) {
if (typeof renderer.gl !== "undefined") {
// make sure the right compositor is active
renderer.setCompositor("quad");
// invalidate the previous corresponding texture so that it can reuploaded once changed
this.glTextureUnit = renderer.cache.getUnit(renderer.cache.get(this.canvas));
renderer.currentCompositor.unbindTexture2D(null, this.glTextureUnit);
}
}
/**
* @ignore
*/
destroy() {
this.context = undefined;
this.canvas = undefined;
}
/**
* The width of this canvas texture in pixels
* @public
* @type {number}
*/
get width() {
return this.canvas.width;
}
set width(val) {
this.canvas.width = Math.round(val);
}
/**
* The height of this canvas texture in pixels
* @public
* @type {number}
*/
get height() {
return this.canvas.height;
}
set height(val) {
this.canvas.height = Math.round(val);
}
}
export default CanvasRenderTarget;