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

# はじめに

#1での(下記)、環境作成と初期設定の続きから行う。(全3記事を予定)

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

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

初期設定した年月を受け取りcalenadarView関数でカレンダーを初期表示する。

```javascript:main.js
 window.onload = async () => {
  //　略
  document.getElementById('appmain').appendChild(app.canvas);
  const fmtYYYYMM = calendar_month.innerText.replaceAll('-', '');
  await calenadarView(fmtYYYYMM);
}

```

calenadarView関数で左サイドの日付を1日からその月の末日まで表示する。

task表示するメインのCanvas部では、

該当の年月に紐づいたLocal上のTaskのデータがあればその数だけTaskをcreateTask関数で表示する。

```javascript:main.js

/**
 * カレンダーを初期表示する関数
 * @param {string} strYYYYMM - 表示する年月
 */
const calenadarView = async (strYYYYMM) => {
  // 左サイドに日を記載
  leftcanvas.style.height = gridHeight + 'px';
  leftcanvas.height = gridHeight;
  leftcanvas.width = cellWidth;
  const leftctx = leftcanvas.getContext('2d');
  leftctx.font = `20px monospace`;
  leftctx.fillStyle = 'lightgray';
  let y = 0;
  for (let i = 1; i <= cellHeightCount; i++) {
    y += cellHeight
    const buffer = 10;
    const day = format({ date: strYYYYMM.substring(0, 4) + '-' + strYYYYMM.substring(4, 6) + '-' + String(i).padStart(2, '0'), format: 'DDddd', tz: 'Asia/Tokyo', })
    leftctx.fillText(day, buffer, y - buffer);
  }
  document.getElementById('timemaincanvas').style.height = gridHeight + 'px';
  // re create
  const ret = await db.query(`SELECT * from task_master WHERE yearmonth = $1;`, [Number(strYYYYMM)]);
  for (const row of ret.rows) {
    await createTask(row.taskid, Number(row.x), Number(row.y), Number(row.width), row.color, row.title);
  }
}

```

UUID,表示位置x,y,表示幅,表示色,表示文字を受け取りカレンダー上にTaskを作成し表示する関数を作成する。

* correctPositionの関数を使い、表示位置x,y,表示幅がカレンダーアプリの罫線上で収まるように補正しておく。

![Canvas要素でカレンダーアプリを作ってみる2_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/8eaaf45d-b767-cdf0-2eac-d462c08b91dc.gif)

* 表示文字はgetBoundsを使い、文字がObject外へ飛び出して見えないように調整。

![Canvas要素でカレンダーアプリを作ってみる2_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/8933c3a7-9523-0ab2-667c-f49abbaa51df.gif)

* ObjectのHover時にtitleが表示されるようにイベントを付与。

![Canvas要素でカレンダーアプリを作ってみる2_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/28c243c3-14a8-b2a4-17c6-c22101bdc1f2.gif)

* ユーザーがObjectをFocus選択できるようにpointerdownでFocusTask関数を使い、あたかもFocusしているように見せる。

![Canvas要素でカレンダーアプリを作ってみる2_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/d5170efd-c2e1-077f-07b3-2e6b0e541b6f.gif)

* Resize時にResizeできるようにResizerをObjectの左右両端に配置。

![Canvas要素でカレンダーアプリを作ってみる2_5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/6bee16cd-3b45-86b9-357c-4cce3a4efe31.gif)

* createdistinctTaskの関数を使い、Task同士が重なり合っていれば赤い警告を表示させる。

![Canvas要素でカレンダーアプリを作ってみる2_6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/5363051a-0a95-61fd-4160-0a972bc64ebf.gif)

* PGliteでLocalのテーブル上に今回作成したTaskを登録しておく。

![Canvas要素でカレンダーアプリを作ってみる2_7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3777149/3e947d0f-0da4-2007-f91e-82bcfdc78073.jpeg)

