Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
TypeScriptでドット絵エディタ実装録: 状態設計と実装判断
Search
Tukudani
May 23, 2026
Programming
78
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
TypeScriptでドット絵エディタ実装録: 状態設計と実装判断
https://2026.tskaigi.org/talks/49
TSKaigi 2026の10分LT登壇資料です
Tukudani
May 23, 2026
Other Decks in Programming
See All in Programming
初心者DevRelとして参加者だった私が、DevRel Talks!#2に登壇するまでにしてきたこと
sokohirai
0
350
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
120
go-spidermonkeyでAIエージェントのCode Modeを実装する
syumai
3
1.5k
フロントエンドUIフレームワークのこれまでとこれから
ssssota
2
1.2k
Can LLMs Replicate 4 Years of Compose Migration? Exploring the boundaries of automation with 279 XML files from a real product
makun
0
130
一参加者から『中の人』へ 〜全通PHPerがブースに立って学んだ、カンファレンスを100倍楽しむコツ〜
wp_daisuke
0
130
thread_parallel_with_free-threaded_Python_and_NumPy.pdf
riku_sakamoto
0
190
Press start. Python's next generation.
willingc
PRO
3
320
typoなんかねぇよ
raspython3
0
680
巨大モノリシックアプリ モダン化大作戦
ktcryomm
0
290
Webの地図
yosuke_furukawa
PRO
3
3.2k
標準パッケージに uuid が追加された 背景から見る Go らしい意思決定 / go_127_uuid_decision
convto
4
3.5k
Featured
See All Featured
Bootstrapping a Software Product
garrettdimon
PRO
306
120k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.6k
Testing 201, or: Great Expectations
jmmastey
46
8.3k
Context Engineering - Making Every Token Count
addyosmani
9
1.1k
How to train your dragon (web standard)
notwaldorf
97
6.8k
The Director’s Chair: Orchestrating AI for Truly Effective Learning
tmiket
1
290
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
28
3.6k
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
880
The SEO Collaboration Effect
kristinabergwall1
1
550
What's in a price? How to price your products and services
michaelherold
247
13k
Ethics towards AI in product and experience design
skipperchong
2
360
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.5k
Transcript
TypeScriptでドット絵エディタ実装録 ~状態設計と実装判断~
自己紹介
自己紹介 つくだに / 佐川 • 出身:福島県 • 所属:株式会社 オプティム フロントエンド推進室
運営メンバー • 趣味:サウナ・ゲーム・酒・ドット絵
4
5 私たちは、AI・IoT技術を社会の隅々に行き渡らせ、 日本のあらゆる産業を根底から変革する 「AXの中核を担う企業」を目指しています。
6
アジェンダ • 作ったものについて • 機能と実装判断 • まとめ 話す 話さない •
フレームワークとか、その他のこと • Canvas API の詳細 • Nuxt / Vue の詳細 • ドット絵エディタアプリ実装のロジック判断 • データ構造 • 直線・塗りつぶし • レイヤー管理 • モバイル操作の工夫
作ったもの WEBアプリ「DotArt」 • 機能 • ドット絵エディタ • ペン / 消しゴム
/ 直線 / 塗りつぶし • Undo・Redo • レイヤー機能(最大3層) • 画像保存・共有(共有機能は停止中) • モバイル・タブレット対応 • 技術スタック • Nuxt (Vue.js)
作ったもの 作った経緯 • 当時、iPhoneにモノクロ4色で簡単にドット絵が描けるアプリがあり、 とても気に入っていた • iOS11 での 32Bitアプリのサポート終了により起動不可に •
このアプリからドット絵を描き始めた人が色つきの絵を描きたくなった時に、 高機能なエディターに移行する前に触りやすい簡単なエディタを作りたかった • AIエージェントが使われだす前だったのもあり、ロジックを自前で実装していった
型システムとライフサイクルの時間軸の衝突 Nullアサーション問題 • 基本的に Canvas 要素で絵を描いているため、DOM参照が必須 • Strict モードだと初期化時は null
許容型にせざるを得ない → canvasRef.value! のような null アサーションが至る所に散らばった • nullチェックを1回行った後は、 non-nullable な変数に再アサインするのがよさそう const canvas = document.querySelector<HTMLCanvasElement>('#drawcanvas'); const ctx = canvas?.getContext('2d'); if (!canvas || !ctx) throw new Error('canvas init failed’); canvasState.canvas = canvas; canvasState.canvasCtx = ctx;
Point型 ── 座標を守る型 Point型 • ドット絵エディタは座標計算だらけ • ドット絵を描く処理だけでなく、マウス操作のカーソル位置監視でも座標を受け取る • 座標情報を一元的に型指定することで広範囲の安全性を担保できた
• 小さい型定義だが、影響範囲が広いほど恩恵も大きい • 型定義のありがたみ、TSありがとう type Point = { X: number; Y: number } // 座標を受け取る関数すべてに適用 const getMousePoint = (wholeCoor: Point): Point => { ... } const figureToolsState = reactive<{ // 直線描画の状態管理 drawingFigure: Point[]; // ドラッグ中の一時表示 figureToolsStart: Point; // 直線の始点 }>({...})
基本データ構造 キャンバスデータ • ドット絵の内容と、パレットデータ等を管理する基本的な概念
基本データ構造 • 塗られた色をパレット配列のインデックスで管理 • ドット絵上の座標と1次元配列の対応は Y座標 × 横幅 + X座標
canvasIndexData[ Y × N + X ] = paletteIndex 0 9 9 9 9 9 9 9 0 0 8 8 8 8 0 0 0 0 0 0 9 0 0 0 0 8 0 0 0 0 8 0 … … [ Y × 0 + X ] [ Y × 1 + X ] … 0 1 2 3 4 5 6 7 8 9 … 仕様
描画ツール 直線ツール • 直線を引く機能
描画ツール 実装 • ブレゼンハムのアルゴリズムを採用 • 一般的な線分描画のアルゴリズム • 直線状で横に1マス進むごとに縦に何マス進むか? を、誤差を積み上げるという手法で解決した let
e = -xdiff; for (let i = 0; i <= xdiff; i++) { Line.push({ ...cell }); cell.X += Xvek; e += ydiff2; if (e >= 0) { cell.Y += Yvek; e -= xdiff2; } }
描画ツール • 例: (0,0) → (6,3) の直線 (※詳細は後ほど確認ください) 横方向に 7
, 縦方向に 3 マス進む ↓ 閾値 = 7(横の距離) 加算量 = 3(縦の距離) 横移動のたびに誤差 += 3 を積み上げ、 誤差が 7 以上になったら縦に1マス進み、誤差 -= 7 する X=0: 誤差= 3 → 7未満 → Y=0 ・・・( 0, 0 ) X=1: 誤差= 6 → 7未満 → Y=0 ・・・( 1, 0 ) X=2: 誤差= 9 → 7以上! -7 → Y=1 ・・・( 2, 1 ) X=3: 誤差= 5 → 7未満 → Y=1 ・・・ ( 3, 1 ) X=4: 誤差= 8 → 7以上! -7 → Y=2 ・・・ ( 4, 2 ) X=5: 誤差= 4 → 7未満 → Y=2 ・・・ ( 5, 2 ) X=6: 誤差= 7 → 7以上! -7 → Y=3 ・・・ ( 6, 3 )
描画ツール バケツツール • 囲われた中を塗りつぶす機能
描画ツール 実装 • 再帰処理(4方向)を採用 • 特にひねりなく、別の色がぶつかるまで上下左右へ走査する const fill = (cell,
color, target) => { if (範囲外) return; if (target[cell.Y * N + cell.X] !== color) return; drawDot(cell); fill({ X: cell.X - 1, Y: cell.Y }, color, target); fill({ X: cell.X + 1, Y: cell.Y }, color, target); fill({ X: cell.X, Y: cell.Y - 1 }, color, target); fill({ X: cell.X, Y: cell.Y + 1 }, color, target); };
描画ツール シンプルさを重視 • どちらも一般的な方式で実装 • ドット絵のスケールでは再帰処理の呼び出し回数もそこまで多くならないので、十分と判断 o 96 × 96
程度であれば問題なく動作 実装判断
レイヤー機能 レイヤー機能 • ドット絵をレイヤー管理する機能
レイヤー機能 実装 • 最大3層、キャンバスデータを積み重ねる layerdCanvasData: { layerName: string // レイヤー名
layerIndex: number // レイヤーの重なり順 active: boolean // 表示/非表示 canvasIndexData: number[] // キャンバスデータ } canvasesIndexData: layerdCanvasData[] // 全レイヤーの配列
レイヤー機能 • ドット絵の表示処理で3層のレイヤーを下から重ねるようにすると、 何か書くたびにキャンバス × 3の比較処理が走ってしまう • 「今描いているレイヤーの上の同じ位置に色があるか」の判定があれば、 レイヤー1枚走査するだけで描画可能なのでは? 思い付き
レイヤー機能 思い付きの結果 レイヤーの合成関数→ (約150行)
レイヤー機能 • やってることはこんな感じ • 元々分岐が多い上に、 「現在描画されている最上位レイヤー」の更新処理等で複雑化している • シンプルな実装から入り、ボトルネックになってから最適化するべきだった What is
this 描画したピクセルは最前面のレイヤーか? ├─ Yes(このレイヤーが最前面) │ ├─ 背景色 かつ 下に色あり → 透過と見なす 下レイヤーの色で描画 │ ├─ 背景色 かつ 下に色なし → 背景と見なす 背景色で描画 │ └─ それ以外 → そのまま描画 └─ No(上位レイヤーが前面) → 描画スキップ、データ更新のみ
モバイル対応 ─ カーソル方式 スマホ操作モード • PC操作をそのままスマホ版に持ち込むと、 指で直接描く形になる →描いているところが指で隠れてしまう • タッチペンが無いと手を放すまでミスに気づけ
ないため、改善したい 指で隠れるので ものすごい見づらい
モバイル対応 ─ カーソル方式 実装 • 「スマホモード」として、 直接タップではなくカーソル操作形式で描画できるように
モバイル対応 ─ カーソル方式 画面内を スワイプすると カーソルも動く ボタン押下中は 描画を行う
モバイル対応 ─ カーソル方式 カーソル方式 • 既存スマホアプリで実装されており、非常に快適な操作が実現されていた • 代案が全く思いつかず、WEBで挙動を再現した! → より良い操作方式があれば知りたい
実装判断
まとめ
まとめ ・nullチェックは1箇所に集約する ・明確な課題が出ていないなら、実装はシンプルに始める ・スマホに対応するなら、操作感にこだわる 振り返って思ったこと: 「動くものを作る」と「正しく作る」は別 →動いたことに満足せず、 設計・計測・振り返りをセットにして初めて完成
ご清聴ありがとうございました! ご紹介したもの以外に興味のある機能があれば、 ぜひ懇親会等でお話ししましょう!