Files
AlgerMusicPlayer/src/renderer/utils/linearColor.ts
T

545 lines
14 KiB
TypeScript
Raw Normal View History

2025-01-18 03:25:21 +08:00
import { useDebounceFn } from '@vueuse/core';
import tinycolor from 'tinycolor2';
interface IColor {
backgroundColor: string;
primaryColor: string;
}
2025-01-18 03:25:21 +08:00
interface ITextColors {
primary: string;
active: string;
theme: string;
}
export interface LyricThemeColor {
id: string;
name: string;
light: string;
dark: string;
}
interface LyricSettings {
isTop: boolean;
theme: 'light' | 'dark';
isLock: boolean;
highlightColor?: string;
}
export const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {
2024-09-18 15:11:20 +08:00
try {
const primaryColor = await getImagePrimaryColor(imageSrc);
return {
backgroundColor: generateGradientBackground(primaryColor),
primaryColor
};
2024-09-18 15:11:20 +08:00
} catch (error) {
console.error('error', error);
return {
backgroundColor: '',
primaryColor: ''
};
2024-09-18 15:11:20 +08:00
}
};
export const getImageBackground = async (img: HTMLImageElement): Promise<IColor> => {
2024-09-18 15:11:20 +08:00
try {
const primaryColor = await getImageColor(img);
return {
backgroundColor: generateGradientBackground(primaryColor),
primaryColor
};
2024-09-18 15:11:20 +08:00
} catch (error) {
console.error('error', error);
return {
backgroundColor: '',
primaryColor: ''
};
2024-09-18 15:11:20 +08:00
}
};
const getImageColor = (img: HTMLImageElement): Promise<string> => {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法获取canvas上下文'));
return;
}
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const color = getAverageColor(imageData.data);
resolve(`rgb(${color.join(',')})`);
});
};
const getImagePrimaryColor = (imageSrc: string): Promise<string> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = imageSrc;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法获取canvas上下文'));
return;
}
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const color = getAverageColor(imageData.data);
resolve(`rgb(${color.join(',')})`);
};
img.onerror = () => reject(new Error('图片加载失败'));
});
};
const getAverageColor = (data: Uint8ClampedArray): number[] => {
let r = 0;
let g = 0;
let b = 0;
let count = 0;
for (let i = 0; i < data.length; i += 4) {
r += data[i];
g += data[i + 1];
b += data[i + 2];
count++;
}
return [Math.round(r / count), Math.round(g / count), Math.round(b / count)];
};
const generateGradientBackground = (color: string): string => {
2025-01-18 03:25:21 +08:00
const tc = tinycolor(color);
const hsl = tc.toHsl();
// 增加亮度和暗度的差异
2025-01-18 03:25:21 +08:00
const lightColor = tinycolor({ h: hsl.h, s: hsl.s * 0.8, l: Math.min(hsl.l + 0.2, 0.95) });
const midColor = tinycolor({ h: hsl.h, s: hsl.s, l: hsl.l });
const darkColor = tinycolor({
h: hsl.h,
s: Math.min(hsl.s * 1.2, 1),
l: Math.max(hsl.l - 0.3, 0.05)
});
2025-01-18 03:25:21 +08:00
return `linear-gradient(to bottom, ${lightColor.toRgbString()} 0%, ${midColor.toRgbString()} 50%, ${darkColor.toRgbString()} 100%)`;
};
2025-01-18 03:25:21 +08:00
export const parseGradient = (gradientStr: string) => {
if (!gradientStr) return [];
// 处理非渐变色
if (!gradientStr.startsWith('linear-gradient')) {
const color = tinycolor(gradientStr);
if (color.isValid()) {
const rgb = color.toRgb();
return [{ r: rgb.r, g: rgb.g, b: rgb.b }];
}
2025-01-18 03:25:21 +08:00
return [];
}
2025-01-18 03:25:21 +08:00
// 处理渐变色,支持 rgb、rgba 和十六进制颜色
const colorMatches = gradientStr.match(/(?:(?:rgb|rgba)\([^)]+\)|#[0-9a-fA-F]{3,8})/g) || [];
return colorMatches.map((color) => {
const tc = tinycolor(color);
const rgb = tc.toRgb();
return { r: rgb.r, g: rgb.g, b: rgb.b };
});
};
export const getTextColors = (gradient: string = ''): ITextColors => {
const defaultColors = {
primary: 'rgba(255, 255, 255, 0.54)',
active: '#ffffff',
theme: 'light'
};
if (!gradient) return defaultColors;
const colors = parseGradient(gradient);
if (!colors.length) return defaultColors;
2025-01-18 03:25:21 +08:00
const mainColor = colors.length === 1 ? colors[0] : colors[1] || colors[0];
const tc = tinycolor(mainColor);
const isDark = tc.getBrightness() > 155; // tinycolor 的亮度范围是 0-255
return {
primary: isDark ? 'rgba(0, 0, 0, 0.54)' : 'rgba(255, 255, 255, 0.54)',
active: isDark ? '#000000' : '#ffffff',
theme: isDark ? 'dark' : 'light'
};
};
export const getHoverBackgroundColor = (isDark: boolean): string => {
return isDark ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.08)';
};
2025-01-18 03:25:21 +08:00
export const animateGradient = (() => {
let currentAnimation: number | null = null;
let isAnimating = false;
let lastProgress = 0;
const validateColors = (colors: ReturnType<typeof parseGradient>) => {
return colors.every(
(color) =>
typeof color.r === 'number' &&
typeof color.g === 'number' &&
typeof color.b === 'number' &&
!Number.isNaN(color.r) &&
!Number.isNaN(color.g) &&
!Number.isNaN(color.b)
);
};
2025-01-18 03:25:21 +08:00
const easeInOutCubic = (x: number): number => {
return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
};
2025-01-18 03:25:21 +08:00
const animate = (
oldGradient: string,
newGradient: string,
onUpdate: (gradient: string) => void,
duration = 300
) => {
// 如果新旧渐变色相同,不执行动画
if (oldGradient === newGradient) {
return null;
}
2025-01-18 03:25:21 +08:00
// 如果正在动画中,取消当前动画
if (currentAnimation !== null) {
cancelAnimationFrame(currentAnimation);
currentAnimation = null;
}
2025-01-18 03:25:21 +08:00
// 解析颜色
const startColors = parseGradient(oldGradient);
const endColors = parseGradient(newGradient);
// 验证颜色数组
if (
!startColors.length ||
!endColors.length ||
!validateColors(startColors) ||
!validateColors(endColors)
) {
console.warn('Invalid color values detected');
onUpdate(newGradient); // 直接更新到目标颜色
return null;
}
2025-01-18 03:25:21 +08:00
// 如果颜色数量不匹配,直接更新到目标颜色
if (startColors.length !== endColors.length) {
onUpdate(newGradient);
return null;
}
2025-01-18 03:25:21 +08:00
isAnimating = true;
const startTime = performance.now();
const animateFrame = (currentTime: number) => {
if (!isAnimating) return null;
const elapsed = currentTime - startTime;
const rawProgress = Math.min(elapsed / duration, 1);
// 使用缓动函数使动画更平滑
const progress = easeInOutCubic(rawProgress);
try {
// 使用上一帧的进度来平滑过渡
const effectiveProgress = lastProgress + (progress - lastProgress) * 0.6;
lastProgress = effectiveProgress;
const currentColors = startColors.map((startColor, i) => {
const start = tinycolor(startColor);
const end = tinycolor(endColors[i]);
return tinycolor.mix(start, end, effectiveProgress * 100);
});
const gradientString = createGradientString(
currentColors.map((c) => {
const rgb = c.toRgb();
return { r: rgb.r, g: rgb.g, b: rgb.b };
})
);
onUpdate(gradientString);
if (rawProgress < 1) {
currentAnimation = requestAnimationFrame(animateFrame);
return currentAnimation;
}
// 确保最终颜色正确
onUpdate(newGradient);
isAnimating = false;
currentAnimation = null;
lastProgress = 0;
return null;
} catch (error) {
console.error('Animation error:', error);
onUpdate(newGradient);
isAnimating = false;
currentAnimation = null;
lastProgress = 0;
return null;
}
};
currentAnimation = requestAnimationFrame(animateFrame);
return currentAnimation;
};
2025-01-18 03:25:21 +08:00
// 使用更短的防抖时间
return useDebounceFn(animate, 50);
})();
export const createGradientString = (
colors: { r: number; g: number; b: number }[],
percentages = [0, 50, 100]
) => {
return `linear-gradient(to bottom, ${colors
.map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`)
.join(', ')})`;
};
// ===== 歌词主题色相关工具函数 =====
/**
* 预设歌词主题色配置
* 注意:name 字段将通过国际化系统动态获取,这里的值仅作为后备
*/
const PRESET_LYRIC_COLORS: LyricThemeColor[] = [
{
id: 'spotify-green',
name: 'Spotify Green', // 后备名称,实际使用时会被国际化替换
light: '#1db954',
dark: '#1ed760'
},
{
id: 'apple-blue',
name: 'Apple Blue',
light: '#007aff',
dark: '#0a84ff'
},
{
id: 'youtube-red',
name: 'YouTube Red',
light: '#ff0000',
dark: '#ff4444'
},
{
id: 'orange',
name: 'Vibrant Orange',
light: '#ff6b35',
dark: '#ff8c42'
},
{
id: 'purple',
name: 'Mystic Purple',
light: '#8b5cf6',
dark: '#a78bfa'
},
{
id: 'pink',
name: 'Cherry Pink',
light: '#ec4899',
dark: '#f472b6'
}
];
/**
* 验证颜色是否有效
*/
export const validateColor = (color: string): boolean => {
if (!color || typeof color !== 'string') return false;
const tc = tinycolor(color);
return tc.isValid() && tc.getAlpha() > 0;
};
/**
* 检查颜色对比度是否符合可读性标准
*/
export const validateColorContrast = (color: string, theme: 'light' | 'dark'): boolean => {
if (!validateColor(color)) return false;
const backgroundColor = theme === 'dark' ? '#000000' : '#ffffff';
const contrast = tinycolor.readability(color, backgroundColor);
return contrast >= 4.5; // WCAG AA 标准
};
/**
* 为特定主题优化颜色
*/
export const optimizeColorForTheme = (color: string, theme: 'light' | 'dark'): string => {
if (!validateColor(color)) {
return getDefaultHighlightColor(theme);
}
const tc = tinycolor(color);
const hsl = tc.toHsl();
if (theme === 'dark') {
// 暗色主题:增加亮度和饱和度
const optimized = tinycolor({
h: hsl.h,
s: Math.min(hsl.s * 1.1, 1),
l: Math.max(hsl.l, 0.4) // 确保最小亮度
});
// 检查对比度,如果不够则进一步调整
if (!validateColorContrast(optimized.toHexString(), theme)) {
return tinycolor({
h: hsl.h,
s: Math.min(hsl.s * 1.2, 1),
l: Math.max(hsl.l * 1.3, 0.5)
}).toHexString();
}
return optimized.toHexString();
} else {
// 亮色主题:适当降低亮度
const optimized = tinycolor({
h: hsl.h,
s: Math.min(hsl.s * 1.05, 1),
l: Math.min(hsl.l, 0.6) // 确保最大亮度
});
// 检查对比度
if (!validateColorContrast(optimized.toHexString(), theme)) {
return tinycolor({
h: hsl.h,
s: Math.min(hsl.s * 1.1, 1),
l: Math.min(hsl.l * 0.8, 0.5)
}).toHexString();
}
return optimized.toHexString();
}
};
/**
* 获取默认高亮颜色
*/
export const getDefaultHighlightColor = (theme?: 'light' | 'dark'): string => {
const defaultColor = PRESET_LYRIC_COLORS[0]; // Spotify 绿
if (!theme) return defaultColor.light;
return theme === 'dark' ? defaultColor.dark : defaultColor.light;
};
/**
* 获取预设主题色列表
*/
export const getLyricThemeColors = (): LyricThemeColor[] => {
return [...PRESET_LYRIC_COLORS];
};
/**
* 根据主题获取预设颜色的实际值
*/
export const getPresetColorValue = (colorId: string, theme: 'light' | 'dark'): string => {
const color = PRESET_LYRIC_COLORS.find(c => c.id === colorId);
if (!color) return getDefaultHighlightColor(theme);
return theme === 'dark' ? color.dark : color.light;
};
/**
* 安全加载歌词设置
*/
const safeLoadLyricSettings = (): LyricSettings => {
try {
const stored = localStorage.getItem('lyricData');
if (stored) {
const parsed = JSON.parse(stored) as LyricSettings;
// 验证 highlightColor 字段
if (parsed.highlightColor && !validateColor(parsed.highlightColor)) {
console.warn('Invalid stored highlight color, removing it');
delete parsed.highlightColor;
}
return parsed;
}
} catch (error) {
console.error('Failed to load lyric settings:', error);
}
// 返回默认设置
return {
isTop: false,
theme: 'dark',
isLock: false
};
};
/**
* 安全保存歌词设置
*/
const safeSaveLyricSettings = (settings: LyricSettings): void => {
try {
localStorage.setItem('lyricData', JSON.stringify(settings));
} catch (error) {
console.error('Failed to save lyric settings:', error);
}
};
/**
* 保存歌词主题色
*/
export const saveLyricThemeColor = (color: string): void => {
if (!validateColor(color)) {
console.warn('Attempted to save invalid color:', color);
return;
}
const settings = safeLoadLyricSettings();
settings.highlightColor = color;
safeSaveLyricSettings(settings);
};
/**
* 加载歌词主题色
*/
export const loadLyricThemeColor = (): string => {
const settings = safeLoadLyricSettings();
if (settings.highlightColor && validateColor(settings.highlightColor)) {
return settings.highlightColor;
}
// 如果没有保存的颜色或颜色无效,返回默认颜色
return getDefaultHighlightColor(settings.theme);
};
/**
* 重置歌词主题色到默认值
*/
export const resetLyricThemeColor = (): void => {
const settings = safeLoadLyricSettings();
delete settings.highlightColor;
safeSaveLyricSettings(settings);
};
/**
* 获取当前有效的歌词主题色
*/
export const getCurrentLyricThemeColor = (theme: 'light' | 'dark'): string => {
const savedColor = loadLyricThemeColor();
if (savedColor && validateColor(savedColor)) {
return optimizeColorForTheme(savedColor, theme);
}
return getDefaultHighlightColor(theme);
};