Three effects. One replay. / original
DEVELOPMENT JOURNAL / 34
Photoshot
CODE × UI
Building a free Photoshop alternative

Three effects. One replay.

A list of effect names is only the start. We stacked Cutout, Texturizer and Colored Pencil on a new portrait, changed their order, undid the result and replayed the recorded action to check reproducibility.

PHOTOSHOT DEVELOPMENT JOURNAL / 34

AI-generated demonstration image; not an actual celebrity photograph or official collaboration. Editing screens and results were made in Photoshot.

PhotoshotDEVELOPMENT JOURNAL / 34FOUNDATION

From the source.

Connect menu names to real processing

The previous episode preserved the source through Smart Filters. Regular filters process the selected layer pixels. general-filters routes the chosen type and parameters to rendering and connects the output to the selection.

The rerun regular-filter rendering suite covers 106 entries. That number is not a claim of identical Photoshop algorithms. These are Photoshot checks for implemented processing paths and results that respond to parameters.

Organize 48 gallery effects into six groups

gallery-effects groups its 48 effects into Artistic, Brush Strokes, Gallery Distort, Sketch, Gallery Stylize and Texture. Depending on the effect, the UI exposes relevant Size, Strength, Tone, Angle, Seed, Ink or Paper controls. Every slider is not meaningful for every effect.

Fresh UI checks covered the six menu groups, preview cancellation, stack controls and reordering, original view, apply/undo/redo and action replay. The painting, printmaking and paper-texture implementations are independent effects, rather than imported Adobe kernels.

