astrion-website/demo/site-assets/astrion-avatar.js
JOJO 64d0a377dc feat: 04版本控制动画 + 演示区5条真实对话mock + 文档同步沙箱重构
- 主页第4行特色动画(版本控制)及 01/02/03 动画迭代
- 演示区对话 mock 全部换为真实对话转换:日常使用(汽车对比新/整理周会速记)、
  StarRaid(弹幕调研/像素飞机设计)、开发实战(猫咪咖啡店多智能体)
- 子智能体 activity、消息截图媒体、文件卡片资源(docx/pdf/html)随站点发布
- sw.js: file/content 二进制 MIME、/api/conversations/media 路由、
  模型列表扩为 K3/V4-Flash/MiniMax-M3/GLM-5.2、CACHE v5
- content/ 同步 8-31 沙箱重构文档更新
2026-09-03 07:47:39 +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
};
})();