mono-tokens.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /* ═══════════════════════════════════════════════════════════════
  2. MONO TOKENS — 风格的唯一正本
  3. 所有 mono 图表(Chart.js / ECharts / 手写 SVG)共享这一份。
  4. 任何文件里出现与本文件冲突的颜色、字体、动画参数,以本文件为准。
  5. 用法:<script src="mono-tokens.js"></script>,全部挂在 window.MONO 上;
  6. 也可以直接把本文件内容内联进单文件 HTML(开源分发时推荐内联)。
  7. ═══════════════════════════════════════════════════════════════ */
  8. (function (global) {
  9. 'use strict';
  10. /* ── 1 · 色板 ──────────────────────────────────────────────
  11. 纸灰底 + 炭黑墨。没有彩色。明度即数据:最重要 = 最黑。 */
  12. const INK = '#1C1C1A'; // 墨:主数据、标题、强调
  13. const PAPER = '#F0EFEB'; // 纸:页面底色 = 浅卡底色(卡片无边框,靠留白分卡)
  14. const MUTED = '#8F8E88'; // 次级文字、副标题
  15. const FAINT = '#C6C5BF'; // 来源行、辅助刻度
  16. const GRID = '#DEDDD6'; // 网格线、发丝线
  17. // 7 级灰阶 ladder:多系列时按重要性从黑到浅分配
  18. const L = ['#1C1C1A', '#4A4944', '#6A6963', '#8F8E88', '#B0AFA9', '#C6C5BF', '#D8D7D1'];
  19. // 5 级简版(waffle 等少系列场景)
  20. const LAD = ['#1C1C1A', '#4A4944', '#8F8E88', '#B0AFA9', '#D8D7D1'];
  21. // 暗卡(dark card)专用:底 #1C1C1A,其上的"墨"反转为纸色
  22. const DARK = {
  23. bg: '#1C1C1A',
  24. ink: '#F0EFEB', // 暗卡上的主数据色
  25. muted: '#8F8E88',
  26. faint: '#55554F', // 暗卡来源行
  27. grid: '#2E2D29', // 暗卡网格 / 发丝线
  28. gridSoft: '#2A2925',
  29. ladder: ['#F0EFEB', '#DCDAD2', '#C9C7BD', '#B3B0A4', '#8F8E88', '#6A6963', '#4A4944'],
  30. };
  31. /* ── 2 · 字体 ────────────────────────────────────────────── */
  32. const FONT = {
  33. family: 'Inter',
  34. // Google Fonts 引入行(放 <head>):
  35. link: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap',
  36. title: { size: 16.5, weight: 700, spacing: '-.02em' }, // 卡内 h2
  37. titleBig: { size: 19, weight: 700, spacing: '-.02em' }, // 独立大图 h2
  38. sub: { size: 11.5, weight: 400 }, // 副标题(图例说明写在这里)
  39. src: { size: 9.5, weight: 500, spacing: '.08em' }, // 来源行,全大写
  40. value: { weight: 800 }, // 图内数值一律 800
  41. axis: { size: 9.5, weight: 600 }, // 轴标签
  42. // SVG 内最小字号下限:半宽卡 6.5px,通栏/大图 5.5px。低于下限改 hover 出。
  43. minHalf: 6.5, minWide: 5.5,
  44. };
  45. /* ── 3 · 形状 ────────────────────────────────────────────── */
  46. const SHAPE = {
  47. cardRadius: 24, // 卡片圆角
  48. cardPad: '28px 28px 20px',
  49. barRadius: 99, // 柱端胶囊圆角(竖柱只圆上端,横柱只圆外端)
  50. tooltipRadius: 12,
  51. };
  52. /* ── 4 · 动画性格 ────────────────────────────────────────
  53. 快进快停,quarticOut / cubicOut,不弹跳(elasticOut 只给波浪入场)。 */
  54. const MOTION = {
  55. enter: 900, // 常规入场 ms
  56. enterSlow: 1200, // 大元素 / 关系图
  57. easing: 'quarticOut',
  58. staggerDot: 12, // 点阵逐个延迟 ms(8–15 区间)
  59. staggerBar: 100, // 条形逐根延迟 ms(80–130 区间)
  60. // CSS 动画类(手写 SVG 用):pop 缩放入场 / fade 淡入 / draw 描线
  61. css: `
  62. .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
  63. @keyframes pop{from{transform:scale(0)}to{transform:none}}
  64. .fade{animation:fade .9s ease both}
  65. @keyframes fade{from{opacity:0}}
  66. .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
  67. @keyframes draw{to{stroke-dashoffset:0}}
  68. @media (prefers-reduced-motion:reduce){
  69. .pop,.fade{animation:none}
  70. .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
  71. }`,
  72. };
  73. /* ── 5 · Tooltip ─────────────────────────────────────────
  74. 浅卡用黑底纸字,暗卡用纸底黑字。ECharts 直接展开这两个对象。 */
  75. const tipLight = { backgroundColor: INK, borderWidth: 0, padding: [10, 14],
  76. textStyle: { color: PAPER, fontFamily: 'Inter', fontSize: 12 } };
  77. const tipDark = { backgroundColor: PAPER, borderWidth: 0, padding: [10, 14],
  78. textStyle: { color: INK, fontFamily: 'Inter', fontSize: 12 } };
  79. /* ── 6 · 确定性伪随机 ────────────────────────────────────
  80. 演示数据一律用它,不用 Math.random()——刷新必须长一样,
  81. 否则截图 / 录屏 / 回归对比全部失效。 */
  82. const rnd = (i, k) => Math.abs(((i * 73856093) ^ (k * 19349663)) % 1000) / 1000;
  83. /* ── 7 · 几何 ────────────────────────────────────────────── */
  84. const D2R = Math.PI / 180;
  85. const pol = (cx, cy, r, deg) => [cx + r * Math.cos(deg * D2R), cy + r * Math.sin(deg * D2R)];
  86. // 环形扇区 path(cornerRadius 自己在调用处用 ECharts sector 或手动倒角)
  87. const sect = (cx, cy, r0, r1, a0, a1) => {
  88. const big = a1 - a0 > 180 ? 1 : 0;
  89. const [xa, ya] = pol(cx, cy, r1, a0), [xb, yb] = pol(cx, cy, r1, a1);
  90. const [xc, yc] = pol(cx, cy, r0, a1), [xd, yd] = pol(cx, cy, r0, a0);
  91. return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
  92. };
  93. // 手绘感圆(editorial 系气泡用):圆周叠两个慢波 + 噪声,seed 定形
  94. const blob = (x, y, r, seed) => {
  95. const n = Math.max(14, Math.round(r * 1.6)), pts = [];
  96. for (let t = 0; t < n; t++) {
  97. const a = t / n * Math.PI * 2;
  98. const w = 1 + .055 * Math.sin(a * 2 + seed * 7) + .04 * Math.sin(a * 3 + seed * 13)
  99. + (rnd(seed + t, 3) - .5) * .03;
  100. pts.push([x + Math.cos(a) * r * w, y + Math.sin(a) * r * w]);
  101. }
  102. let d = `M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
  103. for (let t = 0; t < n; t++) {
  104. const p = pts[t], q = pts[(t + 1) % n];
  105. d += ` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
  106. }
  107. return d + ' Z';
  108. };
  109. /* ── 8 · SVG 快捷 ────────────────────────────────────────── */
  110. const NS = 'http://www.w3.org/2000/svg';
  111. const el = (p, t, a) => { const n = document.createElementNS(NS, t);
  112. for (const k in a) n.setAttribute(k, a[k]); p.appendChild(n); return n; };
  113. const txt = (p, a, s) => { const n = el(p, 'text', a); n.textContent = s; return n; };
  114. const tip = (n, s) => { const t = document.createElementNS(NS, 'title');
  115. t.textContent = s; n.appendChild(t); };
  116. /* ── 9 · 统一 reveal:滚入视野才播,点击重播 ──────────────
  117. 带 timer 登记(keep),重播前清干净,防动画叠加。 */
  118. const timers = {};
  119. const keep = (id, t) => { (timers[id] = timers[id] || []).push(t); };
  120. const obsReveal = (id, fn) => {
  121. const n = document.getElementById(id);
  122. const go = () => {
  123. (timers[id] || []).forEach(clearInterval); timers[id] = [];
  124. if (n.tagName === 'svg' || n.tagName === 'SVG') n.innerHTML = '';
  125. fn(n);
  126. };
  127. const io = new IntersectionObserver(es => {
  128. if (es[0].isIntersecting) { go(); io.disconnect(); }
  129. }, { threshold: .3 });
  130. io.observe(n);
  131. n.style.cursor = 'pointer';
  132. n.addEventListener('click', go);
  133. };
  134. // ECharts 版 reveal
  135. const eReveal = (id, opt) => obsReveal(id, elDom => {
  136. const g = echarts.getInstanceByDom(elDom) || echarts.init(elDom);
  137. g.clear(); g.setOption(opt);
  138. });
  139. /* ── 10 · 卡片骨架(生成新图时照抄这个结构) ──────────────
  140. <div class="card [dark] [wide]">
  141. <h2>结论式标题</h2>
  142. <div class="sub">副标题 · 图例说明 · 时间范围</div>
  143. <div class="ch" id="xx"></div> 或 <svg id="xx" viewBox="0 0 400 320">
  144. <div class="src">图型名 · 系列名 · 数据来源(全大写)</div>
  145. </div> */
  146. const CARD_CSS = `
  147. :root{--bg:${PAPER};--dark:${DARK.bg};--ink:${INK};--muted:${MUTED};--faint:${FAINT};--grid:${GRID}}
  148. *{margin:0;padding:0;box-sizing:border-box}
  149. body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
  150. .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
  151. .card{background:var(--bg);border-radius:${SHAPE.cardRadius}px;padding:${SHAPE.cardPad}}
  152. .card.dark{background:var(--dark);color:${PAPER}}
  153. .card.dark .sub{color:${MUTED}}
  154. .card.dark .src{color:${DARK.faint}}
  155. .card.wide{grid-column:1/-1}
  156. h2{font-weight:${FONT.title.weight};font-size:${FONT.title.size}px;letter-spacing:${FONT.title.spacing};margin-bottom:3px}
  157. .sub{font-size:${FONT.sub.size}px;color:var(--muted);margin-bottom:14px}
  158. .src{font-size:${FONT.src.size}px;color:var(--faint);margin-top:10px;letter-spacing:${FONT.src.spacing};font-weight:${FONT.src.weight}}
  159. .ch{height:320px}
  160. svg text{font-family:'Inter',sans-serif}` + MOTION.css;
  161. global.MONO = { INK, PAPER, MUTED, FAINT, GRID, L, LAD, DARK,
  162. FONT, SHAPE, MOTION, tipLight, tipDark,
  163. rnd, pol, sect, blob, el, txt, tip, obsReveal, eReveal, keep, CARD_CSS };
  164. })(typeof window !== 'undefined' ? window : globalThis);