正射影(球体ビュー)に特化した WebGL2 マップレンダリングフレームワーク
Worker ベース並列パイプライン · OffscreenCanvas · Gint / GeoPBF / AltPBF 統合
OrthoMap はメインスレッドに投影演算・イベント処理・レイヤー管理のみを置き、 すべての描画を OffscreenCanvas + Web Worker に委譲する設計である。 各レイヤーは独立した Worker 上で動作し、メインスレッドへの描画ブロッキングがゼロになる。
初期化フローは 3 ステップで完結する。
// index.js — エントリーポイント
const map = await orthoMap({
target: d3.select("#container"), // 省略時は body
baseName: "osm.street",
center: [139.7, 35.7],
zoom: 4,
accessories: { stars: { maxCount: 500 } },
gadgets: false,
});
// 内部シーケンス:
// 1. orthographic(map, opts) — 投影・イベント・操作系セットアップ
// 2. createLayers(map, opts) — Worker 起動・3 レイヤー生成
// 3. createGadgets(map) — UI コンポーネント登録
D3 の geoOrthographic は球体の半径をスクリーンピクセルで表す scale を持つ。
OrthoMap はこれをウェブ地図標準のズームレベルに変換する。
ズーム 5.5(threshold)を境に、それ以下はベース球体画像のみ、それ以上はスライプルタイルを重ねる。
マウスドラッグ・ピンチ操作は D3 の zoom イベントをベースとし、
球面上の正確な追従のために versor(単位四元数) を用いる。
// ドラッグ開始: タッチ点の球面座標をデカルト3Dへ
v0 = cartesian(proj.invert(pt0)); // [lng,lat] → [x,y,z]
q0 = versor(r0 = proj.rotate()); // 現在回転を四元数化
// ドラッグ中: 球面上の最小回転を四元数合成
const v1 = cartesian(proj.rotate(r0).invert(pt));
const q1 = multiply(q0, delta(v0, v1));
proj.rotate(rotation(q1));
2 点ピンチ時は指の角度差を追加の四元数で合成することで Z 軸回転(地図の傾き) も同時に操作できる。
Ctrl または Meta キー押下時のホイールは カーソル位置を中心に球を Z 軸回転させる。
カーソルの画面座標からカーソルまでの球面ベクトル [x,y,z] を求め、
これを回転軸とする四元数を現在の回転に合成する。
単純な camera rotate ではなく球体上の実点を軸とした物理的な回転である。
両関数はアニメーション中に別操作が割り込んだ場合に即キャンセルできるよう flyTicket(単調増加カウンタ)で制御する。
| 関数 | アルゴリズム | 特徴 |
|---|---|---|
flyToFeature |
d3.interpolateZoom(ズームアウト→移動→ズームイン)+ 線形イージング | 長距離移動時に「空を飛ぶ」ような弧を描く |
zoomToFeature |
2 フェーズ:① threshold zoom まで回転移動、② 目的地でズームイン | タイル不要領域での移動 → 到着後のズームインでタイル読み込みを最小化 |
// zoomToFeature — 2フェーズ構成
// Phase1: 移動中は zoom 5.5 まで(travelMs ≈ 距離 × 2500ms)
const sTrav = min(s1, zval2scale(5.5));
// Phase2: 到着後 2000ms でズームイン(600ms オーバーラップ)
proj.scale(s0 + (sTrav - s0) * tRotEased // 移動フェーズ
+ (s1 - sTrav) * easeCubicInOut(tZoom)); // ズームフェーズ
D3 Dispatch をベースに、ユーザー登録(無名)とシステム登録(名前付き)を分けた API を提供する。
// ユーザーイベント登録 — 自動採番でネームスペース管理
const handler = map.onClick(e => console.log(e.lng, e.lat));
handler.destroy(); // 登録解除
// イベント種類
// Enter / Move / Leave / Drop / Click / ContextMenu
// Drawing / Drawn / Resize / Change / LoadStart / LoadEnd
// Move / Click には標高が付く(AltPBF 統合)
// e = { lng, lat, alt, x, y, shiftKey, metaKey }
各レイヤーは OffscreenCanvas.transferControlToOffscreen() で Canvas の所有権を Worker に移譲し、
メインスレッドとは postMessage のみで通信する。描画ループ全体が Worker 内で完結する。
init — offscreen / dpr / workersset — データ投入(cmd 別)drawing — scale / rotate / panningdrawn — パン終了後の確定描画move — カーソル座標resize — width / heightdestroy — Worker 終了{action:"done", type:"init"}{action:"done", type:"set"}{action:"done", type:"resize"}{action:"identify", featureId}{action:"click", featureId, lng, lat}{action:"redraw"} — context restored 後
drawing はパン・ズーム中にフレームごとに飛ぶ「即時描画」命令であり、
簡易品質での高速描画を目的とする(タイルの新規読み込みはしない)。
drawn はポインタが止まったときに一度だけ送られる「確定描画」命令で、
FBO への高品質レンダリング・picking バッファ更新など重い処理はここで行う。
set メッセージは描画用バイナリデータを含む場合、Transferable として送ることでゼロコピー転送する。
// rawBuffers が含まれる場合のみ Transferable 転送
if (prop?.rawBuffers) {
const { rawBuffers, ...rest } = prop;
worker.postMessage({ type: "set", cmd, data, prop: rest, rawBuffers }, rawBuffers);
}
base.js Worker は 全球等矩形画像(8192×4096) をテクスチャとして球体に貼り付け、
ズームイン時はスライプルタイルを上から重ねる 2 段構成を取る。
フラグメントシェーダー内でピクセルの画面座標から球面座標を逆算し、等矩形テクスチャを直接サンプリングする。 CPU 側での投影変換やバーテックスバッファは不要になる。
// GLSL Fragment Shader (baseFs) — 正射影の逆変換
float x = (gl_FragCoord.x - translate.x) / scale;
float y = (translate.y - gl_FragCoord.y) / scale;
float rho = sqrt(x*x + y*y);
if (rho <= 1.0) { // 球体円内のみ
float c = asin(rho);
float lng = atan(x * sin(c), rho * cos(c));
float lat = asin(y * sin(c) / rho);
applyRotation(rx, ry, rz, lng, lat); // ユーザー回転を適用
vec2 uv = vec2((lng + pi) / (2.0*pi), (lat + pi/2.0) / pi);
outColor = texture(u_image, uv);
} else { discard; } // 球体外はフラグメント破棄
ベーステクスチャは経度方向(S 軸)を REPEAT、緯度方向(T 軸)を CLAMP_TO_EDGE に設定する。
これにより経度 ±180° の日付変更線を跨ぐ描画でシームが発生しない。
タイルテクスチャは隣タイルとの滲みを防ぐため両軸 CLAMP_TO_EDGE。
タイル Worker プール(デフォルト 4 並列)が URL 経由で画像を fetch し、ImageBitmap に変換して
createTileTexture で GL テクスチャ化する。
// タイル優先度: 画面中央に近いタイルを先にロード
ents.sort((a, b) => a[4] - b[4]) // [4] = 中心距離
.forEach(([x, y, zo, pos, dist]) => {
// zo = 極方向オフセット(Y2T/Y4T テーブルで決定)
// 希望ズームになければ z-1, z-2, z-3 のキャッシュを代用
for (let dz of [0, 1, 2, 3]) {
const z = Z0 - (zo + dz); if (z < 0) continue;
const img = TileTub.get(name);
if (img) { /* UV を親タイル領域に変換して描画 */ return; }
}
});
Y2T / Y4T テーブル: 高緯度タイルは Mercator 投影の歪みにより表示面積が極端に小さくなる。 これを補正するためズームレベルごとに緯度方向の LOD オフセット(zo)を定義した定数テーブルで、 極付近タイルを 1〜2 ズーム落としてロードする。
| Name | ベース球体画像 | タイルソース | MaxZoom |
|---|---|---|---|
whiteEarth | whiteEarth.webp | なし | 7 |
google.street | naturalEarth.webp | Google Maps(lyrs=m) | 22 |
google.satellite | google.satellite.webp | Google Maps(lyrs=s) | 22 |
osm.street | naturalEarth.webp | OpenStreetMap JP | 19 |
osm.satellite | osm.satellite.webp | Bing Maps(四分木インデックス) | 19 |
cyberjapan.std | naturalEarth.webp | 国土地理院(日本域)+ OSM(域外) | 19 |
Firefox では createImageBitmap(blob) の結果が WebGL テクスチャとして正常に扱えない問題がある。
UA 判定により OffscreenCanvas を中間バッファとして描画し、
transferToImageBitmap() で再変換する workaround が入っている。
gintBorder.js は Gint の描画機能のうち ライン描画のみ を実装した軽量版である。
hover picking・ポリゴン塗り・FBO は持たず、固定スタイルの複数データセットを 1 パスで描く。
// 4 レイヤーの Gint データ(GeoPBF 経由でロード)
const geoNames = [
"ne_110m_graticules_10", // 経緯線 10°グリッド
"ne_50m_admin_0_boundary_lines_land", // 国境線
"ne_50m_admin_0_boundary_lines_maritime_indicator", // 海上境界
"ne_50m_geographic_lines", // 赤道・回帰線 等
];
// 各データセットの描画スタイル(ダッシュはスクリーンピクセル固定)
const BORDER_GL_STYLES = [
{ color: [1.0, 1.0, 1.0, 0.6], lineWidth: 1.0, dash: [0, 0] }, // graticule (solid)
{ color: [1.0, 1.0, 1.0, 1.0], lineWidth: 1.0, dash: [4, 2] }, // 国境線
{ color: [0.5, 0.5, 1.0, 0.8], lineWidth: 0.8, dash: [4, 2] }, // maritime
{ color: [1.0, 1.0, 1.0, 0.6], lineWidth: 0.8, dash: [4, 2] }, // geographic
];
ダッシュ長はズームに関係なく常に同じ視覚サイズになるよう、頂点シェーダー内で
v_dist_base = vertex_index × scale × sin(1°) として累積距離を計算する。
スクリーン座標でのピクセル距離が一定なので、ズーム変化でダッシュパターンが変形しない。
ライン同士が重なった際に半透明のアルファが積み重なってしまう問題を防ぐため、
blendFuncSeparate でカラーチャンネルと alpha チャンネルのブレンドを分離する。
gl.blendFuncSeparate(
gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, // RGB: 通常アルファブレンド
gl.ONE, gl.ONE_MINUS_SRC_ALPHA // A: 元 alpha を保持
);
ステンシルパス用の頂点シェーダーは、地球の裏面(z < 0)に回った頂点を NDC 原点 (0,0) に潰さず、地平線円(半径 = u_scale)の上に押し出す。 これにより国境線が地平線をまたぐポリゴンでも閉じた扇形が正しく構成され、 塗りつぶしに破綻が生じない。
// GLSL VS_STENCIL — 裏面頂点の地平線押し出し
if (p.z < 0.0) {
vec2 v = p.xy - u_viewport * 0.5;
float d = length(v);
gl_Position = d < 1e-4
? vec4(0.0, 0.0, 0.0, 1.0) // 原点直上は原点へ
: toNDC(u_viewport * 0.5 + v * (u_scale / d)); // 地平線円上へ
return;
}
border.js Worker は Canvas2D コンテキストで動作し、
天文データ・地図表示補助情報を 1 秒インターバルタイマーで更新する。
星・星座線の描画には現在時刻に対応する空の向きが必要で、GST を計算する。
赤経(Right Ascension)から GST を引いた時角(Hour Angle)を使い、球面天文座標から スクリーン 2D 座標に変換する。
恒星カタログ(stars.6)の各星を赤経・赤緯 → スクリーン座標に変換し、
B-V カラーインデックスから色を決定、等級から半径と透明度を計算する。
// B-V → 星の色(温度スペクトル)
const bvColor = bv =>
bv < -0.3 ? "#b2c8ff" : // O 型 — 青白
bv < 0.0 ? "#d9e2ff" : // B 型 — 青白
bv < 0.3 ? "#f8faff" : // A 型 — 白
bv < 0.6 ? "#fff8f0" : // F 型 — 黄白
bv < 0.8 ? "#fff2c8" : // G 型 — 黄(太陽)
bv < 1.1 ? "#ffe0b5" : // K 型 — 橙
"#ffab91"; // M 型 — 赤
// 等級 → 半径・透明度
radius = (9 - mag) * 0.20; // 明るい星ほど大きい
alpha = 1 - mag / 15; // 明るい星ほど不透明
星座線は赤経・赤緯ペアのリスト(MultiLineString)として格納されており、 球面座標 → デカルト3D → 球面投影変換で各点の可視性を判定してから描画する。
// 赤道座標 (ra, dec) → 球面投影 (px, py)
l = ra - skyRot; // 時角
z = sinφ*sinδ + cosφ*cosδ*cos(l); // z < 0 なら地平線以下
x = cosδ * (-sin(l));
y = cosφ*sinδ - sinφ*cosδ*cos(l);
px = cx + sr * (x*cosγ - y*sinγ); // sr = 球体半径より大きなスカイドーム半径
py = cy - sr * (x*sinγ + y*cosγ);
天体の種類(type)に応じてシンボルを描き分ける。
| type | 名称 | シンボル |
|---|---|---|
gc | 球状星団 | 円 + 十字 |
gx / gg | 銀河 | 楕円 |
oc | 散開星団 | 破線円 |
| その他 | 星雲 等 | 矩形 |
画面四辺を各 30 点ずつサンプリング(計 120 点)して地理座標に逆変換し、 ミニグローブ投影に再投影した多角形として視野領域(reticle)を描画する。
// viewport 四辺の地理座標サンプリング
const N = 30;
const edge = (x0, y0, x1, y1) =>
Array.from({ length: N }, (_, i) => {
const t = i / N;
return proj.invert([x0 + (x1-x0)*t, y0 + (y1-y0)*t]);
});
const geoPts = [...edge(0,0,w,0), ...edge(w,0,w,h), ...edge(w,h,0,h), ...edge(0,h,0,0)];
| 機能 | 内容 |
|---|---|
| 夜側(night) | 太陽赤緯・経度から終端線を 31 点のポリゴンで計算、半透明の暗色で塗りつぶす |
| 時計(clock) | UTC を大きなフォントで表示。ズームアウト時(zoom < 2)のみ |
| 座標表示(latlng) | カーソル移動で緯度・経度・標高をリアルタイム更新 |
| スケールバー(scale) | log10 丸め + 単位自動選択(m/km 切り替え、metric/imperial 対応) |
| 帰属表示(credit) | 各ベースマップのライセンス表記(attribution プロパティから) |
gint.js Worker はユーザーが createRemoteLayer({ type:"gint" }) で生成する汎用レイヤーで、
GeoPBF 由来の Gint バイナリを受け取り、ライン・ポリゴン・ポイントを描画する。
hover picking・クリック・ハイライト・ポリゴン塗りつぶしをすべてサポートする。
| ファイル | 役割 |
|---|---|
gintState.js | 共有 GL 状態オブジェクト (s) の定義 |
gintPrograms.js | GLSL シェーダーのコンパイル・リンク・ユニフォーム取得 |
gintTextures.js | arcTex / metaTex / ptTex テクスチャ生成・削除 |
gintFBO.js | baseFBO(クリーンシーン用)・pickFBO(ピッキング用)の生成・削除 |
gintRenderPasses.js | renderCleanScene / drawOverlay / renderPickingBuffer の 3 パス |
gintIdentify.js | GPU picking + JS ポリゴン内包判定、hover / leave ハンドラ |
// ピッキング FBO にフィーチャー ID を RGB 24bit でエンコード
uint fid1 = meta.a + 1u; // 0 = "ヒットなし" → +1 してずらす
v_color = vec4(
float(fid1 & 255u) / 255.0,
float((fid1 >> 8u) & 255u) / 255.0,
float((fid1 >> 16u) & 255u) / 255.0,
1.0
);
// readPixels でカーソル 1px を読み取り
gl.readPixels(pickX, pickY, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px);
const fid1 = px[0] | (px[1] << 8) | (px[2] << 16);
const featureId = fid1 === 0 ? null : fid1 - 1;
GPU picking がヒットしない(fid1 === 0)かつポリゴンデータが存在する場合、
カーソルの地理座標を Morton 整数座標に変換して findPolygon(geopbf)を呼ぶ。
縮小表示でグローブが小さく全コーナーが球体外になり lastViewBbox = null になる場合も動作する。
// activeId が変化したとき(hover 検出後)
// 1. baseFBO → canvas へ blit(クリーンシーンを復元)
gl.blitFramebuffer(0, 0, w, h, 0, 0, w, h, gl.COLOR_BUFFER_BIT, gl.NEAREST);
// 2. アクティブ feature のエッジを太め+黄色で上書き(u_pass=1)
gl.uniform1f(uRender.u_line_width, lineWidth + 2.0);
gl.uniform1i(uRender.u_active_id, activeId);
// 3. ポリゴン範囲が既知なら stencil でアクティブ外を dim
// (maskColor の alpha > 0 かつ polyEdgeByFid に範囲がある場合のみ)
hover 検出 → drawOverlay の全フローが Worker 内で完結する。
postMessage("redraw") は使わず、readPixels → drawOverlay を
同一 Worker 内で連続実行するため、主スレッドのラウンドトリップレイテンシが発生しない。
canvas.addEventListener("webglcontextlost", e => {
e.preventDefault();
// テクスチャ・FBO・プログラムの参照をすべてクリア
s.arcTex = s.metaTex = s.ptTex = s.ptMetaTex = null;
s.baseFBO = s.pickFBO = s.programs = null;
});
canvas.addEventListener("webglcontextrestored", () => {
s.programs = createGintPrograms(s.gl);
createFBOs(); uploadGintTextures();
postMessage({ action: "redraw" }); // 主スレッドに再描画要求
});
native-bucket の Cache(IndexedDB ラッパー)を使い、
ビュー状態とベースマップ名をセッション間で保持する。
map.stat = await Cache("GIS/stat");
map.baseName = await map.stat("base") || "osm.street";
map.view = await map.stat("view") || [[135, 35, 0], 2];
// view = [[経度, 緯度, 回転角], ズーム]
// パン・ズーム確定後(drawn イベント)に自動保存
map.stat("view", getView());
altpbf の createGetHeight が返す関数を map.getHeight として登録する。
Move / Click イベントに alt フィールドが追加され、座標表示や標高断面に利用できる。
UI コンポーネントは map.gadget(name, func) で登録するプラグイン方式で、
内蔵ガジェットとユーザー定義ガジェットを同等に扱える。
ガジェットは this = map として呼ばれる関数で、
map.addFrame で確保した DOM 領域に自由に UI を構築できる。
// 内蔵ガジェットの呼び出し
map.gadget.zoom(); // ズームボタンを leftTop に配置
map.gadget.layers(); // ベースマップ切り替えボタンを追加
const setTip = map.gadget.tip(); // カーソル追従 tip の制御関数を返す
setTip("クリックで詳細表示"); // tip テキストを設定
// ユーザー定義ガジェットの登録と呼び出し
map.gadget("myTool", function () {
map.addFrame("rightTop");
map.rightTop.append("div").text("Hello");
});
map.gadget.myTool();
| フレーム名 | 位置 | 主な用途 |
|---|---|---|
leftTop | 左上 | ズーム・方位・レイヤー切り替え等の操作ボタン |
rightTop | 右上 | 補助ボタン・ユーザー UI |
leftBottom | 左下 | 凡例 |
rightBottom | 右下 | 補助情報 |
leftFrame | 地図左に固定パネル | サイドメニュー(リサイズ対応) |
rightFrame | 地図右に固定パネル | 属性パネル(リサイズ対応) |
overlays | 地図上全面 | tip / pop / explain / loading など |
| ガジェット | 説明 |
|---|---|
zoom() |
ズームイン(+)・アウト(−)ボタン。クリックごとに整数ズームレベルへスナップしながら map.mag() をアニメーション実行する |
north() |
現在の Z 軸回転角(地図の傾き)をボタンのアイコン内 SVG でリアルタイム表示し、クリックすると map.north() で北向きに戻す |
layers() |
ベースマップ切り替えドロップダウン。map.Layers に登録されているレイヤー名を現在ロケールで表示し、選択すると map.setBase() を呼ぶ |
leftPanel(opts)rightPanel(opts) |
サイドパネルの開閉を管理する。opts.active = true にすると地図エリアを実際に押し出すリサイズモード、
省略時は地図上に重なるモーダルモードで動作する。
開閉アニメーションは d3.easeCubic(変更可)で補間する。
戻り値はパネル内コンテンツエリアの D3 Selection
|
full() |
Fullscreen API を使ってフルスクリーン切り替え。フルスクリーン状態に応じてアイコンを自動切替する |
constellation() |
星座線・星座名の表示トグル。クリックすると Accessories Worker に toggle-constellations を送りメシエ天体表示も連動して切り替える |
cpos() |
Geolocation API で現在地を取得し flyToFeature で移動する。
現在地はアニメーション SVG マーカー(パルス円)でオーバーレイ表示され、
地図を動かすと map.tester() で可視判定して自動的に表示/非表示を切り替える
|
shot() |
スクリーンショットをダウンロードする。
全 Canvas レイヤーを OffscreenCanvas に合成した後、
html2canvas でオーバーレイ(ボタン・noprint 要素を除く)を重ね、
WebP 形式でダウンロードする
|
print() |
印刷用に 3 倍解像度でレンダリングし、新規ウィンドウで window.print() を実行する。
縦長・横長を自動判定して CSS で適切に配置する
|
measure() |
距離・面積の計測モードに切り替えるトグルボタン。
有効時は Click イベントをキャンセルして createPolygon ツールを起動し、
クリックで頂点を追加しながらリアルタイムで距離・面積を表示する
|
| ガジェット | 説明 |
|---|---|
loading() |
LoadStart / LoadEnd イベントに連動してタイル読み込み中のインジケーターを表示する。
複数のタイルが同時に読み込まれる場合はすべての名前をリスト表示し、
すべて完了したら自動的に非表示になる
|
explain() |
地図左上に説明テキストや HTML を表示するパネル。
返り値は setContent(html) 関数で、文字列・関数(D3 Selection を受け取る)のどちらも受け付ける。
opts.permanent = true にすると閉じるボタンを表示しない
|
legend() |
地図左下に凡例を表示するパネル。クリックで折りたたみ、再展開ボタンで復元できる。
explain と同様に HTML または D3 関数でコンテンツを設定する
|
tip() |
カーソル位置に追従するツールチップ。
返り値の関数にテキストを渡すと表示、null を渡すと非表示になる。
画面端に近い場合は自動的に反対側に回り込む。
タッチデバイスではオフセットを広めにとる
|
pop() |
地図上の特定地点に固定されたポップアップを作成する。
返り値の関数 pop(content, event) を呼ぶたびに新しいポップアップが生成される。
ポップアップから地点へ引き出し線(Canvas2D)を自動描画し、
ドラッグ移動・ピン固定(位置固定)が可能。pop.clear() で全消去
|
contextmenu() |
右クリック(または長押し)で表示するコンテキストメニュー。
返り値の関数に [{ icon, name, func }] の配列を渡してメニュー項目を設定する
|
// pop の使用例
const pop = map.gadget.pop();
map.onClick(e => {
if (!e) return;
pop(`緯度: ${e.lat.toFixed(4)}<br>経度: ${e.lng.toFixed(4)}`, e);
});
// contextmenu の使用例
const setMenu = map.gadget.contextmenu();
setMenu([
{ name: "この地点を中心に", func: map => map.flyToFeature({ type: "Point", coordinates: [map.center[0], map.center[1]] }) },
{ name: "北に向ける", func: map => map.north() },
]);
map.setProperties() は CSS カスタムプロパティをドキュメントルートに書き込む。
設定できる主なプロパティ:
spaceColor(宇宙背景色)、earthFilter(球体フィルター)、
borderColor(ボタン枠)、buttonSize、fontSize、fontFamily 等。
| メソッド | 説明 |
|---|---|
map.setBase(name) | ベースマップを切り替える(タイルキャッシュリセット含む) |
map.setView(center, zoom, angle) | 即座にビュー変更 |
map.flyToFeature(feature, opts) | d3.interpolateZoom アニメーションで移動 |
map.zoomToFeature(feature, opts) | 2 フェーズアニメーションで移動 |
map.bbox([x0,y0,x1,y1]) | バウンディングボックスへズーム / 現在の bbox 取得 |
map.mag(n, duration) | 現在スケールを n 倍にアニメーション |
map.north(duration) | Z 回転(傾き)をアニメーションで 0 に戻す |
map.autoRotate(true/false) | 自動回転(d3.timer で 0.01°/ms) |
map.tester([lng,lat]) | 点が現在ビューポートに表示されているか確認 |
map.createLayer(opts) | Canvas2D レイヤーを追加(GeoJSON 描画用) |
map.createRemoteLayer(opts) | Worker + OffscreenCanvas レイヤーを追加 |