
無料Photoshop代替の開発記
明暗に色を置く
無料の画像編集ソフトを作るPhotoshot開発記です。今回は暗い画素と明るい画素に色を対応させるグラデーションマップ。既知のRGB補間を調整レイヤー、色の停止点、設定保存につなぐ仕組みを、公開コードと新しい実例で確認します。
AI生成の動作例用写真。編集画面と出力画像は実際のPhotoshotで作成しました。
ソースから始める。
画面の位置ではなく明るさを読む
キャンバスの左から右へ描くグラデーションは位置に従います。グラデーションマップは画素の明るさに従います。暗い髪や明るいニットがどこにあっても、同じ明るさの基準で色を選びます。
出発点は既存の調整レイヤーと、既知の加重和・RGB補間です。引用コードはPhotoshotの公開実装です。記録にない失敗談や、根拠のない外部ライブラリーからの移植という説明は加えていません。
停止点を計算できる順序にする
各停止点はposition、color、次の点までのmidpointを持ちます。保存時の検証では2〜32点、最初は0、最後は100、位置の厳密な昇順を要求します。同じ位置を許すと区間の分母がゼロになり得ます。
revision 2では正規化したRGBに0.30・0.59・0.11を掛けて明るさを求めます。旧経路の0.2126・0.7152・0.0722とは区別が必要です。反転は明るさを1から引き、その位置を挟む二つの停止点を探します。
if (
a.stops !== undefined &&
(a.type !== 'gradient' ||
!Array.isArray(a.stops) ||
a.stops.length < 2 ||
a.stops.length > 32 ||
a.stops[0].position !== 0 ||
a.stops.at(-1)!.position !== 100 ||
a.stops.some(
(s, i) =>
!s ||
!finite(s.position, 0, 100) ||
!finite(s.midpoint, 1, 99) ||
!/^#[0-9a-f]{6}$/i.test(s.color) ||
(i > 0 && s.position <= a.stops![i - 1].position),
))
)
throw Error('그레이디언트 색상점이 올바르지 않습니다.');計算の
内側へ。
中間点・滑らかさ・ディザを分ける
区間内の基本比率は(明るさの位置−前の点)÷(次の点−前の点)です。中間点はべき乗で二色が半分ずつ混ざる位置を動かします。50%なら基本比率を保ちます。停止点自体の移動とは別の操作です。
revision 2の滑らかさは比率に曲線成分を混ぜます。ディザはその後の量子化境界に微小な変化を加える段階です。失われた色情報を復元したり、すべてのバンディングを消す保証はありません。補間はRGBの数値上で行い、知覚色空間での補間ではありません。
同じ原画像に三色を対応させる
2026年10月22日、新しいAI生成ポートレートを公開中のPhotoshotで開き、調整レイヤーを追加しました。停止点は0% #152d54、50% #808080、100% #ffda9c。二つの中間点は50、滑らかさ100、反転オフ、ディザオン、Normal、不透明度70%です。
不透明度は変換後の結果と元画像を合成する段階で働きます。70%の結果は三色へ完全に変換した100%とは異なります。原画像と結果は同じ1536×1024で、作業画面も実際の編集画面です。人物はAI生成モデルであり、芸能人の使用・推薦を示す写真ではありません。
コードを読む。
if (t === 'gradient') {
let y = a.revision === 2 ? 0.3 * r + 0.59 * g + 0.11 * b : luma(r, g, b);
if (v.reverse) y = 1 - y;
const p = y * 100;
let i = 0;
while (i < stops!.length - 2 && p > stops![i + 1].position) i++;
const low = stops![i],
high = stops![i + 1];
let f = clamp((p - low.position) / (high.position - low.position));
f = f ** (Math.log(0.5) / Math.log(low.midpoint / 100));
if (a.revision === 2 && v.smooth) f += ((f * f * (3 - 2 * f) - f) * v.smooth) / 200;
for (let c = 0; c < 3; c++) out[c] = low.rgb[c] * (1 - f) + high.rgb[c] * f;
} gradient: {
name: '그레이디언트 맵',
description: '명암을 여러 색상으로 매핑하고 중간점을 조절합니다.',
controls: [
control('reverse', '반전', 0, 1),
control('dither', '디더링', 0, 1),
control('smooth', '매끄러움', 0, 100, 100),
],
},check('gradient interior stops, midpoint, reverse, dither are persisted', () => {
const a = A.makeAdjustment('gradient');
a.stops = [
{ position: 0, color: '#000000', midpoint: 25 },
{ position: 50, color: '#ff0000', midpoint: 50 },
{ position: 100, color: '#ffffff', midpoint: 50 },
];
near(color(a, [127.5, 127.5, 127.5]), [1, 0, 0]);
near(color(a, [31.875, 31.875, 31.875]), [0.5, 0, 0]);
a.values.reverse = 1;
near(color(a, [255, 255, 255]), [0, 0, 0]);
a.values.dither = 1;
A.validateAdjustment(JSON.parse(JSON.stringify(a)));
});エディターで見る。

検証範囲と残る限界
公開コードの確認基準はコミットd08d365です。2026年10月22日にpublic-adjustmentsとupgrade-renderを実行し、16調整のアルファ・保存往復・合成検査と追加19グループが通過しました。内部停止点、中間点、反転、ディザの保存、不正入力の拒否も含みます。
公開画面から保存したプロジェクトでもrevision 2、三つの停止点、不透明度を照合しました。すべての写真・色空間でAdobe Photoshopと画素単位に一致するという意味ではありません。次の17回では色の系統ごとに成分を調整する選択カラーを扱います。
写真で見る結果。


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