# 卒業研究を支える技術 - 閉曲線データを Svelte で生成

こんにちは、@jabelic (opens new window)です。 本稿はNCC Advent Calendar 2021 (opens new window) の 4 日目の記事です。 私の卒業研究ではレベルセット法のための符合付き距離関数の生成を行っています。符合付き距離関数と言うのは以下のようなサブドメインΩ\Omega内の任意の点から点の集合で表される閉曲線Γ\Gammaに対する最短距離を算出するものです。Ω\Omega内の任意の点が閉曲線Γ\Gammaの内側にあれば負として表すと、符合付き距離関数となります。その逆でも良いです。

そこで必要になるのが閉曲線のデータです。要件は以下の通りです。

  • CSV
  • 2 次元ユークリッド空間で定義された閉曲線
  • 座標は x, y 軸共に[1,1][-1,1]で正規化されている
  • 複数の閉曲線や八の字の閉曲線が欲しい
  • 閉曲線は単純でなくて良い(自己交差を許す)

これを満たす選択肢としては、以下の二つを考えました

  • 閉曲線の関数を用いて、配列から CSV データを生成する
  • お絵かきボードを作って js で CSV データを生成する

前者は確かに閉曲線を表す方程式を使って正確に閉曲線データを生成することができますが、たくさんの種類のデータを生成するにはたくさんの方程式が必要です。

そこで、平凡ではありますがお絵かきボードを作ることにしました。データが閉曲線であることを満たすには、お絵かきボード上でちゃんと丸になるように書けば良いです。

プロダクトの仕様としては、1 色のお絵かきボードで、お絵かきした内容を CSV データとしてダウンロードできるようにしました。技術は Svelte.js を使うことにしました。

# お絵かきボードを Svelte.js で作成

<script></script>
<main>
  <canvas
    id="draw-area"
    width="600px"
    height="400px"
    style="border: 1px solid #000000;"
  />
  <button class="button" on:click="{coordinatesToCSV}">convert!!</button>
</main>
<style></style>

これが Svelte コンポーネント(.svelte)の基本構成です。

<main>にはcanvasbuttonを書いています。buttonを押すとcoordinatesToCSVメソッドが発火して、canvas上にお絵かきした曲線のデータをダウンロードします。

Svalte.js は Vue.js と違って、ルートに複数の Tag を置くことができます。

# canvas の描画機能を実装

描画機能の script は以下の通りです。

import { onMount } from "svelte";

onMount(async () => {
  const canvas = document.querySelector("#draw-area");
  const context = canvas.getContext("2d");
  canvas.addEventListener("mousedown", (event) => dragStart(event, context));
  canvas.addEventListener("mouseup", (event) => dragEnd(event, context));
  canvas.addEventListener("mouseout", (event) => dragEnd(event, context));
  canvas.addEventListener("mousemove", (event) => {
    draw(event.layerX, event.layerY, context);
  });
});
let isDragging = false;
const lastPosition = { x: null, y: null };
const dragStart = (event, context) => {
  console.debug("drag start");
  context.beginPath(); // pathをクリアする
  isDragging = true;
};

const dragEnd = (event, context) => {
  console.debug("drag end");
  context.closePath();
  isDragging = false;
  lastPosition.x = null;
  lastPosition.y = null;
};

/** x, yともにcanvas領域の半分の長さ */
const boardSize = { x: 300, y: 200 };

const draw = (x, y, context) => {
  if (!isDragging) return;
  context.lineCap = "round";
  context.lineJoin = "round";
  context.lineWidth = 2;
  context.strokeStyle = "#000000";
  if (lastPosition.x === null || lastPosition.y === null) {
    context.moveTo(x, y);
  } else {
    context.moveTo(lastPosition.x, lastPosition.y);
  }
  /** 線を書く */
  context.lineTo(x, y);
  context.stroke();
  /** 現在の座標(x, y)を配列に格納する */
  addCoordinate(x - boardSize.x, y - boardSize.y);
  /** 位置情報を更新する */
  lastPosition.x = x;
  lastPosition.y = y;
};
const coordinates = [];
const addCoordinate = (x, y) => {
  x /= boardSize.x;
  y /= boardSize.y;
  coordinates.push([x, -y]);
};

