diff --git a/src/components/video-editor/AnnotationOverlay.tsx b/src/components/video-editor/AnnotationOverlay.tsx index d413fa07..e7bd16e8 100644 --- a/src/components/video-editor/AnnotationOverlay.tsx +++ b/src/components/video-editor/AnnotationOverlay.tsx @@ -1,6 +1,8 @@ import { useRef } from "react"; import { Rnd } from "react-rnd"; import type { AnnotationRegion } from "./types"; +import { BLUR_ANNOTATION_STRENGTH, BASE_PREVIEW_WIDTH } from "./types"; + import { cn } from "@/lib/utils"; import { getArrowComponent } from "./ArrowSvgs"; @@ -112,16 +114,21 @@ export function AnnotationOverlay({ ); case "blur": + const previewScaleFactor = containerWidth / BASE_PREVIEW_WIDTH; + const blurPx = BLUR_ANNOTATION_STRENGTH * previewScaleFactor; + const blurStyle = `blur(${blurPx}px)`; + return (
); + default: return null; } diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 50aabdb5..79e465a6 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -511,8 +511,9 @@ export function AnnotationSettingsPanel({

{t('annotations.blur')}

- Resize and reposition the box on the video to blur sensitive information. + {t('annotations.blurDescription')}

+ diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index a0a8b354..172ef66f 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -176,6 +176,10 @@ export function trimsToClips(trims: TrimRegion[], totalDurationMs: number): Clip } export type AnnotationType = "text" | "image" | "figure" | "blur"; +export const BLUR_ANNOTATION_STRENGTH = 20; +export const BASE_PREVIEW_WIDTH = 1920; +export const BASE_PREVIEW_HEIGHT = 1080; + export type ArrowDirection = | "up" diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index 0839a045..0ef7e26d 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -36,8 +36,10 @@ "tipCycleForward": "Use Tab to cycle through overlapping items.", "tipCycleBackward": "Use Shift+Tab to cycle backwards.", "imageUploadSuccess": "Image uploaded successfully!", - "imageUploadError": "Please upload a JPG, PNG, GIF, or WebP image file." + "imageUploadError": "Please upload a JPG, PNG, GIF, or WebP image file.", + "blurDescription": "Resize and reposition the box on the video to blur sensitive information." }, + "fontStyles": { "classic": "Classic", "editor": "Editor", diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json index 147d64dd..b2fe99af 100644 --- a/src/i18n/locales/es/editor.json +++ b/src/i18n/locales/es/editor.json @@ -36,8 +36,10 @@ "tipCycleForward": "Usa Tab para recorrer los elementos superpuestos.", "tipCycleBackward": "Usa Shift+Tab para recorrer hacia atrás.", "imageUploadSuccess": "¡Imagen subida exitosamente!", - "imageUploadError": "Por favor sube un archivo de imagen JPG, PNG, GIF o WebP." + "imageUploadError": "Por favor sube un archivo de imagen JPG, PNG, GIF o WebP.", + "blurDescription": "Cambia el tamaño y reposiciona el cuadro en el vídeo para difuminar la información sensible." }, + "fontStyles": { "classic": "Clásico", "editor": "Editor", diff --git a/src/i18n/locales/ko/editor.json b/src/i18n/locales/ko/editor.json index eeb8ea61..cb7ac6bc 100644 --- a/src/i18n/locales/ko/editor.json +++ b/src/i18n/locales/ko/editor.json @@ -9,7 +9,9 @@ "text": "텍스트", "image": "이미지", "arrow": "화살표", + "blur": "흐리게", "textContent": "텍스트 내용", + "textPlaceholder": "텍스트를 입력하세요...", "fontStyle": "글꼴 스타일", "selectStyle": "스타일 선택", @@ -35,8 +37,10 @@ "tipCycleForward": "Tab 키로 겹치는 항목을 순환할 수 있습니다.", "tipCycleBackward": "Shift+Tab으로 이전 항목으로 이동할 수 있습니다.", "imageUploadSuccess": "이미지를 업로드했습니다!", - "imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요." + "imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요.", + "blurDescription": "민감한 정보를 흐리게 하려면 비디오의 상자 크기를 조정하고 위치를 변경하십시오." }, + "fontStyles": { "classic": "클래식", "editor": "에디터", diff --git a/src/i18n/locales/nl/editor.json b/src/i18n/locales/nl/editor.json index 919d66ea..fce65f25 100644 --- a/src/i18n/locales/nl/editor.json +++ b/src/i18n/locales/nl/editor.json @@ -9,7 +9,9 @@ "text": "Tekst", "image": "Afbeelding", "arrow": "Pijl", + "blur": "Vervagen", "textContent": "Tekstinhoud", + "textPlaceholder": "Voer je tekst in...", "fontStyle": "Lettertype", "selectStyle": "Selecteer stijl", @@ -35,8 +37,10 @@ "tipCycleForward": "Gebruik Tab om door overlappende items te bladeren.", "tipCycleBackward": "Gebruik Shift+Tab om terug te bladeren.", "imageUploadSuccess": "Afbeelding succesvol geüpload!", - "imageUploadError": "Upload een JPG-, PNG-, GIF- of WebP-afbeelding." + "imageUploadError": "Upload een JPG-, PNG-, GIF- of WebP-afbeelding.", + "blurDescription": "Pas de grootte aan en verplaats het vak op de video om gevoelige informatie te vervagen." }, + "fontStyles": { "classic": "Klassiek", "editor": "Editor", diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json index cf74a5e0..c690523f 100644 --- a/src/i18n/locales/zh-CN/editor.json +++ b/src/i18n/locales/zh-CN/editor.json @@ -36,8 +36,10 @@ "tipCycleForward": "使用 Tab 循环切换重叠项目。", "tipCycleBackward": "使用 Shift+Tab 反向循环切换。", "imageUploadSuccess": "图片上传成功!", - "imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。" + "imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。", + "blurDescription": "调整视频上方框的大小和位置,以模糊敏感信息。" }, + "fontStyles": { "classic": "经典", "editor": "编辑器", diff --git a/src/lib/exporter/annotationRenderer.ts b/src/lib/exporter/annotationRenderer.ts index 20233113..8bdbddeb 100644 --- a/src/lib/exporter/annotationRenderer.ts +++ b/src/lib/exporter/annotationRenderer.ts @@ -1,4 +1,6 @@ import type { AnnotationRegion, ArrowDirection } from "@/components/video-editor/types"; +import { BLUR_ANNOTATION_STRENGTH } from "@/components/video-editor/types"; + export interface AnnotationRenderAssets { imageCache: Map; @@ -6,6 +8,16 @@ export interface AnnotationRenderAssets { const annotationImagePromiseCache = new Map>(); +let blurBufferCanvas: HTMLCanvasElement | null = null; +function getBlurBufferCanvas(): HTMLCanvasElement { + if (typeof document === "undefined") return {} as HTMLCanvasElement; + if (!blurBufferCanvas) { + blurBufferCanvas = document.createElement("canvas"); + } + return blurBufferCanvas; +} + + function getAnnotationImageContent(annotation: AnnotationRegion): string | null { const source = annotation.imageContent || annotation.content; if (!source || !source.startsWith("data:image")) { @@ -359,15 +371,39 @@ export async function renderAnnotations( } break; - case "blur": + case "blur": { + const blurStrength = BLUR_ANNOTATION_STRENGTH * scaleFactor; + const padding = Math.ceil(blurStrength * 2); + ctx.save(); + ctx.beginPath(); ctx.rect(x, y, width, height); ctx.clip(); - ctx.filter = `blur(${20 * scaleFactor}px)`; - ctx.drawImage(ctx.canvas, 0, 0); + + const sx = Math.max(0, x - padding); + const sy = Math.max(0, y - padding); + const sw = Math.min(canvasWidth - sx, width + padding * 2); + const sh = Math.min(canvasHeight - sy, height + padding * 2); + + if (sw > 0 && sh > 0) { + const buffer = getBlurBufferCanvas(); + buffer.width = sw; + buffer.height = sh; + const bCtx = buffer.getContext("2d"); + if (bCtx) { + bCtx.drawImage(ctx.canvas, sx, sy, sw, sh, 0, 0, sw, sh); + + ctx.filter = `blur(${blurStrength}px)`; + ctx.drawImage(buffer, sx, sy); + } + } + ctx.restore(); break; + } + + } } } diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 33f3f686..808aee0b 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -20,7 +20,8 @@ import type { WebcamOverlaySettings, ZoomTransitionEasing, } from "@/components/video-editor/types"; -import { ZOOM_DEPTH_SCALES } from "@/components/video-editor/types"; +import { ZOOM_DEPTH_SCALES, BASE_PREVIEW_WIDTH, BASE_PREVIEW_HEIGHT } from "@/components/video-editor/types"; + import { getAssetPath, getRenderableAssetUrl } from "@/lib/assetPath"; import { findDominantRegion } from "@/components/video-editor/videoPlayback/zoomRegionUtils"; import { @@ -925,12 +926,11 @@ export class FrameRenderer { this.compositeCtx ) { // Calculate scale factor based on export vs preview dimensions - const previewWidth = this.config.previewWidth || 1920; - const previewHeight = this.config.previewHeight || 1080; - const scaleX = this.config.width / previewWidth; - const scaleY = this.config.height / previewHeight; + const scaleX = this.config.width / BASE_PREVIEW_WIDTH; + const scaleY = this.config.height / BASE_PREVIEW_HEIGHT; const scaleFactor = (scaleX + scaleY) / 2; + await renderAnnotations( this.compositeCtx, this.config.annotationRegions, @@ -1004,12 +1004,11 @@ export class FrameRenderer { this.videoContainer.position.set(0, 0); - const previewWidth = this.config.previewWidth || 1920; - const previewHeight = this.config.previewHeight || 1080; const canvasScaleFactor = Math.min( - width / previewWidth, - height / previewHeight, + width / BASE_PREVIEW_WIDTH, + height / BASE_PREVIEW_HEIGHT, ); + const scaledBorderRadius = borderRadius * canvasScaleFactor; this.maskGraphics.clear();