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

# はじめに

#2で(下記)、TaskのCreateの関数を実装した続きから行う。(全3記事を予定)

https://qiita.com/shisojuice/items/8958012ae04b7a1fa52b

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

# プロジェクトの作成(#2の続きから)

MainとなるCanvas内でイベントを付与する。

* Canvas内で何もないところにDragしたら、Taskを新規作成する。

![Canvas要素でカレンダーアプリを作ってみる3_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/ff946c1e-7717-cf23-0b8b-fe6703b3bcdd.gif)

* Canvas内でTask自体をDragしたら、TaskをDragした最終位置に移動させる。

![Canvas要素でカレンダーアプリを作ってみる3_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/f41d1791-d926-0dbc-d303-42f1775c04b3.gif)

* TaskのResizerをDragしたら、横軸上で伸縮する。

![Canvas要素でカレンダーアプリを作ってみる3_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/1ecc6006-7ad5-c3a7-8ef3-463eac99bfb7.gif)

```javascript:main.js

/**
 * TaskObjectの新規追加するとき、Objectを仮表示する関数
 * @param {number} x - 位置x
 * @param {number} y - 位置y
 * @param {number} w - width
 * @param {string} c - 表示色
 */
const createtempTask = (x, y, w, c) => {
  blurTask();
  let tempObj = app.stage.getChildByLabel(tempObjLabel);
  if (tempObj) {
    tempObj.destroy({ children: true, texture: true, textureSource: true, context: true });
  }
  let obj = new PIXI.Graphics()
    .rect(x, y, w, objh)
    .stroke({ width: 1, color: c })
    .fill({ color: 0x000000, alpha: 0 });
  obj.label = tempObjLabel + _Graphics;
  obj.interactive = true;
  obj.buttonMode = true;
  // コンテナ作成
  const container = new PIXI.Container();
  container.zIndex = 5;
  container.label = tempObjLabel;
  container.my_x = x;
  container.my_y = y;
  container.my_width = w;
  container.my_color = c;
  container.addChild(obj);
  app.stage.addChild(container);
}

/**
 * TaskObjectの移動するとき、Objectを仮表示する関数
 * @param {string} cursor - 表示カーソル
 * @param {number} x - 位置x
 * @param {number} y - 位置y
 * @param {number} w - width
 * @param {string} c - 表示色
 */
const createtempMoveTask = (cursor, x, y, w, c) => {
  blurTask();
  let tempMoveObj = app.stage.getChildByLabel(tempMoveObjLabel);
  if (tempMoveObj) {
    tempMoveObj.destroy({ children: true, texture: true, textureSource: true, context: true });
  }
  let obj = new PIXI.Graphics()
    .rect(x, y, w, objh)
    .fill(c);
  obj.label = tempMoveObjLabel + _Graphics;
  obj.interactive = true;
  obj.buttonMode = true;
  obj.cursor = cursor;
  // コンテナ作成
  const container = new PIXI.Container();
  container.zIndex = 5;
  container.label = tempMoveObjLabel;
  container.my_x = x;
  container.my_y = y;
  container.my_width = w;
  container.my_color = c;
  container.addChild(obj);
  app.stage.addChild(container);
}
app.stage.on('pointerdown', (e) => {
  if (draggingTask) return; // ObjectをDrag中ならReturn
  if (resizingTask) return; // ObjectをResize中ならReturn
  createStart = { x: -1, y: -1 };
  // 新しいオブジェクトの作成開始位置をグローバル座標で保存
  const currentPos = e.data.global;
  createStart = { x: currentPos.x, y: currentPos.y };
});

app.stage.on('pointermove', (e) => {
  // ポインタの現在の位置を取得
  const currentPos = e.data.global;
  // ObjectをDrag中
  if (draggingTask) {
    let myObj = app.stage.getChildByLabel(draggingTask.label);
    if (myObj) {
      myObj.destroy({ children: true, texture: true, textureSource: true, context: true });
    }
    deletedistinctTask(draggingTask.label);
    createtempMoveTask('move', currentPos.x - draggingTask.my_dragx, currentPos.y - objh / 2, draggingTask.my_width, draggingTask.my_color);
    return;
  }
  // ObjectをResize中
  if (resizingTask) {
    let myObj = app.stage.getChildByLabel(resizingTask.label);
    if (myObj) {
      myObj.destroy({ children: true, texture: true, textureSource: true, context: true });
    }
    deletedistinctTask(resizingTask.label);
    const x = resizingTask.my_x
    const xw = resizingTask.my_x + resizingTask.my_width
    const y = resizingTask.my_y
    if (resizingTask.my_lr == 'R') {
      if (currentPos.x - x >= cellWidth / 12) {
        createtempMoveTask('col-resize', x, y, currentPos.x - x, resizingTask.my_color);
      }
    }
    if (resizingTask.my_lr == 'L') {
      if (xw - currentPos.x >= cellWidth / 12) {
        createtempMoveTask('col-resize', currentPos.x, y, xw - currentPos.x, resizingTask.my_color);
      }
    }
    return;
  }

  if (createStart.x == -1) return;

  // ObjectをCreate中
  if (currentPos.x - createStart.x > 0) {
    // =>方向にCreate
    createtempTask(createStart.x, createStart.y, currentPos.x - createStart.x, defaultcolor);
  } else {
    // <=方向にCreate
    createtempTask(currentPos.x, createStart.y, createStart.x - currentPos.x, defaultcolor);
  }

});

const pointerUp = async (e) => {
  // ObjectをDrag中
  if (draggingTask) {
    let tempMoveObj = app.stage.getChildByLabel(tempMoveObjLabel);
    if (tempMoveObj) {
      await createTask(draggingTask.label, tempMoveObj.my_x, tempMoveObj.my_y, tempMoveObj.my_width, tempMoveObj.my_color, '');
      tempMoveObj.destroy({ children: true, texture: true, textureSource: true, context: true });
    }
    draggingTask = false;
    return;
  }
  // ObjectをResize中
  if (resizingTask) {
    let tempMoveObj = app.stage.getChildByLabel(tempMoveObjLabel);
    if (tempMoveObj) {
      if (tempMoveObj.my_width >= cellWidth / 12) {
        await createTask(resizingTask.label, tempMoveObj.my_x, tempMoveObj.my_y, tempMoveObj.my_width, tempMoveObj.my_color, '');
      }
      else {
        await createTask(resizingTask.label, tempMoveObj.my_x, tempMoveObj.my_y, cellWidth / 12, tempMoveObj.my_color, '');
      }
      tempMoveObj.destroy({ children: true, texture: true, textureSource: true, context: true });
    }
    resizingTask = false;
  }

  if (createStart.x == -1) return;
  // ObjectをCreate中
  let tempObj = app.stage.getChildByLabel(tempObjLabel);
  if (tempObj) {
    if (tempObj.my_width >= cellWidth / 12) {
      await createTask(crypto.randomUUID().toString(), tempObj.my_x, tempObj.my_y, tempObj.my_width, defaultcolor, '');
    }
    tempObj.destroy({ children: true, texture: true, textureSource: true, context: true });
  }
  createStart = { x: -1, y: -1 };
}

app.stage.on('pointerup', pointerUp);
app.stage.on('pointerupoutside', pointerUp);

```

