fix: 切换Pio Live2D线上v2契约
This commit is contained in:
parent
b6ca7c29fc
commit
c824b1a42e
@ -42,7 +42,7 @@ pnpm exec playwright test e2e/argon-parity/baseline.spec.ts --project=chromium
|
||||
- 左栏 overview sticky/relative 切换用 no-headroom 回归用例固定,解除 fixed 后不得重放卡片入场动画或产生缩放闪烁。
|
||||
- 左栏文章目录/站点概览切换必须保留 Bootstrap tab fade 节奏,采用 active/show 分帧保持 Argon 手感;回归用例需要断言切换中 opacity 处于 0 到 1 之间,而不是只检查最终显隐。
|
||||
- Argon motion、滚动几何、延迟、RAF 调度、DOM id/selector、hash anchor 和跨组件 ref 必须从 `src/factories/blogAnimationFactory.ts` 与 `src/factories/blogDomFactory.ts` 取值;组件和 Hook 不再散写行为层 id、裸 `requestAnimationFrame`、裸 `setTimeout` 或重复 timing。
|
||||
- Live2D 不依赖旧 WordPress `live-2d` 插件、`live2d-widgets`、随机 CDN fallback 模型或插件账号签名;`BlogLive2D` 只在桌面端读取 `VITE_BLOG_LIVE2D_MANIFEST_URL`,默认 `/api/blog/live2d/pio/v1/manifest.json`,并通过自托管 Pio manifest 加载官方 Cubism runtime。Manifest 已提供源 `Idle` motions 时,前端不得再叠加自动 idle bob/breath/sway,只保留 pointer parallax 兜底;线上 Pio 源目录必须遵守 `docs/live2d-pio-minio-directory-standard.md`,保留 `catalog.json`、`textures/manifest.json` 与完整 source texture catalog。
|
||||
- Live2D 不依赖旧 WordPress `live-2d` 插件、`live2d-widgets`、随机 CDN fallback 模型或插件账号签名;`BlogLive2D` 只在桌面端读取 `VITE_BLOG_LIVE2D_MANIFEST_URL`,默认 `/api/blog/live2d/pio/v2/manifest.json`,并通过自托管 Pio manifest 加载官方 Cubism runtime。Manifest 声明 `wordpressParity` 时,Idle/Sleepy、呼吸、鼠标视差、头部点击和身体点击都由生成 runtime 接管,前端不得再叠加 bob/breath/sway/parallax 兜底;线上 Pio 源目录必须遵守 `docs/live2d-pio-minio-directory-standard.md`,保留 `catalog.json`、`textures/manifest.json` 与完整 source texture catalog。
|
||||
- Modal 不强求一比一复刻线上 Argon 动画,打开/关闭 motion 与 `centered` 居中定位交给 antdv-next;外层按 `packages/@core/ui-kit/popup-ui/src/modal/modal.vue` 保留 Header/Content/Footer 三段能力、`p-0`、`max-height`、纵向 flex、Content `min-h-40` 滚动和 `px-5 py-4`/`p-3`/`p-2` 间距,但无 footer slot 时不渲染 footer,不搬 draggable/fullscreen/loading/footer 按钮等复杂能力,自有颜色只守 Blog 主题色和暗色可读性。
|
||||
- Admin 文章预览通过 `VITE_KT_BLOG_WEB_BASE_URL` 打开公开 Blog Web 路由,本地默认 `http://127.0.0.1:5173/#/post/<slug>?adminPreview=1&articleId=<id>`。
|
||||
|
||||
|
||||
@ -11,8 +11,50 @@ const pioManifest = {
|
||||
desktopOnly: true,
|
||||
fallback: null,
|
||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||
motionGroups: {
|
||||
FlickHead: 6,
|
||||
Idle: 6,
|
||||
Sleepy: 1,
|
||||
TapBody: 9,
|
||||
},
|
||||
runtimeScript: '/api/blog/live2d/pio/v1/assets/runtime/pio-runtime.js',
|
||||
version: 'v1',
|
||||
wordpressParity: {
|
||||
hitAreasCustom: {
|
||||
body_x: [-0.3, -0.25],
|
||||
body_y: [0.3, -0.9],
|
||||
head_x: [-0.35, 0.6],
|
||||
head_y: [0.19, -0.2],
|
||||
},
|
||||
layout: {
|
||||
center_x: 0,
|
||||
center_y: -0.05,
|
||||
width: 2,
|
||||
},
|
||||
motionGroups: {
|
||||
FlickHead: {
|
||||
legacyName: 'flick_head',
|
||||
sourceFiles: ['motions/Touch Dere1.mtn'],
|
||||
},
|
||||
Idle: {
|
||||
legacyName: 'idle',
|
||||
sourceFiles: ['motions/Breath1.mtn'],
|
||||
},
|
||||
Sleepy: {
|
||||
legacyName: 'sleepy',
|
||||
sourceFiles: ['motions/Sleeping.mtn'],
|
||||
},
|
||||
TapBody: {
|
||||
legacyName: 'tap_body',
|
||||
sourceFiles: ['motions/Touch1.mtn'],
|
||||
},
|
||||
},
|
||||
parameterRig: {
|
||||
breath: [{ cycle: 3.2345, id: 'ParamBreath', offset: 0.5, peak: 0.5, weight: 1 }],
|
||||
look: [{ factorX: 30, factorXY: 0, factorY: 0, id: 'ParamAngleX' }],
|
||||
},
|
||||
version: '2026-07-05-wordpress-v1',
|
||||
},
|
||||
} as const;
|
||||
|
||||
describe('fetchBlogLive2DManifest', () => {
|
||||
@ -98,6 +140,7 @@ describe('mountOfficialPioRuntime', () => {
|
||||
expect(runtime.mount).toHaveBeenCalledWith({
|
||||
canvas,
|
||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||
wordpressParity: pioManifest.wordpressParity,
|
||||
});
|
||||
});
|
||||
|
||||
@ -133,10 +176,12 @@ describe('mountOfficialPioRuntime', () => {
|
||||
expect(runtime.mount).toHaveBeenNthCalledWith(1, {
|
||||
canvas: firstCanvas,
|
||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||
wordpressParity: pioManifest.wordpressParity,
|
||||
});
|
||||
expect(runtime.mount).toHaveBeenNthCalledWith(2, {
|
||||
canvas: secondCanvas,
|
||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||
wordpressParity: pioManifest.wordpressParity,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -166,13 +211,8 @@ describe('BlogLive2D', () => {
|
||||
'fetch',
|
||||
vi.fn(async () => new Response(JSON.stringify(pioManifest), { status: 200 })),
|
||||
);
|
||||
const frames: FrameRequestCallback[] = [];
|
||||
const cancelFrame = vi.fn();
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
||||
frames.push(callback);
|
||||
return frames.length;
|
||||
});
|
||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(cancelFrame);
|
||||
const requestFrame = vi.spyOn(window, 'requestAnimationFrame');
|
||||
const cancelFrame = vi.spyOn(window, 'cancelAnimationFrame');
|
||||
const destroy = vi.fn();
|
||||
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||
const element = Node.prototype.appendChild.call(document.body, node);
|
||||
@ -193,18 +233,19 @@ describe('BlogLive2D', () => {
|
||||
expect(window.KtPioLive2D?.mount).toHaveBeenCalledWith({
|
||||
canvas: canvas.element,
|
||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||
wordpressParity: pioManifest.wordpressParity,
|
||||
});
|
||||
frames.shift()?.(1000);
|
||||
|
||||
expect((canvas.element as HTMLCanvasElement).style.transform).toContain('translate3d');
|
||||
expect((canvas.element as HTMLCanvasElement).style.transform).toBe('');
|
||||
|
||||
wrapper.unmount();
|
||||
|
||||
expect(destroy).toHaveBeenCalledTimes(1);
|
||||
expect(cancelFrame).toHaveBeenCalled();
|
||||
expect(requestFrame).not.toHaveBeenCalled();
|
||||
expect(cancelFrame).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not start the canvas idle fallback when source idle motions exist', async () => {
|
||||
it('does not start canvas transform fallback when WordPress parity owns motion', async () => {
|
||||
vi.stubGlobal('innerWidth', 1280);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
@ -213,19 +254,12 @@ describe('BlogLive2D', () => {
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
...pioManifest,
|
||||
motionGroups: {
|
||||
Idle: 6,
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
),
|
||||
);
|
||||
const frames: FrameRequestCallback[] = [];
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
||||
frames.push(callback);
|
||||
return frames.length;
|
||||
});
|
||||
const requestFrame = vi.spyOn(window, 'requestAnimationFrame');
|
||||
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||
const element = Node.prototype.appendChild.call(document.body, node);
|
||||
if (element instanceof HTMLScriptElement) {
|
||||
@ -256,7 +290,7 @@ describe('BlogLive2D', () => {
|
||||
} as DOMRect);
|
||||
|
||||
expect(canvasElement.style.transform).toBe('');
|
||||
expect(frames).toHaveLength(0);
|
||||
expect(requestFrame).not.toHaveBeenCalled();
|
||||
|
||||
window.dispatchEvent(new MouseEvent('pointermove', { clientX: 160, clientY: 160 }));
|
||||
|
||||
@ -264,7 +298,7 @@ describe('BlogLive2D', () => {
|
||||
|
||||
canvasElement.dispatchEvent(new MouseEvent('pointermove', { clientX: 210, clientY: 260 }));
|
||||
|
||||
expect(canvasElement.style.transform).toContain('translate3d');
|
||||
expect(canvasElement.style.transform).toBe('');
|
||||
|
||||
canvasElement.dispatchEvent(new MouseEvent('pointerleave'));
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import { blogDomId } from '@/factories/blogDomFactory';
|
||||
import { mountOfficialPioRuntime } from './live2d/officialRuntimeBridge';
|
||||
import type { KtPioLive2DRuntimeHandle } from './live2d/types';
|
||||
|
||||
const LIVE2D_MANIFEST_URL = import.meta.env.VITE_BLOG_LIVE2D_MANIFEST_URL || '/api/blog/live2d/pio/v1/manifest.json';
|
||||
const LIVE2D_MANIFEST_URL = import.meta.env.VITE_BLOG_LIVE2D_MANIFEST_URL || '/api/blog/live2d/pio/v2/manifest.json';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BlogLive2D',
|
||||
@ -39,9 +39,11 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
runtimeHandle = mountedHandle;
|
||||
idleAnimatorHandle = createBlogLive2DIdleAnimator(canvasRef.value, {
|
||||
enableIdleMotion: !manifest.motionGroups?.Idle,
|
||||
});
|
||||
if (!manifest.wordpressParity) {
|
||||
idleAnimatorHandle = createBlogLive2DIdleAnimator(canvasRef.value, {
|
||||
enableIdleMotion: !manifest.motionGroups?.Idle,
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.warn('[KT Blog] Pio Live2D unavailable.', error);
|
||||
}
|
||||
|
||||
@ -28,7 +28,11 @@ export async function mountOfficialPioRuntime(
|
||||
throw new Error('KtPioLive2D runtime was not registered.');
|
||||
}
|
||||
|
||||
return window.KtPioLive2D.mount({ canvas, model3: manifest.model3 });
|
||||
return window.KtPioLive2D.mount({
|
||||
canvas,
|
||||
model3: manifest.model3,
|
||||
wordpressParity: manifest.wordpressParity,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,3 +1,35 @@
|
||||
export interface BlogLive2DWordpressParity {
|
||||
hitAreasCustom: Record<'body_x' | 'body_y' | 'head_x' | 'head_y', readonly [number, number]>;
|
||||
layout: {
|
||||
center_x: number;
|
||||
center_y: number;
|
||||
width: number;
|
||||
};
|
||||
motionGroups: Record<
|
||||
'FlickHead' | 'Idle' | 'Sleepy' | 'TapBody',
|
||||
{
|
||||
legacyName: string;
|
||||
sourceFiles: readonly string[];
|
||||
}
|
||||
>;
|
||||
parameterRig: {
|
||||
breath: readonly {
|
||||
cycle: number;
|
||||
id: string;
|
||||
offset: number;
|
||||
peak: number;
|
||||
weight: number;
|
||||
}[];
|
||||
look: readonly {
|
||||
factorX: number;
|
||||
factorXY: number;
|
||||
factorY: number;
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface BlogLive2DManifest {
|
||||
assets?: {
|
||||
shaderBase?: string;
|
||||
@ -34,6 +66,7 @@ export interface BlogLive2DManifest {
|
||||
manifest: string;
|
||||
};
|
||||
version: string;
|
||||
wordpressParity?: BlogLive2DWordpressParity;
|
||||
}
|
||||
|
||||
export interface KtPioLive2DRuntimeHandle {
|
||||
@ -49,5 +82,9 @@ export interface KtPioLive2DRuntime {
|
||||
* @param options Canvas and model URL emitted by the validated manifest.
|
||||
* @returns Runtime handle used by Vue teardown to release rendering resources.
|
||||
*/
|
||||
mount(options: { canvas: HTMLCanvasElement; model3: string }): Promise<KtPioLive2DRuntimeHandle>;
|
||||
mount(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
model3: string;
|
||||
wordpressParity?: BlogLive2DWordpressParity;
|
||||
}): Promise<KtPioLive2DRuntimeHandle>;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user