/* 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 循环驱动所有实例) —— // 每个实例二选一:useMouse 跟随鼠标;或 manual 目标(动画模块用 handle().lookAt 驱动) 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 tx, ty; if (inst.manual) { tx = inst.manual.x; ty = inst.manual.y; } else if (inst.useMouse) { 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; tx = (dx / dist) * reach; ty = (dy / dist) * reach; } else { tx = 0; ty = 0; } 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); })(); } // 所有 idle 实例都注册进驱动循环:useMouse=false 时由 handle().lookAt 手动驱动 var inst = { svg: svg, eyes: eyes, eyeL: eyeL, eyeR: eyeR, cur: { x: 0, y: 0 }, manual: null, useMouse: opts.track !== false, pathNums: EYE_IDLE.slice(), morphToken: 0 }; svg._aaInst = inst; tracked.push(inst); if (!rafRunning) { rafRunning = true; requestAnimationFrame(trackLoop); } } el.appendChild(svg); return svg; } /* 动画模块用的眼睛控制手柄(仅 idle 模式实例可用): lookAt(dx,dy) 直接给 viewBox 偏移;lookAtPoint(cx,cy) 看向屏幕某点; morphEyesTo(nums,ms) 眼睛形状渐变(困眼/瞪眼);setEyePath 瞬切。 */ function handle(svg) { var inst = svg && svg._aaInst; if (!inst) return null; var h = { lookAt: function (dx, dy) { var m = Math.hypot(dx, dy); if (m > TRACK_MAX) { dx = (dx / m) * TRACK_MAX; dy = (dy / m) * TRACK_MAX; } inst.manual = { x: dx, y: dy }; }, lookAtPoint: function (clientX, clientY) { var rect = inst.svg.getBoundingClientRect(); if (rect.width === 0) return; var cx = rect.left + rect.width / 2; var cy = rect.top + rect.height / 2; var dx = (clientX - cx) / (rect.width / 200); var dy = (clientY - cy) / (rect.height / 200); var d = Math.hypot(dx, dy) || 1; var s = Math.min(d / 60, 1); /* 60 个 viewBox 单位外达到最大注视 */ h.lookAt((dx / d) * TRACK_MAX * s, (dy / d) * TRACK_MAX * s); }, releaseLook: function () { inst.manual = null; }, setEyePath: function (nums) { inst.morphToken++; inst.pathNums = nums.slice(); var d = eyePathD(nums); inst.eyeL.setAttribute('d', d); inst.eyeR.setAttribute('d', d); }, morphEyesTo: function (nums, ms, done) { var token = ++inst.morphToken; var from = inst.pathNums.slice(); var start = null; function stepFn(ts) { if (token !== inst.morphToken) return; /* 被更新的 morph 接管,静默退出 */ if (!start) start = ts; var t = Math.min((ts - start) / ms, 1); var e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; var cur = from.map(function (v, i) { return v + (nums[i] - v) * e; }); var d = eyePathD(cur); inst.eyeL.setAttribute('d', d); inst.eyeR.setAttribute('d', d); if (t < 1) requestAnimationFrame(stepFn); else { inst.pathNums = nums.slice(); if (done) done(); } } requestAnimationFrame(stepFn); } }; return h; } /* 由 IDLE 眼睛派生缩放版本(困=缩短,瞪=加长):只缩放 y 分量(x 分量全为 0) */ function eyeScale(k) { return EYE_IDLE.map(function (v, i) { return i % 2 ? v * k : v; }); } window.AstrionAvatar = { mount: mount, handle: handle, eyeScale: eyeScale, EYE_IDLE: EYE_IDLE, EYE_BLINK: EYE_BLINK }; })();