Grid

初手DrawGrid

drawGrid : Gridの描画

<!DOCTYPE html>
<html lang="jp">
<body>

<canvas id="canvasGrid" width="500" height="500" style="border:1px solid #000000;"></canvas>

<script>
class CanvasGrid {
  constructor(canvasId, gridSize) {
    this.canvas = document.getElementById(canvasId);
    this.ctx = this.canvas.getContext('2d');
    this.gridSize = gridSize;
  }

  drawGrid() {
    const width = this.canvas.width;
    const height = this.canvas.height;

    this.ctx.beginPath();
    this.ctx.strokeStyle = '#aaa';

    // 縦線
    for(let x = 0; x <= width; x += this.gridSize) {
      this.ctx.moveTo(x, 0);
      this.ctx.lineTo(x, height);
    }

    // 横線
    for(let y = 0; y <= height; y += this.gridSize) {
      this.ctx.moveTo(0, y);
      this.ctx.lineTo(width, y);
    }

    this.ctx.stroke();
  }
}

const canvasGrid = new CanvasGrid('canvasGrid', 50);
canvasGrid.drawGrid();
</script>

</body>
</html>



grid-template-columnsプロパティ

<!DOCTYPE html>
<html lang="jp">

<head>
<title>Grid Classes</title>
<style>
  #divGrid {
    border: 1px solid #000000;
  }
</style>
</head>

<body>

<div id="divGrid"></div>
<script>
class DivGrid {
  constructor(parentId, rows, cols) {
    this.parent = document.getElementById(parentId);
    this.rows = rows;
    this.cols = cols;
  }

  createGrid() {
    this.parent.style.display = 'grid';
    this.parent.style.gridTemplateColumns = `repeat(${this.cols}, 1fr)`;
    this.parent.style.gridGap = '10px';

    for(let i = 0; i < this.rows * this.cols; i++) {
      const cell = document.createElement('div');
      cell.style.padding = '20px';
      cell.style.backgroundColor = '#f0f0f0';
      cell.textContent = i + 1;
      this.parent.appendChild(cell);
    }
  }
}

const divGrid = new DivGrid('divGrid', 3, 3); // 3x3グリッド
divGrid.createGrid();
</script>

</body>
</html>


CSSの grid-template-columns プロパティは、グリッドコンテナ内での列のサイズと数を定義するために使用されます。このプロパティによって、柔軟かつ強力なレイアウトシステムを提供し、複雑なデザインやレイアウトを容易に実装できます。

基本的な使用法
grid-template-columns プロパティは、列の幅を定義する値のリストを受け取ります。各値はスペースで区切られ、各列に対応します。

.container {
  display: grid;
  grid-template-columns: 100px 200px 100px;
}

上記の例では、3列のグリッドが作成され、最初の列と最後の列は幅100px、中央の列は幅200pxになります。

フレキシブルなサイズ指定
fr単位
: 利用可能なスペースを分割します。例えば、1fr, 2fr は利用可能なスペースを3つのパートに分割し、最初の列が1パート、二番目の列が2パートを占めます。

.container {
  display: grid;
  grid-template-columns: 1fr 2fr;
}

auto: 列のサイズがその内容に基づいて自動的に決定されます。

.container {
  display: grid;
  grid-template-columns: auto auto;
}

繰り返し
repeat()
: 繰り返しパターンを簡単に記述できます。例えば、repeat(3, 1fr) は 1fr 1fr 1fr と同じです。

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

ミニマムとマキシマムサイズ
minmax(min, max)
: 列の最小サイズと最大サイズを指定します。

.container {
  display: grid;
  grid-template-columns: minmax(100px, 1fr) 2fr;
}

レスポンシブなデザイン
メディアクエリと組み合わせることで、画面サイズに基づいて異なるグリッドレイアウトを適用することができます。

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

@media (max-width: 600px) {
  .container {
    grid-template-columns: 1fr;
  }
}

この例では、画面幅が600px以下になると、列が1つだけになります。

grid-template-columns プロパティを使用すると、ウェブページのレイアウトを精密に制御でき、ユーザーにとってより良いビジュアル体験を提供することが可能になります。

Index座標

CanvasGridクラスにColorオブジェクトを要素とする配列を追加してください。この配列の要素数はグリッドのセルの総数と一致します。

また、グリッド左上を原点とするintグリッド座標(col, row)を入力として、対応するColor配列のindexに変換する関数を作ってください。
次にマウス座標を入力にとり、それがグリッド上である場合、クリック位置に対応するセルのintグリッド座標を返す関数getGridIndex(mouseX,mouseY)を作ってください。
次にマウス座標を入力にとり、それがグリッド上である場合、クリック位置に対応するセルのintグリッド座標を取得し、それをColor配列のindexに変換し、そのindex位置に色をセットするsetGridIndex(mouseX, mouseY, color)関数を作ってください。

Color配列

this.colors = new Array(this.cols * this.rows).fill(null);

グリッド左上を原点とするintグリッド座標(col, row)を入力として、対応するColor配列のindexに変換する関数

  gridCoordToIndex(col, row) {
    return row * this.cols + col;
  }

  indexToGridCoord(index) {
    return {
      col: index % this.cols,
      row: Math.floor(index / this.cols)
    };
  }

getGridIndex

  getGridIndex(mouseX, mouseY) {
    const col = Math.floor(mouseX / this.gridSize);
    const row = Math.floor(mouseY / this.gridSize);
    if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) {
      return { col, row };
    }
    return null;
  }

setGridIndex

  setGridIndex(mouseX, mouseY, color) {
    const gridIndex = this.getGridIndex(mouseX, mouseY);
    if (gridIndex) {
      const index = this.gridCoordToIndex(gridIndex.col, gridIndex.row);
      this.colors[index] = color;
      this.drawGrid();
    }
  }

ここまで

<!DOCTYPE html>
<html lang="jp">
<body>

<canvas id="canvasGrid" width="500" height="500" style="border:1px solid #000000;"></canvas>

