diff --git a/package-lock.json b/package-lock.json index 4f854bc..a449101 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@types/gif.js": "^0.2.5", "@uiw/color-convert": "^2.10.1", "@uiw/react-color-block": "^2.10.1", + "@uiw/react-color-colorful": "^2.9.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dnd-timeline": "^2.4.0", @@ -4073,6 +4074,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.10.1", "resolved": "https://registry.npmjs.org/@uiw/react-color-block/-/react-color-block-2.10.1.tgz", @@ -4092,6 +4123,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.10.1", "resolved": "https://registry.npmjs.org/@uiw/react-color-editable-input/-/react-color-editable-input-2.10.1.tgz", @@ -4106,6 +4169,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.10.1", "resolved": "https://registry.npmjs.org/@uiw/react-color-swatch/-/react-color-swatch-2.10.1.tgz", @@ -4123,6 +4246,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": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", diff --git a/package.json b/package.json index dbd5862..102e97c 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@types/gif.js": "^0.2.5", "@uiw/color-convert": "^2.10.1", "@uiw/react-color-block": "^2.10.1", + "@uiw/react-color-colorful": "^2.9.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dnd-timeline": "^2.4.0", diff --git a/src/components/ui/color-picker.tsx b/src/components/ui/color-picker.tsx new file mode 100644 index 0000000..d8ec2b3 --- /dev/null +++ b/src/components/ui/color-picker.tsx @@ -0,0 +1,161 @@ +import { HsvaColor, hexToHsva } from "@uiw/color-convert"; +import Block from "@uiw/react-color-block"; +import Colorful from "@uiw/react-color-colorful"; +import { useEffect, useState } from "react"; +import { Button } from "./button"; +import { Input } from "./input"; + +type BaseProps = { + selectedColor: string; + colorPalette: string[]; + onUpdateColor: (color: string) => void; +}; + +type ColorPickerProps = + | (BaseProps & { + clearBackgroundOption?: false; + translations: Record<"colorWheel" | "colorPalette", string>; + }) + | (BaseProps & { + clearBackgroundOption: true; + translations: Record<"colorWheel" | "colorPalette" | "clearBackground", string>; + }); + +export default function ColorPicker(props: ColorPickerProps) { + const { selectedColor, colorPalette, translations, onUpdateColor } = props; + const [colorMode, setColorMode] = useState<"wheel" | "palette">("wheel"); + const [hexInput, setHexInput] = useState(selectedColor); + const [transparentColorHSVA, setTransparentColorHSVA] = useState({ + h: 0, + s: 0, + v: 0, + a: 0, + }); + + useEffect(() => { + setHexInput(selectedColor); + }, [selectedColor]); + + 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"; + }; + + // Normalize the hex input. + // Adds a # at the beginning of the input if it's not there. + const normalizeHexDraft = (raw: string) => { + const trimmed = raw.trim(); + if (trimmed === "") return ""; + if (/^[0-9A-Fa-f]/.test(trimmed[0])) return `#${trimmed}`; + return trimmed; + }; + + const handleColorInputChange = (e: React.ChangeEvent) => { + const normalized = normalizeHexDraft(e.target.value); + setHexInput(normalized); + // Check if the normalized hex is a valid hex color. + // It should follow the format #RRGGBB or #RGB. + const isValidHexColor = + /^#[0-9A-Fa-f]{3}$/.test(normalized) || /^#[0-9A-Fa-f]{6}$/.test(normalized); + if (isValidHexColor) { + onUpdateColor(normalized); + } + }; + + const toTransparent = (color: string) => { + if (color === "transparent") return; + const hsva = hexToHsva(color); + hsva.a = 0; + return hsva; + }; + return ( +
+
+ + +
+ {colorMode === "wheel" && ( + <> +
+ {selectedColor} +
+ { + onUpdateColor(color.hex); + }} + style={{ + borderRadius: "8px", + }} + disableAlpha={true} + /> + + + )} + {colorMode === "palette" && ( + { + onUpdateColor(color.hex); + }} + style={{ + width: "100%", + borderRadius: "8px", + }} + /> + )} + {props.clearBackgroundOption === true && ( + + )} +
+ ); +} diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index 4c26c88..3f8064e 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -31,6 +31,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useScopedT } from "@/contexts/I18nContext"; import { type CustomFont, getCustomFonts } from "@/lib/customFonts"; import { cn } from "@/lib/utils"; +import ColorPicker from "../ui/color-picker"; import { AddCustomFontDialog } from "./AddCustomFontDialog"; import { getArrowComponent } from "./ArrowSvgs"; import { @@ -75,7 +76,6 @@ export function AnnotationSettingsPanel({ const t = useScopedT("settings"); const fileInputRef = useRef(null); const [customFonts, setCustomFonts] = useState([]); - const fontStyleLabels: Record = { classic: t("fontStyles.classic"), editor: t("fontStyles.editor"), @@ -388,15 +388,19 @@ export function AnnotationSettingsPanel({ - - { - onStyleChange({ color: color.hex }); + + { + onStyleChange({ color: color }); }} /> @@ -427,31 +431,23 @@ export function AnnotationSettingsPanel({ - - { - onStyleChange({ backgroundColor: color.hex }); + + { + onStyleChange({ backgroundColor: color }); }} /> - diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index f21f018..82e106c 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -1,4 +1,3 @@ -import Block from "@uiw/react-color-block"; import { Bug, Crop, @@ -41,6 +40,7 @@ import { cn } from "@/lib/utils"; import { resolveImageWallpaperUrl, WALLPAPER_PATHS } from "@/lib/wallpaper"; import { type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils"; import { getTestId } from "@/utils/getTestId"; +import ColorPicker from "../ui/color-picker"; import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; import { BlurSettingsPanel } from "./BlurSettingsPanel"; import { CropControl } from "./CropControl"; @@ -1035,7 +1035,7 @@ export function SettingsPanel({ -
+
-
- { - setSelectedColor(color.hex); - onWallpaperChange(color.hex); - }} - style={{ - width: "100%", - borderRadius: "8px", - }} - /> -
+ { + setSelectedColor(color); + onWallpaperChange(color); + }} + />
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index f737cfc..9b85c2b 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -45,7 +45,9 @@ "color": "Color", "gradient": "Gradient", "uploadCustom": "Upload Custom", - "gradientLabel": "Gradient {{index}}" + "gradientLabel": "Gradient {{index}}", + "colorWheel": "Color Wheel", + "colorPalette": "Color Palette" }, "crop": { "title": "Crop", @@ -113,6 +115,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", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index c48ed5c..423b158 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -45,7 +45,9 @@ "color": "Color", "gradient": "Degradado", "uploadCustom": "Subir personalizado", - "gradientLabel": "Degradado {{index}}" + "gradientLabel": "Degradado {{index}}", + "colorWheel": "Rueda de colores", + "colorPalette": "Paleta de colores" }, "crop": { "title": "Recortar", @@ -113,6 +115,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", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 0dff11f..66df1ba 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -52,7 +52,9 @@ "color": "Couleur", "gradient": "Dégradé", "uploadCustom": "Téléverser une image", - "gradientLabel": "Dégradé {{index}}" + "gradientLabel": "Dégradé {{index}}", + "colorWheel": "Roue chromatique", + "colorPalette": "Palette de couleurs" }, "crop": { "title": "Recadrage", @@ -120,6 +122,8 @@ "background": "Arrière-plan", "none": "Aucun", "color": "Couleur", + "colorWheel": "Roue chromatique", + "colorPalette": "Palette de couleurs", "clearBackground": "Supprimer l'arrière-plan", "uploadImage": "Téléverser une image", "supportedFormats": "Formats supportés : JPG, PNG, GIF, WebP", diff --git a/src/i18n/locales/ja-JP/settings.json b/src/i18n/locales/ja-JP/settings.json index 9cad3ef..129217c 100644 --- a/src/i18n/locales/ja-JP/settings.json +++ b/src/i18n/locales/ja-JP/settings.json @@ -52,7 +52,9 @@ "color": "色", "gradient": "グラデーション", "uploadCustom": "カスタムをアップロード", - "gradientLabel": "グラデーション {{index}}" + "gradientLabel": "グラデーション {{index}}", + "colorWheel": "カラーホイール", + "colorPalette": "カラーパレット" }, "crop": { "title": "クロップ", @@ -120,6 +122,8 @@ "background": "背景", "none": "なし", "color": "色", + "colorWheel": "カラーホイール", + "colorPalette": "カラーパレット", "clearBackground": "背景をクリア", "uploadImage": "画像をアップロード", "supportedFormats": "サポートされている形式: JPG, PNG, GIF, WebP", diff --git a/src/i18n/locales/ko-KR/settings.json b/src/i18n/locales/ko-KR/settings.json index cd9f734..5defbb6 100644 --- a/src/i18n/locales/ko-KR/settings.json +++ b/src/i18n/locales/ko-KR/settings.json @@ -44,7 +44,9 @@ "color": "색상", "gradient": "그라디언트", "uploadCustom": "직접 업로드", - "gradientLabel": "그라디언트 {{index}}" + "gradientLabel": "그라디언트 {{index}}", + "colorWheel": "색상 휠", + "colorPalette": "색상 팔레트" }, "crop": { "title": "자르기", @@ -111,6 +113,8 @@ "background": "배경", "none": "없음", "color": "색상", + "colorWheel": "색상 휠", + "colorPalette": "색상 팔레트", "clearBackground": "배경 지우기", "uploadImage": "이미지 업로드", "supportedFormats": "지원 형식: JPG, PNG, GIF, WebP", diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json index 936f75c..7bc60e4 100644 --- a/src/i18n/locales/tr/settings.json +++ b/src/i18n/locales/tr/settings.json @@ -41,7 +41,9 @@ "color": "Renk", "gradient": "Gradyan", "uploadCustom": "Özel Yükle", - "gradientLabel": "Gradyan {{index}}" + "gradientLabel": "Gradyan {{index}}", + "colorWheel": "Renk çarkı", + "colorPalette": "Renk paleti" }, "crop": { "title": "Kırpma", @@ -109,6 +111,8 @@ "background": "Arka Plan", "none": "Yok", "color": "Renk", + "colorWheel": "Renk çarkı", + "colorPalette": "Renk paleti", "clearBackground": "Arka Planı Temizle", "uploadImage": "Görüntü Yükle", "supportedFormats": "Desteklenen biçimler: JPG, PNG, GIF, WebP", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 299483a..5cffcb5 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -45,7 +45,9 @@ "color": "颜色", "gradient": "渐变", "uploadCustom": "上传自定义", - "gradientLabel": "渐变 {{index}}" + "gradientLabel": "渐变 {{index}}", + "colorWheel": "颜色轮", + "colorPalette": "颜色调色板" }, "crop": { "title": "裁剪", @@ -113,6 +115,8 @@ "background": "背景", "none": "无", "color": "颜色", + "colorWheel": "颜色轮", + "colorPalette": "颜色调色板", "clearBackground": "清除背景", "uploadImage": "上传图片", "supportedFormats": "支持的格式:JPG、PNG、GIF、WebP", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 6344a99..652ab5a 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -52,7 +52,9 @@ "color": "顏色", "gradient": "漸層", "uploadCustom": "上傳自訂", - "gradientLabel": "漸層 {{index}}" + "gradientLabel": "漸層 {{index}}", + "colorWheel": "色輪", + "colorPalette": "調色盤" }, "crop": { "title": "裁剪", @@ -120,6 +122,8 @@ "background": "背景", "none": "無", "color": "顏色", + "colorWheel": "色輪", + "colorPalette": "調色盤", "clearBackground": "清除背景", "uploadImage": "上傳圖片", "supportedFormats": "支援的格式:JPG、PNG、GIF、WebP",