シェルスクリプトマガジン

香川大学SLPからお届け!(Vol.73掲載)

著者:岩本 和真

SLPでは最近、Webブラウザで動作するリズムゲームをチームで開発しました。さまざまな曲でプレーできるように、リズムゲーム本体と並行して、ゲームで使用する譜面を作成するツールも開発しました。このツールもWebブラ
ウザで動作します。今回は、この譜面作成ツールの実装について紹介します。

シェルスクリプトマガジン Vol.73は以下のリンク先でご購入できます。

図3 最初に実行されるコード

window.onload = async function() {
  const {bpm, musicL} = getMusicInfoViaElement()
  await getWidth();
  await numberQLine(bpm, musicL);
  await setCanvas();
  await setQLine();
  await setXLine();
  await update();
  await draw();
  await scrollBottom();
}

図4 getMusicInfoViaElement()関数のコード

function getMusicInfoViaElement() {
  const bpm = document.getElementById('bpm').value;
  const musicL = document.getElementById('musicL').value;
  const musicBody = document.getElementById('musicBody').value;
  return {bpm, musicL, musicBody}
}

図5 getWidth()関数のコード

const can = document.getElementById("can2");
const ctx = can.getContext("2d");
can.setAttribute('style', 'background-color: #f6f7d7');
function getWidth() {
  return new Promise(function(resolve) {
    cst = window.getComputedStyle(can);
    canvasW = parseInt(cst.width);
    can.width = canvasW;
    resolve();
  })
}

図6 numberQLine()関数のコード

function numberQLine(bpm, musicL) {
  return new Promise(function(resolve) {
    qLineQty = Math.floor(musicL / (60 / bpm) + 1);
    resolve();
  })
}

図7 setCanvas()関数のコード

let qLineMargin = 80;
function setCanvas() {
  return new Promise(function(resolve) {
    canvasH = qLineQty * qLineMargin;
    can.height = canvasH;
    ctx.translate(0, can.height);
    ctx.scale(1, -1);
    can.style.height = canvasH * 2.5 +'px';
    resolve();
  })
}

図8 setQLine()関数のコード

function setQLine() {
  return new Promise(function(resolve) {
    for (let i = quarterLine.length; i < qLineQty; i++) {
      quarterLine[i] = new QuarterLine(i);
    }
    resolve();
  })
}

図9 QuarterLineクラスのコード

class QuarterLine {
  constructor(no) {
    this.no = no;
    this.y = qLineMargin * this.no + qLineMargin / 4;
  }
  update() {
    this.y = qLineMargin * this.no + qLineMargin / 4;
  }
  draw() {
    ctx.beginPath();
    ctx.strokeStyle = "#3ec1d3";
    ctx.lineWidth = Q_LINE_T;
    ctx.moveTo(0, Math.round(this.y));
    ctx.lineTo(canvasW, Math.round(this.y));
    ctx.stroke();
  }
}

図10 setXLine()関数のコード

function setXLine() {
  return new Promise(function(resolve) {
    for (let i = 0; i < 4; i++) {
      for (let j = xLine[i].length; j < qLineQty; j++) {
        xLine[i][j] = [];
        for (let k = 0; k < 14; k++) {
          xLine[i][j][k] = new XthLine(i, j, k);
        }
      }
    }
    resolve();
  })
}

図11 update()関数のコード

function update() {
  return new Promise(function (resolve) {
    // ノーツの縦の長さを更新
    noteH = (qLineMargin / 9 >= Q_LINE_T || divValue == 8)
             ? qLineMargin / 9 : Q_LINE_T
      // ノーツの位置と各拍、各連符の横線の位置を更新
    const laneMargin = canvasW / 5;
    laneSet = [laneMargin / 2, laneMargin * 1.5,
               laneMargin * 2.5, laneMargin * 3.5,
               laneMargin * 4.5];
    noteW = laneMargin / 3;
    quarterLine.forEach((val) => val.update() )
    editLane.forEach((val) => val.update() )
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < qLineQty; j++) {
        for (let k = 0; k < 14; k++) {
          xLine[i][j][k].update();
        }
      }
    }
    resolve();
  })
}

図12 draw()関数のコード

function draw() {
  return new Promise(function(resolve) {
    ctx.clearRect(0, 0, can.width, can.height);
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < qLineQty; j++) {
        for (let k = 0; k < 14; k++) {
          xLine[i][j][k].draw();
        }
      }
    }
    for (let i = 0; i < qLineQty; i++) {
      quarterLine[i].draw();
    }
    editLane.forEach((val) => val.draw())
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < qLineQty; j++) {
        for (let k = 0; k < 14; k++) {
          xLine[i][j][k].drawNote();
        }
      }
    }
    resolve();
  })
}

図13 draw()関数のコード

function scrollBottom() {
  return new Promise(function(resolve) {
    let target = document.getElementById('scroll');
    target.scrollTop = target.scrollHeight;
    resolve();
  }
)

図14 レーン上のノーツをマウスクリックで制御するためのコード

let mouseDown;
can.addEventListener('mousedown', async function(e) {
  mouseDown = true;
  await pos(e);
  await update();
  await draw();
});
can.addEventListener('mouseup', function(e) {
  mouseDown = false;
});
function pos(e) {
  return new Promise(function(resolve) {
    mouseDownX = (e.clientX -
                  can.getBoundingClientRect().left);
    mouseDownY = -(e.clientY -
                   can.getBoundingClientRect().bottom) / 2.5;
    resolve();
  })
}

図15 スライダ機能のコード

let canScale = document.getElementById('canScale');
canScale.onchange = async function() {
  qLineMargin = this.value;
  await setCanvas();
  await update();
  await draw();
  await scrollBottom();
}

図16 apply()関数のコード

async function apply() {
  const {bpm, musicL} = getMusicInfoViaElement()
  await numberQLine(bpm, musicL);
  await setCanvas();
  await setQLine();
  await setXLine();
  await update();
  await draw();
  return Promise.resolve();
}

図17 convert()関数のコード

function calcNote(jnoteValue, calced, musicBody) {
  return Math.round((jnoteValue+calced+musicBody*1000)*100)/100;
}
async function convert() {
  await apply();
  const fileName = document.getElementById('fileName').value;
  const speed = document.getElementById("speed").value;
  const {bpm, musicL, musicBody} = getMusicInfoViaElement()
  const {noteValue, note32Value, note6Value} = calcNoteValue(bpm)
  const outInfo = Array()
  xLine.forEach((val1, i) => {
    val1.forEach((val2, j) => {
      val2.forEach((val3, k) => {
        if (val3.note) {
          const tmp = k < 8 ? calcNote(j*noteValue, k*note32Value, musicBody)
                      : calcNote(j*noteValue, k%8*note6Value, musicBody)
          outInfo.push(Array(1, i+1, speed, tmp))
        }
      })
   })
 })
 createAndDownloadCsv(fileName, outInfo, bpm, musicL, musicBody);
}