agent-Specialization/website-design/exp/site-assets/astrion-avatar.js
JOJO 39a7a5fc6f chore(website): 归档官网文档内容与设计素材
- website-content/:官网 10 篇文档内容 + README
- website-design/:官网设计稿、动画实验、预览图与资源
- 根目录官网截图两张
- .gitignore 忽略 website-design/exp/static/dist 构建产物(7.1MB)
2026-09-02 14:38:51 +08:00

251 lines
8.5 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 循环驱动所有实例) ——
// 每个实例二选一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
};
})();