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, };