@jjlmoya/utils-audiovisual 1.2.0

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.
Files changed (120) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +198 -0
  3. package/src/category/i18n/es.ts +198 -0
  4. package/src/category/i18n/fr.ts +198 -0
  5. package/src/category/index.ts +17 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +4 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/chromaticLens/bibliography.astro +17 -0
  23. package/src/tool/chromaticLens/component.astro +178 -0
  24. package/src/tool/chromaticLens/i18n/en.ts +246 -0
  25. package/src/tool/chromaticLens/i18n/es.ts +244 -0
  26. package/src/tool/chromaticLens/i18n/fr.ts +244 -0
  27. package/src/tool/chromaticLens/index.ts +43 -0
  28. package/src/tool/chromaticLens/logic.ts +87 -0
  29. package/src/tool/chromaticLens/seo.astro +15 -0
  30. package/src/tool/chromaticLens/style.css +308 -0
  31. package/src/tool/chromaticLens/ui.ts +109 -0
  32. package/src/tool/collageMaker/bibliography.astro +17 -0
  33. package/src/tool/collageMaker/component.astro +302 -0
  34. package/src/tool/collageMaker/i18n/en.ts +233 -0
  35. package/src/tool/collageMaker/i18n/es.ts +231 -0
  36. package/src/tool/collageMaker/i18n/fr.ts +231 -0
  37. package/src/tool/collageMaker/index.ts +51 -0
  38. package/src/tool/collageMaker/logic.ts +134 -0
  39. package/src/tool/collageMaker/seo.astro +15 -0
  40. package/src/tool/collageMaker/style.css +386 -0
  41. package/src/tool/exifCleaner/bibliography.astro +18 -0
  42. package/src/tool/exifCleaner/component.astro +162 -0
  43. package/src/tool/exifCleaner/i18n/en.ts +277 -0
  44. package/src/tool/exifCleaner/i18n/es.ts +277 -0
  45. package/src/tool/exifCleaner/i18n/fr.ts +277 -0
  46. package/src/tool/exifCleaner/index.ts +57 -0
  47. package/src/tool/exifCleaner/logic.ts +135 -0
  48. package/src/tool/exifCleaner/seo.astro +18 -0
  49. package/src/tool/exifCleaner/style.css +289 -0
  50. package/src/tool/exifCleaner/ui.ts +117 -0
  51. package/src/tool/imageCompressor/bibliography.astro +17 -0
  52. package/src/tool/imageCompressor/component.astro +262 -0
  53. package/src/tool/imageCompressor/i18n/en.ts +232 -0
  54. package/src/tool/imageCompressor/i18n/es.ts +230 -0
  55. package/src/tool/imageCompressor/i18n/fr.ts +230 -0
  56. package/src/tool/imageCompressor/index.ts +50 -0
  57. package/src/tool/imageCompressor/logic.ts +79 -0
  58. package/src/tool/imageCompressor/seo.astro +15 -0
  59. package/src/tool/imageCompressor/style.css +503 -0
  60. package/src/tool/printQualityCalculator/bibliography.astro +18 -0
  61. package/src/tool/printQualityCalculator/component.astro +318 -0
  62. package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
  63. package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
  64. package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
  65. package/src/tool/printQualityCalculator/index.ts +56 -0
  66. package/src/tool/printQualityCalculator/logic.ts +53 -0
  67. package/src/tool/printQualityCalculator/seo.astro +18 -0
  68. package/src/tool/printQualityCalculator/style.css +491 -0
  69. package/src/tool/printQualityCalculator/ui.ts +122 -0
  70. package/src/tool/privacyBlur/bibliography.astro +17 -0
  71. package/src/tool/privacyBlur/component.astro +230 -0
  72. package/src/tool/privacyBlur/i18n/en.ts +238 -0
  73. package/src/tool/privacyBlur/i18n/es.ts +236 -0
  74. package/src/tool/privacyBlur/i18n/fr.ts +236 -0
  75. package/src/tool/privacyBlur/index.ts +49 -0
  76. package/src/tool/privacyBlur/logic.ts +249 -0
  77. package/src/tool/privacyBlur/seo.astro +15 -0
  78. package/src/tool/privacyBlur/style.css +332 -0
  79. package/src/tool/privacyBlur/ui.ts +124 -0
  80. package/src/tool/subtitleSync/bibliography.astro +17 -0
  81. package/src/tool/subtitleSync/component.astro +187 -0
  82. package/src/tool/subtitleSync/i18n/en.ts +241 -0
  83. package/src/tool/subtitleSync/i18n/es.ts +241 -0
  84. package/src/tool/subtitleSync/i18n/fr.ts +241 -0
  85. package/src/tool/subtitleSync/index.ts +49 -0
  86. package/src/tool/subtitleSync/logic.ts +91 -0
  87. package/src/tool/subtitleSync/seo.astro +15 -0
  88. package/src/tool/subtitleSync/style.css +325 -0
  89. package/src/tool/subtitleSync/ui.ts +152 -0
  90. package/src/tool/timelapseCalculator/bibliography.astro +15 -0
  91. package/src/tool/timelapseCalculator/component.astro +148 -0
  92. package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
  93. package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
  94. package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
  95. package/src/tool/timelapseCalculator/index.ts +52 -0
  96. package/src/tool/timelapseCalculator/logic.ts +46 -0
  97. package/src/tool/timelapseCalculator/seo.astro +18 -0
  98. package/src/tool/timelapseCalculator/style.css +285 -0
  99. package/src/tool/tvDistance/bibliography.astro +17 -0
  100. package/src/tool/tvDistance/component.astro +178 -0
  101. package/src/tool/tvDistance/i18n/en.ts +223 -0
  102. package/src/tool/tvDistance/i18n/es.ts +223 -0
  103. package/src/tool/tvDistance/i18n/fr.ts +223 -0
  104. package/src/tool/tvDistance/index.ts +49 -0
  105. package/src/tool/tvDistance/logic.ts +47 -0
  106. package/src/tool/tvDistance/seo.astro +15 -0
  107. package/src/tool/tvDistance/style.css +435 -0
  108. package/src/tool/tvDistance/ui.ts +66 -0
  109. package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
  110. package/src/tool/videoFrameExtractor/component.astro +285 -0
  111. package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
  112. package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
  113. package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
  114. package/src/tool/videoFrameExtractor/index.ts +53 -0
  115. package/src/tool/videoFrameExtractor/logic.ts +49 -0
  116. package/src/tool/videoFrameExtractor/seo.astro +15 -0
  117. package/src/tool/videoFrameExtractor/style.css +426 -0
  118. package/src/tool/videoFrameExtractor/ui.ts +179 -0
  119. package/src/tools.ts +25 -0
  120. package/src/types.ts +72 -0
