astrion-website/demo/site-assets/astrion-avatar.js

166 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 };
})();