Events in sequence
Transients, continuous events and bursts on one line. Reorder by drag. Gap is relative to the previous start, so overlapping is a first class move rather than an accident.
Judder · haptic design for iOS
Judder is a native iOS app for designing Core Haptics patterns on a timeline. Events chain on one axis, sound sits underneath them on that same axis, and what leaves is AHAP or a Swift function you paste into your project. It runs on the phone in your hand, so the pattern you draw is the pattern you feel.
This is the real resolver, not a drawing of one. Gap is measured from the previous event's start, so a burst can sit on top of the bed it rides. That overlap is the thing a list of numbered rows can never show you. Every sweep here takes the same two seconds, so four of the five run below real time and the transport says by how much. Every number on the axis is the real one.
Every default in the starter set is checked against published psychophysics, and the papers are cited at the bottom of this page.
The state of things
Core Haptics is a capable API attached to a workflow that has never really existed. The haptic authoring landscape is scattered enough that a 2026 CHI paper had to build a taxonomy before the available tools could even be compared with each other.[1] On iOS the practical result is that most apps ship two taps and a success buzz, because that is all anyone can be confident in.
Fifty transients means fifty objects with a time key you increment yourself. One typo in a parameter name and the event silently does nothing.
Every edit costs a build and a deploy to feel. So the loop is slow, and a slow loop means you stop at the first thing that is not obviously wrong. Adding tactile feedback is worth doing well: it moved touchscreen text entry close to physical keyboard performance in a controlled study.[3]
Intensity, sharpness, duration and gap are four numbers with no picture. You author temporal density without ever seeing density. Give animators an object they manipulate directly instead of per-actuator values and both experts and novices prefer it.[2]
The app
Three tabs and one editor. Patterns are yours and they persist. Open one and it pushes, with the tab bar giving way to a transport. Anything you change plays back on the phone you are holding, in about the time it takes to lift your thumb.
Live app, embedded. Drive it.
What it does
Transients, continuous events and bursts on one line. Reorder by drag. Gap is relative to the previous start, so overlapping is a first class move rather than an accident.
Set a duration and a step and Judder expands the comb for you, clamped against the real ceiling. Drag the step and watch the comb rebuild in place. The starter textures step at 33 and 35 ms, above the roughly 25 ms interval at which two pulses stop being two,[5] which is why grain reads as grain instead of as a buzz.
Continuous events carry a curve, and the block is clipped to its own silhouette. What is drawn is what the engine will run. Under about 30 ms a stimulus is felt as a discrete pulse rather than a vibration, and that boundary holds from 40 to 590 Hz,[6] so a short continuous event is really a transient wearing a duration.
Drop a bed under the haptic lane on the same time axis. Sound is a layer here, not a settings screen, because a hit and its sample either agree or they do not. Audiotactile binding is frequency specific, and strongest in the 200 to 390 Hz band where skin is most sensitive.[7]
Resolved event count against the budget, burst hits against the cap, total span, and the specific warnings that bite. No surprises at runtime.
Take the pattern file, or take a generated Swift function with the loop already written. Copy it out and build. Nothing you ship links back to Judder.
Export
Judder is a design tool, not a framework. Nothing it produces links against it. You get a pattern file or a function, plus the report that tells you it will hold.
// DensityBed.ahap · 50 events · 1.60 s { "Version": 1, "Pattern": [ { "Event": { "Time": 0, "EventType": "HapticContinuous", "EventDuration": 1.6, "EventParameters": [ { "ParameterID": "HapticIntensity", "ParameterValue": 0.70 }, { "ParameterID": "HapticSharpness", "ParameterValue": 0.10 } ] }}, { "Event": { "Time": 0, "EventType": "HapticTransient", "EventParameters": [ { "ParameterID": "HapticIntensity", "ParameterValue": 0.55 }, { "ParameterID": "HapticSharpness", "ParameterValue": 0.75 } ] }}, // 48 more transients at 0.033 s intervals { "Event": { "Time": 1.584, "EventType": "HapticTransient", "EventParameters": [ { "ParameterID": "HapticIntensity", "ParameterValue": 0.55 }, { "ParameterID": "HapticSharpness", "ParameterValue": 0.75 } ] }} ] }
// Generated by Judder. No runtime dependency. import CoreHaptics enum DensityBed { static func pattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [ CHHapticEvent( eventType: .hapticContinuous, parameters: [ .init(parameterID: .hapticIntensity, value: 0.70), .init(parameterID: .hapticSharpness, value: 0.10) ], relativeTime: 0, duration: 1.60 ) ] // burst: step 0.033 s across 1.60 s, clamped at 256 for k in 0..<49 { events.append( CHHapticEvent( eventType: .hapticTransient, parameters: [ .init(parameterID: .hapticIntensity, value: 0.55), .init(parameterID: .hapticSharpness, value: 0.75) ], relativeTime: Double(k) * 0.033 ) ) } return try CHHapticPattern(events: events, parameters: []) } }
Recipes
Judder ships a starter set of the haptics an app actually needs. Open one and take it apart. Save your version over the top. These are the four you will reach for first, and their step and duration values sit where they do for the reasons cited below.
Straight answers
The right column is what Judder does not do. It is printed here rather than left to be found out afterwards.
Provenance
A tool that ships defaults should be able to show its work. Judder's are not taste. The burst step sits where it does because of a measured fusion threshold. The shortest continuous event worth authoring is bounded by a duration threshold, and the sound lane shares the haptic time axis because audiotactile binding turns out to be frequency specific. Every paper behind those choices is listed here, along with what it was used for.
These papers were not written about Judder and do not endorse it. They are the published work its defaults were checked against. Where there is no literature behind a number, the page says so: the 512 event budget, the 256 hit burst cap and the 0.08 s transient tail are limits taken from the app's own engine, not findings from a journal.
judder-haptics.com
Judder is on the App Store. If you ship an iOS app and you have ever settled for a default impact generator because the real thing was too slow to iterate on, this was built for you.