Skip to content

Latest commit

 

History

History
199 lines (132 loc) · 12.4 KB

File metadata and controls

199 lines (132 loc) · 12.4 KB

Contributing to haptics.md

Thanks for helping build an open, portable haptic format. This project only works if patterns feel right on real hardware and the honest platform truth stays intact. This guide keeps two very different contributions cleanly separated so you know what you're signing up for.

  • Type A — add a pattern. The community path. Two files, schema-validated, roughly ten minutes. You do not need to know anything about native haptic APIs.
  • Type B — add or improve a platform target. The engineering path. Real platform expertise, a conformance checklist, and a named maintainer. This is not a ten-minute job, and we won't pretend it is.

Both paths require a DCO sign-off (see below). Read the section that matches what you're doing.


Before you start

  • Spec status is haptic/0.1-draft and explicitly unstable. Fields may change. The only stable surfaces are the frozen Tier-0 intent vocabulary and the degradation contract. Everything else is provisional — build against it, but expect churn.
  • Unknown fields are ignored, never fatal. If you're tempted to add a field the schema doesn't know, open an issue first. Don't smuggle it into a pattern PR.
  • Read the format doc (docs/format.md) and the honesty rules (docs/honesty.md) once. They exist because a haptic format that lies about what survives to each platform is worse than no format at all.

Local dev setup

git clone https://github.com/Bimbi-Digital/haptics.md
cd haptics.md
npm install
npm run dev        # playground + local docs

Run the validator before every PR. CI runs the same command, so a green local run means a green CI run:

npm run validate            # validate every pattern in registry/
npm run validate <path>     # validate a single .hap.md
npm run manifest            # regenerate registry/manifest.json

The validator checks each .hap.md against the JSON schema (spec/haptic-0.1.schema.json): frontmatter shape, the single fenced haptic block, intent tokens against the frozen vocabulary, track event ranges (intensity and sharpness in 0..1), and fallbacks structure. If it passes locally, it passes in CI.


Type A — Add a pattern (the community path)

A pattern is one .hap.md file: YAML frontmatter, human prose describing the feel, and exactly one fenced code block tagged haptic holding the JSON that is the compilable source of truth. That's the whole format.

Patterns are licensed CC0 (see Licensing below). You're contributing them to the public domain.

Step by step

  1. Pick a category and an id. Categories live as folders under registry/. The id is lowercase, dot- or dash-scoped, and unique within the registry — for example notification-success-firm or impact-medium. Check registry/manifest.json so you don't collide.

  2. Copy the template (below) into registry/<category>/<id>.hap.md.

  3. Write the JSON track. This is the timeline: an array of events, each with an at (ms offset), a type (transient or continuous), and intensity / sharpness in 0..1. Optional curve and primitive per event. Keep it short and deliberate — a pattern is a gesture, not a song.

  4. Set an intent if one fits. Intent is resolved natively first and gives the best feel on iOS and Android. Use a token from the frozen Tier-0 vocabulary only:

    impact.light · impact.medium · impact.heavy · impact.soft · impact.rigid · notification.success · notification.warning · notification.error · selection

    If nothing fits, omit intent — the track still synthesizes. Do not invent new intent tokens in a pattern PR.

  5. Fill fallbacks. fallbacks.web is a crude duration-only on/off array in milliseconds — this is the only thing that reaches the web target, and it carries no intensity or sharpness. Assume it will feel blunt. fallbacks.reduced is an intent token for the low-effort rendering.

  6. Write the prose. One or two honest paragraphs: what the pattern is for, what it should feel like, where you tuned it. This is what an author or an agent reads to decide whether to use it.

  7. Regenerate the manifest and validate:

    npm run manifest
    npm run validate registry/<category>/<id>.hap.md
    
  8. Commit both files — the new .hap.md and the updated registry/manifest.json — with a DCO sign-off, and open the PR. That's the two-file PR.

Minimal .hap.md template

---
id: notification-success-firm
title: Success (firm)
spec: "0.1"
version: 0.1.0
category: notification
intent: notification.success
requiredCapability: null
platforms: [ios, android, web:best-effort]
tags: [success, confirm, positive]
feel: Two quick taps, the second firmer, reads as "done."
---

A confirmation cue for a completed action — a save, a submit, a payment
that went through. Two transients close together; the second lands a touch
harder so the pattern resolves upward rather than trailing off.

On iOS and Android this resolves to the native `notification.success`
primitive first. The synthesized track below is the fallback when the
platform has no matching primitive. On web it degrades to a duration-only
double buzz, which is all `navigator.vibrate` can express.

```haptic
{
  "haptic": "0.1",
  "id": "notification-success-firm",
  "intent": "notification.success",
  "track": [
    { "at": 0,  "type": "transient", "intensity": 0.6, "sharpness": 0.7 },
    { "at": 90, "type": "transient", "intensity": 0.9, "sharpness": 0.8 }
  ],
  "fallbacks": {
    "web": [40, 50, 60],
    "reduced": "notification.success"
  }
}
```

requiredCapability is null unless the pattern genuinely needs a specific capability (for example amplitude control or composition primitives). If it's set, the runtime uses it to decide whether to render the track or drop to fallbacks. Don't set it defensively — an over-strict requiredCapability silences the pattern on hardware that could have rendered a decent version.


Type B — Add or improve a platform target

