fix: 修正Live2D全页视线追踪范围
This commit is contained in:
parent
efd642c2ae
commit
3697014831
@ -1,67 +1,77 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { toCubism2ViewPoint } from '../../components/blog/live2d/runtime/cubism2PointerCoordinates'
|
||||
import {
|
||||
toCubism2ModelPoint,
|
||||
toCubism2PageTarget,
|
||||
} from '../../components/blog/live2d/runtime/cubism2PointerCoordinates'
|
||||
|
||||
describe('Cubism2 pointer coordinates', () => {
|
||||
it('preserves source coordinates for points inside the canvas', () => {
|
||||
const canvas = {
|
||||
getBoundingClientRect: () => ({
|
||||
bottom: 270,
|
||||
height: 250,
|
||||
left: 10,
|
||||
right: 290,
|
||||
top: 20,
|
||||
width: 280,
|
||||
x: 10,
|
||||
y: 20,
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
height: 500,
|
||||
width: 560,
|
||||
}
|
||||
const canvas = {
|
||||
getBoundingClientRect: () => ({
|
||||
bottom: 720,
|
||||
height: 250,
|
||||
left: 0,
|
||||
right: 280,
|
||||
top: 470,
|
||||
width: 280,
|
||||
x: 0,
|
||||
y: 470,
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
height: 500,
|
||||
width: 560,
|
||||
}
|
||||
|
||||
expect(toCubism2ViewPoint(canvas, { clientX: 10, clientY: 20 })).toEqual({
|
||||
it('preserves source canvas-local coordinates for model hit testing', () => {
|
||||
expect(toCubism2ModelPoint(canvas, { clientX: 0, clientY: 470 })).toEqual({
|
||||
x: -1,
|
||||
y: 500 / 560,
|
||||
})
|
||||
expect(toCubism2ViewPoint(canvas, { clientX: 290, clientY: 270 })).toEqual({
|
||||
expect(toCubism2ModelPoint(canvas, { clientX: 280, clientY: 720 })).toEqual({
|
||||
x: 1,
|
||||
y: -500 / 560,
|
||||
})
|
||||
expect(toCubism2ViewPoint(canvas, { clientX: 150, clientY: 145 })).toEqual({
|
||||
expect(toCubism2ModelPoint(canvas, { clientX: 140, clientY: 595 })).toEqual({
|
||||
x: 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,
|
||||
}
|
||||
it('uses the whole viewport as a continuous page-level look-at range', () => {
|
||||
const viewport = { innerHeight: 720, innerWidth: 1280 }
|
||||
|
||||
expect(toCubism2ViewPoint(canvas, { clientX: 1_000, clientY: 145 })).toEqual({
|
||||
expect(toCubism2PageTarget(canvas, viewport, { clientX: 140, clientY: 595 })).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
expect(toCubism2PageTarget(canvas, viewport, { clientX: 0, clientY: 595 })).toEqual({
|
||||
x: -1,
|
||||
y: 0,
|
||||
})
|
||||
expect(toCubism2PageTarget(canvas, viewport, { clientX: 1280, clientY: 595 })).toEqual({
|
||||
x: 1,
|
||||
y: 0,
|
||||
})
|
||||
expect(toCubism2ViewPoint(canvas, { clientX: 150, clientY: -1_000 })).toEqual({
|
||||
expect(toCubism2PageTarget(canvas, viewport, { clientX: 140, clientY: 0 })).toEqual({
|
||||
x: 0,
|
||||
y: 500 / 560,
|
||||
})
|
||||
expect(toCubism2ViewPoint(canvas, { clientX: 710, clientY: -355 })).toEqual({
|
||||
x: 1,
|
||||
y: 500 / 560,
|
||||
expect(toCubism2PageTarget(canvas, viewport, { clientX: 140, clientY: 720 })).toEqual({
|
||||
x: 0,
|
||||
y: -500 / 560,
|
||||
})
|
||||
})
|
||||
|
||||
it('retains page-level distance after the pointer leaves the canvas', () => {
|
||||
const viewport = { innerHeight: 720, innerWidth: 1280 }
|
||||
const near = toCubism2PageTarget(canvas, viewport, { clientX: 300, clientY: 595 })
|
||||
const middle = toCubism2PageTarget(canvas, viewport, { clientX: 640, clientY: 595 })
|
||||
const far = toCubism2PageTarget(canvas, viewport, { clientX: 1200, clientY: 595 })
|
||||
|
||||
expect(near.x).toBeCloseTo(160 / 1140)
|
||||
expect(middle.x).toBeCloseTo(500 / 1140)
|
||||
expect(far.x).toBeCloseTo(1060 / 1140)
|
||||
expect(near.x).toBeLessThan(middle.x)
|
||||
expect(middle.x).toBeLessThan(far.x)
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,16 +4,41 @@ export interface Cubism2ViewPoint {
|
||||
}
|
||||
type Cubism2PointerCanvas = Pick<HTMLCanvasElement, 'height' | 'width' | 'getBoundingClientRect'>;
|
||||
type Cubism2ClientPoint = Pick<MouseEvent, 'clientX' | 'clientY'>;
|
||||
type Cubism2PointerViewport = Pick<Window, 'innerHeight' | 'innerWidth'>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Converts a canvas interaction into the source Cubism2 model-view coordinates used for hit testing.
|
||||
* @param canvas Canvas whose bounds and drawing-buffer dimensions define the model-view transform.
|
||||
* @param point Browser client coordinates from a mouse or touch event.
|
||||
* @returns View point projected from the model center to the canvas boundary when needed.
|
||||
* @returns Source view point where both axes use the canvas width as the scale denominator.
|
||||
*/
|
||||
export function toCubism2ViewPoint(
|
||||
export function toCubism2ModelPoint(
|
||||
canvas: Cubism2PointerCanvas,
|
||||
point: Cubism2ClientPoint,
|
||||
): Cubism2ViewPoint {
|
||||
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 coordinateScale = Math.max(canvas.width, 1);
|
||||
return {
|
||||
x: (deviceX - canvas.width / 2) * 2 / coordinateScale,
|
||||
y: (canvas.height / 2 - deviceY) * 2 / coordinateScale,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps page-wide pointer movement continuously into the Cubism2 look-at range around the model center.
|
||||
* @param canvas Canvas whose center is the neutral look-at origin and whose aspect ratio limits vertical motion.
|
||||
* @param viewport Browser viewport whose edges represent the maximum look-at targets in each direction.
|
||||
* @param point Browser client coordinates from a page-level pointer event.
|
||||
* @returns Bounded look-at target that retains distance across the whole visible page.
|
||||
*/
|
||||
export function toCubism2PageTarget(
|
||||
canvas: Cubism2PointerCanvas,
|
||||
viewport: Cubism2PointerViewport,
|
||||
point: Cubism2ClientPoint,
|
||||
): Cubism2ViewPoint {
|
||||
const bounds = canvas.getBoundingClientRect();
|
||||
const cssWidth = Math.max(bounds.width, 1);
|
||||
@ -22,20 +47,20 @@ export function toCubism2ViewPoint(
|
||||
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 horizontalDistanceToViewportEdge = Math.max(
|
||||
deltaX < 0 ? centerX : viewport.innerWidth - centerX,
|
||||
1,
|
||||
);
|
||||
const verticalDistanceToViewportEdge = Math.max(
|
||||
deltaY < 0 ? centerY : viewport.innerHeight - centerY,
|
||||
1,
|
||||
);
|
||||
const verticalRange = canvas.height / Math.max(canvas.width, 1);
|
||||
const normalizedY = deltaY === 0
|
||||
? 0
|
||||
: -deltaY / verticalDistanceToViewportEdge * verticalRange;
|
||||
return {
|
||||
x: (deviceX - canvas.width / 2) * 2 / coordinateScale,
|
||||
y: (canvas.height / 2 - deviceY) * 2 / coordinateScale,
|
||||
x: Math.max(-1, Math.min(1, deltaX / horizontalDistanceToViewportEdge)),
|
||||
y: Math.max(-verticalRange, Math.min(verticalRange, normalizedY)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,10 @@ import {
|
||||
type Cubism2ModelAnimator,
|
||||
} from './cubism2ModelAnimator';
|
||||
import { configureCubism2ModelProjection } from './cubism2ModelProjection';
|
||||
import { toCubism2ViewPoint } from './cubism2PointerCoordinates';
|
||||
import {
|
||||
toCubism2ModelPoint,
|
||||
toCubism2PageTarget,
|
||||
} from './cubism2PointerCoordinates';
|
||||
import type { Live2DCoreModel, Live2DRendererAdapter, Live2DResolvedState } from './live2dRuntimeTypes';
|
||||
import { installCubism2WebGLTextureReleaseHook } from '../vendor/cubism2Core/compatibility/webglTextureRelease';
|
||||
|
||||
@ -104,8 +107,8 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
||||
* Updates the smoothed look-at target from a page-level pointer.
|
||||
* @param event Mouse or touch coordinates in the browser client space.
|
||||
*/
|
||||
const updatePointerTarget = (event: Pick<MouseEvent, 'clientX' | 'clientY'>) => {
|
||||
const point = toCubism2ViewPoint(canvas, event);
|
||||
const updatePagePointerTarget = (event: Pick<MouseEvent, 'clientX' | 'clientY'>) => {
|
||||
const point = toCubism2PageTarget(canvas, window, event);
|
||||
animator?.setPointerTarget(point.x, point.y);
|
||||
};
|
||||
|
||||
@ -123,8 +126,7 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const point = toCubism2ViewPoint(canvas, event);
|
||||
animator?.setPointerTarget(point.x, point.y);
|
||||
const point = toCubism2ModelPoint(canvas, event);
|
||||
void animator?.startMotionForPoint(point.x, point.y).catch((error: unknown) => {
|
||||
console.warn('[KT Blog] Live2D interaction motion failed.', error);
|
||||
});
|
||||
@ -135,7 +137,7 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
||||
* @param event Page-level mouse-move event.
|
||||
*/
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
updatePointerTarget(event);
|
||||
updatePagePointerTarget(event);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -150,7 +152,7 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
||||
const touch = event.touches[0];
|
||||
if (touch) {
|
||||
touchDragging = true;
|
||||
const point = toCubism2ViewPoint(canvas, touch);
|
||||
const point = toCubism2ModelPoint(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);
|
||||
@ -171,7 +173,8 @@ export function createWebGLLive2DRenderer(canvas: HTMLCanvasElement): Live2DRend
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
updatePointerTarget(touch);
|
||||
const point = toCubism2ModelPoint(canvas, touch);
|
||||
animator?.setPointerTarget(point.x, point.y);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user