Files
hyperframes/packages/studio/src/components/editor/BorderRadiusEditor.tsx
T
Miguel Ángel a468550f82 feat(studio): keyframe system — parser, runtime, timeline UI, design panel, gesture recording (#1311)
* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): runtime hooks — global time compiler + keyframe runtime

Add the runtime bridge layer: global time compilation (tween % → clip %),
soft reload after mutations, runtime keyframe preview, and keyframe
commit helper.

* feat(studio): keyframe cache + commit hooks

Add hooks for keyframe cache population (tween → clip-relative %),
mutation dispatch, keyframe snapping, and audio beat detection.

* feat(studio): timeline UI — dopesheet diamonds + keyboard nav

Add dopesheet strip with diamond keyframe indicators, timeline property
rows, keyboard navigation (J/Shift+J/Delete/K), and feature gate
(STUDIO_KEYFRAMES_ENABLED defaults to false).

* feat(studio): design panel — arc controls + ease curve + stagger

Add arc path controls (curviness slider, auto-rotate), motion path SVG
overlay, ease curve visualization, stagger controls, and expanded
animation card. Includes border-radius editor dependency from #1217.

* feat(studio): gesture recording core

Add gesture recording engine with RAF sampling, modifier key property
mapping (Shift→rotationXY, Alt→rotation, Cmd→opacity),
Ramer-Douglas-Peucker simplification, and ghost trail SVG overlay.

* fix(studio): keyframe drag + recording bug bash

21 fixes: capture GSAP base at drag start, translate:none before
gsap.set, skip reapplyPathOffsets for GSAP elements, clamp recording
seek, _auto flag for 100% keyframes, overlay flash fix, block edits
during recording.

* feat(studio): keyframe integration wiring + docs

Wire App.tsx recording orchestration, TimelineToolbar K/R buttons,
PropertyPanel per-property diamonds, shortcuts panel, toast
notifications, and keyframes guide documentation. All gated on
STUDIO_KEYFRAMES_ENABLED (default false).
2026-06-09 18:30:23 -04:00

210 lines
5.4 KiB
TypeScript

import { useCallback, useState } from "react";
import { MetricField } from "./propertyPanelPrimitives";
import { formatNumericValue, parseNumericValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
type Corner = "tl" | "tr" | "br" | "bl";
interface BorderRadiusEditorProps {
tl: number;
tr: number;
br: number;
bl: number;
disabled?: boolean;
onCommit: (corner: Corner | "all", value: number) => void;
}
const PREVIEW_W = 72;
const PREVIEW_H = 52;
const MAX_RADIUS = 26;
function clampRadius(v: number): number {
return Math.max(0, Math.min(MAX_RADIUS, v));
}
function scaleRadius(v: number, maxPx: number): number {
if (maxPx <= 0) return 0;
return clampRadius(Math.round((v / Math.max(maxPx, 1)) * MAX_RADIUS));
}
export function BorderRadiusEditor({
tl,
tr,
br,
bl,
disabled,
onCommit,
}: BorderRadiusEditorProps) {
const uniform = tl === tr && tr === br && br === bl;
const [linked, setLinked] = useState(uniform);
const maxVal = Math.max(tl, tr, br, bl, 1);
const sTL = scaleRadius(tl, maxVal);
const sTR = scaleRadius(tr, maxVal);
const sBR = scaleRadius(br, maxVal);
const sBL = scaleRadius(bl, maxVal);
const handleCornerCommit = useCallback(
(corner: Corner, raw: string) => {
const v = parseNumericValue(raw) ?? 0;
if (linked) {
onCommit("all", v);
} else {
onCommit(corner, v);
}
},
[linked, onCommit],
);
const handleToggleLinked = useCallback(() => {
if (!linked && !uniform) {
onCommit("all", tl);
}
setLinked((l) => !l);
}, [linked, uniform, tl, onCommit]);
const path = buildRoundedRectPath(PREVIEW_W, PREVIEW_H, sTL, sTR, sBR, sBL);
return (
<div className="space-y-3">
<div className="flex items-center gap-3">
<svg
width={PREVIEW_W}
height={PREVIEW_H}
viewBox={`0 0 ${PREVIEW_W} ${PREVIEW_H}`}
className="flex-shrink-0"
>
<path
d={path}
fill="rgba(255,255,255,0.06)"
stroke="rgba(255,255,255,0.24)"
strokeWidth={1.5}
/>
<circle
cx={sTL}
cy={sTL}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
<circle
cx={PREVIEW_W - sTR}
cy={sTR}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
<circle
cx={PREVIEW_W - sBR}
cy={PREVIEW_H - sBR}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
<circle
cx={sBL}
cy={PREVIEW_H - sBL}
r={3}
fill={linked ? "#3b82f6" : "#a78bfa"}
className="cursor-pointer"
/>
</svg>
<button
type="button"
className="flex h-7 w-7 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
onClick={handleToggleLinked}
disabled={disabled}
title={linked ? "Unlink corners" : "Link all corners"}
>
{linked ? (
<svg
width={14}
height={14}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<path d="M6 12H4a4 4 0 010-8h2M10 4h2a4 4 0 010 8h-2M5 8h6" />
</svg>
) : (
<svg
width={14}
height={14}
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<path d="M6 12H4a4 4 0 010-8h2M10 4h2a4 4 0 010 8h-2" />
</svg>
)}
</button>
</div>
{linked ? (
<MetricField
label="All"
value={formatNumericValue(tl)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("tl", next)}
/>
) : (
<div className={RESPONSIVE_GRID}>
<MetricField
label="TL"
value={formatNumericValue(tl)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("tl", next)}
/>
<MetricField
label="TR"
value={formatNumericValue(tr)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("tr", next)}
/>
<MetricField
label="BL"
value={formatNumericValue(bl)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("bl", next)}
/>
<MetricField
label="BR"
value={formatNumericValue(br)}
disabled={disabled}
liveCommit
onCommit={(next) => handleCornerCommit("br", next)}
/>
</div>
)}
</div>
);
}
function buildRoundedRectPath(
w: number,
h: number,
tl: number,
tr: number,
br: number,
bl: number,
): string {
return [
`M ${tl} 0`,
`L ${w - tr} 0`,
`Q ${w} 0 ${w} ${tr}`,
`L ${w} ${h - br}`,
`Q ${w} ${h} ${w - br} ${h}`,
`L ${bl} ${h}`,
`Q 0 ${h} 0 ${h - bl}`,
`L 0 ${tl}`,
`Q 0 0 ${tl} 0`,
"Z",
].join(" ");
}