Stronger visual feedback for keyframe snapping
This commit is contained in:
parent
f4c2fb2a08
commit
816e67a814
7 changed files with 67 additions and 19 deletions
5
examples/dom-cra/src/setupTests.js
Normal file
5
examples/dom-cra/src/setupTests.js
Normal 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'
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 |
|
@ -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]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)`,
|
||||||
|
|
Loading…
Reference in a new issue