Spaces:
Sleeping
Sleeping
File size: 9,574 Bytes
68f7925 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
import type { ThemeExtractionResult } from '@/schema/theme-extraction';
export interface ThemeVariables {
extractedVars: string;
fallbackVars: string;
phase5Vars: string;
legacyMappingVars: string;
}
export class ThemeVariableGenerator {
/**
* 背景色に対して適切なコントラスト比を持つテキスト色を生成(WCAG AA準拠)
*/
private generateAdaptiveTextColor(bgColor: string, theme: ThemeExtractionResult): string {
const luminance = this.getLuminance(bgColor);
// primary_text_colorとinverse_text_colorのコントラスト比を計算
const contrastWithPrimary = this.getContrastRatio(bgColor, theme.colors.primary_text_color);
const contrastWithInverse = this.getContrastRatio(bgColor, theme.colors.inverse_text_color);
const WCAG_AA_THRESHOLD = 4.5; // WCAG AA基準
console.log(
`[ThemeVariables] テキスト色判定: 背景=${bgColor} (明度: ${luminance.toFixed(2)}) | ` +
`primary_text_color=${theme.colors.primary_text_color} (コントラスト: ${contrastWithPrimary.toFixed(2)}) | ` +
`inverse_text_color=${theme.colors.inverse_text_color} (コントラスト: ${contrastWithInverse.toFixed(2)})`,
);
// 両方がWCAG AA基準を満たす場合、より高いコントラストを選択
if (contrastWithPrimary >= WCAG_AA_THRESHOLD && contrastWithInverse >= WCAG_AA_THRESHOLD) {
const selected = contrastWithPrimary > contrastWithInverse ? theme.colors.primary_text_color : theme.colors.inverse_text_color;
console.log(
`[ThemeVariables] ✅ 両方AA基準クリア → より高いコントラストを選択: ${selected} (${contrastWithPrimary > contrastWithInverse ? contrastWithPrimary.toFixed(2) : contrastWithInverse.toFixed(2)})`,
);
return selected;
}
// primary_text_colorのみがWCAG AA基準を満たす場合
if (contrastWithPrimary >= WCAG_AA_THRESHOLD) {
console.log(
`[ThemeVariables] ✅ primary_text_colorのみAA基準クリア → primary_text_color使用 (コントラスト: ${contrastWithPrimary.toFixed(2)})`,
);
return theme.colors.primary_text_color;
}
// inverse_text_colorのみがWCAG AA基準を満たす場合
if (contrastWithInverse >= WCAG_AA_THRESHOLD) {
console.log(
`[ThemeVariables] ✅ inverse_text_colorのみAA基準クリア → inverse_text_color使用 (コントラスト: ${contrastWithInverse.toFixed(2)})`,
);
return theme.colors.inverse_text_color;
}
// どちらもWCAG AA基準を満たさない場合、フォールバック(黒または白)
const fallbackColor = luminance > 0.5 ? '#000000' : '#FFFFFF';
console.warn(`[ThemeVariables] ⚠️ どちらもAA基準未達 → フォールバック使用: ${fallbackColor} (背景明度: ${luminance.toFixed(2)})`);
return fallbackColor;
}
/**
* 相対輝度計算(WCAG 2.1準拠)
*/
private getLuminance(hex: string): number {
try {
const rgb = this.hexToRgb(hex);
if (!rgb) return 0;
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map((c) => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
} catch (error) {
console.warn('[ThemeVariables] 輝度計算エラー:', error);
return 0;
}
}
/**
* HEXをRGBに変換
*/
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
/**
* 2色間のコントラスト比を計算(WCAG 2.1準拠)
* @returns コントラスト比(1〜21の範囲)
*/
private getContrastRatio(color1: string, color2: string): number {
try {
const lum1 = this.getLuminance(color1);
const lum2 = this.getLuminance(color2);
const lighter = Math.max(lum1, lum2);
const darker = Math.min(lum1, lum2);
return (lighter + 0.05) / (darker + 0.05);
} catch (error) {
console.warn('[ThemeVariables] コントラスト比計算エラー:', error);
return 1; // エラー時は最低値を返す
}
}
generateExtractedVariables(theme: ThemeExtractionResult): string {
return `
/* === 抽出されたテーマ変数 === */
--extracted-primary-color: ${theme.colors.primary_color};
--extracted-secondary-color: ${theme.colors.secondary_color};
--extracted-accent-color: ${theme.colors.accent_color};
--extracted-success-semantic-color: ${theme.colors.success_semantic_color};
--extracted-warning-semantic-color: ${theme.colors.warning_semantic_color};
--extracted-error-semantic-color: ${theme.colors.error_semantic_color};
--extracted-info-semantic-color: ${theme.colors.info_semantic_color};
--extracted-primary-background-color: ${theme.colors.primary_background_color};
--extracted-secondary-background-color: ${theme.colors.secondary_background_color};
--extracted-tertiary-background-color: ${theme.colors.tertiary_background_color};
--extracted-overlay-background-color: ${theme.colors.overlay_background_color};
--extracted-primary-text-color: ${theme.colors.primary_text_color};
--extracted-secondary-text-color: ${theme.colors.secondary_text_color};
--extracted-disabled-text-color: ${theme.colors.disabled_text_color};
--extracted-inverse-text-color: ${theme.colors.inverse_text_color};
/* カテゴリータグ用の適応的テキスト色 */
--extracted-category-tag-text: ${this.generateAdaptiveTextColor(theme.colors.accent_color, theme)};`;
}
generateFallbackVariables(theme: ThemeExtractionResult): string {
// tertiary_background用の適応的テキスト色を生成
const tertiaryTextColor = this.generateAdaptiveTextColor(theme.colors.tertiary_background_color, theme);
return `
/* === フォールバック付きテーマ変数 === */
--theme-primary-color: var(--extracted-primary-color, #5a6c7d);
--theme-secondary-color: var(--extracted-secondary-color, #34495e);
--theme-accent-color: var(--extracted-accent-color, #f59e0b);
--theme-category-tag-bg: var(--extracted-accent-color, #f59e0b);
--theme-category-tag-text: var(--extracted-category-tag-text, white); /* フォールバック: 白(#f59e0b背景用) */
--theme-success-color: var(--extracted-success-semantic-color, #28a745);
--theme-warning-color: var(--extracted-warning-semantic-color, #f59e0b);
--theme-error-color: var(--extracted-error-semantic-color, #dc3545);
--theme-info-color: var(--extracted-info-semantic-color, #17a2b8);
--theme-primary-bg: var(--extracted-primary-background-color, white);
--theme-secondary-bg: var(--extracted-secondary-background-color, #f8f9fa);
--theme-tertiary-bg: var(--extracted-tertiary-background-color, #222222);
--theme-overlay-bg: var(--extracted-overlay-background-color, rgba(0,0,0,0.5));
--theme-primary-text: var(--extracted-primary-text-color, #222222);
--theme-secondary-text: var(--extracted-secondary-text-color, #555555);
--theme-disabled-text: var(--extracted-disabled-text-color, #999999);
--theme-inverse-text: var(--extracted-inverse-text-color, white);
--theme-tertiary-text: ${tertiaryTextColor}; /* 適応的テキスト色(明度判定済み) */
--theme-primary-font: var(--extracted-primary-font,
"YuGothic", "Yu Gothic Medium", "Yu Gothic", "Hiragino Sans",
"Hiragino Kaku Gothic ProN", -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Meiryo", sans-serif);`;
}
generatePhase5Variables(): string {
return `
/* === Phase 5: 基本CSS変数化 === */
--primary-color: var(--theme-primary-color);
--text-primary: var(--theme-primary-text);
--text-secondary: var(--theme-secondary-text);
--background-primary: var(--theme-primary-bg);
--background-secondary: var(--theme-secondary-bg);
--background-tertiary: var(--theme-tertiary-bg);
--border-primary: var(--theme-secondary-bg);
--border-secondary: var(--theme-secondary-bg);
--accent-warning: var(--theme-warning-color);`;
}
generateLegacyMappingVariables(): string {
return `
/* === 既存色の変数化(後方互換性確保) === */
--legacy-primary-text: var(--theme-primary-text);
--legacy-accent: var(--theme-primary-color);
--legacy-border: var(--theme-secondary-text);
--legacy-light-bg: var(--theme-secondary-bg);
--legacy-secondary-text: var(--theme-secondary-text);
--section-white-bg: var(--theme-primary-bg);
--section-gray-bg: var(--theme-secondary-bg);
--section-dark-bg: var(--theme-tertiary-bg);`;
}
generateCompleteThemeVariables(theme: ThemeExtractionResult): ThemeVariables {
const extractedVars = this.generateExtractedVariables(theme);
const fallbackVars = this.generateFallbackVariables(theme);
const phase5Vars = this.generatePhase5Variables();
const legacyMappingVars = this.generateLegacyMappingVariables();
return {
extractedVars,
fallbackVars,
phase5Vars,
legacyMappingVars,
};
}
generateThemeCSS(theme: ThemeExtractionResult): string {
const variables = this.generateCompleteThemeVariables(theme);
return `:root {${variables.extractedVars}
${variables.fallbackVars}
${variables.phase5Vars}
${variables.legacyMappingVars}
}`;
}
}
export const themeVariableGenerator = new ThemeVariableGenerator();
|