Skip to content

Commit 2dd58a3

Browse files
Add ricochet playground control and tighten trail follow
Expose the existing `ricochet` option as a slider in the playground, with snippet and shareable-URL round-trip alongside the other controls (demo only — the API and docs already shipped). Stiffen the shared chase/snap spring (170->260) and raise damping to match (20->26) so trailing bubbles track the leader more tightly during a group drag and settle against the edge a touch crisper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fe855ed commit 2dd58a3

8 files changed

Lines changed: 34 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1616

1717
- The built-in fallback bubble icon (painted only when a bubble has no `icon`, with `bubbleIcon` as
1818
its stroke) is now an ellipsis glyph instead of a chat bubble.
19+
- Trailing bubbles track the leader more tightly during a group drag, and bubbles settle against the
20+
edge a touch crisper — the shared chase/snap spring is stiffer and damped to match.
1921

2022
### Internal
2123

@@ -24,6 +26,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2426
external links announce that they open a new tab, and the contrast-aware glyph picks its ink by
2527
WCAG contrast ratio.
2628
- Added `@axe-core/playwright` and an end-to-end accessibility scan over the playground.
29+
- Playground now exposes the existing `ricochet` option as a slider, with snippet and shareable-URL
30+
round-trip alongside the other controls.
2731

2832
## [0.5.1] - 2026-06-14
2933

playground/components/settings-panel.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
// Sliders hand back float-noise like 0.35000000000000003; two decimals
5454
// keep the URL and snippet honest.
5555
const commitVertical = (value: number) => (config.vertical = Math.round(value * 100) / 100);
56+
const commitRicochet = (value: number) => (config.ricochet = Math.round(value * 100) / 100);
5657
5758
const reset = () => Object.assign(config, defaults);
5859
</script>
@@ -205,6 +206,16 @@
205206
onCommit={(v) => (config.panelMaxHeight = v)}
206207
/>
207208

209+
<ControlSlider
210+
label="Ricochet"
211+
min={ranges.ricochet.min}
212+
max={ranges.ricochet.max}
213+
step={ranges.ricochet.step}
214+
value={config.ricochet}
215+
format={(v) => `${Math.round(v * 100)}%`}
216+
onCommit={commitRicochet}
217+
/>
218+
208219
<div class="flex items-center justify-between">
209220
<span class="text-xs text-zinc-400 light:text-zinc-600">Max bubbles</span>
210221
<div class="flex items-center gap-2">

playground/defaults.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export const ranges = {
88
panelWidth: { min: 280, max: 1600, step: 20 },
99
// Panel max height as a viewport percentage — the library takes "%".
1010
panelMaxHeight: { min: 10, max: 100, step: 10 },
11-
maxBubbles: { min: 1, max: 5 }
11+
maxBubbles: { min: 1, max: 5 },
12+
// Bounce restitution: 0 stops a fling dead at the gap, 1 is lossless.
13+
ricochet: { min: 0, max: 1, step: 0.05 }
1214
} as const;
1315

1416
/**
@@ -26,5 +28,6 @@ export const defaults: PlaygroundConfig = {
2628
vertical: 0.5,
2729
panelWidth: 480,
2830
panelMaxHeight: 70,
29-
maxBubbles: 5
31+
maxBubbles: 5,
32+
ricochet: 0.4
3033
};

playground/options.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,6 @@ export const toBubblesOptions = (config: PlaygroundConfig): BubblesOptions => ({
6363
vertical: config.vertical,
6464
panelWidth: config.panelWidth,
6565
panelMaxHeight: `${config.panelMaxHeight}%`,
66-
maxBubbles: config.maxBubbles
66+
maxBubbles: config.maxBubbles,
67+
ricochet: config.ricochet
6768
});

playground/snippet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const configSnippet = (config: PlaygroundConfig): string => {
3030
// full-height panels instead of the contained ones on screen.
3131
lines.push(`\tpanelMaxHeight: "${config.panelMaxHeight}%"`);
3232
if (config.maxBubbles !== defaults.maxBubbles) lines.push(`\tmaxBubbles: ${config.maxBubbles}`);
33+
if (config.ricochet !== defaults.ricochet) lines.push(`\tricochet: ${config.ricochet}`);
3334

3435
if (lines.length === 0) return "createBubbles();";
3536
return `createBubbles({\n${lines.join(",\n")}\n});`;

playground/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface PlaygroundConfig {
1414
/** Panel max height as a percentage of the viewport (10–100). */
1515
panelMaxHeight: number;
1616
maxBubbles: number;
17+
/** Fraction of fling speed a bubble keeps when it bounces off the top/bottom gap (0–1). */
18+
ricochet: number;
1719
}
1820

1921
/** A curated accent color in the controls panel. */

playground/url.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ export const readConfig = (): Partial<PlaygroundConfig> => {
3131
["vertical", "vertical", ranges.vertical],
3232
["panel-width", "panelWidth", ranges.panelWidth],
3333
["panel-max-height", "panelMaxHeight", ranges.panelMaxHeight],
34-
["max-bubbles", "maxBubbles", ranges.maxBubbles]
34+
["max-bubbles", "maxBubbles", ranges.maxBubbles],
35+
["ricochet", "ricochet", ranges.ricochet]
3536
] as const;
37+
// vertical and ricochet are fractions; the rest round to whole numbers.
38+
const fractional = new Set(["vertical", "ricochet"]);
3639
for (const [param, key, range] of numbers) {
3740
const raw = params.get(param);
3841
if (raw === null) continue;
3942
const value = parseClamped(raw, range.min, range.max);
40-
if (value !== undefined) config[key] = key === "vertical" ? value : Math.round(value);
43+
if (value !== undefined) config[key] = fractional.has(key) ? value : Math.round(value);
4144
}
4245
return config;
4346
};
@@ -54,6 +57,7 @@ export const writeConfig = (config: PlaygroundConfig): void => {
5457
params.set("panel-max-height", `${config.panelMaxHeight}`);
5558
}
5659
if (config.maxBubbles !== defaults.maxBubbles) params.set("max-bubbles", `${config.maxBubbles}`);
60+
if (config.ricochet !== defaults.ricochet) params.set("ricochet", `${config.ricochet}`);
5761

5862
const query = params.toString();
5963
history.replaceState(null, "", query ? `?${query}` : location.pathname);

src/physics/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export const TOUCH_VELOCITY_BOOST = 1.5;
2323
export const TOUCH_CHASE_RATE = 1.5;
2424

2525
/** Spring pull toward the wall (1/s²). */
26-
export const SPRING_STIFFNESS = 170;
26+
export const SPRING_STIFFNESS = 260;
2727

28-
/** Spring resistance (1/s). Critical damping is 2·√stiffness ≈ 26 — below that overshoots slightly. */
29-
export const SPRING_DAMPING = 20;
28+
/** Spring resistance (1/s). Critical damping is 2·√stiffness ≈ 32 — below that overshoots slightly. */
29+
export const SPRING_DAMPING = 26;
3030

3131
/** Below this speed (px/s) an axis counts as at rest. */
3232
export const REST_VELOCITY = 10;

0 commit comments

Comments
 (0)