Stronger visual feedback for keyframe snapping

This commit is contained in:
Aria Minaei 2021-08-07 11:17:30 +02:00
parent f4c2fb2a08
commit 816e67a814
7 changed files with 67 additions and 19 deletions

View file

@ -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'

View file

@ -14,7 +14,7 @@ require('esbuild')
{ {
entryPoints: [path.join(playgroundDir, 'src/index.tsx')], entryPoints: [path.join(playgroundDir, 'src/index.tsx')],
target: ['firefox88'], target: ['firefox88'],
loader: {'.png': 'file', '.glb': 'file'}, loader: {'.png': 'file', '.glb': 'file', '.svg': 'dataurl'},
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
define: definedGlobals, define: definedGlobals,

View file

@ -14,7 +14,7 @@ export function createBundles(watch: boolean) {
const esbuildConfig: Parameters<typeof build>[0] = { const esbuildConfig: Parameters<typeof build>[0] = {
entryPoints: [path.join(pathToPackage, 'src/index.ts')], entryPoints: [path.join(pathToPackage, 'src/index.ts')],
target: ['es6'], target: ['es6'],
loader: {'.png': 'file'}, loader: {'.png': 'file', '.svg': 'dataurl'},
bundle: true, bundle: true,
sourcemap: true, sourcemap: true,
define: definedGlobals, define: definedGlobals,

View file

@ -15,12 +15,14 @@ import type KeyframeEditor from './KeyframeEditor'
import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {useLockFrameStampPosition} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider' import {attributeNameThatLocksFramestamp} from '@theatre/studio/panels/SequenceEditorPanel/FrameStampPositionProvider'
import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler' import {useCursorLock} from '@theatre/studio/uiComponents/PointerEventsHandler'
import SnapCursor from './SnapCursor.svg'
export const dotSize = 6 export const dotSize = 6
const hitZoneSize = 12 const hitZoneSize = 12
const snapCursorSize = 34
const dims = (size: number) => ` const dims = (size: number) => `
left: ${-size / 2 + 1}px; left: ${-size / 2}px;
top: ${-size / 2}px; top: ${-size / 2}px;
width: ${size}px; width: ${size}px;
height: ${size}px; height: ${size}px;
@ -54,6 +56,16 @@ const HitZone = styled.div`
#pointer-root.draggingPositionInSequenceEditor & { #pointer-root.draggingPositionInSequenceEditor & {
pointer-events: auto; 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 { &.beingDragged {

View file

@ -0,0 +1,6 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7V1H7" stroke="#74FFDE" stroke-width="0.25" />
<path d="M7 33H1L1 27" stroke="#74FFDE" stroke-width="0.25" />
<path d="M33 27V33H27" stroke="#74FFDE" stroke-width="0.25" />
<path d="M27 1L33 1V7" stroke="#74FFDE" stroke-width="0.25" />
</svg>

After

Width:  |  Height:  |  Size: 358 B

View file

@ -18,13 +18,23 @@ type FrameStampPositionLock = {
set: (pointerPositonInUnitSpace: number) => void set: (pointerPositonInUnitSpace: number) => void
} }
export enum FrameStampPositionType {
hidden,
locked,
snapped,
free,
}
const context = createContext<{ const context = createContext<{
currentD: IDerivation<number> currentD: IDerivation<[pos: number, posType: FrameStampPositionType]>
getLock(): FrameStampPositionLock getLock(): FrameStampPositionLock
}>(null as $IntentionalAny) }>(null as $IntentionalAny)
type LockItem = { type LockItem = {
position: number position: [
pos: number,
posType: FrameStampPositionType.locked | FrameStampPositionType.hidden,
]
id: number id: number
} }
@ -57,7 +67,7 @@ const FrameStampPositionProvider: React.FC<{
...list, ...list,
{ {
id, id,
position: -1, position: [-1, FrameStampPositionType.hidden],
}, },
]) ])
@ -79,7 +89,12 @@ const FrameStampPositionProvider: React.FC<{
newList.splice(index, 1, { newList.splice(index, 1, {
id, id,
position: posInUnitSpace, position: [
posInUnitSpace,
posInUnitSpace === -1
? FrameStampPositionType.hidden
: FrameStampPositionType.locked,
],
}) })
return newList return newList
@ -132,16 +147,17 @@ export const useLockFrameStampPosition = (shouldLock: boolean, val: number) => {
*/ */
export const attributeNameThatLocksFramestamp = export const attributeNameThatLocksFramestamp =
'data-theatre-lock-framestamp-to' 'data-theatre-lock-framestamp-to'
const pointerPositionInUnitSpace = ( const pointerPositionInUnitSpace = (
layoutP: Pointer<SequenceEditorPanelLayout>, layoutP: Pointer<SequenceEditorPanelLayout>,
): IDerivation<number> => { ): IDerivation<[pos: number, posType: FrameStampPositionType]> => {
return prism(() => { return prism(() => {
const rightDims = val(layoutP.rightDims) const rightDims = val(layoutP.rightDims)
const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace) const clippedSpaceToUnitSpace = val(layoutP.clippedSpace.toUnitSpace)
const leftPadding = val(layoutP.scaledSpace.leftPadding) const leftPadding = val(layoutP.scaledSpace.leftPadding)
const mousePos = val(mousePositionD) const mousePos = val(mousePositionD)
if (!mousePos) return -1 if (!mousePos) return [-1, FrameStampPositionType.hidden]
for (const el of mousePos.composedPath()) { for (const el of mousePos.composedPath()) {
if (!(el instanceof HTMLElement || el instanceof SVGElement)) break if (!(el instanceof HTMLElement || el instanceof SVGElement)) break
@ -149,10 +165,11 @@ const pointerPositionInUnitSpace = (
if (el.hasAttribute(attributeNameThatLocksFramestamp)) { if (el.hasAttribute(attributeNameThatLocksFramestamp)) {
const val = el.getAttribute(attributeNameThatLocksFramestamp) const val = el.getAttribute(attributeNameThatLocksFramestamp)
if (typeof val !== 'string') continue if (typeof val !== 'string') continue
if (val === 'hide') return -1 if (val === 'hide') return [-1, FrameStampPositionType.hidden]
const double = parseFloat(val) 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 posInRightDims = clientX - x
const posInUnitSpace = clippedSpaceToUnitSpace(posInRightDims) const posInUnitSpace = clippedSpaceToUnitSpace(posInRightDims)
return posInUnitSpace return [posInUnitSpace, FrameStampPositionType.free]
} else { } else {
return -1 return [-1, FrameStampPositionType.hidden]
} }
}) })
} }

View file

@ -7,7 +7,10 @@ import styled from 'styled-components'
import {stampsGridTheme} from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid' import {stampsGridTheme} from '@theatre/studio/panels/SequenceEditorPanel/FrameGrid/StampsGrid'
import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel' import {zIndexes} from '@theatre/studio/panels/SequenceEditorPanel/SequenceEditorPanel'
import {topStripTheme} from './TopStrip' 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` const Label = styled.div`
position: absolute; position: absolute;
@ -23,12 +26,12 @@ const Label = styled.div`
z-index: ${() => zIndexes.currentFrameStamp}; z-index: ${() => zIndexes.currentFrameStamp};
` `
const Line = styled.div` const Line = styled.div<{posType: FrameStampPositionType}>`
position: absolute; position: absolute;
top: 5px; top: 5px;
left: 0; left: -0px;
bottom: 0; bottom: 0;
width: 1px; width: 0.5px;
background: rgba(100, 100, 100, 0.2); background: rgba(100, 100, 100, 0.2);
pointer-events: none; pointer-events: none;
` `
@ -36,7 +39,7 @@ const Line = styled.div`
const FrameStamp: React.FC<{ const FrameStamp: React.FC<{
layoutP: Pointer<SequenceEditorPanelLayout> layoutP: Pointer<SequenceEditorPanelLayout>
}> = React.memo(({layoutP}) => { }> = React.memo(({layoutP}) => {
const posInUnitSpace = useVal(useFrameStampPositionD()) const [posInUnitSpace, posType] = useVal(useFrameStampPositionD())
const unitSpaceToClippedSpace = useVal(layoutP.clippedSpace.fromUnitSpace) const unitSpaceToClippedSpace = useVal(layoutP.clippedSpace.fromUnitSpace)
const {sequence, formatter, clippedSpaceWidth} = usePrism(() => { const {sequence, formatter, clippedSpaceWidth} = usePrism(() => {
const sequence = val(layoutP.sheet).getSequence() const sequence = val(layoutP.sheet).getSequence()
@ -48,7 +51,11 @@ const FrameStamp: React.FC<{
return <></> return <></>
} }
const snappedPosInUnitSpace = sequence.closestGridPosition(posInUnitSpace) const snappedPosInUnitSpace =
posType === FrameStampPositionType.free
? sequence.closestGridPosition(posInUnitSpace)
: posInUnitSpace
const posInClippedSpace = unitSpaceToClippedSpace(snappedPosInUnitSpace) const posInClippedSpace = unitSpaceToClippedSpace(snappedPosInUnitSpace)
const isVisible = const isVisible =
@ -65,6 +72,7 @@ const FrameStamp: React.FC<{
{formatter.formatForPlayhead(snappedPosInUnitSpace)} {formatter.formatForPlayhead(snappedPosInUnitSpace)}
</Label> </Label>
<Line <Line
posType={posType}
style={{ style={{
opacity: isVisible ? 1 : 0, opacity: isVisible ? 1 : 0,
transform: `translate3d(${posInClippedSpace}px, 0, 0)`, transform: `translate3d(${posInClippedSpace}px, 0, 0)`,