-
Notifications
You must be signed in to change notification settings - Fork 4
/
paletted-canvas.js
212 lines (178 loc) · 6.73 KB
/
paletted-canvas.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
/*
* 2022 Tarpeeksi Hyvae Soft
*
* Software: Paletted canvas (https://github.com/leikareipa/paletted-canvas)
*
* This is an early in-development version of a paletted <canvas>. Future versions will add
* more documentation, fix bugs, etc.
*
*/
const isRunningInWebWorker = (typeof importScripts === "function");
if (!isRunningInWebWorker)
{
// A wrapper interface around ImageData for storing paletted image data.
class IndexedImageData {
#palette
#width
#height
data
constructor(width, height) {
if (
isNaN(width) ||
isNaN(height)
){
throw new Error("This interface supports only numeric 'width' and 'height' as arguments.");
}
this.#width = width;
this.#height = height;
this.data = new Array(width * height);
this.palette = [[0, 0, 0, 0]];
}
// To get the palette index at x as a quadruplet of 8-bit RGBA values, do "palette[x]".
// To modify individual indices of the returned palette, do "palette[x] = [R, G, B, A]".
// To replace the entire palette, do "palette = [[R, G, B, A], [R, G, B, A], ...]".
// When setting palette data, the alpha (A) component is optional - if not defined, a
// default of 255 will be used.
get palette() {
return this.#palette;
}
// Replaces the current palette with a new palette. The new palette should be an array
// containing 8-bit (0-255) RGBA quadruplet arrays; e.g. [[255, 0, 0, 255], [0, 255, 0, 255]]
// for a palette of red and green (the alpha component is optional and will default to
// 255 if not given).
set palette(newPalette) {
if (!Array.isArray(newPalette)) {
throw new Error("The palette must be an array.");
}
if (newPalette.length < 1) {
throw new Error("A palette must consist of at least one color.");
}
if (!newPalette.every(element=>Array.isArray(element))) {
throw new Error("Each entry in the palette must be a sub-array of color channel values.");
}
newPalette.forEach(color=>{
color.length = 4;
if (typeof color[3] === "undefined") {
color[3] = 255;
}
});
const palette = {
byte: newPalette,
dword: new Uint32Array(newPalette.map(color=>((color[3] << 24) | (color[2] << 16) | (color[1] << 8) | color[0]))),
};
// We use a proxy to allow "this.#palette[x] = ..." to modify individual indices even
// though the underlying this.#palette object doesn't have index keys.
this.#palette = new Proxy(palette, {
set: (palette, index, newValue)=>{
palette.byte[index] = newValue;
this.palette = palette.byte;
return true;
},
get: (palette, index)=>{
return (palette[index] || palette.byte[index]);
},
});
}
get width() {
return this.#width;
}
get height() {
return this.#height;
}
get colorSpace() {
return "indexed";
}
};
// A wrapper interface around CanvasRenderingContext2D for manipulating the drawing surface
// of a <canvas> element using indexed colors.
class CanvasRenderingContextIndexed {
#underlyingContext2D
#underlyingImageData
#width
#height
constructor(underlyingContext2D) {
if (!(underlyingContext2D instanceof CanvasRenderingContext2D)) {
throw new Error("CanvasRenderingContextIndexed requires an instance of CanvasRenderingContext2D as an argument.");
}
this.#underlyingContext2D = underlyingContext2D;
this.#width = this.#underlyingContext2D.canvas.width;
this.#height = this.#underlyingContext2D.canvas.height;
this.#underlyingImageData = this.#underlyingContext2D.createImageData(this.#width, this.#height);
this.#underlyingImageData.data.fill(0);
if (
isNaN(this.#width) ||
isNaN(this.#height) ||
(this.#height < 1) ||
(this.#width < 1)
){
throw new Error("Invalid context resolution.");
}
}
createImageData(
width = this.#width,
height = this.#height
)
{
if (width instanceof ImageData) {
throw new Error("This interface supports only 'width' and 'height' as arguments.");
}
if (
(width !== this.#width) ||
(height !== this.#height)
){
throw new Error("This interface can only create images whose resolution matches the size of the canvas.");
}
return new IndexedImageData(width, height);
}
// Returns as an ImageData object the RGBA/8888 pixel data as displayed on the canvas.
getImageData() {
return this.#underlyingImageData;
}
putImageData(indexedImage) {
if (!(indexedImage instanceof IndexedImageData)) {
throw new Error("Only images of type IndexedImageData can be rendered.");
}
if (
(indexedImage.width !== this.#width) ||
(indexedImage.height !== this.#height)
){
throw new Error("Mismatched image resolution: images must be the size of the canvas.");
}
// Convert the paletted image into a 32-bit image on the canvas.
{
const palette = indexedImage.palette.dword;
const pixelBuffer32bit = new Uint32Array(this.#underlyingImageData.data.buffer);
for (let i = 0; i < indexedImage.data.length; i++) {
pixelBuffer32bit[i] = palette[indexedImage.data[i]];
}
}
this.#underlyingContext2D.putImageData(this.#underlyingImageData, 0, 0);
}
}
class HTMLPalettedCanvasElement extends HTMLCanvasElement {
#underlyingContext
#indexedRenderingContext
constructor() {
super();
}
static get observedAttributes() {
return ["width", "height"];
}
attributeChangedCallback(name, oldValue, newValue) {
if ((oldValue != newValue) && ["width", "height"].includes(name)) {
this.#underlyingContext = super.getContext("2d");
this.#indexedRenderingContext = new CanvasRenderingContextIndexed(this.#underlyingContext);
}
}
getContext(contextType = "2d") {
if (contextType !== "2d") {
throw new Error("This interface only supports the '2d' context type.");
}
return this.#indexedRenderingContext;
}
};
window.IndexedImageData = IndexedImageData;
window.CanvasRenderingContextIndexed = CanvasRenderingContextIndexed;
window.HTMLPalettedCanvasElement = HTMLPalettedCanvasElement;
customElements.define("paletted-canvas", HTMLPalettedCanvasElement, {extends: "canvas"});
}