166 lines
5.4 KiB
JavaScript
166 lines
5.4 KiB
JavaScript
/* 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 };
|
||
})();
|