
Building a free Photoshop alternative
Keep transforms as coordinates
We reopened Part 26’s AI original and warped it using two connected planes. This demonstration examines shared boundaries and original preservation rather than portrait beautification.
AI-generated study photograph. Editor captures and outputs show Photoshot in use.
From the source.
Distinguish the rendered view from the retained original
Saving only a warped view makes the basis for later edits unclear. Connected planes retain both the original src and gridWarp.planes. Rendering maps positions from the original plane to its destination.
We used the default two-rectangle layout and moved the shared top and bottom points inward for an obvious change. These are diagnostic coordinates, not recommended portrait-retouching values.
Adjacent planes reference the same vertices
initialPlanes creates six points and two quads. Both quads reference the same middle top and bottom indices. Editing one shared point therefore moves the neighboring boundary, rather than merely trying to match two separate coordinates.
On October 22, 2026, Plane 1’s middle top became x 50%, y 12%, and the middle bottom x 50%, y 88%. Switching to Plane 2 showed the same values at Corner 1 and Corner 4, verifying the connection in the UI.
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};}Inside the
calculation.
Reject folded planes and out-of-range coordinates
connected-planes.ts limits coordinates to 0–1 inside the layer, with at most 40 points and eight planes. Quads must remain convex and consistently oriented. Splitting locates shared edge midpoints and propagates connections to neighbors.
Rendering maps destination coordinates back to the source and interpolates with alpha weighting. Transparent-edge artifacts require color and alpha to be handled together. This demonstration used only the two default planes.
Reopen the project and compare original bytes
After applying the warp, we saved, reopened and saved the project again. The complete layer data matched the earlier file, and the PNG bytes decoded from src exactly matched original.png. This checks preservation beyond visual resemblance.
Moving the middle top and bottom inward leaves transparent checkerboard areas; no new background was filled. No mask was added in this photograph, so its mask behavior is not claimed as directly tested.
Read the code.
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.');}}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;}/** 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.');};In the editor.

Grid, puppet and perspective crop are separate paths
Ordinary Grid Warp uses a bicubic Bezier surface with 16 control points and a 40×40 mesh. precision-warp can apply puppet deformation after a grid or connected-plane warp. Perspective crop adaptively subdivides projected curves and checks horizon crossings and anchor limits through a separate path.
Stored connected-planes reports cover splitting, cancellation, shared edges, PSD/PSB and masks. That full suite was not rerun here. Today’s direct checks concern two-plane coordinates and project roundtrip. The portrait is an AI-generated fictional adult. Part 28 covers content-aware fill and scaling.
Original and connected-plane warp.


AI-generated study photograph. Editor captures and outputs show Photoshot in use.