この記事はZOZOテクノロジーズ #4 Advent Calendar 2020 5日目です。
昨日はkotatsu360さんによるマルチリージョン・マルチアカウントで発生するAWSイベントをChatBotでまるっと通知するでした。
背景
お絵かきや製図、レイアウト関係のアプリ等々、html canvasのような図形描画領域をガンガン使ったアプリ作りたいと思った時にどんな構成でプロトタイピングしていけば良いのか気になったため、3つの環境でサンプル作って試してみました。
世の中のあらゆる技術の中からベストなものを選定するというよりは、いい機会なので触ったことがないものや最近触ってないものを中心に興味駆動で扱ってみました。
対象としたのは以下の3つです。
対象 | version | 期待したことや事前のイメージ |
---|---|---|
Vue.js | 2.6.11 | ・一般的なWebのGUIが一通り実装できる ・情報も山ほどあって安心 ・webアプリとしてそのままリリースできる |
Rust (WebAssembly) | 1.48.0 | ・html側の挙動に引っ張られずにcanvas操作のロジックをサクサクかけそう ・挙動が速そう ・コンパイラが賢そう |
Processing | 3.5.4 | ・canvasではないが、図形の描画に集中できそう ・コードがシンプルでコンパクトになりそう ・学習コスト低そう |
作ったもの
三次ベジェ曲線を描画して頂点をマウスで操作できるシンプルなものを作ってみました。
環境構築とサンプルコード
Vue.js
環境構築はこちらを参考にさせていただきました。
サンプルコード
立ち上げたサンプルプロジェクトのComponentsにサンプルファイルを追加します。
<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
環境構築はこちらを参考にさせていただきました。
サンプルコード
wasm-bindgenのSimple Paint Programを参考に、Bezier曲線を描画できるよう編集しています。
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
環境構築はこちらを参考にさせていただきました。
サンプルコード
仕様がシンプルで、公式のReferenceが丁寧でわかりやすいです。
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でちょっとしたUIコンポーネントなら追加できる。
おわりに
それぞれの技術スタックで図形描画周りを操作するとどんな実装になるのか確認することができました。
今回のようなシンプルな処理では実行速度の大きな違いは見られませんでしたが、より負荷の高い処理でwasmがどれだけ早いのか、C++やgoのサンプルとも比較してみたい思います。それはまたの機会に。
明日はahiruさんによるMetricKit入門です!