惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 【当耐特】
V
Visual Studio Blog
有赞技术团队
有赞技术团队
U
Unit 42
D
Docker
小众软件
小众软件
F
Full Disclosure
I
Intezer
Scott Helme
Scott Helme
P
Privacy International News Feed
P
Proofpoint News Feed
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
B
Blog
Martin Fowler
Martin Fowler
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Spread Privacy
Spread Privacy
宝玉的分享
宝玉的分享
S
Security Affairs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
月光博客
月光博客
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
Schneier on Security
Schneier on Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threat Research - Cisco Blogs
量子位
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Heimdal Security Blog
N
Netflix TechBlog - Medium
H
Hacker News: Front Page
P
Proofpoint News Feed
G
GRAHAM CLULEY
V
Vulnerabilities – Threatpost
S
Schneier on Security

Vercel News

Vercel Open Source Program: Winter 2026 cohort How Notion Workers run untrusted code at scale with Vercel Sandbox How we run Vercel's CDN in front of Discourse From idea to secure checkout in minutes with Stripe Building Slack agents can be easy Scaling redirects to infinity on Vercel Advancing Python typing Gamma builds design-first agents with Vercel How Avalara turns pipe dreams into patent-pending with v0 Keeping community human while scaling with agents How OpenEvidence built a healthcare AI that physicians actually trust Security boundaries in agentic architectures Skills Night: 69,000+ ways agents are getting smarter Video Generation with AI Gateway We Ralph Wiggumed WebStreams to make them 10x faster How Stably ships AI testing agents in hours, not weeks How we built AEO tracking for coding agents Anyone can build agents, but it takes a platform to run them Introducing Geist Pixel The Vercel AI Accelerator is back with $6m in credits Making agent-friendly pages with content negotiation The Vercel OSS Bug Bounty program is now available Introducing the new v0 Run untrusted code with Vercel Sandbox, now generally available How Stripe built a game-changing app in a single flight with v0 How Sensay went from zero to product in six weeks AGENTS.md outperforms skills in our agent evals Agent skills explained: An FAQ Testing if "bash is all you need" AWS databases are now live on the Vercel Marketplace and v0 Use Perplexity Web Search with Vercel AI Gateway Introducing: React Best Practices Nick Bogaty joins Vercel as Chief Revenue Officer How Mux shipped durable video workflows with their @mux/ai SDK How to build agents with filesystems and bash How we made v0 an effective coding agent Stopping the slow death of internal tools Building AI-Generated Pixel Trading Cards with Vercel AI Gateway We removed 80% of our agent’s tools AI SDK 6 Our $1 million hacker challenge for React2Shell Cline now runs on Vercel AI Gateway How to prompt v0 Build smarter workflows with Notion and v0 Vercel launches partner certification Inside Workflow DevKit: How framework integrations work React2Shell Security Bulletin | Vercel Knowledge Base Billions of requests: Black Friday-Cyber Monday 2025 Investing in the Python ecosystem AWS Databases coming to the Vercel Marketplace How we built the v0 iOS app Workflow Builder: Build your own workflow automation platform Vercel Open Source Program: Fall 2025 cohort Self-driving infrastructure Vercel collaborates with Google for Gemini 3 Pro Preview launch Vercel: The anti-vendor-lock-in cloud How Nous Research used BotID to block automated abuse at scale How AI Gateway runs on Fluid compute What we learned building agents at Vercel Build and deploy data applications on Snowflake with v0 BotID Deep Analysis catches a sophisticated bot network in real-time Vercel achieves TISAX AL2 compliance to serve automotive partners Bun runtime on Vercel Functions David Totten Joins Vercel to Lead Global Field Engineering Vercel Ship AI 2025 recap You can just ship agents AI agents and services on the Vercel Marketplace Built-in durability: Introducing Workflow Development Kit Zero-config backends on Vercel AI Cloud Introducing Vercel Agent: Your new Vercel teammate Update regarding Vercel service disruption on October 20, 2025 Agents at work, a partnership with Salesforce and Slack Running Next.js in ChatGPT: How to Build ChatGPT Apps Talha Tariq joins Vercel as CTO of Security Just another (Black) Friday Server rendering benchmarks: Fluid Compute and Cloudflare Workers Towards the AI Cloud: Our Series F Collaborating with Anthropic on Claude Sonnet 4.5 to power intelligent coding agents Preventing the stampede: Request collapsing in the Vercel CDN BotID uncovers hidden SEO poisoning How we made global routing faster with Bloom filters What you need to know about vibe coding Scale to one: How Fluid solves cold starts Addressing security & quality issues with MCP tools - Vercel AI agents at scale: Rox’s Vercel-powered revenue operating system Agentic Infrastructure Zero Data Retention on AI Gateway Optimizing Vercel Sandbox snapshots How Waldium made a blog platform work for humans and AI alike How FLORA shipped a creative agent on Vercel's AI stack Agent responsibly Making Turborepo 96% faster with agents, sandboxes, and humans Unified reporting for all AI Gateway usage new.website joins forces with v0 SERHANT.'s playbook for rapid AI iteration Two startups at global scale without DevOps Chat SDK brings agents to your users 360 billion tokens, 3 million customers, 6 engineers Meet the 2026 Vercel AI Accelerator Cohort Build knowledge agents without embeddings
Building an interactive 3D event badge with React Three Fiber - Vercel – Vercel
Paul Henschel · 2024-04-17 · via Vercel News

