add color wheel to background and annotations

This commit is contained in:
BaptisteAuscher
2026-04-06 20:37:05 +02:00
parent 24928164ca
commit 7e563166a3
7 changed files with 376 additions and 41 deletions
+137
View File
@@ -25,6 +25,7 @@
"@types/gif.js": "^0.2.5",
"@uiw/color-convert": "^2.9.2",
"@uiw/react-color-block": "^2.9.2",
"@uiw/react-color-colorful": "^2.9.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dnd-timeline": "^2.2.0",
@@ -4875,6 +4876,36 @@
"@babel/runtime": ">=7.19.0"
}
},
"node_modules/@uiw/react-color-alpha": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/react-color-alpha/-/react-color-alpha-2.9.6.tgz",
"integrity": "sha512-DNzEVHZ0Izp4NAwzKqTcl4rLdPjSFjyZCP6Q2vKJEglugZ/bdPsmZaos9IYOrgnd1kPDmTSKZ/p8nI7vBIATGw==",
"license": "MIT",
"dependencies": {
"@uiw/color-convert": "2.9.6",
"@uiw/react-drag-event-interactive": "2.9.6"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@uiw/react-color-alpha/node_modules/@uiw/color-convert": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz",
"integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==",
"license": "MIT",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0"
}
},
"node_modules/@uiw/react-color-block": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@uiw/react-color-block/-/react-color-block-2.9.2.tgz",
@@ -4894,6 +4925,38 @@
"react-dom": ">=16.9.0"
}
},
"node_modules/@uiw/react-color-colorful": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/react-color-colorful/-/react-color-colorful-2.9.6.tgz",
"integrity": "sha512-h74zo+ve9Rpv7xwb1dRfoa23yN39b6eYScDIm7V2d5FzkXN6hR7jnnJ7ZUD9Joz/rdaCz1eFQD9ig+wp8+wSnQ==",
"license": "MIT",
"dependencies": {
"@uiw/color-convert": "2.9.6",
"@uiw/react-color-alpha": "2.9.6",
"@uiw/react-color-hue": "2.9.6",
"@uiw/react-color-saturation": "2.9.6"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@uiw/react-color-colorful/node_modules/@uiw/color-convert": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz",
"integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==",
"license": "MIT",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0"
}
},
"node_modules/@uiw/react-color-editable-input": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input/-/react-color-editable-input-2.9.2.tgz",
@@ -4908,6 +4971,66 @@
"react-dom": ">=16.9.0"
}
},
"node_modules/@uiw/react-color-hue": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/react-color-hue/-/react-color-hue-2.9.6.tgz",
"integrity": "sha512-B99dW2/AHMD3py83BrXl94bhXeGCZR1FMpU/FNbIIbUrV9QTiIXDs2/SB/tMD9ltcSP59RD5Sc5m2vCb/8anjw==",
"license": "MIT",
"dependencies": {
"@uiw/color-convert": "2.9.6",
"@uiw/react-color-alpha": "2.9.6"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@uiw/react-color-hue/node_modules/@uiw/color-convert": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz",
"integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==",
"license": "MIT",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0"
}
},
"node_modules/@uiw/react-color-saturation": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/react-color-saturation/-/react-color-saturation-2.9.6.tgz",
"integrity": "sha512-R1tiKbTG2WiJXerkmuaKnBFfzgyZUn08q9OjQSvNH1f3ov2/YeUVlOwQY9MbQE7ytZv+9x+1h0Lpk4QG7AdulQ==",
"license": "MIT",
"dependencies": {
"@uiw/color-convert": "2.9.6",
"@uiw/react-drag-event-interactive": "2.9.6"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@uiw/react-color-saturation/node_modules/@uiw/color-convert": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.6.tgz",
"integrity": "sha512-w8TpU3MRcquurQJxWR1daKcRygu/a0hLP/VGsLMA3ebb41sAZGxMQLHtS+zC/e3ciFNB7BbPrSPlzOcz6w6cRg==",
"license": "MIT",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0"
}
},
"node_modules/@uiw/react-color-swatch": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@uiw/react-color-swatch/-/react-color-swatch-2.9.2.tgz",
@@ -4925,6 +5048,20 @@
"react-dom": ">=16.9.0"
}
},
"node_modules/@uiw/react-drag-event-interactive": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.9.6.tgz",
"integrity": "sha512-jXzt3Xis/BIYap2Hj2++gB3aEUD0mZoVNGfckurrwjAwxasxNiwkmTGxV5er3due0ZgaVKdOAfTRoYKlgZukSg==",
"license": "MIT",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@babel/runtime": ">=7.19.0",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+1
View File
@@ -47,6 +47,7 @@
"@types/gif.js": "^0.2.5",
"@uiw/color-convert": "^2.9.2",
"@uiw/react-color-block": "^2.9.2",
"@uiw/react-color-colorful": "^2.9.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dnd-timeline": "^2.2.0",
@@ -1,4 +1,5 @@
import Block from "@uiw/react-color-block";
import Colorful from "@uiw/react-color-colorful";
import {
AlignCenter,
AlignLeft,
@@ -67,7 +68,7 @@ export function AnnotationSettingsPanel({
const t = useScopedT("settings");
const fileInputRef = useRef<HTMLInputElement>(null);
const [customFonts, setCustomFonts] = useState<CustomFont[]>([]);
const [colorMode, setColorMode] = useState<"wheel" | "palette">("wheel");
const fontStyleLabels: Record<string, string> = {
classic: t("fontStyles.classic"),
editor: t("fontStyles.editor"),
@@ -139,6 +140,15 @@ export function AnnotationSettingsPanel({
event.target.value = "";
};
const getTextColor = (color: string) => {
if (color === "transparent") return "#ffffff";
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
if (luminance > 186) return "#000000";
return "#ffffff";
};
return (
<div className="flex-[2] min-w-0 bg-[#09090b] border border-white/5 rounded-2xl p-4 flex flex-col shadow-xl h-full overflow-y-auto custom-scrollbar">
<div className="mb-6">
@@ -380,17 +390,68 @@ export function AnnotationSettingsPanel({
<ChevronDown className="h-3 w-3 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl">
<Block
color={annotation.style.color}
colors={colorPalette}
onChange={(color) => {
onStyleChange({ color: color.hex });
}}
style={{
borderRadius: "8px",
}}
/>
<PopoverContent
side="top"
className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl"
>
<div className="flex flex-col gap-4 items-center">
{colorMode === "palette" && (
<Block
color={annotation.style.color}
colors={colorPalette}
onChange={(color) => {
onStyleChange({ color: color.hex });
}}
style={{
borderRadius: "8px",
}}
/>
)}
{colorMode === "wheel" && (
<>
<div
className={`w-full h-20 flex items-center justify-center border border-white/10 rounded-lg`}
style={{ backgroundColor: annotation.style.color }}
>
<span style={{ color: getTextColor(annotation.style.color) }}>
{annotation.style.color}
</span>
</div>
<Colorful
color={annotation.style.color}
onChange={(color) => {
onStyleChange({ color: color.hex });
}}
style={{
borderRadius: "8px",
}}
disableAlpha={true}
/>
</>
)}
<div className="flex items-center gap-2 w-full">
<Button
variant="outline"
size="sm"
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
onClick={() => setColorMode("wheel")}
>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{t("annotation.colorWheel")}
</span>
</Button>
<Button
variant="outline"
size="sm"
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
onClick={() => setColorMode("palette")}
>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{t("annotation.colorPalette")}
</span>
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
@@ -419,21 +480,74 @@ export function AnnotationSettingsPanel({
<ChevronDown className="h-3 w-3 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl">
<Block
color={
annotation.style.backgroundColor === "transparent"
? "#000000"
: annotation.style.backgroundColor
}
colors={colorPalette}
onChange={(color) => {
onStyleChange({ backgroundColor: color.hex });
}}
style={{
borderRadius: "8px",
}}
/>
<PopoverContent
side="top"
className="w-[260px] p-3 bg-[#1a1a1c] border border-white/10 rounded-xl shadow-xl"
>
<div className="flex flex-col gap-4 items-center items-center">
{colorMode === "palette" && (
<Block
color={
annotation.style.backgroundColor === "transparent"
? "#000000"
: annotation.style.backgroundColor
}
colors={colorPalette}
onChange={(color) => {
onStyleChange({ backgroundColor: color.hex });
}}
style={{
borderRadius: "8px",
}}
/>
)}
{colorMode === "wheel" && (
<>
<div
className={`w-full h-20 flex items-center justify-center border border-white/10 rounded-lg`}
style={{ backgroundColor: annotation.style.backgroundColor }}
>
<span
style={{ color: getTextColor(annotation.style.backgroundColor) }}
>
{annotation.style.backgroundColor}
</span>
</div>
<Colorful
color={annotation.style.backgroundColor}
onChange={(color) => {
onStyleChange({ backgroundColor: color.hex });
}}
style={{
borderRadius: "8px",
}}
disableAlpha={true}
/>
</>
)}
<div className="flex items-center gap-2 w-full">
<Button
variant="outline"
size="sm"
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
onClick={() => setColorMode("wheel")}
>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{t("annotation.colorWheel")}
</span>
</Button>
<Button
variant="outline"
size="sm"
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
onClick={() => setColorMode("palette")}
>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{t("annotation.colorPalette")}
</span>
</Button>
</div>
</div>
<Button
variant="ghost"
size="sm"
+91 -14
View File
@@ -1,4 +1,5 @@
import Block from "@uiw/react-color-block";
import Colorful from "@uiw/react-color-colorful";
import {
Bug,
Crop,
@@ -41,6 +42,7 @@ import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
import { cn } from "@/lib/utils";
import { type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils";
import { getTestId } from "@/utils/getTestId";
import { Input } from "../ui/input";
import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel";
import { CropControl } from "./CropControl";
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp";
@@ -227,6 +229,7 @@ export function SettingsPanel({
const t = useScopedT("settings");
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
const [customImages, setCustomImages] = useState<string[]>([]);
const [backgroundColorMode, setBackgroundColorMode] = useState<"wheel" | "palette">("wheel");
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -319,6 +322,16 @@ export function SettingsPanel({
[cropRegion, onCropChange, videoWidth, videoHeight, cropAspectLocked],
);
const getTextColor = (color: string) => {
if (color === "transparent") return "#ffffff";
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
if (luminance > 186) return "#000000";
return "#ffffff";
};
const applyCropAspectPreset = useCallback(
(preset: string) => {
if (!cropRegion || !onCropChange) return;
@@ -900,7 +913,7 @@ export function SettingsPanel({
</TabsTrigger>
</TabsList>
<div className="max-h-[min(200px,25vh)] overflow-y-auto custom-scrollbar">
<div className="overflow-y-auto custom-scrollbar">
<TabsContent value="image" className="mt-0 space-y-2">
<input
type="file"
@@ -988,19 +1001,83 @@ export function SettingsPanel({
</TabsContent>
<TabsContent value="color" className="mt-0">
<div className="p-1">
<Block
color={selectedColor}
colors={colorPalette}
onChange={(color) => {
setSelectedColor(color.hex);
onWallpaperChange(color.hex);
}}
style={{
width: "100%",
borderRadius: "8px",
}}
/>
<div className="p-1 flex flex-col gap-4 items-center">
<div className="flex items-center gap-2 w-full">
<Button
variant="outline"
size="sm"
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
onClick={() => setBackgroundColorMode("wheel")}
style={{
backgroundColor:
backgroundColorMode === "wheel" ? "#34B27B" : "transparent",
}}
>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{t("annotation.colorWheel")}
</span>
</Button>
<Button
variant="outline"
size="sm"
className="w-full h-9 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10 px-2"
onClick={() => setBackgroundColorMode("palette")}
style={{
backgroundColor:
backgroundColorMode === "palette" ? "#34B27B" : "transparent",
}}
>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{t("annotation.colorPalette")}
</span>
</Button>
</div>
{backgroundColorMode === "wheel" && (
<>
<div
className={`w-full h-20 flex items-center justify-center border border-white/10 rounded-lg`}
style={{ backgroundColor: selectedColor }}
>
<span style={{ color: getTextColor(selectedColor) }}>
{selectedColor}
</span>
</div>
<Colorful
color={selectedColor}
onChange={(color) => {
setSelectedColor(color.hex);
onWallpaperChange(color.hex);
}}
style={{
borderRadius: "8px",
}}
disableAlpha={true}
/>
<Input
type="text"
value={selectedColor}
className="w-full h-9 rounded-md border border-white/10 bg-white/5 px-2 text-xs text-slate-200 outline-none focus:border-[#34B27B]/50 focus:ring-1 focus:ring-[#34B27B]/30 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
onChange={(e) => {
setSelectedColor(e.target.value);
onWallpaperChange(e.target.value);
}}
/>
</>
)}
{backgroundColorMode === "palette" && (
<Block
color={selectedColor}
colors={colorPalette}
onChange={(color) => {
setSelectedColor(color.hex);
onWallpaperChange(color.hex);
}}
style={{
width: "100%",
borderRadius: "8px",
}}
/>
)}
</div>
</TabsContent>
+2
View File
@@ -108,6 +108,8 @@
"background": "Background",
"none": "None",
"color": "Color",
"colorWheel": "Color Wheel",
"colorPalette": "Color Palette",
"clearBackground": "Clear Background",
"uploadImage": "Upload Image",
"supportedFormats": "Supported formats: JPG, PNG, GIF, WebP",
+2
View File
@@ -108,6 +108,8 @@
"background": "Fondo",
"none": "Ninguno",
"color": "Color",
"colorWheel": "Rueda de colores",
"colorPalette": "Paleta de colores",
"clearBackground": "Quitar fondo",
"uploadImage": "Subir imagen",
"supportedFormats": "Formatos compatibles: JPG, PNG, GIF, WebP",
+2
View File
@@ -108,6 +108,8 @@
"background": "背景",
"none": "无",
"color": "颜色",
"colorWheel": "颜色轮",
"colorPalette": "颜色调色板",
"clearBackground": "清除背景",
"uploadImage": "上传图片",
"supportedFormats": "支持的格式:JPG、PNG、GIF、WebP",