変形を座標で残す / original
DEVELOPMENT JOURNAL / 27
Photoshot
CODE × UI
無料Photoshop代替の開発記

変形を座標で残す

26回のAI原画像を開き直し、二つの接続平面で変形しました。人物の美化ではなく、共有境界と原本保持を確認する例です。

PHOTOSHOT DEVELOPMENT JOURNAL / 27

AI生成の動作例用写真。編集画面と出力画像は実際のPhotoshotで作成しました。

Photoshot無料Photoshop代替の開発記 / 27FOUNDATION

ソースから始める。

変形表示と保存した原本を区別する

変形後の表示だけを保存すると、次の編集の基準が曖昧になります。接続平面は原本srcとgridWarp.planesを共に保持し、元の平面から移動先へ座標を計算します。

写真全体を二つの四角形に分けた初期配置を使い、中央の上下点を内側へ動かしました。人物補正の推奨値ではなく、構造を見やすくする検査値です。

隣の平面と同じ頂点を参照する

initialPlanesは六点と二つの四角形を作ります。中央上下のインデックスを両方が共有するため、一つを動かすと隣の境界も動きます。別々の座標を似せる処理ではありません。

2026年10月22日、Plane 1の中央上をx 50%・y 12%、中央下をx 50%・y 88%へ変更しました。Plane 2のCorner 1とCorner 4にも同じ値が表示され、UIで接続を確認しました。

app/studio/grid-warp.ts · SHA-256 809558da5767 · L4–11Photoshotの実装コード ↗
export type GridWarp={planes?:ConnectedPlanes;enabled:boolean;points:WarpPoint[]};
export function rectangleWarp(x=0,y=0,width=1,height=1):GridWarp{return{enabled:true,points:Array.from({length:16},(_,i)=>({x:x+(i%4)/3*width,y:y+Math.floor(i/4)/3*height}))};}
export function validateGridWarp(w:GridWarp){if(w?.planes)validateConnectedPlanes(w.planes);if(!w||typeof w.enabled!=='boolean'||!Array.isArray(w.points)||w.points.length!==16||w.points.some(p=>!p||[p.x,p.y].some(n=>!Number.isFinite(n)||n<0||n>1)))throw Error('Invalid custom warp.');}
export function gridWarpIdentity(w?:GridWarp){return !w?.enabled||!w.planes&&w.points.every((p,i)=>Math.abs(p.x-(i%4)/3)<1e-10&&Math.abs(p.y-Math.floor(i/4)/3)<1e-10);}
const weights=(t:number)=>[(1-t)**3,3*t*(1-t)**2,3*t*t*(1-t),t**3];
// Bicubic Bezier surface: moving a control point bends the continuous image surface.
export function warpPoint(w:GridWarp,u:number,v:number){const a=weights(u),b=weights(v);let x=0,y=0;for(let j=0;j<4;j++)for(let i=0;i<4;i++){const p=w.points[j*4+i],weight=a[i]*b[j];x+=p.x*weight;y+=p.y*weight;}return{x,y};}
export function gridWarpMesh(w:GridWarp,width:number,height:number){const nx=40,ny=40,points=[];for(let j=0;j<=ny;j++)for(let i=0;i<=nx;i++){const p=warpPoint(w,i/nx,j/ny);points.push({x:i/nx*width,y:j/ny*height,tx:p.x*width,ty:p.y*height});}return{nx,ny,points};}
Photoshot無料Photoshop代替の開発記 / 27IMPLEMENTATION

計算の
内側へ。

折れた平面と範囲外入力を拒否する

connected-planes.tsは座標をレイヤー内0~1、点を最大40、平面を最大8に制限します。四角形は凸形状と一定方向を維持する必要があります。分割は共有辺の中間点を探し隣へ伝えます。

描画では移動先から原本座標へ逆変換し、アルファを考慮して補間します。透明境界の問題には色とアルファを共に扱う必要があります。今回使ったのは初期の二平面だけです。

再読込して原本バイトも比較する

適用後に保存し、開き直して再保存しました。全レイヤーデータが一致し、srcから取り出したPNGバイトもoriginal.pngと完全に一致しました。見た目の類似以上に原本保持を確認できます。

中央上下を内側へ動かしたため、その場所は透明な市松模様です。新しい背景を埋めた結果ではありません。今回はマスクを追加していないので、その動作まで直接試したとは記しません。

PHOTOSHOT / SOURCE NOTES27

コードを読む。