5 min read

In this post, we’ll look at how we made the dropping lanyard for the Vercel Ship 2024 site, diving into the inspiration, tech stack, and code behind the finished product.

Link to headingInspiration

We’ve shared digital tickets for event attendance in the past, but this time, we wanted to take it one step further by creating a tangible experience.

When Studio Basement made a video in Blender that depicted a virtual badge dropping down, we liked the idea of it so much that we started to wonder whether we could make it interactive and run it in the browser.

Ultimately, we wanted a highly shareable element that rewards the user for signing up and makes it worth their time.

Link to headingThe stack

To accomplish the task, we chose the following stack:

While some of the concepts we’re about to cover may not look familiar, don’t let them overwhelm you. The implementation is about 80 lines of mostly declarative code, with a sprinkle of math.

Check out this sandbox first to get an idea of what we’re building:

import * as THREE from 'three'
import { useRef, useState } from 'react'
import { Canvas, extend, useThree, useFrame } from '@react-three/fiber'
import { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier'
import { MeshLineGeometry, MeshLineMaterial } from 'meshline'

extend({ MeshLineGeometry, MeshLineMaterial })

export default function App() {
  return (
    <Canvas camera={{ position: [0, 0, 13], fov: 25 }}>
      <Physics debug interpolate gravity={[0, -40, 0]} timeStep={1 / 60}>
        <Band />
      </Physics>
    </Canvas>
  )
}

function Band() {
  const band = useRef(), fixed = useRef(), j1 = useRef(), j2 = useRef(), j3 = useRef(), card = useRef() // prettier-ignore
  const vec = new THREE.Vector3(), ang = new THREE.Vector3(), rot = new THREE.Vector3(), dir = new THREE.Vector3() // prettier-ignore
  const { width, height } = useThree((state) => state.size)
  const [curve] = useState(() => new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]))
  const [dragged, drag] = useState(false)

  useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]) // prettier-ignore
  useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]) // prettier-ignore
  useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]) // prettier-ignore
  useSphericalJoint(j3, card, [[0, 0, 0], [0, 1.45, 0]]) // prettier-ignore

  useFrame((state, delta) => {
    if (dragged) {
      vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera)
      dir.copy(vec).sub(state.camera.position).normalize()
      vec.add(dir.multiplyScalar(state.camera.position.length()))
        ;[card, j1, j2, j3, fixed].forEach((ref) => ref.current?.wakeUp())
      card.current?.setNextKinematicTranslation({ x: vec.x - dragged.x, y: vec.y - dragged.y, z: vec.z - dragged.z })
    }
    if (fixed.current) {
      // Calculate catmul curve      
      curve.points[0].copy(j3.current.translation())
      curve.points[1].copy(j2.current.translation())
      curve.points[2].copy(j1.current.translation())
      curve.points[3].copy(fixed.current.translation())
      band.current.geometry.setPoints(curve.getPoints(32))
      // Tilt it back towards the screen
      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 })
    }
  })

  return (
    <>
      <group position={[0, 4, 0]}>
        <RigidBody ref={fixed} angularDamping={2} linearDamping={2} type="fixed" />
        <RigidBody position={[0.5, 0, 0]} ref={j1} angularDamping={2} linearDamping={2}>
          <BallCollider args={[0.1]} />
        </RigidBody>
        <RigidBody position={[1, 0, 0]} ref={j2} angularDamping={2} linearDamping={2}>
          <BallCollider args={[0.1]} />
        </RigidBody >
        <RigidBody position={[1.5, 0, 0]} ref={j3} angularDamping={2} linearDamping={2}>
          <BallCollider args={[0.1]} />
        </RigidBody >
        <RigidBody position={[2, 0, 0]} ref={card} angularDamping={2} linearDamping={2} type={dragged ? 'kinematicPosition' : 'dynamic'} >
          <CuboidCollider args={[0.8, 1.125, 0.01]} />
          <mesh
            onPointerUp={(e) => (e.target.releasePointerCapture(e.pointerId), drag(false))}
            onPointerDown={(e) => (e.target.setPointerCapture(e.pointerId), drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation()))))}>
            <planeGeometry args={[0.8 * 2, 1.125 * 2]} />
            <meshBasicMaterial transparent opacity={0.25} color="white" side={THREE.DoubleSide} />
          </mesh>
        </RigidBody >
      </group >
      <mesh ref={band}>
        <meshLineGeometry />
        <meshLineMaterial transparent opacity={0.25} color="white" depthTest={false} resolution={[width, height]} lineWidth={1} />
      </mesh>
    </>
  )
}

