// Source of truth for the palette. Run npm run design after changing values. // See /Design.md for semantic pairings, ratios and component rules. export const THEME = Object.freeze({ background: '#A8BECF', board: '#F2EEE5', boardGrid: '#89867D', track: '#435F78', trackHalo: '#F2EEE5', surface: '#F7F6F2', textPrimary: '#273B50', textSecondary: '#314A5F', primary: '#6652B8', onPrimary: '#FFFFFF', secondary: '#E4EAF0', utility: '#52697E', onUtility: '#FFFFFF', selection: '#DFD9F3', onSelection: '#3F327A', disabled: '#CBD4DD', onDisabled: '#526579', input: '#FFFFFF', reward: '#B9760C', warning: '#A44825', warningSurface: '#F9E3D2', scrim: '#273B507A', focus: '#3F327A', // Approximate center pixels of the accepted, unchanged generated token art. tileBlue: '#2392FD', tilePink: '#FC3E80', tileYellow: '#FDBC28', tilePurple: '#874FFD', arrowLight: '#FFFFFF', arrowDark: '#273B50', }); export const SURFACES = Object.freeze({ surface: 'surface', primary: 'primary', secondary: 'secondary', utility: 'utility', warning: 'warningSurface', reward: 'reward', input: 'input', selection: 'selection', disabled: 'disabled', tileBlue: 'tileBlue', tilePink: 'tilePink', tileYellow: 'tileYellow', tilePurple: 'tilePurple', }); export const TYPOGRAPHY = Object.freeze({level:28,moves:32,dialogTitle:24,body:14,label:12,caption:11}); export const MOTION = Object.freeze({pressMs:150,modalMs:420,starStartMs:300,starGapMs:190,collisionMs:800,rotationMs:240,exitMsPerUnit:120,exitRampMs:80,exitMinTravelMs:140,exitMaxTravelMs:800,exitEffectMs:180,exitLongBaseMs:550,exitLongCellMs:85,exitLongMaxMs:1700,exitLongStartCells:2,exitLongFullCells:5}); export const RADII = Object.freeze({control:14,dialog:24,small:8}); export function luminance(hex){return hex.slice(1,7).match(/../g).map(v=>parseInt(v,16)/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4).reduce((a,v,i)=>a+v*[.2126,.7152,.0722][i],0);} export function contrast(a,b){const x=luminance(a),y=luminance(b);return (Math.max(x,y)+.05)/(Math.min(x,y)+.05);} export const CONTRAST_PAIRS = [ ['正文 / 背景','textPrimary','background',4.5], ['辅助文字 / 背景','textSecondary','background',4.5], ['正文 / 弹窗','textPrimary','surface',4.5], ['辅助文字 / 弹窗','textSecondary','surface',4.5], ['主按钮文字','onPrimary','primary',4.5], ['工具图标','onUtility','utility',3], ['次按钮文字','textPrimary','secondary',4.5], ['选中标签','onSelection','selection',4.5], ['阻挡提示文字','warning','warningSurface',4.5], ['分格 / 棋盘','boardGrid','board',3], ['轨道 / 棋盘','track','board',3], ['轨道 / 浅色外缘','track','trackHalo',3], ['方向箭头 / 蓝棋子','arrowLight','tileBlue',3], ['方向箭头 / 粉棋子','arrowLight','tilePink',3], ['方向箭头 / 黄棋子','arrowDark','tileYellow',3], ['方向箭头 / 紫棋子','arrowLight','tilePurple',3], ]; // V2: global pigment library. A level uses only its curated subset. export const PIGMENTS = Object.freeze([ {name:'晴空蓝', color:THEME.tileBlue}, {name:'莓果粉', color:THEME.tilePink}, {name:'日光黄', color:THEME.tileYellow}, {name:'葡萄紫', color:THEME.tilePurple}, {name:'湖水青', color:'#008B83'}, {name:'珊瑚橙', color:'#EF704F'}, {name:'嫩芽绿', color:'#91C95B'}, {name:'鸢尾蓝', color:'#5267CF'}, {name:'玫瑰红', color:'#C83F66'}, {name:'蜜桃杏', color:'#F5AA83'}, {name:'冰川青', color:'#58C9D5'}, {name:'兰花紫', color:'#AE64C7'}, ].map(p=>Object.freeze({...p, arrow:contrast(THEME.arrowLight,p.color)>=3?THEME.arrowLight:THEME.arrowDark}))); export const LEVEL_THEMES = Object.freeze([ {id:'lagoon',name:'薄荷浅湾',label:'轻松',range:'0–29',background:'#C5E4DF',ink:'#273B50',palette:[0,4,2],description:'冷润的浅湾,配蓝、青与一点日光黄。'}, {id:'apricot',name:'杏色午后',label:'进阶',range:'30–49',background:'#F1D7B8',ink:'#273B50',palette:[5,7,4,9],description:'温暖杏底,以珊瑚、鸢尾蓝和湖水青平衡。'}, {id:'lavender',name:'鸢尾花园',label:'烧脑',range:'50–69',background:'#D8D0EB',ink:'#273B50',palette:[3,6,8,10],description:'柔紫空间,用嫩芽绿与冰川青拉开层次。'}, {id:'rose',name:'玫瑰暮色',label:'挑战',range:'70–100',background:'#E7C3CE',ink:'#273B50',palette:[8,7,2,11],description:'低饱和玫瑰底,深色棋子与日光黄形成节奏。'}, {id:'midnight',name:'午夜极光',label:'每周挑战',range:'特别主题',background:'#293956',ink:'#F7F6F2',palette:[10,11,2,1],description:'深靛蓝舞台,冰川青、兰花紫和明亮暖色。'}, ].map(t=>Object.freeze({...t,palette:Object.freeze(t.palette)}))); export function themeForScore(score){return LEVEL_THEMES[score<30?0:score<50?1:score<70?2:3];} export function pigmentIndex(color,theme){return theme?theme.palette[color%theme.palette.length]:color;} // Keep each world's anchor colors, then add distinct hues before close relatives. export function themeWithColors(theme,count=7){ const limit=[4,7,9,12].includes(count)?count:7; const order=[...new Set([...theme.palette,0,1,2,3,4,5,6,7,8,9,10,11])]; return {...theme,palette:order.slice(0,limit)}; }