- 主页第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 沙箱重构文档更新
251 lines
8.5 KiB
JavaScript
251 lines
8.5 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 循环驱动所有实例) ——
|
||
// 每个实例二选一: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
|
||
};
|
||
})();
|