feat(editor): standardize blur strength, localize help text, and optimize export performance

This commit is contained in:
Mahdy Arief
2026-04-11 07:39:35 +07:00
parent 45ee7105ef
commit a7e65905df
10 changed files with 81 additions and 20 deletions
@@ -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 (
<div
className="h-full w-full bg-slate-400/10"
style={{
backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)",
backdropFilter: blurStyle,
WebkitBackdropFilter: blurStyle,
boxShadow: isSelected ? "inset 0 0 0 1px rgba(255,255,255,0.2)" : "none",
}}
/>
);
default:
return null;
}
@@ -511,8 +511,9 @@ export function AnnotationSettingsPanel({
</div>
<h4 className="text-sm font-medium text-slate-200 mb-1">{t('annotations.blur')}</h4>
<p className="text-xs text-slate-400 max-w-[200px]">
Resize and reposition the box on the video to blur sensitive information.
{t('annotations.blurDescription')}
</p>
</div>
</TabsContent>
</Tabs>
+4
View File
@@ -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"
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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",
+5 -1
View File
@@ -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": "에디터",
+5 -1
View File
@@ -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",
+3 -1
View File
@@ -36,8 +36,10 @@
"tipCycleForward": "使用 Tab 循环切换重叠项目。",
"tipCycleBackward": "使用 Shift+Tab 反向循环切换。",
"imageUploadSuccess": "图片上传成功!",
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。"
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。",
"blurDescription": "调整视频上方框的大小和位置,以模糊敏感信息。"
},
"fontStyles": {
"classic": "经典",
"editor": "编辑器",
+39 -3
View File
@@ -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<string, HTMLImageElement>;
@@ -6,6 +8,16 @@ export interface AnnotationRenderAssets {
const annotationImagePromiseCache = new Map<string, Promise<HTMLImageElement | null>>();
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;
}
}
}
}
+8 -9
View File
@@ -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();