Skip to content

Commit a24c1ff

Browse files
committed
docs: add reactive contract guide and Clip DSL reference
1 parent f79f8fb commit a24c1ff

9 files changed

Lines changed: 218 additions & 5 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
title: Clip
3+
description: DSL function to place a prebuilt pattern on the timeline
4+
---
5+
6+
# Clip
7+
8+
<Callout type="info">
9+
**Module**: `jindong-compose` | **Package**: `io.github.compose.jindong.dsl`
10+
</Callout>
11+
12+
Places a prebuilt `HapticPattern` on the timeline at the current position.
13+
14+
## Signature
15+
16+
```kotlin
17+
@Composable
18+
fun JindongScope.Clip(pattern: HapticPattern)
19+
```
20+
21+
## Parameters
22+
23+
| Parameter | Type | Description |
24+
|-----------|------|-------------|
25+
| `pattern` | `HapticPattern` | The prebuilt pattern to place on the timeline |
26+
27+
## Description
28+
29+
`Clip` drops a self-contained pattern next to inline nodes. It is the bridge between a `HapticPattern` value, typically an algebra result built with `buildHapticPattern`, and the composable DSL. The clip's events are offset by the nodes that precede it, so it schedules relative to its position like any other node.
30+
31+
```kotlin
32+
val heartbeat = buildHapticPattern {
33+
haptic(60.ms)
34+
delay(80.ms)
35+
haptic(40.ms)
36+
}
37+
38+
Jindong(trigger) {
39+
Clip(heartbeat)
40+
Delay(200.ms)
41+
Haptic(60.ms, HapticIntensity.MEDIUM)
42+
}
43+
```
44+
45+
## Freezing and re-firing
46+
47+
A clip captures its `pattern` at compile time. Because `Jindong` compiles content in a single pass with no recomposition (see [The Reactive Contract](/docs/guide/reactive-contract)), swapping the pattern in state alone does not update the clip. Thread the pattern through the trigger keys to make it live:
48+
49+
```kotlin
50+
Jindong(pattern) { Clip(pattern) }
51+
```
52+
53+
`HapticPattern` is a data class, so the key comparison is structural: an equal pattern does not re-fire, a different one does.
54+
55+
<Callout type="warning">
56+
Do not add a `Jindong(pattern, vararg keys)` overload to skip the `Clip` call. `Jindong(p) { ... }` resolves to the existing `vararg keys` overload with `p` as a key, which compiles but plays the lambda instead of `p`. The `Jindong(pattern) { Clip(pattern) }` idiom is the supported way to embed a value.
57+
</Callout>
58+
59+
## Usage
60+
61+
### Composing with the pattern algebra
62+
63+
Clips pair well with the pattern transforms, which return new `HapticPattern` values:
64+
65+
```kotlin
66+
val base = buildHapticPattern {
67+
haptic(50.ms)
68+
delay(50.ms)
69+
haptic(50.ms)
70+
}
71+
72+
Jindong(speed) {
73+
Clip(base.timeStretch(1f / speed))
74+
}
75+
```
76+
77+
### Reusing a pattern across screens
78+
79+
```kotlin
80+
private val confirm = buildHapticPattern {
81+
haptic(50.ms)
82+
delay(50.ms)
83+
haptic(100.ms)
84+
}
85+
86+
@Composable
87+
fun SaveButton(saved: Boolean) {
88+
Jindong(saved) {
89+
if (saved) Clip(confirm)
90+
}
91+
}
92+
```
93+
94+
## Notes
95+
96+
- The pattern is captured when the node is inserted, not on every frame
97+
- Preceding `Delay` and `Haptic` nodes shift the clip's start time
98+
- `HapticPattern` equality is structural, which drives key comparison
99+
- An empty pattern (`HapticPattern.Empty`) places nothing
100+
101+
## See Also
102+
103+
- [The Reactive Contract](/docs/guide/reactive-contract)
104+
- [Sequence](/docs/api/jindong-compose/composable-dsl/sequence)
105+
- [Core API](/docs/api/jindong-core/core-api)

