1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptAdvent Calendar 2024

Day 16

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

Last updated at Posted at 2024-12-05

はじめに

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

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

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

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関数で表示する。

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

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

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

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

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

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

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

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

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

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

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

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

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

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);
  });
}
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周辺ロジックで完成です!ラストスパート頑張ります!

次回につづく。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?