This commit is contained in:
G30 2026-10-13 17:38:06 -04:00 • committed by GitHub
commit 3ecd50ac2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 101 additions and 3 deletions

View file

@ -35,6 +35,52 @@
}
const dispatch = createEventDispatcher();
let canvas: HTMLCanvasElement;
const pointers = new Map<number, { x: number; y: number }>();
let moved = 0,
velocity = 0,
lastMove = 0,
lastTap = 0;
const pointerDown = (event: PointerEvent) => {
if (!renderer) return;
canvas.setPointerCapture(event.pointerId);
pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
if (pointers.size === 1) moved = velocity = 0;
};
const pointerMove = (event: PointerEvent) => {
const point = pointers.get(event.pointerId);
if (!point || !renderer) return;
const dx = event.clientX - point.x,
dy = event.clientY - point.y;
const other = [...pointers.values()].find((p) => p !== point);
const before = other ? Math.hypot(point.x - other.x, point.y - other.y) : 0;
point.x = event.clientX;
point.y = event.clientY;
moved += Math.hypot(dx, dy);
if (other) {
renderer.panView(dx / 2, dy / 2);
renderer.zoomView(before / Math.max(1, Math.hypot(point.x - other.x, point.y - other.y)));
velocity = 0;
} else if (event.buttons & 6 || event.shiftKey) {
renderer.panView(dx, dy);
velocity = 0;
} else {
renderer.rotateView(dx, dy);
velocity = velocity * 0.5 + (dx / Math.max(1, event.timeStamp - lastMove)) * 1000 * 0.5;
}
lastMove = event.timeStamp;
};
const pointerUp = (event: PointerEvent) => {
if (!pointers.delete(event.pointerId) || !renderer || pointers.size) return;
if (moved < 5) {
if (event.timeStamp - lastTap < 300) renderer.resetView();
lastTap = event.timeStamp;
} else if (velocity && event.timeStamp - lastMove < 50) renderer.spinView(velocity);
};
const wheel = (event: WheelEvent) => {
if (!renderer) return;
event.preventDefault();
renderer.zoomView(Math.exp(event.deltaY * (event.deltaMode ? 0.05 : 0.001)));
};
// The parent keys this component by file, keeping settings changes inexpensive.
onMount(() => {
@ -162,13 +208,30 @@
});
</script>
<canvas bind:this={canvas} aria-hidden="true" class="avatar-canvas"></canvas>
<canvas
bind:this={canvas}
aria-hidden="true"
class="avatar-canvas"
on:pointerdown={pointerDown}
on:pointermove={pointerMove}
on:pointerup={pointerUp}
on:pointercancel={pointerUp}
on:click={(event) => {
if (moved >= 5) event.stopPropagation();
}}
on:contextmenu|preventDefault
on:wheel|nonpassive={wheel}
></canvas>
<style>
.avatar-canvas {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
cursor: grab;
touch-action: none;
}
.avatar-canvas:active {
cursor: grabbing;
}
</style>

View file

@ -219,6 +219,11 @@ export async function createAvatarRenderer(
gestureElapsed = 0;
let enabled = true;
let cameraDistance = 0;
let yaw = 0,
pitch = 0,
spin = 0,
zoom = 1;
const pan = new THREE.Vector2();
const switchClip = (clip: THREE.AnimationClip, once: boolean) => {
if (outgoing) {
outgoing.stop();
@ -282,6 +287,30 @@ export async function createAvatarRenderer(
return 'started' as const;
},
cancelGesture,
rotateView(dx: number, dy: number) {
const scale = (2 * Math.PI) / Math.max(height, 1);
yaw -= dx * scale;
pitch = THREE.MathUtils.clamp(pitch + dy * scale, -1.3, 1.3);
spin = 0;
},
spinView(velocity: number) {
spin = (-velocity * 2 * Math.PI) / Math.max(height, 1);
},
panView(dx: number, dy: number) {
const scale =
(2 * cameraDistance * zoom * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2))) /
Math.max(height, 1);
pan.x -= dx * scale;
pan.y += dy * scale;
},
zoomView(scale: number) {
zoom = THREE.MathUtils.clamp(zoom * scale, 0.3, 3);
},
resetView() {
yaw = pitch = spin = 0;
zoom = 1;
pan.set(0, 0);
},
render(delta: number, config: VoiceAvatarConfig, input: AvatarSignals, reducedMotion: boolean) {
const dt = Math.min(delta, 0.05);
elapsed += dt;
@ -420,8 +449,14 @@ export async function createAvatarRenderer(
(Math.max(fitHeight, fitWidth / camera.aspect) * 1.2) / (2 * Math.tan(Math.PI / 12));
// Widen before a limb reaches the edge; never zoom in/out on each gesture.
cameraDistance = Math.max(distance, cameraDistance);
camera.position.set(center.x, center.y, center.z + cameraDistance);
spin = reducedMotion || Math.abs(spin) < 0.01 ? 0 : spin * Math.exp(-dt * 3);
yaw += spin * dt;
camera.position
.setFromSphericalCoords(cameraDistance * zoom, Math.PI / 2 - pitch, yaw)
.add(center);
camera.lookAt(center);
camera.translateX(pan.x);
camera.translateY(pan.y);
renderer.render(scene, camera);
},
dispose() {