documentation/content/docs/api/jindong-compose/composable-dsl/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"delay",
66
"sequence",
77
"repeat",
8-
"repeat-with-index"
8+
"repeat-with-index",
9+
"clip"
910
]
1011
}

documentation/content/docs/api/jindong-compose/jindong.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ This is useful for:
7676

7777
The content block is compiled into a `HapticPattern` containing scheduled events. The pattern is then executed by the `HapticExecutor` provided via `LocalHapticExecutor`.
7878

79+
### The Reactive Contract
80+
81+
`Jindong` reads values and fires playback under three rules:
82+
83+
1. Values read inside `content` are frozen at compile time. The block is compiled once per key change and does not recompose, so a captured state value is read once.
84+
2. Keys are the playback trigger. Put a value in `keys` when its change should play; keep pattern-shaping values inside the block as parameters.
85+
3. Cancel-and-restart is best effort. A key change cancels the in-flight playback first, but the platform stop calls report no completion, so brief physical overlap is possible.
86+
87+
To make a value both shape the pattern and re-fire when it changes, pass it as a key and read it inside the block. For a prebuilt pattern, thread it through the keys and place it with `Clip`:
88+
89+
```kotlin
90+
Jindong(pattern) { Clip(pattern) }
91+
```
92+
93+
See [The Reactive Contract](/docs/guide/reactive-contract) for the full explanation.
94+
7995
## Usage
8096

8197
### Basic Usage
@@ -169,6 +185,7 @@ Within the `JindongScope`, you can use:
169185
| [`Sequence`](/docs/api/jindong-compose/composable-dsl/sequence) | Group events sequentially |
170186
| [`Repeat`](/docs/api/jindong-compose/composable-dsl/repeat) | Repeat content N times |
171187
| [`RepeatWithIndex`](/docs/api/jindong-compose/composable-dsl/repeat-with-index) | Repeat with index access |
188+
| [`Clip`](/docs/api/jindong-compose/composable-dsl/clip) | Place a prebuilt pattern on the timeline |
172189

173190
## Notes
174191

@@ -179,6 +196,7 @@ Within the `JindongScope`, you can use:
179196

180197
## See Also
181198

199+
- [The Reactive Contract](/docs/guide/reactive-contract)
182200
- [JindongProvider](/docs/api/jindong-compose/jindong-provider)
183201
- [JindongScope](/docs/api/jindong-compose/jindong-scope)
184202
- [Quick Start](/docs/guide/quick-start)

documentation/content/docs/api/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"jindong-compose/composable-dsl/delay",
1515
"jindong-compose/composable-dsl/sequence",
1616
"jindong-compose/composable-dsl/repeat",
17-
"jindong-compose/composable-dsl/repeat-with-index"
17+
"jindong-compose/composable-dsl/repeat-with-index",
18+
"jindong-compose/composable-dsl/clip"
1819
]
1920
}

documentation/content/docs/guide/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"thinking-declarative",
44
"why-jindong",
55
"getting-started",
6-
"quick-start"
6+
"quick-start",
7+
"reactive-contract"
78
]
89
}

