tokimeki-image-editor 0.1.8 → 0.1.9
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/AdjustTool.svelte +333 -299
- package/dist/components/Canvas.svelte +274 -115
- package/dist/components/Canvas.svelte.d.ts +1 -0
- package/dist/components/FilterTool.svelte +408 -298
- package/dist/components/ImageEditor.svelte +426 -423
- package/dist/i18n/locales/en.json +79 -68
- package/dist/i18n/locales/ja.json +79 -68
- package/dist/shaders/blur.wgsl +59 -0
- package/dist/shaders/composite.wgsl +46 -0
- package/dist/shaders/grain.wgsl +225 -0
- package/dist/shaders/image-editor.wgsl +296 -0
- package/dist/types.d.ts +3 -1
- package/dist/utils/adjustments.d.ts +2 -1
- package/dist/utils/adjustments.js +100 -13
- package/dist/utils/canvas.d.ts +7 -2
- package/dist/utils/canvas.js +48 -5
- package/dist/utils/filters.js +109 -2
- package/dist/utils/webgpu-render.d.ts +26 -0
- package/dist/utils/webgpu-render.js +1192 -0
- package/package.json +43 -42
|
@@ -1,68 +1,79 @@
|
|
|
1
|
-
{
|
|
2
|
-
"editor": {
|
|
3
|
-
"title": "Image Editor",
|
|
4
|
-
"upload": "Upload Image",
|
|
5
|
-
"crop": "Crop",
|
|
6
|
-
"rotate": "Rotate",
|
|
7
|
-
"adjust": "Adjust",
|
|
8
|
-
"filter": "Filter",
|
|
9
|
-
"blur": "Blur",
|
|
10
|
-
"stamp": "Stamp",
|
|
11
|
-
"none": "None",
|
|
12
|
-
"flip": "Flip",
|
|
13
|
-
"flipHorizontal": "Flip Horizontal",
|
|
14
|
-
"flipVertical": "Flip Vertical",
|
|
15
|
-
"rotateLeft": "Rotate Left",
|
|
16
|
-
"rotateRight": "Rotate Right",
|
|
17
|
-
"export": "Export",
|
|
18
|
-
"reset": "Reset",
|
|
19
|
-
"apply": "Apply",
|
|
20
|
-
"cancel": "Cancel",
|
|
21
|
-
"close": "Close",
|
|
22
|
-
"delete": "Delete",
|
|
23
|
-
"zoom": "Zoom",
|
|
24
|
-
"quality": "Quality",
|
|
25
|
-
"format": "Format",
|
|
26
|
-
"download": "Download",
|
|
27
|
-
"dropImageHere": "Drop image here or click to upload",
|
|
28
|
-
"noImage": "No image loaded",
|
|
29
|
-
"undo": "Undo",
|
|
30
|
-
"redo": "Redo",
|
|
31
|
-
"selectStamp": "Select Stamp"
|
|
32
|
-
},
|
|
33
|
-
"toolbar": {
|
|
34
|
-
"crop": "Crop Tool",
|
|
35
|
-
"rotate": "Rotate & Flip",
|
|
36
|
-
"adjust": "Adjust Image",
|
|
37
|
-
"filter": "Filters",
|
|
38
|
-
"blur": "Blur Tool",
|
|
39
|
-
"stamp": "Stamp Tool",
|
|
40
|
-
"export": "Export Image",
|
|
41
|
-
"undo": "Undo (Ctrl+Z)",
|
|
42
|
-
"redo": "Redo (Ctrl+Shift+Z)"
|
|
43
|
-
},
|
|
44
|
-
"adjustments": {
|
|
45
|
-
"exposure": "Exposure",
|
|
46
|
-
"contrast": "Contrast",
|
|
47
|
-
"highlights": "Highlights",
|
|
48
|
-
"shadows": "Shadows",
|
|
49
|
-
"brightness": "Brightness",
|
|
50
|
-
"saturation": "Saturation",
|
|
51
|
-
"
|
|
52
|
-
"vignette": "Vignette"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
{
|
|
2
|
+
"editor": {
|
|
3
|
+
"title": "Image Editor",
|
|
4
|
+
"upload": "Upload Image",
|
|
5
|
+
"crop": "Crop",
|
|
6
|
+
"rotate": "Rotate",
|
|
7
|
+
"adjust": "Adjust",
|
|
8
|
+
"filter": "Filter",
|
|
9
|
+
"blur": "Blur",
|
|
10
|
+
"stamp": "Stamp",
|
|
11
|
+
"none": "None",
|
|
12
|
+
"flip": "Flip",
|
|
13
|
+
"flipHorizontal": "Flip Horizontal",
|
|
14
|
+
"flipVertical": "Flip Vertical",
|
|
15
|
+
"rotateLeft": "Rotate Left",
|
|
16
|
+
"rotateRight": "Rotate Right",
|
|
17
|
+
"export": "Export",
|
|
18
|
+
"reset": "Reset",
|
|
19
|
+
"apply": "Apply",
|
|
20
|
+
"cancel": "Cancel",
|
|
21
|
+
"close": "Close",
|
|
22
|
+
"delete": "Delete",
|
|
23
|
+
"zoom": "Zoom",
|
|
24
|
+
"quality": "Quality",
|
|
25
|
+
"format": "Format",
|
|
26
|
+
"download": "Download",
|
|
27
|
+
"dropImageHere": "Drop image here or click to upload",
|
|
28
|
+
"noImage": "No image loaded",
|
|
29
|
+
"undo": "Undo",
|
|
30
|
+
"redo": "Redo",
|
|
31
|
+
"selectStamp": "Select Stamp"
|
|
32
|
+
},
|
|
33
|
+
"toolbar": {
|
|
34
|
+
"crop": "Crop Tool",
|
|
35
|
+
"rotate": "Rotate & Flip",
|
|
36
|
+
"adjust": "Adjust Image",
|
|
37
|
+
"filter": "Filters",
|
|
38
|
+
"blur": "Blur Tool",
|
|
39
|
+
"stamp": "Stamp Tool",
|
|
40
|
+
"export": "Export Image",
|
|
41
|
+
"undo": "Undo (Ctrl+Z)",
|
|
42
|
+
"redo": "Redo (Ctrl+Shift+Z)"
|
|
43
|
+
},
|
|
44
|
+
"adjustments": {
|
|
45
|
+
"exposure": "Exposure",
|
|
46
|
+
"contrast": "Contrast",
|
|
47
|
+
"highlights": "Highlights",
|
|
48
|
+
"shadows": "Shadows",
|
|
49
|
+
"brightness": "Brightness",
|
|
50
|
+
"saturation": "Saturation",
|
|
51
|
+
"temperature": "Temperature",
|
|
52
|
+
"vignette": "Vignette",
|
|
53
|
+
"blur": "Blur",
|
|
54
|
+
"grain": "Grain"
|
|
55
|
+
},
|
|
56
|
+
"filters": {
|
|
57
|
+
"vivid": "Vivid",
|
|
58
|
+
"sepia": "Sepia",
|
|
59
|
+
"monochrome": "Monochrome",
|
|
60
|
+
"vintage": "Vintage",
|
|
61
|
+
"warm": "Warm",
|
|
62
|
+
"cool": "Cool",
|
|
63
|
+
"film": "Film",
|
|
64
|
+
"cinematic": "Cinematic",
|
|
65
|
+
"dramatic": "Dramatic",
|
|
66
|
+
"faded": "Faded",
|
|
67
|
+
"golden": "Golden Hour",
|
|
68
|
+
"soft": "Soft",
|
|
69
|
+
"moody": "Moody",
|
|
70
|
+
"pastel": "Pastel",
|
|
71
|
+
"bleach": "Bleach Bypass",
|
|
72
|
+
"grainy": "Grainy",
|
|
73
|
+
"info": "Selecting a filter will override all current adjustments."
|
|
74
|
+
},
|
|
75
|
+
"blur": {
|
|
76
|
+
"strength": "Blur Strength",
|
|
77
|
+
"hint": "Drag on the image to create a blur area. Click on an existing area to edit it."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,68 +1,79 @@
|
|
|
1
|
-
{
|
|
2
|
-
"editor": {
|
|
3
|
-
"title": "画像エディター",
|
|
4
|
-
"upload": "画像をアップロード",
|
|
5
|
-
"crop": "トリミング",
|
|
6
|
-
"rotate": "回転",
|
|
7
|
-
"adjust": "調整",
|
|
8
|
-
"filter": "フィルター",
|
|
9
|
-
"blur": "ぼかし",
|
|
10
|
-
"stamp": "スタンプ",
|
|
11
|
-
"none": "なし",
|
|
12
|
-
"flip": "反転",
|
|
13
|
-
"flipHorizontal": "左右反転",
|
|
14
|
-
"flipVertical": "上下反転",
|
|
15
|
-
"rotateLeft": "左に回転",
|
|
16
|
-
"rotateRight": "右に回転",
|
|
17
|
-
"export": "エクスポート",
|
|
18
|
-
"reset": "リセット",
|
|
19
|
-
"apply": "適用",
|
|
20
|
-
"cancel": "キャンセル",
|
|
21
|
-
"close": "閉じる",
|
|
22
|
-
"delete": "削除",
|
|
23
|
-
"zoom": "ズーム",
|
|
24
|
-
"quality": "品質",
|
|
25
|
-
"format": "形式",
|
|
26
|
-
"download": "ダウンロード",
|
|
27
|
-
"dropImageHere": "画像をドロップまたはクリックしてアップロード",
|
|
28
|
-
"noImage": "画像が読み込まれていません",
|
|
29
|
-
"undo": "元に戻す",
|
|
30
|
-
"redo": "やり直す",
|
|
31
|
-
"selectStamp": "スタンプを選択"
|
|
32
|
-
},
|
|
33
|
-
"toolbar": {
|
|
34
|
-
"crop": "トリミングツール",
|
|
35
|
-
"rotate": "回転・反転",
|
|
36
|
-
"adjust": "画像調整",
|
|
37
|
-
"filter": "フィルター",
|
|
38
|
-
"blur": "ぼかしツール",
|
|
39
|
-
"stamp": "スタンプツール",
|
|
40
|
-
"export": "画像をエクスポート",
|
|
41
|
-
"undo": "元に戻す (Ctrl+Z)",
|
|
42
|
-
"redo": "やり直す (Ctrl+Shift+Z)"
|
|
43
|
-
},
|
|
44
|
-
"adjustments": {
|
|
45
|
-
"exposure": "露出",
|
|
46
|
-
"contrast": "コントラスト",
|
|
47
|
-
"highlights": "ハイライト",
|
|
48
|
-
"shadows": "シャドウ",
|
|
49
|
-
"brightness": "明るさ",
|
|
50
|
-
"saturation": "彩度",
|
|
51
|
-
"
|
|
52
|
-
"vignette": "周辺光量補正"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
1
|
+
{
|
|
2
|
+
"editor": {
|
|
3
|
+
"title": "画像エディター",
|
|
4
|
+
"upload": "画像をアップロード",
|
|
5
|
+
"crop": "トリミング",
|
|
6
|
+
"rotate": "回転",
|
|
7
|
+
"adjust": "調整",
|
|
8
|
+
"filter": "フィルター",
|
|
9
|
+
"blur": "ぼかし",
|
|
10
|
+
"stamp": "スタンプ",
|
|
11
|
+
"none": "なし",
|
|
12
|
+
"flip": "反転",
|
|
13
|
+
"flipHorizontal": "左右反転",
|
|
14
|
+
"flipVertical": "上下反転",
|
|
15
|
+
"rotateLeft": "左に回転",
|
|
16
|
+
"rotateRight": "右に回転",
|
|
17
|
+
"export": "エクスポート",
|
|
18
|
+
"reset": "リセット",
|
|
19
|
+
"apply": "適用",
|
|
20
|
+
"cancel": "キャンセル",
|
|
21
|
+
"close": "閉じる",
|
|
22
|
+
"delete": "削除",
|
|
23
|
+
"zoom": "ズーム",
|
|
24
|
+
"quality": "品質",
|
|
25
|
+
"format": "形式",
|
|
26
|
+
"download": "ダウンロード",
|
|
27
|
+
"dropImageHere": "画像をドロップまたはクリックしてアップロード",
|
|
28
|
+
"noImage": "画像が読み込まれていません",
|
|
29
|
+
"undo": "元に戻す",
|
|
30
|
+
"redo": "やり直す",
|
|
31
|
+
"selectStamp": "スタンプを選択"
|
|
32
|
+
},
|
|
33
|
+
"toolbar": {
|
|
34
|
+
"crop": "トリミングツール",
|
|
35
|
+
"rotate": "回転・反転",
|
|
36
|
+
"adjust": "画像調整",
|
|
37
|
+
"filter": "フィルター",
|
|
38
|
+
"blur": "ぼかしツール",
|
|
39
|
+
"stamp": "スタンプツール",
|
|
40
|
+
"export": "画像をエクスポート",
|
|
41
|
+
"undo": "元に戻す (Ctrl+Z)",
|
|
42
|
+
"redo": "やり直す (Ctrl+Shift+Z)"
|
|
43
|
+
},
|
|
44
|
+
"adjustments": {
|
|
45
|
+
"exposure": "露出",
|
|
46
|
+
"contrast": "コントラスト",
|
|
47
|
+
"highlights": "ハイライト",
|
|
48
|
+
"shadows": "シャドウ",
|
|
49
|
+
"brightness": "明るさ",
|
|
50
|
+
"saturation": "彩度",
|
|
51
|
+
"temperature": "色温度",
|
|
52
|
+
"vignette": "周辺光量補正",
|
|
53
|
+
"blur": "ぼかし",
|
|
54
|
+
"grain": "粒子"
|
|
55
|
+
},
|
|
56
|
+
"filters": {
|
|
57
|
+
"vivid": "ビビッド",
|
|
58
|
+
"sepia": "セピア",
|
|
59
|
+
"monochrome": "モノクローム",
|
|
60
|
+
"vintage": "ビンテージ",
|
|
61
|
+
"warm": "ウォーム",
|
|
62
|
+
"cool": "クール",
|
|
63
|
+
"film": "フィルム",
|
|
64
|
+
"cinematic": "シネマティック",
|
|
65
|
+
"dramatic": "ドラマティック",
|
|
66
|
+
"faded": "フェード",
|
|
67
|
+
"golden": "ゴールデンアワー",
|
|
68
|
+
"soft": "ソフト",
|
|
69
|
+
"moody": "ムーディー",
|
|
70
|
+
"pastel": "パステル",
|
|
71
|
+
"bleach": "ブリーチバイパス",
|
|
72
|
+
"grainy": "粒子",
|
|
73
|
+
"info": "フィルターを選択すると、すべての調整値が上書きされます。"
|
|
74
|
+
},
|
|
75
|
+
"blur": {
|
|
76
|
+
"strength": "ぼかしの強さ",
|
|
77
|
+
"hint": "画像上をドラッグしてぼかし領域を作成します。既存の領域をクリックすると編集できます。"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
struct VertexOutput {
|
|
2
|
+
@builtin(position) position: vec4<f32>,
|
|
3
|
+
@location(0) uv: vec2<f32>,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
struct BlurUniforms {
|
|
7
|
+
direction: vec2<f32>, // (1, 0) for horizontal, (0, 1) for vertical
|
|
8
|
+
radius: f32,
|
|
9
|
+
padding: f32,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
@group(0) @binding(0) var mySampler: sampler;
|
|
13
|
+
@group(0) @binding(1) var myTexture: texture_2d<f32>;
|
|
14
|
+
@group(0) @binding(2) var<uniform> blur: BlurUniforms;
|
|
15
|
+
|
|
16
|
+
@vertex
|
|
17
|
+
fn vs_main(@builtin(vertex_index) VertexIndex: u32) -> VertexOutput {
|
|
18
|
+
var pos = array<vec2<f32>, 3>(
|
|
19
|
+
vec2<f32>(-1.0, -1.0),
|
|
20
|
+
vec2<f32>( 3.0, -1.0),
|
|
21
|
+
vec2<f32>(-1.0, 3.0)
|
|
22
|
+
);
|
|
23
|
+
var output: VertexOutput;
|
|
24
|
+
output.position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
|
|
25
|
+
// Convert NDC to UV with Y-flip (NDC bottom-left maps to UV top-left)
|
|
26
|
+
let uv = pos[VertexIndex] * 0.5 + 0.5;
|
|
27
|
+
output.uv = vec2<f32>(uv.x, 1.0 - uv.y);
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@fragment
|
|
32
|
+
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
|
33
|
+
let r = i32(blur.radius);
|
|
34
|
+
|
|
35
|
+
// Pass-through if radius is 0
|
|
36
|
+
if (r == 0) {
|
|
37
|
+
return textureSample(myTexture, mySampler, uv);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let texSize = vec2<f32>(textureDimensions(myTexture));
|
|
41
|
+
let texelSize = 1.0 / texSize;
|
|
42
|
+
|
|
43
|
+
// Gaussian blur weights (for radius up to 10)
|
|
44
|
+
// Pre-normalized weights for different radii
|
|
45
|
+
var color = vec4<f32>(0.0);
|
|
46
|
+
|
|
47
|
+
// Simple box blur for now (equal weights)
|
|
48
|
+
// TODO: Use proper Gaussian weights
|
|
49
|
+
let totalSamples = f32(r * 2 + 1);
|
|
50
|
+
|
|
51
|
+
// Sample along the blur direction
|
|
52
|
+
for (var i = -r; i <= r; i = i + 1) {
|
|
53
|
+
let offset = vec2<f32>(f32(i)) * blur.direction * texelSize;
|
|
54
|
+
let sampleUV = uv + offset;
|
|
55
|
+
color = color + textureSample(myTexture, mySampler, sampleUV) / totalSamples;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return color;
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
}
|