You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
5.2 KiB

7 months ago
import { Color } from './color.js';
function disableImageSmoothing({ctx}) {
ctx.imageSmoothingEnabled = false;
if (ctx.imageSmoothingEnabled !== false) {
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
}
};
class Canvas extends HTMLCanvasElement { // {{{
constructor({height=600, width=800}) {
super();
this.ctx = this.getContext('2d');
this.temp = document.createElement('canvas');
this.temp.ctx = this.temp.getContext('2d');
this.resize({height, width});
}
getData() {
return this.ctx.getImageData(0, 0, this.width, this.height).data;
}
fromDataUrl({dataUrl}) {
const image = new Image();
image.src = dataUrl;
image.onload = () => {
this.width = image.width;
this.height = image.height;
this.style.width = `${image.width}px`;
this.style.height = `${image.height}px`;
this.ctx.drawImage(image, 0, 0);
disableImageSmoothing(this.ctx);
};
return this;
}
setHeight({height}) {
this.height = height;
this.style.height = `${height}px`;
disableImageSmoothing(this.ctx);
return this;
}
setWidth({width}) {
this.width = width;
this.style.width = `${width}px`;
disableImageSmoothing(this.ctx);
return this;
}
resize({height, width}) {
this.height = height;
this.width = width;
this.style.height = `${height}px`;
this.style.width = `${width}px`;
disableImageSmoothing(this.ctx);
return this;
}
clear() {
this.ctx.clearRect(0, 0, this.width, this.height);
return this;
}
fill({color}) {
this.ctx.fillStyle = color.toRgbaString();
this.ctx.fillRect(0, 0, this.width, this.height);
return this;
}
getColorAtPixel({x, y}) {
const data = this.getData();
const index = (y * this.width + x) * 4;
return new Color().fromRgbaArray({
r: data[index],
g: data[index + 1],
b: data[index + 2],
a: data[index + 3]
});
}
setColorAtPixel({x, y, color}) {
const data = this.getData();
const index = (y * this.width + x) * 4;
const rgbaArray = color.toRgbaArray();
data[index] = rgbaArray[0]; // red
data[index + 1] = rgbaArray[1]; // green
data[index + 2] = rgbaArray[2]; // blue
data[index + 3] = rgbaArray[3]; // alpha
return this;
}
drawLineWithPixels({x1, y1, x2, y2, color}) {
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
const sx = x1 < x2 ? 1 : -1;
const sy = y1 < y2 ? 1 : -1;
let err = dx - dy;
while (true) {
this.setColorAtPixel(x1, y1, color);
if (x1 === x2 && y1 === y2) break;
const e2 = err * 2;
if (e2 > -dy) { err -= dy; x1 += sx; }
if (e2 < dx) { err += dx; y1 += sy; }
}
return this;
}
drawEmptyRectangle({x, y, width, height, color}) {
this.ctx.fillStyle = color.toRgbaString();
for (let x1 = x; x1 < x + width; x1++) {
this.setPixel(x1, y, color.toRgbaArray());
}
for (let y1 = y; y1 < y + height; y1++) {
this.setPixel(x, y1, color.toRgbaString());
}
for (let x1 = x; x1 < x + width; x1++) {
this.setPixel(x1, y + height, color.toRgbaString());
}
for (let y1 = y; y1 < y + height; y1++) {
this.setPixel(x + width, y1, color.toRgbaString());
}
return this;
}
drawRectangle({x, y, width, height, color}) {
for (let x1 = x; x1 < x + width; x1++) {
for (let y1 = y; y1 < y + height; y1++) {
this.setPixel(x1, y1, color.toRgbaString());
}
}
return this;
}
drawEmptyCircle({x, y, diameter, color}) {
const radius = Math.floor(diameter / 2);
for (let y1 = -radius; y1 <= radius; y1++) {
for (let x1 = -radius; x1 <= radius; x1++) {
if (Math.abs(x1 * x1 + y1 * y1 - radius * radius) < 2) {
this.setPixel(x + x1, y + y1, color.toRgbaString());
}
}
}
return this;
}
drawCircle({x, y, diameter, color}) {
if (diameter === 1) {
this.drawPixel(x, y, color.toRgbaString());
}
this.ctx.fillStyle = color.toRgbaString();
let radius = Math.floor(diameter / 2);
let radiusSquared = radius * radius;
for (let y1 = -radius; y1 <= radius; y1++) {
for (let x1 = -radius; x1 <= radius; x1++) {
if ((x1 * x1 + y1 * y1) <= radiusSquared - radius) {
this.drawRectangle(x + x1, y + y1, 1, 1, color.toRgbaString());
}
}
}
return this;
}
drawLineWithCircles({x1, y1, x2, y2, diameter, color}) {
const dx = x2 - x1;
const dy = y2 - y1;
const distance = Math.sqrt(dx * dx + dy * dy);
const steps = Math.ceil(distance / (diameter / 3));
for (let i = 0; i <= steps; i++) {
const x = Math.round(x1 + (dx * i) / steps);
const y = Math.round(y1 + (dy * i) / steps);
this.drawCircle(x, y, diameter, color.toRgbaString());
}
return this;
}
floodFill({x, y, color}) {
const targetColor = this.getColorAtPixel(x, y);
const fillColor = color;
if (targetColor.match({color: fillColor})) return;
const stack = [{x, y}];
while (stack.length > 0) {
const {x, y} = stack.pop();
const currentColor = this.getColorAtPixel(x, y);
if (currentColor.match({color: targetColor})) {
this.setColorAtPixel({x, y, color: fillColor});
if (x > 0) stack.push({x: x - 1, y});
if (x < this.width - 1) stack.push({x: x + 1, y});
if (y > 0) stack.push({x, y: y - 1});
if (y < this.height - 1) stack.push({x, y: y + 1});
}
}
return this;
}
}
// }}}
export { Canvas, };