documentation/content/docs/guide/quick-start.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ Jindong(level) {
270270
```
271271

272272
If a captured value is not in the keys, the pattern keeps the value from the last
273-
key change instead of updating.
273+
key change instead of updating. [The Reactive Contract](/docs/guide/reactive-contract)
274+
covers when values are read and when playback fires.
274275

275276
## Next Steps
276277

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
title: The Reactive Contract
3+
description: How Jindong decides when to play and which values it reads
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout';
7+
8+
# The Reactive Contract
9+
10+
`Jindong` behaves like `LaunchedEffect`: it plays the pattern in its content block, and a change to any key restarts it. Three rules describe exactly which values it reads and when it fires. Everything about reactive `Jindong` usage follows from them.
11+
12+
## Rule 1: values in content are frozen at compile time
13+
14+
When a key changes, `Jindong` compiles the content block into a pattern once and plays it. The compilation runs in a composition that produces the pattern in a single pass and is then torn down, so there is no recomposition. A state value the block reads is read once, at that compile, and does not update on its own afterward.
15+
16+
```kotlin
17+
var level by remember { mutableStateOf(1) }
18+
19+
// `level` is read once, when the current key last changed.
20+
Jindong(trigger) {
21+
RepeatWithIndex(level) { index ->
22+
Haptic(50.ms)
23+
}
24+
}
25+
```
26+
27+
If `level` changes but `trigger` does not, the played pattern keeps the `level` from the last time `trigger` changed.
28+
29+
## Rule 2: keys are the playback trigger
30+
31+
A key change is what fires playback. Put a value in the keys exactly when its change should play. Values that only shape the pattern belong inside the block as parameters.
32+
33+
The distinction matters most with continuous input. A slider value passed as a key fires a vibration on every drag step:
34+
35+
```kotlin
36+
// Fires on every value change while dragging.
37+
Jindong(sliderValue) {
38+
Haptic((sliderValue * 100).toInt().ms)
39+
}
40+
```
41+
42+
The same value as a parameter shapes the next playback without firing on each step:
43+
44+
```kotlin
45+
// Reads sliderValue when `commit` changes; does not fire mid-drag.
46+
Jindong(commit) {
47+
Haptic((sliderValue * 100).toInt().ms)
48+
}
49+
```
50+
51+
To make a value both shape the pattern and re-fire when it changes, pass it as a key and read it inside the block:
52+
53+
```kotlin
54+
Jindong(level) {
55+
RepeatWithIndex(level) { index ->
56+
Haptic(50.ms, HapticIntensity.Custom(1f - index * 0.1f))
57+
}
58+
}
59+
```
60+
61+
<Callout type="info">
62+
Prebuilt patterns follow the same rule. `Clip` captures its pattern at compile time, so thread the pattern through the keys to re-fire on change: `Jindong(pattern) { Clip(pattern) }`. See [Clip](/docs/api/jindong-compose/composable-dsl/clip).
63+
</Callout>
64+
65+
## Rule 3: cancel-and-restart is best effort
66+
67+
A key change cancels the in-flight playback before starting the new one. The manager serializes this with a state lock, so a fast sequence of key changes cancels in order and the last one wins.
68+
69+
The cancellation itself is best effort. Neither `Vibrator.cancel()` on Android nor `CHHapticPatternPlayer.stop` on iOS reports when the hardware has actually stopped, so a few milliseconds of physical overlap between the old and new playback are possible. Patterns that change keys rapidly should not assume a clean cut between them.
70+
71+
## Choosing keys
72+
73+
| Value | Where it goes | Effect |
74+
|-------|---------------|--------|
75+
| Should fire playback when it changes | Key | Each change plays |
76+
| Shapes the pattern, read at the next fire | Parameter in the block | Frozen until a key changes |
77+
| Should both shape and fire | Key and read in the block | Each change plays with the new value |
78+
79+
## See also
80+
81+
- [Jindong API](/docs/api/jindong-compose/jindong)
82+
- [Clip](/docs/api/jindong-compose/composable-dsl/clip)
83+
- [Quick Start](/docs/guide/quick-start)

documentation/content/docs/guide/thinking-declarative.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,6 @@ Declarative programming means:
8989
2. **Delegate control** to the framework
9090
3. **Trust the abstraction** to handle details
9191

92-
This mental model—focusing on "what" rather than "how"—is the foundation of effective Jindong usage.
92+
This mental model, focusing on "what" rather than "how", is the foundation of effective Jindong usage.
93+
94+
The one place the framework's control shows through is timing: `Jindong` reads its content and fires playback under a small set of rules. [The Reactive Contract](/docs/guide/reactive-contract) spells them out.

documentation/content/docs/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"api/jindong-compose/composable-dsl/sequence",
1717
"api/jindong-compose/composable-dsl/repeat",
1818
"api/jindong-compose/composable-dsl/repeat-with-index",
19+
"api/jindong-compose/composable-dsl/clip",
1920
"---Contributing---",
2021
"...contributing"
2122
]

0 commit comments

Comments
 (0)