app/studio/general-filters.ts · SHA-256 da2a0b0df622 · L21–26Photoshot implementation ↗
export const generalFilterNames={...galleryEffectNames,lighting:'Lighting Effects',extrude:'Extrude',lensProfile:'Lens Profile Correction',...legacyFilterNames,...filterNames,...advancedFilterNames,displace:'Displace',smartSharpen:'Smart Sharpen',shakeReduction:'Shake Reduction',shear:'Shear',deinterlace:'De-Interlace',sharpen:'Sharpen',sharpenMore:'Sharpen More',edges:'Find Edges',emboss:'Emboss',solarize:'Solarize',posterize:'Posterize',offset:'Offset',twirl:'Twirl',ripple:'Ripple',lens:'Lens Correction'};
export type GeneralFilterType=keyof typeof generalFilterNames;
export type GeneralFilter=Omit<SmartFilter,'type'> & {type:GeneralFilterType;halftoneShape?:'dot'|'line'|'circle';ditherMode?:'ordered'|'diffusion';ditherColor?:boolean;galleryInk?:string;galleryPaper?:string;lighting?:LightingSettings;extrude?:ExtrudeSettings;lensProfile?:LensProfileSettings;offsetX:number;offsetY:number;edge:'wrap'|'clamp'|'transparent';scale:number;vertical:number;horizontal:number;vignette:number;displacement?:{width:number;height:number;data:string};mapMode?:'stretch'|'tile';redFringe?:number;blueFringe?:number;shear?:number[];removeBlur?:'gaussian'|'motion';iterations?:number;kernel?:number[];divisor?:number;bias?:number};
export const defaultGeneralFilter=(type:GeneralFilterType):GeneralFilter=>({id:crypto.randomUUID(),type,radius:type==='posterize'?6:3,amount:type==='lens'?0:type==='noise'?10:100,threshold:type==='traceContour'||Object.hasOwn(galleryEffectNames,type)?128:0,opacity:1,blend:'normal',enabled:true,angle:0,seed:1,distribution:'uniform',monochromatic:true,offsetX:0,offsetY:0,edge:type==='offset'?'wrap':(type==='lens'||type==='lensProfile')?'transparent':'clamp',scale:100,vertical:0,horizontal:0,vignette:0,...(Object.hasOwn(galleryEffectNames,type)?{galleryInk:'#24202c',galleryPaper:'#f7eedb'}:{}),...(type==='lighting'?{lighting:defaultLighting()}:{}),...(type==='extrude'?{extrude:defaultExtrude(),radius:16,amount:30,angle:-45}:{}),...(type==='displace'?{offsetX:10,offsetY:10,mapMode:'stretch' as const}:{}),...(type==='lens'?{redFringe:0,blueFringe:0}:{}),...((type==='smartSharpen'||type==='shakeReduction')?{removeBlur:(type==='shakeReduction'?'motion':'gaussian') as 'motion'|'gaussian',iterations:5,radius:type==='shakeReduction'?2:1}:{}),...(type==='shear'?{shear:[0,20,-20,20,0]}:{}),...(type==='custom'?{kernel:[0,-1,0,-1,5,-1,0,-1,0],divisor:1,bias:0}:{})});
export function validateGeneralFilters(value:unknown):asserts value is GeneralFilter[]{
 if(!Array.isArray(value)||!value.length||value.length>16)throw Error('Choose between 1 and 16 filters.');
PhotoshotDEVELOPMENT JOURNAL / 34IMPLEMENTATION

Inside the
calculation.

Change parameters and order on this portrait

We opened the new AI demonstration PNG in public Photoshot and started recording an action. Cutout used Tone Detail 90. We added Texturizer and Colored Pencil, set the selected effect Strength to 65 and Opacity to 0.45, then used Move Up.

The displayed combination was made through those actual controls. We checked that the saved output differed from the source. The before and after are the same generated source and its Photoshot-processed result, rather than two independently generated pictures.

Undo, replay and compare

After Apply Filters, recording stopped. Undo restored the exact pre-filter layer src. Playing the recorded action reproduced the exact first output. The saved project also reopened with the same state.

This check concerns this input and its three recorded effects. It does not promise an identical visual impression on a different photograph. The useful criterion is that effect types and parameters survive recording and reproduce the result for the same input.

PHOTOSHOT / SOURCE NOTES34

Read the code.

app/studio/general-filters.ts · SHA-256 da2a0b0df622 · L47–49Photoshot implementation ↗
export const supportsPrecisionGeneralFilter=(type:string)=>!!smartFilterType(type)||hasPrecisionGeneralKernel(type);
export function canGeneralFilter(doc:StudioDoc,l?:Layer,type?:string){return (!(doc.precision&&(doc.precision.depth!==8||doc.precision.space!=='RGB'))||!!type&&supportsPrecisionGeneralFilter(type))&&!!l&&!layerLocked(doc,l,'pixels')&&(l.kind==='image'||l.kind==='paint')&&!l.smartObject&&!l.psdData?.placedLayer&&!l.shape&&!l.gridWarp&&!l.puppet;}
const check=(cancel:()=>boolean)=>{if(cancel())throw Error('Filter cancelled. No changes applied.');};
app/studio/general-filters.ts · SHA-256 da2a0b0df622 · L95–98Photoshot implementation ↗
export async function applyGeneralFilters(doc:StudioDoc,layer:Layer,filters:GeneralFilter[],cancel=()=>false):Promise<StudioDoc>{
 validateGeneralFilters(filters);if(doc.precision&&(doc.precision.depth!==8||doc.precision.space!=='RGB')&&filters.some(f=>f.enabled&&f.opacity&&!supportsPrecisionGeneralFilter(f.type)))throw Error('This stack contains a filter that requires RGB 8-bit. Remove it or convert explicitly in Image > Color Mode and Precision.');if(!canGeneralFilter(doc,layer,'gaussian')||!filters.every(f=>!f.enabled||!f.opacity||canGeneralFilter(doc,layer,f.type)))throw Error('Select an unlocked raster layer. Use Rasterize Layer for text, shapes or Smart Objects.');if(layer.width*layer.height>16_000_000)throw Error('General filters support up to 16 megapixels per layer.');
 if(doc.precision&&(doc.precision.depth!==8||doc.precision.space!=='RGB')){
  check(cancel);const before=await precisionLayer(layer,doc.precision,{masks:false});check(cancel);const after=await renderPrecisionGeneralFilters(before,filters,1,cancel),clip=doc.selection?selectionForLayer(doc.selection,layer):undefined,n=precisionChannels(before.space),stride=n+1;
app/studio/gallery-effects.ts · SHA-256 6bde97046e2b · L22–27Photoshot implementation ↗
const clamp=(v:number,a=0,b=255)=>Math.max(a,Math.min(b,v));
/** Independent painting, printmaking and paper-texture effects; not Adobe kernels. */
export async function galleryEffectPixels(image:ImageData,f:GeneralFilter,ratio=1,cancel=()=>false){
 const {width:w,height:h,data:src}=image,n=w*h;
 if(n>4_000_000)throw Error('Gallery painting and texture effects support up to 4 MP.');
 const check=async()=>{await new Promise<void>(r=>setTimeout(r,0));if(cancel())throw Error('Filter cancelled. No changes applied.');};
PhotoshotDEVELOPMENT JOURNAL / 34IN PRACTICE

In the editor.

Three effects. One replay. / workspace
Public Photoshot controls for Tone Detail, the effect stack, strength and opacity.

Check current size and precision boundaries

Regular filters check a 16 MP limit per layer. Painting and texture gallery effects have a separate 4 MP limit. The 1536×1024 portrait used here fits both. We did not treat oversized processing as a successful case.

Some filters use precision-specific kernels, so describing the entire regular-filter implementation as RGB8-only would be inaccurate. Supported paths differ by filter. Next we move from effect lists to Liquify meshes and dedicated filter workspaces.

PhotoshotDEVELOPMENT JOURNAL / 34BEFORE / AFTER

Source and result.

Three effects. One replay. / original
AI-generated demonstration source, not an official photograph.
Three effects. One replay. / result
Actual three-effect output from the same source. Action replay matched the first application.

AI-generated demonstration image; not an actual celebrity photograph or official collaboration. Editing screens and results were made in Photoshot.