```javascript:main.js
/**
 * TaskObjectをCreateする関数
 * @param {string} name - 一意の名前(UUID)
 * @param {number} objx - 表示位置x
 * @param {number} objy - 表示位置y
 * @param {number} objw - 表示幅w
 * @param {string} c - 表示色
 * @param {string} title - 表示文字
 */
const createTask = async (name, objx, objy, objw, c, title) => {
  const { x, y, w } = correctPosition(objx, objy, objw, objh);
  // Obj本体
  let obj = new PIXI.Graphics()
    .rect(x, y, w, objh)
    .fill(c);
  obj.label = name + _Graphics;
  obj.interactive = true;
  obj.buttonMode = true;
  // Obj Resize
  let objL = new PIXI.Graphics()
    .rect(x, y, 4, objh)
    .fill(c);
  objL.label = name + _ResizeL;
  objL.interactive = true;
  objL.buttonMode = true;
  objL.cursor = 'col-resize';
  let objR = new PIXI.Graphics()
    .rect(x + w - 4, y, 4, objh)
    .fill(c);
  objR.label = name + _ResizeR;
  objR.interactive = true;
  objR.buttonMode = true;
  objR.cursor = 'col-resize';
  // TextView作成
  const txt = title === '' ? defaulttitle : title;
  let text = new PIXI.Text({
    text: txt,
    style: new PIXI.TextStyle({
      fontFamily: 'monospace',
      fontSize: objh,
      fill: 0xffffff,
      wordWrapWidth: w,
    })
  })
  text.label = name + _Text;
  text.x = x + 4;
  text.y = y;
  text.interactive = true;
  text.buttonMode = true;
  const bounds = text.getBounds();
  if (w < bounds.width) {
    let temptext = '';
    for (let i = 0; i < w / (cellHeight / 4) - 2; i++) {
      temptext += text.text[i];
    }
    text.text = temptext
  }
  // コンテナ作成
  const container = new PIXI.Container();
  container.zIndex = 1;
  container.label = name;
  container.my_x = x;
  container.my_y = y;
  container.my_width = w;
  container.my_color = c;
  container.my_title = txt;
  container.normalobj = true;
  container.addChild(obj);
  container.addChild(objL);
  container.addChild(objR);
  container.addChild(text);
  app.stage.addChild(container);
  const fmtYYYYMM = calendar_month.innerText.replaceAll('-', '');
  createdistinctTask(name); // Obj同士の重なりを確認
  focusTask(name); // 初期Focus
  obj.on('pointerover', (e) => { txthover(txt); });
  text.on('pointerover', (e) => { txthover(txt); });
  objL.on('pointerover', (e) => { txthover(txt); });
  objR.on('pointerover', (e) => { txthover(txt); });
  obj.on('pointerdown', (e) => {
    draggingTask = container;
    // rect＿topleftの距離
    const currentPos = e.data.global;
    draggingTask.my_dragx = currentPos.x - x;
    focusTask(name);
  });
  text.on('pointerdown', (e) => {
    draggingTask = container;
    // rect＿topleftの距離
    const currentPos = e.data.global;
    draggingTask.my_dragx = currentPos.x - x;
    focusTask(name);
  });
  objL.on('pointerdown', (e) => {
    resizingTask = container;
    resizingTask.my_lr = 'L';
    focusTask(name);
  });
  objR.on('pointerdown', (e) => {
    resizingTask = container;
    resizingTask.my_lr = 'R';
    focusTask(name);
  });
}
```

