mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
* 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).
210 lines
5.4 KiB
TypeScript
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(" ");
|
|
}
|