mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 15:55:35 +00:00
feat(editor): standardize blur strength, localize help text, and optimize export performance
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "에디터",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -36,8 +36,10 @@
|
||||
"tipCycleForward": "使用 Tab 循环切换重叠项目。",
|
||||
"tipCycleBackward": "使用 Shift+Tab 反向循环切换。",
|
||||
"imageUploadSuccess": "图片上传成功!",
|
||||
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。"
|
||||
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。",
|
||||
"blurDescription": "调整视频上方框的大小和位置,以模糊敏感信息。"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
"classic": "经典",
|
||||
"editor": "编辑器",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user