画面上部のtaskchangeのボタン押下のイベントで

対象Taskを一度削除しておき、指定されたInput内の値で新たにTaskを作成する。

![Canvas要素でカレンダーアプリを作ってみる3_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/a23c88c2-4887-f2b8-ba26-94af55a7f4a7.gif)


```javascript:main.js
taskchange.addEventListener('click', async (e) => {
  const taskId = e.target.dataset.taskid;
  if (taskId == '') return;  
  const color = taskcolor.value;
  const title = tasktitle.value;
  let targetcontainer = app.stage.getChildByLabel(taskId);
  if (targetcontainer) {
    blurTask();
    targetcontainer.destroy({ children: true, texture: true, textureSource: true, context: true });
    // re create
    await createTask(taskId, targetcontainer.my_x, targetcontainer.my_y, targetcontainer.my_width, color, title);
    focusTask(taskId);
  }
});
```

画面上部のtaskdeleteのボタン押下のイベントでtaskを削除する。


![Canvas要素でカレンダーアプリを作ってみる3_5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/56328b88-24eb-298a-a63c-c75a664f057c.gif)


```javascript:main.js
taskdelete.addEventListener('click', async (e) => {
  const taskId = e.target.dataset.taskid;
  if (taskId == '') return;
  let targetcontainer = app.stage.getChildByLabel(taskId);
  if (targetcontainer) {
    deletedistinctTask(taskId);
    targetcontainer.destroy({ children: true, texture: true, textureSource: true, context: true });
    // focus destroy
    blurTask();
  }
});
```