```javascript:main.js
/**
 * TaskObjectの位置を補正する関数
 * @param {number} x - 位置x
 * @param {number} y - 位置y
 * @param {number} w - width
 * @param {number} h - height
 * @returns {{x: number, y: number, w: number}} 補正された位置情報オブジェクト。
 */
const correctPosition = (x, y, w, h) => {
  // check if the object is out of bounds (x,y)
  let objX = x < 0 ? 0 : x;
  let objY = y < 0 ? 0 : y + h > gridHeight ? gridHeight - h : y;
  // check if the object is correcting (x,y)
  objX = objX - (objX % (cellWidth / 12));
  objY = objY - (objY % cellHeight) + padtoph;
  // check if the object is out of bounds width
  let objW = w <= cellWidth / 12 ? cellWidth / 12 : x + w > cellWidth * maxtime ? cellWidth * maxtime - x : w;
  // check if the object is correcting width
  objW = objW - (objW % (cellWidth / 12));
  return { x: objX, y: objY, w: objW };
}
/**
 * TaskObject内のTextをHover時にTitleを表示する関数
 * @param {string} txt
 */
const txthover = (txt) => {
  document.getElementById('appmain').title = txt;
  setTimeout(() => {
    document.getElementById('appmain').title = '';
  }, 1000)
}
/**
 * TaskObjectをDistinctを表示する関数
 * @param {string} name - 一意の名前
 */
const createdistinctTask = (name) => {
  const targetcontainer = app.stage.getChildByLabel(name);
  const targetchild = targetcontainer.getChildByLabel(name + _Graphics);
  app.stage.children.forEach(container => {
    const lbl = container.label
    if (lbl != name && container.normalobj) {
      if (targetcontainer.my_y == container.my_y) {
        const a1 = targetcontainer.my_x;
        const a2 = targetcontainer.my_x + targetcontainer.my_width;
        const b1 = container.my_x;
        const b2 = container.my_x + container.my_width;
        const overlap = Math.min(a2, b2) - Math.max(a1, b1)
        if (overlap > 0) {
          let objD = app.stage.getChildByLabel(name + lbl + _Distinct);
          if (objD) {
            objD.destroy({ children: true, texture: true, textureSource: true, context: true });
          }
          objD = new PIXI.Graphics()
            .rect(Math.max(a1, b1), container.my_y - padtoph, overlap, cellHeight)
            .fill(0xff0000);
          objD.label = name + lbl + _Distinct;
          objD.my_distinct_ids = [name, lbl];
          objD.interactive = true;
          objD.buttonMode = true;
          app.stage.addChild(objD);
          objD.on('pointerover', (e) => { txthover(name + '\r\n' + lbl); });
        }
      }
    }
  })
}
/**
 * TaskObjectをDistinctを削除する関数
 * @param {string} name - 一意の名前
 */
const deletedistinctTask = (name) => {
  app.stage.children.forEach(objD => {
    if (objD.my_distinct_ids) {
      if (objD.my_distinct_ids.includes(name)) {
        if (objD) {
          objD.destroy({ children: true, texture: true, textureSource: true, context: true });
        }
      }
    }
  })
}
/**
 * TaskObjectをFocusする関数
 * @param {string} name - 一意の名前
 */
const focusTask = (name) => {
  blurTask();
  let targetcontainer = app.stage.getChildByLabel(name);
  if (targetcontainer) {
    // Obj Focus
    let objF = new PIXI.Graphics()
      .rect(targetcontainer.my_x, targetcontainer.my_y + objh, targetcontainer.my_width, 3)
      .fill(0xffff00);
    objF.my_id = name;
    const container = new PIXI.Container();
    container.zIndex = 2;
    container.label = focusGraphics;
    container.my_id = name;
    container.addChild(objF);
    app.stage.addChild(container);
    targetcontainer.zIndex = 2;
    app.stage.children.forEach(container => {
      const lbl = container.label
      if (lbl != name && lbl && container.normalobj) {
        container.zIndex = 1;
      }
    })
    // focus color&title
    taskchange.disabled = false;
    taskdelete.disabled = false;
    taskchange.dataset.taskid = name;
    taskdelete.dataset.taskid = name;
    tasktitle.disabled = false;
    taskcolor.disabled = false;
    tasktitle.value = targetcontainer.my_title;
    taskcolor.value = targetcontainer.my_color;
  }
}
/**
 * TaskObjectをBlurする関数
 */
const blurTask = () => {
  let objF = app.stage.getChildByLabel(focusGraphics);
  if (objF) {
    objF.destroy({ children: true, texture: true, textureSource: true, context: true });
    // blur color&title
    taskchange.disabled = true;
    taskdelete.disabled = true;
    taskchange.dataset.taskid = '';
    taskdelete.dataset.taskid = '';
    tasktitle.disabled = true;
    taskcolor.disabled = true;
    tasktitle.value = '';
    taskcolor.value = '#000000';
  }
}
/**
 * TaskObject内のTextをHover時にTitleを表示する関数
 * @param {string} txt
 */
const txthover = (txt) => {
  document.getElementById('appmain').title = txt;
  setTimeout(() => {
    document.getElementById('appmain').title = '';
  }, 1000)
}
/**
 * TaskObjectをDistinctを表示する関数
 * @param {string} name - 一意の名前
 */
const createdistinctTask = (name) => {
  const targetcontainer = app.stage.getChildByLabel(name);
  const targetchild = targetcontainer.getChildByLabel(name + _Graphics);
  app.stage.children.forEach(container => {
    const lbl = container.label
    if (lbl != name && container.normalobj) {
      if (targetcontainer.my_y == container.my_y) {
        const a1 = targetcontainer.my_x;
        const a2 = targetcontainer.my_x + targetcontainer.my_width;
        const b1 = container.my_x;
        const b2 = container.my_x + container.my_width;
        const overlap = Math.min(a2, b2) - Math.max(a1, b1)
        if (overlap > 0) {
          let objD = app.stage.getChildByLabel(name + lbl + _Distinct);
          if (objD) {
            objD.destroy({ children: true, texture: true, textureSource: true, context: true });
          }
          objD = new PIXI.Graphics()
            .rect(Math.max(a1, b1), container.my_y - padtoph, overlap, cellHeight)
            .fill(0xff0000);
          objD.label = name + lbl + _Distinct;
          objD.my_distinct_ids = [name, lbl];
          objD.interactive = true;
          objD.buttonMode = true;
          app.stage.addChild(objD);
          objD.on('pointerover', (e) => { txthover(name + '\r\n' + lbl); });
        }
      }
    }
  })
}
/**
 * TaskObjectをDistinctを削除する関数
 * @param {string} name - 一意の名前
 */
const deletedistinctTask = (name) => {
  app.stage.children.forEach(objD => {
    if (objD.my_distinct_ids) {
      if (objD.my_distinct_ids.includes(name)) {
        if (objD) {
          objD.destroy({ children: true, texture: true, textureSource: true, context: true });
        }
      }
    }
  })
}
/**
 * TaskObjectをFocusする関数
 * @param {string} name - 一意の名前
 */
const focusTask = (name) => {
  blurTask();
  let targetcontainer = app.stage.getChildByLabel(name);
  if (targetcontainer) {
    // Obj Focus
    let objF = new PIXI.Graphics()
      .rect(targetcontainer.my_x, targetcontainer.my_y + objh, targetcontainer.my_width, 3)
      .fill(0xffff00);
    objF.my_id = name;
    const container = new PIXI.Container();
    container.zIndex = 2;
    container.label = focusGraphics;
    container.my_id = name;
    container.addChild(objF);
    app.stage.addChild(container);
    targetcontainer.zIndex = 2;
    app.stage.children.forEach(container => {
      const lbl = container.label
      if (lbl != name && lbl && container.normalobj) {
        container.zIndex = 1;
      }
    })
    // focus color&title
    taskchange.disabled = false;
    taskdelete.disabled = false;
    taskchange.dataset.taskid = name;
    taskdelete.dataset.taskid = name;
    tasktitle.disabled = false;
    taskcolor.disabled = false;
    tasktitle.value = targetcontainer.my_title;
    taskcolor.value = targetcontainer.my_color;
  }
}
/**
 * TaskObjectをBlurする関数
 */
const blurTask = () => {
  let objF = app.stage.getChildByLabel(focusGraphics);
  if (objF) {
    objF.destroy({ children: true, texture: true, textureSource: true, context: true });
    // blur color&title
    taskchange.disabled = true;
    taskdelete.disabled = true;
    taskchange.dataset.taskid = '';
    taskdelete.dataset.taskid = '';
    tasktitle.disabled = true;
    taskcolor.disabled = true;
    tasktitle.value = '';
    taskcolor.value = '#000000';
  }
}

```



# まとめ

今回は、メインのロジックとなるTaskCreateの関数やTask上のイベント付与まで行った。

次回のD&D周辺ロジックで完成です！ラストスパート頑張ります！

次回につづく。

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