/* Astrion 品牌形象组件 —— 渲染 + 眨眼 + 鼠标追踪 * * 用法: * AstrionAvatar.mount(el, { mode: 'idle' | 'think', blink: true, track: true }) * * idle 模式:竖线双眼,随机眨眼,眼睛组随鼠标方向轻微偏移。 * think 模式:三个波浪圆点(不追踪、不眨眼)。 */ (function () { 'use strict'; var SVG_NS = 'http://www.w3.org/2000/svg'; // —— 精确还原 StatusAvatar.vue 的几何参数 —— var FRAME_PATH = 'M 175.959,107.000 Q 180.000,100.000 175.959,93.000 ' + 'L 144.041,37.718 Q 140.000,30.718 131.917,30.718 ' + 'L 68.083,30.718 Q 60.000,30.718 55.959,37.718 ' + 'L 24.041,93.000 Q 20.000,100.000 24.041,107.000 ' + 'L 55.959,162.282 Q 60.000,169.282 68.083,169.282 ' + 'L 131.917,169.282 Q 140.000,169.282 144.041,162.282 Z'; // 眼睛 path 的 18 个数字:M a,b Q c,d e,f Q g,h i,j Q k,l m,n Q o,p q,r var EYE_IDLE = [0, -11, 0, -5.5, 0, 0, 0, 5.5, 0, 11, 0, 5.5, 0, 0, 0, -5.5, 0, -11]; var EYE_BLINK = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; var EYE_LEFT = { x: 84, y: 96 }; var EYE_RIGHT = { x: 116, y: 96 }; // 鼠标追踪参数(viewBox 单位) var TRACK_MAX = 7; // 最大偏移 var TRACK_RANGE = 140; // 屏幕 px 内达到最大偏移 var TRACK_LERP = 0.14; function eyePathD(n) { return ( 'M ' + n[0] + ',' + n[1] + ' Q ' + n[2] + ',' + n[3] + ' ' + n[4] + ',' + n[5] + ' Q ' + n[6] + ',' + n[7] + ' ' + n[8] + ',' + n[9] + ' Q ' + n[10] + ',' + n[11] + ' ' + n[12] + ',' + n[13] + ' Q ' + n[14] + ',' + n[15] + ' ' + n[16] + ',' + n[17] ); } function svgEl(tag, attrs) { var el = document.createElementNS(SVG_NS, tag); for (var k in attrs) el.setAttribute(k, attrs[k]); return el; } // —— 眨眼 morph:同构 path 数字线性插值 —— function morphEyes(eyes, from, to, duration, done) { var start = null; function step(ts) { if (!start) start = ts; var t = Math.min((ts - start) / duration, 1); var e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; // easeInOutQuad var cur = from.map(function (v, i) { return v + (to[i] - v) * e; }); var d = eyePathD(cur); eyes[0].setAttribute('d', d); eyes[1].setAttribute('d', d); if (t < 1) requestAnimationFrame(step); else if (done) done(); } requestAnimationFrame(step); } // —— 全局鼠标追踪(单 rAF 循环驱动所有实例) —— var tracked = []; var mouse = { x: -9999, y: -9999 }; var rafRunning = false; function trackLoop() { for (var i = 0; i < tracked.length; i++) { var inst = tracked[i]; if (!document.contains(inst.svg)) { tracked.splice(i--, 1); continue; } var rect = inst.svg.getBoundingClientRect(); if (rect.width === 0) continue; var cx = rect.left + rect.width / 2; var cy = rect.top + rect.height / 2; var dx = mouse.x - cx; var dy = mouse.y - cy; var dist = Math.hypot(dx, dy) || 1; var reach = Math.min(dist / TRACK_RANGE, 1) * TRACK_MAX; var tx = (dx / dist) * reach; var ty = (dy / dist) * reach; inst.cur.x += (tx - inst.cur.x) * TRACK_LERP; inst.cur.y += (ty - inst.cur.y) * TRACK_LERP; inst.eyes.setAttribute( 'transform', 'translate(' + inst.cur.x.toFixed(2) + ',' + inst.cur.y.toFixed(2) + ')' ); } if (tracked.length > 0) requestAnimationFrame(trackLoop); else rafRunning = false; } window.addEventListener('mousemove', function (e) { mouse.x = e.clientX; mouse.y = e.clientY; }, { passive: true }); function mount(el, opts) { opts = opts || {}; var mode = opts.mode || 'idle'; var svg = svgEl('svg', { class: 'astrion-avatar', viewBox: '0 0 200 200', 'aria-hidden': 'true' }); svg.appendChild(svgEl('path', { class: 'aa-frame', d: FRAME_PATH })); if (mode === 'think') { var think = svgEl('g', { class: 'aa-think' }); [80, 100, 120].forEach(function (x) { think.appendChild(svgEl('circle', { class: 'aa-think-dot', cx: x, cy: 100, r: 7 })); }); svg.appendChild(think); } else { var eyes = svgEl('g', { class: 'aa-eyes' }); var eyeL = svgEl('path', { class: 'aa-eye', transform: 'translate(' + EYE_LEFT.x + ',' + EYE_LEFT.y + ')', d: eyePathD(EYE_IDLE) }); var eyeR = svgEl('path', { class: 'aa-eye', transform: 'translate(' + EYE_RIGHT.x + ',' + EYE_RIGHT.y + ')', d: eyePathD(EYE_IDLE) }); eyes.appendChild(eyeL); eyes.appendChild(eyeR); svg.appendChild(eyes); var pair = [eyeL, eyeR]; if (opts.blink !== false) { (function scheduleBlink() { var delay = 2200 + Math.random() * 2600; setTimeout(function () { if (!document.contains(svg)) return; morphEyes(pair, EYE_IDLE, EYE_BLINK, 120, function () { setTimeout(function () { morphEyes(pair, EYE_BLINK, EYE_IDLE, 170, scheduleBlink); }, 60); }); }, delay); })(); } if (opts.track !== false) { tracked.push({ svg: svg, eyes: eyes, cur: { x: 0, y: 0 } }); if (!rafRunning) { rafRunning = true; requestAnimationFrame(trackLoop); } } } el.appendChild(svg); return svg; } window.AstrionAvatar = { mount: mount }; })();