feat(editor): enhance blur annotations with custom intensity, censorship color, and rounded corners

This commit is contained in:
Mahdy Arief
2026-04-11 08:06:45 +07:00
parent 431ee5ff92
commit 1567eebe8a
12 changed files with 118 additions and 21 deletions
@@ -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,
};
})
: [];
+3
View File
@@ -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 = {
+3 -1
View File
@@ -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": {
+3 -1
View File
@@ -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": {
+3 -1
View File
@@ -38,7 +38,9 @@
"tipCycleBackward": "Shift+Tab으로 이전 항목으로 이동할 수 있습니다.",
"imageUploadSuccess": "이미지를 업로드했습니다!",
"imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요.",
"blurDescription": "민감한 정보를 흐리게 하려면 비디오의 상자 크기를 조정하고 위치를 변경하십시오."
"blurStrength": "블러 강도: {{strength}}",
"solidColor": "단색 (검열)",
"borderRadius": "테두리 반경"
},
"fontStyles": {
+3 -1
View File
@@ -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": {
+3 -1
View File
@@ -37,7 +37,9 @@
"tipCycleBackward": "使用 Shift+Tab 反向循环切换。",
"imageUploadSuccess": "图片上传成功!",
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。",
"blurDescription": "调整视频上方框的大小和位置,以模糊敏感信息。"
"blurStrength": "模糊强度: {{strength}}",
"solidColor": "纯色 (审查)",
"borderRadius": "边框半径"
},
"fontStyles": {
+10 -4
View File
@@ -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);
}
}
}