はじめに
PixiJSとCanvas要素を使用してカレンダーアプリを作りたくなったので作ってみる。(全3記事を予定)
(Canvas内で行わなくても普通にDOMでも作ることは可能ですが、PixiJSの勉強もかねています🧑🎨)
PixiJSとは、(下記URLより)
PixiJS は本質的に、WebGL (またはオプションで Canvas) を使用して画像やその他の 2D ビジュアル コンテンツを表示するレンダリング システムです。
PixiJS は、完全なシーン グラフ (レンダリングするオブジェクトの階層) を提供し、
クリック イベントやタッチ イベントを処理できるようにするインタラクション サポートを提供します。
今回作るカレンダーアプリ
こんな感じのアプリを作成する。
主な機能は
- Calendar表示
- D&D(Drag And Drop)による登録・移動
- 時間軸のResize
- Taskの重複の警告
- Title・Colorの変更
- HoverTitle
- LocalStrage保存機能
成果物DEMO_URL
今回使用するライブラリなど
-
Pixijs
(描画ライブラリとして使用)
https://pixijs.com/ -
tempo
(日付ライブラリとして使用)
https://tempo.formkit.com/ -
PGlite
(IndexedDB内の処理に使用)
https://pglite.dev/ -
Vite
(Buildツール)
https://ja.vite.dev/
環境作成
ViteのVanillaのTemplateをベースに作成する。
今回は、プロジェクト名は my-calendar で作成する。
npm create vite@latest my-calendar -- --template vanilla
プロジェクトの作成
htmlとcssを用いて画面を作成する。
画面上部には表示時の年月と左右のページ送りボタンとテキスト入力とカラー指定のInput、Taskの更新ボタンと削除ボタンを配置する。
<!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>mycalendar</title>
</head>
<body>
<div class="row">
<button autofocus id="arrowleft">◀</button>
<div id="calendar_month"></div>
<button id="arrowright">▶</button>
</div>
<div class="row">
<input type="color" id="taskcolor">
<input type="text" id="tasktitle" maxlength="30">
<button id="taskchange" data-taskid="">TASK CHANGE</button>
<button id="taskdelete" data-taskid="">TASK DELETE</button>
</div>
<div class="row">
<button id="calendarsave" data-taskid="">CALENDAR SAVE</button>
</div>
<div id="gr">
<canvas id="padcanvas"></canvas>
<canvas id="headcanvas"></canvas>
<canvas id="leftcanvas"></canvas>
<div id="appmain"></div>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>
Canvas内のCalendar上の罫線はPixiJSで引いても良いが、
より簡単にCSSのbackground-imageやbackground-sizeを使用して罫線を引いておく。
body {
background-color: #000;
margin: 0;
padding: 0;
font-family: monospace;
font-size: 12px;
color: lightgray;
}
.row {
display: flex;
align-items: center;
margin: 4px;
}
#arrowright,
#arrowleft,
#taskchange,
#taskdelete {
font-size: 10px;
height: 20px;
}
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);
}
#gr {
display: grid;
grid-template-columns: 60px 1440px;
width: 100%;
}
#padcanvas {
background-color: black;
height: 20px;
width: 60px;
background-size: auto;
position: sticky;
left: 0;
}
#headcanvas {
height: 20px;
width: 1440px;
background-size: auto;
}
#leftcanvas {
background-color: black;
position: sticky;
left: 0;
height: 1240px;
background-size: 60px 40px;
}
#timemaincanvas {
height: 1240px;
background-size: 60px 40px;
left: 60px;
}
windowのLoadイベントでPGliteを使用して
local上に保持しておきたい、task用のテーブルをなければ、Create。
カレンダーの上部の時間軸を表示し、
task操作を行うCanvas内はPixiJSのapp.initで初期設定しておく。
// 略
const db = new PGlite('idb://my-pgdata');
const app = new PIXI.Application();
const cellWidth = 60;
const cellHeight = 40;
const objh = cellHeight / 2;
const padtoph = cellHeight / 4;
const maxtime = 24;
let maxDays = 31;
let cellHeightCount = maxDays;
let gridHeight = cellHeight * cellHeightCount;
const calendarsave = document.getElementById('calendarsave');
const taskdelete = document.getElementById('taskdelete');
const taskchange = document.getElementById('taskchange');
const tasktitle = document.getElementById('tasktitle');
const taskcolor = document.getElementById('taskcolor');
const calendar_month = document.getElementById('calendar_month');
const padcanvas = document.getElementById('padcanvas');
const leftcanvas = document.getElementById('leftcanvas');
const headcanvas = document.getElementById('headcanvas');
let createStart = { x: -1, y: -1 };
let draggingTask = false;
let resizingTask = false;
const _Graphics = '_Graph';
const _ResizeL = '_ResizeL';
const _ResizeR = '_ResizeR';
const _Distinct = '_Distinct';
const _Text = '_Text';
const tempObjLabel = 'tempObjLabel';
const tempMoveObjLabel = 'tempMoveObjLabel';
const focusGraphics = 'focusGraph';
const defaulttitle = 'Empty!';
const defaultcolor = '#ffa500';
window.onload = async () => {
await db.query(`
CREATE TABLE IF NOT EXISTS task_master (
taskid UUID PRIMARY KEY NOT NULL,
title VARCHAR(30) NOT NULL,
color CHAR(7) NOT NULL,
x DECIMAL(4, 0) NOT NULL,
y DECIMAL(4, 0) NOT NULL,
width DECIMAL(4, 0) NOT NULL,
yearmonth DECIMAL(6, 0) NOT NULL );`)
const dt = new Date();
maxDays = monthDays(dt);
gridHeight = cellHeight * maxDays;
calendar_month.innerText = format({ date: dt, format: 'YYYY-MM', tz: 'Asia/Tokyo', });
// ヘッダー部の初期設定
padcanvas.height = cellHeight / 2;
padcanvas.width = cellWidth;
// ヘッダーに時間を記載
headcanvas.height = cellHeight / 2;
headcanvas.width = cellWidth * maxtime;
const headctx = headcanvas.getContext('2d');
headctx.font = `15px monospace`;
headctx.fillStyle = 'lightgray';
let x = 0;
for (let i = 1; i < maxtime; i++) {
x += cellWidth
const buffer = i < 10 ? 3 : 5;
headctx.fillText(i, x - buffer, 15);
}
// カレンダー 初期設定
await app.init({
width: cellWidth * maxtime,
height: gridHeight,
backgroundAlpha: 0
});
app.canvas.id = 'timemaincanvas';
app.stage.interactive = true;
app.stage.hitArea = app.renderer.screen;
document.getElementById('appmain').appendChild(app.canvas);
}
// 略
まとめ
今回は、構成の説明と画面の初期設定まで行った。
次回はカレンダーアプリ内のTask登録やインタラクティブな操作のロジックを記事にしていく。
次回につづく。