tokimeki-image-editor 0.1.13 → 0.2.1
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/dist/components/AnnotationTool.svelte +900 -0
- package/dist/components/AnnotationTool.svelte.d.ts +15 -0
- package/dist/components/Canvas.svelte +14 -7
- package/dist/components/Canvas.svelte.d.ts +2 -1
- package/dist/components/ImageEditor.svelte +30 -4
- package/dist/components/Toolbar.svelte +12 -1
- package/dist/i18n/locales/en.json +13 -0
- package/dist/i18n/locales/ja.json +13 -0
- package/dist/types.d.ts +16 -1
- package/dist/utils/canvas.d.ts +9 -4
- package/dist/utils/canvas.js +140 -8
- package/dist/utils/history.d.ts +1 -1
- package/dist/utils/history.js +6 -2
- package/package.json +1 -1
- package/dist/shaders/blur.wgsl +0 -59
- package/dist/shaders/composite.wgsl +0 -46
- package/dist/shaders/grain.wgsl +0 -225
- package/dist/shaders/image-editor.wgsl +0 -296
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
struct VertexOutput {
|
|
2
|
-
@builtin(position) position: vec4<f32>,
|
|
3
|
-
@location(0) uv: vec2<f32>,
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
struct CompositeUniforms {
|
|
7
|
-
// Blur area bounds in normalized coordinates (0-1)
|
|
8
|
-
minX: f32,
|
|
9
|
-
minY: f32,
|
|
10
|
-
maxX: f32,
|
|
11
|
-
maxY: f32,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
@group(0) @binding(0) var mySampler: sampler;
|
|
15
|
-
@group(0) @binding(1) var blurredTexture: texture_2d<f32>;
|
|
16
|
-
@group(0) @binding(2) var baseTexture: texture_2d<f32>;
|
|
17
|
-
@group(0) @binding(3) var<uniform> composite: CompositeUniforms;
|
|
18
|
-
|
|
19
|
-
@vertex
|
|
20
|
-
fn vs_main(@builtin(vertex_index) VertexIndex: u32) -> VertexOutput {
|
|
21
|
-
var pos = array<vec2<f32>, 3>(
|
|
22
|
-
vec2<f32>(-1.0, -1.0),
|
|
23
|
-
vec2<f32>( 3.0, -1.0),
|
|
24
|
-
vec2<f32>(-1.0, 3.0)
|
|
25
|
-
);
|
|
26
|
-
var output: VertexOutput;
|
|
27
|
-
output.position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
|
|
28
|
-
// Convert NDC to UV with Y-flip (NDC bottom-left maps to UV top-left)
|
|
29
|
-
let uv = pos[VertexIndex] * 0.5 + 0.5;
|
|
30
|
-
output.uv = vec2<f32>(uv.x, 1.0 - uv.y);
|
|
31
|
-
return output;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@fragment
|
|
35
|
-
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
|
36
|
-
// Sample both textures first (must be in uniform control flow)
|
|
37
|
-
let blurredColor = textureSample(blurredTexture, mySampler, uv);
|
|
38
|
-
let baseColor = textureSample(baseTexture, mySampler, uv);
|
|
39
|
-
|
|
40
|
-
// Check if pixel is inside blur area bounds
|
|
41
|
-
let inside = uv.x >= composite.minX && uv.x <= composite.maxX &&
|
|
42
|
-
uv.y >= composite.minY && uv.y <= composite.maxY;
|
|
43
|
-
|
|
44
|
-
// Use select to choose between blurred and base
|
|
45
|
-
return select(baseColor, blurredColor, inside);
|
|
46
|
-
}
|
package/dist/shaders/grain.wgsl
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// Grain shader - applies film grain on top of processed image
|
|
2
|
-
// This shader is designed to be used as a final pass after blur
|
|
3
|
-
|
|
4
|
-
struct VertexOutput {
|
|
5
|
-
@builtin(position) position: vec4<f32>,
|
|
6
|
-
@location(0) uv: vec2<f32>,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
struct GrainUniforms {
|
|
10
|
-
// Grain parameters (1 param)
|
|
11
|
-
grainAmount: f32, // 0 to 100
|
|
12
|
-
|
|
13
|
-
// Viewport (4 params)
|
|
14
|
-
viewportZoom: f32,
|
|
15
|
-
viewportOffsetX: f32,
|
|
16
|
-
viewportOffsetY: f32,
|
|
17
|
-
viewportScale: f32,
|
|
18
|
-
|
|
19
|
-
// Transform (4 params)
|
|
20
|
-
rotation: f32, // in radians
|
|
21
|
-
flipHorizontal: f32, // 1.0 or -1.0
|
|
22
|
-
flipVertical: f32, // 1.0 or -1.0
|
|
23
|
-
transformScale: f32,
|
|
24
|
-
|
|
25
|
-
// Canvas dimensions (2 params)
|
|
26
|
-
canvasWidth: f32,
|
|
27
|
-
canvasHeight: f32,
|
|
28
|
-
|
|
29
|
-
// Image dimensions (2 params)
|
|
30
|
-
imageWidth: f32,
|
|
31
|
-
imageHeight: f32,
|
|
32
|
-
|
|
33
|
-
// Crop area (4 params)
|
|
34
|
-
cropX: f32,
|
|
35
|
-
cropY: f32,
|
|
36
|
-
cropWidth: f32,
|
|
37
|
-
cropHeight: f32,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
@group(0) @binding(0) var mySampler: sampler;
|
|
41
|
-
@group(0) @binding(1) var myTexture: texture_2d<f32>;
|
|
42
|
-
@group(0) @binding(2) var<uniform> params: GrainUniforms;
|
|
43
|
-
|
|
44
|
-
// Simple pass-through vertex shader
|
|
45
|
-
@vertex
|
|
46
|
-
fn vs_main(@builtin(vertex_index) VertexIndex: u32) -> VertexOutput {
|
|
47
|
-
var pos = array<vec2<f32>, 3>(
|
|
48
|
-
vec2<f32>(-1.0, -1.0),
|
|
49
|
-
vec2<f32>( 3.0, -1.0),
|
|
50
|
-
vec2<f32>(-1.0, 3.0)
|
|
51
|
-
);
|
|
52
|
-
var output: VertexOutput;
|
|
53
|
-
output.position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
|
|
54
|
-
// UV coordinates map directly to texture (0-1)
|
|
55
|
-
output.uv = pos[VertexIndex] * 0.5 + 0.5;
|
|
56
|
-
output.uv.y = 1.0 - output.uv.y; // Flip Y for texture coordinates
|
|
57
|
-
return output;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Helper functions
|
|
61
|
-
fn getLuminance(color: vec3<f32>) -> f32 {
|
|
62
|
-
return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// High-quality 2D hash function for uniform random numbers in [0, 1]
|
|
66
|
-
fn hash2d(p: vec2<f32>) -> f32 {
|
|
67
|
-
let p3 = fract(vec3<f32>(p.x, p.y, p.x) * vec3<f32>(0.1031, 0.1030, 0.0973));
|
|
68
|
-
let dot_p3 = dot(p3, vec3<f32>(p3.y, p3.z, p3.x) + 33.33);
|
|
69
|
-
return fract((p3.x + p3.y) * p3.z + dot_p3);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Generate two independent uniform random numbers
|
|
73
|
-
fn hash2d_dual(p: vec2<f32>) -> vec2<f32> {
|
|
74
|
-
let p3 = fract(vec3<f32>(p.x, p.y, p.x) * vec3<f32>(0.1031, 0.1030, 0.0973));
|
|
75
|
-
let p4 = fract(vec3<f32>(p.y, p.x, p.y) * vec3<f32>(0.0973, 0.1031, 0.1030));
|
|
76
|
-
let dot_p3 = dot(p3, vec3<f32>(p3.y, p3.z, p3.x) + 33.33);
|
|
77
|
-
let dot_p4 = dot(p4, vec3<f32>(p4.y, p4.z, p4.x) + 45.67);
|
|
78
|
-
return vec2<f32>(
|
|
79
|
-
fract((p3.x + p3.y) * p3.z + dot_p3),
|
|
80
|
-
fract((p4.x + p4.y) * p4.z + dot_p4)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Box-Muller transform: converts two uniform random variables to Gaussian distribution
|
|
85
|
-
// Returns a sample from N(0, 1) - standard normal distribution
|
|
86
|
-
fn boxMuller(u1: f32, u2: f32) -> f32 {
|
|
87
|
-
// Ensure u1 is not zero to avoid log(0)
|
|
88
|
-
let u1_safe = max(u1, 0.0001);
|
|
89
|
-
let radius = sqrt(-2.0 * log(u1_safe));
|
|
90
|
-
let theta = 2.0 * 3.14159265359 * u2;
|
|
91
|
-
return radius * cos(theta);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Generate Gaussian-distributed noise using Box-Muller transform
|
|
95
|
-
fn gaussianNoise(p: vec2<f32>) -> f32 {
|
|
96
|
-
let uniforms = hash2d_dual(p);
|
|
97
|
-
return boxMuller(uniforms.x, uniforms.y);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Overlay blend mode - enhances contrast
|
|
101
|
-
// When a < 0.5: result = 2 * a * b
|
|
102
|
-
// When a >= 0.5: result = 1 - 2 * (1-a) * (1-b)
|
|
103
|
-
fn overlayBlend(base: f32, blend: f32) -> f32 {
|
|
104
|
-
if (base < 0.5) {
|
|
105
|
-
return 2.0 * base * blend;
|
|
106
|
-
} else {
|
|
107
|
-
return 1.0 - 2.0 * (1.0 - base) * (1.0 - blend);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Soft light blend mode - gentler than overlay
|
|
112
|
-
fn softLightBlend(base: f32, blend: f32) -> f32 {
|
|
113
|
-
if (blend < 0.5) {
|
|
114
|
-
return base - (1.0 - 2.0 * blend) * base * (1.0 - base);
|
|
115
|
-
} else {
|
|
116
|
-
let d = select(sqrt(base), base, base < 0.25);
|
|
117
|
-
return base + (2.0 * blend - 1.0) * (d - base);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
@fragment
|
|
122
|
-
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
|
123
|
-
// Sample the blurred/processed texture
|
|
124
|
-
var color = textureSample(myTexture, mySampler, uv);
|
|
125
|
-
var rgb = color.rgb;
|
|
126
|
-
|
|
127
|
-
// Calculate image-space coordinates for grain
|
|
128
|
-
// Convert UV (0-1) to canvas coordinates
|
|
129
|
-
var canvasCoord = uv * vec2<f32>(params.canvasWidth, params.canvasHeight);
|
|
130
|
-
|
|
131
|
-
// Convert canvas coordinates (top-left origin) to centered coordinates
|
|
132
|
-
var coord = canvasCoord - vec2<f32>(params.canvasWidth * 0.5, params.canvasHeight * 0.5);
|
|
133
|
-
|
|
134
|
-
// Reverse the viewport transformations to get image-space coordinates
|
|
135
|
-
// 1. Subtract viewport offset
|
|
136
|
-
coord = coord - vec2<f32>(params.viewportOffsetX, params.viewportOffsetY);
|
|
137
|
-
|
|
138
|
-
// 2. Inverse zoom/scale
|
|
139
|
-
let totalScale = params.viewportScale * params.viewportZoom * params.transformScale;
|
|
140
|
-
coord = coord / totalScale;
|
|
141
|
-
|
|
142
|
-
// 3. Inverse rotation
|
|
143
|
-
if (params.rotation != 0.0) {
|
|
144
|
-
let cos_r = cos(-params.rotation);
|
|
145
|
-
let sin_r = sin(-params.rotation);
|
|
146
|
-
coord = vec2<f32>(
|
|
147
|
-
coord.x * cos_r - coord.y * sin_r,
|
|
148
|
-
coord.x * sin_r + coord.y * cos_r
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// 4. Inverse flip
|
|
153
|
-
coord.x = coord.x * params.flipHorizontal;
|
|
154
|
-
coord.y = coord.y * params.flipVertical;
|
|
155
|
-
|
|
156
|
-
// 5. Convert to image pixel coordinates
|
|
157
|
-
var imageCoord: vec2<f32>;
|
|
158
|
-
if (params.cropWidth > 0.0 && params.cropHeight > 0.0) {
|
|
159
|
-
// Crop mode: coord is in crop-centered space
|
|
160
|
-
let cropLocalX = coord.x + params.cropWidth * 0.5;
|
|
161
|
-
let cropLocalY = coord.y + params.cropHeight * 0.5;
|
|
162
|
-
imageCoord = vec2<f32>(
|
|
163
|
-
params.cropX + cropLocalX,
|
|
164
|
-
params.cropY + cropLocalY
|
|
165
|
-
);
|
|
166
|
-
} else {
|
|
167
|
-
// No crop: coord is in image-centered space
|
|
168
|
-
imageCoord = coord + vec2<f32>(params.imageWidth * 0.5, params.imageHeight * 0.5);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Apply film grain if enabled
|
|
172
|
-
if (params.grainAmount > 0.0) {
|
|
173
|
-
let grainIntensity = params.grainAmount / 100.0;
|
|
174
|
-
|
|
175
|
-
// Calculate luminance for grain masking
|
|
176
|
-
let luma = getLuminance(rgb);
|
|
177
|
-
|
|
178
|
-
// === Authentic film grain using uniform distribution ===
|
|
179
|
-
// Film grain is random, sharp, and granular - not smooth Gaussian
|
|
180
|
-
|
|
181
|
-
// Primary grain layer (2.5-3 pixels) - sharp, visible grain
|
|
182
|
-
let grain1 = hash2d(floor(imageCoord / 2.8)) - 0.5;
|
|
183
|
-
|
|
184
|
-
// Secondary grain layer (3.5-4 pixels) - adds texture variation
|
|
185
|
-
let grain2 = hash2d(floor(imageCoord / 3.6) + vec2<f32>(123.45, 678.90)) - 0.5;
|
|
186
|
-
|
|
187
|
-
// Tertiary grain layer (5-6 pixels) - larger grain structure
|
|
188
|
-
let grain3 = hash2d(floor(imageCoord / 5.2) + vec2<f32>(345.67, 890.12)) - 0.5;
|
|
189
|
-
|
|
190
|
-
// Coarse grain layer (7-8 pixels) - clumping effect
|
|
191
|
-
let grain4 = hash2d(floor(imageCoord / 7.5) + vec2<f32>(567.89, 234.56)) - 0.5;
|
|
192
|
-
|
|
193
|
-
// Combine with emphasis on visible mid-size grain
|
|
194
|
-
// Sharp transitions create authentic film texture
|
|
195
|
-
let grainNoise = grain1 * 0.35 + grain2 * 0.3 + grain3 * 0.25 + grain4 * 0.1;
|
|
196
|
-
|
|
197
|
-
// Enhance grain contrast for more pronounced effect
|
|
198
|
-
let enhancedGrain = sign(grainNoise) * pow(abs(grainNoise), 0.8);
|
|
199
|
-
|
|
200
|
-
// === Luminance-based masking ===
|
|
201
|
-
// Grain visible across most tones, natural falloff at extremes
|
|
202
|
-
var lumaMask = 1.0 - pow(abs(luma - 0.5) * 2.0, 1.5);
|
|
203
|
-
lumaMask = smoothstep(0.1, 1.0, lumaMask);
|
|
204
|
-
|
|
205
|
-
// Hide grain in very dark areas (outside image bounds)
|
|
206
|
-
let shadowMask = smoothstep(0.02, 0.12, luma);
|
|
207
|
-
|
|
208
|
-
// Slightly reduce grain in very bright areas
|
|
209
|
-
let highlightMask = 1.0 - smoothstep(0.9, 1.0, luma);
|
|
210
|
-
|
|
211
|
-
lumaMask = lumaMask * shadowMask * highlightMask;
|
|
212
|
-
|
|
213
|
-
// === Apply strong, visible grain ===
|
|
214
|
-
let grainStrength = grainIntensity * 0.35;
|
|
215
|
-
|
|
216
|
-
// Apply grain with luminance masking
|
|
217
|
-
let grainValue = enhancedGrain * grainStrength * lumaMask;
|
|
218
|
-
rgb = rgb + vec3<f32>(grainValue);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Clamp final result
|
|
222
|
-
rgb = clamp(rgb, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
223
|
-
|
|
224
|
-
return vec4<f32>(rgb, color.a);
|
|
225
|
-
}
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
struct VertexOutput {
|
|
2
|
-
@builtin(position) position: vec4<f32>,
|
|
3
|
-
@location(0) uv: vec2<f32>,
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
struct Uniforms {
|
|
7
|
-
// Adjustments (11 params)
|
|
8
|
-
brightness: f32,
|
|
9
|
-
contrast: f32,
|
|
10
|
-
exposure: f32,
|
|
11
|
-
highlights: f32,
|
|
12
|
-
shadows: f32,
|
|
13
|
-
saturation: f32,
|
|
14
|
-
temperature: f32,
|
|
15
|
-
sepia: f32,
|
|
16
|
-
grayscale: f32,
|
|
17
|
-
vignette: f32,
|
|
18
|
-
grain: f32,
|
|
19
|
-
|
|
20
|
-
// Viewport (4 params)
|
|
21
|
-
viewportZoom: f32,
|
|
22
|
-
viewportOffsetX: f32,
|
|
23
|
-
viewportOffsetY: f32,
|
|
24
|
-
viewportScale: f32,
|
|
25
|
-
|
|
26
|
-
// Transform (4 params)
|
|
27
|
-
rotation: f32, // in radians
|
|
28
|
-
flipHorizontal: f32, // 1.0 or -1.0
|
|
29
|
-
flipVertical: f32, // 1.0 or -1.0
|
|
30
|
-
transformScale: f32,
|
|
31
|
-
|
|
32
|
-
// Canvas dimensions (2 params)
|
|
33
|
-
canvasWidth: f32,
|
|
34
|
-
canvasHeight: f32,
|
|
35
|
-
|
|
36
|
-
// Image dimensions (2 params)
|
|
37
|
-
imageWidth: f32,
|
|
38
|
-
imageHeight: f32,
|
|
39
|
-
|
|
40
|
-
// Crop area (4 params)
|
|
41
|
-
cropX: f32,
|
|
42
|
-
cropY: f32,
|
|
43
|
-
cropWidth: f32,
|
|
44
|
-
cropHeight: f32,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
@group(0) @binding(0) var mySampler: sampler;
|
|
48
|
-
@group(0) @binding(1) var myTexture: texture_2d<f32>;
|
|
49
|
-
@group(0) @binding(2) var<uniform> params: Uniforms;
|
|
50
|
-
|
|
51
|
-
// Full-screen triangle vertex shader
|
|
52
|
-
@vertex
|
|
53
|
-
fn vs_main(@builtin(vertex_index) VertexIndex: u32) -> VertexOutput {
|
|
54
|
-
var pos = array<vec2<f32>, 3>(
|
|
55
|
-
vec2<f32>(-1.0, -1.0),
|
|
56
|
-
vec2<f32>( 3.0, -1.0),
|
|
57
|
-
vec2<f32>(-1.0, 3.0)
|
|
58
|
-
);
|
|
59
|
-
var output: VertexOutput;
|
|
60
|
-
output.position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
|
|
61
|
-
|
|
62
|
-
// Convert NDC (-1 to 1) to canvas coordinates centered at origin
|
|
63
|
-
// NDC: -1 → -canvasWidth/2, 0 → 0, 1 → canvasWidth/2
|
|
64
|
-
// Note: NDC Y increases upward, but canvas Y increases downward, so flip Y
|
|
65
|
-
var coord = pos[VertexIndex] * vec2<f32>(params.canvasWidth, params.canvasHeight) * 0.5;
|
|
66
|
-
coord.y = -coord.y;
|
|
67
|
-
|
|
68
|
-
// Reverse the 2D canvas transformations:
|
|
69
|
-
// In 2D: translate(center + offset) → scale(zoom) → rotate → flip → draw(-w/2, -h/2)
|
|
70
|
-
// In WebGPU (reverse): screen → un-translate → un-scale → un-rotate → un-flip → texture
|
|
71
|
-
|
|
72
|
-
// 1. Subtract viewport offset (inverse of translate)
|
|
73
|
-
coord = coord - vec2<f32>(params.viewportOffsetX, params.viewportOffsetY);
|
|
74
|
-
|
|
75
|
-
// 2. Inverse zoom/scale
|
|
76
|
-
let totalScale = params.viewportScale * params.viewportZoom * params.transformScale;
|
|
77
|
-
coord = coord / totalScale;
|
|
78
|
-
|
|
79
|
-
// 3. Inverse rotation
|
|
80
|
-
if (params.rotation != 0.0) {
|
|
81
|
-
let cos_r = cos(-params.rotation);
|
|
82
|
-
let sin_r = sin(-params.rotation);
|
|
83
|
-
coord = vec2<f32>(
|
|
84
|
-
coord.x * cos_r - coord.y * sin_r,
|
|
85
|
-
coord.x * sin_r + coord.y * cos_r
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 4. Inverse flip
|
|
90
|
-
coord.x = coord.x * params.flipHorizontal;
|
|
91
|
-
coord.y = coord.y * params.flipVertical;
|
|
92
|
-
|
|
93
|
-
// 5. Convert to texture coordinates
|
|
94
|
-
// After inverse transformations, coord is in drawing space (centered at origin)
|
|
95
|
-
|
|
96
|
-
// When there's a crop, viewport.scale is adjusted to fit crop area to canvas
|
|
97
|
-
// This means coord values are scaled according to crop dimensions
|
|
98
|
-
// We need to account for this when mapping to texture coordinates
|
|
99
|
-
|
|
100
|
-
// ALWAYS use crop-aware logic
|
|
101
|
-
if (params.cropWidth > 0.0 && params.cropHeight > 0.0) {
|
|
102
|
-
// coord is in crop-centered space (units: pixels in crop area after inverse transforms)
|
|
103
|
-
// The 2D canvas draws: drawImage(img, cropX, cropY, cropW, cropH, -cropW/2, -cropH/2, cropW, cropH)
|
|
104
|
-
// This means the crop region is drawn centered, from (-cropW/2, -cropH/2) to (cropW/2, cropH/2)
|
|
105
|
-
|
|
106
|
-
// Map from drawing space to texture coordinates:
|
|
107
|
-
// Drawing space: coord ranges from (-cropW/2, -cropH/2) to (cropW/2, cropH/2)
|
|
108
|
-
// Texture space: we want to read from (cropX, cropY) to (cropX+cropW, cropY+cropH)
|
|
109
|
-
|
|
110
|
-
// Convert from centered coordinates to 0-based coordinates within crop region
|
|
111
|
-
let cropLocalX = coord.x + params.cropWidth * 0.5;
|
|
112
|
-
let cropLocalY = coord.y + params.cropHeight * 0.5;
|
|
113
|
-
|
|
114
|
-
// Convert to texture coordinates by adding crop offset and normalizing by image size
|
|
115
|
-
output.uv = vec2<f32>(
|
|
116
|
-
(params.cropX + cropLocalX) / params.imageWidth,
|
|
117
|
-
(params.cropY + cropLocalY) / params.imageHeight
|
|
118
|
-
);
|
|
119
|
-
} else {
|
|
120
|
-
// No crop - standard transformation
|
|
121
|
-
// Convert from image-centered space to top-left origin
|
|
122
|
-
coord = coord + vec2<f32>(params.imageWidth * 0.5, params.imageHeight * 0.5);
|
|
123
|
-
|
|
124
|
-
// Normalize to 0-1 range
|
|
125
|
-
output.uv = coord / vec2<f32>(params.imageWidth, params.imageHeight);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return output;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Helper functions
|
|
132
|
-
fn getLuminance(color: vec3<f32>) -> f32 {
|
|
133
|
-
return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Improved 2D hash function for better randomness
|
|
137
|
-
fn hash2d(p: vec2<f32>) -> f32 {
|
|
138
|
-
let p3 = fract(vec3<f32>(p.x, p.y, p.x) * vec3<f32>(0.1031, 0.1030, 0.0973));
|
|
139
|
-
let dot_p3 = dot(p3, vec3<f32>(p3.y, p3.z, p3.x) + 33.33);
|
|
140
|
-
return fract((p3.x + p3.y) * p3.z + dot_p3);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
fn rgbToHsl(rgb: vec3<f32>) -> vec3<f32> {
|
|
144
|
-
let maxVal = max(rgb.r, max(rgb.g, rgb.b));
|
|
145
|
-
let minVal = min(rgb.r, min(rgb.g, rgb.b));
|
|
146
|
-
var h = 0.0;
|
|
147
|
-
var s = 0.0;
|
|
148
|
-
let l = (maxVal + minVal) / 2.0;
|
|
149
|
-
|
|
150
|
-
if (maxVal != minVal) {
|
|
151
|
-
let d = maxVal - minVal;
|
|
152
|
-
s = select(d / (maxVal + minVal), d / (2.0 - maxVal - minVal), l > 0.5);
|
|
153
|
-
|
|
154
|
-
if (maxVal == rgb.r) {
|
|
155
|
-
h = ((rgb.g - rgb.b) / d + select(0.0, 6.0, rgb.g < rgb.b)) / 6.0;
|
|
156
|
-
} else if (maxVal == rgb.g) {
|
|
157
|
-
h = ((rgb.b - rgb.r) / d + 2.0) / 6.0;
|
|
158
|
-
} else {
|
|
159
|
-
h = ((rgb.r - rgb.g) / d + 4.0) / 6.0;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return vec3<f32>(h, s, l);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
fn hslToRgb(hsl: vec3<f32>) -> vec3<f32> {
|
|
167
|
-
let h = hsl.x;
|
|
168
|
-
let s = hsl.y;
|
|
169
|
-
let l = hsl.z;
|
|
170
|
-
|
|
171
|
-
if (s == 0.0) {
|
|
172
|
-
return vec3<f32>(l, l, l);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
let q = select(l + s - l * s, l * (1.0 + s), l < 0.5);
|
|
176
|
-
let p = 2.0 * l - q;
|
|
177
|
-
|
|
178
|
-
let r = hue2rgb(p, q, h + 1.0 / 3.0);
|
|
179
|
-
let g = hue2rgb(p, q, h);
|
|
180
|
-
let b = hue2rgb(p, q, h - 1.0 / 3.0);
|
|
181
|
-
|
|
182
|
-
return vec3<f32>(r, g, b);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
fn hue2rgb(p: f32, q: f32, t_: f32) -> f32 {
|
|
186
|
-
var t = t_;
|
|
187
|
-
if (t < 0.0) { t += 1.0; }
|
|
188
|
-
if (t > 1.0) { t -= 1.0; }
|
|
189
|
-
if (t < 1.0 / 6.0) { return p + (q - p) * 6.0 * t; }
|
|
190
|
-
if (t < 1.0 / 2.0) { return q; }
|
|
191
|
-
if (t < 2.0 / 3.0) { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
|
|
192
|
-
return p;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
@fragment
|
|
196
|
-
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
|
197
|
-
// Sample texture FIRST (must be in uniform control flow before any branching)
|
|
198
|
-
var color = textureSample(myTexture, mySampler, clamp(uv, vec2<f32>(0.0), vec2<f32>(1.0)));
|
|
199
|
-
var rgb = color.rgb;
|
|
200
|
-
|
|
201
|
-
// Check if outside texture bounds (0-1) and set to black
|
|
202
|
-
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
|
|
203
|
-
rgb = vec3<f32>(0.0);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// When crop is active, only show the crop region - black out everything else
|
|
207
|
-
if (params.cropWidth > 0.0) {
|
|
208
|
-
let cropMinU = params.cropX / params.imageWidth;
|
|
209
|
-
let cropMaxU = (params.cropX + params.cropWidth) / params.imageWidth;
|
|
210
|
-
let cropMinV = params.cropY / params.imageHeight;
|
|
211
|
-
let cropMaxV = (params.cropY + params.cropHeight) / params.imageHeight;
|
|
212
|
-
|
|
213
|
-
// If UV is outside the crop region, render black
|
|
214
|
-
if (uv.x < cropMinU || uv.x > cropMaxU || uv.y < cropMinV || uv.y > cropMaxV) {
|
|
215
|
-
rgb = vec3<f32>(0.0);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// 1. Brightness
|
|
220
|
-
if (params.brightness != 0.0) {
|
|
221
|
-
let factor = 1.0 + (params.brightness / 200.0);
|
|
222
|
-
rgb = rgb * factor;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// 2. Contrast
|
|
226
|
-
if (params.contrast != 0.0) {
|
|
227
|
-
let factor = 1.0 + (params.contrast / 200.0);
|
|
228
|
-
rgb = (rgb - 0.5) * factor + 0.5;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// 3. Exposure
|
|
232
|
-
if (params.exposure != 0.0) {
|
|
233
|
-
rgb = rgb * exp2(params.exposure / 100.0);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// 4. Shadows and Highlights
|
|
237
|
-
if (params.shadows != 0.0 || params.highlights != 0.0) {
|
|
238
|
-
let luma = getLuminance(rgb);
|
|
239
|
-
|
|
240
|
-
if (params.shadows != 0.0) {
|
|
241
|
-
let shadowMask = pow(1.0 - luma, 2.0);
|
|
242
|
-
rgb = rgb - rgb * (params.shadows / 100.0) * shadowMask * 0.5;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (params.highlights != 0.0) {
|
|
246
|
-
let highlightMask = pow(luma, 2.0);
|
|
247
|
-
rgb = rgb + rgb * (params.highlights / 100.0) * highlightMask * 0.5;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// 5. Saturation
|
|
252
|
-
if (params.saturation != 0.0) {
|
|
253
|
-
rgb = clamp(rgb, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
254
|
-
var hsl = rgbToHsl(rgb);
|
|
255
|
-
hsl.y = clamp(hsl.y * (1.0 + params.saturation / 100.0), 0.0, 1.0);
|
|
256
|
-
rgb = hslToRgb(hsl);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 5.5. Color Temperature
|
|
260
|
-
// Warm (positive): add red, subtract blue
|
|
261
|
-
// Cool (negative): subtract red, add blue
|
|
262
|
-
if (params.temperature != 0.0) {
|
|
263
|
-
let temp = params.temperature / 100.0;
|
|
264
|
-
rgb.r = rgb.r + temp * 0.1;
|
|
265
|
-
rgb.b = rgb.b - temp * 0.1;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 6. Sepia
|
|
269
|
-
if (params.sepia != 0.0) {
|
|
270
|
-
let sepiaAmount = params.sepia / 100.0;
|
|
271
|
-
let tr = 0.393 * rgb.r + 0.769 * rgb.g + 0.189 * rgb.b;
|
|
272
|
-
let tg = 0.349 * rgb.r + 0.686 * rgb.g + 0.168 * rgb.b;
|
|
273
|
-
let tb = 0.272 * rgb.r + 0.534 * rgb.g + 0.131 * rgb.b;
|
|
274
|
-
rgb = mix(rgb, vec3<f32>(tr, tg, tb), sepiaAmount);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// 7. Grayscale
|
|
278
|
-
if (params.grayscale != 0.0) {
|
|
279
|
-
let gray = getLuminance(rgb);
|
|
280
|
-
rgb = mix(rgb, vec3<f32>(gray), params.grayscale / 100.0);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// 8. Vignette
|
|
284
|
-
if (params.vignette != 0.0) {
|
|
285
|
-
let center = vec2<f32>(0.5, 0.5);
|
|
286
|
-
let dist = distance(uv, center);
|
|
287
|
-
let vignetteFactor = params.vignette / 100.0;
|
|
288
|
-
let vignetteAmount = pow(dist * 1.4, 2.0);
|
|
289
|
-
rgb = rgb * (1.0 + vignetteFactor * vignetteAmount * 1.5);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Clamp final result
|
|
293
|
-
rgb = clamp(rgb, vec3<f32>(0.0), vec3<f32>(1.0));
|
|
294
|
-
|
|
295
|
-
return vec4<f32>(rgb, color.a);
|
|
296
|
-
}
|