三つの効果をもう一度 / original
DEVELOPMENT JOURNAL / 34
Photoshot
CODE × UI
無料Photoshop代替の開発記

三つの効果をもう一度

効果名の多さだけでは編集は完成しません。新しい写真にCutout、Texturizer、Colored Pencilを重ねて順序を変更しました。適用を取り消し、記録したアクションで同じ結果を再現できるか確認します。

PHOTOSHOT DEVELOPMENT JOURNAL / 34

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

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

ソースから始める。

メニュー名を実際の処理につなぐ

前回は原本を残すスマートフィルターでした。通常フィルターは選択レイヤーの画素を処理します。general-filtersが種類とパラメーターを描画処理に渡し、結果と選択範囲を結びます。

今回再実行した通常フィルター描画検査は106項目を扱います。この数字はPhotoshopと同一のアルゴリズムという意味ではありません。処理経路が実装され、パラメーターに応じた結果が出るかを確かめるPhotoshotの検査です。

48効果を六つのグループに分ける

gallery-effectsのグループはArtistic、Brush Strokes、Gallery Distort、Sketch、Gallery Stylize、Textureです。計48効果があり、種類に応じてSize、Strength、Tone、Angle、Seed、Ink、Paperの必要な調整を表示します。すべての効果で同じスライダーを使う構造ではありません。

新しいUI検査では六つのメニュー、プレビューの取消、スタック操作と並べ替え、原本表示、適用後の取消と復帰、アクション再生を確認しました。実装は独自の絵画・版画・紙質処理で、Adobeのカーネルを流用したものではありません。

app/studio/general-filters.ts · SHA-256 da2a0b0df622 · L21–26Photoshotの実装コード ↗
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.');
Photoshot無料Photoshop代替の開発記 / 34IMPLEMENTATION

計算の
内側へ。

今回の写真で数値と順序を変える

公開版で新しいAI作例PNGを開き、アクション記録を開始しました。CutoutのTone Detailを90にしてTexturizerとColored Pencilを追加しました。選択効果のStrengthは65、Opacityは0.45にし、Move Upを実行しました。

画面にある組み合わせは実際の操作で作りました。適用後の出力が原本と異なることも確認しています。前後写真は別々に生成した二枚ではなく、一枚の生成原本とPhotoshotで処理した結果です。

取消後にアクションで再現する

Apply Filtersの後に記録を停止しました。Undoでレイヤーのsrcは適用前と完全一致し、記録したアクションの再生後は最初の適用結果と完全一致しました。保存プロジェクトの再読込結果も一致しました。

対象は今回の写真と三つの記録効果です。別の写真でも同じ見た目になるという保証ではありません。同じ入力と設定から同じ結果を作るため、効果の種類と数値を記録できているかを確認しました。

PHOTOSHOT / SOURCE NOTES34

コードを読む。

app/studio/general-filters.ts · SHA-256 da2a0b0df622 · L47–49Photoshotの実装コード ↗
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の実装コード ↗
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の実装コード ↗
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.');};
Photoshot無料Photoshop代替の開発記 / 34IN PRACTICE

エディターで見る。

三つの効果をもう一度 / workspace
Tone Detail、効果スタック、強度、不透明度を調整した公開版画面。

サイズと精度の境界も確認する

通常フィルターはレイヤー当たり16MPを確認し、絵画・質感ギャラリー効果は別途4MPに制限します。今回の1536×1024画像は両方の範囲内です。上限を超える処理を成功例として扱っていません。

一部は精度専用カーネルを使うため、通常フィルター全体をRGB8専用とは書けません。対応経路は効果によって異なります。次回は効果一覧から、メッシュを動かすLiquifyと専用作業画面へ進みます。

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

原本と編集結果。

三つの効果をもう一度 / original
AI生成の作例原本。公式撮影写真ではありません。
三つの効果をもう一度 / result
同じ原本に三つの効果を適用した実出力。アクション再生は最初の結果と一致しました。

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

確認したコードと検査

コード・実行結果・検査日 ↗