Bezier描画用サンプル作ってみた

この記事はZOZOテクノロジーズ #4 Advent Calendar 2020 5日目です。
昨日は[kotatsu360](https://qiita.com/kotatsu360)さんによる[マルチリージョン・マルチアカウントで発生するAWSイベントをChatBotでまるっと通知する](https://qiita.com/kotatsu360/items/76a448f4d0f99273d7b4)でした。

## 背景
お絵かきや製図、レイアウト関係のアプリ等々、html canvasのような図形描画領域をガンガン使ったアプリ作りたいと思った時にどんな構成でプロトタイピングしていけば良いのか気になったため、3つの環境でサンプル作って試してみました。
世の中のあらゆる技術の中からベストなものを選定するというよりは、いい機会なので触ったことがないものや最近触ってないものを中心に興味駆動で扱ってみました。

対象としたのは以下の3つです。

| 対象| version | 期待したことや事前のイメージ |
|:-:|:-:|:--|
| Vue.js | 2.6.11   | ・一般的なWebのGUIが一通り実装できる<br>・情報も山ほどあって安心<br>・webアプリとしてそのままリリースできる |
| Rust (Web­Assembly) | 1.48.0  | ・html側の挙動に引っ張られずにcanvas操作のロジックをサクサクかけそう<br>・挙動が速そう<br>・コンパイラが賢そう |
| Processing  | 3.5.4  | ・canvasではないが、図形の描画に集中できそう<br>・コードがシンプルでコンパクトになりそう<br>・学習コスト低そう |

## 作ったもの
三次ベジェ曲線を描画して頂点をマウスで操作できるシンプルなものを作ってみました。
![bezier_sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322248/9274186d-aa23-7c7e-04aa-249ceabcdb54.gif)

## 環境構築とサンプルコード

### Vue.js
環境構築は[こちら](https://qiita.com/reflet/items/16f90cbb0db68fd6a498)を参考にさせていただきました。
#### サンプルコード
立ち上げたサンプルプロジェクトのComponentsにサンプルファイルを追加します。

```vuejs:Sample.vue
<template lang='pug'>
div
  canvas#canvas(@mousedown='down', @mouseup='up', @mouseout='up', @mousemove='move')
</template>

<script>
export default {
  data() {
    return {
      points: [
        {
          x: 10,
          y: 10
        },
        {
          x: 100,
          y: 30
        },
        {
          x: 200,
          y: 30
        },
        {
          x: 300,
          y: 10
        }
      ],
    }
  },
  mounted() {
    this.dragging = false
    this.pointWidth = 10
    this.draw()
  },
  methods: {
    draw: function() {
      this.canvas = document.getElementById('canvas')
      this.canvas.width = 640
      this.canvas.height = 480
      document.body.appendChild(this.canvas)
      this.ctx = this.canvas.getContext('2d')
      const w = this.pointWidth

      // draw handles
      this.drawHandle(this.points[0], this.points[1]);
      this.drawHandle(this.points[3], this.points[2]);

      // draw points
      this.points.forEach(p => {this.ctx.fillRect(p.x-w/2, p.y-w/2, w, w)})

      // draw bezier curve
      this.ctx.beginPath();
      this.ctx.moveTo(this.points[0].x, this.points[0].y);
      this.ctx.bezierCurveTo(this.points[1].x, this.points[1].y, this.points[2].x, this.points[2].y, this.points[3].x, this.points[3].y);
      this.ctx.stroke();
    },

    drawHandle(from, to) {
    this.ctx.beginPath();
    this.ctx.moveTo(from.x, from.y);
    this.ctx.lineTo(to.x, to.y);
    this.ctx.stroke();
    },

    down: function(e) {
      var offsetX = this.canvas.getBoundingClientRect().left
      var offsetY = this.canvas.getBoundingClientRect().top

      const x = e.clientX - offsetX
      const y = e.clientY - offsetY
      this.points.forEach(p => {this.objDrag(p, x, y)})
    },
    
    objDrag: function(point, x, y) {
      const w = this.pointWidth
      if (point.x-w/2 < x && (point.x + w/2) > x && point.y-w/2 < y && (point.y + w/2) > y) {
        this.dragging = true
        this.target = point
        this.relX = this.target.x - x
        this.relY = this.target.y - y
      }
    },

    move: function(e) {
      var offsetX = this.canvas.getBoundingClientRect().left
      var offsetY = this.canvas.getBoundingClientRect().top

      const x = e.clientX - offsetX
      const y = e.clientY - offsetY

      if (this.dragging) {
        this.target.x = x + this.relX
        this.target.y = y + this.relY
        this.draw()
      }
    },

    up: function() {
      this.dragging = false
    }
  }
}
</script>
```

### Rust
環境構築は[こちら](https://qiita.com/tiohsa/items/caa923e4efb689e1250b?)を参考にさせていただきました。

#### サンプルコード
wasm-bindgenの[Simple Paint Program](https://rustwasm.github.io/wasm-bindgen/examples/paint.html)を参考に、Bezier曲線を描画できるよう編集しています。

```rust:lib.rs
use std::{ rc::Rc, cell::{Cell, RefCell} };
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

#[derive(Debug, Clone)]
struct Point {
    x: f64,
    y: f64
}

#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
    const CANVAS_W: f64 = 640.0;
    const CANVAS_H: f64 = 480.0;
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document
        .create_element("canvas")?
        .dyn_into::<web_sys::HtmlCanvasElement>()?;
    document.body().unwrap().append_child(&canvas)?;
    canvas.set_width(CANVAS_W as u32);
    canvas.set_height(CANVAS_H as u32);
    canvas.style().set_property("border", "solid")?;
    let context = canvas
        .get_context("2d")?
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()?;
    let context = Rc::new(context);
    const W: f64 = 10.0; // width for point rect
    let id: Option<usize> = None; // index of dragged point 
    let id = Rc::new(Cell::new(id));
    let points = vec![Point{x: 10.0, y: 10.0},Point{x: 100.0, y: 30.0},Point{x: 200.0, y: 30.0},Point{x: 300.0, y: 10.0}];
    draw(points.clone(), W, context.clone());
    let points = Rc::new(RefCell::new(points));
    {
        let context = context.clone();
        let id = id.clone();
        let points = points.clone();
        let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
            context.begin_path();
            let mouse_x = event.offset_x() as f64;
            let mouse_y = event.offset_y() as f64;
            for (i, p) in points.borrow().iter().enumerate() {
                if mouse_x - W/2.0 < p.x && p.x <  mouse_x + W/2.0 && mouse_y - W/2.0 < p.y && p.y < mouse_y + W/2.0 {
                    id.set(Some(i));
                }
            }
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
        closure.forget();
    }
    {
        let context = context.clone();
        let id = id.clone();
        let points = points.clone();
        let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
            if id.get() != None {
                context.clear_rect(0.0, 0.0, CANVAS_W, CANVAS_H);
                let mouse_x = event.offset_x() as f64;
                let mouse_y = event.offset_y() as f64;
                points.borrow_mut()[id.get().unwrap()] = Point{x: mouse_x, y: mouse_y};
                draw(points.borrow().to_vec(), W, context.clone());
            }
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())?;
        closure.forget();
    }
    {
        let closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
            if id.get() != None {
                id.set(None);
            }
        }) as Box<dyn FnMut(_)>);
        canvas.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref())?;
        closure.forget();
    }

    Ok(())
}

fn draw(points: Vec<Point>, w: f64, context: Rc<web_sys::CanvasRenderingContext2d> ) {
    //draw curve handles
    draw_handle(points[0].clone(), points[1].clone(), context.clone());
    draw_handle(points[3].clone(), points[2].clone(), context.clone());

    //draw points
    for p in points.iter() {
        context.fill_rect(p.x - w/2.0, p.y - w/2.0, w, w);
    }

    //draw bezier curve
    context.begin_path();
    context.move_to(points[0].x,points[0].y);
    context.bezier_curve_to(points[1].x,points[1].y,points[2].x,points[2].y,points[3].x,points[3].y);
    context.stroke();
}

fn draw_handle(from: Point, to: Point, context: Rc<web_sys::CanvasRenderingContext2d>) {
    context.begin_path();
    context.move_to(from.x, from.y);
    context.line_to(to.x, to.y);
    context.stroke();
}
```
### Processing
環境構築は[こちら](https://qiita.com/greatnekochan/items/010237ab3313757d0b0b)を参考にさせていただきました。

#### サンプルコード
仕様がシンプルで、[公式のReference](https://processing.org/reference/)が丁寧でわかりやすいです。

```java:SimpleBezier.pde

float POINT_WIDTH = 10;
Point points[] ={new Point(10.0, 10.0), new Point(100.0, 30.0), new Point(200.0, 30.0), new Point(300.0, 10.0)};
int dragPointId = -1;

class Point {
    float x, y;
    Point(float x, float y) {
        this.x = x;
        this.y = y;
    }
}

void setup() {
    size(640, 480); 
}

void draw() {
    background(255);
    drawHandle(points[0], points[1]);
    drawHandle(points[3], points[2]);
    drawPoints();
    drawBezier();
}

void drawHandle(Point from, Point to) {
    noFill();
    stroke(0);
    line(from.x, from.y, to.x, to.y);
}

void drawPoints() {
    fill(0);
    noStroke();
    float pw = POINT_WIDTH;
    for (Point p: points) {
        square(p.x-pw/2, p.y-pw/2, pw);
    }
}

void drawBezier() {
    noFill();
    stroke(0);
    bezier(points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y, points[3].x, points[3].y);
}

void mouseDragged() {
    for (int i=0; i<points.length; i++) {
        float pw = POINT_WIDTH;
        Point p = points[i];
        if (
            p.x - pw/2 < mouseX &&
            p.x + pw/2 > mouseX &&
            p.y - pw/2 < mouseY &&
            p.y + pw/2 > mouseY
        ) {
            dragPointId = i;
        }
    }
    if (dragPointId != -1) {
        points[dragPointId].x = mouseX;
        points[dragPointId].y = mouseY;
    }
}

void mouseReleased() {
    dragPointId = -1;
}
```

## 所感
上記のサンプル作成後もしばらくいじってみた所感です。
### Vue.js
- 情報が多いので基本的なことは悩まなくて済む。
- canvasへの描画に関してはコードが冗長になりがちだが、それ以外のUIコンポーネントとの連携はスムーズで機能拡張しやすい。
- 型の扱いが柔軟（もっと硬く書きたければTSに切り替え可能という意味で）。

### Rust
- 暗黙的イミュータブルや借用周りでエラーがめっちゃ出たけどエラーメッセージが親切。エラー出しながら学習しやすい。
- canvas操作も特にRustを意識せずに書ける。js用のメソッド名をスネークケースに変えればだいたい動く。
- ホットリロードもVue.jsと遜色なく快適。コンパイルのストレスも特に感じない。（コードの量にもよるかも）

### Processing
- 描画に集中でき、描画用のメソッドも一式揃っているのでコードがシンプル。
- ベース言語がJavaなので足りなければJavaのライブラリをインポートしてどうにでもなる。
- 拡張機能[controlP5](http://www.sojamo.de/libraries/controlP5/)でちょっとしたUIコンポーネントなら追加できる。

## おわりに
それぞれの技術スタックで図形描画周りを操作するとどんな実装になるのか確認することができました。
今回のようなシンプルな処理では実行速度の大きな違いは見られませんでしたが、より負荷の高い処理でwasmがどれだけ早いのか、C++やgoのサンプルとも比較してみたい思います。それはまたの機会に。

明日は[ahiru](https://qiita.com/ahiru___)さんによるMetricKit入門です！