はじめに
PixiJSとCanvas要素を使用してGridを作りたくなったので作ってみる。(全2記事を予定)
(Canvas内で行わなくても普通にDOMでも作ることは可能ですが、PixiJSの勉強もかねています🧑🎨)
PixiJSとは、(下記URLより)
PixiJS は本質的に、WebGL (またはオプションで Canvas) を使用して画像やその他の 2D ビジュアル コンテンツを表示するレンダリング システムです。
PixiJS は、完全なシーン グラフ (レンダリングするオブジェクトの階層) を提供し、
クリック イベントやタッチ イベントを処理できるようにするインタラクション サポートを提供します。
今回作るGrid
こんな感じのアプリを作成する。
主な機能は
- Grid表示
- Clickによる移動(Focus)
- Tabによる移動(Focus)
- 矢印キーによる移動(Focus)
- テキスト入力
- Hoverテキスト(Tooltip)
成果物DEMO_URL
今回使用するライブラリなど
- Pixijs(描画ライブラリとして使用)
- Vite(Buildツール)
環境作成
ViteのVanillaのTemplateをベースに作成する。
今回は、プロジェクト名は my-grid で作成する。
npm create vite@latest my-grid -- --template vanilla
プロジェクトの作成
htmlとcssを用いて画面を作成する。
<!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を使用して罫線を引いておく。
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を作成する。
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時の機能を付与する。
/**
* 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しているように見せる。
/**
* 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内のイベント操作のロジックを記事にしていく。
次回につづく。