Canvas要素でカレンダーアプリを作ってみる#1

# はじめに

PixiJSとCanvas要素を使用してカレンダーアプリを作りたくなったので作ってみる。(全3記事を予定)

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

PixiJSとは、(下記URLより)

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

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

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

https://pixijs.com/

# 今回作るカレンダーアプリ

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

主な機能は

* Calendar表示

![Canvas要素でカレンダーアプリを作ってみる1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/823ba568-a0ae-d34e-4e4a-730caff94a4a.jpeg)

* D&D(Drag And Drop)による登録・移動

![Canvas要素でカレンダーアプリを作ってみる2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/5d603777-01e8-8bf6-0953-f5495e87dbbd.gif)

* 時間軸のResize

![Canvas要素でカレンダーアプリを作ってみる3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/f1b6d55d-270a-c064-e63c-c38b2a1a550a.gif)

* Taskの重複の警告

![Canvas要素でカレンダーアプリを作ってみる4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/a575b2fa-8b7d-4b5a-d1b1-caf7637ef4a0.gif)


* Title・Colorの変更

![Canvas要素でカレンダーアプリを作ってみる5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/327b3061-7726-1e49-dd2e-e32004f5f2c6.gif)


* HoverTitle 

![Canvas要素でカレンダーアプリを作ってみる6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/91aefd1a-a8a5-220b-8935-e0fa96c5a3e7.gif)

* LocalStrage保存機能

![Canvas要素でカレンダーアプリを作ってみる7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/8f691d10-6ad5-1b61-9adc-89cb5614e5b5.gif)

# 成果物DEMO_URL

https://shisojuice.github.io/canvas_calendar/

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

* 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の更新ボタンと削除ボタンを配置する。

```html: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>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を使用して罫線を引いておく。

```css:style.css
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で初期設定しておく。

```javascript:main.js
//　略
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登録やインタラクティブな操作のロジックを記事にしていく。

次回につづく。

https://qiita.com/shisojuice/items/3c1b0e1e21eda5c24443

https://qiita.com/shisojuice/items/830717674e646872ead4