@radialapps/memories-vips-editor 0.1.1 → 0.1.3
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 +18 -15
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +3 -4
- package/dist/math.js.map +1 -1
- package/dist/pipeline.d.ts +2 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +12 -22
- package/dist/pipeline.js.map +1 -1
- package/dist/shaders.d.ts +2 -3
- package/dist/shaders.d.ts.map +1 -1
- package/dist/shaders.js +9 -12
- package/dist/shaders.js.map +1 -1
- package/dist/transforms/crop.d.ts.map +1 -1
- package/dist/transforms/crop.js +7 -21
- package/dist/transforms/crop.js.map +1 -1
- package/dist/transforms/index.js +2 -2
- package/dist/transforms/index.js.map +1 -1
- package/dist/transforms/sepia.d.ts.map +1 -1
- package/dist/transforms/sepia.js +5 -4
- package/dist/transforms/sepia.js.map +1 -1
- package/dist/transforms/sharpen.js +1 -1
- package/dist/transforms/state.d.ts +1 -1
- package/dist/transforms/state.d.ts.map +1 -1
- package/dist/transforms/state.js +9 -9
- package/dist/transforms/state.js.map +1 -1
- package/dist/transforms/transform.d.ts +2 -2
- package/dist/transforms/transform.d.ts.map +1 -1
- package/dist/transforms/transform.js +4 -1
- package/dist/transforms/transform.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +11 -0
- package/src/math.ts +40 -0
- package/src/pipeline.ts +490 -0
- package/src/shaders.ts +55 -0
- package/src/transforms/blur.ts +51 -0
- package/src/transforms/brightnessContrast.ts +78 -0
- package/src/transforms/crop.ts +115 -0
- package/src/transforms/exposure.ts +59 -0
- package/src/transforms/grayscale.ts +42 -0
- package/src/transforms/hueSaturation.ts +86 -0
- package/src/transforms/index.ts +49 -0
- package/src/transforms/invert.ts +41 -0
- package/src/transforms/rotate.ts +211 -0
- package/src/transforms/sepia.ts +64 -0
- package/src/transforms/sharpen.ts +86 -0
- package/src/transforms/state.ts +26 -0
- package/src/transforms/transform.ts +104 -0
- package/src/transforms/vibrance.ts +61 -0
- package/src/transforms/vignette.ts +78 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// sepia.ts — glfx.js sepia port, minus its per-channel min(1.0): like the VIPS
|
|
2
|
+
// side, values stay unclipped until the end so later edits see the same input.
|
|
3
|
+
import { clampf } from '../math.js';
|
|
4
|
+
import {
|
|
5
|
+
isNumInRange,
|
|
6
|
+
setFloatUniform,
|
|
7
|
+
type ParamControl,
|
|
8
|
+
type RenderContext,
|
|
9
|
+
type Target,
|
|
10
|
+
type Transform,
|
|
11
|
+
} from './transform.js';
|
|
12
|
+
|
|
13
|
+
export interface SepiaEdit {
|
|
14
|
+
edit: 'sepia';
|
|
15
|
+
/** 0 (original) .. 1 (full sepia). Default 1. */
|
|
16
|
+
amount?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Full = Required<SepiaEdit>;
|
|
20
|
+
|
|
21
|
+
const FRAG = `
|
|
22
|
+
precision mediump float;
|
|
23
|
+
uniform sampler2D texture;
|
|
24
|
+
uniform float amount;
|
|
25
|
+
varying vec2 texCoord;
|
|
26
|
+
void main() {
|
|
27
|
+
vec4 color = texture2D(texture, texCoord);
|
|
28
|
+
float r = color.r;
|
|
29
|
+
float g = color.g;
|
|
30
|
+
float b = color.b;
|
|
31
|
+
color.r = (r * (1.0 - (0.607 * amount))) + (g * (0.769 * amount)) + (b * (0.189 * amount));
|
|
32
|
+
color.g = (r * 0.349 * amount) + (g * (1.0 - (0.314 * amount))) + (b * 0.168 * amount);
|
|
33
|
+
color.b = (r * 0.272 * amount) + (g * 0.534 * amount) + (b * (1.0 - (0.869 * amount)));
|
|
34
|
+
gl_FragColor = color;
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export class SepiaTransform implements Transform<'sepia', SepiaEdit> {
|
|
39
|
+
public readonly edit = 'sepia' as const;
|
|
40
|
+
public controls: ParamControl[] = [
|
|
41
|
+
{ key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01, def: 1 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
public defaults(): Full {
|
|
45
|
+
return { edit: 'sepia', amount: 1 };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public validate(params: SepiaEdit): string[] {
|
|
49
|
+
if (params.amount !== undefined && !isNumInRange(params.amount, 0, 1))
|
|
50
|
+
return ['amount must be a number in [0,1]'];
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public normalize(params: SepiaEdit): Full {
|
|
55
|
+
return { edit: 'sepia', amount: clampf(0, params.amount ?? 1, 1) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public apply(ctx: RenderContext, from: Target, to: Target, params: Full): Target {
|
|
59
|
+
ctx.pass(FRAG, from, to, (gl, p) => {
|
|
60
|
+
setFloatUniform(gl, p, 'amount', params.amount);
|
|
61
|
+
});
|
|
62
|
+
return to;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// sharpen.ts — glfx-style unsharp mask (gaussian + mix).
|
|
2
|
+
import { clampf, gaussianKernel } from '../math.js';
|
|
3
|
+
import {
|
|
4
|
+
isNumInRange,
|
|
5
|
+
setFloatUniform,
|
|
6
|
+
type ParamControl,
|
|
7
|
+
type RenderContext,
|
|
8
|
+
type Target,
|
|
9
|
+
type Transform,
|
|
10
|
+
} from './transform.js';
|
|
11
|
+
|
|
12
|
+
export interface SharpenEdit {
|
|
13
|
+
edit: 'sharpen';
|
|
14
|
+
/** Blur radius for the unsharp mask, in pixels, 0..50. Default 2. */
|
|
15
|
+
radius?: number;
|
|
16
|
+
/** 0 (no change) .. 5. Default 0.5. */
|
|
17
|
+
amount?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Full = Required<SharpenEdit>;
|
|
21
|
+
|
|
22
|
+
const MIX_FRAG = `
|
|
23
|
+
precision mediump float;
|
|
24
|
+
uniform sampler2D blurredTexture;
|
|
25
|
+
uniform sampler2D originalTexture;
|
|
26
|
+
uniform float strength;
|
|
27
|
+
varying vec2 texCoord;
|
|
28
|
+
void main() {
|
|
29
|
+
vec4 blurred = texture2D(blurredTexture, texCoord);
|
|
30
|
+
vec4 original = texture2D(originalTexture, texCoord);
|
|
31
|
+
gl_FragColor = vec4(mix(blurred.rgb, original.rgb, 1.0 + strength), original.a);
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export class SharpenTransform implements Transform<'sharpen', SharpenEdit> {
|
|
36
|
+
public readonly edit = 'sharpen' as const;
|
|
37
|
+
public controls: ParamControl[] = [
|
|
38
|
+
{ key: 'radius', label: 'radius px', min: 0, max: 20, step: 0.5, def: 2 },
|
|
39
|
+
{ key: 'amount', label: 'amount', min: 0, max: 5, step: 0.05, def: 0.5 },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
public defaults(): Full {
|
|
43
|
+
return { edit: 'sharpen', radius: 2, amount: 0.5 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public validate(params: SharpenEdit): string[] {
|
|
47
|
+
const errs: string[] = [];
|
|
48
|
+
if (params.radius !== undefined && !isNumInRange(params.radius, 0, 50))
|
|
49
|
+
errs.push('radius must be a number in [0,50]');
|
|
50
|
+
if (params.amount !== undefined && !isNumInRange(params.amount, 0, 5))
|
|
51
|
+
errs.push('amount must be a number in [0,5]');
|
|
52
|
+
return errs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public normalize(params: SharpenEdit): Full {
|
|
56
|
+
return {
|
|
57
|
+
edit: 'sharpen',
|
|
58
|
+
radius: clampf(0, params.radius ?? 2, 50),
|
|
59
|
+
amount: clampf(0, params.amount ?? 0.5, 5),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public apply(ctx: RenderContext, from: Target, to: Target, params: Full): Target {
|
|
64
|
+
const radius = params.radius * ctx.resolutionScale;
|
|
65
|
+
if (radius <= 0 || params.amount <= 0) {
|
|
66
|
+
ctx.blit(from, to);
|
|
67
|
+
return to;
|
|
68
|
+
}
|
|
69
|
+
ctx.blit(from, ctx.extra);
|
|
70
|
+
const kernel = gaussianKernel(radius);
|
|
71
|
+
ctx.gauss(from, to, true, kernel);
|
|
72
|
+
ctx.gauss(to, from, false, kernel);
|
|
73
|
+
ctx.pass(
|
|
74
|
+
MIX_FRAG,
|
|
75
|
+
from,
|
|
76
|
+
to,
|
|
77
|
+
(gl, p) => {
|
|
78
|
+
const uB = gl.getUniformLocation(p, 'blurredTexture');
|
|
79
|
+
if (uB) gl.uniform1i(uB, 0);
|
|
80
|
+
setFloatUniform(gl, p, 'strength', params.amount);
|
|
81
|
+
},
|
|
82
|
+
{ tex: ctx.extra.tex, unit: 1, name: 'originalTexture' },
|
|
83
|
+
);
|
|
84
|
+
return to;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// transforms/state.ts — {version, edits} state validation/serialization.
|
|
2
|
+
import { EDIT_STATE_VERSION, type EditState } from '../types.js';
|
|
3
|
+
import { normalizeEdit, validateEdit } from './index.js';
|
|
4
|
+
|
|
5
|
+
export function validateState(state: EditState): string[] {
|
|
6
|
+
if (typeof state !== 'object' || state === null) return ['state must be an object'];
|
|
7
|
+
const errs: string[] = [];
|
|
8
|
+
if (state.version !== EDIT_STATE_VERSION) errs.push(`version must be ${EDIT_STATE_VERSION}`);
|
|
9
|
+
if (!Array.isArray(state.edits)) return [...errs, 'edits must be an array'];
|
|
10
|
+
state.edits.forEach((edit, i) => {
|
|
11
|
+
for (const e of validateEdit(edit)) errs.push(`edits[${i}]: ${e}`);
|
|
12
|
+
});
|
|
13
|
+
return errs;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stateToJSON(state: EditState): string {
|
|
17
|
+
return JSON.stringify({ version: EDIT_STATE_VERSION, edits: state.edits.map(normalizeEdit) });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Parse + normalize; throws on invalid shape. */
|
|
21
|
+
export function stateFromJSON(json: string): EditState {
|
|
22
|
+
const raw = JSON.parse(json) as EditState;
|
|
23
|
+
const errs = validateState(raw);
|
|
24
|
+
if (errs.length > 0) throw new Error(`invalid edit state: ${errs.join('; ')}`);
|
|
25
|
+
return { version: EDIT_STATE_VERSION, edits: raw.edits.map(normalizeEdit) };
|
|
26
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// transform.ts — generic signature every transform implements.
|
|
2
|
+
//
|
|
3
|
+
// A transform is one named pixel edit (brightnessContrast, sepia, ...).
|
|
4
|
+
// The pipeline (pipeline.ts) stays generic: it looks the transform up by
|
|
5
|
+
// `edit.edit` in the registry (transforms/index.ts), normalizes params
|
|
6
|
+
// through it, and calls apply(). The VIPS side mirrors this shape with a
|
|
7
|
+
// `Transform` base class plus one header per transform (src/transforms/).
|
|
8
|
+
import type { EditName } from '../types.js';
|
|
9
|
+
|
|
10
|
+
export interface Target {
|
|
11
|
+
tex: WebGLTexture;
|
|
12
|
+
fb: WebGLFramebuffer;
|
|
13
|
+
/** Pixel size of the target (targets are reallocated on geometry change). */
|
|
14
|
+
w: number;
|
|
15
|
+
h: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ExtraTex {
|
|
19
|
+
tex: WebGLTexture;
|
|
20
|
+
unit: number;
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type UniformSetter = (gl: WebGLRenderingContext, prog: WebGLProgram) => void;
|
|
25
|
+
|
|
26
|
+
/** What a transform may do: single passes, blits, separable gaussian. */
|
|
27
|
+
export interface RenderContext {
|
|
28
|
+
/** Input (source) size for the current edit. */
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
/** Output size for the current edit (differs for crop/rotate). */
|
|
32
|
+
outWidth: number;
|
|
33
|
+
outHeight: number;
|
|
34
|
+
/** Fragment-shader highp support (rotate picks precision accordingly). */
|
|
35
|
+
highp: boolean;
|
|
36
|
+
/** Working/native resolution ratio; pixel-unit radii scale by this (1 when no downscale). */
|
|
37
|
+
resolutionScale: number;
|
|
38
|
+
extra: Target;
|
|
39
|
+
pass(
|
|
40
|
+
frag: string,
|
|
41
|
+
from: Target,
|
|
42
|
+
to: Target,
|
|
43
|
+
setUniforms?: UniformSetter,
|
|
44
|
+
extraTex?: ExtraTex,
|
|
45
|
+
): void;
|
|
46
|
+
blit(from: Target, to: Target): void;
|
|
47
|
+
gauss(from: Target, to: Target, horizontal: boolean, kernel: number[]): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ParamControl {
|
|
51
|
+
key: string;
|
|
52
|
+
label: string;
|
|
53
|
+
min: number;
|
|
54
|
+
max: number;
|
|
55
|
+
step: number;
|
|
56
|
+
def: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Transform<Name extends string, P extends object> {
|
|
60
|
+
/** Discriminator, e.g. 'sepia'. Matches the "edit" JSON key. */
|
|
61
|
+
readonly edit: Name;
|
|
62
|
+
/** No-change params (used by Add flows and normalize()). */
|
|
63
|
+
defaults(): Required<P>;
|
|
64
|
+
/** Slider schema for generic UIs (demo). */
|
|
65
|
+
controls: ParamControl[];
|
|
66
|
+
/** Human-readable problems; empty means VIPS-appliable. */
|
|
67
|
+
validate(params: P): string[];
|
|
68
|
+
/** Fill defaults + clamp, so output always passes validate(). */
|
|
69
|
+
normalize(params: P): Required<P>;
|
|
70
|
+
/** Render one edit reading `from`, writing `to`; returns result target. */
|
|
71
|
+
apply(ctx: RenderContext, from: Target, to: Target, params: Required<P>): Target;
|
|
72
|
+
/**
|
|
73
|
+
* Output size for the given input size (pixel ops omit this: identity).
|
|
74
|
+
* Geometric edits (crop/rotate) implement it with the exact VIPS formula
|
|
75
|
+
* so the pipeline can pre-size `to`; `apply` must fill exactly that.
|
|
76
|
+
*/
|
|
77
|
+
outputDims?(inW: number, inH: number, params: Required<P>): { w: number; h: number };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Number in [lo,hi], finite. Shared range check for validate(). */
|
|
81
|
+
export function isNumInRange(v: unknown, lo: number, hi: number): boolean {
|
|
82
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= lo && v <= hi;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function setFloatUniform(
|
|
86
|
+
gl: WebGLRenderingContext,
|
|
87
|
+
prog: WebGLProgram,
|
|
88
|
+
name: string,
|
|
89
|
+
value: number,
|
|
90
|
+
): void {
|
|
91
|
+
const u = gl.getUniformLocation(prog, name);
|
|
92
|
+
if (u) gl.uniform1f(u, value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Unknown runtime names (including Object.prototype keys) yield undefined. */
|
|
96
|
+
export function lookup(
|
|
97
|
+
registry: Record<EditName, Transform<string, object>>,
|
|
98
|
+
name: unknown,
|
|
99
|
+
): Transform<string, object> | undefined {
|
|
100
|
+
if (typeof name !== 'string' || !Object.prototype.hasOwnProperty.call(registry, name)) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return registry[name as EditName];
|
|
104
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// vibrance.ts — glfx.js vibrance port.
|
|
2
|
+
import { clampf } from '../math.js';
|
|
3
|
+
import {
|
|
4
|
+
isNumInRange,
|
|
5
|
+
setFloatUniform,
|
|
6
|
+
type ParamControl,
|
|
7
|
+
type RenderContext,
|
|
8
|
+
type Target,
|
|
9
|
+
type Transform,
|
|
10
|
+
} from './transform.js';
|
|
11
|
+
|
|
12
|
+
export interface VibranceEdit {
|
|
13
|
+
edit: 'vibrance';
|
|
14
|
+
/** -1 (muted) .. 1 (max). Default 0. */
|
|
15
|
+
amount?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Full = Required<VibranceEdit>;
|
|
19
|
+
|
|
20
|
+
const FRAG = `
|
|
21
|
+
precision mediump float;
|
|
22
|
+
uniform sampler2D texture;
|
|
23
|
+
uniform float amount;
|
|
24
|
+
varying vec2 texCoord;
|
|
25
|
+
void main() {
|
|
26
|
+
vec4 color = texture2D(texture, texCoord);
|
|
27
|
+
float average = (color.r + color.g + color.b) / 3.0;
|
|
28
|
+
float mx = max(color.r, max(color.g, color.b));
|
|
29
|
+
float amt = (mx - average) * (-amount * 3.0);
|
|
30
|
+
color.rgb = mix(color.rgb, vec3(mx), amt);
|
|
31
|
+
gl_FragColor = color;
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export class VibranceTransform implements Transform<'vibrance', VibranceEdit> {
|
|
36
|
+
public readonly edit = 'vibrance' as const;
|
|
37
|
+
public controls: ParamControl[] = [
|
|
38
|
+
{ key: 'amount', label: 'amount', min: -1, max: 1, step: 0.01, def: 0 },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
public defaults(): Full {
|
|
42
|
+
return { edit: 'vibrance', amount: 0 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public validate(params: VibranceEdit): string[] {
|
|
46
|
+
if (params.amount !== undefined && !isNumInRange(params.amount, -1, 1))
|
|
47
|
+
return ['amount must be a number in [-1,1]'];
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public normalize(params: VibranceEdit): Full {
|
|
52
|
+
return { edit: 'vibrance', amount: clampf(-1, params.amount ?? 0, 1) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public apply(ctx: RenderContext, from: Target, to: Target, params: Full): Target {
|
|
56
|
+
ctx.pass(FRAG, from, to, (gl, p) => {
|
|
57
|
+
setFloatUniform(gl, p, 'amount', params.amount);
|
|
58
|
+
});
|
|
59
|
+
return to;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// vignette.ts — glfx.js vignette port (closed-form smoothstep).
|
|
2
|
+
import { clampf } from '../math.js';
|
|
3
|
+
import {
|
|
4
|
+
isNumInRange,
|
|
5
|
+
setFloatUniform,
|
|
6
|
+
type ParamControl,
|
|
7
|
+
type RenderContext,
|
|
8
|
+
type Target,
|
|
9
|
+
type Transform,
|
|
10
|
+
} from './transform.js';
|
|
11
|
+
|
|
12
|
+
export interface VignetteEdit {
|
|
13
|
+
edit: 'vignette';
|
|
14
|
+
/** 0 (center) .. 1 (edge). Default 0.5. */
|
|
15
|
+
size?: number;
|
|
16
|
+
/** 0 (none) .. 1 (max darkening). Default 0.5. */
|
|
17
|
+
amount?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Full = Required<VignetteEdit>;
|
|
21
|
+
|
|
22
|
+
const FRAG = `
|
|
23
|
+
precision mediump float;
|
|
24
|
+
uniform sampler2D texture;
|
|
25
|
+
uniform float size;
|
|
26
|
+
uniform float amount;
|
|
27
|
+
varying vec2 texCoord;
|
|
28
|
+
void main() {
|
|
29
|
+
vec4 color = texture2D(texture, texCoord);
|
|
30
|
+
float dx = texCoord.x - 0.5;
|
|
31
|
+
float dy = texCoord.y - 0.5;
|
|
32
|
+
float dist = sqrt(dx * dx + dy * dy);
|
|
33
|
+
float d = dist * (amount + size);
|
|
34
|
+
float e0 = 0.8;
|
|
35
|
+
float e1 = size * 0.799;
|
|
36
|
+
float t = clamp((d - e0) / (e1 - e0), 0.0, 1.0);
|
|
37
|
+
float s = t * t * (3.0 - 2.0 * t);
|
|
38
|
+
color.rgb *= s;
|
|
39
|
+
gl_FragColor = color;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
export class VignetteTransform implements Transform<'vignette', VignetteEdit> {
|
|
44
|
+
public readonly edit = 'vignette' as const;
|
|
45
|
+
public controls: ParamControl[] = [
|
|
46
|
+
{ key: 'size', label: 'size', min: 0, max: 1, step: 0.01, def: 0.5 },
|
|
47
|
+
{ key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01, def: 0.5 },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
public defaults(): Full {
|
|
51
|
+
return { edit: 'vignette', size: 0.5, amount: 0.5 };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public validate(params: VignetteEdit): string[] {
|
|
55
|
+
const errs: string[] = [];
|
|
56
|
+
if (params.size !== undefined && !isNumInRange(params.size, 0, 1))
|
|
57
|
+
errs.push('size must be a number in [0,1]');
|
|
58
|
+
if (params.amount !== undefined && !isNumInRange(params.amount, 0, 1))
|
|
59
|
+
errs.push('amount must be a number in [0,1]');
|
|
60
|
+
return errs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public normalize(params: VignetteEdit): Full {
|
|
64
|
+
return {
|
|
65
|
+
edit: 'vignette',
|
|
66
|
+
size: clampf(0, params.size ?? 0.5, 1),
|
|
67
|
+
amount: clampf(0, params.amount ?? 0.5, 1),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public apply(ctx: RenderContext, from: Target, to: Target, params: Full): Target {
|
|
72
|
+
ctx.pass(FRAG, from, to, (gl, p) => {
|
|
73
|
+
setFloatUniform(gl, p, 'size', params.size);
|
|
74
|
+
setFloatUniform(gl, p, 'amount', params.amount);
|
|
75
|
+
});
|
|
76
|
+
return to;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// types.ts — edit list model shared by the WebGL preview and the VIPS applier.
|
|
2
|
+
//
|
|
3
|
+
// JSON wire format (what the client serializes / what vips-preview accepts as
|
|
4
|
+
// the "edits" field of convert/thumbnail):
|
|
5
|
+
// { "version": 1, "edits": [ { "edit": "brightnessContrast", ... }, ... ] }
|
|
6
|
+
// Per-edit shapes live next to their Transform implementations
|
|
7
|
+
// (src/transforms/); this file only forms the union.
|
|
8
|
+
import type { BlurEdit } from './transforms/blur.js';
|
|
9
|
+
import type { BrightnessContrastEdit } from './transforms/brightnessContrast.js';
|
|
10
|
+
import type { CropEdit } from './transforms/crop.js';
|
|
11
|
+
import type { ExposureEdit } from './transforms/exposure.js';
|
|
12
|
+
import type { GrayscaleEdit } from './transforms/grayscale.js';
|
|
13
|
+
import type { HueSaturationEdit } from './transforms/hueSaturation.js';
|
|
14
|
+
import type { InvertEdit } from './transforms/invert.js';
|
|
15
|
+
import type { RotateEdit } from './transforms/rotate.js';
|
|
16
|
+
import type { SepiaEdit } from './transforms/sepia.js';
|
|
17
|
+
import type { SharpenEdit } from './transforms/sharpen.js';
|
|
18
|
+
import type { VibranceEdit } from './transforms/vibrance.js';
|
|
19
|
+
import type { VignetteEdit } from './transforms/vignette.js';
|
|
20
|
+
|
|
21
|
+
export type EditName =
|
|
22
|
+
| 'brightnessContrast'
|
|
23
|
+
| 'exposure'
|
|
24
|
+
| 'hueSaturation'
|
|
25
|
+
| 'sepia'
|
|
26
|
+
| 'vibrance'
|
|
27
|
+
| 'blur'
|
|
28
|
+
| 'sharpen'
|
|
29
|
+
| 'vignette'
|
|
30
|
+
| 'invert'
|
|
31
|
+
| 'grayscale'
|
|
32
|
+
| 'crop'
|
|
33
|
+
| 'rotate';
|
|
34
|
+
|
|
35
|
+
export type Edit =
|
|
36
|
+
| BrightnessContrastEdit
|
|
37
|
+
| ExposureEdit
|
|
38
|
+
| HueSaturationEdit
|
|
39
|
+
| SepiaEdit
|
|
40
|
+
| VibranceEdit
|
|
41
|
+
| BlurEdit
|
|
42
|
+
| SharpenEdit
|
|
43
|
+
| VignetteEdit
|
|
44
|
+
| InvertEdit
|
|
45
|
+
| GrayscaleEdit
|
|
46
|
+
| CropEdit
|
|
47
|
+
| RotateEdit;
|
|
48
|
+
|
|
49
|
+
export const EDIT_STATE_VERSION = 1 as const;
|
|
50
|
+
|
|
51
|
+
export interface EditState {
|
|
52
|
+
version: typeof EDIT_STATE_VERSION;
|
|
53
|
+
edits: Edit[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function emptyState(): EditState {
|
|
57
|
+
return { version: EDIT_STATE_VERSION, edits: [] };
|
|
58
|
+
}
|