Link to headingBuilding a rough draft

The basic imports we need revolve around our canvas, physics, and the thick line for the lanyard:

App.js

import * as THREE from 'three'

import { useRef, useState } from 'react'

import { Canvas, extend, useThree, useFrame } from '@react-three/fiber'

import { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier'

import { MeshLineGeometry, MeshLineMaterial } from 'meshline'

In order to use the MeshLine library, which is vanilla Three.js in React, we need to extend it. The extend function extends React Three Fiber's catalog of known JSX elements. Components added this way can then be referenced in the scene graph using camel casing, similar to native primitives (e.g., <mesh>):

App.js

extend({ MeshLineGeometry, MeshLineMaterial })

Link to headingSetting up the canvas

Now we can set up a basic canvas. We need React Three Fiber’s <Canvas> component, which is a doorway into declarative Three.js. We also add a <Physics> provider, which allows us to tie shapes to physics; in Rapier, this is called a <RigidBody>.

With this, we have everything we need:

App.js

export default function App() {

return (

<Canvas>

<Physics>

{/* ... */}

</Physics>

</Canvas>

)

}

Link to headingThe band component

Now let’s make the band happen. We need a couple of references to access them later on. The canvas size is important for meshline, and a THREE.CatmullRomCurve3 helps us to calculate a smooth curve with just a few points. We only need four points for the physics joints:

App.js

function Band() {

// References for the band and the joints

const band = useRef()

const fixed = useRef()

const j1 = useRef()

const j2 = useRef()

const j3 = useRef()

// Canvas size

const { width, height } = useThree((state) => state.size)

// A Catmull-Rom curve

const [curve] = useState(() => new THREE.CatmullRomCurve3([

new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()

]))

Link to headingDefining the physics joints

A joint is a physics constraint that tells the engine how shapes interact with one another. We’ll now start to connect the joints, and we’ll later define a fixed <RigidBody> that cannot move.

We hang the first joint on the useRopeJoint the Rapier provides (there are a lot of different constraints for rotations, distances, etc.). The other joints will hang on each other. Basically, we will have made a chain that hangs on a fixed point.

useRopeJoint requires two <RigidBody> references:

  1. Two anchor points for each (we’re using [0, 0, 0], which is the center point)

  2. A length (we’re using 1)

Our rope is ready to swing!

App.js

useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1])

useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1])

useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1])

Link to headingCreating a curve

Rapier will now move the joints along an invisible rope, and we can feed our Catmull-Rom curve the positions of these joints. We let it make a smooth, interpolated curve with 32 points and forward that to the meshline.

We do this at runtime at 60 or 120 FPS, depending on the monitor’s refresh rate. React Three Fiber gives us an out to handle frame-based animations with the useFrame Hook:

App.js

useFrame(() => {

curve.points[0].copy(j3.current.translation())

curve.points[1].copy(j2.current.translation())

curve.points[2].copy(j1.current.translation())

curve.points[3].copy(fixed.current.translation())

band.current.geometry.setPoints(curve.getPoints(32))

})

Link to headingThe view

