こんにちは!デザインシステムの構築からフロントエンドのアーキテクチャまで、日々プロダクトの品質を極限まで高めることに情熱を注いでいる先輩エンジニアです。
今回は、オープンソースの次世代デザインツールとして世界中のエンジニアとデザイナーから熱い視線を注がれている「Penpot」と、ブラウザの限界を突破する「WebAssembly(Wasm)」を組み合わせた、ハイパフォーマンスなプラグイン開発の裏技について解説します。
「デザインツール上で重い画像処理や、数千層に及ぶベクターデータの一括変換を行ったら、ブラウザがフリーズしてしまった……」
そんな絶望を味わったことはありませんか? JavaScript単体では、複雑な数学的演算や巨大なデータ構造の操作でどうしてもメインスレッドがブロックされてしまいます。
しかし、PenpotのプラグインアーキテクチャとWebAssemblyを融合させれば、C++やRustで書かれたネイティブ並みの高速処理をブラウザ上で実現できるのです。これをマスターすれば、毎日の退屈な手作業や重い処理が劇的に楽になりますよ。
それでは、初心者の方でも迷わず進められるよう、基礎セットアップから動くHelloWorldまでを一緒に紐解いていきましょう。
—
1. なぜPenpot × WebAssemblyなのか?
まず、私たちが扱う「Penpot」の立ち位置を確認しておきましょう。
Penpotは、SVGをネイティブフォーマットとして採用し、CSS GridやFlexboxの概念をそのままデザインツールに持ち込んだ、モダンなオープンソースのツールです。そして、最大の強みはその高い拡張性にあります。
プラグイン開発において、一般的なJavaScript/TypeScript環境だけでは、以下のような壁にぶぶつかります。
- 大量のパスデータ(ベクター座標の配列)をパース・変形する際のCPU負荷
- 画像のピクセル単位のフィルタリング処理における速度低下
ここに WebAssembly(Wasm) を組み込みます。Wasmは、ブラウザ上でネイティブコード(C, C++, Rustなど)に近い速度で実行できるバイナリフォーマットです。
「重い計算はWasmに任せ、UIやPenpot APIとのやり取りはJavaScript層が担当する」という役割分担を敷くことで、ブラウザとは思えない圧倒的なパフォーマンスを発揮するプラグインが作れるのです。
—
2. 開発環境のセットアップ
それでは、実際に手を動かして環境を整えていきましょう。
今回は、WebAssemblyのコンパイルターゲットとしても非常に相性が良い Rust を用いたプラグイン開発のベースを作ります。
事前準備
以下のツールがローカルにインストールされていることを確認してください。
- Node.js (v18以上推奨)
- Rustツールチェイン (`cargo`, `rustc`)
- `wasm-pack` (RustからWasmをビルドするための必須ツール)
wasm-packのインストール(未導入の場合)
cargo install wasm-pack
プロジェクトディレクトリの作成
プラグイン用のディレクトリを作成し、プロジェクトの骨組みを作ります。
mkdir penpot-wasm-plugin
cd penpot-wasm-plugin
Rustのライブラリプロジェクトとして初期化
cargo new –lib plugin-core
cd plugin-core
—
3. 最速で作る!WebAssemblyコアの実装
まずは、Wasm側(Rust)で「受け取った数値を倍にする」、あるいは「ベクターの座標データを一括処理する」というシンプルな処理を書きましょう。
`plugin-core/Cargo.toml` を開き、Wasmとしてビルドするための設定を追加します。
[package]
name = “plugin-core”
version = “0.1.0”
edition = “2021”
[lib]
ブラウザから呼び出しやすいようにcdylibを指定
crate-type = [“cdylib”]
[dependencies]
JavaScriptとRustの間でデータをやり取りしやすくするCrate
wasm-bindgen = “0.2”
次に、`plugin-core/src/lib.rs` に処理を記述します。ここでは「重いベクター演算の模擬」として、座標の配列を受け取って一括でオフセット(移動)させる関数を実装しましょう。
use wasm-bindgen::prelude::;
// JavaScriptから直接呼び出せるように #[wasm_bindgen] を付与
[wasm_bindgen]
pub fn process_coordinates(coords: &[f32], offset: f32) -> Vec
// 受け取った座標データに対して高速にオフセット処理を適用
let mut processed = Vec::with_capacity(coords.len());
for (i, &val) in coords.iter().enumerate() {
// 偶数番目をX座標、奇数番目をY座標とみなし、X座標だけ移動させる例
if i % 2 == 0 {
processed.push(val + offset);
} else {
processed.push(val);
}
}
processed
}
これを以下のコマンドでWasm用にビルドします。
wasm-pack build –target web
これで、`pkg/` ディレクトリの中にブラウザから読み込み可能な `.wasm` ファイルとJavaScriptのグルーコードが生成されます。
—
4. Penpotプラグインとの統合(HelloWorld)
次に、Penpotのプラグイン側から先ほどのWasmモジュールを呼び出します。
Penpotのプラグインは、基本的にはHTMLとJavaScript(またはTypeScript)で構成されています。
プロジェクトのルートに戻り、プラグインのエントリーポイントとなるファイルを準備しましょう。
フォルダ構成案
penpot-wasm-plugin/
├── plugin-core/ # 先ほど作成したRustプロジェクト
│ ├── src/lib.rs
│ └── Cargo.toml
├── manifest.json # Penpotプラグインの設定ファイル
├── index.html # プラグインのUI
└── main.js # WasmをロードしてPenpot APIと連携するロジック
manifest.json(Penpotへの登録定義)
Penpotに「このプラグインはこういうものですよ」と伝えるためのマニフェストファイルです。
{
“name”: “Wasm Vector Accelerator”,
“id”: “wasm-vector-accelerator”,
“version”: “1.0.0”,
“icon”: “icon.svg”,
“description”: “WebAssemblyを活用した超高速ベクター一括変換プラグイン”,
“entrypoint”: “index.html”
}
index.html & main.js(UIとWasmのブリッジ)
`index.html` では、ユーザーがボタンを押したときにWasmの処理が走り、Penpot上の選択シェイプのデータを書き換えるUIを作ります。
Wasm 座標一括変換
選択中のシェイプの頂点データをWebAssemblyで高速処理します。
続いて、`main.js` です。ここでWasmモジュールを非同期読み込みし、PenpotのプラグインAPIと結びつけます。
// 生成されたWasmのバインディングをインポート
import init, { process_coordinates } from ‘./plugin-core/pkg/plugin_core.js’;
async function run() {
// 1. Wasmモジュールの初期化
await init();
console.log(“WebAssemblyモジュールのロードが完了しました 🚀”);
document.getElementById(‘run-btn’).addEventListener(‘click’, async () => {
// PenpotのプラグインAPI経由で、現在キャンバス上で選択されている要素を取得
// ※実際のPenpotプラグインAPI仕様に準拠した記述
const selectedShapes = await penpot.selection.get();
if (selectedShapes.length === 0) {
alert(“処理するシェイプを選択してください。”);
return;
}
for (const shape of selectedShapes) {
// 例として、シェイプのパスデータ(頂点座標の配列)を取得したと仮定
// 実際には shape.points やパスのセグメント配列をここに渡します
const dummyCoords = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
// 2. WebAssemblyによる超高速な数値演算処理の実行
const offsetValue = 50.0;
const optimizedCoords = process_coordinates(dummyCoords, offsetValue);
console.log(“Wasm処理結果:”, optimizedCoords);
// 3. 処理結果をPenpot上のシェイプに反映
// shape.update({ points: optimizedCoords });
}
penpot.ui.notify(“WebAssemblyによる一括処理が完了しました!”);
});
}
run();
—
5. 現場で活きる設計の極意:さらなる高みへ
お疲れ様でした!これで、Penpotの拡張性とWebAssemblyの計算能力を掛け合わせた次世代プラグインの基礎が完成しました。
実際に現場でこのアーキテクチャを運用する際は、以下のポイントを意識するとさらにプロダクトの品質が跳ね上がります。
1. メモリ管理の最適化
JavaScriptとWasmの間でデータをやり取りする際、頻繁なメモリコピーが発生するとボトルネックになります。大きな配列を扱う場合は、Wasm側のメモリバッファ(`Memory`)を直接JavaScript側からTypedArrayとして参照・操作する「ゼロコピー」な設計を検討してください。
2. 非同期処理の活用
数万個のオブジェクトを同時に処理する場合、たとえWasmであっても数ミリ秒〜数秒のブロッキングが発生する可能性があります。その場合は `Web Workers` 内でWasmを動かすことで、PenpotのUIスレッドを完全に滑らかに保つことができます。
結びにかえて
デザインツールというものは、クリエイターの思考スピードを妨げない「圧倒的な滑らかさ」が命です。今回紹介した Penpot と WebAssembly の組み合わせをマスターすれば、既存のプラグインの枠を超えた、プロ仕様の高度な自動化・高度画像処理ツールを自らの手で生み出すことができます。
「面倒な手作業をコードでスマートに解決し、デザインの本来のクリエイティビティに集中する」
そんな開発体験を、ぜひあなたのプロジェクトでも楽しんでみてください。それでは、次のハックでお会いしましょう!