From efd642c2ae3cff031d829d38122419b960b3712d Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 15 Jul 2026 21:46:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DLive2D=E5=85=A8?= =?UTF-8?q?=E9=A1=B5=E8=A7=86=E7=BA=BF=E8=BF=BD=E8=B8=AA=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../live2d/Cubism2PointerCoordinates.spec.ts | 33 ++++++++++++++- .../live2d/Cubism2PointerTracking.spec.ts | 32 +++++++++++++++ .../runtime/cubism2PointerCoordinates.ts | 24 ++++++++--- .../live2d/runtime/webglLive2DRenderer.ts | 41 ++++++++++--------- 4 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 src/__tests__/live2d/Cubism2PointerTracking.spec.ts diff --git a/src/__tests__/live2d/Cubism2PointerCoordinates.spec.ts b/src/__tests__/live2d/Cubism2PointerCoordinates.spec.ts index 88b286a..02b9134 100644 --- a/src/__tests__/live2d/Cubism2PointerCoordinates.spec.ts +++ b/src/__tests__/live2d/Cubism2PointerCoordinates.spec.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import { toCubism2ViewPoint } from '../../components/blog/live2d/runtime/cubism2PointerCoordinates' describe('Cubism2 pointer coordinates', () => { - it('uses canvas-local coordinates and canvas width for both source axes', () => { + it('preserves source coordinates for points inside the canvas', () => { const canvas = { getBoundingClientRect: () => ({ bottom: 270, @@ -33,4 +33,35 @@ describe('Cubism2 pointer coordinates', () => { y: 0, }) }) + + it('projects page-level pointers onto the canvas boundary around the model center', () => { + const canvas = { + getBoundingClientRect: () => ({ + bottom: 270, + height: 250, + left: 10, + right: 290, + top: 20, + width: 280, + x: 10, + y: 20, + toJSON: () => ({}), + }), + height: 500, + width: 560, + } + + expect(toCubism2ViewPoint(canvas, { clientX: 1_000, clientY: 145 })).toEqual({ + x: 1, + y: 0, + }) + expect(toCubism2ViewPoint(canvas, { clientX: 150, clientY: -1_000 })).toEqual({ + x: 0, + y: 500 / 560, + }) + expect(toCubism2ViewPoint(canvas, { clientX: 710, clientY: -355 })).toEqual({ + x: 1, + y: 500 / 560, + }) + }) }) diff --git a/src/__tests__/live2d/Cubism2PointerTracking.spec.ts b/src/__tests__/live2d/Cubism2PointerTracking.spec.ts new file mode 100644 index 0000000..db0916f --- /dev/null +++ b/src/__tests__/live2d/Cubism2PointerTracking.spec.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { createWebGLLive2DRenderer } from '../../components/blog/live2d/runtime/webglLive2DRenderer' + +describe('Cubism2 page-level pointer tracking', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('tracks mouse movement across the page and removes every listener on destroy', () => { + const canvas = document.createElement('canvas') + const windowAdd = vi.spyOn(window, 'addEventListener') + const windowRemove = vi.spyOn(window, 'removeEventListener') + const documentAdd = vi.spyOn(document, 'addEventListener') + const documentRemove = vi.spyOn(document, 'removeEventListener') + const canvasAdd = vi.spyOn(canvas, 'addEventListener') + + const renderer = createWebGLLive2DRenderer(canvas) + + expect(windowAdd).toHaveBeenCalledWith('mousemove', expect.any(Function)) + expect(documentAdd).toHaveBeenCalledWith('mouseleave', expect.any(Function)) + expect(canvasAdd).not.toHaveBeenCalledWith('mousemove', expect.any(Function)) + expect(canvasAdd).not.toHaveBeenCalledWith('mouseout', expect.any(Function)) + const mouseMoveHandler = windowAdd.mock.calls.find(([type]) => type === 'mousemove')?.[1] + const mouseLeaveHandler = documentAdd.mock.calls.find(([type]) => type === 'mouseleave')?.[1] + + renderer.destroy() + + expect(windowRemove).toHaveBeenCalledWith('mousemove', mouseMoveHandler) + expect(documentRemove).toHaveBeenCalledWith('mouseleave', mouseLeaveHandler) + }) +}) diff --git a/src/components/blog/live2d/runtime/cubism2PointerCoordinates.ts b/src/components/blog/live2d/runtime/cubism2PointerCoordinates.ts index aecf958..da31153 100644 --- a/src/components/blog/live2d/runtime/cubism2PointerCoordinates.ts +++ b/src/components/blog/live2d/runtime/cubism2PointerCoordinates.ts @@ -2,15 +2,14 @@ export interface Cubism2ViewPoint { x: number; y: number; } - type Cubism2PointerCanvas = Pick; type Cubism2ClientPoint = Pick; /** - * Converts a browser pointer into the source runtime's canvas-local Cubism2 view coordinates. - * @param canvas Canvas whose drawing-buffer dimensions define `deviceToScreen`. + * Converts a page-level browser pointer into the bounded WordPress Cubism2 view range. + * @param canvas Canvas whose bounds and drawing-buffer dimensions define the view range. * @param point Browser client coordinates from a mouse or touch event. - * @returns Source view point where both axes use canvas width as the scale denominator. + * @returns View point projected from the model center to the canvas boundary when needed. */ export function toCubism2ViewPoint( canvas: Cubism2PointerCanvas, @@ -19,8 +18,21 @@ export function toCubism2ViewPoint( const bounds = canvas.getBoundingClientRect(); const cssWidth = Math.max(bounds.width, 1); const cssHeight = Math.max(bounds.height, 1); - const deviceX = (point.clientX - bounds.left) * canvas.width / cssWidth; - const deviceY = (point.clientY - bounds.top) * canvas.height / cssHeight; + const centerX = bounds.left + cssWidth / 2; + const centerY = bounds.top + cssHeight / 2; + const deltaX = point.clientX - centerX; + const deltaY = point.clientY - centerY; + const horizontalProjection = deltaX === 0 + ? Number.POSITIVE_INFINITY + : cssWidth / 2 / Math.abs(deltaX); + const verticalProjection = deltaY === 0 + ? Number.POSITIVE_INFINITY + : cssHeight / 2 / Math.abs(deltaY); + const projectionScale = Math.min(1, horizontalProjection, verticalProjection); + const projectedX = centerX + deltaX * projectionScale; + const projectedY = centerY + deltaY * projectionScale; + const deviceX = (projectedX - bounds.left) * canvas.width / cssWidth; + const deviceY = (projectedY - bounds.top) * canvas.height / cssHeight; const coordinateScale = Math.max(canvas.width, 1); return { x: (deviceX - canvas.width / 2) * 2 / coordinateScale, diff --git a/src/components/blog/live2d/runtime/webglLive2DRenderer.ts b/src/components/blog/live2d/runtime/webglLive2DRenderer.ts index 977cc0f..bb80420 100644 --- a/src/components/blog/live2d/runtime/webglLive2DRenderer.ts +++ b/src/components/blog/live2d/runtime/webglLive2DRenderer.ts @@ -101,15 +101,12 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend }; /** - * Tracks the mouse in the source canvas-local coordinate space. - * @param event Canvas mouse move event. + * Updates the smoothed look-at target from a page-level pointer. + * @param event Mouse or touch coordinates in the browser client space. */ - const handleModelTurnHead = (event: Pick) => { + const updatePointerTarget = (event: Pick) => { const point = toCubism2ViewPoint(canvas, event); animator?.setPointerTarget(point.x, point.y); - void animator?.startMotionForPoint(point.x, point.y).catch((error: unknown) => { - console.warn('[KT Blog] Live2D interaction motion failed.', error); - }); }; /** Restores the source look-front behavior after the pointer leaves or is released. */ @@ -126,16 +123,19 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend return; } event.preventDefault(); - handleModelTurnHead(event); + const point = toCubism2ViewPoint(canvas, event); + animator?.setPointerTarget(point.x, point.y); + void animator?.startMotionForPoint(point.x, point.y).catch((error: unknown) => { + console.warn('[KT Blog] Live2D interaction motion failed.', error); + }); }; /** - * Handles the source hover behavior, including hit-motion attempts. - * @param event Canvas mouse-move event. + * Preserves the WordPress widget's page-wide gaze tracking. + * @param event Page-level mouse-move event. */ const handleMouseMove = (event: MouseEvent) => { - event.preventDefault(); - handleModelTurnHead(event); + updatePointerTarget(event); }; /** @@ -150,7 +150,11 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend const touch = event.touches[0]; if (touch) { touchDragging = true; - handleModelTurnHead(touch); + const point = toCubism2ViewPoint(canvas, touch); + animator?.setPointerTarget(point.x, point.y); + void animator?.startMotionForPoint(point.x, point.y).catch((error: unknown) => { + console.warn('[KT Blog] Live2D interaction motion failed.', error); + }); } }; @@ -167,8 +171,7 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend return; } event.preventDefault(); - const point = toCubism2ViewPoint(canvas, touch); - animator?.setPointerTarget(point.x, point.y); + updatePointerTarget(touch); }; /** @@ -192,20 +195,18 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend activeTexture = null; }; + window.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseleave', handlePointerRelease); canvas.addEventListener('mousedown', handleMouseDown); - canvas.addEventListener('mousemove', handleMouseMove); - canvas.addEventListener('mouseup', handlePointerRelease); - canvas.addEventListener('mouseout', handlePointerRelease); canvas.addEventListener('touchstart', handleTouchStart, { passive: false }); canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); canvas.addEventListener('touchend', handleTouchEnd, { passive: false }); return { destroy() { + window.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseleave', handlePointerRelease); canvas.removeEventListener('mousedown', handleMouseDown); - canvas.removeEventListener('mousemove', handleMouseMove); - canvas.removeEventListener('mouseup', handlePointerRelease); - canvas.removeEventListener('mouseout', handlePointerRelease); canvas.removeEventListener('touchstart', handleTouchStart); canvas.removeEventListener('touchmove', handleTouchMove); canvas.removeEventListener('touchend', handleTouchEnd);