@radialapps/memories-vips-editor 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +25 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +35 -0
- package/dist/math.js.map +1 -1
- package/dist/pipeline.d.ts +7 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +23 -1
- package/dist/pipeline.js.map +1 -1
- package/dist/smallPreview.d.ts +31 -0
- package/dist/smallPreview.d.ts.map +1 -0
- package/dist/smallPreview.js +118 -0
- package/dist/smallPreview.js.map +1 -0
- package/dist/transforms/flip.d.ts +18 -0
- package/dist/transforms/flip.d.ts.map +1 -0
- package/dist/transforms/flip.js +43 -0
- package/dist/transforms/flip.js.map +1 -0
- package/dist/transforms/highlights.d.ts +17 -0
- package/dist/transforms/highlights.d.ts.map +1 -0
- package/dist/transforms/highlights.js +49 -0
- package/dist/transforms/highlights.js.map +1 -0
- package/dist/transforms/index.d.ts.map +1 -1
- package/dist/transforms/index.js +8 -0
- package/dist/transforms/index.js.map +1 -1
- package/dist/transforms/shadows.d.ts +17 -0
- package/dist/transforms/shadows.d.ts.map +1 -0
- package/dist/transforms/shadows.js +49 -0
- package/dist/transforms/shadows.js.map +1 -0
- package/dist/transforms/warmth.d.ts +17 -0
- package/dist/transforms/warmth.d.ts.map +1 -0
- package/dist/transforms/warmth.js +46 -0
- package/dist/transforms/warmth.js.map +1 -0
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -3
- package/src/math.ts +40 -0
- package/src/pipeline.ts +25 -1
- package/src/smallPreview.ts +120 -0
- package/src/transforms/flip.ts +64 -0
- package/src/transforms/highlights.ts +67 -0
- package/src/transforms/index.ts +8 -0
- package/src/transforms/shadows.ts +67 -0
- package/src/transforms/warmth.ts +64 -0
- package/src/types.ts +14 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// highlights.ts — boost/recover the bright end, weighted by brightness.
|
|
2
|
+
//
|
|
3
|
+
// weight w = clamp(avg,0,1)^2 (0 in blacks, 1 in whites, quadratic falloff),
|
|
4
|
+
// then rgb += (amount * 0.5) * w. The 0.5 keeps the gray curve monotonic over
|
|
5
|
+
// the whole slider range (a full-strength recovery would push white below
|
|
6
|
+
// near-white). Same formula in src/transforms/highlights.h and
|
|
7
|
+
// math.ts applyHighlights, so the canvas preview is WYSIWYG.
|
|
8
|
+
import { clampf } from '../math.js';
|
|
9
|
+
import { isNumInRange, setFloatUniform, } from './transform.js';
|
|
10
|
+
const FRAG = `
|
|
11
|
+
precision mediump float;
|
|
12
|
+
uniform sampler2D texture;
|
|
13
|
+
uniform float amount;
|
|
14
|
+
varying vec2 texCoord;
|
|
15
|
+
void main() {
|
|
16
|
+
vec4 color = texture2D(texture, texCoord);
|
|
17
|
+
float avg = (color.r + color.g + color.b) / 3.0;
|
|
18
|
+
float l = clamp(avg, 0.0, 1.0);
|
|
19
|
+
float w = l * l;
|
|
20
|
+
color.rgb += (amount * 0.5) * w;
|
|
21
|
+
gl_FragColor = color;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
export class HighlightsTransform {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.edit = 'highlights';
|
|
27
|
+
this.controls = [
|
|
28
|
+
{ key: 'amount', label: 'amount', min: -1, max: 1, step: 0.01, def: 0 },
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
defaults() {
|
|
32
|
+
return { edit: 'highlights', amount: 0 };
|
|
33
|
+
}
|
|
34
|
+
validate(params) {
|
|
35
|
+
if (params.amount !== undefined && !isNumInRange(params.amount, -1, 1))
|
|
36
|
+
return ['amount must be a number in [-1,1]'];
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
normalize(params) {
|
|
40
|
+
return { edit: 'highlights', amount: clampf(-1, params.amount ?? 0, 1) };
|
|
41
|
+
}
|
|
42
|
+
apply(ctx, from, to, params) {
|
|
43
|
+
ctx.pass(FRAG, from, to, (gl, p) => {
|
|
44
|
+
setFloatUniform(gl, p, 'amount', params.amount);
|
|
45
|
+
});
|
|
46
|
+
return to;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=highlights.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlights.js","sourceRoot":"","sources":["../../src/transforms/highlights.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,0EAA0E;AAC1E,+DAA+D;AAC/D,6DAA6D;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EACL,YAAY,EACZ,eAAe,GAKhB,MAAM,gBAAgB,CAAC;AAUxB,MAAM,IAAI,GAAG;;;;;;;;;;;;;CAaZ,CAAC;AAEF,MAAM,OAAO,mBAAmB;IAAhC;QACkB,SAAI,GAAG,YAAqB,CAAC;QACtC,aAAQ,GAAmB;YAChC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE;SACxE,CAAC;IAsBJ,CAAC;IApBQ,QAAQ;QACb,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAC3C,CAAC;IAEM,QAAQ,CAAC,MAAsB;QACpC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,mCAAmC,CAAC,CAAC;QAC/C,OAAO,EAAE,CAAC;IACZ,CAAC;IAEM,SAAS,CAAC,MAAsB;QACrC,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAC3E,CAAC;IAEM,KAAK,CAAC,GAAkB,EAAE,IAAY,EAAE,EAAU,EAAE,MAAY;QACrE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;YACjC,eAAe,CAAC,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,CAAC;IACZ,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transforms/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transforms/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAkBxD,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAiBlE,CAAC;AAEF,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAKjD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAI9C"}
|
package/dist/transforms/index.js
CHANGED
|
@@ -3,14 +3,18 @@ import { BlurTransform } from './blur.js';
|
|
|
3
3
|
import { BrightnessContrastTransform } from './brightnessContrast.js';
|
|
4
4
|
import { CropTransform } from './crop.js';
|
|
5
5
|
import { ExposureTransform } from './exposure.js';
|
|
6
|
+
import { FlipTransform } from './flip.js';
|
|
6
7
|
import { GrayscaleTransform } from './grayscale.js';
|
|
8
|
+
import { HighlightsTransform } from './highlights.js';
|
|
7
9
|
import { HueSaturationTransform } from './hueSaturation.js';
|
|
8
10
|
import { InvertTransform } from './invert.js';
|
|
9
11
|
import { RotateTransform } from './rotate.js';
|
|
10
12
|
import { SepiaTransform } from './sepia.js';
|
|
13
|
+
import { ShadowsTransform } from './shadows.js';
|
|
11
14
|
import { SharpenTransform } from './sharpen.js';
|
|
12
15
|
import { VibranceTransform } from './vibrance.js';
|
|
13
16
|
import { VignetteTransform } from './vignette.js';
|
|
17
|
+
import { WarmthTransform } from './warmth.js';
|
|
14
18
|
export const TRANSFORMS = {
|
|
15
19
|
brightnessContrast: new BrightnessContrastTransform(),
|
|
16
20
|
exposure: new ExposureTransform(),
|
|
@@ -20,10 +24,14 @@ export const TRANSFORMS = {
|
|
|
20
24
|
blur: new BlurTransform(),
|
|
21
25
|
sharpen: new SharpenTransform(),
|
|
22
26
|
vignette: new VignetteTransform(),
|
|
27
|
+
shadows: new ShadowsTransform(),
|
|
28
|
+
highlights: new HighlightsTransform(),
|
|
29
|
+
warmth: new WarmthTransform(),
|
|
23
30
|
invert: new InvertTransform(),
|
|
24
31
|
grayscale: new GrayscaleTransform(),
|
|
25
32
|
crop: new CropTransform(),
|
|
26
33
|
rotate: new RotateTransform(),
|
|
34
|
+
flip: new FlipTransform(),
|
|
27
35
|
};
|
|
28
36
|
/** Human-readable problems; empty means the edit is VIPS-appliable. */
|
|
29
37
|
export function validateEdit(edit) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/transforms/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAkB,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/transforms/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAkB,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,CAAC,MAAM,UAAU,GAAgD;IACrE,kBAAkB,EAAE,IAAI,2BAA2B,EAAE;IACrD,QAAQ,EAAE,IAAI,iBAAiB,EAAE;IACjC,aAAa,EAAE,IAAI,sBAAsB,EAAE;IAC3C,KAAK,EAAE,IAAI,cAAc,EAAE;IAC3B,QAAQ,EAAE,IAAI,iBAAiB,EAAE;IACjC,IAAI,EAAE,IAAI,aAAa,EAAE;IACzB,OAAO,EAAE,IAAI,gBAAgB,EAAE;IAC/B,QAAQ,EAAE,IAAI,iBAAiB,EAAE;IACjC,OAAO,EAAE,IAAI,gBAAgB,EAAE;IAC/B,UAAU,EAAE,IAAI,mBAAmB,EAAE;IACrC,MAAM,EAAE,IAAI,eAAe,EAAE;IAC7B,MAAM,EAAE,IAAI,eAAe,EAAE;IAC7B,SAAS,EAAE,IAAI,kBAAkB,EAAE;IACnC,IAAI,EAAE,IAAI,aAAa,EAAE;IACzB,MAAM,EAAE,IAAI,eAAe,EAAE;IAC7B,IAAI,EAAE,IAAI,aAAa,EAAE;CAC1B,CAAC;AAEF,uEAAuE;AACvE,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACjF,MAAM,CAAC,GAAG,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,IAAU;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,CAAC,SAAS,CAAC,IAAI,CAAS,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ParamControl, type RenderContext, type Target, type Transform } from './transform.js';
|
|
2
|
+
export interface ShadowsEdit {
|
|
3
|
+
edit: 'shadows';
|
|
4
|
+
/** -1 (crush shadows) .. 1 (lift shadows). Default 0. */
|
|
5
|
+
amount?: number;
|
|
6
|
+
}
|
|
7
|
+
type Full = Required<ShadowsEdit>;
|
|
8
|
+
export declare class ShadowsTransform implements Transform<'shadows', ShadowsEdit> {
|
|
9
|
+
readonly edit: "shadows";
|
|
10
|
+
controls: ParamControl[];
|
|
11
|
+
defaults(): Full;
|
|
12
|
+
validate(params: ShadowsEdit): string[];
|
|
13
|
+
normalize(params: ShadowsEdit): Full;
|
|
14
|
+
apply(ctx: RenderContext, from: Target, to: Target, params: Full): Target;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=shadows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadows.d.ts","sourceRoot":"","sources":["../../src/transforms/shadows.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,KAAK,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;AAiBlC,qBAAa,gBAAiB,YAAW,SAAS,CAAC,SAAS,EAAE,WAAW,CAAC;IACxE,SAAgB,IAAI,EAAG,SAAS,CAAU;IACnC,QAAQ,EAAE,YAAY,EAAE,CAE7B;IAEK,QAAQ,IAAI,IAAI;IAIhB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,EAAE;IAMvC,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIpC,KAAK,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM;CAMjF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// shadows.ts — lift/crush the dark end, weighted by darkness.
|
|
2
|
+
//
|
|
3
|
+
// weight w = (1 - clamp(avg,0,1))^2 (1 in blacks, 0 in whites, quadratic
|
|
4
|
+
// falloff), then rgb += (amount * 0.5) * w. The 0.5 keeps the gray curve
|
|
5
|
+
// monotonic over the whole slider range (a full-strength lift would reverse
|
|
6
|
+
// deep shadows past mid-gray). Same formula in src/transforms/shadows.h
|
|
7
|
+
// and math.ts applyShadows, so the canvas preview is WYSIWYG.
|
|
8
|
+
import { clampf } from '../math.js';
|
|
9
|
+
import { isNumInRange, setFloatUniform, } from './transform.js';
|
|
10
|
+
const FRAG = `
|
|
11
|
+
precision mediump float;
|
|
12
|
+
uniform sampler2D texture;
|
|
13
|
+
uniform float amount;
|
|
14
|
+
varying vec2 texCoord;
|
|
15
|
+
void main() {
|
|
16
|
+
vec4 color = texture2D(texture, texCoord);
|
|
17
|
+
float avg = (color.r + color.g + color.b) / 3.0;
|
|
18
|
+
float l = clamp(avg, 0.0, 1.0);
|
|
19
|
+
float w = (1.0 - l) * (1.0 - l);
|
|
20
|
+
color.rgb += (amount * 0.5) * w;
|
|
21
|
+
gl_FragColor = color;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
export class ShadowsTransform {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.edit = 'shadows';
|
|
27
|
+
this.controls = [
|
|
28
|
+
{ key: 'amount', label: 'amount', min: -1, max: 1, step: 0.01, def: 0 },
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
defaults() {
|
|
32
|
+
return { edit: 'shadows', amount: 0 };
|
|
33
|
+
}
|
|
34
|
+
validate(params) {
|
|
35
|
+
if (params.amount !== undefined && !isNumInRange(params.amount, -1, 1))
|
|
36
|
+
return ['amount must be a number in [-1,1]'];
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
normalize(params) {
|
|
40
|
+
return { edit: 'shadows', amount: clampf(-1, params.amount ?? 0, 1) };
|
|
41
|
+
}
|
|
42
|
+
apply(ctx, from, to, params) {
|
|
43
|
+
ctx.pass(FRAG, from, to, (gl, p) => {
|
|
44
|
+
setFloatUniform(gl, p, 'amount', params.amount);
|
|
45
|
+
});
|
|
46
|
+
return to;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=shadows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadows.js","sourceRoot":"","sources":["../../src/transforms/shadows.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,yEAAyE;AACzE,4EAA4E;AAC5E,wEAAwE;AACxE,8DAA8D;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EACL,YAAY,EACZ,eAAe,GAKhB,MAAM,gBAAgB,CAAC;AAUxB,MAAM,IAAI,GAAG;;;;;;;;;;;;;CAaZ,CAAC;AAEF,MAAM,OAAO,gBAAgB;IAA7B;QACkB,SAAI,GAAG,SAAkB,CAAC;QACnC,aAAQ,GAAmB;YAChC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE;SACxE,CAAC;IAsBJ,CAAC;IApBQ,QAAQ;QACb,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACxC,CAAC;IAEM,QAAQ,CAAC,MAAmB;QACjC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,mCAAmC,CAAC,CAAC;QAC/C,OAAO,EAAE,CAAC;IACZ,CAAC;IAEM,SAAS,CAAC,MAAmB;QAClC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IACxE,CAAC;IAEM,KAAK,CAAC,GAAkB,EAAE,IAAY,EAAE,EAAU,EAAE,MAAY;QACrE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;YACjC,eAAe,CAAC,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,CAAC;IACZ,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ParamControl, type RenderContext, type Target, type Transform } from './transform.js';
|
|
2
|
+
export interface WarmthEdit {
|
|
3
|
+
edit: 'warmth';
|
|
4
|
+
/** -1 (cool) .. 1 (warm). Default 0. */
|
|
5
|
+
amount?: number;
|
|
6
|
+
}
|
|
7
|
+
type Full = Required<WarmthEdit>;
|
|
8
|
+
export declare class WarmthTransform implements Transform<'warmth', WarmthEdit> {
|
|
9
|
+
readonly edit: "warmth";
|
|
10
|
+
controls: ParamControl[];
|
|
11
|
+
defaults(): Full;
|
|
12
|
+
validate(params: WarmthEdit): string[];
|
|
13
|
+
normalize(params: WarmthEdit): Full;
|
|
14
|
+
apply(ctx: RenderContext, from: Target, to: Target, params: Full): Target;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=warmth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warmth.d.ts","sourceRoot":"","sources":["../../src/transforms/warmth.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,KAAK,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;AAejC,qBAAa,eAAgB,YAAW,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC;IACrE,SAAgB,IAAI,EAAG,QAAQ,CAAU;IAClC,QAAQ,EAAE,YAAY,EAAE,CAE7B;IAEK,QAAQ,IAAI,IAAI;IAIhB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE;IAMtC,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAInC,KAAK,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAG,MAAM;CAMjF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// warmth.ts — white-balance-style temperature shift (red/blue gains).
|
|
2
|
+
//
|
|
3
|
+
// r *= 1 + 0.3*amount, b *= 1 - 0.3*amount, g unchanged: +1 is warm
|
|
4
|
+
// (amber), -1 is cool (blue). Blacks stay black (multiplicative, like a
|
|
5
|
+
// white-balance gain). Same factors in src/transforms/warmth.h and
|
|
6
|
+
// math.ts warmthGains, so the canvas preview is WYSIWYG.
|
|
7
|
+
import { clampf } from '../math.js';
|
|
8
|
+
import { isNumInRange, setFloatUniform, } from './transform.js';
|
|
9
|
+
const FRAG = `
|
|
10
|
+
precision mediump float;
|
|
11
|
+
uniform sampler2D texture;
|
|
12
|
+
uniform float amount;
|
|
13
|
+
varying vec2 texCoord;
|
|
14
|
+
void main() {
|
|
15
|
+
vec4 color = texture2D(texture, texCoord);
|
|
16
|
+
color.r *= 1.0 + amount * 0.3;
|
|
17
|
+
color.b *= 1.0 - amount * 0.3;
|
|
18
|
+
gl_FragColor = color;
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
export class WarmthTransform {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.edit = 'warmth';
|
|
24
|
+
this.controls = [
|
|
25
|
+
{ key: 'amount', label: 'amount', min: -1, max: 1, step: 0.01, def: 0 },
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
defaults() {
|
|
29
|
+
return { edit: 'warmth', amount: 0 };
|
|
30
|
+
}
|
|
31
|
+
validate(params) {
|
|
32
|
+
if (params.amount !== undefined && !isNumInRange(params.amount, -1, 1))
|
|
33
|
+
return ['amount must be a number in [-1,1]'];
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
normalize(params) {
|
|
37
|
+
return { edit: 'warmth', amount: clampf(-1, params.amount ?? 0, 1) };
|
|
38
|
+
}
|
|
39
|
+
apply(ctx, from, to, params) {
|
|
40
|
+
ctx.pass(FRAG, from, to, (gl, p) => {
|
|
41
|
+
setFloatUniform(gl, p, 'amount', params.amount);
|
|
42
|
+
});
|
|
43
|
+
return to;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=warmth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warmth.js","sourceRoot":"","sources":["../../src/transforms/warmth.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,oEAAoE;AACpE,wEAAwE;AACxE,mEAAmE;AACnE,yDAAyD;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,OAAO,EACL,YAAY,EACZ,eAAe,GAKhB,MAAM,gBAAgB,CAAC;AAUxB,MAAM,IAAI,GAAG;;;;;;;;;;;CAWZ,CAAC;AAEF,MAAM,OAAO,eAAe;IAA5B;QACkB,SAAI,GAAG,QAAiB,CAAC;QAClC,aAAQ,GAAmB;YAChC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE;SACxE,CAAC;IAsBJ,CAAC;IApBQ,QAAQ;QACb,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACvC,CAAC;IAEM,QAAQ,CAAC,MAAkB;QAChC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;YACpE,OAAO,CAAC,mCAAmC,CAAC,CAAC;QAC/C,OAAO,EAAE,CAAC;IACZ,CAAC;IAEM,SAAS,CAAC,MAAkB;QACjC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IACvE,CAAC;IAEM,KAAK,CAAC,GAAkB,EAAE,IAAY,EAAE,EAAU,EAAE,MAAY;QACrE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;YACjC,eAAe,CAAC,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,CAAC;IACZ,CAAC;CACF"}
|
package/dist/types.d.ts
CHANGED
|
@@ -2,16 +2,20 @@ import type { BlurEdit } from './transforms/blur.js';
|
|
|
2
2
|
import type { BrightnessContrastEdit } from './transforms/brightnessContrast.js';
|
|
3
3
|
import type { CropEdit } from './transforms/crop.js';
|
|
4
4
|
import type { ExposureEdit } from './transforms/exposure.js';
|
|
5
|
+
import type { FlipEdit } from './transforms/flip.js';
|
|
5
6
|
import type { GrayscaleEdit } from './transforms/grayscale.js';
|
|
7
|
+
import type { HighlightsEdit } from './transforms/highlights.js';
|
|
6
8
|
import type { HueSaturationEdit } from './transforms/hueSaturation.js';
|
|
7
9
|
import type { InvertEdit } from './transforms/invert.js';
|
|
8
10
|
import type { RotateEdit } from './transforms/rotate.js';
|
|
9
11
|
import type { SepiaEdit } from './transforms/sepia.js';
|
|
12
|
+
import type { ShadowsEdit } from './transforms/shadows.js';
|
|
10
13
|
import type { SharpenEdit } from './transforms/sharpen.js';
|
|
11
14
|
import type { VibranceEdit } from './transforms/vibrance.js';
|
|
12
15
|
import type { VignetteEdit } from './transforms/vignette.js';
|
|
13
|
-
|
|
14
|
-
export type
|
|
16
|
+
import type { WarmthEdit } from './transforms/warmth.js';
|
|
17
|
+
export type EditName = 'brightnessContrast' | 'exposure' | 'hueSaturation' | 'sepia' | 'vibrance' | 'blur' | 'sharpen' | 'vignette' | 'shadows' | 'highlights' | 'warmth' | 'invert' | 'grayscale' | 'crop' | 'rotate' | 'flip';
|
|
18
|
+
export type Edit = BrightnessContrastEdit | ExposureEdit | HueSaturationEdit | SepiaEdit | VibranceEdit | BlurEdit | SharpenEdit | VignetteEdit | ShadowsEdit | HighlightsEdit | WarmthEdit | InvertEdit | GrayscaleEdit | CropEdit | RotateEdit | FlipEdit;
|
|
15
19
|
export declare const EDIT_STATE_VERSION: 1;
|
|
16
20
|
export interface EditState {
|
|
17
21
|
version: typeof EDIT_STATE_VERSION;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,MAAM,QAAQ,GAChB,oBAAoB,GACpB,UAAU,GACV,eAAe,GACf,OAAO,GACP,UAAU,GACV,MAAM,GACN,SAAS,GACT,UAAU,GACV,SAAS,GACT,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,MAAM,GACN,QAAQ,GACR,MAAM,CAAC;AAEX,MAAM,MAAM,IAAI,GACZ,sBAAsB,GACtB,YAAY,GACZ,iBAAiB,GACjB,SAAS,GACT,YAAY,GACZ,QAAQ,GACR,WAAW,GACX,YAAY,GACZ,WAAW,GACX,cAAc,GACd,UAAU,GACV,UAAU,GACV,aAAa,GACb,QAAQ,GACR,UAAU,GACV,QAAQ,CAAC;AAEb,eAAO,MAAM,kBAAkB,EAAG,CAAU,CAAC;AAE7C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,kBAAkB,CAAC;IACnC,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED,wBAAgB,UAAU,IAAI,SAAS,CAEtC"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA4DA,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAU,CAAC;AAO7C,MAAM,UAAU,UAAU;IACxB,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AACpD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radialapps/memories-vips-editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Headless WebGL preview library for vips-static edits. Renders an ordered edit list on a caller-provided canvas; serializes the same list as JSON for the vips-preview CLI (convert/thumbnail ... \"edits\": [...]).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// index.ts — minimal public surface: the pipeline,
|
|
2
|
-
// helpers, and the types needed to consume them (including
|
|
3
|
-
// custom Transform). Internal helpers (lookup, isNumInRange,
|
|
1
|
+
// index.ts — minimal public surface: the pipeline, small previews, the
|
|
2
|
+
// registry, state helpers, and the types needed to consume them (including
|
|
3
|
+
// writing a custom Transform). Internal helpers (lookup, isNumInRange,
|
|
4
4
|
// setFloatUniform, math utils, per-transform types) live in their own
|
|
5
5
|
// modules — import those directly.
|
|
6
6
|
export { EditPipeline } from './pipeline.js';
|
|
7
|
+
export { SmallPreview } from './smallPreview.js';
|
|
7
8
|
export { TRANSFORMS, validateEdit, normalizeEdit } from './transforms/index.js';
|
|
8
9
|
export { validateState, stateToJSON, stateFromJSON } from './transforms/state.js';
|
|
9
10
|
export type { Transform, ParamControl, RenderContext, Target } from './transforms/transform.js';
|
package/src/math.ts
CHANGED
|
@@ -18,6 +18,46 @@ export function exposureMult(stops: number): number {
|
|
|
18
18
|
return Math.pow(2, clampf(-5, stops, 5));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Shadows weight for a clamped 0..1 luma: (1-l)^2 (dark-localized). */
|
|
22
|
+
export function shadowsWeight(luma01: number): number {
|
|
23
|
+
const l = clampf(0, luma01, 1);
|
|
24
|
+
return (1 - l) * (1 - l);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Highlights weight for a clamped 0..1 luma: l^2 (bright-localized). */
|
|
28
|
+
export function highlightsWeight(luma01: number): number {
|
|
29
|
+
const l = clampf(0, luma01, 1);
|
|
30
|
+
return l * l;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Gray-pixel output of the shadows edit: v + (a*0.5)*(1-v)^2.
|
|
34
|
+
*
|
|
35
|
+
* The 0.5 keeps the curve monotonic increasing over the whole amount range
|
|
36
|
+
* (d/dv = 1-2k(1-v) >= 0 for k <= 0.5): without it black would lift past
|
|
37
|
+
* mid-gray at high amounts. Reference for the GLSL in shadows.ts and the
|
|
38
|
+
* loop in src/transforms/shadows.h.
|
|
39
|
+
*/
|
|
40
|
+
export function applyShadowsGray(v: number, amount: number): number {
|
|
41
|
+
return v + clampf(-1, amount, 1) * 0.5 * shadowsWeight(v);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Gray-pixel output of the highlights edit: v + (a*0.5)*v^2.
|
|
45
|
+
*
|
|
46
|
+
* The 0.5 keeps the curve monotonic increasing over the whole amount range
|
|
47
|
+
* (d/dv = 1+2kv >= 0 for k >= -0.5): without it white would recover below
|
|
48
|
+
* near-white at high negative amounts. Reference for the GLSL in
|
|
49
|
+
* highlights.ts and the loop in src/transforms/highlights.h.
|
|
50
|
+
*/
|
|
51
|
+
export function applyHighlightsGray(v: number, amount: number): number {
|
|
52
|
+
return v + clampf(-1, amount, 1) * 0.5 * highlightsWeight(v);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Red/blue white-balance gains for a warmth amount in [-1,1]. */
|
|
56
|
+
export function warmthGains(amount: number): { r: number; b: number } {
|
|
57
|
+
const a = clampf(-1, amount, 1);
|
|
58
|
+
return { r: 1 + a * 0.3, b: 1 - a * 0.3 };
|
|
59
|
+
}
|
|
60
|
+
|
|
21
61
|
/** 1D gaussian kernel for a blur radius (sigma = radius/2). Preview + docs.
|
|
22
62
|
*
|
|
23
63
|
* Single source of truth, ported term-for-term to src/transforms/gaussian.h:
|
package/src/pipeline.ts
CHANGED
|
@@ -183,7 +183,7 @@ export class EditPipeline {
|
|
|
183
183
|
const g = scratch.getContext('2d');
|
|
184
184
|
if (!g) throw new Error('setImage: 2D context unavailable for downscale');
|
|
185
185
|
g.imageSmoothingQuality = 'high';
|
|
186
|
-
g.drawImage(source
|
|
186
|
+
g.drawImage(EditPipeline.toDrawableSource(source), 0, 0, w, h);
|
|
187
187
|
upload = scratch;
|
|
188
188
|
}
|
|
189
189
|
this.imgW = w;
|
|
@@ -487,4 +487,28 @@ export class EditPipeline {
|
|
|
487
487
|
}
|
|
488
488
|
this.draw(BLIT_FRAG, from, null, this.outW, this.outH);
|
|
489
489
|
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* drawImage() rejects ImageData — a valid TexImageSource that texImage2D
|
|
493
|
+
* accepts — so stage ImageData through a 2D canvas first. Everything else
|
|
494
|
+
* passes through untouched. Duck-types ({width,height,data}) instead of
|
|
495
|
+
* instanceof so cross-realm ImageData works too.
|
|
496
|
+
*/
|
|
497
|
+
public static toDrawableSource(source: TexImageSource): CanvasImageSource {
|
|
498
|
+
const o = source as unknown as { data?: unknown; width?: unknown; height?: unknown };
|
|
499
|
+
if (
|
|
500
|
+
typeof o.width !== 'number' ||
|
|
501
|
+
typeof o.height !== 'number' ||
|
|
502
|
+
!(o.data instanceof Uint8ClampedArray)
|
|
503
|
+
) {
|
|
504
|
+
return source as unknown as CanvasImageSource;
|
|
505
|
+
}
|
|
506
|
+
const staged = document.createElement('canvas');
|
|
507
|
+
staged.width = o.width;
|
|
508
|
+
staged.height = o.height;
|
|
509
|
+
const g = staged.getContext('2d');
|
|
510
|
+
if (!g) throw new Error('toDrawableSource: 2D context unavailable for ImageData upload');
|
|
511
|
+
g.putImageData(source as ImageData, 0, 0);
|
|
512
|
+
return staged;
|
|
513
|
+
}
|
|
490
514
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// smallPreview.ts — cheap small previews for an edit list.
|
|
2
|
+
//
|
|
3
|
+
// A SmallPreview owns one hidden WebGL canvas + EditPipeline plus a
|
|
4
|
+
// downscaled 2D copy of the source (longest side <= maxDim, default 512).
|
|
5
|
+
// setSource() downscales and uploads once; each render() then only re-runs
|
|
6
|
+
// the edit passes and snapshots the result into a fresh caller-owned 2D
|
|
7
|
+
// canvas — no re-upload, no shader recompile, no disturbance of any main
|
|
8
|
+
// preview canvas. Returning a snapshot (not the live canvas) keeps repeated
|
|
9
|
+
// renders safe to hold (e.g. a filmstrip of edit variants).
|
|
10
|
+
//
|
|
11
|
+
// Pixel radii (blur/sharpen) are in preview pixels here, so large radii look
|
|
12
|
+
// relatively stronger than at full res; everything else matches the main
|
|
13
|
+
// preview. Each instance holds one WebGL context: call dispose() when done
|
|
14
|
+
// (browsers cap live contexts at ~16 per page).
|
|
15
|
+
import { EditPipeline } from './pipeline.js';
|
|
16
|
+
import type { Edit } from './types.js';
|
|
17
|
+
|
|
18
|
+
export class SmallPreview {
|
|
19
|
+
/** Default longest side of the downscaled working copy. */
|
|
20
|
+
public static readonly MAX_DIM = 512;
|
|
21
|
+
|
|
22
|
+
private pipe: EditPipeline | null = null;
|
|
23
|
+
private view: HTMLCanvasElement | null = null;
|
|
24
|
+
private hasSource = false;
|
|
25
|
+
private readonly maxDim: number;
|
|
26
|
+
|
|
27
|
+
public constructor(maxDim: number = SmallPreview.MAX_DIM) {
|
|
28
|
+
if (!Number.isFinite(maxDim) || maxDim < 1)
|
|
29
|
+
throw new Error('SmallPreview: maxDim must be a number >= 1');
|
|
30
|
+
this.maxDim = Math.floor(maxDim);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Last render output size (downscaled source size when no geometric edit applies). */
|
|
34
|
+
public get width(): number {
|
|
35
|
+
return this.pipe?.width ?? 0;
|
|
36
|
+
}
|
|
37
|
+
public get height(): number {
|
|
38
|
+
return this.pipe?.height ?? 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Downscale `source` to <= maxDim (aspect-fit, never upscaled) through a
|
|
43
|
+
* 2D canvas and upload it once to the hidden pipeline. Replaces any
|
|
44
|
+
* previous source. Throws when WebGL/2D is unavailable.
|
|
45
|
+
*/
|
|
46
|
+
public setSource(source: TexImageSource): void {
|
|
47
|
+
const dims = this.sourceDims(source);
|
|
48
|
+
if (!dims.w || !dims.h) throw new Error('setSource: source has no width/height');
|
|
49
|
+
// drawImage() rejects ImageData: stage it through a 2D canvas first.
|
|
50
|
+
const drawable = EditPipeline.toDrawableSource(source);
|
|
51
|
+
const longest = Math.max(dims.w, dims.h);
|
|
52
|
+
const scale = longest > this.maxDim ? this.maxDim / longest : 1;
|
|
53
|
+
const w = Math.max(1, Math.round(dims.w * scale));
|
|
54
|
+
const h = Math.max(1, Math.round(dims.h * scale));
|
|
55
|
+
const base = document.createElement('canvas');
|
|
56
|
+
base.width = w;
|
|
57
|
+
base.height = h;
|
|
58
|
+
const g = base.getContext('2d');
|
|
59
|
+
if (!g) throw new Error('setSource: 2D context unavailable for downscale');
|
|
60
|
+
g.imageSmoothingQuality = 'high';
|
|
61
|
+
g.drawImage(drawable, 0, 0, w, h);
|
|
62
|
+
if (!this.pipe || !this.view) {
|
|
63
|
+
const view = document.createElement('canvas');
|
|
64
|
+
this.view = view;
|
|
65
|
+
this.pipe = new EditPipeline(view);
|
|
66
|
+
}
|
|
67
|
+
this.pipe.setImage(base);
|
|
68
|
+
this.hasSource = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Render `edits` over the downscaled source and return the result as a
|
|
73
|
+
* fresh caller-owned 2D canvas (sized to the rendered output area).
|
|
74
|
+
* The hidden pipeline keeps its compiled programs between calls, so
|
|
75
|
+
* repeated renders only pay for the edit passes plus a small copy.
|
|
76
|
+
*/
|
|
77
|
+
public render(edits: Edit[]): HTMLCanvasElement {
|
|
78
|
+
const pipe = this.pipe;
|
|
79
|
+
const view = this.view;
|
|
80
|
+
if (!pipe || !view || !this.hasSource)
|
|
81
|
+
throw new Error('render: no source (call setSource first)');
|
|
82
|
+
pipe.setEdits(edits);
|
|
83
|
+
const out = document.createElement('canvas');
|
|
84
|
+
out.width = pipe.width;
|
|
85
|
+
out.height = pipe.height;
|
|
86
|
+
const g = out.getContext('2d');
|
|
87
|
+
if (!g) throw new Error('render: 2D context unavailable for snapshot');
|
|
88
|
+
// preserveDrawingBuffer (set by EditPipeline) keeps this reliable.
|
|
89
|
+
g.drawImage(view, 0, 0);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Release the hidden WebGL context; render() throws until setSource again. */
|
|
94
|
+
public dispose(): void {
|
|
95
|
+
const view = this.view;
|
|
96
|
+
this.pipe = null;
|
|
97
|
+
this.view = null;
|
|
98
|
+
this.hasSource = false;
|
|
99
|
+
if (!view) return;
|
|
100
|
+
try {
|
|
101
|
+
// getContext('webgl') returns the existing context for this canvas.
|
|
102
|
+
view.getContext('webgl')?.getExtension('WEBGL_lose_context')?.loseContext();
|
|
103
|
+
} catch {
|
|
104
|
+
// Context already gone — nothing to release.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Pixel size of any TexImageSource upload (mirrors EditPipeline). */
|
|
109
|
+
private sourceDims(source: TexImageSource): { w: number; h: number } {
|
|
110
|
+
const anySrc = source as unknown as Record<string, unknown>;
|
|
111
|
+
const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
112
|
+
const nw = num(anySrc['naturalWidth']);
|
|
113
|
+
const nh = num(anySrc['naturalHeight']);
|
|
114
|
+
if (nw > 0 && nh > 0) return { w: nw, h: nh };
|
|
115
|
+
const vw = num(anySrc['videoWidth']);
|
|
116
|
+
const vh = num(anySrc['videoHeight']);
|
|
117
|
+
if (vw > 0 && vh > 0) return { w: vw, h: vh };
|
|
118
|
+
return { w: num(anySrc['width']), h: num(anySrc['height']) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// flip.ts — mirror left-right (horizontal) or top-bottom (vertical).
|
|
2
|
+
//
|
|
3
|
+
// Pure UV swizzle (like rotate's quarter-turn path): exact under any
|
|
4
|
+
// precision, dims unchanged, alpha mirrors along untouched. The VIPS side
|
|
5
|
+
// (src/transforms/flip.h) uses fliphor()/flipver() and marks the edit
|
|
6
|
+
// geometric so the gainmap mirrors with the base image.
|
|
7
|
+
import type { ParamControl, RenderContext, Target, Transform } from './transform.js';
|
|
8
|
+
|
|
9
|
+
export type FlipDirection = 'horizontal' | 'vertical';
|
|
10
|
+
|
|
11
|
+
export interface FlipEdit {
|
|
12
|
+
edit: 'flip';
|
|
13
|
+
/** Mirror axis: 'horizontal' mirrors left-right. Default 'horizontal'. */
|
|
14
|
+
direction?: FlipDirection;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Full = Required<FlipEdit>;
|
|
18
|
+
|
|
19
|
+
const FRAG = `
|
|
20
|
+
precision mediump float;
|
|
21
|
+
uniform sampler2D texture;
|
|
22
|
+
uniform int vertical;
|
|
23
|
+
varying vec2 texCoord;
|
|
24
|
+
void main() {
|
|
25
|
+
vec2 uv = vertical == 1
|
|
26
|
+
? vec2(texCoord.x, 1.0 - texCoord.y)
|
|
27
|
+
: vec2(1.0 - texCoord.x, texCoord.y);
|
|
28
|
+
gl_FragColor = texture2D(texture, uv);
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
export class FlipTransform implements Transform<'flip', FlipEdit> {
|
|
33
|
+
public readonly edit = 'flip' as const;
|
|
34
|
+
public controls: ParamControl[] = [];
|
|
35
|
+
|
|
36
|
+
public defaults(): Full {
|
|
37
|
+
return { edit: 'flip', direction: 'horizontal' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public validate(params: FlipEdit): string[] {
|
|
41
|
+
if (
|
|
42
|
+
params.direction !== undefined &&
|
|
43
|
+
params.direction !== 'horizontal' &&
|
|
44
|
+
params.direction !== 'vertical'
|
|
45
|
+
)
|
|
46
|
+
return ['direction must be "horizontal" or "vertical"'];
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public normalize(params: FlipEdit): Full {
|
|
51
|
+
return {
|
|
52
|
+
edit: 'flip',
|
|
53
|
+
direction: params.direction === 'vertical' ? 'vertical' : 'horizontal',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public apply(ctx: RenderContext, from: Target, to: Target, params: Full): Target {
|
|
58
|
+
ctx.pass(FRAG, from, to, (gl, p) => {
|
|
59
|
+
const uV = gl.getUniformLocation(p, 'vertical');
|
|
60
|
+
if (uV) gl.uniform1i(uV, params.direction === 'vertical' ? 1 : 0);
|
|
61
|
+
});
|
|
62
|
+
return to;
|
|
63
|
+
}
|
|
64
|
+
}
|