Home

Keto Mojo, MyMojoHealth

Tempo, Pace Calculator

Shipping an iOS running app without opening Figma

Tempo was an experiment: could I ship a polished iOS app by designing directly in Claude Code and Xcode, never opening Figma? I prototyped the whole interaction in the browser, then built it in SwiftUI with a live Metal shader that repaints the screen with your pace. It shipped live on the App Store, approved on the first try, free, with no accounts and no tracking.

Role: Solo, design and build

Contribution: Concept, UX, Visual design, Metal shaders, SwiftUI, App Store

Your pace has a temperature

Your pace has a temperature

01 · THE EXPERIMENT

The whole bet was to skip Figma and design directly in code. I began in the browser, prototyping the pace to color mapping as a live WebGL shader, so I could drag a slider and watch the screen change temperature long before writing a line of Swift.

// Browser prototype: pace drives a live mesh gradient
vec3 warm = vec3(1.00, 0.18, 0.18);   // fast, hot
vec3 cool = vec3(0.06, 0.75, 0.69);   // easy, teal

// 3:30 per km is 210s, 5:45 is 345s
float t = clamp((pace - 210.0) / 135.0, 0.0, 1.0);
vec3 base = mix(warm, cool, t);       // the temperature axis

03 · THE INTERMEDIATE PROTOTYPES

From the first rough build to the shipped app, the look was tuned in the open. Each prototype answered one question: is the gradient too busy, does the ruler read like an instrument, does the scrub feel liquid?

// The WebGL prototype, ported almost line for line to Metal
[[stitchable]] half4 meshGradient(float2 pos, half4 color,
        float2 size, float time,
        float3 c0, float3 c1, float3 c2, float3 c3, float stir) {
    float2 uv = pos / size;
    float2 warp = fbm(uv * 2.0 + time * 0.1);
    uv += (0.16 + 0.32 * stir) * (warp - 0.5);  // gooey while scrubbing
    return half4(blend(uv, c0, c1, c2, c3), 1.0);
}

04 · PACE BECOMES COLOR

Pace has no artwork, so pace itself became the artwork. Every pace maps to one point on a single temperature axis: red hot when you are flat out, cool teal when you are cruising, with no purple detour in the middle.

func paletteColors() -> [SIMD3<Float>] {
    // 3:30 (210s) fully red  ->  5:45 (345s) fully cool
    let t = Float(min(max((secondsPerKm - 210) / 135, 0), 1))
    let anchors = [
        (rgb(0xFF2E2E), rgb(0x10C0B0)),   // red    -> teal
        (rgb(0xFF7A1A), rgb(0x38C89C)),   // orange -> mint
    ]
    return anchors.map { $0.0 + ($0.1 - $0.0) * t }
}

05 · SIX ICONS, ONE SHADER

If pace is color and the icon is the gradient, then the icon can be a pace mood. Rather than approximate it, each one is rendered from the exact same Metal shader through a small offscreen pass. One source of truth.

// The icon is rendered from the SAME shader as the app.
let tex = device.makeTexture(descriptor: desc)!
let enc = buffer.makeRenderCommandEncoder(descriptor: pass)!
enc.setRenderPipelineState(pipeline)
enc.setFragmentBytes(&uniforms, length: stride, index: 0)
enc.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
// change the shader, regenerate all six icons.

06 · ON THE APP STORE

07 · THE TEMPERATURE PALETTE

Race Red

Tempo Orange

Slate

Mint

Cool Teal

Home

Tempo, Pace Calculator