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'
|
import { toCubism2ViewPoint } from '../../components/blog/live2d/runtime/cubism2PointerCoordinates'
|
||||||
|
|
||||||
describe('Cubism2 pointer coordinates', () => {
|
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 = {
|
const canvas = {
|
||||||
getBoundingClientRect: () => ({
|
getBoundingClientRect: () => ({
|
||||||
bottom: 270,
|
bottom: 270,
|
||||||
@ -33,4 +33,35 @@ describe('Cubism2 pointer coordinates', () => {
|
|||||||
y: 0,
|
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;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cubism2PointerCanvas = Pick<HTMLCanvasElement, 'height' | 'width' | 'getBoundingClientRect'>;
|
type Cubism2PointerCanvas = Pick<HTMLCanvasElement, 'height' | 'width' | 'getBoundingClientRect'>;
|
||||||
type Cubism2ClientPoint = Pick<MouseEvent, 'clientX' | 'clientY'>;
|
type Cubism2ClientPoint = Pick<MouseEvent, 'clientX' | 'clientY'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a browser pointer into the source runtime's canvas-local Cubism2 view coordinates.
|
* Converts a page-level browser pointer into the bounded WordPress Cubism2 view range.
|
||||||
* @param canvas Canvas whose drawing-buffer dimensions define `deviceToScreen`.
|
* @param canvas Canvas whose bounds and drawing-buffer dimensions define the view range.
|
||||||
* @param point Browser client coordinates from a mouse or touch event.
|
* @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(
|
export function toCubism2ViewPoint(
|
||||||
canvas: Cubism2PointerCanvas,
|
canvas: Cubism2PointerCanvas,
|
||||||
@ -19,8 +18,21 @@ export function toCubism2ViewPoint(
|
|||||||
const bounds = canvas.getBoundingClientRect();
|
const bounds = canvas.getBoundingClientRect();
|
||||||
const cssWidth = Math.max(bounds.width, 1);
|
const cssWidth = Math.max(bounds.width, 1);
|
||||||
const cssHeight = Math.max(bounds.height, 1);
|
const cssHeight = Math.max(bounds.height, 1);
|
||||||
const deviceX = (point.clientX - bounds.left) * canvas.width / cssWidth;
|
const centerX = bounds.left + cssWidth / 2;
|
||||||
const deviceY = (point.clientY - bounds.top) * canvas.height / cssHeight;
|
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);
|
const coordinateScale = Math.max(canvas.width, 1);
|
||||||
return {
|
return {
|
||||||
x: (deviceX - canvas.width / 2) * 2 / coordinateScale,
|
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.
|
* Updates the smoothed look-at target from a page-level pointer.
|
||||||
* @param event Canvas mouse move event.
|
* @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);
|
const point = toCubism2ViewPoint(canvas, event);
|
||||||
animator?.setPointerTarget(point.x, point.y);
|
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. */
|
/** Restores the source look-front behavior after the pointer leaves or is released. */
|
||||||
@ -126,16 +123,19 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
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.
|
* Preserves the WordPress widget's page-wide gaze tracking.
|
||||||
* @param event Canvas mouse-move event.
|
* @param event Page-level mouse-move event.
|
||||||
*/
|
*/
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
updatePointerTarget(event);
|
||||||
handleModelTurnHead(event);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,7 +150,11 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
|||||||
const touch = event.touches[0];
|
const touch = event.touches[0];
|
||||||
if (touch) {
|
if (touch) {
|
||||||
touchDragging = true;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const point = toCubism2ViewPoint(canvas, touch);
|
updatePointerTarget(touch);
|
||||||
animator?.setPointerTarget(point.x, point.y);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -192,20 +195,18 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
|||||||
activeTexture = null;
|
activeTexture = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseleave', handlePointerRelease);
|
||||||
canvas.addEventListener('mousedown', handleMouseDown);
|
canvas.addEventListener('mousedown', handleMouseDown);
|
||||||
canvas.addEventListener('mousemove', handleMouseMove);
|
|
||||||
canvas.addEventListener('mouseup', handlePointerRelease);
|
|
||||||
canvas.addEventListener('mouseout', handlePointerRelease);
|
|
||||||
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
|
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||||
canvas.addEventListener('touchend', handleTouchEnd, { passive: false });
|
canvas.addEventListener('touchend', handleTouchEnd, { passive: false });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseleave', handlePointerRelease);
|
||||||
canvas.removeEventListener('mousedown', handleMouseDown);
|
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
|
||||||
canvas.removeEventListener('mouseup', handlePointerRelease);
|
|
||||||
canvas.removeEventListener('mouseout', handlePointerRelease);
|
|
||||||
canvas.removeEventListener('touchstart', handleTouchStart);
|
canvas.removeEventListener('touchstart', handleTouchStart);
|
||||||
canvas.removeEventListener('touchmove', handleTouchMove);
|
canvas.removeEventListener('touchmove', handleTouchMove);
|
||||||
canvas.removeEventListener('touchend', handleTouchEnd);
|
canvas.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user