Dot絵メーカー②ImgLoader
前回
次回
ver0.04:Resizer
セルの幅と個数を決定してからグリッドの幅を決定する方式と
グリッドの幅とセルの個数からセルの幅を決定する方式があるが、
後者の場合、Resizeする度にセルの幅が変化するため、一回キャンパスをクリアしないとグリッド線が上からぐちゃぐちゃに描画されることになる。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Grids</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
GUI追加した。
追加が適当なため、resizerでサイズ変えると各種GUIの描画がアップデートされずに置いてかれたりする。
main.js
class ContainerResizer {
constructor(target) {
this.w = 20;
this.h = 20;
this.target = target;
let y = (this.target.y + this.target.containerHeight - this.h);
let x = this.target.x + this.target.containerWidth;
// draggableのdiv要素を生成
const resizeDiv = document.createElement('div');
resizeDiv.textContent = "┛";
//CSSスタイルの設定
resizeDiv.style.color = "green";
resizeDiv.style.fontSize = "20px";
// draggableの初期位置をcanvasの上辺の位置に設定
//this.dragger.style.top = (this.targetElement.offsetTop - 20) + 'px';
resizeDiv.style.top = y + "px";
resizeDiv.style.left = x + "px";
resizeDiv.style.width = this.w + "px";
resizeDiv.style.height = this.h + "px";
resizeDiv.style.backgroundColor = "white";
resizeDiv.style.border = "1px solid black";
resizeDiv.style.position = "absolute";
resizeDiv.style.cursor = "grab";
document.body.appendChild(resizeDiv);
this.resizer = resizeDiv;
this.isDragging = false;
this.offsetX = 0;
this.offsetY = 0;
this.bindEvents();
}
update() {
//console.log("ok");
let y = (this.target.y + this.target.containerHeight - this.h);
let x = this.target.x + this.target.containerWidth;
this.resizer.style.top = y + "px";
this.resizer.style.left = x + "px";
}
bindEvents() {
this.resizer.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.endDrag.bind(this));
}
startDrag(e) {
this.isDragging = true;
// this.startX = e.clientX; // ドラッグ開始時のマウスのX座標を保存
// this.startY = e.clientY; // ドラッグ開始時のマウスのY座標を保存
// this.offsetX = e.clientX - this.resizer.getBoundingClientRect().left;
// this.offsetY = e.clientY - this.resizer.getBoundingClientRect().top;
this.resizer.style.cursor = 'grabbing';
}
drag(e) {
if (this.isDragging) {
// const dx = e.clientX - this.startX; // ドラッグ開始時からのX方向の差分を計算
// const dy = e.clientY - this.startY; // ドラッグ開始時からのY方向の差分を計算
// if (typeof this.target.resizeContainerSize === 'function') {
// //console.log("ok");
// const newWidth = this.target.containerWidth + dx;
// const newHeight = this.target.containerHeight + dy;
// this.target.resizeContainerSize(newWidth, newHeight);
// }
const min_size = 10;
const dx = e.clientX - this.target.x;
const dy = e.clientY - this.target.y;
if (dx < min_size || dy < min_size) {
dx = min_size;
dy = min_size;
}
if (typeof this.target.resizeContainerSize === 'function') {
this.target.resizeContainerSize(dx, dy);
}
this.update();
}
}
endDrag() {
this.isDragging = false;
this.resizer.style.cursor = 'grab';
this.update();
}
}
class GridDragger {
constructor(target) {
let w = 20;
let h = 20;
this.target = target;
let y = (this.target.y - h);
let x = this.target.x;
// draggableのdiv要素を生成
const draggableDiv = document.createElement('div');
draggableDiv.textContent = "◎";
//CSSスタイルの設定
draggableDiv.style.color = "black";
draggableDiv.style.fontSize = "20px";
// draggableの初期位置をcanvasの上辺の位置に設定
//this.dragger.style.top = (this.targetElement.offsetTop - 20) + 'px';
draggableDiv.style.top = y + "px";
draggableDiv.style.left = x + "px";
draggableDiv.style.width = w + "px";
draggableDiv.style.height = h + "px";
draggableDiv.style.backgroundColor = "white";
draggableDiv.style.border = "1px solid black";
draggableDiv.style.position = "absolute";
draggableDiv.style.cursor = "grab";
document.body.appendChild(draggableDiv);
this.dragger = draggableDiv;
this.isDragging = false;
this.offsetX = 0;
this.offsetY = 0;
this.bindEvents();
}
bindEvents() {
this.dragger.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.endDrag.bind(this));
}
startDrag(e) {
this.isDragging = true;
this.offsetX = e.clientX - this.dragger.getBoundingClientRect().left;
this.offsetY = e.clientY - this.dragger.getBoundingClientRect().top;
this.dragger.style.cursor = 'grabbing';
}
drag(e) {
if (this.isDragging) {
let x = e.clientX - this.offsetX;
let y = e.clientY - this.offsetY;
this.dragger.style.left = x + 'px';
this.dragger.style.top = y + 'px';
this.target.x = x;
this.target.y = (y + this.dragger.offsetHeight);
//console.log(this.target);
//console.log(this.target.updateSliderPositions);
// ColorPickerである場合、スライダーの位置も更新
if (typeof this.target.updateSliderPositions === 'function') {
//console.log("ok");
this.target.updateSliderPositions();
}
if (typeof this.target.updateGUIPositions === 'function') {
//console.log("ok");
this.target.updateGUIPositions();
}
//強引
resizerDrawingCanvas.update();
}
}//drag
endDrag() {
this.isDragging = false;
this.dragger.style.cursor = 'grab';
}
}
function createCanvas(x, y, width, height, id) {
const canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = width;//canvas要素はwidth,heightを持つので+"px"は不要
canvas.height = height;
// ここでCSSスタイルを設定
canvas.style.border = "1px solid black";
canvas.style.position = "absolute";
canvas.style.top = y + 'px';
canvas.style.left = x + 'px';
return canvas;
}
function createCanvasContainer(canvas) {
const container = document.createElement('div');
container.id = canvas.id + "Container";
//console.log(canvas.width);
container.style.width = canvas.width + "px";//div要素はwidth,heightを持たないので+"px"が必要
container.style.height = canvas.height + "px";
//console.log(container.width);
container.style.border = "1px solid black";
container.style.position = "absolute";
container.style.top = canvas.y + "px";
container.style.left = canvas.x + "px";
//canvas側は座標を(0,0)に
canvas.style.top = 0 + 'px';
canvas.style.left = 0 + 'px';
container.style.overflow = "scroll";
container.appendChild(canvas);
return container;
}
class Color {
constructor(r, g, b, a = 255) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
toHexString() {
return `#${this.r.toString(16).padStart(2, '0')}${this.g.toString(16).padStart(2, '0')}${this.b.toString(16).padStart(2, '0')}`;
}
toHSV() {
const r = this.r / 255;
const g = this.g / 255;
const b = this.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, v = max;
const diff = max - min;
s = max === 0 ? 0 : diff / max;
if (max === min) {
h = 0; // achromatic
} else {
switch (max) {
case r:
h = (g - b) / diff + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / diff + 2;
break;
case b:
h = (r - g) / diff + 4;
break;
}
h /= 6;
}
// hの値が0から1の間で返ってくるので、0から360の間で取得するために360を掛けます
h = Math.round(h * 360);
// s, vは0から1の間で返ってくるので、パーセンテージとして扱いたい場合は100を掛けます
s = Math.round(s * 100);
v = Math.round(v * 100);
return { h, s, v };
}
}
class Grid {
constructor(rowCount, colCount, canvasContainer) {
this.canvasContainer = canvasContainer;
//this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
//this.currentColor = new Color(0, 0, 0); // Default black color
this.rowCount = rowCount;
this.colCount = colCount;
this._cellWidth = this.containerWidth / this.colCount;
this._cellHeight = this.containerHeight / this.rowCount;
this.cells = [];
this.cellInitColor = new Color(255, 255, 255);
//Line
this.onGridLine = true;
this.lineColor = '#000000';
this.lineWidth = 1;
//Canvasのw,hからcellのsizeを決定する
this.fitCellToContainer = false;
//cellのwidthとheightをイコールに保つ
this.isSquareCell = true;
for (let i = 0; i < this.rowCount; i++) {
let row = [];
for (let j = 0; j < this.colCount; j++) {
row.push(new Color(255, 255, 255)); // Default white color
}
this.cells.push(row);
}
}//constructor
//他のGUIに接続
get currentColor() {
return CurrentInk.color;
}
set currentColor(color) {
CurrentInk.color = color;
CurrentInk.update();
}
get canvas() {
return this.canvasContainer.querySelector('canvas');
}
clearCanvas() {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
//this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
getAbsolutePositionX(element) {
let xPosition = 0;
while (element) {
xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
element = element.offsetParent;
}
return xPosition;
}
getAbsolutePositionY(element) {
let yPosition = 0;
while (element) {
yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
element = element.offsetParent;
}
return yPosition;
}
get absX() {
return this.getAbsolutePositionX(this.canvas);
}
get absY() {
return this.getAbsolutePositionY(this.canvas);
}
get x() {
//return this.canvas.offsetLeft;
return this.canvasContainer.offsetLeft;
}
set x(value) {
//this.canvas.style.left = value + 'px';
this.canvasContainer.style.left = value + 'px';
}
get y() {
//return this.canvas.offsetTop;
return this.canvasContainer.offsetTop;
}
set y(value) {
//this.canvas.style.top = value + 'px';
this.canvasContainer.style.top = value + 'px';
}
get canvasWidth() {
//canvasはwidthをもってる,divはもってない
return this.canvas.width;
}
get containerWidth() {
return this.canvasContainer.offsetWidth;
//const computedStyle = getComputedStyle(this.canvasContainer);
//return computedStyle.width;
}
get canvasHeight() {
return this.canvas.height;
}
get containerHeight() {
return this.canvasContainer.offsetHeight;
//const computedStyle = getComputedStyle(this.canvasContainer);
//return computedStyle.height;
}
get lowerEdge() {
const cw = this.containerWidth / this.colCount;
const ch = this.containerHeight / this.rowCount;
if (cw < ch) {
return cw;
} else {
return ch;
}
}
get cellWidth() {
//コンテナにグリッドをおさめる、かつセルは正方形
if (this.fitCellToContainer && this.isSquareCell) { return this.lowerEdge; }
//コンテナにグリッドをおさめる、セルは正方形とは限らない
if (this.fitCellToContainer) { return this.containerWidth / this.colCount; }
return Math.floor(this._cellWidth);
}
get cellHeight() {
//コンテナにグリッドをおさめる、かつセルは正方形
if (this.fitCellToContainer && this.isSquareCell) { return this.lowerEdge; }
//コンテナにグリッドをおさめる、セルは正方形とは限らない
if (this.fitCellToContainer) { return this.containerHeight / this.rowCount; }
return Math.floor(this._cellHeight);
}
get gridWidth() {
//コンテナ基準
if (this.fitCellToContainer) { return this.containerWidth; }
//セル基準
return this._cellWidth * this.colCount;
}
get gridHeight() {
//コンテナ基準
if (this.fitCellToContainer) { return this.containerHeight; }
//セル基準
return this._cellHeight * this.rowCount;
}
getCellAtCoordinates(x, y) {
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
return { row, col };
}
setColorAt(row, col, color) {
if (row < 0 || row >= this.cells.length) { return; }
if (col < 0 || col >= this.cells[row].length) { return; }
this.cells[row][col] = color;
this.drawCell(row, col, color);
}
getColorAt(row, col) {
if (row < 0 || row >= this.cells.length) { return; }
if (col < 0 || col >= this.cells[row].length) { return; }
return this.cells[row][col];
}
drawCell(row, col, color) {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
drawCells() {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
this.drawCell(row, col, this.cells[row][col]);
}
}
}
drawGridLine(sx, sy, ex, ey) {
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineTo(ex, ey);
this.ctx.stroke();
}
drawGridLines() {
const we = this.colCount * this.cellWidth;
const he = this.rowCount * this.cellHeight;
for (let row = 0; row < this.rowCount; row++) {
this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
for (let col = 0; col < this.colCount; col++) {
this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
}
}
this.drawGridLine(0, he, we, he);
this.drawGridLine(we, 0, we, he);
}
draw() {
this.clearCanvas();
this.drawCells();
if (this.onGridLine) {
this.drawGridLines();
}
}
refresh() {
this.clearCanvas();
this.drawCells();
this.drawGridLines()
}
// clearCells() {
// this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
// this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
// }
//Gridの要素数の変更
resizeElementCount(newRowCount, newColCount) {
const oldCells = this.cells;
const oldRowCount = this.rowCount;
const oldColCount = this.colCount;
// Update dimensions
this.rowCount = newRowCount;
this.colCount = newColCount;
//this.cellWidth = this.w / newColCount;
//this.cellHeight = this.h / newRowCount;
//console.log(this.gridWidth);
this.canvas.width = this.gridWidth;
this.canvas.height = this.gridHeight;
// Initialize new cells with default color
this.cells = [];
for (let i = 0; i < newRowCount; i++) {
let row = [];
for (let j = 0; j < newColCount; j++) {
row.push(new Color(255, 255, 255)); // Default white color
}
this.cells.push(row);
}
// Copy the content from the old cells to the new cells
for (let r = 0; r < oldRowCount; r++) {
for (let c = 0; c < oldColCount; c++) {
if (r < newRowCount && c < newColCount) {
this.cells[r][c] = oldCells[r][c];
}
}
}
// Redraw
this.draw();
}//resize
resizeContainerSize(newWidth, newHeight) {
this.canvasContainer.style.width = newWidth + "px";
this.canvasContainer.style.height = newHeight + "px";
}
}
class ColorPicker extends Grid {
constructor(rowCount, colCount, canvasContainer) {
super(rowCount, colCount, canvasContainer);
this.guiContainer = document.createElement('div');
this.guiContainer.style.position = "absolute";
document.body.appendChild(this.guiContainer);
const fr = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Red", "Red");
const fg = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Green", "Green");
const fb = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Blue", "Blue");
const fa = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 255, "Alpha", "Alpha");
this.sliderRed = fr.slider;
this.sliderGreen = fg.slider;
this.sliderBlue = fb.slider;
this.sliderAlpha = fa.slider;
// スライダーのイベントリスナーを追加
this.sliderRed.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderGreen.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderBlue.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderAlpha.addEventListener('input', this.updateColorPicker.bind(this));
this.canvas.addEventListener('click', function (e) {
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
this.currentColor = this.getColorAt(coords.row, coords.col);
}.bind(this));
// 初期位置の設定
//this.initGUIPosition();
this.updateSliderPositions();
}//constructor
appendFlexSliderWithLabel(parent, minValue, maxValue, initialValue, sliderId, labelText) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルとスライダーの間のスペース
// ラベルを作成
const labelElem = this.appendLabel(flexContainer, labelText);
// スライダーを作成
const slider = this.appendSlider(flexContainer, minValue, maxValue, initialValue, sliderId);
// ラベルのfor属性を設定して、関連付ける
labelElem.setAttribute("for", sliderId);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
//return flexContainer;
return { slider, label: labelElem, container: flexContainer };
}
appendSlider(parent, minValue, maxValue, initialValue, sliderId) {
const slider = document.createElement('input');
slider.type = "range";
slider.min = minValue;
slider.max = maxValue;
slider.value = initialValue;
if (sliderId) {
slider.id = sliderId;
}
//slider.style.position = "absolute";
parent.appendChild(slider);
return slider;
}
appendLabel(parent, label) {
const labelElem = document.createElement("label");
//labelElem.style.position = "absolute";
labelElem.innerText = label + ": ";
parent.appendChild(labelElem);
parent.appendChild(document.createElement("br"));
// ラベルにIDを追加
labelElem.id = label + "-label";
return labelElem;
}
// カラーピッカーキャンバスの色を変更する関数
updateColorPicker() {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
let redValue = Math.floor(255 * (row / this.rowCount));
let greenValue = Math.floor(255 * (col / this.colCount));
let color = new Color(redValue, greenValue, Number(this.sliderBlue.value), Number(this.sliderAlpha.value));
this.setColorAt(row, col, color);
}
}
if (this.onGridLine) {
this.drawGridLines();
}
}
// スライダーの位置を更新する関数
updateSliderPositions() {
this.guiContainer.style.left = this.x + "px";
this.guiContainer.style.top = this.y + this.containerHeight + 10 + "px";
}
}
class DrawingGrid extends Grid {
constructor(rowCount, colCount, canvasContainer) {
super(rowCount, colCount, canvasContainer);
this.isDragging = false;
// ドラッグ開始
this.canvas.addEventListener('mousedown', function (e) {
this.isDragging = true;
// クリックしたセルの色を変更
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
this.setColorAt(coords.row, coords.col, this.currentColor);
//this.drawGridLines();
}.bind(this));
//この実装だとfillRect連発で重くなる
// // ドラッグ中
// this.canvas.addEventListener('mousemove', function (e) {
// if (!this.isDragging) return;
// // ドラッグ中のセルの色を変更
// let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
// this.setColorAt(coords.row, coords.col, colorPickerGrid.currentColor);
// //this.drawGridLines();
// }.bind(this));
// メンバ変数として最後に変更されたセルの座標を追加
this.lastColoredCell = { row: null, col: null };
// ドラッグ中の処理
this.canvas.addEventListener('mousemove', function (e) {
if (!this.isDragging) return;
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
// 最後に変更されたセルと現在のセルが同じ場合はreturn
if (this.lastColoredCell.row === coords.row && this.lastColoredCell.col === coords.col) {
return;
}
this.setColorAt(coords.row, coords.col, colorPickerGrid.currentColor);
this.lastColoredCell = coords; // 更新したセルの座標を保存
}.bind(this));
// ドラッグ終了
document.addEventListener('mouseup', function () {
this.isDragging = false;
this.drawGridLines();
}.bind(this));
this.guiContainer = document.createElement('div');
this.guiContainer.style.position = "absolute";
document.body.appendChild(this.guiContainer);
const fri = this.appendFlexInputWithLabel(this.guiContainer, "Row", 1, 1028, rowCount);
const fci = this.appendFlexInputWithLabel(this.guiContainer, "Col", 1, 1028, colCount);
this.rowInput = fri.input;
this.colInput = fci.input;
const fcwi = this.appendFlexInputWithLabel(this.guiContainer, "Cell Width", 1, 30, this._cellWidth);
const fchi = this.appendFlexInputWithLabel(this.guiContainer, "Cell Height", 1, 30, this._cellHeight);
this.cellWidthInput = fcwi.input;
this.cellHeightInput = fchi.input;
// チェックボックス
const fitCellCheckbox = this.appendFlexCheckboxWithLabel(this.guiContainer, "Fit Cell To Container", this.fitCellToContainer);
fitCellCheckbox.checkbox.addEventListener('change', (e) => {
this.fitCellToContainer = e.target.checked;
});
const isSquareCellCheckbox = this.appendFlexCheckboxWithLabel(this.guiContainer, "Is Square Cell", this.isSquareCell);
isSquareCellCheckbox.checkbox.addEventListener('change', (e) => {
this.isSquareCell = e.target.checked;
});
// Add Done button
this.doneButton = this.appendButton("Done");
this.guiContainer.appendChild(this.doneButton);
// Add event listener for button
this.doneButton.addEventListener('click', () => {
const newCellWidth = Number(this.cellWidthInput.value);
const newCellHeight = Number(this.cellHeightInput.value);
this._cellWidth = newCellWidth;
this._cellHeight = newCellHeight;
const newRow = Number(this.rowInput.value);
const newCol = Number(this.colInput.value);
this.resizeElementCount(newRow, newCol);
this.refresh();
});
this.updateGUIPositions();
}//constructor
appendFlexInputWithLabel(parent, labelText, min, max, initialValue) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルと入力の間のスペース
// ラベルを作成
const labelElem = document.createElement("label");
labelElem.innerText = labelText + ": ";
labelElem.id = labelText + "-label"; // ラベルにIDを追加
flexContainer.appendChild(labelElem);
// 入力要素を作成
const input = document.createElement("input");
input.type = "number";
input.min = min;
input.max = max;
input.value = initialValue;
flexContainer.appendChild(input);
// ラベルのfor属性を設定して、関連付ける
labelElem.setAttribute("for", labelText);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
return { input, label: labelElem, container: flexContainer };
}
appendButton(text) {
const button = document.createElement("button");
// ラベルにIDを追加
button.id = text + "-label";
button.innerText = text;
//button.style.position = "absolute";
document.body.appendChild(button);
return button;
}
appendFlexCheckboxWithLabel(parent, labelText, initialChecked) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルと入力の間のスペース
// チェックボックスを作成
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = initialChecked;
checkbox.id = labelText + "-checkbox"; // チェックボックスにIDを追加
flexContainer.appendChild(checkbox);
// ラベルを作成
const labelElem = document.createElement("label");
labelElem.innerText = labelText;
labelElem.setAttribute("for", checkbox.id);
flexContainer.appendChild(labelElem);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
return { checkbox, label: labelElem, container: flexContainer };
}
updateGUIPositions() {
this.guiContainer.style.left = this.x + "px";
this.guiContainer.style.top = this.y + this.containerHeight + 10 + "px";
}//updateGUIPositions
}//DrawingGrid
class Ink {
constructor(color) {
this.color = color;
this.element = this.createDiv();
this.update();
document.body.appendChild(this.element);
}
createDiv() {
const div = document.createElement('div');
div.style.width = '200px';
div.style.height = '100px';
//div.style.backgroundColor = this.color.toHexString();
const hsv = this.color.toHSV();
div.innerHTML = `
RGB: ${this.color.r}, ${this.color.g}, ${this.color.b}<br>
HSV: ${hsv.h}, ${hsv.s}, ${hsv.v}<br>
HEX: ${this.color.toHexString()}
`;
// Drag and Drop処理
let isDragging = false;
let offsetX, offsetY;
div.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - div.getBoundingClientRect().left;
offsetY = e.clientY - div.getBoundingClientRect().top;
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', () => {
isDragging = false;
document.removeEventListener('mousemove', move);
});
});
function move(e) {
if (isDragging) {
div.style.left = (e.clientX - offsetX) + 'px';
div.style.top = (e.clientY - offsetY) + 'px';
}
}
div.style.position = 'absolute';
return div;
}//createDiv
update() {
this.element.style.backgroundColor = this.color.toHexString();
const hsv = this.color.toHSV();
this.element.innerHTML = `
RGB: ${this.color.r}, ${this.color.g}, ${this.color.b}<br>
HSV: ${hsv.h}, ${hsv.s}, ${hsv.v}<br>
HEX: ${this.color.toHexString()}
`;
}
}
const currentColor = new Color(0, 0, 0);
const CurrentInk = new Ink(currentColor);
// colorPickerCanvasのキャンバスを生成
const colorPickerCanvas = createCanvas(20, 20, 255, 255, "colorPickerCanvas");
const colorPickerCanvasContainer = createCanvasContainer(colorPickerCanvas);
document.body.appendChild(colorPickerCanvasContainer);
const colorPickerGrid = new ColorPicker(20, 20, colorPickerCanvasContainer);
colorPickerGrid.updateColorPicker();
// drawingCanvasのキャンバスを生成
const drawingCanvas = createCanvas(20, 300, 500, 500, "drawingCanvas");
const drawingCanvasContainer = createCanvasContainer(drawingCanvas);
document.body.appendChild(drawingCanvasContainer);
const drawingGrid = new DrawingGrid(10, 10, drawingCanvasContainer);
let draggerColorPicker = new GridDragger(colorPickerGrid);
let draggerDrawingCanvas = new GridDragger(drawingGrid);
let resizerDrawingCanvas = new ContainerResizer(drawingGrid);
colorPickerGrid.draw();
drawingGrid.draw();
ver0.05ファイル分割
こっからはローカルサーバーが必要。
メモ帳ではめんどくさくなる頃合い。
GUIも少し整理
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Grids</title>
</head>
<body>
<script type="module" src="main.js"></script>
</body>
</html>
main.js
import {Color, Ink } from "./color.js";
import {ContainerResizer, GridDragger}from "./gui.js";
import {Grid, ColorPicker, DrawingGrid}from "./grid.js";
function createCanvas(x, y, width, height, id) {
const canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = width;//canvas要素はwidth,heightを持つので+"px"は不要
canvas.height = height;
// ここでCSSスタイルを設定
canvas.style.border = "1px solid black";
canvas.style.position = "absolute";
canvas.style.top = y + 'px';
canvas.style.left = x + 'px';
return canvas;
}
function createCanvasContainer(canvas) {
const container = document.createElement('div');
container.id = canvas.id + "Container";
//console.log(canvas.width);
container.style.width = canvas.width + "px";//div要素はwidth,heightを持たないので+"px"が必要
container.style.height = canvas.height + "px";
//console.log(container.width);
container.style.border = "1px solid black";
container.style.position = "absolute";
container.style.top = canvas.y + "px";
container.style.left = canvas.x + "px";
//canvas側は座標を(0,0)に
canvas.style.top = 0 + 'px';
canvas.style.left = 0 + 'px';
container.style.overflow = "scroll";
container.appendChild(canvas);
return container;
}
class ImgLoader {
constructor(x,y,w,h){
// フレックスコンテナを生成
const flexContainer = document.createElement('div');
flexContainer.id = 'flexContainer';
flexContainer.style.display = "flex";
flexContainer.style.flexDirection = "column";
//flexContainer.style.alignItems = "center";
flexContainer.style.left = x + "px";
flexContainer.style.top = y + "px";
// 画像表示エリアのdivを生成
const scrollframe = document.createElement('div');
scrollframe.id = 'scrollframe';
scrollframe.style.overflow = "scroll";
scrollframe.style.width = w + "px";
scrollframe.style.maxHeight = h + "px";
const selectedImage = document.createElement('img');
selectedImage.id = 'selectedImage';
selectedImage.alt = "選択された画像がここに表示されます";
selectedImage.width = 300;
scrollframe.appendChild(selectedImage);
flexContainer.appendChild(scrollframe);
// ファイルインプットを生成
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = 'fileInput';
fileInput.accept = 'image/*';
flexContainer.appendChild(fileInput);
document.body.appendChild(flexContainer);
// ファイルが選択された時のイベントリスナーを追加
fileInput.addEventListener('change', function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
selectedImage.src = event.target.result;
}
reader.readAsDataURL(file);
}
});
}
}//ImgLoader
let imgLoader = new ImgLoader(0,0,300,300);
const currentColor = new Color(0, 0, 0);
export const CurrentInk = new Ink(currentColor);
// colorPickerCanvasのキャンバスを生成
const colorPickerCanvas = createCanvas(20, 20, 255, 255, "colorPickerCanvas");
const colorPickerCanvasContainer = createCanvasContainer(colorPickerCanvas);
document.body.appendChild(colorPickerCanvasContainer);
const colorPickerGrid = new ColorPicker(20, 20, colorPickerCanvasContainer);
colorPickerGrid.updateColorPicker();
// drawingCanvasのキャンバスを生成
const drawingCanvas = createCanvas(20, 300, 500, 500, "drawingCanvas");
const drawingCanvasContainer = createCanvasContainer(drawingCanvas);
document.body.appendChild(drawingCanvasContainer);
const drawingGrid = new DrawingGrid(10, 10, drawingCanvasContainer);
let draggerColorPicker = new GridDragger(colorPickerGrid);
let draggerDrawingCanvas = new GridDragger(drawingGrid);
let resizerDrawingCanvas = new ContainerResizer(drawingGrid);
drawingGrid.resizer = resizerDrawingCanvas;
colorPickerGrid.draw();
drawingGrid.draw();
color.js
class Color {
constructor(r, g, b, a = 255) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
toHexString() {
return `#${this.r.toString(16).padStart(2, '0')}${this.g.toString(16).padStart(2, '0')}${this.b.toString(16).padStart(2, '0')}`;
}
toHSV() {
const r = this.r / 255;
const g = this.g / 255;
const b = this.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, v = max;
const diff = max - min;
s = max === 0 ? 0 : diff / max;
if (max === min) {
h = 0; // achromatic
} else {
switch (max) {
case r:
h = (g - b) / diff + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / diff + 2;
break;
case b:
h = (r - g) / diff + 4;
break;
}
h /= 6;
}
// hの値が0から1の間で返ってくるので、0から360の間で取得するために360を掛けます
h = Math.round(h * 360);
// s, vは0から1の間で返ってくるので、パーセンテージとして扱いたい場合は100を掛けます
s = Math.round(s * 100);
v = Math.round(v * 100);
return { h, s, v };
}
}
class Ink {
constructor(color) {
this.color = color;
this.element = this.createDiv();
this.update();
document.body.appendChild(this.element);
}
createDiv() {
const div = document.createElement('div');
div.style.width = '200px';
div.style.height = '100px';
//div.style.backgroundColor = this.color.toHexString();
const hsv = this.color.toHSV();
div.innerHTML = `
RGB: ${this.color.r}, ${this.color.g}, ${this.color.b}<br>
HSV: ${hsv.h}, ${hsv.s}, ${hsv.v}<br>
HEX: ${this.color.toHexString()}
`;
// Drag and Drop処理
let isDragging = false;
let offsetX, offsetY;
div.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - div.getBoundingClientRect().left;
offsetY = e.clientY - div.getBoundingClientRect().top;
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', () => {
isDragging = false;
document.removeEventListener('mousemove', move);
});
});
function move(e) {
if (isDragging) {
div.style.left = (e.clientX - offsetX) + 'px';
div.style.top = (e.clientY - offsetY) + 'px';
}
}
div.style.position = 'absolute';
return div;
}//createDiv
update() {
this.element.style.backgroundColor = this.color.toHexString();
const hsv = this.color.toHSV();
this.element.innerHTML = `
RGB: ${this.color.r}, ${this.color.g}, ${this.color.b}<br>
HSV: ${hsv.h}, ${hsv.s}, ${hsv.v}<br>
HEX: ${this.color.toHexString()}
`;
}
}
export{Color, Ink};
gui.js
import { Color, Ink } from "./color.js";
import {Grid, ColorPicker, DrawingGrid}from "./grid.js";
class ContainerResizer {
constructor(target) {
this.w = 20;
this.h = 20;
this.target = target;
let y = (this.target.y + this.target.containerHeight - this.h);
let x = this.target.x + this.target.containerWidth;
// draggableのdiv要素を生成
const resizeDiv = document.createElement('div');
resizeDiv.textContent = "┛";
//CSSスタイルの設定
resizeDiv.style.color = "green";
resizeDiv.style.fontSize = "20px";
// draggableの初期位置をcanvasの上辺の位置に設定
//this.dragger.style.top = (this.targetElement.offsetTop - 20) + 'px';
resizeDiv.style.top = y + "px";
resizeDiv.style.left = x + "px";
resizeDiv.style.width = this.w + "px";
resizeDiv.style.height = this.h + "px";
resizeDiv.style.backgroundColor = "white";
resizeDiv.style.border = "1px solid black";
resizeDiv.style.position = "absolute";
resizeDiv.style.cursor = "grab";
document.body.appendChild(resizeDiv);
this.resizer = resizeDiv;
this.isDragging = false;
this.offsetX = 0;
this.offsetY = 0;
this.bindEvents();
}
//targetからの相対位置(初期設定)
get relationalX() {
return this.target.x + this.target.containerWidth;
}
get relationalY() {
return this.target.y + this.target.containerHeight - this.h;
}
update() {
this.resizer.style.left = this.relationalX + "px";
this.resizer.style.top = this.relationalY + "px";
}
bindEvents() {
this.resizer.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.endDrag.bind(this));
}
startDrag(e) {
this.isDragging = true;
this.resizer.style.cursor = 'grabbing';
}
drag(e) {
if (this.isDragging) {
const min_size = 10;
const dx = e.clientX - this.target.x;
const dy = e.clientY - this.target.y;
if (dx < min_size || dy < min_size) {
dx = min_size;
dy = min_size;
}
if (typeof this.target.resizeContainerSize === 'function') {
this.target.resizeContainerSize(dx, dy);
}
this.update();
}
}
endDrag() {
this.isDragging = false;
this.resizer.style.cursor = 'grab';
this.update();
//Fit Cell To Containerがtrueの時はウィンドウに追随してセルの大きさが変わる
//ifで場合分けしてもいいが
this.target.refresh();
}
}
class GridDragger {
constructor(target) {
this.w = 20;
this.h = 20;
this.target = target;
let y = this.target.y - this.h;
let x = this.target.x;
// draggableのdiv要素を生成
const draggableDiv = document.createElement('div');
draggableDiv.textContent = "◎";
//CSSスタイルの設定
draggableDiv.style.color = "black";
draggableDiv.style.fontSize = "20px";
// draggableの初期位置をcanvasの上辺の位置に設定
//this.dragger.style.top = (this.targetElement.offsetTop - 20) + 'px';
draggableDiv.style.top = y + "px";
draggableDiv.style.left = x + "px";
draggableDiv.style.width = this.w + "px";
draggableDiv.style.height = this.h + "px";
draggableDiv.style.backgroundColor = "white";
draggableDiv.style.border = "1px solid black";
draggableDiv.style.position = "absolute";
draggableDiv.style.cursor = "grab";
document.body.appendChild(draggableDiv);
this.dragger = draggableDiv;
this.isDragging = false;
this.offsetX = 0;
this.offsetY = 0;
this.bindEvents();
}
//targetからの相対位置(初期設定)
get relationalX() {
return this.target.x;
}
get relationalY() {
return this.target.y - this.h;
}
//InitPosでもある
update() {
this.dragger.style.left = this.relationalX + 'px';
this.dragger.style.top = this.relationalY + 'px';
}
bindEvents() {
this.dragger.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.endDrag.bind(this));
}
startDrag(e) {
this.isDragging = true;
this.offsetX = e.clientX - this.dragger.getBoundingClientRect().left;
this.offsetY = e.clientY - this.dragger.getBoundingClientRect().top;
this.dragger.style.cursor = 'grabbing';
}
drag(e) {
if (this.isDragging) {
let x = e.clientX - this.offsetX;
let y = e.clientY - this.offsetY;
this.dragger.style.left = x + 'px';
this.dragger.style.top = y + 'px';
this.target.x = x;
this.target.y = (y + this.dragger.offsetHeight);
if (typeof this.target.translatedUpdate === 'function') {
this.target.translatedUpdate();
}
}
}//drag
endDrag() {
this.isDragging = false;
this.dragger.style.cursor = 'grab';
}
}
export {ContainerResizer, GridDragger};
grid.js
import { Color, Ink } from "./color.js";
import { ContainerResizer, GridDragger } from "./gui.js";
import { CurrentInk } from "./main.js";
class Grid {
constructor(rowCount, colCount, canvasContainer) {
this.canvasContainer = canvasContainer;
//this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
//this.currentColor = new Color(0, 0, 0); // Default black color
this.rowCount = rowCount;
this.colCount = colCount;
this._cellWidth = this.containerWidth / this.colCount;
this._cellHeight = this.containerHeight / this.rowCount;
this.cells = [];
this.cellInitColor = new Color(255, 255, 255);
//Line
this.onGridLine = true;
this.lineColor = '#000000';
this.lineWidth = 1;
//Canvasのw,hからcellのsizeを決定する
this.fitCellToContainer = false;
//cellのwidthとheightをイコールに保つ
this.isSquareCell = true;
for (let i = 0; i < this.rowCount; i++) {
let row = [];
for (let j = 0; j < this.colCount; j++) {
row.push(new Color(255, 255, 255)); // Default white color
}
this.cells.push(row);
}
}//constructor
//他のGUIに接続
get currentColor() {
return CurrentInk.color;
}
set currentColor(color) {
CurrentInk.color = color;
CurrentInk.update();
}
get canvas() {
return this.canvasContainer.querySelector('canvas');
}
clearCanvas() {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
//this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
getAbsolutePositionX(element) {
let xPosition = 0;
while (element) {
xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
element = element.offsetParent;
}
return xPosition;
}
getAbsolutePositionY(element) {
let yPosition = 0;
while (element) {
yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
element = element.offsetParent;
}
return yPosition;
}
get absX() {
return this.getAbsolutePositionX(this.canvas);
}
get absY() {
return this.getAbsolutePositionY(this.canvas);
}
get x() {
//return this.canvas.offsetLeft;
return this.canvasContainer.offsetLeft;
}
set x(value) {
//this.canvas.style.left = value + 'px';
this.canvasContainer.style.left = value + 'px';
}
get y() {
//return this.canvas.offsetTop;
return this.canvasContainer.offsetTop;
}
set y(value) {
//this.canvas.style.top = value + 'px';
this.canvasContainer.style.top = value + 'px';
}
get canvasWidth() {
//canvasはwidthをもってる,divはもってない
return this.canvas.width;
}
get containerWidth() {
return this.canvasContainer.offsetWidth;
//const computedStyle = getComputedStyle(this.canvasContainer);
//return computedStyle.width;
}
get canvasHeight() {
return this.canvas.height;
}
get containerHeight() {
return this.canvasContainer.offsetHeight;
//const computedStyle = getComputedStyle(this.canvasContainer);
//return computedStyle.height;
}
get lowerEdge() {
const cw = this.containerWidth / this.colCount;
const ch = this.containerHeight / this.rowCount;
if (cw < ch) {
return cw;
} else {
return ch;
}
}
get cellWidth() {
//コンテナにグリッドをおさめる、かつセルは正方形
if (this.fitCellToContainer && this.isSquareCell) { return this.lowerEdge; }
//コンテナにグリッドをおさめる、セルは正方形とは限らない
if (this.fitCellToContainer) { return this.containerWidth / this.colCount; }
return Math.floor(this._cellWidth);
}
get cellHeight() {
//コンテナにグリッドをおさめる、かつセルは正方形
if (this.fitCellToContainer && this.isSquareCell) { return this.lowerEdge; }
//コンテナにグリッドをおさめる、セルは正方形とは限らない
if (this.fitCellToContainer) { return this.containerHeight / this.rowCount; }
return Math.floor(this._cellHeight);
}
get gridWidth() {
//コンテナ基準
if (this.fitCellToContainer) { return this.containerWidth; }
//セル基準
return this._cellWidth * this.colCount;
}
get gridHeight() {
//コンテナ基準
if (this.fitCellToContainer) { return this.containerHeight; }
//セル基準
return this._cellHeight * this.rowCount;
}
getCellAtCoordinates(x, y) {
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
return { row, col };
}
setColorAt(row, col, color) {
if (row < 0 || row >= this.cells.length) { return; }
if (col < 0 || col >= this.cells[row].length) { return; }
this.cells[row][col] = color;
this.drawCell(row, col, color);
}
getColorAt(row, col) {
if (row < 0 || row >= this.cells.length) { return; }
if (col < 0 || col >= this.cells[row].length) { return; }
return this.cells[row][col];
}
drawCell(row, col, color) {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
drawCells() {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
this.drawCell(row, col, this.cells[row][col]);
}
}
}
drawGridLine(sx, sy, ex, ey) {
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineTo(ex, ey);
this.ctx.stroke();
}
drawGridLines() {
const we = this.colCount * this.cellWidth;
const he = this.rowCount * this.cellHeight;
// 横のグリッドラインを描画
for (let row = 0; row <= this.rowCount; row++) { // 注意: ここを <= に変更して、最後の線も描画するようにします
this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
}
// 縦のグリッドラインを描画
for (let col = 0; col <= this.colCount; col++) { // 注意: こちらも <= に変更
this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
}
// for (let row = 0; row < this.rowCount; row++) {
// this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
// for (let col = 0; col < this.colCount; col++) {
// this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
// }
// }
// this.drawGridLine(0, he, we, he);
// this.drawGridLine(we, 0, we, he);
}
draw() {
this.clearCanvas();
this.drawCells();
if (this.onGridLine) {
this.drawGridLines();
}
}
refresh() {
this.clearCanvas();
this.drawCells();
this.drawGridLines()
}
// clearCells() {
// this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
// this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
// }
//Gridの要素数の変更
resizeElementCount(newRowCount, newColCount) {
const oldCells = this.cells;
const oldRowCount = this.rowCount;
const oldColCount = this.colCount;
// Update dimensions
this.rowCount = newRowCount;
this.colCount = newColCount;
//this.cellWidth = this.w / newColCount;
//this.cellHeight = this.h / newRowCount;
//console.log(this.gridWidth);
this.canvas.width = this.gridWidth;
this.canvas.height = this.gridHeight;
// Initialize new cells with default color
this.cells = [];
for (let i = 0; i < newRowCount; i++) {
let row = [];
for (let j = 0; j < newColCount; j++) {
row.push(new Color(255, 255, 255)); // Default white color
}
this.cells.push(row);
}
// Copy the content from the old cells to the new cells
for (let r = 0; r < oldRowCount; r++) {
for (let c = 0; c < oldColCount; c++) {
if (r < newRowCount && c < newColCount) {
this.cells[r][c] = oldCells[r][c];
}
}
}
// Redraw
this.draw();
}//resize
resizeContainerSize(newWidth, newHeight) {
this.canvasContainer.style.width = newWidth + "px";
this.canvasContainer.style.height = newHeight + "px";
}
}
class ColorPicker extends Grid {
constructor(rowCount, colCount, canvasContainer) {
super(rowCount, colCount, canvasContainer);
this.guiContainer = document.createElement('div');
this.guiContainer.style.position = "absolute";
document.body.appendChild(this.guiContainer);
const fr = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Red", "Red");
const fg = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Green", "Green");
const fb = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Blue", "Blue");
const fa = this.appendFlexSliderWithLabel(this.guiContainer, 0, 255, 255, "Alpha", "Alpha");
this.sliderRed = fr.slider;
this.sliderGreen = fg.slider;
this.sliderBlue = fb.slider;
this.sliderAlpha = fa.slider;
// スライダーのイベントリスナーを追加
this.sliderRed.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderGreen.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderBlue.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderAlpha.addEventListener('input', this.updateColorPicker.bind(this));
this.canvas.addEventListener('click', function (e) {
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
this.currentColor = this.getColorAt(coords.row, coords.col);
}.bind(this));
this.resizer = null;
// 初期位置の設定
//this.initGUIPosition();
this.updateSliderPositions();
}//constructor
appendFlexSliderWithLabel(parent, minValue, maxValue, initialValue, sliderId, labelText) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルとスライダーの間のスペース
// ラベルを作成
const labelElem = this.appendLabel(flexContainer, labelText);
// スライダーを作成
const slider = this.appendSlider(flexContainer, minValue, maxValue, initialValue, sliderId);
// ラベルのfor属性を設定して、関連付ける
labelElem.setAttribute("for", sliderId);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
//return flexContainer;
return { slider, label: labelElem, container: flexContainer };
}
appendSlider(parent, minValue, maxValue, initialValue, sliderId) {
const slider = document.createElement('input');
slider.type = "range";
slider.min = minValue;
slider.max = maxValue;
slider.value = initialValue;
if (sliderId) {
slider.id = sliderId;
}
//slider.style.position = "absolute";
parent.appendChild(slider);
return slider;
}
appendLabel(parent, label) {
const labelElem = document.createElement("label");
//labelElem.style.position = "absolute";
labelElem.innerText = label + ": ";
parent.appendChild(labelElem);
parent.appendChild(document.createElement("br"));
// ラベルにIDを追加
labelElem.id = label + "-label";
return labelElem;
}
// カラーピッカーキャンバスの色を変更する関数
updateColorPicker() {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
let redValue = Math.floor(255 * (row / this.rowCount));
let greenValue = Math.floor(255 * (col / this.colCount));
let color = new Color(redValue, greenValue, Number(this.sliderBlue.value), Number(this.sliderAlpha.value));
this.setColorAt(row, col, color);
}
}
if (this.onGridLine) {
this.drawGridLines();
}
}
// スライダーの位置を更新する関数
updateSliderPositions() {
this.guiContainer.style.left = this.x + "px";
this.guiContainer.style.top = this.y + this.containerHeight + 10 + "px";
}
//平行移動した時の更新
translatedUpdate() {
this.guiContainer.style.left = this.x + "px";
this.guiContainer.style.top = this.y + this.containerHeight + 10 + "px";
}
}
class DrawingGrid extends Grid {
constructor(rowCount, colCount, canvasContainer) {
super(rowCount, colCount, canvasContainer);
this.isDragging = false;
// ドラッグ開始
this.canvas.addEventListener('mousedown', function (e) {
this.isDragging = true;
// クリックしたセルの色を変更
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
this.setColorAt(coords.row, coords.col, this.currentColor);
//this.drawGridLines();
}.bind(this));
//この実装だとfillRect連発で重くなる
// // ドラッグ中
// this.canvas.addEventListener('mousemove', function (e) {
// if (!this.isDragging) return;
// // ドラッグ中のセルの色を変更
// let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
// this.setColorAt(coords.row, coords.col, colorPickerGrid.currentColor);
// //this.drawGridLines();
// }.bind(this));
// メンバ変数として最後に変更されたセルの座標を追加
this.lastColoredCell = { row: null, col: null };
// ドラッグ中の処理
this.canvas.addEventListener('mousemove', function (e) {
if (!this.isDragging) return;
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
// 最後に変更されたセルと現在のセルが同じ場合はreturn
if (this.lastColoredCell.row === coords.row && this.lastColoredCell.col === coords.col) {
return;
}
this.setColorAt(coords.row, coords.col, CurrentInk.color);
this.lastColoredCell = coords; // 更新したセルの座標を保存
}.bind(this));
// ドラッグ終了
document.addEventListener('mouseup', function () {
this.isDragging = false;
this.drawGridLines();
}.bind(this));
this.guiContainer = document.createElement('div');
this.guiContainer.style.position = "absolute";
document.body.appendChild(this.guiContainer);
const fri = this.appendFlexInputWithLabel(this.guiContainer, "Row", 1, 1028, rowCount);
const fci = this.appendFlexInputWithLabel(this.guiContainer, "Col", 1, 1028, colCount);
this.rowInput = fri.input;
this.colInput = fci.input;
const fcwi = this.appendFlexInputWithLabel(this.guiContainer, "Cell Width", 1, 30, this._cellWidth);
const fchi = this.appendFlexInputWithLabel(this.guiContainer, "Cell Height", 1, 30, this._cellHeight);
this.cellWidthInput = fcwi.input;
this.cellHeightInput = fchi.input;
// チェックボックス
const fitCellCheckbox = this.appendFlexCheckboxWithLabel(this.guiContainer, "Fit Cell To Container", this.fitCellToContainer);
fitCellCheckbox.checkbox.addEventListener('change', (e) => {
this.fitCellToContainer = e.target.checked;
});
const isSquareCellCheckbox = this.appendFlexCheckboxWithLabel(this.guiContainer, "Is Square Cell", this.isSquareCell);
isSquareCellCheckbox.checkbox.addEventListener('change', (e) => {
this.isSquareCell = e.target.checked;
});
// Add Done button
this.doneButton = this.appendButton("Done");
this.guiContainer.appendChild(this.doneButton);
// Add event listener for button
this.doneButton.addEventListener('click', () => {
const newCellWidth = Number(this.cellWidthInput.value);
const newCellHeight = Number(this.cellHeightInput.value);
this._cellWidth = newCellWidth;
this._cellHeight = newCellHeight;
const newRow = Number(this.rowInput.value);
const newCol = Number(this.colInput.value);
this.resizeElementCount(newRow, newCol);
this.refresh();
});
// Add Done button
this.redrawButton = this.appendButton("Redraw");
this.guiContainer.appendChild(this.redrawButton);
// Add event listener for button
this.redrawButton.addEventListener('click', () => {
this.refresh();
//this.clearCanvas();
});
this.updateGUIPositions();
}//constructor
appendFlexInputWithLabel(parent, labelText, min, max, initialValue) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルと入力の間のスペース
// ラベルを作成
const labelElem = document.createElement("label");
labelElem.innerText = labelText + ": ";
labelElem.id = labelText + "-label"; // ラベルにIDを追加
flexContainer.appendChild(labelElem);
// 入力要素を作成
const input = document.createElement("input");
input.type = "number";
input.min = min;
input.max = max;
input.value = initialValue;
flexContainer.appendChild(input);
// ラベルのfor属性を設定して、関連付ける
labelElem.setAttribute("for", labelText);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
return { input, label: labelElem, container: flexContainer };
}
appendButton(text) {
const button = document.createElement("button");
// ラベルにIDを追加
button.id = text + "-label";
button.innerText = text;
//button.style.position = "absolute";
document.body.appendChild(button);
return button;
}
appendFlexCheckboxWithLabel(parent, labelText, initialChecked) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルと入力の間のスペース
// チェックボックスを作成
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = initialChecked;
checkbox.id = labelText + "-checkbox"; // チェックボックスにIDを追加
flexContainer.appendChild(checkbox);
// ラベルを作成
const labelElem = document.createElement("label");
labelElem.innerText = labelText;
labelElem.setAttribute("for", checkbox.id);
flexContainer.appendChild(labelElem);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
return { checkbox, label: labelElem, container: flexContainer };
}
updateGUIPositions() {
this.guiContainer.style.left = this.x + "px";
this.guiContainer.style.top = this.y + this.containerHeight + 10 + "px";
}//updateGUIPositions
//平行移動した時の更新
translatedUpdate() {
this.guiContainer.style.left = this.x + "px";
this.guiContainer.style.top = this.y + this.containerHeight + 10 + "px";
this.resizer.update();
}
}//DrawingGrid
export { Grid, ColorPicker, DrawingGrid };
ver0.06ImgLoader
DOM要素の階層が適当だったのをそれなりに直す。
GridクラスはmainFrameというdiv要素を有し、キャンバス(scrollbar用のdivに包まれている)だのGUIだのはこの要素に放り込むことに。
mainFrameはflexboxなどにしても良い。
resizerがmainFrameを捕捉し損ねるバグが発生中。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Grids</title>
</head>
<body>
<script type="module" src="main.js"></script>
</body>
</html>
main.js
画像をロードして解像度を下げるなどし、パレット化。
import { Color, Ink } from "./color.js";
import { ContainerResizer, GridDragger } from "./gui.js";
import { Grid, ImgPicker, ColorPicker, DrawingGrid } from "./grid.js";
import * as div from './div.js';
class ImgLoader {
constructor(x, y, w, h) {
this.rowCount = 100;
this.colCount = 100;
// フレックスコンテナを生成
const flexContainer = document.createElement('div');
flexContainer.id = 'flexContainer';
flexContainer.style.position = "absolute";
flexContainer.style.display = "flex";
flexContainer.style.flexDirection = "column";
//flexContainer.style.alignItems = "center";
flexContainer.style.left = x + "px";
flexContainer.style.top = y + "px";
this.mainFrame = flexContainer;
// 画像表示エリアのdivを生成
const scrollframe = document.createElement('div');
scrollframe.id = 'scrollframe';
scrollframe.contentEditable = true; // 画像のペーストを可能にする。
scrollframe.style.overflow = "scroll";
scrollframe.style.width = w + "px";
scrollframe.style.maxHeight = h + "px";
const selectedImage = document.createElement('img');
selectedImage.id = 'selectedImage';
selectedImage.alt = "選択された画像がここに表示されます";
selectedImage.width = 300;
scrollframe.appendChild(selectedImage);
flexContainer.appendChild(scrollframe);
// canvasを生成
const canvas = document.createElement('canvas');
canvas.id = "ImgPicker";
canvas.width = w;//canvas要素はwidth,heightを持つので+"px"は不要
canvas.height = h;
// ここでCSSスタイルを設定
canvas.style.border = "1px solid black";
//canvas.style.position = "absolute";
// canvas.style.top = y + 'px';
// canvas.style.left = x + 'px';
const gridScrollframe = document.createElement('div');
gridScrollframe.id = 'gridScrollframe';
gridScrollframe.style.overflow = "scroll";
gridScrollframe.style.width = w + "px";
gridScrollframe.style.maxHeight = h + "px";
gridScrollframe.appendChild(canvas);
document.body.appendChild(gridScrollframe);
this.picker = new ImgPicker(this.rowCount, this.colCount, gridScrollframe);
flexContainer.appendChild(this.picker.canvasContainer);
flexContainer.appendChild(this.picker.guiContainer);
// ファイルインプットを生成
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = 'fileInput';
fileInput.accept = 'image/*';
flexContainer.appendChild(fileInput);
document.body.appendChild(flexContainer);
const self = this; // `this`をクラスのインスタンスとして保存
//クリップボードから画像張り付け
//ペーストイベントをscrollframeに追加
scrollframe.addEventListener('paste', async (event) => {
const clipboardData = event.clipboardData || window.clipboardData;
if (!clipboardData) {
return;
}
const items = clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith('image/')) {
const blob = item.getAsFile();
const reader = new FileReader();
reader.onload = async (event) => {
selectedImage.src = event.target.result;
try {
const cells = await self.processImageByGrid(selectedImage.src, self.rowCount, self.colCount, self.getAverageColor);
self.picker.cells = cells;
self.picker.draw();
} catch (error) {
console.error("Error processing the image:", error);
}
}
reader.readAsDataURL(blob);
}
}
});
// // ファイルが選択された時のイベントリスナーを追加
fileInput.addEventListener('change', async function () {
const file = this.files[0]; // この`this`はイベントリスナ内の`fileInput`を指します
if (file) {
const reader = new FileReader();
reader.onload = async (event) => {
selectedImage.src = event.target.result;
try {
const cells = await self.processImageByGrid(selectedImage.src, self.rowCount, self.colCount, self.getAverageColor);
self.picker.cells = cells;
self.picker.draw();
} catch (error) {
console.error("Error processing the image:", error);
}
};
reader.readAsDataURL(file);
}
});
}//constructor
get x() {
return this.mainFrame.offsetLeft;
}
set x(value) {
this.mainFrame.style.left = value + 'px';
}
get y() {
return this.mainFrame.offsetTop;
}
set y(value) {
this.mainFrame.style.top = value + 'px';
}
async processImageByGrid(imageUrl, rowCount, colCount, colorDeciderFunction) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function () {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
const cellWidth = image.width / colCount;
const cellHeight = image.height / rowCount;
const cells = [];
for (let i = 0; i < rowCount; i++) {
const row = [];
for (let j = 0; j < colCount; j++) {
const cellImageData = ctx.getImageData(j * cellWidth, i * cellHeight, cellWidth, cellHeight).data;
const representativeColor = colorDeciderFunction(cellImageData);
row.push(representativeColor);
}
cells.push(row);
}
resolve(cells);
};
image.onerror = function () {
reject(new Error('Failed to load image.'));
};
image.src = imageUrl;
});
}
// 例: 平均色を取得するcolorDeciderFunctionの実装
getAverageColor(imageData) {
let r = 0, g = 0, b = 0;
const pixelCount = imageData.length / 4;
for (let i = 0; i < imageData.length; i += 4) {
r += imageData[i];
g += imageData[i + 1];
b += imageData[i + 2];
}
return new Color(r / pixelCount, g / pixelCount, b / pixelCount);
}
}//ImgLoader
let imgLoader = new ImgLoader(0, 0, 300, 300);
let draggerImgLoader = new GridDragger(imgLoader);
const currentColor = new Color(0, 0, 0);
export const CurrentInk = new Ink(currentColor);
// colorPickerCanvasのキャンバスを生成
const colorPickerCanvas = div.CreateCanvas(20, 20, 255, 255, "colorPickerCanvas");
const colorPickerCanvasContainer = div.CreateCanvasContainer(colorPickerCanvas);
//document.body.appendChild(colorPickerCanvasContainer);
const colorPickerGrid = new ColorPicker(20, 20, colorPickerCanvasContainer);
colorPickerGrid.updateColorPicker();
// drawingCanvasのキャンバスを生成
const drawingCanvas = div.CreateCanvas(20, 300, 500, 500, "drawingCanvas");
const drawingCanvasContainer = div.CreateCanvasContainer(drawingCanvas);
//document.body.appendChild(drawingCanvasContainer);
const drawingGrid = new DrawingGrid(10, 10, drawingCanvasContainer);
let draggerColorPicker = new GridDragger(colorPickerGrid);
let draggerDrawingCanvas = new GridDragger(drawingGrid);
let resizerDrawingCanvas = new ContainerResizer(drawingGrid);
drawingGrid.resizer = resizerDrawingCanvas;
colorPickerGrid.draw();
drawingGrid.draw();
imgLoader.picker.draw();
color.js
class Color {
constructor(r, g, b, a = 255) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
toHexString() {
return `#${this.r.toString(16).padStart(2, '0')}${this.g.toString(16).padStart(2, '0')}${this.b.toString(16).padStart(2, '0')}`;
}
toHSV() {
const r = this.r / 255;
const g = this.g / 255;
const b = this.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, v = max;
const diff = max - min;
s = max === 0 ? 0 : diff / max;
if (max === min) {
h = 0; // achromatic
} else {
switch (max) {
case r:
h = (g - b) / diff + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / diff + 2;
break;
case b:
h = (r - g) / diff + 4;
break;
}
h /= 6;
}
// hの値が0から1の間で返ってくるので、0から360の間で取得するために360を掛けます
h = Math.round(h * 360);
// s, vは0から1の間で返ってくるので、パーセンテージとして扱いたい場合は100を掛けます
s = Math.round(s * 100);
v = Math.round(v * 100);
return { h, s, v };
}
}
class Ink {
constructor(color) {
this.color = color;
this.element = this.createDiv();
this.update();
document.body.appendChild(this.element);
}
createDiv() {
const div = document.createElement('div');
div.style.width = '200px';
div.style.height = '100px';
//div.style.backgroundColor = this.color.toHexString();
const hsv = this.color.toHSV();
div.innerHTML = `
RGB: ${this.color.r}, ${this.color.g}, ${this.color.b}<br>
HSV: ${hsv.h}, ${hsv.s}, ${hsv.v}<br>
HEX: ${this.color.toHexString()}
`;
// Drag and Drop処理
let isDragging = false;
let offsetX, offsetY;
div.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - div.getBoundingClientRect().left;
offsetY = e.clientY - div.getBoundingClientRect().top;
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', () => {
isDragging = false;
document.removeEventListener('mousemove', move);
});
});
function move(e) {
if (isDragging) {
div.style.left = (e.clientX - offsetX) + 'px';
div.style.top = (e.clientY - offsetY) + 'px';
}
}
div.style.position = 'absolute';
return div;
}//createDiv
update() {
this.element.style.backgroundColor = this.color.toHexString();
const hsv = this.color.toHSV();
this.element.innerHTML = `
RGB: ${this.color.r}, ${this.color.g}, ${this.color.b}<br>
HSV: ${hsv.h}, ${hsv.s}, ${hsv.v}<br>
HEX: ${this.color.toHexString()}
`;
}
}
export{Color, Ink};
grid.js
import { Color, Ink } from "./color.js";
import { ContainerResizer, GridDragger } from "./gui.js";
import { CurrentInk } from "./main.js";
import * as div from './div.js';
class Grid {
constructor(rowCount, colCount, canvasContainer) {
this.mainFrame = document.createElement('div');
this.mainFrame.style.position = "absolute";
document.body.appendChild(this.mainFrame);
this.mainFrame.appendChild(canvasContainer);
this.canvasContainer = canvasContainer;
//this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
//this.currentColor = new Color(0, 0, 0); // Default black color
this.rowCount = rowCount;
this.colCount = colCount;
this._cellWidth = this.containerWidth / this.colCount;
this._cellHeight = this.containerHeight / this.rowCount;
this.cells = [];
this.cellInitColor = new Color(255, 255, 255);
//Line
this.onGridLine = true;
this.lineColor = '#000000';
this.lineWidth = 1;
//Canvasのw,hからcellのsizeを決定する
this.fitCellToContainer = false;
//cellのwidthとheightをイコールに保つ
this.isSquareCell = true;
for (let i = 0; i < this.rowCount; i++) {
let row = [];
for (let j = 0; j < this.colCount; j++) {
row.push(new Color(255, 255, 255)); // Default white color
}
this.cells.push(row);
}
}//constructor
//他のGUIに接続
get currentColor() {
return CurrentInk.color;
}
set currentColor(color) {
CurrentInk.color = color;
CurrentInk.update();
}
get canvas() {
return this.canvasContainer.querySelector('canvas');
}
clearCanvas() {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
//this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
getAbsolutePositionX(element) {
let xPosition = 0;
while (element) {
xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
element = element.offsetParent;
}
return xPosition;
}
getAbsolutePositionY(element) {
let yPosition = 0;
while (element) {
yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
element = element.offsetParent;
}
return yPosition;
}
get absX() {
return this.getAbsolutePositionX(this.canvas);
}
get absY() {
return this.getAbsolutePositionY(this.canvas);
}
get x() {
//return this.canvas.offsetLeft;
//return this.canvasContainer.offsetLeft;
return this.mainFrame.offsetLeft;
}
set x(value) {
//this.canvas.style.left = value + 'px';
//this.canvasContainer.style.left = value + 'px';
this.mainFrame.style.left = value + 'px';
}
get y() {
//return this.canvas.offsetTop;
//return this.canvasContainer.offsetTop;
return this.mainFrame.offsetTop;
}
set y(value) {
//this.canvas.style.top = value + 'px';
//this.canvasContainer.style.top = value + 'px';
this.mainFrame.style.top = value + 'px';
}
get canvasWidth() {
//canvasはwidthをもってる,divはもってない
return this.canvas.width;
}
get containerWidth() {
return this.canvasContainer.offsetWidth;
//const computedStyle = getComputedStyle(this.canvasContainer);
//return computedStyle.width;
}
get canvasHeight() {
return this.canvas.height;
}
get containerHeight() {
return this.canvasContainer.offsetHeight;
//const computedStyle = getComputedStyle(this.canvasContainer);
//return computedStyle.height;
}
get lowerEdge() {
const cw = this.containerWidth / this.colCount;
const ch = this.containerHeight / this.rowCount;
if (cw < ch) {
return cw;
} else {
return ch;
}
}
get cellWidth() {
//コンテナにグリッドをおさめる、かつセルは正方形
if (this.fitCellToContainer && this.isSquareCell) { return this.lowerEdge; }
//コンテナにグリッドをおさめる、セルは正方形とは限らない
if (this.fitCellToContainer) { return this.containerWidth / this.colCount; }
return Math.floor(this._cellWidth);
}
get cellHeight() {
//コンテナにグリッドをおさめる、かつセルは正方形
if (this.fitCellToContainer && this.isSquareCell) { return this.lowerEdge; }
//コンテナにグリッドをおさめる、セルは正方形とは限らない
if (this.fitCellToContainer) { return this.containerHeight / this.rowCount; }
return Math.floor(this._cellHeight);
}
get gridWidth() {
//コンテナ基準
if (this.fitCellToContainer) { return this.containerWidth; }
//セル基準
return this._cellWidth * this.colCount;
}
get gridHeight() {
//コンテナ基準
if (this.fitCellToContainer) { return this.containerHeight; }
//セル基準
return this._cellHeight * this.rowCount;
}
getCellAtCoordinates(x, y) {
const col = Math.floor(x / this.cellWidth);
const row = Math.floor(y / this.cellHeight);
return { row, col };
}
setColorAt(row, col, color) {
if (row < 0 || row >= this.cells.length) { return; }
if (col < 0 || col >= this.cells[row].length) { return; }
this.cells[row][col] = color;
this.drawCell(row, col, color);
}
getColorAt(row, col) {
if (row < 0 || row >= this.cells.length) { return; }
if (col < 0 || col >= this.cells[row].length) { return; }
return this.cells[row][col];
}
drawCell(row, col, color) {
this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a / 255})`;
this.ctx.fillRect(col * this.cellWidth, row * this.cellHeight, this.cellWidth, this.cellHeight);
}
drawCells() {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
this.drawCell(row, col, this.cells[row][col]);
}
}
}
drawGridLine(sx, sy, ex, ey) {
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(sx, sy);
this.ctx.lineTo(ex, ey);
this.ctx.stroke();
}
drawGridLines() {
const we = this.colCount * this.cellWidth;
const he = this.rowCount * this.cellHeight;
// 横のグリッドラインを描画
for (let row = 0; row <= this.rowCount; row++) { // 注意: ここを <= に変更して、最後の線も描画するようにします
this.drawGridLine(0, row * this.cellHeight, we, row * this.cellHeight);
}
// 縦のグリッドラインを描画
for (let col = 0; col <= this.colCount; col++) { // 注意: こちらも <= に変更
this.drawGridLine(col * this.cellWidth, 0, col * this.cellWidth, he);
}
}
draw() {
this.clearCanvas();
this.drawCells();
if (this.onGridLine) {
this.drawGridLines();
}
}
refresh() {
this.clearCanvas();
this.drawCells();
this.drawGridLines()
}
// clearCells() {
// this.ctx.fillStyle = `rgba(${this.cellInitColor.r},${this.cellInitColor.g},${this.cellInitColor.b},${this.cellInitColor.a / 255})`;
// this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
// }
//Gridの要素数の変更
resizeElementCount(newRowCount, newColCount) {
const oldCells = this.cells;
const oldRowCount = this.rowCount;
const oldColCount = this.colCount;
// Update dimensions
this.rowCount = newRowCount;
this.colCount = newColCount;
//this.cellWidth = this.w / newColCount;
//this.cellHeight = this.h / newRowCount;
//console.log(this.gridWidth);
this.canvas.width = this.gridWidth;
this.canvas.height = this.gridHeight;
// Initialize new cells with default color
this.cells = [];
for (let i = 0; i < newRowCount; i++) {
let row = [];
for (let j = 0; j < newColCount; j++) {
row.push(new Color(255, 255, 255)); // Default white color
}
this.cells.push(row);
}
// Copy the content from the old cells to the new cells
for (let r = 0; r < oldRowCount; r++) {
for (let c = 0; c < oldColCount; c++) {
if (r < newRowCount && c < newColCount) {
this.cells[r][c] = oldCells[r][c];
}
}
}
// Redraw
this.draw();
}//resize
resizeContainerSize(newWidth, newHeight) {
this.canvasContainer.style.width = newWidth + "px";
this.canvasContainer.style.height = newHeight + "px";
}
}
//追随の平行移動などは全てflexboxに任せる
class ImgPicker extends Grid {
constructor(rowCount, colCount, canvasContainer) {
super(rowCount, colCount, canvasContainer);
this.guiContainer = document.createElement('div');
//this.guiContainer.style.position = "absolute";
//document.body.appendChild(this.guiContainer);
this.mainFrame.appendChild(this.guiContainer);
const fh = div.AppendFlexSliderWithLabel(this.guiContainer, 0, 360, 0, "Hue", "Hue");
const fs = div.AppendFlexSliderWithLabel(this.guiContainer, 0, 1, 0, "Saturation", "Saturation");
const fb = div.AppendFlexSliderWithLabel(this.guiContainer, 0, 1, 0, "Brightness", "Brightness");
this.sliderRed = fh.slider;
this.sliderGreen = fs.slider;
this.sliderBlue = fb.slider;
// スライダーのイベントリスナーを追加
this.sliderRed.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderGreen.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderBlue.addEventListener('input', this.updateColorPicker.bind(this));
this.canvas.addEventListener('click', function (e) {
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
this.currentColor = this.getColorAt(coords.row, coords.col);
}.bind(this));
this.resizer = null;
}//constructor
//色調を変更する
updateColorPicker() {
}
}//ImgPicker
class ColorPicker extends Grid {
constructor(rowCount, colCount, canvasContainer) {
super(rowCount, colCount, canvasContainer);
this.guiContainer = document.createElement('div');
this.guiContainer.style.position = "absolute";
//document.body.appendChild(this.guiContainer);
this.mainFrame.appendChild(this.guiContainer);
const fr = div.AppendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Red", "Red");
const fg = div.AppendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Green", "Green");
const fb = div.AppendFlexSliderWithLabel(this.guiContainer, 0, 255, 0, "Blue", "Blue");
const fa = div.AppendFlexSliderWithLabel(this.guiContainer, 0, 255, 255, "Alpha", "Alpha");
this.sliderRed = fr.slider;
this.sliderGreen = fg.slider;
this.sliderBlue = fb.slider;
this.sliderAlpha = fa.slider;
// スライダーのイベントリスナーを追加
this.sliderRed.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderGreen.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderBlue.addEventListener('input', this.updateColorPicker.bind(this));
this.sliderAlpha.addEventListener('input', this.updateColorPicker.bind(this));
this.canvas.addEventListener('click', function (e) {
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
this.currentColor = this.getColorAt(coords.row, coords.col);
}.bind(this));
this.resizer = null;
}//constructor
// カラーピッカーキャンバスの色を変更する関数
updateColorPicker() {
for (let row = 0; row < this.rowCount; row++) {
for (let col = 0; col < this.colCount; col++) {
let redValue = Math.floor(255 * (row / this.rowCount));
let greenValue = Math.floor(255 * (col / this.colCount));
let color = new Color(redValue, greenValue, Number(this.sliderBlue.value), Number(this.sliderAlpha.value));
this.setColorAt(row, col, color);
}
}
if (this.onGridLine) {
this.drawGridLines();
}
}
}
class DrawingGrid extends Grid {
constructor(rowCount, colCount, canvasContainer) {
super(rowCount, colCount, canvasContainer);
this.isDragging = false;
// ドラッグ開始
this.canvas.addEventListener('mousedown', function (e) {
this.isDragging = true;
// クリックしたセルの色を変更
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
this.setColorAt(coords.row, coords.col, this.currentColor);
//this.drawGridLines();
}.bind(this));
//この実装だとfillRect連発で重くなる
// // ドラッグ中
// this.canvas.addEventListener('mousemove', function (e) {
// if (!this.isDragging) return;
// // ドラッグ中のセルの色を変更
// let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
// this.setColorAt(coords.row, coords.col, colorPickerGrid.currentColor);
// //this.drawGridLines();
// }.bind(this));
// メンバ変数として最後に変更されたセルの座標を追加
this.lastColoredCell = { row: null, col: null };
// ドラッグ中の処理
this.canvas.addEventListener('mousemove', function (e) {
if (!this.isDragging) return;
let coords = this.getCellAtCoordinates(e.offsetX, e.offsetY);
// 最後に変更されたセルと現在のセルが同じ場合はreturn
if (this.lastColoredCell.row === coords.row && this.lastColoredCell.col === coords.col) {
return;
}
this.setColorAt(coords.row, coords.col, CurrentInk.color);
this.lastColoredCell = coords; // 更新したセルの座標を保存
}.bind(this));
// ドラッグ終了
document.addEventListener('mouseup', function () {
this.isDragging = false;
this.drawGridLines();
}.bind(this));
this.guiContainer = document.createElement('div');
this.guiContainer.style.position = "absolute";
document.body.appendChild(this.guiContainer);
this.mainFrame.appendChild(this.guiContainer);
const fri = div.AppendFlexInputWithLabel(this.guiContainer, "Row", 1, 1028, rowCount);
const fci = div.AppendFlexInputWithLabel(this.guiContainer, "Col", 1, 1028, colCount);
this.rowInput = fri.input;
this.colInput = fci.input;
const fcwi = div.AppendFlexInputWithLabel(this.guiContainer, "Cell Width", 1, 30, this._cellWidth);
const fchi = div.AppendFlexInputWithLabel(this.guiContainer, "Cell Height", 1, 30, this._cellHeight);
this.cellWidthInput = fcwi.input;
this.cellHeightInput = fchi.input;
// チェックボックス
const fitCellCheckbox = div.AppendFlexCheckboxWithLabel(this.guiContainer, "Fit Cell To Container", this.fitCellToContainer);
fitCellCheckbox.checkbox.addEventListener('change', (e) => {
this.fitCellToContainer = e.target.checked;
});
const isSquareCellCheckbox = div.AppendFlexCheckboxWithLabel(this.guiContainer, "Is Square Cell", this.isSquareCell);
isSquareCellCheckbox.checkbox.addEventListener('change', (e) => {
this.isSquareCell = e.target.checked;
});
// Add Done button
this.doneButton = div.AppendButton(this.guiContainer, "Done");
this.doneButton.addEventListener('click', () => {
const newCellWidth = Number(this.cellWidthInput.value);
const newCellHeight = Number(this.cellHeightInput.value);
this._cellWidth = newCellWidth;
this._cellHeight = newCellHeight;
const newRow = Number(this.rowInput.value);
const newCol = Number(this.colInput.value);
this.resizeElementCount(newRow, newCol);
this.refresh();
});
this.redrawButton = div.AppendButton(this.guiContainer, "Redraw");
this.redrawButton.addEventListener('click', () => {
this.refresh();
});
//this.updateGUIPositions();
}//constructor
//平行移動した時の更新
translatedUpdate() {
this.resizer.update();
}
}//DrawingGrid
export { Grid, ImgPicker, ColorPicker, DrawingGrid };
gui.js
import { Color, Ink } from "./color.js";
import {Grid, ColorPicker, DrawingGrid}from "./grid.js";
import * as div from './div.js';
class ContainerResizer {
constructor(target) {
this.w = 20;
this.h = 20;
this.target = target;
let y = this.target.y + this.target.containerHeight - this.h;
let x = this.target.x + this.target.containerWidth;
// draggableのdiv要素を生成
const resizeDiv = document.createElement('div');
resizeDiv.textContent = "┛";
//CSSスタイルの設定
resizeDiv.style.color = "green";
resizeDiv.style.fontSize = "20px";
// draggableの初期位置をcanvasの上辺の位置に設定
//this.dragger.style.top = (this.targetElement.offsetTop - 20) + 'px';
resizeDiv.style.top = y + "px";
resizeDiv.style.left = x + "px";
resizeDiv.style.width = this.w + "px";
resizeDiv.style.height = this.h + "px";
resizeDiv.style.backgroundColor = "white";
resizeDiv.style.border = "1px solid black";
resizeDiv.style.position = "absolute";
resizeDiv.style.cursor = "grab";
document.body.appendChild(resizeDiv);
this.resizer = resizeDiv;
this.isDragging = false;
this.offsetX = 0;
this.offsetY = 0;
this.bindEvents();
}
//targetからの相対位置(初期設定)
get relationalX() {
return this.target.x + this.target.containerWidth;
}
get relationalY() {
return this.target.y + this.target.containerHeight - this.h;
}
update() {
this.resizer.style.left = this.relationalX + "px";
this.resizer.style.top = this.relationalY + "px";
}
bindEvents() {
this.resizer.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.endDrag.bind(this));
}
startDrag(e) {
this.isDragging = true;
this.resizer.style.cursor = 'grabbing';
}
drag(e) {
if (this.isDragging) {
const min_size = 10;
const dx = e.clientX - this.target.x;
const dy = e.clientY - this.target.y;
if (dx < min_size || dy < min_size) {
dx = min_size;
dy = min_size;
}
if (typeof this.target.resizeContainerSize === 'function') {
this.target.resizeContainerSize(dx, dy);
}
this.update();
}
}
endDrag() {
this.isDragging = false;
this.resizer.style.cursor = 'grab';
this.update();
//Fit Cell To Containerがtrueの時はウィンドウに追随してセルの大きさが変わる
//ifで場合分けしてもいいが
this.target.refresh();
}
}
class GridDragger {
constructor(target) {
this.w = 20;
this.h = 20;
this.target = target;
let y = this.target.y - this.h;
let x = this.target.x;
// draggableのdiv要素を生成
const draggableDiv = document.createElement('div');
draggableDiv.textContent = "◎";
//CSSスタイルの設定
draggableDiv.style.color = "black";
draggableDiv.style.fontSize = "20px";
// draggableの初期位置をcanvasの上辺の位置に設定
//this.dragger.style.top = (this.targetElement.offsetTop - 20) + 'px';
draggableDiv.style.top = y + "px";
draggableDiv.style.left = x + "px";
draggableDiv.style.width = this.w + "px";
draggableDiv.style.height = this.h + "px";
draggableDiv.style.backgroundColor = "white";
draggableDiv.style.border = "1px solid black";
draggableDiv.style.position = "absolute";
draggableDiv.style.cursor = "grab";
document.body.appendChild(draggableDiv);
this.dragger = draggableDiv;
this.isDragging = false;
this.offsetX = 0;
this.offsetY = 0;
this.bindEvents();
}
//targetからの相対位置(初期設定)
get relationalX() {
return this.target.x;
}
get relationalY() {
return this.target.y - this.h;
}
//InitPosでもある
update() {
this.dragger.style.left = this.relationalX + 'px';
this.dragger.style.top = this.relationalY + 'px';
}
bindEvents() {
this.dragger.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.endDrag.bind(this));
}
startDrag(e) {
this.isDragging = true;
this.offsetX = e.clientX - this.dragger.getBoundingClientRect().left;
this.offsetY = e.clientY - this.dragger.getBoundingClientRect().top;
this.dragger.style.cursor = 'grabbing';
}
drag(e) {
if (this.isDragging) {
let x = e.clientX - this.offsetX;
let y = e.clientY - this.offsetY;
this.dragger.style.left = x + 'px';
this.dragger.style.top = y + 'px';
this.target.x = x;
this.target.y = (y + this.dragger.offsetHeight);
if (typeof this.target.translatedUpdate === 'function') {
this.target.translatedUpdate(
}
}
}//drag
endDrag() {
this.isDragging = false;
this.dragger.style.cursor = 'grab';
}
}
export {ContainerResizer, GridDragger};
div.js
export function CreateCanvas(x, y, width, height, id) {
const canvas = document.createElement('canvas');
canvas.id = id;
//canvas.style.position = "absolute";
canvas.style.border = "1px solid black";
canvas.style.top = y + 'px';
canvas.style.left = x + 'px';
canvas.width = width;//canvas要素はwidth,heightを持つので+"px"は不要
canvas.height = height;
return canvas;
}
export function CreateCanvasContainer(canvas) {
const container = document.createElement('div');
container.id = canvas.id + "Container";
//container.style.position = "absolute";
container.style.border = "1px solid black";
container.style.top = canvas.y + "px";
container.style.left = canvas.x + "px";
container.style.width = canvas.width + "px";//div要素はwidth,heightを持たないので+"px"が必要
container.style.height = canvas.height + "px";
//canvas側は座標を(0,0)に
canvas.style.top = 0 + 'px';
canvas.style.left = 0 + 'px';
container.style.overflow = "scroll";
container.appendChild(canvas);
//parent.appendChild(container);
return container;
}
export function AppendButton(parent, text) {
const button = document.createElement("button");
button.id = text + "-button";;
button.innerText = text;
//button.style.position = "absolute";
parent.appendChild(button);
return button;
}
export function AppendFlexInputWithLabel(parent, labelText, min, max, initialValue) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルと入力の間のスペース
// ラベルを作成
const labelElem = document.createElement("label");
labelElem.innerText = labelText + ": ";
labelElem.id = labelText + "-label"; // ラベルにIDを追加
flexContainer.appendChild(labelElem);
// 入力要素を作成
const input = document.createElement("input");
input.type = "number";
input.min = min;
input.max = max;
input.value = initialValue;
flexContainer.appendChild(input);
// ラベルのfor属性を設定して、関連付ける
labelElem.setAttribute("for", labelText);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
return { input, label: labelElem, container: flexContainer };
}
export function AppendFlexCheckboxWithLabel(parent, labelText, initialChecked) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルと入力の間のスペース
// チェックボックスを作成
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = initialChecked;
checkbox.id = labelText + "-checkbox"; // チェックボックスにIDを追加
flexContainer.appendChild(checkbox);
// ラベルを作成
const labelElem = document.createElement("label");
labelElem.innerText = labelText;
labelElem.setAttribute("for", checkbox.id);
flexContainer.appendChild(labelElem);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
return { checkbox, label: labelElem, container: flexContainer };
}
export function AppendFlexSliderWithLabel(parent, minValue, maxValue, initialValue, sliderId, labelText) {
// Flex containerを作成
const flexContainer = document.createElement('div');
flexContainer.style.display = 'flex';
flexContainer.style.alignItems = 'center';
flexContainer.style.gap = '10px'; // ラベルとスライダーの間のスペース
// ラベルを作成
const labelElem = this.AppendLabel(flexContainer, labelText);
// スライダーを作成
const slider = this.AppendSlider(flexContainer, minValue, maxValue, initialValue, sliderId);
// ラベルのfor属性を設定して、関連付ける
labelElem.setAttribute("for", sliderId);
// flex containerを親要素に追加
parent.appendChild(flexContainer);
//return flexContainer;
return { slider, label: labelElem, container: flexContainer };
}
export function AppendSlider(parent, minValue, maxValue, initialValue, sliderId) {
const slider = document.createElement('input');
slider.type = "range";
slider.min = minValue;
slider.max = maxValue;
slider.value = initialValue;
if (sliderId) {
slider.id = sliderId;
}
//slider.style.position = "absolute";
parent.appendChild(slider);
return slider;
}
export function AppendLabel(parent, label) {
const labelElem = document.createElement("label");
//labelElem.style.position = "absolute";
labelElem.innerText = label + ": ";
parent.appendChild(labelElem);
parent.appendChild(document.createElement("br"));
// ラベルにIDを追加
labelElem.id = label + "-label";
return labelElem;
}
この記事が気に入ったらサポートをしてみませんか?