# CSV 形式でデータをダウンロード

さらにcoordinates配列の座標情報を CSV 形式に変換するために以下を定義しています。

const coordinatesToCSV = () => {
  let csvContent = "data:text/csv;charset=utf-8,";
  coordinates.forEach((rowArray) => {
    let row = rowArray.join(",");
    csvContent += row + "\r\n";
  });
  let encodedUri = encodeURI(csvContent);
  window.open(encodedUri);
};

csv に変換するためには、接頭辞に"data:text/csv;charset=utf-8,"として その後に続くデータを1,2\r\n3,4\r\n4,5\r\nとすると、

csvContent変数は"data:text/csv;charset=utf-8,1,2\r\n3,4\r\n4,5\r\n"となります。

これをencodeURIメソッドに渡すと

 1,2
 3,4
 4,5

と言うデータが生成されます。 さらにそれを Window インターフェイスの open() メソッドに渡すと、CSV ファイルをダウンロードできます。 このcoordinatesToCSVメソッドをボタンで発火させれば完成です。

完成版
<script>
  import { onMount } from "svelte";

  onMount(async () => {
    const canvas = document.querySelector("#draw-area");
    const context = canvas.getContext("2d");
    canvas.addEventListener("mousedown", (event) => dragStart(event, context));
    canvas.addEventListener("mouseup", (event) => dragEnd(event, context));
    canvas.addEventListener("mouseout", (event) => dragEnd(event, context));
    canvas.addEventListener("mousemove", (event) => {
      draw(event.layerX, event.layerY, context);
    });
  });
  let isDragging = false;
  const lastPosition = { x: null, y: null };
  const dragStart = (event, context) => {
    console.debug("drag start");
    context.beginPath();
    isDragging = true;
  };

  const dragEnd = (event, context) => {
    console.debug("drag end");
    context.closePath();
    isDragging = false;
    lastPosition.x = null;
    lastPosition.y = null;
  };

  const boardSize = { x: 300, y: 200 };
  const draw = (x, y, context) => {
    if (!isDragging) return;
    context.lineCap = "round";
    context.lineJoin = "round";
    context.lineWidth = 2;
    context.strokeStyle = "#000000";
    if (lastPosition.x === null || lastPosition.y === null) {
      context.moveTo(x, y);
      addCoordinate(x - boardSize.x, y - boardSize.y);
    } else {
      context.moveTo(lastPosition.x, lastPosition.y);
      addCoordinate(x - boardSize.x, y - boardSize.y);
    }
    context.lineTo(x, y);
    context.stroke();
    lastPosition.x = x;
    lastPosition.y = y;
  };
  const coordinates = [];
  const addCoordinate = (x, y) => {
    x /= boardSize.x;
    y /= boardSize.y;
    coordinates.push([x, -y]);
  };
  //   https://www.delftstack.com/ja/howto/javascript/export-javascript-csv/
  const coordinatesToCSV = () => {
    let csvContent = "data:text/csv;charset=utf-8,";
    coordinates.forEach((rowArray) => {
      let row = rowArray.join(",");
      csvContent += row + "\r\n";
    });
    let encodedUri = encodeURI(csvContent);
    window.open(encodedUri);
  };
</script>

<main>
  <canvas
    id="draw-area"
    width="600px"
    height="400px"
    style="border: 1px solid #000000;"
  />
  <button class="button" on:click="{coordinatesToCSV}">convert!!</button>
</main>

<style></style>

# 終わりに

svelte を使った意味はほぼありませんが、将来の拡張に備えてリッチな技術選択をしたことは間違ってはいないと思っています。

Last Updated: 12/6/2021, 7:43:16 PM