TUIOSurface
a tangible surface with nothing plugged in: a stand-in tracker sends real TUIO frames to 127.0.0.1, and the touches, the turning marker, and the shape are all drawn from what comes back off the wire; turn simulate off and point a real table or phone app at port 3333 (TUIOReceiver, TUIOCursor, TUIOObject, TUIOBlob)
- Run
swift run --package-path Examples Example-Integration-TUIOSurface- Imports
OllinOSC- Source
- Examples/Integration/TUIOSurface/Sketch.swift
import Ollin
import OllinOSC
/// A tangible surface on screen: touches, tagged pieces, and shapes arriving as
/// TUIO, the protocol marker tables and touch walls speak.
///
/// Like the OSC example, it runs both ends so it works on any machine with no
/// hardware: with `simulate` on, a stand-in tracker sends real TUIO frames to
/// `127.0.0.1` and everything drawn here comes back off the wire. Turn
/// `simulate` off and point a real tracker at this Mac on port 3333 to drive it
/// instead. Marker tables, touch walls, and the phone apps that send finger
/// positions all speak this.
@main
final class TUIOSurface: Sketch {
@Param var simulate = true
let surface = TUIOReceiver()
var tracker: OSCSender!
var frame = 0
let palette = CosinePalette.neon
override func setup() {
try? surface.start()
tracker = OSCSender(host: "127.0.0.1", port: TUIOReceiver.defaultPort)
}
override func draw() {
if simulate { sendAFrame() }
background(Color(white: 0.07))
drawTheSurfaceEdge()
for touch in surface.cursors { draw(touch) }
for piece in surface.objects { draw(piece) }
for shape in surface.blobs { draw(shape) }
drawTheStatusLine()
}
// MARK: What the tracker reports
private func draw(_ touch: TUIOCursor) {
let at = touch.position(in: bounds)
// Ids come in a run, so the color is spread by a stride rather than
// taken straight from the number, or neighboring touches read alike.
let tint = palette.color(at: (Double(touch.id) * 0.37).truncatingRemainder(dividingBy: 1))
// The whisker is the touch's own velocity, a quarter second ahead.
strokeWeight(3 * scale)
stroke(tint.withAlpha(0.5))
let ahead = Vector2(touch.velocity.x * width, touch.velocity.y * height) * 0.25
drawLine(at, at + ahead)
noStroke()
fill(tint.withAlpha(0.18))
drawCircle(center: at, radius: 46 * scale)
fill(tint)
drawCircle(center: at, radius: 14 * scale)
fill(Color(white: 0.85))
textSize(18 * scale)
drawText("\(touch.id)", at: at + Vector2(24, -24) * scale)
}
private func draw(_ piece: TUIOObject) {
withState {
translate(piece.position(in: bounds))
rotate(piece.angle)
noFill()
stroke(Color(white: 0.95))
strokeWeight(4 * scale)
drawRect(center: .zero, width: 130 * scale, height: 130 * scale)
// One corner marked, so the turn is readable at a glance.
fill(Color(white: 0.95))
noStroke()
drawCircle(center: Vector2(-52, -52) * scale, radius: 9 * scale)
textSize(34 * scale)
drawText("\(piece.symbol)", at: Vector2(-14, 12) * scale)
}
}
private func draw(_ shape: TUIOBlob) {
let box = shape.bounds(in: bounds)
withState {
translate(box.center)
rotate(shape.angle)
noStroke()
fill(Color(white: 1, alpha: 0.14))
drawEllipse(center: .zero, radiusX: box.width / 2, radiusY: box.height / 2)
}
}
// MARK: The stand-in tracker
/// Sends one surface frame the way a tracker would: the `alive` list, a
/// `set` for everything that moved, then the frame number that commits them.
private func sendAFrame() {
frame += 1
let touches = (0..<3).map { index -> (id: Int, point: Vector2) in
let phase = time * 0.6 + Double(index) * 2.1
return (id: 11 + index,
point: Vector2(0.5 + 0.34 * sin(phase), 0.5 + 0.34 * sin(phase * 1.37 + 0.8)))
}
let alive = OSCMessage("/tuio/2Dcur",
arguments: [.string("alive")] + touches.map { .int(Int32($0.id)) })
let sets: [OSCMessage] = touches.map { touch in
// Velocity in surface widths per second, which is what the whisker reads.
let ahead = Vector2(0.34 * 0.6 * cos(time * 0.6 + Double(touch.id - 11) * 2.1),
0.34 * 0.6 * 1.37 * cos((time * 0.6 + Double(touch.id - 11) * 2.1) * 1.37 + 0.8))
return OSCMessage("/tuio/2Dcur", .string("set"), .int(Int32(touch.id)),
.float(Float(touch.point.x)), .float(Float(touch.point.y)),
.float(Float(ahead.x)), .float(Float(ahead.y)), .float(0))
}
let fseq = OSCMessage("/tuio/2Dcur", .string("fseq"), .int(Int32(frame)))
tracker.send(OSCBundle(.immediate, messages: [alive] + sets + [fseq]))
let turn = time * 0.5
tracker.send(OSCBundle(.immediate, messages: [
OSCMessage("/tuio/2Dobj", arguments: [.string("alive"), .int(41)]),
OSCMessage("/tuio/2Dobj", .string("set"), .int(41), .int(7),
.float(Float(0.5 + 0.18 * cos(time * 0.35))),
.float(Float(0.5 + 0.18 * sin(time * 0.35))),
.float(Float(turn)), .float(0), .float(0), .float(0.5), .float(0), .float(0)),
OSCMessage("/tuio/2Dobj", .string("fseq"), .int(Int32(frame))),
]))
let breath = 0.22 + 0.06 * sin(time * 1.1)
tracker.send(OSCBundle(.immediate, messages: [
OSCMessage("/tuio/2Dblb", arguments: [.string("alive"), .int(61)]),
OSCMessage("/tuio/2Dblb", .string("set"), .int(61),
.float(0.33), .float(0.4), .float(Float(sin(time * 0.2))),
.float(Float(breath)), .float(Float(breath * 0.6)),
.float(Float(breath * breath * 0.6)),
.float(0), .float(0), .float(0), .float(0), .float(0)),
OSCMessage("/tuio/2Dblb", .string("fseq"), .int(Int32(frame))),
]))
}
// MARK: Chrome
private func drawTheSurfaceEdge() {
noFill()
stroke(Color(white: 0.22))
strokeWeight(2 * scale)
drawRect(corner: Vector2(1, 1) * scale, width: width - 2 * scale, height: height - 2 * scale)
}
private func drawTheStatusLine() {
noStroke()
fill(Color(white: 0.55))
textSize(22 * scale)
let heard = surface.framesReceived
let line = heard == 0
? "listening for a tracker on port \(TUIOReceiver.defaultPort)"
: [count(surface.cursors.count, "touch", "touches"),
count(surface.objects.count, "piece", "pieces"),
count(surface.blobs.count, "shape", "shapes")].joined(separator: ", ")
drawText(line, 30 * scale, height - 36 * scale)
drawText(surface.sourceName ?? "port \(TUIOReceiver.defaultPort)", 30 * scale, 46 * scale)
}
private func count(_ n: Int, _ one: String, _ many: String) -> String {
"\(n) \(n == 1 ? one : many)"
}
}