<script>
class Color {
  constructor(r, g, b) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  toString() {
    return `rgb(${this.r},${this.g},${this.b})`;
  }
}

class CanvasGrid {
  constructor(canvasId, gridSize) {
    this.canvas = document.getElementById(canvasId);
    this.ctx = this.canvas.getContext('2d');
    this.gridSize = gridSize;
    this.cols = Math.floor(this.canvas.width / this.gridSize);
    this.rows = Math.floor(this.canvas.height / this.gridSize);
    this.colors = new Array(this.cols * this.rows).fill(null);

    this.canvas.addEventListener('click', this.handleClick.bind(this));
  }

  drawGrid() {
    const width = this.canvas.width;
    const height = this.canvas.height;

    this.ctx.beginPath();
    this.ctx.strokeStyle = '#aaa';

    // 縦線
    for(let x = 0; x <= width; x += this.gridSize) {
      this.ctx.moveTo(x, 0);
      this.ctx.lineTo(x, height);
    }

    // 横線
    for(let y = 0; y <= height; y += this.gridSize) {
      this.ctx.moveTo(0, y);
      this.ctx.lineTo(width, y);
    }

    this.ctx.stroke();

    // セルの色を描画
    for (let i = 0; i < this.colors.length; i++) {
      if (this.colors[i]) {
        const {col, row} = this.indexToGridCoord(i);
        this.ctx.fillStyle = this.colors[i].toString();
        this.ctx.fillRect(col * this.gridSize, row * this.gridSize, this.gridSize, this.gridSize);
      }
    }
  }

  gridCoordToIndex(col, row) {
    return row * this.cols + col;
  }

  indexToGridCoord(index) {
    return {
      col: index % this.cols,
      row: Math.floor(index / this.cols)
    };
  }

  getGridIndex(mouseX, mouseY) {
    const col = Math.floor(mouseX / this.gridSize);
    const row = Math.floor(mouseY / this.gridSize);
    if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) {
      return { col, row };
    }
    return null;
  }

  setGridIndex(mouseX, mouseY, color) {
    const gridIndex = this.getGridIndex(mouseX, mouseY);
    if (gridIndex) {
      const index = this.gridCoordToIndex(gridIndex.col, gridIndex.row);
      this.colors[index] = color;
      this.drawGrid();
    }
  }

  handleClick(event) {
    const rect = this.canvas.getBoundingClientRect();
    const mouseX = event.clientX - rect.left;
    const mouseY = event.clientY - rect.top;
    const randomColor = new Color(
      Math.floor(Math.random() * 256),
      Math.floor(Math.random() * 256),
      Math.floor(Math.random() * 256)
    );
    this.setGridIndex(mouseX, mouseY, randomColor);
  }
}

const canvasGrid = new CanvasGrid('canvasGrid', 50);
canvasGrid.drawGrid();
</script>

</body>
</html>

位置抽象Index座標

setStyle(element, styles) : DOMに対するスタイルの適用
getGridIndex(x, y) : インデックス座標を返す。入力(x, y)が結構微妙。基本的にはGridDOMの左上を(0, 0)と考えたマウス座標。

コード上では
const rect = canvasGrid.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
となってるが、
event.offsetX
event.offsetY
を用いた方が良いように思われる。

Clientの差とOffsetは多くのケースで一致するが、異なる場合や思ってもない値が返ってくる時もある。
親子関係がある場合で、子や親にイベントがとられてる場合や表示位置がabsoluteだったりなんだったりの場合。
ボーダーやパディングがある場合。

ボーダーやパディングがある場合:
offsetXは、要素のコンテンツ領域内の相対的な位置を返します。
一方、getBoundingClientRect().leftは、要素のボーダーの外側の位置を返します。したがって、要素にボーダーやパディングがある場合、offsetXとgetBoundingClientRect().left - event.clientXの値は異なる可能性があります。

つまり親子関係がぐちゃぐちゃの場合はClient系を用い、ボーダーやパディングでぐちゃぐちゃの場合はOffsetが有利と思われる。

クリックしたらコンソールにIndex座標を出力

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic Canvas Grid</title>
</head>
<body>

<script src="main.js"></script>
</body>
</html>

main.js

class CanvasGrid {
  constructor(canvasId, containerId, cellSize, rowCount, colCount) {
    this.container = document.createElement('div');
    this.container.id = containerId;
    this.setStyle(this.container, {
      width: '400px', // コンテナの幅
      height: '400px', // コンテナの高さ
      overflow: 'auto', // 必要に応じてスクロールバーを表示
      border: '1px solid black' // 枠線
    });

    this.canvas = document.createElement('canvas');
    this.canvas.id = canvasId;
    this.setStyle(this.canvas, {
      display: 'block' // canvasの余白を除去
    });

    this.ctx = this.canvas.getContext('2d');
    this.cellSize = cellSize;
    this.rowCount = rowCount;
    this.colCount = colCount;

    // canvasのサイズをグリッドに合わせて設定
    this.canvas.width = this.cellSize * this.colCount;
    this.canvas.height = this.cellSize * this.rowCount;

    // コンテナにcanvasを追加
    this.container.appendChild(this.canvas);
    document.body.appendChild(this.container);

    this.drawGrid();
  }

  setStyle(element, styles) {
    for (const property in styles) {
      element.style[property] = styles[property];
    }
  }

  drawGrid() {
    for (let row = 0; row < this.rowCount; row++) {
      for (let col = 0; col < this.colCount; col++) {
        this.ctx.strokeRect(col * this.cellSize, row * this.cellSize, this.cellSize, this.cellSize);
      }
    }
  }

  getGridIndex(x, y) {
    const col = Math.floor(x / this.cellSize);
    const row = Math.floor(y / this.cellSize);
    return col >= 0 && col < this.colCount && row >= 0 && row < this.rowCount ? { row, col } : null;
  }
}

