1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptAdvent Calendar 2024

Day 10

Canvas要素でGridを作ってみる#1

Last updated at Posted at 2024-12-04

はじめに

PixiJSとCanvas要素を使用してGridを作りたくなったので作ってみる。(全2記事を予定)

(Canvas内で行わなくても普通にDOMでも作ることは可能ですが、PixiJSの勉強もかねています🧑‍🎨)

PixiJSとは、(下記URLより)

PixiJS は本質的に、WebGL (またはオプションで Canvas) を使用して画像やその他の 2D ビジュアル コンテンツを表示するレンダリング システムです。

PixiJS は、完全なシーン グラフ (レンダリングするオブジェクトの階層) を提供し、

クリック イベントやタッチ イベントを処理できるようにするインタラクション サポートを提供します。

今回作るGrid

こんな感じのアプリを作成する。

主な機能は

  • Grid表示

Canvas要素でGridを作ってみる1.jpg

  • Clickによる移動(Focus)

Canvas要素でGridを作ってみる2.gif

  • Tabによる移動(Focus)

Canvas要素でGridを作ってみる3.gif

  • 矢印キーによる移動(Focus)

Canvas要素でGridを作ってみる4.gif

  • テキスト入力

Canvas要素でGridを作ってみる5.gif

  • Hoverテキスト(Tooltip)

Canvas要素でGridを作ってみる6.gif

成果物DEMO_URL

今回使用するライブラリなど

  • Pixijs(描画ライブラリとして使用)

  • Vite(Buildツール)

環境作成

ViteのVanillaのTemplateをベースに作成する。

今回は、プロジェクト名は my-grid で作成する。

npm create vite@latest my-grid -- --template vanilla

プロジェクトの作成

htmlとcssを用いて画面を作成する。

index.html
<!doctype html>
<html lang="ja">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="style.css" />
  <title>Pixi_Grid</title>
</head>

<body id="main">
  <div id="app_pixigrid"></div>
  <script type="module" src="/main.js"></script>
</body>

</html>

Canvas内のGrid上の罫線はPixiJSで引いても良いが、

より簡単にCSSのbackground-imageやbackground-sizeを使用して罫線を引いておく。

style.css
body {
    background-color: black;
    margin: 0;
    padding: 0;
    font-family: monospace;
    display: grid;
    align-items: start;
    justify-content: start;
}

canvas {
    display: block;
    border: 1px solid lightgray;
    background-image:
        linear-gradient(to right, lightgray 1px, transparent 1px),
        linear-gradient(to bottom, lightgray 1px, transparent 1px);    
}

#app_pixigrid {
    width: 100%;
    height: 100vh;
    overflow: scroll;
}

#app_pixigrid_focus {
    font-family: monospace;
    position: fixed;
    z-index: 2;
    font-size: 16px;
    color: white;
    background-color: black;
}

#app_pixigrid_focus:focus {
    background-color: gray;
}

windowのLoadイベントで

Canvas内はPixiJSのapp.initで初期設定しておく。

Canvas内に列、行の数だけcreateDetailCellContainerの関数で

Cellを作成する。

main.js
import * as PIXI from 'pixi.js';
const app = new PIXI.Application();

const colCount = Number(100);
const rowCount = Number(100);
const cellWidth = Number(60);
const cellHeight = Number(20);
const gridWidth = cellWidth * colCount;
const gridHeight = cellHeight * rowCount;

const canvas_id = 'app_pixigrid';
const _Graphics = '_Graphics';
const _TextGraphics = '_TextGraphics';

// GridのFocus
let last_focus_lbl = null;
const focus_input = document.createElement('input');
focus_input.id = canvas_id + '_focus';
focus_input.autocomplete = 'off';
focus_input.spellcheck = false;

window.onload = async () => {
  await app.init({
    width: gridWidth,
    height: gridHeight,
    backgroundAlpha: 0
  });
  app.canvas.id = 'maincanvas';
  app.stage.interactive = true;
  app.stage.hitArea = app.renderer.screen;
  document.getElementById(canvas_id).appendChild(app.canvas);
  document.getElementById('maincanvas').style.backgroundSize = `${cellWidth}px ${cellHeight}px`;
  for (let col = 0; col < colCount; col++) {
    for (let row = 0; row < rowCount; row++) {
      const txt = col + '-' + row;
      const container = createDetailCellContainer(col, row, txt);
      app.stage.addChild(container);
    }
  }
  detailCellFocus(0, 0); // InitFocus
}

