Ollin

InverseKinematics

five IKChain tentacles reach toward a swimming lure, a stiffness parameter (maxBend) runs from rope to whip, and a toggle swaps the even solver for the tip-curling one

Run
swift run --package-path Examples Example-Motion-InverseKinematics
Source
Examples/Motion/InverseKinematics/Sketch.swift
import Ollin

/// Tentacles that reach: five `IKChain`s planted along the bottom edge, all
/// straining toward a lure that swims a slow loop overhead. Each chain is a
/// row of rigid segments solved fresh every frame, warm-started from its last
/// pose, so the arms move coherently instead of snapping.
///
/// The `stiffness` parameter is `maxBend`, the angle each segment may fold against
/// its neighbor: low values make ropes, high values make whips. The `whippy`
/// toggle swaps the solver, and the difference is the whole aesthetic choice:
/// the default spreads motion evenly down the arm (smooth, plant-like), the
/// alternative favors the segments near the tip, so the arms curl and lash.
/// Press the mouse to steal the lure.
@main
final class InverseKinematics: Sketch {
    @Param(0.05 ... 0.8, icon: "line.diagonal.arrow", group: "Arms") var stiffness = 0.22
    @Param(icon: "scribble.variable", group: "Arms") var whippy = false

    private var arms: [IKChain] = []
    private let colors = [Color(hex: 0x2C7DA0), Color(hex: 0x62B6CB),
                          Color(hex: 0x8FBFA0), Color(hex: 0xE9C46A),
                          Color(hex: 0xD1495B)]

    override var loopDuration: Double? { 16 }

    override func setup() {
        arms = (0 ..< 5).map { i in
            let base = Vector2(width * (0.18 + 0.16 * Double(i)), height - 40)
            return IKChain(from: base, segments: 15 + 2 * i, length: 40)
        }
    }

    override func draw() {
        background(Color(hex: 0x11141B))

        // The lure: a slow figure through looping noise so the lap closes,
        // unless the mouse has taken over.
        let t = loopProgress(over: 16)
        var lure = Vector2(width * (0.5 + signedNoise(3, loop: t, radius: 1.3) * 0.36),
                           height * (0.34 + signedNoise(9, loop: t, radius: 1.3) * 0.22))
        if mouseIsPressed { lure = mouse }

        strokeCap(.round)
        for (i, arm) in arms.enumerated() {
            arm.maxBend = stiffness
            arm.solver = whippy ? .ccd : .fabrik
            arm.reach(toward: lure)

            // Draw thick at the root, thin at the tip.
            let color = colors[i]
            for s in 0 ..< arm.lengths.count {
                let taper = 1 - Double(s) / Double(arm.lengths.count)
                stroke(color.withAlpha(0.55 + 0.45 * taper))
                strokeWeight(2 + 13 * taper * taper)
                drawLine(arm.joints[s], arm.joints[s + 1])
            }
            noStroke()
            fill(color)
            drawCircle(center: arm.tip, radius: 7)
        }

        // The lure itself.
        fill(.white)
        drawCircle(center: lure, radius: 10)
        noFill()
        stroke(Color.white.withAlpha(0.35))
        strokeWeight(2)
        drawCircle(center: lure, radius: 18)

        drawCaption(mouseIsPressed ? "the arms follow the mouse"
                                   : "press and drag to steal the lure")
    }
}

↑↓ move ↩ open