Now we need the view. It consists of the fixed <RigidBody type="fixed">, three <RigidBody>'s for the joints (j1, j2, and j3), and the meshline that we extended above. The joints are positioned in a way that makes them fall down with a slight swing:

App.js

return (

<>

<RigidBody ref={fixed} type="fixed" />

<RigidBody position={[0.5, 0, 0]} ref={j1}>

<BallCollider args={[0.1]} />

</RigidBody>

<RigidBody position={[1, 0, 0]} ref={j2}>

<BallCollider args={[0.1]} />

</RigidBody >

<RigidBody position={[1.5, 0, 0]} ref={j3}>

<BallCollider args={[0.1]} />

</RigidBody >

<mesh ref={band}>

<meshLineGeometry />

<meshLineMaterial color="white" resolution={[width, height]} lineWidth={1} />

</mesh>

</>

)

}

Link to headingThe card component

All that’s missing is the interactive card, which we need to attach to the end of the last joint. For this, we’ll need a new reference, some variables for math, a state for dragging, and a new joint. This time, we use a spherical joint so the card can rotate:

App.js

const card = useRef()

const vec = new THREE.Vector3()

const ang = new THREE.Vector3()

const rot = new THREE.Vector3()

const dir = new THREE.Vector3()

const [dragged, drag] = useState(false)

useSphericalJoint(j3, card, [[0, 0, 0], [0, 1.45, 0]])

Rapier defines a few rigid-body types:

  • fixed, which isn’t affected by anything

  • dynamic, the default, which reacts to any other rigid body

  • kinematicPosition, which is the position controlled by the user, not the engine

The card needs to be kinematic when dragged and dynamic when it’s not. We will later use pointerEvents to set the dragged state.

Our previous useFrame now changes to this:

App.js

useFrame((state) => {

if (dragged) {

vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera)

dir.copy(vec).sub(state.camera.position).normalize()

vec.add(dir.multiplyScalar(state.camera.position.length()))

card.current.setNextKinematicTranslation({ x: vec.x - dragged.x, y: vec.y - dragged.y, z: vec.z - dragged.z })

}

// Calculate Catmull curve

curve.points[0].copy(j3.current.translation())

curve.points[1].copy(j2.current.translation())

curve.points[2].copy(j1.current.translation())

curve.points[3].copy(fixed.current.translation())

band.current.geometry.setPoints(curve.getPoints(32))

// Tilt the card back towards the screen

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 })

})

Calculating the dragged state is the complicated bit of the code. Without going into too much detail, if you want to translate a pointer event coordinate to a 3D object, this is called a camera unprojection. Three.js has a method for this, unproject(state.camera), which does most of the math.

The obtained vector gets applied as a kinematic translation. We move the card with the mouse/trackpad, and the lanyard joints will follow it where it goes.

Another hard nut to crack is that we allow the card to rotate, but we want it to always rotate from back to front—which is not physically accurate, of course, but the experience would suffer otherwise. To solve this, we use the current rotational velocity card.current.angvel() and the rotation card.current.rotation(), and spin the y-axis towards the front.

Link to headingThe card’s rigid body and pointer events

We use a <CuboidCollider> (a box shape) for the card and drop a <mesh> inside that will move along with the <RigidBody>. This mesh will later be exchanged with the Blender model.

The pointer events for up and down set the drag state. On the down point, we grab the current point of the model, e.point, and subtract the card’s position in space, card.current.translation(). We need this offset for the useFrame above to calculate the correct kinematic position:

App.js

<RigidBody ref={card} type={dragged ? 'kinematicPosition' : 'dynamic'} >

<CuboidCollider args={[0.8, 1.125, 0.01]} />

<mesh

onPointerUp={(e) => drag(false)}

onPointerDown={(e) => drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation())))}>

<planeGeometry args={[0.8 * 2, 1.125 * 2]} />

<meshBasicMaterial color="white" side={THREE.DoubleSide} />

</mesh>

</RigidBody>

Link to headingAdding the dynamic name

We wanted the card to display the name of the user dynamically. To achieve that, we’ll create a new scene that renders the user's name alongside a base texture. Then, we’ll use Drei's <RenderTexture> component to render that scene into a texture.

We start by creating a scene that renders the base of the badge texture:

App.js

<PerspectiveCamera makeDefault manual aspect={1.05} position={[0.49, 0.22, 2]} />

<mesh>

<planeGeometry args={[planeWidth, -planeWidth / textureAspect]} />

