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.
205 lines
5.2 KiB
205 lines
5.2 KiB
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, };
|
|
|