From 816e67a814ddd5f8ae5d655e1c1afff762f31b5d Mon Sep 17 00:00:00 2001 From: Aria Minaei Date: Sat, 7 Aug 2021 11:17:30 +0200 Subject: [PATCH] Stronger visual feedback for keyframe snapping --- examples/dom-cra/src/setupTests.js | 5 +++ packages/playground/devEnv/servePlayground.ts | 2 +- theatre/devEnv/buildUtils.ts | 2 +- .../KeyframeEditor/Dot.tsx | 14 ++++++- .../KeyframeEditor/SnapCursor.svg | 6 +++ .../FrameStampPositionProvider.tsx | 37 ++++++++++++++----- .../RightOverlay/FrameStamp.tsx | 20 +++++++--- 7 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 examples/dom-cra/src/setupTests.js create mode 100644 theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SnapCursor.svg diff --git a/examples/dom-cra/src/setupTests.js b/examples/dom-cra/src/setupTests.js new file mode 100644 index 0000000..52aaef1 --- /dev/null +++ b/examples/dom-cra/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom' diff --git a/packages/playground/devEnv/servePlayground.ts b/packages/playground/devEnv/servePlayground.ts index 4c9aae8..900f43f 100644 --- a/packages/playground/devEnv/servePlayground.ts +++ b/packages/playground/devEnv/servePlayground.ts @@ -14,7 +14,7 @@ require('esbuild') { entryPoints: [path.join(playgroundDir, 'src/index.tsx')], target: ['firefox88'], - loader: {'.png': 'file', '.glb': 'file'}, + loader: {'.png': 'file', '.glb': 'file', '.svg': 'dataurl'}, bundle: true, sourcemap: true, define: definedGlobals, diff --git a/theatre/devEnv/buildUtils.ts b/theatre/devEnv/buildUtils.ts index 43665a7..39ec16b 100644 --- a/theatre/devEnv/buildUtils.ts +++ b/theatre/devEnv/buildUtils.ts @@ -14,7 +14,7 @@ export function createBundles(watch: boolean) { const esbuildConfig: Parameters[0] = { entryPoints: [path.join(pathToPackage, 'src/index.ts')], target: ['es6'], - loader: {'.png': 'file'}, + loader: {'.png': 'file', '.svg': 'dataurl'}, bundle: true, sourcemap: true, define: definedGlobals, diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx index 45f4497..c4b977c 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/Dot.tsx @@ -15,12 +15,14 @@ import type KeyframeEditor from './KeyframeEditor' import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler' +import SnapCursor from './SnapCursor.svg' export const dotSize = 6 const hitZoneSize = 12 +const snapCursorSize = 34 const dims = (size: number) => ` - left: ${-size / 2 + 1}px; + left: ${-size / 2}px; top: ${-size / 2}px; width: ${size}px; height: ${size}px; @@ -54,6 +56,16 @@ const HitZone = styled.div` #pointer-root.draggingPositionInSequenceEditor & { pointer-events: auto; + &:hover:after { + position: absolute; + top: calc(50% - ${snapCursorSize / 2}px); + left: calc(50% - ${snapCursorSize / 2}px); + width: ${snapCursorSize}px; + height: ${snapCursorSize}px; + display: block; + content: ' '; + background: url(${SnapCursor}) no-repeat 100% 100%; + } } &.beingDragged { diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SnapCursor.svg b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SnapCursor.svg new file mode 100644 index 0000000..6ea5abb --- /dev/null +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/BasicKeyframedTrack/KeyframeEditor/SnapCursor.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx b/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx index 551d2b0..b0a8cbd 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/FrameStampPositionProvider.tsx @@ -18,13 +18,23 @@ type FrameStampPositionLock = { set: (pointerPositonInUnitSpace: number) => void } +export enum FrameStampPositionType { + hidden, + locked, + snapped, + free, +} + const context = createContext<{ - currentD: IDerivation + currentD: IDerivation<[pos: number, posType: FrameStampPositionType]> getLock(): FrameStampPositionLock }>(null as $IntentionalAny) type LockItem = { - position: number + position: [ + pos: number, + posType: FrameStampPositionType.locked | FrameStampPositionType.hidden, + ] id: number } @@ -57,7 +67,7 @@ const FrameStampPositionProvider: React.FC<{ ...list, { id, - position: -1, + position: [-1, FrameStampPositionType.hidden], }, ]) @@ -79,7 +89,12 @@ const FrameStampPositionProvider: React.FC<{ newList.splice(index, 1, { id, - position: posInUnitSpace, + position: [ + posInUnitSpace, + posInUnitSpace === -1 + ? FrameStampPositionType.hidden + : FrameStampPositionType.locked, + ], }) return newList @@ -132,16 +147,17 @@ export const useLockFrameStampPosition = (shouldLock: boolean, val: number) => { */ export const attributeNameThatLocksFramestamp = 'data-theatre-lock-framestamp-to' + const pointerPositionInUnitSpace = ( layoutP: Pointer, -): IDerivation => { +): IDerivation<[pos: number, posType: FrameStampPositionType]> => { return prism(() => { const rightDims = val(layoutP.rightDims) const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace) const leftPadding = val(layoutP.scaledSpace.leftPadding) const mousePos = val(mousePositionD) - if (!mousePos) return -1 + if (!mousePos) return [-1, FrameStampPositionType.hidden] for (const el of mousePos.composedPath()) { if (!(el instanceof HTMLElement || el instanceof SVGElement)) break @@ -149,10 +165,11 @@ const pointerPositionInUnitSpace = ( if (el.hasAttribute(attributeNameThatLocksFramestamp)) { const val = el.getAttribute(attributeNameThatLocksFramestamp) if (typeof val !== 'string') continue - if (val === 'hide') return -1 + if (val === 'hide') return [-1, FrameStampPositionType.hidden] const double = parseFloat(val) - if (isFinite(double) && double >= 0) return double + if (isFinite(double) && double >= 0) + return [double, FrameStampPositionType.snapped] } } @@ -167,9 +184,9 @@ const pointerPositionInUnitSpace = ( const posInRightDims = clientX - x const posInUnitSpace = clippedSpaceToUnitSpace(posInRightDims) - return posInUnitSpace + return [posInUnitSpace, FrameStampPositionType.free] } else { - return -1 + return [-1, FrameStampPositionType.hidden] } }) } diff --git a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FrameStamp.tsx b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FrameStamp.tsx index a9cebeb..46df21c 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FrameStamp.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/RightOverlay/FrameStamp.tsx @@ -7,7 +7,10 @@ import styled from 'styled-components' import {stampsGridTheme} from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid' import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' import {topStripTheme} from './TopStrip' -import {useFrameStampPositionD} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' +import { + FrameStampPositionType, + useFrameStampPositionD, +} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' const Label = styled.div` position: absolute; @@ -23,12 +26,12 @@ const Label = styled.div` z-index: ${() => zIndexes.currentFrameStamp}; ` -const Line = styled.div` +const Line = styled.div<{posType: FrameStampPositionType}>` position: absolute; top: 5px; - left: 0; + left: -0px; bottom: 0; - width: 1px; + width: 0.5px; background: rgba(100, 100, 100, 0.2); pointer-events: none; ` @@ -36,7 +39,7 @@ const Line = styled.div` const FrameStamp: React.FC<{ layoutP: Pointer }> = React.memo(({layoutP}) => { - const posInUnitSpace = useVal(useFrameStampPositionD()) + const [posInUnitSpace, posType] = useVal(useFrameStampPositionD()) const unitSpaceToClippedSpace = useVal(layoutP.clippedSpace.fromUnitSpace) const {sequence, formatter, clippedSpaceWidth} = usePrism(() => { const sequence = val(layoutP.sheet).getSequence() @@ -48,7 +51,11 @@ const FrameStamp: React.FC<{ return <> } - const snappedPosInUnitSpace = sequence.closestGridPosition(posInUnitSpace) + const snappedPosInUnitSpace = + posType === FrameStampPositionType.free + ? sequence.closestGridPosition(posInUnitSpace) + : posInUnitSpace + const posInClippedSpace = unitSpaceToClippedSpace(snappedPosInUnitSpace) const isVisible = @@ -65,6 +72,7 @@ const FrameStamp: React.FC<{ {formatter.formatForPlayhead(snappedPosInUnitSpace)}