<meshBasicMaterial transparent alphaMap={texture} side={THREE.BackSide} />

</mesh>

The result of using Drei's <RenderTexture> component to render our badge texture.

The result of using Drei's <RenderTexture> component to render our badge texture.

We have the badge texture, but we’re still missing the name. We’ll add it to the scene using Drei's <Text3D> component:

App.js

<Center bottom right>

<Resize key={resizeId} maxHeight={0.45} maxWidth={0.925}>

<Text3D

bevelEnabled={false}

bevelSize={0}

font="/ship/2024/badge/Geist_Regular.json"

height={0}

rotation={[0, Math.PI, Math.PI]}>

{user.firstName}

</Text3D>

<Text3D

bevelEnabled={false}

bevelSize={0}

font="/ship/2024/badge/Geist_Regular.json"

height={0}

position={[0, 1.4, 0]}

rotation={[0, Math.PI, Math.PI]}>

{user.lastName}

</Text3D>

</Resize>

</Center>

This is an entirely different scene—we want to add the result of the render into our badge as a color. We achieve this using the <RenderTexture> component, which will render our scene into a texture we can attach to the mesh.map:

App.js

<mesh geometry={nodes.card.geometry}>

<meshPhysicalMaterial

clearcoat={1}

clearcoatRoughness={0.15}

iridescence={1}

iridescenceIOR={1}

iridescenceThicknessRange={[0, 2400]}

metalness={0.5}

roughness={0.3}

>

<RenderTexture attach="map" height={2000} width={2000}>

<BadgeTexture user={user} />

</RenderTexture>

</meshPhysicalMaterial>

</mesh>

Link to headingFinishing touches

We have everything in place now. The basic meshes are quickly changed out for the Blender models, and with a little bit of tweaking and math, we make the simulation more stable and less shaky. Here's the sandbox we used to prototype the component for the Ship site:

import * as THREE from 'three'
import { useEffect, useRef, useState } from 'react'
import { Canvas, extend, useThree, useFrame } from '@react-three/fiber'
import { useGLTF, useTexture, Environment, Lightformer } from '@react-three/drei'
import { BallCollider, CuboidCollider, Physics, RigidBody, useRopeJoint, useSphericalJoint } from '@react-three/rapier'
import { MeshLineGeometry, MeshLineMaterial } from 'meshline'
import { useControls } from 'leva'

extend({ MeshLineGeometry, MeshLineMaterial })
useGLTF.preload('https://assets.vercel.com/image/upload/contentful/image/e5382hct74si/5huRVDzcoDwnbgrKUo1Lzs/53b6dd7d6b4ffcdbd338fa60265949e1/tag.glb')
useTexture.preload('https://assets.vercel.com/image/upload/contentful/image/e5382hct74si/SOT1hmCesOHxEYxL7vkoZ/c57b29c85912047c414311723320c16b/band.jpg')

export default function App() {
  const { debug } = useControls({ debug: false })
  return (
    <Canvas camera={{ position: [0, 0, 13], fov: 25 }}>
      <ambientLight intensity={Math.PI} />
      <Physics debug={debug} interpolate gravity={[0, -40, 0]} timeStep={1 / 60}>
        <Band />
      </Physics>
      <Environment background blur={0.75}>
        <color attach="background" args={['black']} />
        <Lightformer intensity={2} color="white" position={[0, -1, 5]} rotation={[0, 0, Math.PI / 3]} scale={[100, 0.1, 1]} />
        <Lightformer intensity={3} color="white" position={[-1, -1, 1]} rotation={[0, 0, Math.PI / 3]} scale={[100, 0.1, 1]} />
        <Lightformer intensity={3} color="white" position={[1, 1, 1]} rotation={[0, 0, Math.PI / 3]} scale={[100, 0.1, 1]} />
        <Lightformer intensity={10} color="white" position={[-10, 0, 14]} rotation={[0, Math.PI / 2, Math.PI / 3]} scale={[100, 10, 1]} />
      </Environment>
    </Canvas>
  )
}

