← Photoshot · ガイドと開発記↗エディターを開く
Photoshot / Development01

無料画像編集ソフト開発記 第1回:GitHubのコードとAIでレイヤー・マスクをつなぐ

無料のPhotoshop代替を目指すPhotoshotは、すべてをゼロから書いたわけではない。GitHubで開発されるReactやPixiJSを利用し、文書構造、レイヤー、マスクを結ぶコードをAIとともに書いた。初期コミットと現在の実装から、借りた部分と新しく実装した部分をたどる。

出典照合 · 2026年10月16日改訂

記事を読む ↓
レイヤー・マスク編のAI生成ポートレート
PORTRAIT STUDY / 01AI-generated image
実際のPhotoshot編集画面 · 2026年10月13日
実際のPhotoshot編集画面 · 2026年10月13日
BUILD LOG / 01

GitHubから持ってきた土台

Reactは画面と状態管理、初期のPixiJSは画像レイヤーの描画を担当した。初期公開コミットではpixi.jsからApplication、Sprite、Textureを読み込んでいる。名前だけを参考にしたのではなく、実行経路に組み込まれた依存ライブラリだった。

ここで「GitHubのコードを使った」とは、パッケージを導入し公開APIを利用したという意味だ。完成済みのエディターを丸ごと複製したわけではない。

コード抜粋 · src/stage.tsx · eead3ca · L1–5 ↗
'use client';
import { useEffect, useRef } from 'react';
import { Application, Sprite, Texture } from 'pixi.js';
import 'pixi.js/advanced-blend-modes';
import { layerSurface, type StudioDoc } from './document';
BUILD LOG / 02

AIと書いたのは接続部分

PixiJSはPhotoshotが位置や回転、不透明度をどこに保存するか知らない。AIと書いた接続コードはレイヤーの描画面からSpriteを作り、Photoshot側の値をPixiJSへ渡す。下のlは文書内のレイヤーデータ、sは描画側のオブジェクトだ。

既存ライブラリと独自のデータ構造の間を埋めることが、AIと行った実装の具体的な役割だった。

コード抜粋 · src/stage.tsx · eead3ca · L27–33 ↗
const s = new Sprite(Texture.from(canvas));
s.anchor.set(0.5);
s.position.set(l.x + l.width / 2, l.y + l.height / 2);
s.rotation = (l.rotation * Math.PI) / 180;
s.alpha = l.opacity;
s.blendMode = l.blend;
a.stage.addChild(s);
BUILD LOG / 03

元画像を消さずにマスクを適用する

レイヤーが表示できても、マスクを塗った時に元の画素まで消えては困る。Photopeaのマスク説明は動作の参考資料とし、Photoshotの接続処理は別に実装した。

現在のコードは一時的なレイヤー描画面へdestination-inを適用し、処理後にsource-overへ戻す。元画像とマスクを別々に保持するため、マスクを無効にすれば元の見え方に戻せる。

コード抜粋 · src/document.ts · v0.16.0 · L192–196 ↗
if (layer.mask?.enabled) {
  ctx.globalCompositeOperation = 'destination-in';
  ctx.drawImage(maskSurface(layer.mask), 0, 0, c.width, c.height);
  ctx.globalCompositeOperation = 'source-over';
}
BUILD LOG / 04

初期PixiJSと現在のCanvasを区別する

開発が進むと、プレビューと書き出しを同じ合成経路にそろえる必要が出てきた。v0.16.0のStageは初期のSprite生成ではなくcompositeを呼び、表示サイズの上限と古い計算を中止する条件を渡す。

初期にはPixiJSを実際に使い、その後はCanvasベースの共通合成処理へ移った。この二段階を区別することが、GitHubの履歴から開発を説明する意味だ。

コード抜粋 · src/stage.tsx · v0.16.0 · L31–35 ↗
const image = await composite(d, false, {
  maxEdge: 1600,
  cancelled: () => !mounted.current || r !== revision.current,
});
if (!mounted.current) break;
BUILD LOG / 05

画面の裏で確認したこと

掲載画像は実際の編集画面と実装記録だ。レイヤー順序、マスクの切り替え、保存と再読み込み、書き出しまで確認した。マスク描画テストには元画像を変更しないことの確認も含まれる。

AIが速くコードを書くことと、編集結果が保たれることは別の問題だ。ライブラリの責任範囲と、独自の文書・保存・合成コードの責任範囲を分けて調べた。

実際の文字マスク編集画面
実際の文字マスク編集画面
実際の画像書き出し画面
実際の画像書き出し画面
BUILD LOG / 06

利用したもの、実装したもの

利用したのはReactと初期PixiJSの機能。参考にしたのはマスクの動作説明。AIと新規作成・修正したのはレイヤーの接続、マスク適用、保存構造、その後の共通合成処理だった。既存の部品を編集ソフトにつなげる作業が出発点になった。

次回はGitHubでKonvaのコントラスト処理を読み、独自実装を比較・修正した過程を追う。今回のコードは初期公開コミットとv0.16.0から抜粋した。

実装と参考資料

ガイドと開発記の一覧 ↗↗実際に編集する