画面上部のcalendarsaveのボタン押下のイベントでtaskを保存する。

画面上部の左右のボタン押下のイベントでtaskを保存し、カレンダーを翌月OR先月にする。

![Canvas要素でカレンダーアプリを作ってみる3_6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/c08378e3-6416-3a53-4e3b-ae1de2019bad.gif)

```javascript:main.js
/**
 * Taskを保存する関数
 */
const saveTasks = async () => {
  let count = 0;
  const fmtYYYYMM = calendar_month.innerText.replaceAll('-', '');
  for (const child of app.stage.children) {
    if (child.my_color && child.my_title && child.my_width && child.my_x && child.my_y) {
      const ret = await db.query(`INSERT INTO task_master (taskid,title,color,x,y,width,yearmonth) 
        SELECT $1,$2,$3,$4,$5,$6,$7
        WHERE NOT EXISTS ( SELECT 1 FROM task_master WHERE taskid = $1 );`,
        [child.label, child.my_title, child.my_color, child.my_x, child.my_y, child.my_width, Number(fmtYYYYMM)]);
      count = count + ret.affectedRows;
    }
  }
  if (count > 0) {
    window.alert(`${calendar_month.innerText}のTaskを${count}件保存しました`)
  }
}

calendarsave.addEventListener('click', async (e) => {
  await saveTasks();
});

document.getElementById('arrowleft').addEventListener('click', async () => {
  await saveTasks();
  const prevdt = addMonth(parse({ date: calendar_month.innerText + '-01', format: 'YYYY-MM-DD', locale: 'ja', }), -1);
  maxDays = monthDays(prevdt);
  gridHeight = cellHeight * maxDays;
  calendar_month.innerText = format({ date: prevdt, format: 'YYYY-MM', tz: 'Asia/Tokyo', });

  blurTask();
  const fmtYYYYMM = calendar_month.innerText.replaceAll('-', '');
  while (app.stage.children.length > 0) {
    app.stage.children.shift().destroy({ children: true, texture: true, textureSource: true, context: true });
  }
  await calenadarView(fmtYYYYMM);
})

document.getElementById('arrowright').addEventListener('click', async () => {
  await saveTasks();
  const nextdt = addMonth(parse({ date: calendar_month.innerText + '-01', format: 'YYYY-MM-DD', locale: 'ja', }), 1);
  maxDays = monthDays(nextdt);
  gridHeight = cellHeight * maxDays;
  calendar_month.innerText = format({ date: nextdt, format: 'YYYY-MM', tz: 'Asia/Tokyo', });

  blurTask();
  const fmtYYYYMM = calendar_month.innerText.replaceAll('-', '');
  while (app.stage.children.length > 0) {
    app.stage.children.shift().destroy({ children: true, texture: true, textureSource: true, context: true });
  }
  await calenadarView(fmtYYYYMM);
})
```

まだまだ、微妙なところはありますが、完成！💪

# 今回の成果物

ソース

https://github.com/shisojuice/canvas_calendar

DemoURL

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

# まとめ

全3記事でカレンダーアプリをPixiJSで実装してみた。

思いのほか、PGliteの処理が重たいので、時間があればSuspend処理や

シンプルにローカルストレージを使ったほうが見栄えはいいと思います。🤔

PixiJSは2Dに特化していて、学習コストも低かったので引き続き、学習していきたい。📝