PhoneHands
The hands the phone sees (tap Hands, up to 4), drawn as small solid skeletons standing in the room. They are lifted to metric 3D through the LiDAR depth. A pinch closes into a bright bead. Without LiDAR the same stream draws as a flat overlay. Needs import OllinPhone.
- Run
swift run --package-path Examples Example-3D-Phone-PhoneHands- Imports
OllinPhone- Source
- Examples/3D/Phone/PhoneHands/Sketch.swift
import Foundation
import Ollin
import OllinPhone
/// The hands the phone sees, standing in the room as solid little skeletons. The
/// capture app finds up to four 21-joint hands on-device and lifts each joint to
/// metric 3D through the LiDAR depth, so a hand here floats where the real hand
/// is, in meters, in the same world the depth sweep and the room mesh use. Left
/// and right have their own color, a pinch closes into a bright bead at the
/// fingertips, and on a phone with no LiDAR the same stream arrives flat and
/// draws as a 2D overlay instead.
///
/// Setup: build + run the Ollin capture app (Apps/OllinPhoneApp) on the iPhone,
/// choose **Hands**, connect the cable, and hold a hand in front of the rear
/// camera. Pinch: thumb to index closes the bead.
@main
final class PhoneHands: Sketch {
let device = PhoneDevice()
/// Where the orbit looks, eased frame-to-frame so live noise doesn't jitter it.
var orbitCenter: Vector3?
let rightColor = Color(hex: 0xE8A34F) // warm for right hands
let leftColor = Color(hex: 0x6FA8E8) // cool for left
let unknownColor = Color(white: 0.75)
override func setup() {
device.start()
}
override func draw() {
background(Color(white: 0.05))
let hands = device.latestHands
guard !hands.isEmpty else {
var text = device.waitingMessage + "\n\n" +
"Run the Ollin capture app on the iPhone in Hands mode,\n" +
"connect the cable, and hold a hand in front of its rear camera."
if let m = device.latestMotion {
text += String(format: "\n\nmotion live · gravity (% .2f, % .2f, % .2f)",
m.gravity.x, m.gravity.y, m.gravity.z)
}
return drawStatus(text, style: .info)
}
let lifted = hands.filter(\.hasWorldPositions)
if lifted.isEmpty {
drawFlatHands(hands)
} else {
drawLiftedHands(lifted)
}
drawCaption(caption(for: hands, lifted: !lifted.isEmpty))
}
// MARK: The 3D room (a LiDAR phone)
private func drawLiftedHands(_ hands: [PhoneHand]) {
// Aim at the middle of everything lifted; a hand is small, so orbit close.
var center = Vector3.zero
for hand in hands { center += hand.center }
center /= Double(hands.count)
if let c = orbitCenter { orbitCenter = c.lerp(to: center, 0.1) } else { orbitCenter = center }
cameraShowcase(.turntable(period: .tau / 0.35), target: orbitCenter ?? center,
radius: 0.9, elevation: 0.15, fieldOfView: .pi / 3)
environment(.studio.intensified(to: 0.9))
for hand in hands {
let tone = color(of: hand)
withState {
material(.clay)
fill(tone)
for (a, b) in hand.bones() {
drawCapsule(from: a, to: b, radius: 0.006)
}
for joint in PhoneHandJoint.allCases {
guard let p = hand.position(joint) else { continue }
let tip = PhoneHand.tips.contains(joint)
withState {
translate(p.x, p.y, p.z)
drawSphere(radius: tip ? 0.011 : 0.008)
}
}
}
// A closed pinch turns into one bright bead between the fingertips.
if let d = hand.pinchDistance, d < 0.025,
let thumb = hand.position(.thumbTip), let index = hand.position(.indexTip) {
let mid = thumb.lerp(to: index, 0.5)
withState {
material(.glossy)
fill(.white)
translate(mid.x, mid.y, mid.z)
drawSphere(radius: 0.014)
}
}
}
}
// MARK: The flat overlay (no LiDAR to lift through)
private func drawFlatHands(_ hands: [PhoneHand]) {
for hand in hands {
let tone = color(of: hand)
withState {
stroke(tone)
strokeWeight(6)
for (a, b) in hand.bones(in: bounds) { drawLine(a, b) }
noStroke()
fill(tone)
for (_, p) in hand.points(in: bounds) {
drawCircle(center: p, radius: 9)
}
}
}
}
// MARK: Shared
private func color(of hand: PhoneHand) -> Color {
switch hand.chirality {
case .right: return rightColor
case .left: return leftColor
case .unknown: return unknownColor
}
}
private func caption(for hands: [PhoneHand], lifted: Bool) -> String {
var parts = ["PhoneHands, \(hands.count) in view · \(lifted ? "3D" : "2D, no LiDAR")"]
for hand in hands {
guard let d = hand.pinchDistance else { continue }
let side = hand.chirality == .left ? "left" : "right"
parts.append(String(format: "%@ pinch %.0f mm", side, d * 1000))
}
return parts.joined(separator: " · ")
}
}