function Band({ maxSpeed = 50, minSpeed = 10 }) {
  const band = useRef(), fixed = useRef(), j1 = useRef(), j2 = useRef(), j3 = useRef(), card = useRef() // prettier-ignore
  const vec = new THREE.Vector3(), ang = new THREE.Vector3(), rot = new THREE.Vector3(), dir = new THREE.Vector3() // prettier-ignore
  const segmentProps = { type: 'dynamic', canSleep: true, colliders: false, angularDamping: 2, linearDamping: 2 }
  const { nodes, materials } = useGLTF('https://assets.vercel.com/image/upload/contentful/image/e5382hct74si/5huRVDzcoDwnbgrKUo1Lzs/53b6dd7d6b4ffcdbd338fa60265949e1/tag.glb')
  const texture = useTexture('https://assets.vercel.com/image/upload/contentful/image/e5382hct74si/SOT1hmCesOHxEYxL7vkoZ/c57b29c85912047c414311723320c16b/band.jpg')
  const { width, height } = useThree((state) => state.size)
  const [curve] = useState(() => new THREE.CatmullRomCurve3([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]))
  const [dragged, drag] = useState(false)
  const [hovered, hover] = useState(false)

  useRopeJoint(fixed, j1, [[0, 0, 0], [0, 0, 0], 1]) // prettier-ignore
  useRopeJoint(j1, j2, [[0, 0, 0], [0, 0, 0], 1]) // prettier-ignore
  useRopeJoint(j2, j3, [[0, 0, 0], [0, 0, 0], 1]) // prettier-ignore
  useSphericalJoint(j3, card, [[0, 0, 0], [0, 1.45, 0]]) // prettier-ignore

  useEffect(() => {
    if (hovered) {
      document.body.style.cursor = dragged ? 'grabbing' : 'grab'
      return () => void (document.body.style.cursor = 'auto')
    }
  }, [hovered, dragged])

  useFrame((state, delta) => {
    if (dragged) {
      vec.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera)
      dir.copy(vec).sub(state.camera.position).normalize()
      vec.add(dir.multiplyScalar(state.camera.position.length()))
      ;[card, j1, j2, j3, fixed].forEach((ref) => ref.current?.wakeUp())
      card.current?.setNextKinematicTranslation({ x: vec.x - dragged.x, y: vec.y - dragged.y, z: vec.z - dragged.z })
    }
    if (fixed.current) {
      // Fix most of the jitter when over pulling the card
      ;[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)))
      })
      // Calculate catmul curve
      curve.points[0].copy(j3.current.translation())
      curve.points[1].copy(j2.current.lerped)
      curve.points[2].copy(j1.current.lerped)
      curve.points[3].copy(fixed.current.translation())
      band.current.geometry.setPoints(curve.getPoints(32))
      // Tilt it back towards the screen
      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 })
    }
  })

  curve.curveType = 'chordal'
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping

  return (
    <>
      <group position={[0, 4, 0]}>
        <RigidBody ref={fixed} {...segmentProps} type="fixed" />
        <RigidBody position={[0.5, 0, 0]} ref={j1} {...segmentProps}>
          <BallCollider args={[0.1]} />
        </RigidBody>
        <RigidBody position={[1, 0, 0]} ref={j2} {...segmentProps}>
          <BallCollider args={[0.1]} />
        </RigidBody>
        <RigidBody position={[1.5, 0, 0]} ref={j3} {...segmentProps}>
          <BallCollider args={[0.1]} />
        </RigidBody>
        <RigidBody position={[2, 0, 0]} ref={card} {...segmentProps} 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) => (e.target.releasePointerCapture(e.pointerId), drag(false))}
            onPointerDown={(e) => (e.target.setPointerCapture(e.pointerId), drag(new THREE.Vector3().copy(e.point).sub(vec.copy(card.current.translation()))))}>
            <mesh geometry={nodes.card.geometry}>
              <meshPhysicalMaterial map={materials.base.map} map-anisotropy={16} clearcoat={1} clearcoatRoughness={0.15} roughness={0.3} metalness={0.5} />
            </mesh>
            <mesh geometry={nodes.clip.geometry} material={materials.metal} material-roughness={0.3} />
            <mesh geometry={nodes.clamp.geometry} material={materials.metal} />
          </group>
        </RigidBody>
      </group>
      <mesh ref={band}>
        <meshLineGeometry />
        <meshLineMaterial color="white" depthTest={false} resolution={[width, height]} useMap map={texture} repeat={[-3, 1]} lineWidth={1} />
      </mesh>
    </>
  )
}

And that's it! Once you get the hang of the basics and start playing with simple shapes, the possibilities are endless.

Get your virtual badge.

Register for Vercel Ship '24 to learn about AI-native user experiences, building composable web applications, and the latest from our partners and community.

Register Today