Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 45 additions & 29 deletions src/ts-default/Components/Lanyard/Lanyard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable react/no-unknown-property */
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Canvas, extend, useFrame } from '@react-three/fiber';
import { Canvas, extend, useFrame, type ThreeElement, type ThreeEvent } from '@react-three/fiber';
import { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';
import {
BallCollider,
Expand All @@ -10,7 +10,8 @@ import {
RigidBody,
useRopeJoint,
useSphericalJoint,
RigidBodyProps
type RapierRigidBody,
type RigidBodyProps
} from '@react-three/rapier';
import { MeshLineGeometry, MeshLineMaterial } from 'meshline';
import * as THREE from 'three';
Expand All @@ -23,6 +24,13 @@ import './Lanyard.css';

extend({ MeshLineGeometry, MeshLineMaterial });

declare module '@react-three/fiber' {
interface ThreeElements {
meshLineGeometry: ThreeElement<typeof MeshLineGeometry>;
meshLineMaterial: ThreeElement<typeof MeshLineMaterial>;
}
}

// 1x1 transparent pixel — lets useTexture be called unconditionally when a
// front/back image isn't supplied.
const BLANK_PIXEL =
Expand Down Expand Up @@ -131,6 +139,10 @@ interface BandProps {
lanyardWidth?: number;
}

type LanyardRigidBody = RapierRigidBody & {
lerped?: THREE.Vector3;
};

function Band({
maxSpeed = 50,
minSpeed = 0,
Expand All @@ -141,27 +153,34 @@ function Band({
lanyardImage = null,
lanyardWidth = 1
}: BandProps) {
// Using "any" for refs since the exact types depend on Rapier's internals
const band = useRef<any>(null);
const fixed = useRef<any>(null);
const j1 = useRef<any>(null);
const j2 = useRef<any>(null);
const j3 = useRef<any>(null);
const card = useRef<any>(null);
const band = useRef<THREE.Mesh<InstanceType<typeof MeshLineGeometry>, InstanceType<typeof MeshLineMaterial>>>(null!);
const fixed = useRef<RapierRigidBody>(null!);
const j1 = useRef<LanyardRigidBody>(null!);
const j2 = useRef<LanyardRigidBody>(null!);
const j3 = useRef<RapierRigidBody>(null!);
const card = useRef<RapierRigidBody>(null!);

const vec = new THREE.Vector3();
const ang = new THREE.Vector3();
const rot = new THREE.Vector3();
const dir = new THREE.Vector3();

const segmentProps: any = {
type: 'dynamic' as RigidBodyProps['type'],
const segmentProps: RigidBodyProps = {
type: 'dynamic',
canSleep: true,
colliders: false,
angularDamping: 4,
linearDamping: 4
};

const getLerped = (body: LanyardRigidBody): THREE.Vector3 => {
if (!body.lerped) {
body.lerped = new THREE.Vector3().copy(body.translation());
}

return body.lerped;
};

const { nodes, materials } = useGLTF(cardGLB) as any;
const texture = useTexture(lanyardImage || lanyard);
// useTexture must be called unconditionally; use a blank pixel when an image
Expand Down Expand Up @@ -253,21 +272,18 @@ function Band({
}
if (fixed.current) {
[j1, j2].forEach(ref => {
if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());
const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));
ref.current.lerped.lerp(
ref.current.translation(),
delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))
);
const lerped = getLerped(ref.current);
const clampedDistance = Math.max(0.1, Math.min(1, lerped.distanceTo(ref.current.translation())));
lerped.lerp(ref.current.translation(), delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed)));
});
curve.points[0].copy(j3.current.translation());
curve.points[1].copy(j2.current.lerped);
curve.points[2].copy(j1.current.lerped);
curve.points[1].copy(getLerped(j2.current));
curve.points[2].copy(getLerped(j1.current));
curve.points[3].copy(fixed.current.translation());
band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));
ang.copy(card.current.angvel());
rot.copy(card.current.rotation());
card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });
card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z }, true);
}
});

