mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 02:59:07 +08:00
fix: follow-up to PR#9384 (#9495)
* fix: set the default text color of 'P' to change with the theme * Update AddProviderPopup.tsx * refactor(utils): 将 generateColorFromChar 函数从 naming 模块移动到 style 模块 移动 generateColorFromChar 函数到更合适的 style 模块,并更新相关测试文件 * feat(style): 添加十六进制颜色验证和前景色计算功能 添加颜色工具函数包括: - 十六进制颜色格式验证 - RGB值转换 - 相对亮度计算 - 根据背景色自动选择前景色功能 这些功能用于确保颜色可访问性和文字可读性 * refactor(types): 将HexColor类型移动到types模块 将HexColor类型定义从style.ts移动到types/index.ts中,保持类型定义集中管理 * feat(ProviderSettings): 为自定义提供商添加前景色计算 添加 getForegroundColor 工具函数用于计算自定义提供商 logo 的前景色 在 ProvidersList 和 AddProviderPopup 组件中应用前景色计算 确保 logo 文字在不同背景色下保持可读性 * refactor(types): 将 isHexColor 函数从 utils/style.ts 移动到 types/index.ts 统一颜色相关类型和函数的存放位置,提高代码组织性 * feat(图标): 添加PoeLogo图标并支持自定义尺寸 在ProviderSettings页面中添加PoeLogo图标支持,并扩展getProviderAvatar函数以支持自定义尺寸参数 修复SVGIcon组件中fill-rule属性的命名错误,统一使用camelCase命名规范 * refactor(providers): 移除poe.svg并使用svg图标组件 * fix(SVGIcon): 修正SVG属性stop-color为stopColor以符合React规范 * Update src/renderer/src/types/index.ts --------- Co-authored-by: icarus <eurfelux@gmail.com> Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com>
This commit is contained in:
parent
2633a1429a
commit
4b02878390
@ -1 +0,0 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Poe</title><path d="M20.708 6.876a1.412 1.412 0 00-1.029-.415h-.006a2.019 2.019 0 01-2.02-2.023A1.415 1.415 0 0016.254 3H4.871A1.412 1.412 0 003.47 4.434a2.026 2.026 0 01-2.025 2.025v.002A1.414 1.414 0 000 7.883v3.642a1.414 1.414 0 001.444 1.42 2.025 2.025 0 012.025 2.02v3.693a.5.5 0 00.89.313l2.051-2.567h9.843a1.412 1.412 0 001.4-1.434v-.002c0-1.12.904-2.025 2.026-2.025a1.412 1.412 0 001.446-1.42V7.88c0-.363-.14-.727-.417-1.005zm-2.42 4.687a2.025 2.025 0 01-2.025 2.005H4.861a2.025 2.025 0 01-2.025-2.005v-3.72A2.026 2.026 0 014.86 5.838h11.4a2.026 2.026 0 012.026 2.005v3.72h.002z"></path><path d="M7.413 7.57A1.422 1.422 0 005.99 8.99v1.422a1.422 1.422 0 102.844 0V8.99c0-.784-.636-1.422-1.422-1.422zm6.297 0a1.422 1.422 0 00-1.422 1.421v1.422a1.422 1.422 0 102.844 0V8.99c0-.784-.636-1.422-1.422-1.422z"></path><path d="M7.292 22.643l1.993-2.492h9.844a1.413 1.413 0 001.4-1.434 2.025 2.025 0 012.017-2.027h.01A1.409 1.409 0 0024 15.27v-3.594c0-.344-.113-.68-.324-.951l-.397-.519v4.127a1.415 1.415 0 01-1.444 1.42h-.007a2.026 2.026 0 00-2.018 2.025 1.415 1.415 0 01-1.402 1.436H8.565l-2.169 2.712a.574.574 0 00.896.715v.002z" fill="url(#lobe-icons-poe-fill-0)"></path><path d="M5.004 19.992l2.12-2.65h9.844a1.414 1.414 0 001.402-1.437c0-1.116.9-2.021 2.014-2.025h.012a1.413 1.413 0 001.443-1.422v-4.13l.52.68c.21.273.324.607.324.95v3.594a1.416 1.416 0 01-1.443 1.42h-.01a2.026 2.026 0 00-2.016 2.026 1.414 1.414 0 01-1.402 1.435H7.97l-1.916 2.4a.671.671 0 01-1.049-.839v-.002z" fill="url(#lobe-icons-poe-fill-1)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-poe-fill-0" x1="34.01" x2="1.086" y1="7.303" y2="27.715"><stop stop-color="#46A6F7"></stop><stop offset="1" stop-color="#8364FF"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-poe-fill-1" x1="4.915" x2="24.34" y1="23.511" y2="9.464"><stop stop-color="#FF44D3"></stop><stop offset="1" stop-color="#CF4BFF"></stop></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@ -117,7 +117,7 @@ export function BingLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -193,7 +193,7 @@ export function ExaLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -211,30 +211,75 @@ export function BochaLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="1em" height="1em" viewBox="0 0 135 116" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.5754 13.8123C24.6109 7.94459 39.1223 12.9435 44.9955 24.9805L57.5355 50.6805C60.4695 56.6936 57.9756 63.9478 51.9652 66.8832C51.9627 66.8844 51.9602 66.8856 51.9577 66.8868C45.94 69.8206 38.6843 67.3212 35.7477 61.3027L12.5754 13.8123Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
opacity="0.64774"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 38.3013C9.46916 28.836 24.813 28.836 34.2822 38.3013L55.2526 59.2631C59.9819 63.9904 59.9852 71.6582 55.2601 76.3896C55.2576 76.3921 55.2551 76.3946 55.2526 76.397C50.5181 81.1297 42.8461 81.1297 38.1116 76.397L0 38.3013Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M86.8777 18.0444C113.939 18.0444 135.876 39.9725 135.876 67.0222C135.876 80.2286 129.086 93.6477 120.585 102.457L117.065 98.2367C111.026 90.9998 108.882 81.2777 111.314 72.1702C111.755 70.5198 111.976 69.0033 111.976 67.6209C111.976 53.6689 100.661 42.3586 86.7029 42.3586C72.7452 42.3586 61.4303 53.6689 61.4303 67.6209C61.4303 81.5728 72.7452 92.8831 86.7029 92.8831C89.3159 92.8831 91.8363 92.4867 94.2071 91.7508C101.312 89.5455 109.054 91.3768 114.419 96.5322L120.585 102.457C111.83 110.626 99.7992 116 86.8777 116C59.8168 116 37.8796 94.0719 37.8796 67.0222C37.8796 39.9725 59.8168 18.0444 86.8777 18.0444Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M37.8796 0C51.2677 0 62.1208 10.8581 62.1208 24.2522V41.7389C62.1208 55.133 51.2677 65.9911 37.8796 65.9911V0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function PoeLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
height="1em"
|
||||
width="1em"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}>
|
||||
<title>Poe</title>
|
||||
<path d="M20.708 6.876a1.412 1.412 0 00-1.029-.415h-.006a2.019 2.019 0 01-2.02-2.023A1.415 1.415 0 0016.254 3H4.871A1.412 1.412 0 003.47 4.434a2.026 2.026 0 01-2.025 2.025v.002A1.414 1.414 0 000 7.883v3.642a1.414 1.414 0 001.444 1.42 2.025 2.025 0 012.025 2.02v3.693a.5.5 0 00.89.313l2.051-2.567h9.843a1.412 1.412 0 001.4-1.434v-.002c0-1.12.904-2.025 2.026-2.025a1.412 1.412 0 001.446-1.42V7.88c0-.363-.14-.727-.417-1.005zm-2.42 4.687a2.025 2.025 0 01-2.025 2.005H4.861a2.025 2.025 0 01-2.025-2.005v-3.72A2.026 2.026 0 014.86 5.838h11.4a2.026 2.026 0 012.026 2.005v3.72h.002z"></path>
|
||||
<path d="M7.413 7.57A1.422 1.422 0 005.99 8.99v1.422a1.422 1.422 0 102.844 0V8.99c0-.784-.636-1.422-1.422-1.422zm6.297 0a1.422 1.422 0 00-1.422 1.421v1.422a1.422 1.422 0 102.844 0V8.99c0-.784-.636-1.422-1.422-1.422z"></path>
|
||||
<path
|
||||
d="M7.292 22.643l1.993-2.492h9.844a1.413 1.413 0 001.4-1.434 2.025 2.025 0 012.017-2.027h.01A1.409 1.409 0 0024 15.27v-3.594c0-.344-.113-.68-.324-.951l-.397-.519v4.127a1.415 1.415 0 01-1.444 1.42h-.007a2.026 2.026 0 00-2.018 2.025 1.415 1.415 0 01-1.402 1.436H8.565l-2.169 2.712a.574.574 0 00.896.715v.002z"
|
||||
fill="url(#lobe-icons-poe-fill-0)"></path>
|
||||
<path
|
||||
d="M5.004 19.992l2.12-2.65h9.844a1.414 1.414 0 001.402-1.437c0-1.116.9-2.021 2.014-2.025h.012a1.413 1.413 0 001.443-1.422v-4.13l.52.68c.21.273.324.607.324.95v3.594a1.416 1.416 0 01-1.443 1.42h-.01a2.026 2.026 0 00-2.016 2.026 1.414 1.414 0 01-1.402 1.435H7.97l-1.916 2.4a.671.671 0 01-1.049-.839v-.002z"
|
||||
fill="url(#lobe-icons-poe-fill-1)"></path>
|
||||
<defs>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="lobe-icons-poe-fill-0"
|
||||
x1="34.01"
|
||||
x2="1.086"
|
||||
y1="7.303"
|
||||
y2="27.715">
|
||||
<stop stopColor="#46A6F7"></stop>
|
||||
<stop offset="1" stop-color="#8364FF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="lobe-icons-poe-fill-1"
|
||||
x1="4.915"
|
||||
x2="24.34"
|
||||
y1="23.511"
|
||||
y2="9.464">
|
||||
<stop stopColor="#FF44D3"></stop>
|
||||
<stop offset="1" stop-color="#CF4BFF"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,7 +38,6 @@ import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
|
||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||
import PerplexityProviderLogo from '@renderer/assets/images/providers/perplexity.png'
|
||||
import Ph8ProviderLogo from '@renderer/assets/images/providers/ph8.png'
|
||||
import PoeProviderLogo from '@renderer/assets/images/providers/poe.svg'
|
||||
import PPIOProviderLogo from '@renderer/assets/images/providers/ppio.png'
|
||||
import QiniuProviderLogo from '@renderer/assets/images/providers/qiniu.webp'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
@ -649,7 +648,7 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
|
||||
vertexai: VertexAIProviderLogo,
|
||||
'new-api': NewAPIProviderLogo,
|
||||
'aws-bedrock': AwsProviderLogo,
|
||||
poe: PoeProviderLogo
|
||||
poe: 'svg' // use svg icon component
|
||||
} as const
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
|
||||
@ -5,7 +5,7 @@ import { TopView } from '@renderer/components/TopView'
|
||||
import { PROVIDER_LOGO_MAP } from '@renderer/config/providers'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import { Provider, ProviderType } from '@renderer/types'
|
||||
import { compressImage, generateColorFromChar } from '@renderer/utils'
|
||||
import { compressImage, generateColorFromChar, getForegroundColor } from '@renderer/utils'
|
||||
import { Divider, Dropdown, Form, Input, Modal, Popover, Select, Upload } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -182,6 +182,10 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
||||
}
|
||||
]
|
||||
|
||||
// for logo
|
||||
const backgroundColor = generateColorFromChar(name)
|
||||
const color = name ? getForegroundColor(backgroundColor) : 'white'
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@ -224,8 +228,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
||||
{logo ? (
|
||||
<ProviderLogo src={logo} />
|
||||
) : (
|
||||
<ProviderInitialsLogo
|
||||
style={name ? { backgroundColor: generateColorFromChar(name) } : { color: 'black' }}>
|
||||
<ProviderInitialsLogo style={name ? { backgroundColor, color } : undefined}>
|
||||
{getInitials()}
|
||||
</ProviderInitialsLogo>
|
||||
)}
|
||||
@ -294,7 +297,6 @@ const ProviderInitialsLogo = styled.div`
|
||||
transition: opacity 0.3s ease;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
color: white;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { DropResult } from '@hello-pangea/dnd'
|
||||
import { loggerService } from '@logger'
|
||||
import { DraggableVirtualList, useDraggableReorder } from '@renderer/components/DraggableList'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { DeleteIcon, EditIcon, PoeLogo } from '@renderer/components/Icons'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
@ -11,6 +11,7 @@ import {
|
||||
generateColorFromChar,
|
||||
getFancyProviderName,
|
||||
getFirstCharacter,
|
||||
getForegroundColor,
|
||||
matchKeywordsInModel,
|
||||
matchKeywordsInProvider,
|
||||
uuid
|
||||
@ -406,22 +407,31 @@ const ProvidersList: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getProviderAvatar = (provider: Provider) => {
|
||||
const getProviderAvatar = (provider: Provider, size: number = 25) => {
|
||||
// 特殊处理一下svg格式
|
||||
if (isSystemProvider(provider)) {
|
||||
switch (provider.id) {
|
||||
case 'poe':
|
||||
return <PoeLogo fontSize={size} />
|
||||
}
|
||||
}
|
||||
|
||||
const logoSrc = getProviderLogo(provider.id)
|
||||
if (logoSrc) {
|
||||
return <ProviderLogo draggable="false" shape="circle" src={logoSrc} size={25} />
|
||||
return <ProviderLogo draggable="false" shape="circle" src={logoSrc} size={size} />
|
||||
}
|
||||
|
||||
const customLogo = providerLogos[provider.id]
|
||||
if (customLogo) {
|
||||
return <ProviderLogo draggable="false" shape="square" src={customLogo} size={25} />
|
||||
return <ProviderLogo draggable="false" shape="square" src={customLogo} size={size} />
|
||||
}
|
||||
|
||||
// generate color for custom provider
|
||||
const backgroundColor = generateColorFromChar(provider.name)
|
||||
const color = provider.name ? getForegroundColor(backgroundColor) : 'white'
|
||||
|
||||
return (
|
||||
<ProviderLogo
|
||||
size={25}
|
||||
shape="square"
|
||||
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
|
||||
<ProviderLogo size={size} shape="square" style={{ backgroundColor, color, minWidth: size }}>
|
||||
{getFirstCharacter(provider.name)}
|
||||
</ProviderLogo>
|
||||
)
|
||||
|
||||
@ -1148,3 +1148,13 @@ export type AtLeast<T extends string, U> = {
|
||||
} & {
|
||||
[key: string]: U
|
||||
}
|
||||
|
||||
export type HexColor = string
|
||||
|
||||
/**
|
||||
* 检查字符串是否为有效的十六进制颜色值
|
||||
* @param value 待检查的字符串
|
||||
*/
|
||||
export const isHexColor = (value: string): value is HexColor => {
|
||||
return /^#([0-9A-F]{3}){1,2}$/i.test(value)
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
firstLetter,
|
||||
generateColorFromChar,
|
||||
getBaseModelName,
|
||||
getBriefInfo,
|
||||
getDefaultGroupName,
|
||||
@ -222,28 +221,6 @@ describe('naming', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateColorFromChar', () => {
|
||||
it('should generate a valid hex color code', () => {
|
||||
// 验证生成有效的十六进制颜色代码
|
||||
const result = generateColorFromChar('A')
|
||||
expect(result).toMatch(/^#[0-9a-fA-F]{6}$/)
|
||||
})
|
||||
|
||||
it('should generate consistent color for same input', () => {
|
||||
// 验证相同输入生成一致的颜色
|
||||
const result1 = generateColorFromChar('A')
|
||||
const result2 = generateColorFromChar('A')
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
it('should generate different colors for different inputs', () => {
|
||||
// 验证不同输入生成不同的颜色
|
||||
const result1 = generateColorFromChar('A')
|
||||
const result2 = generateColorFromChar('B')
|
||||
expect(result1).not.toBe(result2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFirstCharacter', () => {
|
||||
it('should return first character of string', () => {
|
||||
// 验证返回字符串的第一个字符
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { classNames } from '../style'
|
||||
import { classNames, generateColorFromChar } from '../style'
|
||||
|
||||
describe('style', () => {
|
||||
describe('classNames', () => {
|
||||
@ -75,4 +75,26 @@ describe('style', () => {
|
||||
expect(classNames('foo', '', 'bar')).toBe('foo bar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateColorFromChar', () => {
|
||||
it('should generate a valid hex color code', () => {
|
||||
// 验证生成有效的十六进制颜色代码
|
||||
const result = generateColorFromChar('A')
|
||||
expect(result).toMatch(/^#[0-9a-fA-F]{6}$/)
|
||||
})
|
||||
|
||||
it('should generate consistent color for same input', () => {
|
||||
// 验证相同输入生成一致的颜色
|
||||
const result1 = generateColorFromChar('A')
|
||||
const result2 = generateColorFromChar('A')
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
it('should generate different colors for different inputs', () => {
|
||||
// 验证不同输入生成不同的颜色
|
||||
const result1 = generateColorFromChar('A')
|
||||
const result2 = generateColorFromChar('B')
|
||||
expect(result1).not.toBe(result2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -143,34 +143,6 @@ export function removeSpecialCharactersForTopicName(str: string): string {
|
||||
return str.replace(/["'\r\n]+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字符生成颜色代码,用于 avatar。
|
||||
* @param {string} char 输入字符
|
||||
* @returns {string} 十六进制颜色字符串
|
||||
*/
|
||||
export function generateColorFromChar(char: string): string {
|
||||
// 使用字符的Unicode值作为随机种子
|
||||
const seed = char.charCodeAt(0)
|
||||
|
||||
// 使用简单的线性同余生成器创建伪随机数
|
||||
const a = 1664525
|
||||
const c = 1013904223
|
||||
const m = Math.pow(2, 32)
|
||||
|
||||
// 生成三个伪随机数作为RGB值
|
||||
let r = (a * seed + c) % m
|
||||
let g = (a * r + c) % m
|
||||
let b = (a * g + c) % m
|
||||
|
||||
// 将伪随机数转换为0-255范围内的整数
|
||||
r = Math.floor((r / m) * 256)
|
||||
g = Math.floor((g / m) * 256)
|
||||
b = Math.floor((b / m) * 256)
|
||||
|
||||
// 返回十六进制颜色字符串
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串的第一个字符。
|
||||
* @param {string} str 输入字符串
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { HexColor, isHexColor } from '@renderer/types'
|
||||
|
||||
type ClassValue = string | number | boolean | undefined | null | ClassDictionary | ClassArray
|
||||
|
||||
interface ClassDictionary {
|
||||
@ -43,3 +45,99 @@ export function classNames(...args: ClassValue[]): string {
|
||||
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function checkHexColor(value: string) {
|
||||
if (!isHexColor(value)) {
|
||||
throw new Error(`Invalid hex color string: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
function getRGB(hex: HexColor): [number, number, number] {
|
||||
checkHexColor(hex)
|
||||
// 移除开头的#号
|
||||
const cleanHex = hex.charAt(0) === '#' ? hex.slice(1) : hex
|
||||
|
||||
// 将hex转换为RGB值
|
||||
const r = parseInt(cleanHex.slice(0, 2), 16)
|
||||
const g = parseInt(cleanHex.slice(2, 4), 16)
|
||||
const b = parseInt(cleanHex.slice(4, 6), 16)
|
||||
|
||||
return [r, g, b]
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算相对亮度
|
||||
*
|
||||
* 相对亮度是一个介于0-1之间的值,用于表示颜色的亮度。
|
||||
* 这个计算基于 WCAG 2.0 规范,用于确定颜色的可访问性。
|
||||
*
|
||||
* 计算步骤:
|
||||
* 1. 将RGB值标准化到0-1范围
|
||||
* 2. 对每个颜色通道应用gamma校正
|
||||
* 3. 根据人眼对不同颜色的敏感度进行加权计算
|
||||
*
|
||||
* @param r - 红色通道值 (0-255)
|
||||
* @param g - 绿色通道值 (0-255)
|
||||
* @param b - 蓝色通道值 (0-255)
|
||||
* @returns 相对亮度值 (0-1)
|
||||
*
|
||||
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||
*/
|
||||
function getRelativeLuminance(r: number, g: number, b: number): number {
|
||||
const rs = r / 255
|
||||
const gs = g / 255
|
||||
const bs = b / 255
|
||||
const normalize = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4))
|
||||
return 0.2126 * normalize(rs) + 0.7152 * normalize(gs) + 0.0722 * normalize(bs)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字符生成颜色代码,用于 avatar。
|
||||
* @param {string} char 输入字符
|
||||
* @returns {HexColor} 十六进制颜色字符串
|
||||
*/
|
||||
export function generateColorFromChar(char: string): HexColor {
|
||||
// 使用字符的Unicode值作为随机种子
|
||||
const seed = char.charCodeAt(0)
|
||||
|
||||
// 使用简单的线性同余生成器创建伪随机数
|
||||
const a = 1664525
|
||||
const c = 1013904223
|
||||
const m = Math.pow(2, 32)
|
||||
|
||||
// 生成三个伪随机数作为RGB值
|
||||
let r = (a * seed + c) % m
|
||||
let g = (a * r + c) % m
|
||||
let b = (a * g + c) % m
|
||||
|
||||
// 将伪随机数转换为0-255范围内的整数
|
||||
r = Math.floor((r / m) * 256)
|
||||
g = Math.floor((g / m) * 256)
|
||||
b = Math.floor((b / m) * 256)
|
||||
|
||||
// 返回十六进制颜色字符串
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据背景色获取合适的前景色(文字颜色)
|
||||
*
|
||||
* 该函数基于 WCAG 2.0 规范中的相对亮度计算方法,
|
||||
* 通过计算背景色的相对亮度来决定使用黑色还是白色作为前景色,
|
||||
* 以确保文字的可读性。
|
||||
*
|
||||
* @param {HexColor} backgroundColor - 背景色的十六进制颜色值(例如:'#FFFFFF')
|
||||
* @returns {HexColor} 返回适合的前景色,要么是黑色('#000000')要么是白色('#FFFFFF')
|
||||
*
|
||||
* @see https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color
|
||||
*
|
||||
* @throws {Error} 当输入的颜色值格式不正确时抛出错误
|
||||
*/
|
||||
export function getForegroundColor(backgroundColor: HexColor): HexColor {
|
||||
checkHexColor(backgroundColor)
|
||||
|
||||
const [r, g, b] = getRGB(backgroundColor)
|
||||
const luminance = getRelativeLuminance(r, g, b)
|
||||
|
||||
return luminance > 0.179 ? '#000000' : '#FFFFFF'
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user