createDetailCellContainer関数で

column、rowのindexと表示するテキストを引数に受け取り、Cellを返す。

テキストはCellの幅内に収まるように調整する。

pointeroverにHover時の機能、pointerdownにFocus時の機能を付与する。

main.js
/**
 * Detail部のCellをCreateする関数です。
 * @param {number} index_column CellColumnIndex
 * @param {number} index_row CellRowIndex
 * @param {string} txt CellText
 * @returns {PIXI.Container} PIXI.Container(Cell)を返す
 */
const createDetailCellContainer = (index_column, index_row, txt) => {
  // Object群作成
  let obj = new PIXI.Graphics()
    .rect(index_column * cellWidth, index_row * cellHeight, cellWidth, cellHeight)
    .fill(0xffffff);
  obj.alpha = 0.0;
  obj.label = _Graphics;
  obj.interactive = true;
  obj.buttonMode = true;
  let text = new PIXI.Text({
    text: txt,
    style: new PIXI.TextStyle({
      fontFamily: 'monospace',
      fontSize: cellHeight - 4,
      fill: 0xffffff,
    })
  })
  text.label = _TextGraphics;
  text.x = index_column * cellWidth + 4;
  text.y = index_row * cellHeight;
  text.interactive = true;
  text.buttonMode = true;
  const bounds = text.getBounds();
  if (cellWidth < bounds.width) {
    let temptext = '';
    let maxHalfWords = Math.floor(cellWidth / Math.floor(cellHeight / 2));
    for (let i = 0; i < txt.length; i++) {
      if (/^[a-zA-Z0-9!-/:-@\[-`{-~ァ-ン゙゚]+$/.test(txt[i])) {
        maxHalfWords = maxHalfWords - 1;
      }
      else {
        maxHalfWords = maxHalfWords - 2;
      }
      if (maxHalfWords < 0) {
        break;
      }
      temptext += txt[i];
    }
    text.text = temptext
  }
  obj.on('pointerover', (e) => { txthover(txt); });
  text.on('pointerover', (e) => { txthover(txt); });
  obj.on('pointerdown', (e) => {
    detailCellFocus(index_column, index_row);
  });
  text.on('pointerdown', (e) => {
    detailCellFocus(index_column, index_row);
  });
  // Container作成
  const lbl = `col${index_column}row${index_row}`;
  const container = new PIXI.Container();
  container.zIndex = 1;
  container.label = lbl;
  container.my_col = index_column;
  container.my_row = index_row;
  container.my_txt = txt;
  container.addChild(obj);
  container.addChild(text);
  return container;
}

/**
 * CellのHover時にTextのTitleを表示する関数です。
 * @param {string} txt CellText
 */
const txthover = (txt) => {
  document.getElementById(canvas_id).title = txt;
}

HTMLInputを使い、実際にそのCanvas内のCellに対して

Focusしているように見せる。

main.js
/**
 * Detail部のCellをFocusする関数です。
 * @param {number} index_column CellColumnIndex
 * @param {number} index_row CellRowIndex
 */
const detailCellFocus = (index_column, index_row) => {
  detailCellBlur();
  let targetcontainer = app.stage.getChildByLabel(`col${index_column}row${index_row}`);
  if (targetcontainer) {
    // HTMLInputFocus
    focus_input.value = targetcontainer.my_txt; focus_input.title = targetcontainer.my_txt;
    focus_input.dataset.my_col = targetcontainer.my_col; focus_input.dataset.my_row = targetcontainer.my_row;
    focus_input.style.width = (cellWidth - 8) + 'px'; focus_input.style.height = (cellHeight - 6) + 'px';
    const rect = document.getElementById(canvas_id).getBoundingClientRect();
    const scr_left = document.getElementById(canvas_id).scrollLeft;
    const scr_top = document.getElementById(canvas_id).scrollTop;
    focus_input.style.top = ((targetcontainer.my_row * cellHeight) + rect.top - scr_top) + 'px'; focus_input.style.left = ((targetcontainer.my_col * cellWidth) + rect.left - scr_left) + 'px';
    document.getElementById(canvas_id).appendChild(focus_input);
    requestAnimationFrame(() => {
      focus_input.focus();
      focus_input.select();
    });
  }
}


まとめ

今回は、構成の説明と画面の初期設定まで行った。

次回はGrid内のイベント操作のロジックを記事にしていく。

次回につづく。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?