Expand All @@ -277,34 +293,34 @@ function Band({
return (
<>
<group position={[0, 4, 0]}>
<RigidBody ref={fixed} {...segmentProps} type={'fixed' as RigidBodyProps['type']} />
<RigidBody position={[0.5, 0, 0]} ref={j1} {...segmentProps} type={'dynamic' as RigidBodyProps['type']}>
<RigidBody ref={fixed} {...segmentProps} type="fixed" />
<RigidBody position={[0.5, 0, 0]} ref={j1} {...segmentProps} type="dynamic">
<BallCollider args={[0.1]} />
</RigidBody>
<RigidBody position={[1, 0, 0]} ref={j2} {...segmentProps} type={'dynamic' as RigidBodyProps['type']}>
<RigidBody position={[1, 0, 0]} ref={j2} {...segmentProps} type="dynamic">
<BallCollider args={[0.1]} />
</RigidBody>
<RigidBody position={[1.5, 0, 0]} ref={j3} {...segmentProps} type={'dynamic' as RigidBodyProps['type']}>
<RigidBody position={[1.5, 0, 0]} ref={j3} {...segmentProps} type="dynamic">
<BallCollider args={[0.1]} />
</RigidBody>
<RigidBody
position={[2, 0, 0]}
ref={card}
{...segmentProps}
type={dragged ? ('kinematicPosition' as RigidBodyProps['type']) : ('dynamic' as RigidBodyProps['type'])}
type={dragged ? 'kinematicPosition' : 'dynamic'}
>
<CuboidCollider args={[0.8, 1.125, 0.01]} />
<group
scale={2.25}
position={[0, -1.2, -0.05]}
onPointerOver={() => hover(true)}
onPointerOut={() => hover(false)}
onPointerUp={(e: any) => {
e.target.releasePointerCapture(e.pointerId);
onPointerUp={(e: ThreeEvent<PointerEvent>) => {
(e.target as Element).releasePointerCapture(e.pointerId);
drag(false);
}}
onPointerDown={(e: any) => {
e.target.setPointerCapture(e.pointerId);
onPointerDown={(e: ThreeEvent<PointerEvent>) => {
(e.target as Element).setPointerCapture(e.pointerId);
drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())));
}}
>
Expand Down
74 changes: 45 additions & 29 deletions src/ts-tailwind/Components/Lanyard/Lanyard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable react/no-unknown-property */
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Canvas, extend, useFrame } from '@react-three/fiber';
import { Canvas, extend, useFrame, type ThreeElement, type ThreeEvent } from '@react-three/fiber';
import { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei';
import {
BallCollider,
Expand All @@ -10,7 +10,8 @@ import {
RigidBody,
useRopeJoint,
useSphericalJoint,
RigidBodyProps
type RapierRigidBody,
type RigidBodyProps
} from '@react-three/rapier';
import { MeshLineGeometry, MeshLineMaterial } from 'meshline';
import * as THREE from 'three';
Expand All @@ -21,6 +22,13 @@ import lanyard from './lanyard.png';

extend({ MeshLineGeometry, MeshLineMaterial });

declare module '@react-three/fiber' {
interface ThreeElements {
meshLineGeometry: ThreeElement<typeof MeshLineGeometry>;
meshLineMaterial: ThreeElement<typeof MeshLineMaterial>;
}
}

// 1x1 transparent pixel — lets useTexture be called unconditionally when a
// front/back image isn't supplied.
const BLANK_PIXEL =
Expand Down Expand Up @@ -129,6 +137,10 @@ interface BandProps {
lanyardWidth?: number;
}

type LanyardRigidBody = RapierRigidBody & {
lerped?: THREE.Vector3;
};

function Band({
maxSpeed = 50,
minSpeed = 0,
Expand All @@ -139,27 +151,34 @@ function Band({
lanyardImage = null,
lanyardWidth = 1
}: BandProps) {
// Using "any" for refs since the exact types depend on Rapier's internals
const band = useRef<any>(null);
const fixed = useRef<any>(null);
const j1 = useRef<any>(null);
const j2 = useRef<any>(null);
const j3 = useRef<any>(null);
const card = useRef<any>(null);
const band = useRef<THREE.Mesh<InstanceType<typeof MeshLineGeometry>, InstanceType<typeof MeshLineMaterial>>>(null!);
const fixed = useRef<RapierRigidBody>(null!);
const j1 = useRef<LanyardRigidBody>(null!);
const j2 = useRef<LanyardRigidBody>(null!);
const j3 = useRef<RapierRigidBody>(null!);
const card = useRef<RapierRigidBody>(null!);

const vec = new THREE.Vector3();
const ang = new THREE.Vector3();
const rot = new THREE.Vector3();
const dir = new THREE.Vector3();

const segmentProps: any = {
type: 'dynamic' as RigidBodyProps['type'],
const segmentProps: RigidBodyProps = {
type: 'dynamic',
canSleep: true,
colliders: false,
angularDamping: 4,
linearDamping: 4
};

const getLerped = (body: LanyardRigidBody): THREE.Vector3 => {
if (!body.lerped) {
body.lerped = new THREE.Vector3().copy(body.translation());
}

return body.lerped;
};

const { nodes, materials } = useGLTF(cardGLB) as any;
const texture = useTexture(lanyardImage || lanyard);
// useTexture must be called unconditionally; use a blank pixel when an image
Expand Down Expand Up @@ -251,21 +270,18 @@ function Band({
}
if (fixed.current) {
[j1, j2].forEach(ref => {
if (!ref.current.lerped) ref.current.lerped = new THREE.Vector3().copy(ref.current.translation());
const clampedDistance = Math.max(0.1, Math.min(1, ref.current.lerped.distanceTo(ref.current.translation())));
ref.current.lerped.lerp(
ref.current.translation(),
delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed))
);
const lerped = getLerped(ref.current);
const clampedDistance = Math.max(0.1, Math.min(1, lerped.distanceTo(ref.current.translation())));
lerped.lerp(ref.current.translation(), delta * (minSpeed + clampedDistance * (maxSpeed - minSpeed)));
});
curve.points[0].copy(j3.current.translation());
curve.points[1].copy(j2.current.lerped);
curve.points[2].copy(j1.current.lerped);
curve.points[1].copy(getLerped(j2.current));
curve.points[2].copy(getLerped(j1.current));
curve.points[3].copy(fixed.current.translation());
band.current.geometry.setPoints(curve.getPoints(isMobile ? 16 : 32));
ang.copy(card.current.angvel());
rot.copy(card.current.rotation());
card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z });
card.current.setAngvel({ x: ang.x, y: ang.y - rot.y * 0.25, z: ang.z }, true);
}
});

Expand All @@ -275,34 +291,34 @@ function Band({
return (
<>
<group position={[0, 4, 0]}>
<RigidBody ref={fixed} {...segmentProps} type={'fixed' as RigidBodyProps['type']} />
<RigidBody position={[0.5, 0, 0]} ref={j1} {...segmentProps} type={'dynamic' as RigidBodyProps['type']}>
<RigidBody ref={fixed} {...segmentProps} type="fixed" />
<RigidBody position={[0.5, 0, 0]} ref={j1} {...segmentProps} type="dynamic">
<BallCollider args={[0.1]} />
</RigidBody>
<RigidBody position={[1, 0, 0]} ref={j2} {...segmentProps} type={'dynamic' as RigidBodyProps['type']}>
<RigidBody position={[1, 0, 0]} ref={j2} {...segmentProps} type="dynamic">
<BallCollider args={[0.1]} />
</RigidBody>
<RigidBody position={[1.5, 0, 0]} ref={j3} {...segmentProps} type={'dynamic' as RigidBodyProps['type']}>
<RigidBody position={[1.5, 0, 0]} ref={j3} {...segmentProps} type="dynamic">
<BallCollider args={[0.1]} />
</RigidBody>
<RigidBody
position={[2, 0, 0]}
ref={card}
{...segmentProps}
type={dragged ? ('kinematicPosition' as RigidBodyProps['type']) : ('dynamic' as RigidBodyProps['type'])}
type={dragged ? 'kinematicPosition' : 'dynamic'}
>
<CuboidCollider args={[0.8, 1.125, 0.01]} />
<group
scale={2.25}
position={[0, -1.2, -0.05]}
onPointerOver={() => hover(true)}
onPointerOut={() => hover(false)}
onPointerUp={(e: any) => {
e.target.releasePointerCapture(e.pointerId);
onPointerUp={(e: ThreeEvent<PointerEvent>) => {
(e.target as Element).releasePointerCapture(e.pointerId);
drag(false);
}}
onPointerDown={(e: any) => {
e.target.setPointerCapture(e.pointerId);
onPointerDown={(e: ThreeEvent<PointerEvent>) => {
(e.target as Element).setPointerCapture(e.pointerId);
drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())));
}}
>
Expand Down