@hyperframes/studio 0.8.103 → 0.8.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-DG795Hwu.js → hyperframes-player-tnfCiVdR.js} +1 -1
- package/dist/assets/{index-BiEtDkae.js → index-Ca6LZlVb.js} +1 -1
- package/dist/assets/index-Cre9X4RS.css +1 -0
- package/dist/assets/{index-BPJRf2X9.js → index-DEDhPmPD.js} +215 -215
- package/dist/assets/{index-DuCZ3pQw.js → index-zpe332hb.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-QADHJFDF.js} +2 -2
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +181 -168
- package/dist/index.html +2 -2
- package/dist/index.js +1018 -919
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +8 -8
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +4 -4
- package/src/components/StudioHeader.tsx +2 -1
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +5 -6
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanel.tsx +3 -3
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayGestures.ts +13 -19
- package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
- package/src/components/editor/domEditOverlayStartGesture.ts +15 -25
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/manualEdits.test.ts +3 -9
- package/src/components/editor/manualEdits.ts +0 -1
- package/src/components/editor/manualEditsDom.ts +3 -13
- package/src/components/editor/manualEditsDomPatches.test.ts +3 -131
- package/src/components/editor/manualEditsDomPatches.ts +1 -53
- package/src/components/editor/manualEditsSnapshot.ts +3 -0
- package/src/components/editor/manualEditsTypes.ts +2 -0
- package/src/components/editor/manualOffsetDrag.test.ts +41 -0
- package/src/components/editor/manualOffsetDrag.ts +6 -27
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +42 -6
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +2 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +29 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +8 -5
- package/src/components/editor/propertyPanelTypes.ts +1 -0
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/rotateInlineSave.test.ts +111 -0
- package/src/components/editor/rotationDraft.test.ts +111 -0
- package/src/components/editor/rotationDraft.ts +145 -0
- package/src/components/editor/useDomEditNudge.test.tsx +33 -0
- package/src/components/editor/useDomEditNudge.ts +6 -2
- package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/draggedGsapPosition.ts +28 -0
- package/src/hooks/elementOffsetStager.ts +34 -13
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
- package/src/hooks/plainRotation.test.ts +122 -0
- package/src/hooks/plainRotation.ts +42 -0
- package/src/hooks/useClipboard.source.test.ts +42 -4
- package/src/hooks/useClipboard.ts +13 -3
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.ts +5 -5
- package/src/hooks/useDomGeometryCommit.test.tsx +79 -1
- package/src/hooks/useDomGeometryCommit.ts +16 -17
- package/src/hooks/useDomGeometryCommits.test.tsx +233 -33
- package/src/hooks/useDomGeometryCommits.ts +27 -80
- package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
- package/src/hooks/useGsapAwareEditing.ts +43 -42
- package/src/hooks/useInlineTextEdit.test.tsx +33 -3
- package/src/hooks/useInlineTextEdit.ts +2 -2
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.tsx +2 -2
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +0 -9
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +204 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +2 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
- /package/dist/{chunk-HIGHVWYD.js.map → chunk-QADHJFDF.js.map} +0 -0
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
* Gesture-begin functions: startGroupDrag and startGesture.
|
|
3
3
|
* These are pure "start a new gesture" operations — no draft rect updates.
|
|
4
4
|
*/
|
|
5
|
-
import { readElementGsapNumber } from "../../utils/elementGsap";
|
|
6
5
|
import { type DomEditSelection } from "./domEditing";
|
|
7
6
|
import {
|
|
7
|
+
applyManualOffsetDragDraft,
|
|
8
8
|
createManualOffsetDragMember,
|
|
9
|
-
readGsapRotation,
|
|
10
9
|
restoreManualOffsetDragMembers,
|
|
11
10
|
type ManualOffsetDragMember,
|
|
12
11
|
} from "./manualOffsetDrag";
|
|
12
|
+
import { readCssRotationTarget, readRotationBase } from "./rotationDraft";
|
|
13
13
|
import {
|
|
14
14
|
beginStudioManualEditGesture,
|
|
15
15
|
captureStudioBoxSize,
|
|
16
16
|
captureStudioPathOffset,
|
|
17
17
|
captureStudioRotation,
|
|
18
18
|
readStudioBoxSize,
|
|
19
|
-
readStudioRotation,
|
|
20
19
|
} from "./manualEdits";
|
|
21
20
|
import {
|
|
22
21
|
type OverlayRect,
|
|
@@ -32,6 +31,7 @@ import {
|
|
|
32
31
|
type UseDomEditOverlayGesturesOptions,
|
|
33
32
|
} from "./domEditOverlayGestures";
|
|
34
33
|
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
|
|
34
|
+
import { gsapWritesRotation } from "../../hooks/gsapRuntimeKeyframes";
|
|
35
35
|
import { logResize, resetResizeMoveLog } from "../../utils/resizeDebug";
|
|
36
36
|
import { logDrag, readDragPositions, resetDragMoveLog } from "../../utils/dragDebug";
|
|
37
37
|
|
|
@@ -171,18 +171,6 @@ export function startGesture(
|
|
|
171
171
|
Number.isFinite(rawContentScaleX) && rawContentScaleX > 0 ? rawContentScaleX : 1;
|
|
172
172
|
const contentScaleY =
|
|
173
173
|
Number.isFinite(rawContentScaleY) && rawContentScaleY > 0 ? rawContentScaleY : 1;
|
|
174
|
-
let resizeAnchor: GestureState["resizeAnchor"];
|
|
175
|
-
if (kind === "resize") {
|
|
176
|
-
const startBcr = sel.element.getBoundingClientRect();
|
|
177
|
-
resizeAnchor = {
|
|
178
|
-
anchorX: startBcr.x,
|
|
179
|
-
anchorY: startBcr.y,
|
|
180
|
-
baseGsapX: readElementGsapNumber(sel.element, "x") ?? 0,
|
|
181
|
-
baseGsapY: readElementGsapNumber(sel.element, "y") ?? 0,
|
|
182
|
-
pinX: 0,
|
|
183
|
-
pinY: 0,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
174
|
let initialPathOffset = captureStudioPathOffset(sel.element);
|
|
187
175
|
let manualEditDragToken: string | undefined;
|
|
188
176
|
let pathOffsetMember: ManualOffsetDragMember | undefined;
|
|
@@ -205,11 +193,9 @@ export function startGesture(
|
|
|
205
193
|
initialPathOffset = result.member.initialPathOffset;
|
|
206
194
|
manualEditDragToken = result.member.gestureToken;
|
|
207
195
|
} else {
|
|
208
|
-
// Center-anchored corner resize (CapCut model): the element scales about its
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// its center per frame (the memberless else-branch is only a defensive fallback
|
|
212
|
-
// if member creation fails, e.g. the element can't take a manual offset).
|
|
196
|
+
// Center-anchored corner resize (CapCut model): the element scales about its planted CENTER,
|
|
197
|
+
// so every corner needs the member that re-pins the center per frame (the memberless
|
|
198
|
+
// branch is only a fallback for an element that can't take a manual offset).
|
|
213
199
|
const needsAnchorOffset = kind === "resize" && sel.capabilities.canApplyManualOffset;
|
|
214
200
|
if (needsAnchorOffset) {
|
|
215
201
|
const result = createManualOffsetDragMember({
|
|
@@ -217,11 +203,14 @@ export function startGesture(
|
|
|
217
203
|
selection: sel,
|
|
218
204
|
element: sel.element,
|
|
219
205
|
rect,
|
|
206
|
+
gesture: "resize",
|
|
220
207
|
});
|
|
221
208
|
if (result.ok) {
|
|
222
209
|
pathOffsetMember = result.member;
|
|
223
210
|
initialPathOffset = result.member.initialPathOffset;
|
|
224
211
|
manualEditDragToken = result.member.gestureToken;
|
|
212
|
+
// Hold a % translate as the same px now, so a growing box can't drag it along mid-frame.
|
|
213
|
+
if (result.member.plainTranslate) applyManualOffsetDragDraft(result.member, 0, 0);
|
|
225
214
|
} else {
|
|
226
215
|
manualEditDragToken = beginStudioManualEditGesture(sel.element);
|
|
227
216
|
}
|
|
@@ -230,10 +219,11 @@ export function startGesture(
|
|
|
230
219
|
}
|
|
231
220
|
}
|
|
232
221
|
|
|
233
|
-
// Rotation base
|
|
234
|
-
// move never asks GSAP: reading a property makes it bake the CSS
|
|
235
|
-
const
|
|
236
|
-
const
|
|
222
|
+
// Rotation base: the angle the element shows. An element GSAP does not turn, or a plain-translate
|
|
223
|
+
// move, never asks GSAP: reading a property makes it bake the CSS into its transform.
|
|
224
|
+
const plain = !!pathOffsetMember?.plainTranslate || !gsapWritesRotation(sel.element);
|
|
225
|
+
const plainRotation = plain && kind === "rotate" ? readCssRotationTarget(sel.element) : null;
|
|
226
|
+
const rotation = { angle: readRotationBase(sel.element, plain) };
|
|
237
227
|
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
238
228
|
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
|
|
239
229
|
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
|
|
@@ -281,11 +271,11 @@ export function startGesture(
|
|
|
281
271
|
actualWidth,
|
|
282
272
|
actualHeight,
|
|
283
273
|
actualRotation: rotation.angle,
|
|
274
|
+
plainRotation,
|
|
284
275
|
editScaleX: rect.editScaleX,
|
|
285
276
|
editScaleY: rect.editScaleY,
|
|
286
277
|
contentScaleX,
|
|
287
278
|
contentScaleY,
|
|
288
|
-
resizeAnchor,
|
|
289
279
|
manualEditDragToken,
|
|
290
280
|
snapContext,
|
|
291
281
|
resizeHandle: kind === "resize" ? (options?.resizeHandle ?? "se") : undefined,
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
applyStudioPathOffset,
|
|
11
11
|
applyStudioPathOffsetDraft,
|
|
12
12
|
applyStudioRotation,
|
|
13
|
-
applyStudioRotationDraft,
|
|
14
13
|
beginStudioManualEditGesture,
|
|
15
14
|
captureStudioBoxSize,
|
|
16
15
|
captureStudioRotation,
|
|
@@ -210,16 +209,11 @@ describe("studio manual edits", () => {
|
|
|
210
209
|
expect(card.style.getPropertyValue("rotate")).toContain(STUDIO_ROTATION_PROP);
|
|
211
210
|
expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
|
|
212
211
|
|
|
213
|
-
applyStudioRotationDraft(card, { angle: -12.2604 });
|
|
214
|
-
expect(readStudioRotation(card)).toEqual({ angle: -12.26 });
|
|
215
|
-
expect(card.style.getPropertyValue("rotate")).toBe("calc(8deg + -12.26deg)");
|
|
216
|
-
expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
|
|
217
|
-
|
|
218
212
|
const snapshot = captureStudioRotation(card);
|
|
219
|
-
|
|
213
|
+
applyStudioRotation(card, { angle: 45 });
|
|
220
214
|
restoreStudioRotation(card, snapshot);
|
|
221
|
-
expect(readStudioRotation(card)).toEqual({ angle:
|
|
222
|
-
expect(card.style.getPropertyValue(
|
|
215
|
+
expect(readStudioRotation(card)).toEqual({ angle: 24.247 });
|
|
216
|
+
expect(card.style.getPropertyValue(STUDIO_ROTATION_PROP)).toBe("24.247deg");
|
|
223
217
|
expect(card.style.getPropertyValue("transform-origin")).toBe("center center");
|
|
224
218
|
});
|
|
225
219
|
|
|
@@ -253,9 +253,9 @@ function stripGsapTranslateFromTransform(element: HTMLElement): void {
|
|
|
253
253
|
// — as the non-GSAP path does — composes ON TOP of GSAP's transform, and the
|
|
254
254
|
// subsequent strip/reapply math compounds into a runaway matrix that flings the
|
|
255
255
|
// element off-canvas. So for GSAP-animated elements we keep `translate: none`
|
|
256
|
-
// and push the offset straight into GSAP's x/y via gsap.set; the var() offset
|
|
257
|
-
//
|
|
258
|
-
//
|
|
256
|
+
// and push the offset straight into GSAP's x/y via gsap.set; the var() offset an
|
|
257
|
+
// older Studio saved stays in the file, and GSAP re-reads it at init on reload.
|
|
258
|
+
// Returns true when handled as GSAP (caller must skip the CSS path).
|
|
259
259
|
// fallow-ignore-next-line complexity
|
|
260
260
|
function applyStudioPathOffsetViaGsap(
|
|
261
261
|
element: HTMLElement,
|
|
@@ -538,13 +538,3 @@ export function applyStudioRotation(element: HTMLElement, rotation: { angle: num
|
|
|
538
538
|
composeStudioRotationValue(element, `var(${STUDIO_ROTATION_PROP}, 0deg)`),
|
|
539
539
|
);
|
|
540
540
|
}
|
|
541
|
-
|
|
542
|
-
export function applyStudioRotationDraft(element: HTMLElement, rotation: { angle: number }): void {
|
|
543
|
-
promoteInlineForTransform(element);
|
|
544
|
-
writeStudioRotationVars(element, rotation, { updateBase: false });
|
|
545
|
-
element.setAttribute(STUDIO_ROTATION_DRAFT_ATTR, "true");
|
|
546
|
-
element.style.setProperty(
|
|
547
|
-
"rotate",
|
|
548
|
-
composeStudioRotationValue(element, `${roundTo3(rotation.angle)}deg`),
|
|
549
|
-
);
|
|
550
|
-
}
|
|
@@ -42,16 +42,14 @@ import {
|
|
|
42
42
|
STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
|
|
43
43
|
} from "./studioMotionTypes";
|
|
44
44
|
import {
|
|
45
|
-
buildPathOffsetPatches,
|
|
46
45
|
buildClearPathOffsetPatches,
|
|
47
46
|
buildBoxSizePatches,
|
|
48
47
|
buildClearBoxSizePatches,
|
|
49
|
-
buildRotationPatches,
|
|
50
48
|
buildClearRotationPatches,
|
|
51
49
|
buildMotionPatches,
|
|
52
50
|
buildClearMotionPatches,
|
|
53
51
|
} from "./manualEditsDomPatches";
|
|
54
|
-
import { applyStudioBoxSize
|
|
52
|
+
import { applyStudioBoxSize } from "./manualEditsDom";
|
|
55
53
|
|
|
56
54
|
/* ── helpers ── */
|
|
57
55
|
|
|
@@ -72,39 +70,7 @@ function assertClearCoversKeys(buildOps: PatchOperation[], clearOps: PatchOperat
|
|
|
72
70
|
|
|
73
71
|
/* ── Path offset ─────────────────────────────────────────────────────────── */
|
|
74
72
|
|
|
75
|
-
describe("
|
|
76
|
-
function populatedPathEl(): HTMLElement {
|
|
77
|
-
const e = div();
|
|
78
|
-
e.style.setProperty(STUDIO_OFFSET_X_PROP, "10px");
|
|
79
|
-
e.style.setProperty(STUDIO_OFFSET_Y_PROP, "20px");
|
|
80
|
-
e.style.setProperty("translate", "10px 20px");
|
|
81
|
-
e.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, "5px 10px");
|
|
82
|
-
e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "3px");
|
|
83
|
-
e.style.setProperty("display", "flex");
|
|
84
|
-
e.setAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, "block");
|
|
85
|
-
return e;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
it("populated: captures offset styles, attrs, display, and transform-display marker in declaration order", () => {
|
|
89
|
-
const ops = buildPathOffsetPatches(populatedPathEl());
|
|
90
|
-
expect(ops).toEqual([
|
|
91
|
-
{ type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: "10px" },
|
|
92
|
-
{ type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: "20px" },
|
|
93
|
-
{ type: "inline-style", property: "translate", value: "10px 20px" },
|
|
94
|
-
{ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
|
|
95
|
-
{ type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: "5px 10px" },
|
|
96
|
-
{ type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: "3px" },
|
|
97
|
-
{ type: "inline-style", property: "display", value: "flex" },
|
|
98
|
-
{ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: "block" },
|
|
99
|
-
]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("empty: bare element yields only the path-offset marker", () => {
|
|
103
|
-
expect(buildPathOffsetPatches(div())).toEqual([
|
|
104
|
-
{ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
|
|
105
|
-
]);
|
|
106
|
-
});
|
|
107
|
-
|
|
73
|
+
describe("buildClearPathOffsetPatches", () => {
|
|
108
74
|
it("clear: restores translate from STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR and display from STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR", () => {
|
|
109
75
|
const e = div();
|
|
110
76
|
e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "5px");
|
|
@@ -128,11 +94,6 @@ describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => {
|
|
|
128
94
|
const ops = buildClearPathOffsetPatches(e);
|
|
129
95
|
expect(ops.find((o) => o.property === "translate")?.value).toBeNull();
|
|
130
96
|
});
|
|
131
|
-
|
|
132
|
-
it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => {
|
|
133
|
-
const e = populatedPathEl();
|
|
134
|
-
assertClearCoversKeys(buildPathOffsetPatches(e), buildClearPathOffsetPatches(e));
|
|
135
|
-
});
|
|
136
97
|
});
|
|
137
98
|
|
|
138
99
|
/* ── Box size ────────────────────────────────────────────────────────────── */
|
|
@@ -287,93 +248,9 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => {
|
|
|
287
248
|
});
|
|
288
249
|
});
|
|
289
250
|
|
|
290
|
-
/* ── Combined box-size + path-offset (anchored-corner resize) ──────────────── */
|
|
291
|
-
|
|
292
|
-
describe("anchored-corner combined patch: [...buildBoxSizePatches, ...buildPathOffsetPatches]", () => {
|
|
293
|
-
// NW/NE/SW resize commits size AND anchor offset in ONE persist. The two
|
|
294
|
-
// builders read the same already-mutated element and are concatenated; this
|
|
295
|
-
// is only safe if their {type,property} keys are disjoint (no builder
|
|
296
|
-
// overwrites the other's op when the source patcher applies them in order).
|
|
297
|
-
it("concatenation of both builders emits disjoint {type,property} keys (no collision)", () => {
|
|
298
|
-
const e = div();
|
|
299
|
-
applyStudioBoxSize(e, { width: 300, height: 200 });
|
|
300
|
-
applyStudioPathOffset(e, { x: 10, y: 20 });
|
|
301
|
-
|
|
302
|
-
const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
|
|
303
|
-
const keys = combined.map(opKey);
|
|
304
|
-
expect(new Set(keys).size, `duplicate {type,property} key in combined patch: ${keys}`).toBe(
|
|
305
|
-
keys.length,
|
|
306
|
-
);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("combined patch carries BOTH markers so a soft-reload re-hydrates size and offset together", () => {
|
|
310
|
-
const e = div();
|
|
311
|
-
applyStudioBoxSize(e, { width: 300, height: 200 });
|
|
312
|
-
applyStudioPathOffset(e, { x: 10, y: 20 });
|
|
313
|
-
|
|
314
|
-
const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
|
|
315
|
-
const has = (property: string) =>
|
|
316
|
-
combined.some((op) => op.type === "attribute" && op.property === property);
|
|
317
|
-
expect(has(STUDIO_BOX_SIZE_ATTR)).toBe(true);
|
|
318
|
-
expect(has(STUDIO_PATH_OFFSET_ATTR)).toBe(true);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("order is size-first: every box-size op precedes every path-offset op", () => {
|
|
322
|
-
const e = div();
|
|
323
|
-
applyStudioBoxSize(e, { width: 300, height: 200 });
|
|
324
|
-
applyStudioPathOffset(e, { x: 10, y: 20 });
|
|
325
|
-
|
|
326
|
-
const boxKeys = new Set(buildBoxSizePatches(e).map(opKey));
|
|
327
|
-
const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
|
|
328
|
-
const lastBoxIdx = combined.reduce((acc, op, i) => (boxKeys.has(opKey(op)) ? i : acc), -1);
|
|
329
|
-
const firstOffsetIdx = combined.findIndex(
|
|
330
|
-
(op) => op.type === "attribute" && op.property === STUDIO_PATH_OFFSET_ATTR,
|
|
331
|
-
);
|
|
332
|
-
expect(firstOffsetIdx).toBeGreaterThan(lastBoxIdx);
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
|
|
336
251
|
/* ── Rotation ────────────────────────────────────────────────────────────── */
|
|
337
252
|
|
|
338
|
-
describe("
|
|
339
|
-
function populatedRotEl(): HTMLElement {
|
|
340
|
-
const e = div();
|
|
341
|
-
e.style.setProperty(STUDIO_ROTATION_PROP, "45");
|
|
342
|
-
e.style.setProperty("rotate", "45deg");
|
|
343
|
-
e.style.setProperty("transform-origin", "left center");
|
|
344
|
-
e.style.setProperty("display", "block");
|
|
345
|
-
e.setAttribute(STUDIO_ORIGINAL_ROTATE_ATTR, "0deg");
|
|
346
|
-
e.setAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, "0deg");
|
|
347
|
-
e.setAttribute(STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR, "center center");
|
|
348
|
-
e.setAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, "flex");
|
|
349
|
-
return e;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
it("populated: captures rotation styles, attrs, and transform-display marker in declaration order", () => {
|
|
353
|
-
const ops = buildRotationPatches(populatedRotEl());
|
|
354
|
-
expect(ops).toEqual([
|
|
355
|
-
{ type: "inline-style", property: STUDIO_ROTATION_PROP, value: "45" },
|
|
356
|
-
{ type: "inline-style", property: "rotate", value: "45deg" },
|
|
357
|
-
{ type: "inline-style", property: "transform-origin", value: "left center" },
|
|
358
|
-
{ type: "inline-style", property: "display", value: "block" },
|
|
359
|
-
{ type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" },
|
|
360
|
-
{ type: "attribute", property: STUDIO_ORIGINAL_ROTATE_ATTR, value: "0deg" },
|
|
361
|
-
{ type: "attribute", property: STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, value: "0deg" },
|
|
362
|
-
{
|
|
363
|
-
type: "attribute",
|
|
364
|
-
property: STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
|
|
365
|
-
value: "center center",
|
|
366
|
-
},
|
|
367
|
-
{ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: "flex" },
|
|
368
|
-
]);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("empty: bare element yields only the rotation marker", () => {
|
|
372
|
-
expect(buildRotationPatches(div())).toEqual([
|
|
373
|
-
{ type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" },
|
|
374
|
-
]);
|
|
375
|
-
});
|
|
376
|
-
|
|
253
|
+
describe("buildClearRotationPatches", () => {
|
|
377
254
|
it("clear: restores rotate and transform-origin from orig attrs, nulls draft attr", () => {
|
|
378
255
|
const e = div();
|
|
379
256
|
e.setAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, "30deg");
|
|
@@ -405,11 +282,6 @@ describe("buildRotationPatches / buildClearRotationPatches", () => {
|
|
|
405
282
|
const ops = buildClearRotationPatches(e);
|
|
406
283
|
expect(ops.find((o) => o.property === "rotate")?.value).toBeNull();
|
|
407
284
|
});
|
|
408
|
-
|
|
409
|
-
it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => {
|
|
410
|
-
const e = populatedRotEl();
|
|
411
|
-
assertClearCoversKeys(buildRotationPatches(e), buildClearRotationPatches(e));
|
|
412
|
-
});
|
|
413
285
|
});
|
|
414
286
|
|
|
415
287
|
/* ── Motion ──────────────────────────────────────────────────────────────── */
|
|
@@ -72,36 +72,6 @@ function appendTransformDisplayOps(element: HTMLElement, ops: PatchOperation[]):
|
|
|
72
72
|
|
|
73
73
|
/* ── Path offset patches ─────────────────────────────────────────── */
|
|
74
74
|
|
|
75
|
-
export function buildPathOffsetPatches(element: HTMLElement): PatchOperation[] {
|
|
76
|
-
const ops: PatchOperation[] = [];
|
|
77
|
-
collectInlineStyleOps(element, [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP], ops);
|
|
78
|
-
// When GSAP owns the element's transform, the live inline translate is kept
|
|
79
|
-
// at "none" (the offset lives in GSAP's cache — see applyStudioPathOffset).
|
|
80
|
-
// Persist the var() expression in that case, so a reload re-folds the offset.
|
|
81
|
-
const inlineTranslate = element.style.getPropertyValue("translate");
|
|
82
|
-
const hasOffsetVars =
|
|
83
|
-
element.style.getPropertyValue(STUDIO_OFFSET_X_PROP) ||
|
|
84
|
-
element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
|
|
85
|
-
const translateValue =
|
|
86
|
-
inlineTranslate && inlineTranslate !== "none"
|
|
87
|
-
? inlineTranslate
|
|
88
|
-
: hasOffsetVars
|
|
89
|
-
? `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)`
|
|
90
|
-
: null;
|
|
91
|
-
if (translateValue) {
|
|
92
|
-
ops.push({ type: "inline-style", property: "translate", value: translateValue });
|
|
93
|
-
}
|
|
94
|
-
ops.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" });
|
|
95
|
-
collectAttributeOps(
|
|
96
|
-
element,
|
|
97
|
-
[STUDIO_ORIGINAL_TRANSLATE_ATTR, STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR],
|
|
98
|
-
ops,
|
|
99
|
-
);
|
|
100
|
-
collectInlineStyleOps(element, ["display"], ops);
|
|
101
|
-
collectAttributeOps(element, [STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR], ops);
|
|
102
|
-
return ops;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
75
|
export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperation[] {
|
|
106
76
|
const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
|
|
107
77
|
const ops: PatchOperation[] = [
|
|
@@ -118,7 +88,7 @@ export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperatio
|
|
|
118
88
|
|
|
119
89
|
/* ── Box size patches ────────────────────────────────────────────── */
|
|
120
90
|
|
|
121
|
-
const BOX_SIZE_STYLE_PROPS = [
|
|
91
|
+
export const BOX_SIZE_STYLE_PROPS = [
|
|
122
92
|
"width",
|
|
123
93
|
"height",
|
|
124
94
|
"min-width",
|
|
@@ -185,28 +155,6 @@ export function buildClearBoxSizePatches(element: HTMLElement): PatchOperation[]
|
|
|
185
155
|
|
|
186
156
|
/* ── Rotation patches ────────────────────────────────────────────── */
|
|
187
157
|
|
|
188
|
-
const ROTATION_STYLE_PROPS = [
|
|
189
|
-
STUDIO_ROTATION_PROP,
|
|
190
|
-
"rotate",
|
|
191
|
-
"transform-origin",
|
|
192
|
-
"display",
|
|
193
|
-
] as const;
|
|
194
|
-
|
|
195
|
-
const ROTATION_ORIG_ATTRS = [
|
|
196
|
-
STUDIO_ORIGINAL_ROTATE_ATTR,
|
|
197
|
-
STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
|
|
198
|
-
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
|
|
199
|
-
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
|
|
200
|
-
] as const;
|
|
201
|
-
|
|
202
|
-
export function buildRotationPatches(element: HTMLElement): PatchOperation[] {
|
|
203
|
-
const ops: PatchOperation[] = [];
|
|
204
|
-
collectInlineStyleOps(element, ROTATION_STYLE_PROPS, ops);
|
|
205
|
-
ops.push({ type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" });
|
|
206
|
-
collectAttributeOps(element, ROTATION_ORIG_ATTRS, ops);
|
|
207
|
-
return ops;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
158
|
export function buildClearRotationPatches(element: HTMLElement): PatchOperation[] {
|
|
211
159
|
const origInlineRotate = element.getAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR);
|
|
212
160
|
const origRotationTransformOrigin = element.getAttribute(
|
|
@@ -82,6 +82,8 @@ export function captureStudioBoxSize(element: HTMLElement): StudioBoxSizeSnapsho
|
|
|
82
82
|
export function captureStudioRotation(element: HTMLElement): StudioRotationSnapshot {
|
|
83
83
|
return {
|
|
84
84
|
rotate: element.style.getPropertyValue("rotate"),
|
|
85
|
+
transform: element.style.getPropertyValue("transform"),
|
|
86
|
+
display: element.style.getPropertyValue("display"),
|
|
85
87
|
transformOrigin: element.style.getPropertyValue("transform-origin"),
|
|
86
88
|
studioRotation: element.style.getPropertyValue(STUDIO_ROTATION_PROP),
|
|
87
89
|
marker: element.getAttribute(STUDIO_ROTATION_ATTR),
|
|
@@ -263,6 +265,7 @@ function restoreOriginalTranslateProperty(element: HTMLElement): void {
|
|
|
263
265
|
export function clearStudioPathOffset(element: HTMLElement): void {
|
|
264
266
|
if (
|
|
265
267
|
element.hasAttribute(STUDIO_PATH_OFFSET_ATTR) ||
|
|
268
|
+
element.hasAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR) ||
|
|
266
269
|
styleUsesStudioOffset(element.style.getPropertyValue("translate"))
|
|
267
270
|
) {
|
|
268
271
|
restoreOriginalTranslateProperty(element);
|
|
@@ -652,3 +652,44 @@ describe("a move of an element GSAP does not position", () => {
|
|
|
652
652
|
expect(member().plainTranslate).toBe(false);
|
|
653
653
|
});
|
|
654
654
|
});
|
|
655
|
+
|
|
656
|
+
describe("a resize's anchor member", () => {
|
|
657
|
+
function anchorMember(tweenVars?: Record<string, number>) {
|
|
658
|
+
const window = new Window();
|
|
659
|
+
const element = window.document.createElement("div");
|
|
660
|
+
element.id = "box";
|
|
661
|
+
window.document.body.append(element);
|
|
662
|
+
const tweens = tweenVars ? [{ vars: tweenVars, targets: () => [element] }] : [];
|
|
663
|
+
Object.assign(window, { __timelines: { main: { getChildren: () => tweens } } });
|
|
664
|
+
element.getBoundingClientRect = () => {
|
|
665
|
+
const [x = 0, y = 0] = element.style
|
|
666
|
+
.getPropertyValue("translate")
|
|
667
|
+
.split(" ")
|
|
668
|
+
.map((v) => Number.parseFloat(v));
|
|
669
|
+
return new window.DOMRect(10 + (x || 0), 20 + (y || 0), 100, 50);
|
|
670
|
+
};
|
|
671
|
+
const result = createManualOffsetDragMember({
|
|
672
|
+
key: "box",
|
|
673
|
+
selection: { element } as never,
|
|
674
|
+
element,
|
|
675
|
+
rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
|
|
676
|
+
gesture: "resize",
|
|
677
|
+
});
|
|
678
|
+
if (!result.ok) throw new Error("member");
|
|
679
|
+
return result.member;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
it("keeps the centre with the element's own plain translate when GSAP does not own the box", () => {
|
|
683
|
+
const member = anchorMember();
|
|
684
|
+
expect(member.plainTranslate).toBe(true);
|
|
685
|
+
applyManualOffsetDragDraft(member, -10.25, 3.5);
|
|
686
|
+
expect(member.element.style.getPropertyValue("translate")).toBe("-10.25px 3.5px");
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it.each(["width", "maxWidth", "minHeight", "flexBasis", "scale", "scaleX"])(
|
|
690
|
+
"leaves a box whose %s GSAP tweens, which the CSS box writer also sets, to the GSAP writer",
|
|
691
|
+
(channel) => {
|
|
692
|
+
expect(anchorMember({ [channel]: 300 }).plainTranslate).toBe(false);
|
|
693
|
+
},
|
|
694
|
+
);
|
|
695
|
+
});
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type StudioPathOffsetSnapshot,
|
|
12
12
|
} from "./manualEdits";
|
|
13
13
|
import { computeDraggedGsapPosition } from "../../hooks/draggedGsapPosition";
|
|
14
|
-
import { gsapWritesPosition } from "../../hooks/gsapRuntimeKeyframes";
|
|
14
|
+
import { gsapWritesBox, gsapWritesPosition } from "../../hooks/gsapRuntimeKeyframes";
|
|
15
15
|
import { readTranslatePx, UNREADABLE_TRANSLATE, writeTranslatePx } from "./plainTranslate";
|
|
16
16
|
|
|
17
17
|
interface OffsetDragGsap {
|
|
@@ -19,7 +19,7 @@ interface OffsetDragGsap {
|
|
|
19
19
|
getProperty: (el: Element, prop: string) => number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function getOffsetDragGsap(element: HTMLElement): OffsetDragGsap | null {
|
|
22
|
+
export function getOffsetDragGsap(element: HTMLElement): OffsetDragGsap | null {
|
|
23
23
|
const win = element.ownerDocument.defaultView as
|
|
24
24
|
| (Window & { gsap?: Partial<OffsetDragGsap> })
|
|
25
25
|
| null;
|
|
@@ -53,27 +53,6 @@ function applyOffsetDragDraftViaGsap(
|
|
|
53
53
|
return true;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
* Live rotation preview through the GSAP channel — the SAME channel the commit
|
|
58
|
-
* lands in (a `tl.set`/keyframe rotation), mirroring `applyOffsetDragDraftViaGsap`.
|
|
59
|
-
* GSAP owns the transform rotation, so neutralize the CSS `rotate` longhand to keep
|
|
60
|
-
* the two channels from composing. `angle` is the absolute target rotation. Returns
|
|
61
|
-
* false when gsap is unavailable (caller falls back to the CSS draft).
|
|
62
|
-
*/
|
|
63
|
-
export function applyRotationDraftViaGsap(element: HTMLElement, angle: number): boolean {
|
|
64
|
-
const gsap = getOffsetDragGsap(element);
|
|
65
|
-
if (!gsap) return false;
|
|
66
|
-
element.style.setProperty("rotate", "none");
|
|
67
|
-
gsap.set(element, { rotation: angle });
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Current GSAP transform rotation — the single-source rotation base. 0 if gsap is unavailable. */
|
|
72
|
-
export function readGsapRotation(element: HTMLElement): number {
|
|
73
|
-
const gsap = getOffsetDragGsap(element);
|
|
74
|
-
return gsap ? Number(gsap.getProperty(element, "rotation")) || 0 : 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
56
|
const DEFAULT_OFFSET_PROBE_PX = 100;
|
|
78
57
|
const MIN_PROBE_VECTOR_LENGTH_PX = 0.01;
|
|
79
58
|
const MIN_MATRIX_DETERMINANT = 0.000001;
|
|
@@ -337,11 +316,11 @@ export function createManualOffsetDragMember(input: {
|
|
|
337
316
|
selection: DomEditSelection;
|
|
338
317
|
element: HTMLElement;
|
|
339
318
|
rect: ManualOffsetDragRect;
|
|
340
|
-
gesture?: "drag" | "nudge"; //
|
|
319
|
+
gesture?: "drag" | "nudge" | "resize"; // resize: the anchor that keeps its centre planted
|
|
341
320
|
}): ManualOffsetDragMemberResult {
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
// so a stale
|
|
321
|
+
const gsapOwns = input.gesture === "resize" ? gsapWritesBox : gsapWritesPosition;
|
|
322
|
+
const plainTranslate = !!input.gesture && !gsapOwns(input.element);
|
|
323
|
+
// The APPLIED offset, never a dormant var, so a stale one can't fling the element off-screen.
|
|
345
324
|
const initialOffset = plainTranslate
|
|
346
325
|
? readTranslatePx(input.element)
|
|
347
326
|
: readAppliedStudioPathOffset(input.element);
|
|
@@ -14,8 +14,6 @@ import {
|
|
|
14
14
|
buildDomEditStylePatchOperation,
|
|
15
15
|
buildDomEditTextPatchOperation,
|
|
16
16
|
} from "./domEditingLayers";
|
|
17
|
-
import { buildPathOffsetPatches } from "./manualEditsDomPatches";
|
|
18
|
-
import { STUDIO_OFFSET_X_PROP, STUDIO_PATH_OFFSET_ATTR } from "./manualEditsTypes";
|
|
19
17
|
import { makeSelection } from "../../hooks/domSelectionTestHarness";
|
|
20
18
|
import { buildTextFieldChildOperations } from "../../hooks/domEditTextFieldCommitOps";
|
|
21
19
|
|
|
@@ -126,22 +124,6 @@ describe("persist seam source mutation", () => {
|
|
|
126
124
|
expect(findByHfId(html, "qa-image").getAttribute("style")).toContain("opacity: 0.4");
|
|
127
125
|
});
|
|
128
126
|
|
|
129
|
-
it("persists detached jsdom path offset operations", () => {
|
|
130
|
-
const element = document.createElement("div");
|
|
131
|
-
element.style.setProperty(STUDIO_OFFSET_X_PROP, "24px");
|
|
132
|
-
|
|
133
|
-
const html = patchAndExpectChange(
|
|
134
|
-
indexHtml,
|
|
135
|
-
clientTarget({ id: "qa-shape", hfId: "qa-shape", tagName: "div" }),
|
|
136
|
-
buildPathOffsetPatches(element),
|
|
137
|
-
);
|
|
138
|
-
const shape = findByHfId(html, "qa-shape");
|
|
139
|
-
|
|
140
|
-
expect(shape.getAttribute("style")).toContain(`${STUDIO_OFFSET_X_PROP}: 24px`);
|
|
141
|
-
expect(shape.getAttribute("style")).toContain("translate: var(--hf-studio-offset-x, 0px)");
|
|
142
|
-
expect(shape.getAttribute(STUDIO_PATH_OFFSET_ATTR)).toBe("true");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
127
|
it("persists timeline data-start attribute operation", () => {
|
|
146
128
|
const html = patchAndExpectChange(
|
|
147
129
|
indexHtml,
|