Adapters translate the format to a specific platform runtime. This is real engineering against real, quirky APIs, and a wrong adapter makes the whole project dishonest — it either lies that something worked or crashes an app that should have degraded silently. We hold these to a higher bar on purpose.

An adapter is not accepted without a named maintainer who commits to it and a passing conformance checklist. If you're not sure you can carry that, open an issue and propose it first — we'd rather scope it with you than reject a large PR.

The ground rules an adapter must honor

  • The degradation ladder is deterministic and top-down: intent (native semantic primitive) → track (synthesized timeline) → fallbacks.web (duration-only milliseconds) → silent no-op. Implement it in that order.
  • A device with no actuator is a valid target that produces nothing. No error, no throw. App logic must never gate on haptic success.
  • A runtime must never lie that amplitude worked. If the device reports no amplitude control, degrade honestly — do not scale a fixed-amplitude buzz and call it intensity.
  • Respect the OS system-haptics setting (on iOS, Settings → Accessibility → Touch → Vibration), not prefers-reduced-motion. Haptics are a feedback and accessibility channel. If the user has turned system haptics off, emit nothing.

Platform truth you must not paper over

  • Web (navigator.vibrate) is duration-only, gesture-gated, throttled, Android-Chrome-family only, and does not exist on iOS Safari or desktop. The label is web (Android Chrome), best-effort, duration-only. A web adapter reads fallbacks.web and nothing else — no intensity, no sharpness. On iOS Safari it renders an explicit "haptics unavailable" state, never a fake buzz.
  • Android requires the VIBRATE manifest permission. VibratorManager is API 31+; amplitude control is API 26+ and many devices report hasAmplitudeControl() == false; mid-range ERM devices often fail areAllPrimitivesSupported(). Emitted code must include a real capability guard (areAllPrimitivesSupported / hasAmplitudeControl) with an actual fallback path, never a silent lie.
  • iOS targets UIFeedbackGenerator, .sensoryFeedback (iOS 17+), and Core Haptics / AHAP.
  • React Native (expo-haptics) and Flutter (HapticFeedback) are intent-tier only. Their public APIs expose semantic constants and nothing more. An adapter for these maps intent tokens and stops there — custom track and sharpness need a native module that hasn't shipped. Do not fake a timeline on top of a semantic constant.
  • Game controllers are a rumble-only escape hatch: weak and strong motors, no waveform. Map coarsely and label it as such.

Adapter workflow

  1. Copy adapters/TEMPLATE/ to adapters/<platform>/. The template carries the interface, the capability-guard scaffold, and the conformance harness.
  2. Implement the ladder. Keep the honest capability guards from the template — they're the point, not boilerplate to delete.
  3. Fill in adapters/<platform>/CONFORMANCE.md — the checklist ships with the template. Every box must be genuinely checked, including "silent no-op on a device with no actuator" and "degrades without lying about amplitude."
  4. Add yourself as the maintainer in the adapter's OWNERS file.
  5. Open the PR with the conformance checklist filled in and the DCO sign-off.

We will ask to see it degrade on real hardware, or at minimum a documented emulation of the no-actuator and no-amplitude paths. Screenshots or a short capture help.


The taste bar: core vs community

There are two tiers, and they're judged differently.

  • Community set — everything that passes the schema and the honesty rules. Low bar by design: if it validates, feels like something deliberate, and doesn't lie about degradation, it's in. This is most of the registry, and we want it large.
  • Core set — a small, curated collection that ships as the vetted defaults and seeds the CLI's offline vendor. Higher bar: the feel is tuned across at least iOS and Android on real devices, the prose is exact, the fallbacks are considered rather than auto-generated, and it earns its place against near-duplicates already in core. Promotion into core is a maintainer decision, made in the open on the PR. A pattern being in community is not a lesser status — most patterns belong there.

If you want your pattern in core, say so in the PR and tell us what hardware you tuned it on.


DCO sign-off (required on every PR)

Every commit must carry a Signed-off-by line — the Developer Certificate of Origin. It's a lightweight statement that you wrote the contribution, or otherwise have the right to submit it under the project's license. We use the DCO instead of a CLA so contributing stays low-friction while the provenance stays clean.

Add it automatically with:

git commit -s -m "add notification-success-firm pattern"

which appends:

Signed-off-by: Your Name <you@example.com>

CI checks for it. Use your real name and a real email.


Licensing

  • Pattern snippets (.hap.md files in registry/) are contributed under CC0 — public domain. You're placing the pattern in the commons so anyone can copy, own, and modify it with no attribution burden.
  • Code (adapters, CLI, compiler, site) is Apache-2.0.

A note on patents, stated plainly: Apache-2.0's patent grant covers only patents held by contributors on their own contributions. It does not shield you from third-party haptic patents (for example, patents held by Immersion). The license does not provide freedom to operate, and we don't claim it does. Every pattern in this registry is built only from public, documented platform APIs — but that is not a patent-clearance guarantee, and you remain responsible for your own use.


Code of Conduct

By participating you agree to the Code of Conduct. Be precise, be kind, assume good faith, and keep the honesty rules sacred — they're what makes this format trustworthy.


Getting help

Open a discussion for questions and a pull request for changes. If you're unsure whether your idea is a pattern (Type A) or a platform change (Type B), open an issue and we'll help you route it. New pattern authors are exactly who this format is for — start with the template above and you'll have a working PR before your coffee's cold.