@@ -0,0 +1,249 @@
1
+ export interface Layer {
2
+ id: number;
3
+ type: "pixel" | "blur" | "solid";
4
+ x: number;
5
+ y: number;
6
+ w: number;
7
+ h: number;
8
+ intensity: number;
9
+ }
10
+
11
+ export type ToolType = "pixel" | "blur" | "solid";
12
+
13
+ interface FaceDetection {
14
+ box: { x: number; y: number; width: number; height: number };
15
+ }
16
+
17
+ declare const faceapi: {
18
+ loadTinyFaceDetectorModel: (path: string) => Promise<void>;
19
+ TinyFaceDetectorOptions: (opts: { inputSize: number; scoreThreshold: number }) => object;
20
+ detectAllFaces: (img: HTMLImageElement, detector: object) => Promise<FaceDetection[]>;
21
+ };
22
+
23
+ export class PrivacyBlurEngine {
24
+ private canvas: HTMLCanvasElement;
25
+ private ctx: CanvasRenderingContext2D;
26
+ private image: HTMLImageElement | null = null;
27
+ private layers: Layer[] = [];
28
+
29
+ private isDragging = false;
30
+ private startX = 0;
31
+ private startY = 0;
32
+ private currentSelection: { x: number; y: number; w: number; h: number } | null = null;
33
+ private tool: ToolType = "pixel";
34
+ private intensity = 10;
35
+ private isFaceApiLoaded = false;
36
+
37
+ constructor(canvas: HTMLCanvasElement) {
38
+ this.canvas = canvas;
39
+ this.ctx = this.canvas.getContext("2d", { willReadFrequently: true })!;
40
+ }
41
+
42
+ public setImage(img: HTMLImageElement) {
43
+ this.image = img;
44
+ this.layers = [];
45
+ this.canvas.width = img.naturalWidth;
46
+ this.canvas.height = img.naturalHeight;
47
+ this.redraw();
48
+ }
49
+
50
+ public setTool(tool: ToolType) {
51
+ this.tool = tool;
52
+ }
53
+
54
+ public setIntensity(val: number) {
55
+ this.intensity = val;
56
+ if (this.layers.length > 0) {
57
+ const last = this.layers[this.layers.length - 1];
58
+ if (last) {
59
+ last.intensity = this.intensity;
60
+ this.redraw();
61
+ }
62
+ }
63
+ }
64
+
65
+ public updateLastLayerTool(tool: ToolType) {
66
+ this.tool = tool;
67
+ if (this.layers.length > 0) {
68
+ const last = this.layers[this.layers.length - 1];
69
+ if (last) {
70
+ last.type = tool;
71
+ this.redraw();
72
+ }
73
+ }
74
+ }
75
+
76
+ public undo(): boolean {
77
+ if (this.layers.length === 0) return false;
78
+ this.layers.pop();
79
+ this.redraw();
80
+ return this.layers.length > 0;
81
+ }
82
+
83
+ public getCanvasData(): string {
84
+ return this.canvas.toDataURL("image/webp", 0.9);
85
+ }
86
+
87
+ public hasLayers(): boolean {
88
+ return this.layers.length > 0;
89
+ }
90
+
91
+ public async detectFaces(onLoading: (text: string) => void): Promise<boolean> {
92
+ if (!this.image) return false;
93
+
94
+ try {
95
+ if (typeof faceapi === "undefined") {
96
+ throw new Error("FaceAPI not loaded");
97
+ }
98
+
99
+ if (!this.isFaceApiLoaded) {
100
+ onLoading("Descargando modelos...");
101
+ await faceapi.loadTinyFaceDetectorModel(
102
+ "https://justadudewhohacks.github.io/face-api.js/models"
103
+ );
104
+ this.isFaceApiLoaded = true;
105
+ }
106
+
107
+ onLoading("Analizando...");
108
+
109
+ const detections = await faceapi.detectAllFaces(
110
+ this.image,
111
+ new faceapi.TinyFaceDetectorOptions({ inputSize: 608, scoreThreshold: 0.4 })
112
+ );
113
+
114
+ if (detections.length === 0) {
115
+ return false;
116
+ } else {
117
+ detections.forEach((d: FaceDetection) => {
118
+ const { x, y, width, height } = d.box;
119
+ const pad = width * 0.15;
120
+ this.layers.push({
121
+ id: Date.now() + Math.random(),
122
+ type: this.tool,
123
+ x: x - pad,
124
+ y: y - pad * 1.5,
125
+ w: width + pad * 2,
126
+ h: height + pad * 2,
127
+ intensity: this.intensity,
128
+ });
129
+ });
130
+ this.redraw();
131
+ return true;
132
+ }
133
+ } catch (e) {
134
+ console.error(e);
135
+ throw e;
136
+ }
137
+ }
138
+
139
+ public startDragging(clientX: number, clientY: number) {
140
+ if (!this.image) return;
141
+ this.isDragging = true;
142
+ const rect = this.canvas.getBoundingClientRect();
143
+ const scaleX = this.canvas.width / rect.width;
144
+ const scaleY = this.canvas.height / rect.height;
145
+
146
+ this.startX = (clientX - rect.left) * scaleX;
147
+ this.startY = (clientY - rect.top) * scaleY;
148
+ this.currentSelection = { x: this.startX, y: this.startY, w: 0, h: 0 };
149
+ }
150
+
151
+ public updateDragging(clientX: number, clientY: number) {
152
+ if (!this.isDragging || !this.image) return;
153
+ const rect = this.canvas.getBoundingClientRect();
154
+ const scaleX = this.canvas.width / rect.width;
155
+ const scaleY = this.canvas.height / rect.height;
156
+
157
+ const currentX = (clientX - rect.left) * scaleX;
158
+ const currentY = (clientY - rect.top) * scaleY;
159
+
160
+ this.currentSelection = {
161
+ x: Math.min(this.startX, currentX),
162
+ y: Math.min(this.startY, currentY),
163
+ w: Math.abs(currentX - this.startX),
164
+ h: Math.abs(currentY - this.startY)
165
+ };
166
+ this.redraw();
167
+ }
168
+
169
+ public endDragging(): boolean {
170
+ if (!this.isDragging || !this.currentSelection) return false;
171
+ this.isDragging = false;
172
+ const { w, h } = this.currentSelection;
173
+
174
+ if (w > 5 && h > 5) {
175
+ this.layers.push({
176
+ id: Date.now(),
177
+ type: this.tool,
178
+ x: this.currentSelection.x,
179
+ y: this.currentSelection.y,
180
+ w,
181
+ h,
182
+ intensity: this.intensity,
183
+ });
184
+ this.currentSelection = null;
185
+ this.redraw();
186
+ return true;
187
+ }
188
+
189
+ this.currentSelection = null;
190
+ this.redraw();
191
+ return false;
192
+ }
193
+
194
+ public cancelDragging() {
195
+ this.isDragging = false;
196
+ this.currentSelection = null;
197
+ this.redraw();
198
+ }
199
+
200
+ private redraw() {
201
+ if (!this.image) return;
202
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
203
+ this.ctx.filter = "none";
204
+ this.ctx.drawImage(this.image, 0, 0);
205
+
206
+ this.layers.forEach((layer) => this.applyLayer(layer));
207
+
208
+ if (this.currentSelection) {
209
+ this.ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
210
+ this.ctx.lineWidth = 2;
211
+ this.ctx.strokeRect(this.currentSelection.x, this.currentSelection.y, this.currentSelection.w, this.currentSelection.h);
212
+ }
213
+ }
214
+
215
+ private applyLayer(layer: Layer) {
216
+ if (!this.image) return;
217
+ if (layer.type === "solid") {
218
+ this.ctx.fillStyle = "#000000";
219
+ this.ctx.fillRect(layer.x, layer.y, layer.w, layer.h);
220
+ } else if (layer.type === "blur") {
221
+ this.ctx.save();
222
+ this.ctx.beginPath();
223
+ this.ctx.rect(layer.x, layer.y, layer.w, layer.h);
224
+ this.ctx.clip();
225
+ this.ctx.filter = `blur(${layer.intensity * 2}px)`;
226
+ this.ctx.drawImage(this.image, 0, 0);
227
+ this.ctx.restore();
228
+ this.ctx.filter = "none";
229
+ } else if (layer.type === "pixel") {
230
+ const size = Math.max(2, layer.intensity);
231
+ const w = Math.floor(layer.w);
232
+ const h = Math.floor(layer.h);
233
+
234
+ const scaledW = w / size;
235
+ const scaledH = h / size;
236
+
237
+ this.ctx.imageSmoothingEnabled = false;
238
+ const tempCanvas = document.createElement("canvas");
239
+ tempCanvas.width = scaledW;
240
+ tempCanvas.height = scaledH;
241
+ const tCtx = tempCanvas.getContext("2d")!;
242
+ tCtx.imageSmoothingEnabled = false;
243
+
244
+ tCtx.drawImage(this.image, layer.x, layer.y, w, h, 0, 0, scaledW, scaledH);
245
+ this.ctx.drawImage(tempCanvas, 0, 0, scaledW, scaledH, layer.x, layer.y, w, h);
246
+ this.ctx.imageSmoothingEnabled = true;
247
+ }
248
+ }
249
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { privacyBlur } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await privacyBlur.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ <SEORenderer content={{ locale, sections: content.seo || [] }} />
@@ -0,0 +1,332 @@
1
+ .pb-root {
2
+ --pb-bg: #fff;
3
+ --pb-bg-muted: #f8fafc;
4
+ --pb-border: #e2e8f0;
5
+ --pb-text: #0f172a;
6
+ --pb-text-muted: #64748b;
7
+ --pb-primary: #6366f1;
8
+ --pb-primary-light: rgba(99,102,241,0.1);
9
+ --pb-shadow: rgba(0,0,0,0.06);
10
+ --pb-toolbar-bg: rgba(255,255,255,0.92);
11
+ --pb-radius: 1.25rem;
12
+
13
+ width: 100%;
14
+ padding: 1rem;
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 1rem;
18
+ height: 90vh;
19
+ min-height: 600px;
20
+ }
21
+
22
+ .theme-dark .pb-root {
23
+ --pb-bg: #09090b;
24
+ --pb-bg-muted: #18181b;
25
+ --pb-border: #27272a;
26
+ --pb-text: #fafafa;
27
+ --pb-text-muted: #71717a;
28
+ --pb-primary: #818cf8;
29
+ --pb-primary-light: rgba(129,140,248,0.12);
30
+ --pb-shadow: rgba(0,0,0,0.4);
31
+ --pb-toolbar-bg: rgba(9,9,11,0.92);
32
+ }
33
+
34
+ .pb-toolbar {
35
+ position: sticky;
36
+ top: 0.5rem;
37
+ z-index: 100;
38
+ max-width: 1200px;
39
+ margin: 0 auto;
40
+ width: 100%;
41
+ background: var(--pb-toolbar-bg);
42
+ backdrop-filter: blur(20px);
43
+ border: 1px solid var(--pb-border);
44
+ border-radius: var(--pb-radius);
45
+ padding: 0.5rem 1rem;
46
+ box-shadow: 0 8px 32px var(--pb-shadow);
47
+ display: flex;
48
+ flex-wrap: wrap;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ gap: 0.75rem;
52
+ }
53
+
54
+ .pb-tool-selector {
55
+ display: flex;
56
+ gap: 0.25rem;
57
+ background: var(--pb-bg-muted);
58
+ border-radius: 0.75rem;
59
+ padding: 0.25rem;
60
+ }
61
+
62
+ .pb-tool-btn {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 0.4rem;
66
+ padding: 0.4rem 0.875rem;
67
+ border-radius: 0.625rem;
68
+ font-size: 0.8rem;
69
+ font-weight: 700;
70
+ color: var(--pb-text-muted);
71
+ border: none;
72
+ background: transparent;
73
+ cursor: pointer;
74
+ transition: all 0.15s;
75
+ }
76
+
77
+ .pb-tool-btn-active {
78
+ background: var(--pb-bg);
79
+ color: var(--pb-text);
80
+ box-shadow: 0 2px 8px var(--pb-shadow);
81
+ }
82
+
83
+ .pb-settings-row {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 0.875rem;
87
+ }
88
+
89
+ .pb-intensity-wrap {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 0.5rem;
93
+ color: var(--pb-text-muted);
94
+ }
95
+
96
+ .pb-slider {
97
+ width: 90px;
98
+ accent-color: var(--pb-primary);
99
+ }
100
+
101
+ .pb-auto-btn {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.4rem;
105
+ padding: 0.45rem 0.875rem;
106
+ background: var(--pb-primary-light);
107
+ color: var(--pb-primary);
108
+ border: none;
109
+ border-radius: 0.75rem;
110
+ font-size: 0.8rem;
111
+ font-weight: 700;
112
+ cursor: pointer;
113
+ transition: opacity 0.15s;
114
+ }
115
+
116
+ .pb-auto-btn:hover {
117
+ opacity: 0.8;
118
+ }
119
+
120
+ .pb-action-group {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.625rem;
124
+ }
125
+
126
+ .pb-undo-btn {
127
+ width: 2.5rem;
128
+ height: 2.5rem;
129
+ border-radius: 0.75rem;
130
+ background: var(--pb-bg-muted);
131
+ border: 1px solid var(--pb-border);
132
+ color: var(--pb-text-muted);
133
+ cursor: pointer;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ transition: all 0.15s;
138
+ }
139
+
140
+ .pb-undo-btn:hover:not(:disabled) {
141
+ border-color: var(--pb-primary);
142
+ color: var(--pb-primary);
143
+ }
144
+
145
+ .pb-undo-btn:disabled {
146
+ opacity: 0.35;
147
+ cursor: not-allowed;
148
+ }
149
+
150
+ .pb-download-btn {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 0.5rem;
154
+ padding: 0.55rem 1.25rem;
155
+ background: var(--pb-primary);
156
+ color: #fff;
157
+ border: none;
158
+ border-radius: 0.75rem;
159
+ font-size: 0.875rem;
160
+ font-weight: 700;
161
+ cursor: pointer;
162
+ box-shadow: 0 4px 14px rgba(99,102,241,0.35);
163
+ transition: all 0.2s;
164
+ }
165
+
166
+ .pb-download-btn:hover:not(:disabled) {
167
+ transform: translateY(-1px);
168
+ box-shadow: 0 6px 20px rgba(99,102,241,0.45);
169
+ }
170
+
171
+ .pb-download-btn:disabled {
172
+ opacity: 0.4;
173
+ cursor: not-allowed;
174
+ box-shadow: none;
175
+ }
176
+
177
+ .pb-icon {
178
+ width: 1.1rem;
179
+ height: 1.1rem;
180
+ flex-shrink: 0;
181
+ }
182
+
183
+ .pb-workspace {
184
+ flex: 1;
185
+ position: relative;
186
+ background: var(--pb-bg-muted);
187
+ border: 2px dashed var(--pb-border);
188
+ border-radius: var(--pb-radius);
189
+ display: flex;
190
+ flex-direction: column;
191
+ align-items: center;
192
+ justify-content: center;
193
+ overflow: hidden;
194
+ transition: border-color 0.2s;
195
+ }
196
+
197
+ .pb-dragging .pb-workspace {
198
+ border-color: var(--pb-primary);
199
+ background: var(--pb-primary-light);
200
+ }
201
+
202
+ .pb-empty {
203
+ width: 100%;
204
+ height: 100%;
205
+ display: flex;
206
+ flex-direction: column;
207
+ align-items: center;
208
+ justify-content: center;
209
+ text-align: center;
210
+ cursor: pointer;
211
+ padding: 2rem;
212
+ gap: 0.75rem;
213
+ }
214
+
215
+ .pb-upload-icon {
216
+ width: 5rem;
217
+ height: 5rem;
218
+ background: var(--pb-bg);
219
+ border: 1px solid var(--pb-border);
220
+ border-radius: 1.25rem;
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: center;
224
+ color: var(--pb-primary);
225
+ box-shadow: 0 8px 24px var(--pb-shadow);
226
+ margin-bottom: 0.5rem;
227
+ }
228
+
229
+ .pb-icon-lg {
230
+ width: 2.5rem;
231
+ height: 2.5rem;
232
+ }
233
+
234
+ .pb-empty-title {
235
+ font-size: 1.75rem;
236
+ font-weight: 900;
237
+ color: var(--pb-text);
238
+ margin: 0;
239
+ }
240
+
241
+ .pb-empty-sub {
242
+ font-size: 1rem;
243
+ color: var(--pb-text-muted);
244
+ margin: 0;
245
+ }
246
+
247
+ .pb-badges {
248
+ display: flex;
249
+ gap: 0.75rem;
250
+ margin-top: 0.5rem;
251
+ }
252
+
253
+ .pb-badge {
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 0.35rem;
257
+ background: var(--pb-bg);
258
+ border: 1px solid var(--pb-border);
259
+ border-radius: 9999px;
260
+ padding: 0.35rem 0.75rem;
261
+ font-size: 0.75rem;
262
+ font-weight: 700;
263
+ color: var(--pb-text-muted);
264
+ }
265
+
266
+ .pb-badge-icon {
267
+ width: 0.875rem;
268
+ height: 0.875rem;
269
+ }
270
+
271
+ .pb-loader {
272
+ position: absolute;
273
+ inset: 0;
274
+ background: rgba(0,0,0,0.5);
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: center;
278
+ justify-content: center;
279
+ gap: 1rem;
280
+ z-index: 10;
281
+ }
282
+
283
+ .pb-spinner {
284
+ width: 2.5rem;
285
+ height: 2.5rem;
286
+ border: 3px solid rgba(255,255,255,0.2);
287
+ border-top-color: #fff;
288
+ border-radius: 50%;
289
+ animation: pb-spin 0.7s linear infinite;
290
+ }
291
+
292
+ @keyframes pb-spin {
293
+ to { transform: rotate(360deg); }
294
+ }
295
+
296
+ .pb-loader-text {
297
+ color: #fff;
298
+ font-weight: 800;
299
+ font-size: 0.9rem;
300
+ margin: 0;
301
+ }
302
+
303
+ .pb-canvas-wrap {
304
+ position: absolute;
305
+ inset: 0;
306
+ background: #000;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ }
311
+
312
+ .pb-canvas-wrap canvas {
313
+ width: 100%;
314
+ height: 100%;
315
+ object-fit: contain;
316
+ cursor: crosshair;
317
+ }
318
+
319
+ .pb-hidden {
320
+ display: none;
321
+ }
322
+
323
+ @media (max-width: 640px) {
324
+ .pb-root {
325
+ padding: 0.5rem;
326
+ height: auto;
327
+ min-height: 500px;
328
+ }
329
+ .pb-empty-title {
330
+ font-size: 1.25rem;
331
+ }
332
+ }
@@ -0,0 +1,124 @@
1
+ import { PrivacyBlurEngine, type ToolType } from './logic';
2
+ import type { PrivacyBlurUI } from './index';
3
+
4
+ export function initPrivacyBlur() {
5
+ const root = document.getElementById('privacy-blur-root');
6
+ if (!root) return;
7
+
8
+ const labels = JSON.parse(root.dataset.ui || '{}') as PrivacyBlurUI;
9
+ const canvas = root.querySelector('#editor-canvas') as HTMLCanvasElement;
10
+ const engine = new PrivacyBlurEngine(canvas);
11
+
12
+ const fileInput = root.querySelector('#file-input') as HTMLInputElement;
13
+ const emptyState = root.querySelector('#empty-state') as HTMLElement;
14
+ const canvasContainer = root.querySelector('#canvas-container') as HTMLElement;
15
+ const undoBtn = root.querySelector('#undo-btn') as HTMLButtonElement;
16
+ const downloadBtn = root.querySelector('#download-btn') as HTMLButtonElement;
17
+ const intensityRange = root.querySelector('#intensity-range') as HTMLInputElement;
18
+ const btnAutoFace = root.querySelector('#btnAutoFace') as HTMLButtonElement;
19
+ const loaderOverlay = root.querySelector('#loader-overlay') as HTMLElement;
20
+ const loaderText = root.querySelector('#loader-text') as HTMLElement;
21
+
22
+ const toolBtns = root.querySelectorAll('.tool-btn-orig');
23
+
24
+ const updateControls = () => {
25
+ undoBtn.disabled = !engine.hasLayers();
26
+ downloadBtn.disabled = !engine.hasLayers();
27
+ };
28
+
29
+ const handleFile = (file: File) => {
30
+ const reader = new FileReader();
31
+ reader.onload = (e) => {
32
+ const img = new Image();
33
+ img.onload = () => {
34
+ engine.setImage(img);
35
+ emptyState.classList.add('hidden');
36
+ canvasContainer.classList.remove('hidden');
37
+ updateControls();
38
+ };
39
+ img.src = e.target?.result as string;
40
+ };
41
+ reader.readAsDataURL(file);
42
+ };
43
+
44
+ fileInput.onchange = () => {
45
+ if (fileInput.files?.[0]) handleFile(fileInput.files[0]);
46
+ };
47
+
48
+ emptyState.onclick = () => fileInput.click();
49
+
50
+ root.ondragover = (e) => {
51
+ e.preventDefault();
52
+ root.classList.add('dragging');
53
+ };
54
+ root.ondragleave = () => root.classList.remove('dragging');
55
+ root.ondrop = (e) => {
56
+ e.preventDefault();
57
+ root.classList.remove('dragging');
58
+ if (e.dataTransfer?.files[0]) handleFile(e.dataTransfer.files[0]);
59
+ };
60
+
61
+ toolBtns.forEach(btn => {
62
+ (btn as HTMLElement).onclick = () => {
63
+ toolBtns.forEach(b => b.classList.remove('active'));
64
+ btn.classList.add('active');
65
+ engine.updateLastLayerTool((btn as HTMLElement).dataset.tool as ToolType);
66
+ };
67
+ });
68
+
69
+ intensityRange.oninput = () => {
70
+ engine.setIntensity(parseInt(intensityRange.value));
71
+ };
72
+
73
+ undoBtn.onclick = () => {
74
+ engine.undo();
75
+ updateControls();
76
+ };
77
+
78
+ downloadBtn.onclick = () => {
79
+ const link = document.createElement('a');
80
+ link.download = `privacy_protected_${Date.now()}.webp`;
81
+ link.href = engine.getCanvasData();
82
+ link.click();
83
+ };
84
+
85
+ btnAutoFace.onclick = async () => {
86
+ loaderOverlay.classList.remove('hidden');
87
+ try {
88
+ const found = await engine.detectFaces((text) => {
89
+ loaderText.textContent = text;
90
+ });
91
+ if (!found) {
92
+ alert(labels.noFacesDetected);
93
+ }
94
+ updateControls();
95
+ } catch (err) {
96
+ console.error(err);
97
+ alert("Error in detection");
98
+ } finally {
99
+ loaderOverlay.classList.add('hidden');
100
+ }
101
+ };
102
+
103
+ canvas.onmousedown = (e) => engine.startDragging(e.clientX, e.clientY);
104
+ window.onmousemove = (e) => engine.updateDragging(e.clientX, e.clientY);
105
+ window.onmouseup = () => {
106
+ if (engine.endDragging()) updateControls();
107
+ };
108
+
109
+ canvas.addEventListener('touchstart', (e) => {
110
+ e.preventDefault();
111
+ engine.startDragging(e.touches[0].clientX, e.touches[0].clientY);
112
+ }, { passive: false });
113
+
114
+ canvas.addEventListener('touchmove', (e) => {
115
+ e.preventDefault();
116
+ engine.updateDragging(e.touches[0].clientX, e.touches[0].clientY);
117
+ }, { passive: false });
118
+
119
+ canvas.addEventListener('touchend', () => {
120
+ if (engine.endDragging()) updateControls();
121
+ });
122
+
123
+ updateControls();
124
+ }