app/studio/connected-planes.ts · SHA-256 5c950db23324 · L2–5Photoshotの実装コード ↗
export type ConnectedPlanes={layout:{x:number;y:number}[];destination:{x:number;y:number}[];quads:[number,number,number,number][]};
export const initialPlanes=():ConnectedPlanes=>({layout:[{x:0,y:0},{x:.5,y:0},{x:1,y:0},{x:0,y:1},{x:.5,y:1},{x:1,y:1}],destination:[{x:0,y:0},{x:.5,y:0},{x:1,y:0},{x:0,y:1},{x:.5,y:1},{x:1,y:1}],quads:[[0,1,4,3],[1,2,5,4]]});
const convex=(q:{x:number;y:number}[])=>q.every((p,i)=>{const b=q[(i+1)%4],c=q[(i+2)%4];return (b.x-p.x)*(c.y-b.y)-(b.y-p.y)*(c.x-b.x)>1e-6;});
export function validateConnectedPlanes(p:ConnectedPlanes){if(!p||!Array.isArray(p.layout)||!Array.isArray(p.destination)||p.layout.length!==p.destination.length||p.layout.length<4||p.layout.length>40||!Array.isArray(p.quads)||!p.quads.length||p.quads.length>8||[...p.layout,...p.destination].some(v=>!v||[v.x,v.y].some(n=>!Number.isFinite(n)||n<0||n>1)))throw Error('Connected plane points must stay inside the layer bounds.');for(const q of p.quads){if(q.length!==4||new Set(q).size!==4||q.some(i=>!Number.isInteger(i)||i<0||i>=p.layout.length)||!convex(q.map(i=>p.layout[i]))||!convex(q.map(i=>p.destination[i])))throw Error('Keep each connected plane convex and clockwise.');}}
app/studio/precision-warp.ts · SHA-256 48a2cf67ff77 · L8–9Photoshotの実装コード ↗
export function planesPrecision(source:PrecisionRaster,p:ConnectedPlanes){validateConnectedPlanes(p);const {width,height}=source,{sample,result,stride}=sampler(source);for(const indices of p.quads){const layout=indices.map(i=>p.layout[i]) as Quad,dest=indices.map(i=>p.destination[i]) as Quad,a=homography(layout),b=inverse(homography(dest)),x0=Math.max(0,Math.floor(Math.min(...dest.map(v=>v.x))*width)),x1=Math.min(width-1,Math.ceil(Math.max(...dest.map(v=>v.x))*width)),y0=Math.max(0,Math.floor(Math.min(...dest.map(v=>v.y))*height)),y1=Math.min(height-1,Math.ceil(Math.max(...dest.map(v=>v.y))*height));for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++){const uv=mapMatrix(b,(x+.5)/width,(y+.5)/height);if(uv.x< -1e-7||uv.x>1+1e-7||uv.y< -1e-7||uv.y>1+1e-7)continue;const at=mapMatrix(a,uv.x,uv.y);sample(at.x*width-.5,at.y*height-.5,(y*width+x)*stride);}}return result;}
export function warpPrecision(source:PrecisionRaster,grid?:GridWarp,puppet?:PuppetWarp){let r=source;if(grid?.enabled&&!gridWarpIdentity(grid))r=grid.planes?planesPrecision(r,grid.planes):meshPrecision(r,gridWarpMesh(grid,r.width,r.height));if(puppet?.enabled&&!warpIdentity(puppet))r=meshPrecision(r,puppetMesh(puppet,r.width,r.height));return r;}
app/studio/projective-path.ts · SHA-256 5b98aac97a07 · L4–7Photoshotの実装コード ↗
/** Projected cubic curves are rational; adaptively subdivide only when perspective is present. */
export function projectPath(p:PathShape,forward:number[],width=1,height=1):PathShape{
 const map=(p:Point)=>{const q=mapMatrix(forward,p.x,p.y);return {x:q.x*width,y:q.y*height};};
 const checkRegion=(points:Point[])=>{const z=points.map(p=>forward[6]*p.x+forward[7]*p.y+forward[8]);if(z.some(v=>Math.abs(v)<1e-7)||z.some(v=>v*z[0]<0))throw Error('The perspective horizon crosses a path.');};
Photoshot無料Photoshop代替の開発記 / 27IN PRACTICE

エディターで見る。

変形を座標で残す / workspace
実際のPhotoshot作業画面。設定と写真を一緒に確認できます。

格子・パペット・遠近切抜きを分ける

通常Grid Warpは16制御点の三次ベジエ曲面と40×40メッシュを使います。precision-warpは格子や接続平面の後にパペット変形を適用できます。遠近切抜きは投影曲線を適応分割し、地平線交差とアンカー上限を調べる別経路です。

既存connected-planes報告には分割、取消、共有辺、PSD/PSB、マスクの事例がありますが、今回全体を再実行してはいません。直接検査は二平面座標とプロジェクト往復です。人物はAI生成の成人架空モデルです。28回はコンテンツに応じた塗りつぶしと拡縮です。

Photoshot無料Photoshop代替の開発記 / 27BEFORE / AFTER

原画像と接続平面の変形。

変形を座標で残す / original
01 / AI生成の実例原画像
変形を座標で残す / result
02 / 二平面の変形・プロジェクト再読込

AI生成の動作例用写真。編集画面と出力画像は実際のPhotoshotで作成しました。

確認したコードと検査

コード・検査記録・日付 ↗