document.addEventListener('DOMContentLoaded', function() {
  const canvasGrid = new CanvasGrid('canvasGrid', 'canvasContainer', 50, 20, 20);
  
  // テスト用: クリックした位置のインデックスをコンソールに出力
  canvasGrid.canvas.addEventListener('click', function(event) {
    const rect = canvasGrid.canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    const index = canvasGrid.getGridIndex(x, y);
    console.log(index);
  });   
});

色データ(色オブジェクトの配列)と、グリッドの対応

CanvasGridクラスにカラーオブジェクト{a,r,g,b}の配列を所持させ、 Draw時は対応する色でDrawRectしてください。配列の色はグリッドの左上から右に向かって詰めていき、colCount-1を右端として折り返します(キャリッジリターンのように)。また、デフォルトカラーをクラスに所持させ、配列に対応する色オブジェクトがない場合はその色で塗ります。初期値は白です。 グリッドのセルの数より色配列の要素の数が多い場合、余った分は無視されます。

index座標と色オブジェクトを入力にとり、対応するカラー配列の要素を入力に置き換える関数を作成してください。つまり内部でindex座標を配列のインデックスに変換する必要があります。インデックスが不正なら無視します。 また、マウスがcanvasをクリックした時、マウス座標をindex座標に変換して上記の関数を実行する関数も作ってください。この関数は仮引数に色を受ける必要があります。
また、インデックスを入力にとり、対応するセルの色オブジェクト(つまりそれに対応する色配列の要素)を返却する関数を作ってください。
また、マウスがcanvasをクリックした時、マウス座標をインデックス座標に変換し、対応するセルの色オブジェクト(つまりそれに対応する色配列の要素)を返却する関数を作ってください。

  1. 特定のインデックス座標に色を設定する関数 - この関数は、インデックス座標と色オブジェクトを受け取り、内部のカラー配列の対応する要素を更新します。

  2. マウスクリックによりセルの色を更新する関数 - マウスの位置からインデックス座標を求め、特定の色でそのセルを更新します。

  3. 特定のインデックスのセルの色を取得する関数 - 指定されたインデックス座標のセルの色オブジェクトを返却します。

  4. マウスクリックによりセルの色を取得する関数 - マウスの位置からインデックス座標を求め、そのセルの色オブジェクトを返却します。

で、この要件だと配列を初期化しないので、要素数2の配列にindex5でアクセスするような変な感じになる。なので初期化処理の追加。

グリッドのセルの数と同数の配列の要素を指定された色オブジェクトで充填する関数作ってください。

main.js

class CanvasGrid {
  constructor(canvasId, containerId, cellSize, rowCount, colCount, colors = []) {
    this.container = document.createElement('div');
    this.container.id = containerId;
    this.setStyle(this.container, {
      width: '400px', // コンテナの幅
      height: '400px', // コンテナの高さ
      overflow: 'auto', // 必要に応じてスクロールバーを表示
      border: '1px solid black' // 枠線
    });

    this.canvas = document.createElement('canvas');
    this.canvas.id = canvasId;
    this.setStyle(this.canvas, {
      display: 'block' // canvasの余白を除去
    });

    this.ctx = this.canvas.getContext('2d');
    this.cellSize = cellSize;
    this.rowCount = rowCount;
    this.colCount = colCount;
    this.colors = colors;
    this.defaultColor = {a: 255, r: 255, g: 255, b: 255}; // デフォルトは白

    this.InitColors();

    // canvasのサイズをグリッドに合わせて設定
    this.canvas.width = this.cellSize * this.colCount;
    this.canvas.height = this.cellSize * this.rowCount;

    // コンテナにcanvasを追加
    this.container.appendChild(this.canvas);
    document.body.appendChild(this.container);    

    this.drawGrid();
  }
  
  // 全セルを指定された色で充填する関数
  fillColors(color) {
    const totalCells = this.rowCount * this.colCount;
    this.colors = Array(totalCells).fill(color);
    this.drawGrid(); // グリッドを再描画して変更を反映
  }
  
  InitColors(){
    this.fillColors(this.defaultColor);
  }  

  setStyle(element, styles) {
    for (const property in styles) {
      element.style[property] = styles[property];
    }
  }

