fix: 修复Live2D全页视线追踪范围
This commit is contained in:
parent
30bc26c031
commit
efd642c2ae
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
32
src/__tests__/live2d/Cubism2PointerTracking.spec.ts
Normal file
32
src/__tests__/live2d/Cubism2PointerTracking.spec.ts
Normal file
@ -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)
|
||||
})
|
||||
})
|
||||
@ -2,15 +2,14 @@ export interface Cubism2ViewPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
type Cubism2PointerCanvas = Pick<HTMLCanvasElement, 'height' | 'width' | 'getBoundingClientRect'>;
|
||||
type Cubism2ClientPoint = Pick<MouseEvent, 'clientX' | 'clientY'>;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
@ -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<MouseEvent, 'clientX' | 'clientY'>) => {
|
||||
const updatePointerTarget = (event: Pick<MouseEvent, 'clientX' | 'clientY'>) => {
|
||||
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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user