feat: M5 背景图上传/压缩+智能取色(FR-A8/A9,smart-color 下沉 shared,canvas 采样→analyzeImage 推导强调色/模糊/表面不透明尊重手动项,持久化+重开恢复)

This commit is contained in:
hunter-dev
2026-08-26 09:20:45 +08:00
parent 545bc38370
commit 81b410e281
11 changed files with 222 additions and 96 deletions
+1
View File
@@ -5,3 +5,4 @@ export * from './types/doc.js';
export * from './types/search.js';
export * from './types/graph.js';
export * from './constants.js';
export * from './utils/smart-color.js';
+2 -2
View File
@@ -58,8 +58,8 @@ export interface IpcMap {
[IPC.winClose]: { args: void; result: void };
[IPC.copyMd]: { args: { path: string }; result: string };
[IPC.printPdf]: { args: void; result: void };
[IPC.appearanceLoad]: { args: void; result: { themeId: string; params: Record<string, unknown>; effect: string } | null };
[IPC.appearanceSave]: { args: { themeId: string; params: Record<string, unknown>; effect: string }; result: void };
[IPC.appearanceLoad]: { args: void; result: { themeId: string; params: Record<string, unknown>; effect: string; bgImage: string | null } | null };
[IPC.appearanceSave]: { args: { themeId: string; params: Record<string, unknown>; effect: string; bgImage: string | null }; result: void };
[IPC.keymapLoad]: { args: void; result: { id: string; combo: string }[] | null };
[IPC.keymapSave]: { args: { id: string; combo: string }[]; result: void };
}
+75
View File
@@ -0,0 +1,75 @@
// 智能取色:分析背景图亮度/主色 → 推荐深浅模式与强调色(FR-A9)
// 纯函数,输入采样后的像素数据,输出推荐参数
export interface ImageSample {
/** 采样像素的 [r,g,b] 列表(已降采样) */
pixels: [number, number, number][];
}
export interface SmartColorResult {
mode: 'dark' | 'light';
accentHue: number; // 0-360,约束在冷色/紫红区间
accent: string; // hex 强调色
suggestedBgOpacity: number; // 0-1
suggestedBlur: number; // px
suggestedSurfaceOpacity: number; // 0-1
}
function luminance(r: number, g: number, b: number): number {
const f = (c: number) => { c /= 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); };
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
}
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0; const l = (max + min) / 2; const d = max - min; const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
if (d !== 0) {
switch (max) {
case r: h = ((g - b) / d) % 6; break;
case g: h = (b - r) / d + 2; break;
default: h = (r - g) / d + 4;
}
h *= 60; if (h < 0) h += 360;
}
return [h, s, l];
}
function hslToHex(h: number, s: number, l: number): string {
const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) [r, g, b] = [c, x, 0]; else if (h < 120) [r, g, b] = [x, c, 0];
else if (h < 180) [r, g, b] = [0, c, x]; else if (h < 240) [r, g, b] = [0, x, c];
else if (h < 300) [r, g, b] = [x, 0, c]; else [r, g, b] = [c, 0, x];
const to = (v: number) => Math.round((v + m) * 255).toString(16).padStart(2, '0');
return '#' + to(r) + to(g) + to(b);
}
function constrainHue(h: number): number {
if (h >= 180 && h <= 330) return h;
if (h < 180) return h < 90 ? 200 : 210;
return 290;
}
export function analyzeImage(sample: ImageSample): SmartColorResult {
const { pixels } = sample;
if (pixels.length === 0) {
return { mode: 'dark', accentHue: 210, accent: '#4a9eff', suggestedBgOpacity: 0.6, suggestedBlur: 20, suggestedSurfaceOpacity: 0.5 };
}
const avgLum = pixels.reduce((s, p) => s + luminance(p[0], p[1], p[2]), 0) / pixels.length;
const mode: 'dark' | 'light' = avgLum < 0.5 ? 'dark' : 'light';
const buckets = new Array(12).fill(0);
let dominantHue = 210;
for (const p of pixels) {
const [h, s] = rgbToHsl(p[0], p[1], p[2]);
if (s > 0.15) buckets[Math.floor(h / 30) % 12]++;
}
let max = 0;
for (let i = 0; i < 12; i++) if (buckets[i] > max) { max = buckets[i]; dominantHue = i * 30 + 15; }
const accentHue = constrainHue(dominantHue);
const accent = hslToHex(accentHue, 0.7, mode === 'dark' ? 0.6 : 0.45);
const suggestedBgOpacity = Math.round(Math.max(0.3, Math.min(0.8, 0.5 + (0.5 - avgLum) * 0.4)) * 100) / 100;
const suggestedBlur = avgLum < 0.4 ? 24 : avgLum > 0.7 ? 12 : 18;
const suggestedSurfaceOpacity = mode === 'dark' ? 0.5 : 0.7;
return { mode, accentHue, accent, suggestedBgOpacity, suggestedBlur, suggestedSurfaceOpacity };
}