fix: 对齐Pio源动作加载边界
This commit is contained in:
parent
c2cd06ae4a
commit
b6ca7c29fc
@ -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。
|
- 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。
|
||||||
- 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>`。
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,8 @@ const pioManifest = {
|
|||||||
character: 'pio',
|
character: 'pio',
|
||||||
desktopOnly: true,
|
desktopOnly: true,
|
||||||
fallback: null,
|
fallback: null,
|
||||||
model3: '/api/blog/live2d/pio/v1/pio.model3.json',
|
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||||
runtimeScript: '/api/blog/live2d/pio/v1/pio-runtime.js',
|
runtimeScript: '/api/blog/live2d/pio/v1/assets/runtime/pio-runtime.js',
|
||||||
version: 'v1',
|
version: 'v1',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ describe('fetchBlogLive2DManifest', () => {
|
|||||||
|
|
||||||
expect(manifest.character).toBe('pio');
|
expect(manifest.character).toBe('pio');
|
||||||
expect(manifest.fallback).toBeNull();
|
expect(manifest.fallback).toBeNull();
|
||||||
expect(manifest.model3).toBe('/api/blog/live2d/pio/v1/pio.model3.json');
|
expect(manifest.model3).toBe('/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects non-Pio or fallback manifests before runtime loading', async () => {
|
it('rejects non-Pio or fallback manifests before runtime loading', async () => {
|
||||||
@ -97,7 +97,7 @@ describe('mountOfficialPioRuntime', () => {
|
|||||||
expect(runtime.mount).toHaveBeenCalledTimes(2);
|
expect(runtime.mount).toHaveBeenCalledTimes(2);
|
||||||
expect(runtime.mount).toHaveBeenCalledWith({
|
expect(runtime.mount).toHaveBeenCalledWith({
|
||||||
canvas,
|
canvas,
|
||||||
model3: '/api/blog/live2d/pio/v1/pio.model3.json',
|
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -132,11 +132,11 @@ describe('mountOfficialPioRuntime', () => {
|
|||||||
expect(runtime.mount).toHaveBeenCalledTimes(2);
|
expect(runtime.mount).toHaveBeenCalledTimes(2);
|
||||||
expect(runtime.mount).toHaveBeenNthCalledWith(1, {
|
expect(runtime.mount).toHaveBeenNthCalledWith(1, {
|
||||||
canvas: firstCanvas,
|
canvas: firstCanvas,
|
||||||
model3: '/api/blog/live2d/pio/v1/pio.model3.json',
|
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||||
});
|
});
|
||||||
expect(runtime.mount).toHaveBeenNthCalledWith(2, {
|
expect(runtime.mount).toHaveBeenNthCalledWith(2, {
|
||||||
canvas: secondCanvas,
|
canvas: secondCanvas,
|
||||||
model3: '/api/blog/live2d/pio/v1/pio.model3.json',
|
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -192,7 +192,7 @@ describe('BlogLive2D', () => {
|
|||||||
expect(canvas.exists()).toBe(true);
|
expect(canvas.exists()).toBe(true);
|
||||||
expect(window.KtPioLive2D?.mount).toHaveBeenCalledWith({
|
expect(window.KtPioLive2D?.mount).toHaveBeenCalledWith({
|
||||||
canvas: canvas.element,
|
canvas: canvas.element,
|
||||||
model3: '/api/blog/live2d/pio/v1/pio.model3.json',
|
model3: '/api/blog/live2d/pio/v1/assets/model/pio.moc-reconstructed.model3.json',
|
||||||
});
|
});
|
||||||
frames.shift()?.(1000);
|
frames.shift()?.(1000);
|
||||||
|
|
||||||
@ -203,6 +203,75 @@ describe('BlogLive2D', () => {
|
|||||||
expect(destroy).toHaveBeenCalledTimes(1);
|
expect(destroy).toHaveBeenCalledTimes(1);
|
||||||
expect(cancelFrame).toHaveBeenCalled();
|
expect(cancelFrame).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not start the canvas idle fallback when source idle motions exist', async () => {
|
||||||
|
vi.stubGlobal('innerWidth', 1280);
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(
|
||||||
|
async () =>
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||||
|
const element = Node.prototype.appendChild.call(document.body, node);
|
||||||
|
if (element instanceof HTMLScriptElement) {
|
||||||
|
window.KtPioLive2D = {
|
||||||
|
mount: vi.fn(async () => ({ destroy: vi.fn() })),
|
||||||
|
};
|
||||||
|
element.onload?.(new Event('load'));
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mount(BlogLive2D);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const canvas = wrapper.find('canvas#live2d');
|
||||||
|
expect(canvas.exists()).toBe(true);
|
||||||
|
const canvasElement = canvas.element as HTMLCanvasElement;
|
||||||
|
vi.spyOn(canvasElement, 'getBoundingClientRect').mockReturnValue({
|
||||||
|
bottom: 420,
|
||||||
|
height: 320,
|
||||||
|
left: 100,
|
||||||
|
right: 320,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
top: 100,
|
||||||
|
width: 220,
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
} as DOMRect);
|
||||||
|
|
||||||
|
expect(canvasElement.style.transform).toBe('');
|
||||||
|
expect(frames).toHaveLength(0);
|
||||||
|
|
||||||
|
window.dispatchEvent(new MouseEvent('pointermove', { clientX: 160, clientY: 160 }));
|
||||||
|
|
||||||
|
expect(canvasElement.style.transform).toBe('');
|
||||||
|
|
||||||
|
canvasElement.dispatchEvent(new MouseEvent('pointermove', { clientX: 210, clientY: 260 }));
|
||||||
|
|
||||||
|
expect(canvasElement.style.transform).toContain('translate3d');
|
||||||
|
|
||||||
|
canvasElement.dispatchEvent(new MouseEvent('pointerleave'));
|
||||||
|
|
||||||
|
expect(canvasElement.style.transform).toBe('');
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createBlogLive2DIdleAnimator', () => {
|
describe('createBlogLive2DIdleAnimator', () => {
|
||||||
|
|||||||
@ -39,7 +39,9 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runtimeHandle = mountedHandle;
|
runtimeHandle = mountedHandle;
|
||||||
idleAnimatorHandle = createBlogLive2DIdleAnimator(canvasRef.value);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,38 @@
|
|||||||
export interface BlogLive2DManifest {
|
export interface BlogLive2DManifest {
|
||||||
|
assets?: {
|
||||||
|
shaderBase?: string;
|
||||||
|
sourceTextureBase?: string;
|
||||||
|
textureBase?: string;
|
||||||
|
};
|
||||||
character: 'pio';
|
character: 'pio';
|
||||||
|
directoryStandard?: {
|
||||||
|
catalog: string;
|
||||||
|
opsRoot: string;
|
||||||
|
publicRoot: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
desktopOnly: boolean;
|
desktopOnly: boolean;
|
||||||
fallback: null;
|
fallback: null;
|
||||||
integrity?: {
|
integrity?: {
|
||||||
manifestSha256?: string;
|
manifestSha256?: string;
|
||||||
|
mocSha256?: string;
|
||||||
|
model3Sha256?: string;
|
||||||
|
motionValidationSha256?: string;
|
||||||
|
runtimeSha256?: string;
|
||||||
|
sourceMotionExportSha256?: string;
|
||||||
|
sourceTextureManifestSha256?: string;
|
||||||
};
|
};
|
||||||
model3: string;
|
model3: string;
|
||||||
|
motionGroups?: Record<string, number>;
|
||||||
|
runtimeRig?: {
|
||||||
|
type: 'moc3-model-motion';
|
||||||
|
validation: string;
|
||||||
|
};
|
||||||
runtimeScript: string;
|
runtimeScript: string;
|
||||||
|
sourceTextures?: {
|
||||||
|
count: number;
|
||||||
|
manifest: string;
|
||||||
|
};
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,11 @@ export interface BlogLive2DIdleAnimatorHandle {
|
|||||||
destroy: () => void;
|
destroy: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BlogLive2DIdleAnimatorOptions {
|
||||||
|
enableIdleMotion?: boolean;
|
||||||
|
enablePointerParallax?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns CSS custom property block generated from the Blog animation factory.
|
* @returns CSS custom property block generated from the Blog animation factory.
|
||||||
*/
|
*/
|
||||||
@ -116,14 +121,18 @@ export function createBlogFrameScheduler(callback: () => void): BlogFrameSchedul
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a lightweight visual idle loop around the self-hosted Pio canvas.
|
* Adds lightweight fallback motion and pointer parallax around the self-hosted Pio canvas.
|
||||||
* The current reconstructed MOC has no shipped motion/physics files, so this
|
|
||||||
* wrapper-level motion keeps the character alive without claiming Cubism rigging.
|
|
||||||
* @param canvas Pio canvas owned by `BlogLive2D`; its inline transform is restored on destroy.
|
* @param canvas Pio canvas owned by `BlogLive2D`; its inline transform is restored on destroy.
|
||||||
|
* @param options Flags that disable idle fallback once source model motions are available.
|
||||||
* @returns Lifecycle handle that must be destroyed with the runtime mount handle.
|
* @returns Lifecycle handle that must be destroyed with the runtime mount handle.
|
||||||
*/
|
*/
|
||||||
export function createBlogLive2DIdleAnimator(canvas: HTMLCanvasElement): BlogLive2DIdleAnimatorHandle {
|
export function createBlogLive2DIdleAnimator(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
options: BlogLive2DIdleAnimatorOptions = {},
|
||||||
|
): BlogLive2DIdleAnimatorHandle {
|
||||||
const initialTransform = canvas.style.transform;
|
const initialTransform = canvas.style.transform;
|
||||||
|
const enableIdleMotion = options.enableIdleMotion ?? true;
|
||||||
|
const enablePointerParallax = options.enablePointerParallax ?? true;
|
||||||
let frameId = 0;
|
let frameId = 0;
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let startedAt = 0;
|
let startedAt = 0;
|
||||||
@ -131,27 +140,81 @@ export function createBlogLive2DIdleAnimator(canvas: HTMLCanvasElement): BlogLiv
|
|||||||
let pointerTargetY = 0;
|
let pointerTargetY = 0;
|
||||||
let pointerX = 0;
|
let pointerX = 0;
|
||||||
let pointerY = 0;
|
let pointerY = 0;
|
||||||
|
let pointerActive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the canvas transform to the value owned before this fallback animator.
|
||||||
|
*/
|
||||||
|
const restoreTransform = () => {
|
||||||
|
canvas.style.transform = initialTransform;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the visitor pointer as a small parallax target around the fixed Pio canvas.
|
* Tracks the visitor pointer as a small parallax target around the fixed Pio canvas.
|
||||||
* @param event Pointer movement on the document viewport.
|
* @param event Pointer movement on the active parallax surface.
|
||||||
*/
|
*/
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
const onPointerMove = (event: PointerEvent) => {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const right = rect.left + rect.width;
|
||||||
|
const bottom = rect.top + rect.height;
|
||||||
|
const pointerInsideCanvas =
|
||||||
|
event.clientX >= rect.left && event.clientX <= right && event.clientY >= rect.top && event.clientY <= bottom;
|
||||||
|
if (!enableIdleMotion && !pointerInsideCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerActive = true;
|
||||||
const centerX = rect.left + rect.width / 2;
|
const centerX = rect.left + rect.width / 2;
|
||||||
const centerY = rect.top + rect.height / 2;
|
const centerY = rect.top + rect.height / 2;
|
||||||
const normalizedX = Math.max(-1, Math.min(1, (event.clientX - centerX) / Math.max(rect.width, 1)));
|
const normalizedX = Math.max(-1, Math.min(1, (event.clientX - centerX) / Math.max(rect.width, 1)));
|
||||||
const normalizedY = Math.max(-1, Math.min(1, (event.clientY - centerY) / Math.max(rect.height, 1)));
|
const normalizedY = Math.max(-1, Math.min(1, (event.clientY - centerY) / Math.max(rect.height, 1)));
|
||||||
pointerTargetX = normalizedX * BLOG_LIVE2D_IDLE_MOTION.pointerMaxX;
|
pointerTargetX = normalizedX * BLOG_LIVE2D_IDLE_MOTION.pointerMaxX;
|
||||||
pointerTargetY = normalizedY * BLOG_LIVE2D_IDLE_MOTION.pointerMaxY;
|
pointerTargetY = normalizedY * BLOG_LIVE2D_IDLE_MOTION.pointerMaxY;
|
||||||
|
if (!enableIdleMotion) {
|
||||||
|
pointerX = pointerTargetX;
|
||||||
|
pointerY = pointerTargetY;
|
||||||
|
writeTransform(0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lets the idle loop ease Pio back to neutral when the pointer leaves the document.
|
* Lets the idle loop ease Pio back to neutral when the pointer leaves the document.
|
||||||
*/
|
*/
|
||||||
const onPointerLeave = () => {
|
const onPointerLeave = () => {
|
||||||
|
pointerActive = false;
|
||||||
pointerTargetX = 0;
|
pointerTargetX = 0;
|
||||||
pointerTargetY = 0;
|
pointerTargetY = 0;
|
||||||
|
if (!enableIdleMotion) {
|
||||||
|
pointerX = 0;
|
||||||
|
pointerY = 0;
|
||||||
|
restoreTransform();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the composed fallback transform for either idle frames or pointer-only updates.
|
||||||
|
* @param elapsedSeconds Elapsed time used by idle bob, breath, and sway terms.
|
||||||
|
*/
|
||||||
|
const writeTransform = (elapsedSeconds: number) => {
|
||||||
|
if (!enableIdleMotion && !pointerActive) {
|
||||||
|
restoreTransform();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bob = enableIdleMotion ? Math.sin(elapsedSeconds * 1.65) * BLOG_LIVE2D_IDLE_MOTION.bobPx : 0;
|
||||||
|
const scale = enableIdleMotion ? 1 + Math.sin(elapsedSeconds * 2.1) * BLOG_LIVE2D_IDLE_MOTION.breathScale : 1;
|
||||||
|
const sway = enableIdleMotion ? Math.sin(elapsedSeconds * 0.95) * BLOG_LIVE2D_IDLE_MOTION.swayDeg : 0;
|
||||||
|
const rotate =
|
||||||
|
sway + (pointerX / BLOG_LIVE2D_IDLE_MOTION.pointerMaxX) * BLOG_LIVE2D_IDLE_MOTION.pointerMaxRotateDeg;
|
||||||
|
|
||||||
|
canvas.style.transform = [
|
||||||
|
initialTransform,
|
||||||
|
`translate3d(${(pointerX * 0.45).toFixed(2)}px, ${(bob + pointerY * 0.35).toFixed(2)}px, 0)`,
|
||||||
|
`rotate(${rotate.toFixed(3)}deg)`,
|
||||||
|
`scale(${scale.toFixed(4)})`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -169,34 +232,37 @@ export function createBlogLive2DIdleAnimator(canvas: HTMLCanvasElement): BlogLiv
|
|||||||
const elapsedSeconds = (timestamp - startedAt) / 1000;
|
const elapsedSeconds = (timestamp - startedAt) / 1000;
|
||||||
pointerX += (pointerTargetX - pointerX) * BLOG_LIVE2D_IDLE_MOTION.pointerSmoothing;
|
pointerX += (pointerTargetX - pointerX) * BLOG_LIVE2D_IDLE_MOTION.pointerSmoothing;
|
||||||
pointerY += (pointerTargetY - pointerY) * BLOG_LIVE2D_IDLE_MOTION.pointerSmoothing;
|
pointerY += (pointerTargetY - pointerY) * BLOG_LIVE2D_IDLE_MOTION.pointerSmoothing;
|
||||||
const bob = Math.sin(elapsedSeconds * 1.65) * BLOG_LIVE2D_IDLE_MOTION.bobPx;
|
writeTransform(elapsedSeconds);
|
||||||
const scale = 1 + Math.sin(elapsedSeconds * 2.1) * BLOG_LIVE2D_IDLE_MOTION.breathScale;
|
|
||||||
const rotate =
|
|
||||||
Math.sin(elapsedSeconds * 0.95) * BLOG_LIVE2D_IDLE_MOTION.swayDeg
|
|
||||||
+ (pointerX / BLOG_LIVE2D_IDLE_MOTION.pointerMaxX) * BLOG_LIVE2D_IDLE_MOTION.pointerMaxRotateDeg;
|
|
||||||
|
|
||||||
canvas.style.transform = [
|
|
||||||
initialTransform,
|
|
||||||
`translate3d(${(pointerX * 0.45).toFixed(2)}px, ${(bob + pointerY * 0.35).toFixed(2)}px, 0)`,
|
|
||||||
`rotate(${rotate.toFixed(3)}deg)`,
|
|
||||||
`scale(${scale.toFixed(4)})`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
frameId = requestBlogFrame(tick);
|
frameId = requestBlogFrame(tick);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (enablePointerParallax) {
|
||||||
|
if (enableIdleMotion) {
|
||||||
window.addEventListener('pointermove', onPointerMove, { passive: true });
|
window.addEventListener('pointermove', onPointerMove, { passive: true });
|
||||||
window.addEventListener('pointerleave', onPointerLeave);
|
window.addEventListener('pointerleave', onPointerLeave);
|
||||||
|
} else {
|
||||||
|
canvas.addEventListener('pointermove', onPointerMove, { passive: true });
|
||||||
|
canvas.addEventListener('pointerleave', onPointerLeave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enableIdleMotion) {
|
||||||
frameId = requestBlogFrame(tick);
|
frameId = requestBlogFrame(tick);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
cancelBlogFrame(frameId);
|
cancelBlogFrame(frameId);
|
||||||
|
if (enablePointerParallax) {
|
||||||
|
if (enableIdleMotion) {
|
||||||
window.removeEventListener('pointermove', onPointerMove);
|
window.removeEventListener('pointermove', onPointerMove);
|
||||||
window.removeEventListener('pointerleave', onPointerLeave);
|
window.removeEventListener('pointerleave', onPointerLeave);
|
||||||
canvas.style.transform = initialTransform;
|
} else {
|
||||||
|
canvas.removeEventListener('pointermove', onPointerMove);
|
||||||
|
canvas.removeEventListener('pointerleave', onPointerLeave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restoreTransform();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user