mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-10-03 02:57:55 +00:00
Merge pull request #210 from mahdyarief/feat/blur-annotation
feat(blur): implement blur annotation support
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useRef } from "react";
|
||||
import { Rnd } from "react-rnd";
|
||||
import type { AnnotationRegion } from "./types";
|
||||
import { BLUR_ANNOTATION_STRENGTH, BASE_PREVIEW_WIDTH, type AnnotationRegion } from "./types";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getArrowComponent } from "./ArrowSvgs";
|
||||
|
||||
@@ -111,6 +112,25 @@ export function AnnotationOverlay({
|
||||
</div>
|
||||
);
|
||||
|
||||
case "blur": {
|
||||
const previewScaleFactor = containerWidth / BASE_PREVIEW_WIDTH;
|
||||
const currentBlurStrength = annotation.blurIntensity ?? BLUR_ANNOTATION_STRENGTH;
|
||||
const blurPx = currentBlurStrength * previewScaleFactor;
|
||||
const blurStyle = `blur(${blurPx}px)`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full bg-slate-400/10"
|
||||
style={{
|
||||
backdropFilter: blurStyle,
|
||||
WebkitBackdropFilter: blurStyle,
|
||||
backgroundColor: annotation.blurColor || "transparent",
|
||||
borderRadius: `${(annotation.style.borderRadius ?? 0) * previewScaleFactor}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useState, useEffect, useMemo } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown, Info } from "lucide-react";
|
||||
import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown, Info, SquareDashed } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import Block from '@uiw/react-color-block';
|
||||
import type { AnnotationRegion, AnnotationType, ArrowDirection, FigureData } from "./types";
|
||||
@@ -21,6 +21,8 @@ interface AnnotationSettingsPanelProps {
|
||||
onTypeChange: (type: AnnotationType) => void;
|
||||
onStyleChange: (style: Partial<AnnotationRegion['style']>) => void;
|
||||
onFigureDataChange?: (figureData: FigureData) => void;
|
||||
onBlurIntensityChange?: (intensity: number) => void;
|
||||
onBlurColorChange?: (color: string) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
@@ -43,6 +45,8 @@ export function AnnotationSettingsPanel({
|
||||
onTypeChange,
|
||||
onStyleChange,
|
||||
onFigureDataChange,
|
||||
onBlurIntensityChange,
|
||||
onBlurColorChange,
|
||||
onDelete,
|
||||
}: AnnotationSettingsPanelProps) {
|
||||
const t = useScopedT('editor');
|
||||
@@ -128,7 +132,7 @@ export function AnnotationSettingsPanel({
|
||||
|
||||
{/* Type Selector */}
|
||||
<Tabs value={annotation.type} onValueChange={(value) => onTypeChange(value as AnnotationType)} className="mb-6">
|
||||
<TabsList className="mb-4 bg-white/5 border border-white/5 p-1 w-full grid grid-cols-3 h-auto rounded-xl">
|
||||
<TabsList className="mb-4 bg-white/5 border border-white/5 p-1 w-full grid grid-cols-4 h-auto rounded-xl">
|
||||
<TabsTrigger value="text" className="data-[state=active]:bg-[#2563EB] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all gap-2">
|
||||
<Type className="w-4 h-4" />
|
||||
{t('annotations.text')}
|
||||
@@ -143,6 +147,10 @@ export function AnnotationSettingsPanel({
|
||||
</svg>
|
||||
{t('annotations.arrow')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="blur" className="data-[state=active]:bg-[#2563EB] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all gap-2">
|
||||
<SquareDashed className="w-4 h-4" />
|
||||
{t('annotations.blur')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Text Content */}
|
||||
@@ -499,6 +507,95 @@ export function AnnotationSettingsPanel({
|
||||
</Popover>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="blur" className="mt-0 space-y-4">
|
||||
<div className="p-4 bg-white/5 rounded-xl border border-white/10 flex flex-col items-center">
|
||||
<div className="w-full space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-slate-200">
|
||||
{t('annotations.blurStrength', undefined, { strength: annotation.blurIntensity ?? 20 })}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[annotation.blurIntensity ?? 20]}
|
||||
onValueChange={([value]) => onBlurIntensityChange?.(value)}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-3 mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-slate-200">
|
||||
{t('annotations.solidColor', 'Solid Color (Censorship)')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => onBlurColorChange?.('')}
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full border-2 flex items-center justify-center transition-all",
|
||||
!annotation.blurColor || annotation.blurColor === 'transparent' ? "border-[#2563EB] scale-110" : "border-transparent hover:border-white/20"
|
||||
)}
|
||||
title={t('annotations.none', 'None')}
|
||||
>
|
||||
<div className="w-5 h-5 rounded-full bg-slate-800 flex items-center justify-center overflow-hidden relative">
|
||||
<div className="absolute w-full h-0.5 bg-red-500 rotate-45" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onBlurColorChange?.('#000000')}
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full border-2 transition-all bg-black",
|
||||
annotation.blurColor === '#000000' ? "border-[#2563EB] scale-110" : "border-transparent hover:border-white/20"
|
||||
)}
|
||||
title="Black"
|
||||
/>
|
||||
<button
|
||||
onClick={() => onBlurColorChange?.('#FFFFFF')}
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full border-2 transition-all bg-white",
|
||||
annotation.blurColor === '#FFFFFF' ? "border-[#2563EB] scale-110" : "border-transparent hover:border-white/20"
|
||||
)}
|
||||
title="White"
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full border-2 transition-all flex items-center justify-center overflow-hidden relative",
|
||||
annotation.blurColor && !['#000000', '#FFFFFF', 'transparent', ''].includes(annotation.blurColor) ? "border-[#2563EB] scale-110" : "border-transparent hover:border-white/20"
|
||||
)}
|
||||
style={{ backgroundColor: (annotation.blurColor && !['#000000', '#FFFFFF', 'transparent', ''].includes(annotation.blurColor)) ? annotation.blurColor : 'transparent' }}
|
||||
title="Custom Color"
|
||||
>
|
||||
{(!annotation.blurColor || ['#000000', '#FFFFFF', 'transparent', ''].includes(annotation.blurColor)) && (
|
||||
<div className="w-full h-full flex items-center justify-center bg-white/5">
|
||||
<div className="w-full h-full bg-gradient-to-tr from-red-500 via-green-500 to-blue-500 opacity-50" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl">
|
||||
<Block
|
||||
color={annotation.blurColor || '#2563EB'}
|
||||
colors={colorPalette}
|
||||
onChange={(color) => {
|
||||
onBlurColorChange?.(color.hex);
|
||||
}}
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -223,6 +223,8 @@ interface SettingsPanelProps {
|
||||
onAnnotationTypeChange?: (id: string, type: AnnotationType) => void;
|
||||
onAnnotationStyleChange?: (id: string, style: Partial<AnnotationRegion["style"]>) => void;
|
||||
onAnnotationFigureDataChange?: (id: string, figureData: FigureData) => void;
|
||||
onAnnotationBlurIntensityChange?: (id: string, intensity: number) => void;
|
||||
onAnnotationBlurColorChange?: (id: string, color: string) => void;
|
||||
onAnnotationDelete?: (id: string) => void;
|
||||
autoCaptions?: CaptionCue[];
|
||||
autoCaptionSettings?: AutoCaptionSettings;
|
||||
@@ -570,6 +572,8 @@ export function SettingsPanel({
|
||||
onAnnotationTypeChange,
|
||||
onAnnotationStyleChange,
|
||||
onAnnotationFigureDataChange,
|
||||
onAnnotationBlurIntensityChange,
|
||||
onAnnotationBlurColorChange,
|
||||
onAnnotationDelete,
|
||||
autoCaptions = [],
|
||||
autoCaptionSettings = DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
@@ -1301,6 +1305,16 @@ export function SettingsPanel({
|
||||
? (figureData) => onAnnotationFigureDataChange(selectedAnnotation.id, figureData)
|
||||
: undefined
|
||||
}
|
||||
onBlurIntensityChange={
|
||||
onAnnotationBlurIntensityChange
|
||||
? (intensity) => onAnnotationBlurIntensityChange(selectedAnnotation.id, intensity)
|
||||
: undefined
|
||||
}
|
||||
onBlurColorChange={
|
||||
onAnnotationBlurColorChange
|
||||
? (color) => onAnnotationBlurColorChange(selectedAnnotation.id, color)
|
||||
: undefined
|
||||
}
|
||||
onDelete={() => onAnnotationDelete(selectedAnnotation.id)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2689,6 +2689,11 @@ export default function VideoEditor() {
|
||||
if (!region.figureData) {
|
||||
updatedRegion.figureData = { ...DEFAULT_FIGURE_DATA };
|
||||
}
|
||||
} else if (type === "blur") {
|
||||
updatedRegion.content = "";
|
||||
if (region.blurIntensity === undefined) {
|
||||
updatedRegion.blurIntensity = 20;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedRegion;
|
||||
@@ -2714,6 +2719,18 @@ export default function VideoEditor() {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleAnnotationBlurIntensityChange = useCallback((id: string, blurIntensity: number) => {
|
||||
setAnnotationRegions((prev) =>
|
||||
prev.map((region) => (region.id === id ? { ...region, blurIntensity } : region)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleAnnotationBlurColorChange = useCallback((id: string, blurColor: string) => {
|
||||
setAnnotationRegions((prev) =>
|
||||
prev.map((region) => (region.id === id ? { ...region, blurColor } : region)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleAnnotationPositionChange = useCallback(
|
||||
(id: string, position: { x: number; y: number }) => {
|
||||
setAnnotationRegions((prev) =>
|
||||
@@ -4511,6 +4528,8 @@ export default function VideoEditor() {
|
||||
onAnnotationTypeChange={handleAnnotationTypeChange}
|
||||
onAnnotationStyleChange={handleAnnotationStyleChange}
|
||||
onAnnotationFigureDataChange={handleAnnotationFigureDataChange}
|
||||
onAnnotationBlurIntensityChange={handleAnnotationBlurIntensityChange}
|
||||
onAnnotationBlurColorChange={handleAnnotationBlurColorChange}
|
||||
onAnnotationDelete={handleAnnotationDelete}
|
||||
selectedSpeedId={selectedSpeedId}
|
||||
selectedSpeedValue={
|
||||
|
||||
@@ -399,7 +399,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
id: region.id,
|
||||
startMs,
|
||||
endMs,
|
||||
type: region.type === "image" || region.type === "figure" ? region.type : "text",
|
||||
type: region.type === "image" || region.type === "figure" || region.type === "blur" ? region.type : "text",
|
||||
content: typeof region.content === "string" ? region.content : "",
|
||||
textContent: typeof region.textContent === "string" ? region.textContent : undefined,
|
||||
imageContent: typeof region.imageContent === "string" ? region.imageContent : undefined,
|
||||
@@ -446,6 +446,10 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
...region.figureData,
|
||||
}
|
||||
: undefined,
|
||||
blurIntensity: isFiniteNumber(region.blurIntensity)
|
||||
? clamp(region.blurIntensity, 1, 100)
|
||||
: 20,
|
||||
blurColor: typeof region.blurColor === "string" ? region.blurColor : undefined,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
@@ -175,7 +175,11 @@ export function trimsToClips(trims: TrimRegion[], totalDurationMs: number): Clip
|
||||
return clips;
|
||||
}
|
||||
|
||||
export type AnnotationType = "text" | "image" | "figure";
|
||||
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"
|
||||
@@ -212,6 +216,7 @@ export interface AnnotationTextStyle {
|
||||
fontStyle: "normal" | "italic";
|
||||
textDecoration: "none" | "underline";
|
||||
textAlign: "left" | "center" | "right";
|
||||
borderRadius: number;
|
||||
}
|
||||
|
||||
function getDefaultAnnotationFontFamily() {
|
||||
@@ -243,6 +248,8 @@ export interface AnnotationRegion {
|
||||
style: AnnotationTextStyle;
|
||||
zIndex: number;
|
||||
figureData?: FigureData;
|
||||
blurIntensity?: number;
|
||||
blurColor?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_ANNOTATION_POSITION: AnnotationPosition = {
|
||||
@@ -264,6 +271,7 @@ export const DEFAULT_ANNOTATION_STYLE: AnnotationTextStyle = {
|
||||
fontStyle: "normal",
|
||||
textDecoration: "none",
|
||||
textAlign: "center",
|
||||
borderRadius: 8,
|
||||
};
|
||||
|
||||
export const DEFAULT_FIGURE_DATA: FigureData = {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"text": "Text",
|
||||
"image": "Image",
|
||||
"arrow": "Arrow",
|
||||
"blur": "Blur",
|
||||
"textContent": "Text Content",
|
||||
"textPlaceholder": "Enter your text...",
|
||||
"fontStyle": "Font Style",
|
||||
@@ -35,8 +36,12 @@
|
||||
"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.",
|
||||
"blurStrength": "Blur Strength: {{strength}}",
|
||||
"solidColor": "Solid Color (Censorship)",
|
||||
"borderRadius": "Border Radius"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
"classic": "Classic",
|
||||
"editor": "Editor",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"text": "Texto",
|
||||
"image": "Imagen",
|
||||
"arrow": "Flecha",
|
||||
"blur": "Desenfoque",
|
||||
"textContent": "Contenido de texto",
|
||||
"textPlaceholder": "Ingresa tu texto...",
|
||||
"fontStyle": "Estilo de fuente",
|
||||
@@ -35,8 +36,12 @@
|
||||
"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.",
|
||||
"blurStrength": "Fuerza del Desenfoque: {{strength}}",
|
||||
"solidColor": "Color Sólido (Censura)",
|
||||
"borderRadius": "Radio del Borde"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
"classic": "Clásico",
|
||||
"editor": "Editor",
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"text": "텍스트",
|
||||
"image": "이미지",
|
||||
"arrow": "화살표",
|
||||
"blur": "흐리게",
|
||||
"textContent": "텍스트 내용",
|
||||
|
||||
"textPlaceholder": "텍스트를 입력하세요...",
|
||||
"fontStyle": "글꼴 스타일",
|
||||
"selectStyle": "스타일 선택",
|
||||
@@ -35,8 +37,12 @@
|
||||
"tipCycleForward": "Tab 키로 겹치는 항목을 순환할 수 있습니다.",
|
||||
"tipCycleBackward": "Shift+Tab으로 이전 항목으로 이동할 수 있습니다.",
|
||||
"imageUploadSuccess": "이미지를 업로드했습니다!",
|
||||
"imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요."
|
||||
"imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요.",
|
||||
"blurStrength": "블러 강도: {{strength}}",
|
||||
"solidColor": "단색 (검열)",
|
||||
"borderRadius": "테두리 반경"
|
||||
},
|
||||
|
||||
"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,12 @@
|
||||
"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.",
|
||||
"blurStrength": "Vervagingssterkte: {{strength}}",
|
||||
"solidColor": "Effen Kleur (Censuur)",
|
||||
"borderRadius": "Hoekradius"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
"classic": "Klassiek",
|
||||
"editor": "Editor",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"text": "文本",
|
||||
"image": "图片",
|
||||
"arrow": "箭头",
|
||||
"blur": "模糊",
|
||||
"textContent": "文本内容",
|
||||
"textPlaceholder": "输入文本...",
|
||||
"fontStyle": "字体样式",
|
||||
@@ -35,8 +36,12 @@
|
||||
"tipCycleForward": "使用 Tab 循环切换重叠项目。",
|
||||
"tipCycleBackward": "使用 Shift+Tab 反向循环切换。",
|
||||
"imageUploadSuccess": "图片上传成功!",
|
||||
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。"
|
||||
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。",
|
||||
"blurStrength": "模糊强度: {{strength}}",
|
||||
"solidColor": "纯色 (审查)",
|
||||
"borderRadius": "边框半径"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
"classic": "经典",
|
||||
"editor": "编辑器",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AnnotationRegion, ArrowDirection } from "@/components/video-editor/types";
|
||||
import { BLUR_ANNOTATION_STRENGTH, type AnnotationRegion, type ArrowDirection } from "@/components/video-editor/types";
|
||||
|
||||
|
||||
export interface AnnotationRenderAssets {
|
||||
imageCache: Map<string, HTMLImageElement>;
|
||||
@@ -6,6 +7,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")) {
|
||||
@@ -358,6 +369,47 @@ export async function renderAnnotations(
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "blur": {
|
||||
const blurStrength = (annotation.blurIntensity ?? BLUR_ANNOTATION_STRENGTH) * scaleFactor;
|
||||
const padding = Math.ceil(blurStrength * 2);
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.beginPath();
|
||||
const borderRadius = (annotation.style.borderRadius ?? 0) * scaleFactor;
|
||||
ctx.roundRect(x, y, width, height, borderRadius);
|
||||
ctx.clip();
|
||||
|
||||
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);
|
||||
|
||||
if (annotation.blurColor && annotation.blurColor !== "transparent") {
|
||||
ctx.filter = "none";
|
||||
ctx.fillStyle = annotation.blurColor;
|
||||
ctx.fillRect(x, y, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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