mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 12:34:50 +02:00
[eric] theme: knob turns relative with seam-safe delta + stuck-drag sealed; minimize parks in 250ms
This commit is contained in:
@@ -5,7 +5,7 @@ import React, { useCallback, useRef } from 'react';
|
||||
|
||||
export const SquiggleSlider: React.FC<{ value: number; onChange: (v: number) => void; width?: number }> = ({ value, onChange, width = 170 }) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const dragging = useRef(false);
|
||||
const [dragging, setDragging] = React.useState(false);
|
||||
const apply = useCallback((clientX: number) => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
@@ -22,12 +22,14 @@ export const SquiggleSlider: React.FC<{ value: number; onChange: (v: number) =>
|
||||
<div
|
||||
ref={ref}
|
||||
title="Grain"
|
||||
onPointerDown={(e) => { dragging.current = true; (e.target as HTMLElement).setPointerCapture?.(e.pointerId); apply(e.clientX); }}
|
||||
onPointerMove={(e) => { if (dragging.current) apply(e.clientX); }}
|
||||
onPointerUp={() => { dragging.current = false; }}
|
||||
style={{ position: 'relative', width, height: H, cursor: 'pointer', touchAction: 'none', flexShrink: 0 }}
|
||||
onPointerDown={(e) => { setDragging(true); e.currentTarget.setPointerCapture?.(e.pointerId); apply(e.clientX); }}
|
||||
onPointerMove={(e) => { if (dragging) apply(e.clientX); }}
|
||||
onPointerUp={() => setDragging(false)}
|
||||
onPointerCancel={() => setDragging(false)}
|
||||
onLostPointerCapture={() => setDragging(false)}
|
||||
style={{ position: 'relative', width, height: H, cursor: dragging ? 'grabbing' : 'grab', touchAction: 'none', flexShrink: 0 }}
|
||||
>
|
||||
<svg width={width} height={H} style={{ display: 'block' }}>
|
||||
<svg width={width} height={H} style={{ display: 'block', pointerEvents: 'none' }}>
|
||||
<path d={wavePath} stroke="currentColor" strokeOpacity={0.5} strokeWidth={2} fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
<div style={{
|
||||
@@ -42,29 +44,43 @@ export const SquiggleSlider: React.FC<{ value: number; onChange: (v: number) =>
|
||||
export const Knob: React.FC<{ value: number; onChange: (v: number) => void; size?: number }> = ({ value, onChange, size = 34 }) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [grabbing, setGrabbing] = React.useState(false);
|
||||
// A real knob turns RELATIVE to where you grabbed it; jumping to the grab angle feels broken.
|
||||
const start = useRef<{ deg: number; value: number } | null>(null);
|
||||
const angle = -135 + value * 270;
|
||||
// Turn like a physical knob: the indicator chases the pointer's angle around the center.
|
||||
const applyAngle = useCallback((clientX: number, clientY: number) => {
|
||||
const pointerDeg = useCallback((clientX: number, clientY: number): number => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
if (!el) return 0;
|
||||
const r = el.getBoundingClientRect();
|
||||
const deg = Math.atan2(clientX - (r.left + r.width / 2), (r.top + r.height / 2) - clientY) * (180 / Math.PI);
|
||||
onChange(Math.min(1, Math.max(0, (Math.max(-135, Math.min(135, deg)) + 135) / 270)));
|
||||
}, [onChange]);
|
||||
return Math.atan2(clientX - (r.left + r.width / 2), (r.top + r.height / 2) - clientY) * (180 / Math.PI);
|
||||
}, []);
|
||||
const end = (): void => { setGrabbing(false); start.current = null; };
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
title="Intensity"
|
||||
onPointerDown={(e) => { setGrabbing(true); (e.target as HTMLElement).setPointerCapture?.(e.pointerId); applyAngle(e.clientX, e.clientY); }}
|
||||
onPointerMove={(e) => { if (grabbing) applyAngle(e.clientX, e.clientY); }}
|
||||
onPointerUp={() => setGrabbing(false)}
|
||||
onPointerDown={(e) => {
|
||||
setGrabbing(true);
|
||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||||
start.current = { deg: pointerDeg(e.clientX, e.clientY), value };
|
||||
}}
|
||||
onPointerMove={(e) => {
|
||||
if (!grabbing || !start.current) return;
|
||||
let delta = pointerDeg(e.clientX, e.clientY) - start.current.deg;
|
||||
// Crossing the +-180 seam must not spin the knob a full turn.
|
||||
if (delta > 180) delta -= 360;
|
||||
if (delta < -180) delta += 360;
|
||||
onChange(Math.min(1, Math.max(0, start.current.value + delta / 270)));
|
||||
}}
|
||||
onPointerUp={end}
|
||||
onPointerCancel={end}
|
||||
onLostPointerCapture={end}
|
||||
style={{
|
||||
position: 'relative', width: size + 10, height: size + 10, display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'center', cursor: grabbing ? 'grabbing' : 'grab', touchAction: 'none', flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'absolute', inset: 0, borderRadius: 999, border: '2px dotted currentColor', opacity: 0.4 }} />
|
||||
<div style={{ width: size - 8, height: size - 8, borderRadius: 999, background: '#3a3835', transform: `rotate(${angle}deg)`, display: 'flex', justifyContent: 'center' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, borderRadius: 999, border: '2px dotted currentColor', opacity: 0.4, pointerEvents: 'none' }} />
|
||||
<div style={{ width: size - 8, height: size - 8, borderRadius: 999, background: '#3a3835', transform: `rotate(${angle}deg)`, display: 'flex', justifyContent: 'center', pointerEvents: 'none' }}>
|
||||
<div style={{ width: 3, height: 9, borderRadius: 2, background: '#fff', marginTop: 2 }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -559,7 +559,8 @@ const BrowserCard: React.FC<Props> = ({
|
||||
const park = (): void => { if (parked) return; parked = true; dispatch(toggleMinimizeCard({ cardId: browserId })); };
|
||||
if (capture && typeof (capture as Promise<unknown>).then === 'function') {
|
||||
// capturePage can hang forever on off-screen guests (Electron 42); the timer guarantees the park.
|
||||
window.setTimeout(park, 800);
|
||||
// 250ms: captures land in ~100-200ms when visible, and a snappy minimize beats a perfect thumbnail.
|
||||
window.setTimeout(park, 250);
|
||||
(capture as Promise<{ toDataURL(): string }>)
|
||||
.then((img) => { saveMinimizedShot(browserId, img.toDataURL()); })
|
||||
.catch(() => undefined)
|
||||
|
||||
Reference in New Issue
Block a user