  drawGrid() {
    let colorIndex = 0;
    for (let row = 0; row < this.rowCount; row++) {
      for (let col = 0; col < this.colCount; col++) {
        const color = this.colors[colorIndex] || this.defaultColor;
        this.ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a / 255})`;
        this.ctx.fillRect(col * this.cellSize, row * this.cellSize, this.cellSize, this.cellSize);
        this.ctx.strokeRect(col * this.cellSize, row * this.cellSize, this.cellSize, this.cellSize);
        if (colorIndex < this.colors.length - 1) {
          colorIndex++;
        }
      }
    }
  }

  getGridIndex(x, y) {
    const col = Math.floor(x / this.cellSize);
    const row = Math.floor(y / this.cellSize);
    return col >= 0 && col < this.colCount && row >= 0 && row < this.rowCount ? { row, col } : null;
  }
  
  // セルの色を更新する関数
  setColorAtIndex(row, col, color) {
    const index = row * this.colCount + col;
    this.colors[index] = color;
    this.drawGrid(); // グリッドを再描画して変更を反映
          
    //if (index >= 0 && index < this.rowCount * this.colCount) {
    //  this.colors[index] = color;
    //  this.drawGrid(); // グリッドを再描画して変更を反映
    //}
  }
  
  // マウスクリックでセルの色を更新する関数
  updateCellColorOnClick(color) {
    this.canvas.addEventListener('click', (event) => {
      const rect = this.canvas.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const y = event.clientY - rect.top;
      const { row, col } = this.getGridIndex(x, y);
      this.setColorAtIndex(row, col, color);
    });
  }

  // 特定のインデックスのセルの色を取得する関数
  getColorAtIndex(row, col) {
    const index = row * this.colCount + col;
    if (index >= 0 && index < this.colors.length) {
      return this.colors[index];
    }
    return this.defaultColor; // 色が設定されていなければデフォルトの色を返す
  }
  
  // マウスクリックでセルの色を取得する関数
  getCellColorOnClick() {
    this.canvas.addEventListener('click', (event) => {
      const rect = this.canvas.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const y = event.clientY - rect.top;
      const { row, col } = this.getGridIndex(x, y);
      const color = this.getColorAtIndex(row, col);
      console.log(color); // 色情報をコンソールに出力(または他の処理)
    });
  }  
  
}

document.addEventListener('DOMContentLoaded', function() {
  const canvasGrid = new CanvasGrid('canvasGrid', 'canvasContainer', 50, 20, 20);
  
  // 色の更新例
  canvasGrid.updateCellColorOnClick({a: 255, r: 255, g: 0, b: 0}); // 赤色で更新

  // セルの色を取得する例
  canvasGrid.getCellColorOnClick(); 
});

 


Cell基準のWidth, Hieght。Grid基準のWidth, Height

両対応

また、配列は二重配列
また、Google Colab実行用に%%htmlが入れてある。

%%html
<div id="gridContainer" style="width: 500px; height: 500px;"></div>
<script>
class Color {
  constructor(r, g, b, a = 255) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }
}

class Grid {
  constructor(rowCount, colCount, width, height) {
    this.canvas = document.createElement('canvas');
    this.canvas.width = width;
    this.canvas.height = height;
    this.ctx = this.canvas.getContext("2d");
    this.currentColor = new Color(0, 0, 0); // Default black color

    this.rowCount = rowCount;
    this.colCount = colCount;
    this._cellWidth = width / this.colCount;
    this._cellHeight = height / this.rowCount;

    this.cells = [];
    this.cellInitColor = new Color(255, 255, 255);

    //Line
    this.onGridLine = true;
    this.lineColor = '#000000';
    this.lineWidth = 1;

    //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);
    }
  }

  get width() {
    return this.canvas.width;
  }

  get height() {
    return this.canvas.height;
  }

  get lowerEdge() {
    const cw = this.width / this.colCount;
    const ch = this.height / this.rowCount;
    return Math.min(cw, ch);
  }

  get cellWidth() {
    return this.isSquareCell ? this.lowerEdge : this._cellWidth;
  }

  get cellHeight() {
    return this.isSquareCell ? this.lowerEdge : this._cellHeight;
  }

  get gridWidth() {
    return this.cellWidth * this.colCount;
  }

  get gridHeight() {
    return this.cellHeight * this.rowCount;
  }

  getGridIndex(x, y) {
    const col = Math.floor(x / this.cellWidth);
    const row = Math.floor(y / this.cellHeight);
    return { row, col };
  }

  setColorAtIndex(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);
  }

  getColorAtIndex(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.clearCells();
    this.drawCells();
    if (this.onGridLine) {
      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.width, this.height);
  }

  resize(newRowCount, newColCount, newWidth, newHeight) {
    const oldCells = this.cells;
    const oldRowCount = this.rowCount;
    const oldColCount = this.colCount;

    // Update dimensions
    this.rowCount = newRowCount;
    this.colCount = newColCount;
    this.canvas.width = newWidth;
    this.canvas.height = newHeight;
    this._cellWidth = newWidth / newColCount;
    this._cellHeight = newHeight / newRowCount;

    // 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();
  }

  appendTo(container) {
    container.appendChild(this.canvas);
  }
}

// Gridの作成と表示
const gridContainer = document.getElementById('gridContainer');
const grid = new Grid(10, 10, 500, 500);
grid.appendTo(gridContainer);

// グリッドの操作例
grid.setColorAtIndex(0, 0, new Color(255, 0, 0));
grid.setColorAtIndex(1, 1, new Color(0, 255, 0));
grid.setColorAtIndex(2, 2, new Color(0, 0, 255));
grid.draw();

// クリックイベントの追加
grid.canvas.addEventListener('click', (event) => {
  const rect = grid.canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;
  const { row, col } = grid.getGridIndex(x, y);
  grid.setColorAtIndex(row, col, new Color(Math.random() * 255, Math.random() * 255, Math.random() * 255));
  grid.draw();
});
</script>

改善点

  • パフォーマンス:draw() メソッドは全てのセルを再描画しています。大きなグリッドの場合、これは効率的ではない可能性があります。変更されたセルのみを更新する最適化を検討できるかもしれません。

  • エラーハンドリング:getColorAtIndex() メソッドは、無効なインデックスの場合に何も返さず、暗黙的に undefined を返します。エラー状態を明示的に処理することを検討できます。

  • 型チェック:JavaScript は動的型付け言語ですが、メソッドに渡される引数の型を確認することで、より堅牢なコードになる可能性があります。

  • メモリ使用:大きなグリッドを作成する場合、多くのメモリを消費する可能性があります。必要に応じて、メモリ効率の良い実装を検討することができます。

パフォーマンスの改善:dirtyRegion

  constructor(rowCount, colCount, width, height) {
    // ... 既存のコンストラクタコード ...

    this.dirtyRegion = new Set(); // 更新が必要なセルを追跡
  }

  // ... 他の既存のメソッド ...

  setColorAtIndex(row, col, color) {
    if (!this.isValidIndex(row, col)) return false;

    this.cells[row][col] = color;
    this.dirtyRegion.add(`${row},${col}`);
    return true;
  }
  draw() {
    if (this.dirtyRegion.size > 0) {
      for (let cellKey of this.dirtyRegion) {
        const [row, col] = cellKey.split(',').map(Number);
        this.drawCell(row, col, this.cells[row][col]);
      }
      this.dirtyRegion.clear();
    }
    
    if (this.onGridLine) {
      this.drawGridLines();
    }
  }

エラーハンドリングと型チェック

  setColorAtIndex(row, col, color) {
    if (!this.isValidIndex(row, col)) return false;

    this.cells[row][col] = color;
    this.dirtyRegion.add(`${row},${col}`);
    return true;
  }

  getColorAtIndex(row, col) {
    if (!this.isValidIndex(row, col)) return null;

    return this.cells[row][col];
  }

  isValidIndex(row, col) {
    return row >= 0 && row < this.rowCount && col >= 0 && col < this.colCount;
  }

メモリ効率の良いresize

  // メモリ効率の良いリサイズ
  resize(newRowCount, newColCount, newWidth, newHeight) {
    const oldCells = this.cells;
    const oldRowCount = this.rowCount;
    const oldColCount = this.colCount;

    // Update dimensions
    this.rowCount = newRowCount;
    this.colCount = newColCount;
    this.canvas.width = newWidth;
    this.canvas.height = newHeight;
    this._cellWidth = newWidth / newColCount;
    this._cellHeight = newHeight / newRowCount;

    // 新しいセルの配列を作成(すべてのセルを一度に作成するのではなく、必要に応じて作成)
    this.cells = new Array(newRowCount);
    for (let i = 0; i < newRowCount; i++) {
      this.cells[i] = new Array(newColCount);
    }

    // 古いセルの内容をコピー
    const minRowCount = Math.min(oldRowCount, newRowCount);
    const minColCount = Math.min(oldColCount, newColCount);
    for (let r = 0; r < minRowCount; r++) {
      for (let c = 0; c < minColCount; c++) {
        this.cells[r][c] = oldCells[r][c];
      }
    }

    // 新しいセルを白で初期化
    for (let r = 0; r < newRowCount; r++) {
      for (let c = 0; c < newColCount; c++) {
        if (this.cells[r][c] === undefined) {
          this.cells[r][c] = new Color(255, 255, 255);
        }
      }
    }

    // 全体を再描画
    this.clearCells();
    this.drawCells();
    if (this.onGridLine) {
      this.drawGridLines();
    }
  }


個別

%%html
<div id="cellBasedGridContainer" style="margin-bottom: 20px;"></div>
<div id="gridBasedGridContainer"></div>
<script>
class Color {
  constructor(r, g, b, a = 255) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }
}

class BaseGrid {
  constructor(rowCount, colCount) {
    this.rowCount = rowCount;
    this.colCount = colCount;
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext("2d");
    this.currentColor = new Color(0, 0, 0);
    this.cells = [];
    this.cellInitColor = new Color(255, 255, 255);
    this.onGridLine = true;
    this.lineColor = '#000000';
    this.lineWidth = 1;

    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));
      }
      this.cells.push(row);
    }
  }

  setColorAtIndex(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);
  }

  getColorAtIndex(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.canvas.width;
    const he = this.canvas.height;

    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.clearCells();
    this.drawCells();
    if (this.onGridLine) {
      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.canvas.width, this.canvas.height);
  }

  appendTo(container) {
    container.appendChild(this.canvas);
  }

  getGridIndex(x, y) {
    const col = Math.floor(x / this.cellWidth);
    const row = Math.floor(y / this.cellHeight);
    return { row, col };
  }
}

class CellBasedGrid extends BaseGrid {
  constructor(rowCount, colCount, cellWidth, cellHeight) {
    super(rowCount, colCount);
    this.cellWidth = cellWidth;
    this.cellHeight = cellHeight;
    this.canvas.width = this.cellWidth * this.colCount;
    this.canvas.height = this.cellHeight * this.rowCount;
  }

  resize(newRowCount, newColCount, newCellWidth, newCellHeight) {
    this.rowCount = newRowCount;
    this.colCount = newColCount;
    this.cellWidth = newCellWidth;
    this.cellHeight = newCellHeight;
    this.canvas.width = this.cellWidth * this.colCount;
    this.canvas.height = this.cellHeight * this.rowCount;
    this.cells = Array(this.rowCount).fill().map(() => Array(this.colCount).fill(new Color(255, 255, 255)));
    this.draw();
  }
}

class GridBasedGrid extends BaseGrid {
  constructor(rowCount, colCount, gridWidth, gridHeight) {
    super(rowCount, colCount);
    this.canvas.width = gridWidth;
    this.canvas.height = gridHeight;
    this.cellWidth = this.canvas.width / this.colCount;
    this.cellHeight = this.canvas.height / this.rowCount;
  }

  resize(newRowCount, newColCount, newGridWidth, newGridHeight) {
    this.rowCount = newRowCount;
    this.colCount = newColCount;
    this.canvas.width = newGridWidth;
    this.canvas.height = newGridHeight;
    this.cellWidth = this.canvas.width / this.colCount;
    this.cellHeight = this.canvas.height / this.rowCount;
    this.cells = Array(this.rowCount).fill().map(() => Array(this.colCount).fill(new Color(255, 255, 255)));
    this.draw();
  }
}

// CellBasedGridの作成と表示
const cellBasedGridContainer = document.getElementById('cellBasedGridContainer');
const cellBasedGrid = new CellBasedGrid(10, 10, 30, 30);
cellBasedGrid.appendTo(cellBasedGridContainer);
cellBasedGrid.draw();

// GridBasedGridの作成と表示
const gridBasedGridContainer = document.getElementById('gridBasedGridContainer');
const gridBasedGrid = new GridBasedGrid(10, 10, 400, 300);
gridBasedGrid.appendTo(gridBasedGridContainer);
gridBasedGrid.draw();

// クリックイベントの追加(両方のグリッドに)
[cellBasedGrid, gridBasedGrid].forEach(grid => {
  grid.canvas.addEventListener('click', (event) => {
    const rect = grid.canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    const { row, col } = grid.getGridIndex(x, y);
    grid.setColorAtIndex(row, col, new Color(Math.random() * 255, Math.random() * 255, Math.random() * 255));
    grid.draw();
  });
});
</script>

セルがdiv

%%html
<div id="divBasedGridContainer" style="margin-bottom: 20px;"></div>
<script>
class Color {
  constructor(r, g, b, a = 255) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }

  toString() {
    return `rgba(${this.r},${this.g},${this.b},${this.a / 255})`;
  }
}

class DivBasedGrid {
  constructor(rowCount, colCount, cellWidth, cellHeight) {
    this.rowCount = rowCount;
    this.colCount = colCount;
    this.cellWidth = cellWidth;
    this.cellHeight = cellHeight;
    this.container = document.createElement('div');
    this.container.style.display = 'grid';
    this.container.style.gridTemplateColumns = `repeat(${colCount}, ${cellWidth}px)`;
    this.container.style.gridTemplateRows = `repeat(${rowCount}, ${cellHeight}px)`;
    this.container.style.gap = '1px';
    this.container.style.backgroundColor = '#000'; // Grid line color
    this.container.style.width = `${colCount * cellWidth + (colCount - 1)}px`;
    this.container.style.height = `${rowCount * cellHeight + (rowCount - 1)}px`;

    this.cells = [];
    this.cellInitColor = new Color(255, 255, 255);

    for (let i = 0; i < this.rowCount; i++) {
      let row = [];
      for (let j = 0; j < this.colCount; j++) {
        const cell = document.createElement('div');
        cell.style.backgroundColor = this.cellInitColor.toString();
        cell.style.width = `${cellWidth}px`;
        cell.style.height = `${cellHeight}px`;
        this.container.appendChild(cell);
        row.push({
          element: cell,
          color: new Color(255, 255, 255)
        });
      }
      this.cells.push(row);
    }
  }

  setColorAtIndex(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 = color;
    this.cells[row][col].element.style.backgroundColor = color.toString();
  }

  getColorAtIndex(row, col) {
    if (row < 0 || row >= this.cells.length) return;
    if (col < 0 || col >= this.cells[row].length) return;

    return this.cells[row][col].color;
  }

  getGridIndex(x, y) {
    const col = Math.floor(x / (this.cellWidth + 1)); // +1 for gap
    const row = Math.floor(y / (this.cellHeight + 1)); // +1 for gap
    return { row, col };
  }

  appendTo(container) {
    container.appendChild(this.container);
  }

  resize(newRowCount, newColCount, newCellWidth, newCellHeight) {
    // Remove all existing cells
    while (this.container.firstChild) {
      this.container.removeChild(this.container.firstChild);
    }

    // Update properties
    this.rowCount = newRowCount;
    this.colCount = newColCount;
    this.cellWidth = newCellWidth;
    this.cellHeight = newCellHeight;

    // Update container styles
    this.container.style.gridTemplateColumns = `repeat(${newColCount}, ${newCellWidth}px)`;
    this.container.style.gridTemplateRows = `repeat(${newRowCount}, ${newCellHeight}px)`;
    this.container.style.width = `${newColCount * newCellWidth + (newColCount - 1)}px`;
    this.container.style.height = `${newRowCount * newCellHeight + (newRowCount - 1)}px`;

    // Recreate cells
    this.cells = [];
    for (let i = 0; i < this.rowCount; i++) {
      let row = [];
      for (let j = 0; j < this.colCount; j++) {
        const cell = document.createElement('div');
        cell.style.backgroundColor = this.cellInitColor.toString();
        cell.style.width = `${newCellWidth}px`;
        cell.style.height = `${newCellHeight}px`;
        this.container.appendChild(cell);
        row.push({
          element: cell,
          color: new Color(255, 255, 255)
        });
      }
      this.cells.push(row);
    }
  }
}

// DivBasedGridの作成と表示
const divBasedGridContainer = document.getElementById('divBasedGridContainer');
const divBasedGrid = new DivBasedGrid(10, 10, 30, 30);
divBasedGrid.appendTo(divBasedGridContainer);

// クリックイベントの追加
divBasedGrid.container.addEventListener('click', (event) => {
  const rect = divBasedGrid.container.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;
  const { row, col } = divBasedGrid.getGridIndex(x, y);
  const newColor = new Color(Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256));
  divBasedGrid.setColorAtIndex(row, col, newColor);
});

// リサイズのデモ
setTimeout(() => {
  divBasedGrid.resize(15, 15, 20, 20);
}, 5000);
</script>

ひと通り

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive Canvas Grid</title>
    <style>
        #gridContainer {
            border: 1px solid #000;
            margin: 20px auto;
        }
        #controls {
            text-align: center;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div id="controls">
        <button id="resizeBtn">Resize Grid</button>
        <input type="color" id="colorPicker" value="#000000">
    </div>
    <div id="gridContainer" style="width: 500px; height: 500px;"></div>

<script>
class Color {
    constructor(r, g, b, a = 255) {
        this.r = r;
        this.g = g;
        this.b = b;
        this.a = a;
    }

    static fromHex(hex) {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);
        return new Color(r, g, b);
    }
}

class Grid {
    constructor(rowCount, colCount, width, height) {
        this.canvas = document.createElement('canvas');
        this.canvas.width = width;
        this.canvas.height = height;
        this.ctx = this.canvas.getContext("2d");
        this.currentColor = new Color(0, 0, 0); // Default black color

        this.rowCount = rowCount;
        this.colCount = colCount;
        this._cellWidth = width / this.colCount;
        this._cellHeight = height / this.rowCount;

        this.cells = [];
        this.cellInitColor = new Color(255, 255, 255);

        this.onGridLine = true;
        this.lineColor = '#000000';
        this.lineWidth = 1;

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

        this.dirtyRegion = new Set(); // 更新が必要なセルを追跡
    }

    get width() {
        return this.canvas.width;
    }

    get height() {
        return this.canvas.height;
    }

    get lowerEdge() {
        const cw = this.width / this.colCount;
        const ch = this.height / this.rowCount;
        return Math.min(cw, ch);
    }

    get cellWidth() {
        return this.isSquareCell ? this.lowerEdge : this._cellWidth;
    }

    get cellHeight() {
        return this.isSquareCell ? this.lowerEdge : this._cellHeight;
    }

    get gridWidth() {
        return this.cellWidth * this.colCount;
    }

    get gridHeight() {
        return this.cellHeight * this.rowCount;
    }

    getGridIndex(x, y) {
        const col = Math.floor(x / this.cellWidth);
        const row = Math.floor(y / this.cellHeight);
        return { row, col };
    }

    setColorAtIndex(row, col, color) {
        if (!this.isValidIndex(row, col)) return false;

        this.cells[row][col] = color;
        this.dirtyRegion.add(`${row},${col}`);
        return true;
    }

    getColorAtIndex(row, col) {
        if (!this.isValidIndex(row, col)) return null;

        return this.cells[row][col];
    }

    isValidIndex(row, col) {
        return row >= 0 && row < this.rowCount && col >= 0 && col < this.colCount;
    }

    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() {
        if (this.dirtyRegion.size > 0) {
            for (let cellKey of this.dirtyRegion) {
                const [row, col] = cellKey.split(',').map(Number);
                this.drawCell(row, col, this.cells[row][col]);
            }
            this.dirtyRegion.clear();
        }
        
        if (this.onGridLine) {
            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.width, this.height);
    }

    resize(newRowCount, newColCount, newWidth, newHeight) {
        const oldCells = this.cells;
        const oldRowCount = this.rowCount;
        const oldColCount = this.colCount;

        // Update dimensions
        this.rowCount = newRowCount;
        this.colCount = newColCount;
        this.canvas.width = newWidth;
        this.canvas.height = newHeight;
        this._cellWidth = newWidth / newColCount;
        this._cellHeight = newHeight / newRowCount;

        // 新しいセルの配列を作成(すべてのセルを一度に作成するのではなく、必要に応じて作成)
        this.cells = new Array(newRowCount);
        for (let i = 0; i < newRowCount; i++) {
            this.cells[i] = new Array(newColCount);
        }

        // 古いセルの内容をコピー
        const minRowCount = Math.min(oldRowCount, newRowCount);
        const minColCount = Math.min(oldColCount, newColCount);
        for (let r = 0; r < minRowCount; r++) {
            for (let c = 0; c < minColCount; c++) {
                this.cells[r][c] = oldCells[r][c];
            }
        }

        // 新しいセルを白で初期化
        for (let r = 0; r < newRowCount; r++) {
            for (let c = 0; c < newColCount; c++) {
                if (this.cells[r][c] === undefined) {
                    this.cells[r][c] = new Color(255, 255, 255);
                }
            }
        }

        // 全体を再描画
        this.clearCells();
        this.drawCells();
        if (this.onGridLine) {
            this.drawGridLines();
        }
    }

    appendTo(container) {
        container.appendChild(this.canvas);
    }
}

// Gridの作成と表示
const gridContainer = document.getElementById('gridContainer');
const grid = new Grid(10, 10, 500, 500);
grid.appendTo(gridContainer);

// 初期グリッドの描画
grid.draw();

// クリックイベントの追加
grid.canvas.addEventListener('click', (event) => {
    const rect = grid.canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    const { row, col } = grid.getGridIndex(x, y);
    grid.setColorAtIndex(row, col, grid.currentColor);
    grid.draw();
});

// リサイズボタンの処理
document.getElementById('resizeBtn').addEventListener('click', () => {
    const newSize = prompt("Enter new grid size (e.g., '20,20' for 20x20 grid):", "20,20");
    if (newSize) {
        const [rows, cols] = newSize.split(',').map(Number);
        if (rows > 0 && cols > 0) {
            grid.resize(rows, cols, grid.width, grid.height);
        }
    }
});

// カラーピッカーの処理
document.getElementById('colorPicker').addEventListener('input', (event) => {
    grid.currentColor = Color.fromHex(event.target.value);
});
</script>

</body>
</html>

ツール追加

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive Canvas Grid with Drawing Tools</title>
    <style>
        #gridContainer {
            border: 1px solid #000;
            margin: 20px auto;
        }
        #controls {
            text-align: center;
            margin-bottom: 20px;
        }
        .tool-btn {
            margin: 0 5px;
        }
    </style>
</head>
<body>
    <div id="controls">
        <button id="resizeBtn">Resize Grid</button>
        <input type="color" id="colorPicker" value="#000000">
        <button class="tool-btn" data-tool="point">Point</button>
        <button class="tool-btn" data-tool="pen">Pen</button>
        <button class="tool-btn" data-tool="eraser">Eraser</button>
        <button class="tool-btn" data-tool="line">Line</button>
    </div>
    <div id="gridContainer" style="width: 500px; height: 500px;"></div>

<script>
class Color {
    constructor(r, g, b, a = 255) {
        this.r = r;
        this.g = g;
        this.b = b;
        this.a = a;
    }

    static fromHex(hex) {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);
        return new Color(r, g, b);
    }
}

class Grid {
    constructor(rowCount, colCount, width, height) {
        this.canvas = document.createElement('canvas');
        this.canvas.width = width;
        this.canvas.height = height;
        this.ctx = this.canvas.getContext("2d");
        this.currentColor = new Color(0, 0, 0); // Default black color

        this.rowCount = rowCount;
        this.colCount = colCount;
        this._cellWidth = width / this.colCount;
        this._cellHeight = height / this.rowCount;

        this.cells = [];
        this.cellInitColor = new Color(255, 255, 255);

        this.onGridLine = true;
        this.lineColor = '#000000';
        this.lineWidth = 1;

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

        this.dirtyRegion = new Set(); // 更新が必要なセルを追跡

        this.tool = 'point';
        this.isDrawing = false;
        this.lastCell = null;
        this.lineStartCell = null;
    }

    get width() {
        return this.canvas.width;
    }

    get height() {
        return this.canvas.height;
    }

    get lowerEdge() {
        const cw = this.width / this.colCount;
        const ch = this.height / this.rowCount;
        return Math.min(cw, ch);
    }

    get cellWidth() {
        return this.isSquareCell ? this.lowerEdge : this._cellWidth;
    }

    get cellHeight() {
        return this.isSquareCell ? this.lowerEdge : this._cellHeight;
    }

    get gridWidth() {
        return this.cellWidth * this.colCount;
    }

    get gridHeight() {
        return this.cellHeight * this.rowCount;
    }

    getGridIndex(x, y) {
        const col = Math.floor(x / this.cellWidth);
        const row = Math.floor(y / this.cellHeight);
        return { row, col };
    }

    setColorAtIndex(row, col, color) {
        if (!this.isValidIndex(row, col)) return false;

        this.cells[row][col] = color;
        this.dirtyRegion.add(`${row},${col}`);
        return true;
    }

    getColorAtIndex(row, col) {
        if (!this.isValidIndex(row, col)) return null;

        return this.cells[row][col];
    }

    isValidIndex(row, col) {
        return row >= 0 && row < this.rowCount && col >= 0 && col < this.colCount;
    }

    drawCell(row, col, color) {
        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() {
        if (this.dirtyRegion.size > 0) {
            for (let cellKey of this.dirtyRegion) {
                const [row, col] = cellKey.split(',').map(Number);
                this.drawCell(row, col, this.cells[row][col]);
            }
            this.dirtyRegion.clear();
        }
        
        if (this.onGridLine) {
            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.width, this.height);
    }

    resize(newRowCount, newColCount, newWidth, newHeight) {
        const oldCells = this.cells;
        const oldRowCount = this.rowCount;
        const oldColCount = this.colCount;

        // Update dimensions
        this.rowCount = newRowCount;
        this.colCount = newColCount;
        this.canvas.width = newWidth;
        this.canvas.height = newHeight;
        this._cellWidth = newWidth / newColCount;
        this._cellHeight = newHeight / newRowCount;

        // 新しいセルの配列を作成(すべてのセルを一度に作成するのではなく、必要に応じて作成)
        this.cells = new Array(newRowCount);
        for (let i = 0; i < newRowCount; i++) {
            this.cells[i] = new Array(newColCount);
        }

        // 古いセルの内容をコピー
        const minRowCount = Math.min(oldRowCount, newRowCount);
        const minColCount = Math.min(oldColCount, newColCount);
        for (let r = 0; r < minRowCount; r++) {
            for (let c = 0; c < minColCount; c++) {
                this.cells[r][c] = oldCells[r][c];
            }
        }

        // 新しいセルを白で初期化
        for (let r = 0; r < newRowCount; r++) {
            for (let c = 0; c < newColCount; c++) {
                if (this.cells[r][c] === undefined) {
                    this.cells[r][c] = new Color(255, 255, 255);
                }
            }
        }

        // 全体を再描画
        this.clearCells();
        this.drawCells();
        if (this.onGridLine) {
            this.drawGridLines();
        }
    }

    drawLine(startCell, endCell) {
        const dx = Math.abs(endCell.col - startCell.col);
        const dy = Math.abs(endCell.row - startCell.row);
        const sx = startCell.col < endCell.col ? 1 : -1;
        const sy = startCell.row < endCell.row ? 1 : -1;
        let err = dx - dy;

        let currentCell = {...startCell};

        while (true) {
            this.setColorAtIndex(currentCell.row, currentCell.col, this.currentColor);
            
            if (currentCell.col === endCell.col && currentCell.row === endCell.row) break;
            
            const e2 = 2 * err;
            if (e2 > -dy) {
                err -= dy;
                currentCell.col += sx;
            }
            if (e2 < dx) {
                err += dx;
                currentCell.row += sy;
            }
        }
    }

    handleMouseDown(event) {
        const rect = this.canvas.getBoundingClientRect();
        const x = event.clientX - rect.left;
        const y = event.clientY - rect.top;
        const cell = this.getGridIndex(x, y);

        switch (this.tool) {
            case 'point':
            case 'pen':
            case 'eraser':
                this.isDrawing = true;
                this.setColorAtIndex(cell.row, cell.col, this.tool === 'eraser' ? this.cellInitColor : this.currentColor);
                this.lastCell = cell;
                break;
            case 'line':
                if (!this.lineStartCell) {
                    this.lineStartCell = cell;
                } else {
                    this.drawLine(this.lineStartCell, cell);
                    this.lineStartCell = null;
                }
                break;
        }

        this.draw();
    }

    handleMouseMove(event) {
        if (!this.isDrawing || (this.tool !== 'pen' && this.tool !== 'eraser')) return;

        const rect = this.canvas.getBoundingClientRect();
        const x = event.clientX - rect.left;
        const y = event.clientY - rect.top;
        const cell = this.getGridIndex(x, y);

        if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
            this.drawLine(this.lastCell, cell);
            this.lastCell = cell;
            this.draw();
        }
    }

    handleMouseUp() {
        this.isDrawing = false;
    }

    appendTo(container) {
        container.appendChild(this.canvas);
    }
}

// Gridの作成と表示
const gridContainer = document.getElementById('gridContainer');
const grid = new Grid(10, 10, 500, 500);
grid.appendTo(gridContainer);

// 初期グリッドの描画
grid.draw();

// マウスイベントの追加
grid.canvas.addEventListener('mousedown', (event) => grid.handleMouseDown(event));
grid.canvas.addEventListener('mousemove', (event) => grid.handleMouseMove(event));
grid.canvas.addEventListener('mouseup', () => grid.handleMouseUp());

// リサイズボタンの処理
document.getElementById('resizeBtn').addEventListener('click', () => {
    const newSize = prompt("Enter new grid size (e.g., '20,20' for 20x20 grid):", "20,20");
    if (newSize) {
        const [rows, cols] = newSize.split(',').map(Number);
        if (rows > 0 && cols > 0) {
            grid.resize(rows, cols, grid.width, grid.height);
        }
    }
});

// カラーピッカーの処理
document.getElementById('colorPicker').addEventListener('input', (event) => {
    grid.currentColor = Color.fromHex(event.target.value);
});

// ツール選択の処理
document.querySelectorAll('.tool-btn').forEach(btn => {
    btn.addEventListener('click', (event) => {
        grid.tool = event.target.dataset.tool;
        grid.lineStartCell = null; // ライン描画をリセット
    });
});
</script>

</body>
</html>





この記事が気に入ったらサポートをしてみませんか?