mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
feat(editor): enhance blur annotations with custom intensity, censorship color, and rounded corners
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useRef } from "react";
|
||||
import { Rnd } from "react-rnd";
|
||||
import type { AnnotationRegion } from "./types";
|
||||
import { BLUR_ANNOTATION_STRENGTH, BASE_PREVIEW_WIDTH } from "./types";
|
||||
import { BLUR_ANNOTATION_STRENGTH, BASE_PREVIEW_WIDTH, type AnnotationRegion } from "./types";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getArrowComponent } from "./ArrowSvgs";
|
||||
@@ -125,7 +124,8 @@ export function AnnotationOverlay({
|
||||
style={{
|
||||
backdropFilter: blurStyle,
|
||||
WebkitBackdropFilter: blurStyle,
|
||||
boxShadow: isSelected ? "inset 0 0 0 1px rgba(255,255,255,0.2)" : "none",
|
||||
backgroundColor: annotation.blurColor || "transparent",
|
||||
borderRadius: `${(annotation.style.borderRadius ?? 0) * previewScaleFactor}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ interface AnnotationSettingsPanelProps {
|
||||
onStyleChange: (style: Partial<AnnotationRegion['style']>) => void;
|
||||
onFigureDataChange?: (figureData: FigureData) => void;
|
||||
onBlurIntensityChange?: (intensity: number) => void;
|
||||
onBlurColorChange?: (color: string) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ export function AnnotationSettingsPanel({
|
||||
onStyleChange,
|
||||
onFigureDataChange,
|
||||
onBlurIntensityChange,
|
||||
onBlurColorChange,
|
||||
onDelete,
|
||||
}: AnnotationSettingsPanelProps) {
|
||||
const t = useScopedT('editor');
|
||||
@@ -507,15 +509,7 @@ export function AnnotationSettingsPanel({
|
||||
</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 justify-center text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-[#2563EB]/20 flex items-center justify-center mb-3">
|
||||
<SquareDashed className="w-6 h-6 text-[#2563EB]" />
|
||||
</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] mb-4">
|
||||
{t('annotations.blurDescription')}
|
||||
</p>
|
||||
|
||||
<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">
|
||||
@@ -531,6 +525,75 @@ export function AnnotationSettingsPanel({
|
||||
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>
|
||||
|
||||
@@ -224,6 +224,7 @@ interface SettingsPanelProps {
|
||||
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;
|
||||
@@ -572,6 +573,7 @@ export function SettingsPanel({
|
||||
onAnnotationStyleChange,
|
||||
onAnnotationFigureDataChange,
|
||||
onAnnotationBlurIntensityChange,
|
||||
onAnnotationBlurColorChange,
|
||||
onAnnotationDelete,
|
||||
autoCaptions = [],
|
||||
autoCaptionSettings = DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
@@ -1308,6 +1310,11 @@ export function SettingsPanel({
|
||||
? (intensity) => onAnnotationBlurIntensityChange(selectedAnnotation.id, intensity)
|
||||
: undefined
|
||||
}
|
||||
onBlurColorChange={
|
||||
onAnnotationBlurColorChange
|
||||
? (color) => onAnnotationBlurColorChange(selectedAnnotation.id, color)
|
||||
: undefined
|
||||
}
|
||||
onDelete={() => onAnnotationDelete(selectedAnnotation.id)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2725,6 +2725,12 @@ export default function VideoEditor() {
|
||||
);
|
||||
}, []);
|
||||
|
||||
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) =>
|
||||
@@ -4523,6 +4529,7 @@ export default function VideoEditor() {
|
||||
onAnnotationStyleChange={handleAnnotationStyleChange}
|
||||
onAnnotationFigureDataChange={handleAnnotationFigureDataChange}
|
||||
onAnnotationBlurIntensityChange={handleAnnotationBlurIntensityChange}
|
||||
onAnnotationBlurColorChange={handleAnnotationBlurColorChange}
|
||||
onAnnotationDelete={handleAnnotationDelete}
|
||||
selectedSpeedId={selectedSpeedId}
|
||||
selectedSpeedValue={
|
||||
|
||||
@@ -449,6 +449,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
blurIntensity: isFiniteNumber(region.blurIntensity)
|
||||
? clamp(region.blurIntensity, 1, 100)
|
||||
: 20,
|
||||
blurColor: typeof region.blurColor === "string" ? region.blurColor : undefined,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
@@ -216,6 +216,7 @@ export interface AnnotationTextStyle {
|
||||
fontStyle: "normal" | "italic";
|
||||
textDecoration: "none" | "underline";
|
||||
textAlign: "left" | "center" | "right";
|
||||
borderRadius: number;
|
||||
}
|
||||
|
||||
function getDefaultAnnotationFontFamily() {
|
||||
@@ -248,6 +249,7 @@ export interface AnnotationRegion {
|
||||
zIndex: number;
|
||||
figureData?: FigureData;
|
||||
blurIntensity?: number;
|
||||
blurColor?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_ANNOTATION_POSITION: AnnotationPosition = {
|
||||
@@ -269,6 +271,7 @@ export const DEFAULT_ANNOTATION_STYLE: AnnotationTextStyle = {
|
||||
fontStyle: "normal",
|
||||
textDecoration: "none",
|
||||
textAlign: "center",
|
||||
borderRadius: 8,
|
||||
};
|
||||
|
||||
export const DEFAULT_FIGURE_DATA: FigureData = {
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
"tipCycleBackward": "Use Shift+Tab to cycle backwards.",
|
||||
"imageUploadSuccess": "Image uploaded successfully!",
|
||||
"imageUploadError": "Please upload a JPG, PNG, GIF, or WebP image file.",
|
||||
"blurDescription": "Resize and reposition the box on the video to blur sensitive information."
|
||||
"blurStrength": "Blur Strength: {{strength}}",
|
||||
"solidColor": "Solid Color (Censorship)",
|
||||
"borderRadius": "Border Radius"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
"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.",
|
||||
"blurDescription": "Cambia el tamaño y reposiciona el cuadro en el vídeo para difuminar la información sensible."
|
||||
"blurStrength": "Fuerza del Desenfoque: {{strength}}",
|
||||
"solidColor": "Color Sólido (Censura)",
|
||||
"borderRadius": "Radio del Borde"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
"tipCycleBackward": "Shift+Tab으로 이전 항목으로 이동할 수 있습니다.",
|
||||
"imageUploadSuccess": "이미지를 업로드했습니다!",
|
||||
"imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요.",
|
||||
"blurDescription": "민감한 정보를 흐리게 하려면 비디오의 상자 크기를 조정하고 위치를 변경하십시오."
|
||||
"blurStrength": "블러 강도: {{strength}}",
|
||||
"solidColor": "단색 (검열)",
|
||||
"borderRadius": "테두리 반경"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
"tipCycleBackward": "Gebruik Shift+Tab om terug te bladeren.",
|
||||
"imageUploadSuccess": "Afbeelding succesvol geüpload!",
|
||||
"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."
|
||||
"blurStrength": "Vervagingssterkte: {{strength}}",
|
||||
"solidColor": "Effen Kleur (Censuur)",
|
||||
"borderRadius": "Hoekradius"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
"tipCycleBackward": "使用 Shift+Tab 反向循环切换。",
|
||||
"imageUploadSuccess": "图片上传成功!",
|
||||
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。",
|
||||
"blurDescription": "调整视频上方框的大小和位置,以模糊敏感信息。"
|
||||
"blurStrength": "模糊强度: {{strength}}",
|
||||
"solidColor": "纯色 (审查)",
|
||||
"borderRadius": "边框半径"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AnnotationRegion, ArrowDirection } from "@/components/video-editor/types";
|
||||
import { BLUR_ANNOTATION_STRENGTH } from "@/components/video-editor/types";
|
||||
import { BLUR_ANNOTATION_STRENGTH, type AnnotationRegion, type ArrowDirection } from "@/components/video-editor/types";
|
||||
|
||||
|
||||
export interface AnnotationRenderAssets {
|
||||
@@ -372,13 +371,14 @@ export async function renderAnnotations(
|
||||
break;
|
||||
|
||||
case "blur": {
|
||||
const blurStrength = BLUR_ANNOTATION_STRENGTH * scaleFactor;
|
||||
const blurStrength = (annotation.blurIntensity ?? BLUR_ANNOTATION_STRENGTH) * scaleFactor;
|
||||
const padding = Math.ceil(blurStrength * 2);
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, width, height);
|
||||
const borderRadius = (annotation.style.borderRadius ?? 0) * scaleFactor;
|
||||
ctx.roundRect(x, y, width, height, borderRadius);
|
||||
ctx.clip();
|
||||
|
||||
const sx = Math.max(0, x - padding);
|
||||
@@ -396,6 +396,12 @@ export async function renderAnnotations(
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user