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 后不得重放卡片入场动画或产生缩放闪烁。
|
- 左栏 overview sticky/relative 切换用 no-headroom 回归用例固定,解除 fixed 后不得重放卡片入场动画或产生缩放闪烁。
|
||||||
- 左栏文章目录/站点概览切换必须保留 Bootstrap tab fade 节奏,采用 active/show 分帧保持 Argon 手感;回归用例需要断言切换中 opacity 处于 0 到 1 之间,而不是只检查最终显隐。
|
- 左栏文章目录/站点概览切换必须保留 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。
|
- 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 主题色和暗色可读性。
|
- 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>`。
|
- 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,
|
desktopOnly: true,
|
||||||
fallback: null,
|
fallback: null,
|
||||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
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',
|
runtimeScript: '/api/blog/live2d/pio/v1/assets/runtime/pio-runtime.js',
|
||||||
version: 'v1',
|
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;
|
} as const;
|
||||||
|
|
||||||
describe('fetchBlogLive2DManifest', () => {
|
describe('fetchBlogLive2DManifest', () => {
|
||||||
@ -98,6 +140,7 @@ describe('mountOfficialPioRuntime', () => {
|
|||||||
expect(runtime.mount).toHaveBeenCalledWith({
|
expect(runtime.mount).toHaveBeenCalledWith({
|
||||||
canvas,
|
canvas,
|
||||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
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, {
|
expect(runtime.mount).toHaveBeenNthCalledWith(1, {
|
||||||
canvas: firstCanvas,
|
canvas: firstCanvas,
|
||||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||||
|
wordpressParity: pioManifest.wordpressParity,
|
||||||
});
|
});
|
||||||
expect(runtime.mount).toHaveBeenNthCalledWith(2, {
|
expect(runtime.mount).toHaveBeenNthCalledWith(2, {
|
||||||
canvas: secondCanvas,
|
canvas: secondCanvas,
|
||||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||||
|
wordpressParity: pioManifest.wordpressParity,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -166,13 +211,8 @@ describe('BlogLive2D', () => {
|
|||||||
'fetch',
|
'fetch',
|
||||||
vi.fn(async () => new Response(JSON.stringify(pioManifest), { status: 200 })),
|
vi.fn(async () => new Response(JSON.stringify(pioManifest), { status: 200 })),
|
||||||
);
|
);
|
||||||
const frames: FrameRequestCallback[] = [];
|
const requestFrame = vi.spyOn(window, 'requestAnimationFrame');
|
||||||
const cancelFrame = vi.fn();
|
const cancelFrame = vi.spyOn(window, 'cancelAnimationFrame');
|
||||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
|
||||||
frames.push(callback);
|
|
||||||
return frames.length;
|
|
||||||
});
|
|
||||||
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(cancelFrame);
|
|
||||||
const destroy = vi.fn();
|
const destroy = vi.fn();
|
||||||
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||||
const element = Node.prototype.appendChild.call(document.body, node);
|
const element = Node.prototype.appendChild.call(document.body, node);
|
||||||
@ -193,18 +233,19 @@ describe('BlogLive2D', () => {
|
|||||||
expect(window.KtPioLive2D?.mount).toHaveBeenCalledWith({
|
expect(window.KtPioLive2D?.mount).toHaveBeenCalledWith({
|
||||||
canvas: canvas.element,
|
canvas: canvas.element,
|
||||||
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
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();
|
wrapper.unmount();
|
||||||
|
|
||||||
expect(destroy).toHaveBeenCalledTimes(1);
|
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('innerWidth', 1280);
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'fetch',
|
'fetch',
|
||||||
@ -213,19 +254,12 @@ describe('BlogLive2D', () => {
|
|||||||
new Response(
|
new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
...pioManifest,
|
...pioManifest,
|
||||||
motionGroups: {
|
|
||||||
Idle: 6,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{ status: 200 },
|
{ status: 200 },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const frames: FrameRequestCallback[] = [];
|
const requestFrame = vi.spyOn(window, 'requestAnimationFrame');
|
||||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
|
||||||
frames.push(callback);
|
|
||||||
return frames.length;
|
|
||||||
});
|
|
||||||
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||||
const element = Node.prototype.appendChild.call(document.body, node);
|
const element = Node.prototype.appendChild.call(document.body, node);
|
||||||
if (element instanceof HTMLScriptElement) {
|
if (element instanceof HTMLScriptElement) {
|
||||||
@ -256,7 +290,7 @@ describe('BlogLive2D', () => {
|
|||||||
} as DOMRect);
|
} as DOMRect);
|
||||||
|
|
||||||
expect(canvasElement.style.transform).toBe('');
|
expect(canvasElement.style.transform).toBe('');
|
||||||
expect(frames).toHaveLength(0);
|
expect(requestFrame).not.toHaveBeenCalled();
|
||||||
|
|
||||||
window.dispatchEvent(new MouseEvent('pointermove', { clientX: 160, clientY: 160 }));
|
window.dispatchEvent(new MouseEvent('pointermove', { clientX: 160, clientY: 160 }));
|
||||||
|
|
||||||
@ -264,7 +298,7 @@ describe('BlogLive2D', () => {
|
|||||||
|
|
||||||
canvasElement.dispatchEvent(new MouseEvent('pointermove', { clientX: 210, clientY: 260 }));
|
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'));
|
canvasElement.dispatchEvent(new MouseEvent('pointerleave'));
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { blogDomId } from '@/factories/blogDomFactory';
|
|||||||
import { mountOfficialPioRuntime } from './live2d/officialRuntimeBridge';
|
import { mountOfficialPioRuntime } from './live2d/officialRuntimeBridge';
|
||||||
import type { KtPioLive2DRuntimeHandle } from './live2d/types';
|
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({
|
export default defineComponent({
|
||||||
name: 'BlogLive2D',
|
name: 'BlogLive2D',
|
||||||
@ -39,9 +39,11 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runtimeHandle = mountedHandle;
|
runtimeHandle = mountedHandle;
|
||||||
idleAnimatorHandle = createBlogLive2DIdleAnimator(canvasRef.value, {
|
if (!manifest.wordpressParity) {
|
||||||
enableIdleMotion: !manifest.motionGroups?.Idle,
|
idleAnimatorHandle = createBlogLive2DIdleAnimator(canvasRef.value, {
|
||||||
});
|
enableIdleMotion: !manifest.motionGroups?.Idle,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.warn('[KT Blog] Pio Live2D unavailable.', error);
|
console.warn('[KT Blog] Pio Live2D unavailable.', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,11 @@ export async function mountOfficialPioRuntime(
|
|||||||
throw new Error('KtPioLive2D runtime was not registered.');
|
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 {
|
export interface BlogLive2DManifest {
|
||||||
assets?: {
|
assets?: {
|
||||||
shaderBase?: string;
|
shaderBase?: string;
|
||||||
@ -34,6 +66,7 @@ export interface BlogLive2DManifest {
|
|||||||
manifest: string;
|
manifest: string;
|
||||||
};
|
};
|
||||||
version: string;
|
version: string;
|
||||||
|
wordpressParity?: BlogLive2DWordpressParity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KtPioLive2DRuntimeHandle {
|
export interface KtPioLive2DRuntimeHandle {
|
||||||
@ -49,5 +82,9 @@ export interface KtPioLive2DRuntime {
|
|||||||
* @param options Canvas and model URL emitted by the validated manifest.
|
* @param options Canvas and model URL emitted by the validated manifest.
|
||||||
* @returns Runtime handle used by Vue teardown to release rendering resources.
|
* @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