Adds independent zooms, prevents elements from effecting root or body

This commit is contained in:
Steve Ruiz 2021-09-09 14:06:45 +01:00
parent b00e0d3a95
commit 44e1c7dfdc
11 changed files with 148 additions and 94 deletions

View file

@ -11,10 +11,18 @@ interface BoundsProps {
bounds: TLBounds
rotation: number
isLocked: boolean
viewportWidth: number
}
export function Bounds({ zoom, bounds, rotation, isLocked }: BoundsProps): JSX.Element {
const size = (Utils.isMobileSize() ? 10 : 8) / zoom // Touch target size
export function Bounds({
zoom,
bounds,
viewportWidth,
rotation,
isLocked,
}: BoundsProps): JSX.Element {
const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom // Touch target size
const size = 8 / zoom // Touch target size
const center = Utils.getBoundsCenter(bounds)
return (
@ -28,15 +36,50 @@ export function Bounds({ zoom, bounds, rotation, isLocked }: BoundsProps): JSX.E
<CenterHandle bounds={bounds} isLocked={isLocked} />
{!isLocked && (
<>
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Top} />
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Right} />
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Bottom} />
<EdgeHandle size={size} bounds={bounds} edge={TLBoundsEdge.Left} />
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.TopLeft} />
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.TopRight} />
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.BottomRight} />
<CornerHandle size={size} bounds={bounds} corner={TLBoundsCorner.BottomLeft} />
<RotateHandle size={size} bounds={bounds} />
<EdgeHandle targetSize={targetSize} size={size} bounds={bounds} edge={TLBoundsEdge.Top} />
<EdgeHandle
targetSize={targetSize}
size={size}
bounds={bounds}
edge={TLBoundsEdge.Right}
/>
<EdgeHandle
targetSize={targetSize}
size={size}
bounds={bounds}
edge={TLBoundsEdge.Bottom}
/>
<EdgeHandle
targetSize={targetSize}
size={size}
bounds={bounds}
edge={TLBoundsEdge.Left}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
corner={TLBoundsCorner.TopLeft}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
corner={TLBoundsCorner.TopRight}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
corner={TLBoundsCorner.BottomRight}
/>
<CornerHandle
targetSize={targetSize}
size={size}
bounds={bounds}
corner={TLBoundsCorner.BottomLeft}
/>
<RotateHandle targetSize={targetSize} size={size} bounds={bounds} />
</>
)}
</g>

View file

@ -9,16 +9,15 @@ const cornerBgClassnames = {
[TLBoundsCorner.BottomLeft]: 'tl-transparent tl-cursor-nesw',
}
interface CornerHandleProps {
size: number
targetSize: number
bounds: TLBounds
corner: TLBoundsCorner
}
export const CornerHandle = React.memo(
({
size,
corner,
bounds,
}: {
size: number
bounds: TLBounds
corner: TLBoundsCorner
}): JSX.Element => {
({ size, targetSize, corner, bounds }: CornerHandleProps): JSX.Element => {
const events = useBoundsHandleEvents(corner)
const isTop = corner === TLBoundsCorner.TopLeft || corner === TLBoundsCorner.TopRight
@ -28,19 +27,20 @@ export const CornerHandle = React.memo(
<g>
<rect
className={cornerBgClassnames[corner]}
x={(isLeft ? -1 : bounds.width + 1) - size}
y={(isTop ? -1 : bounds.height + 1) - size}
width={size * 2}
height={size * 2}
x={(isLeft ? -1 : bounds.width + 1) - targetSize}
y={(isTop ? -1 : bounds.height + 1) - targetSize}
width={targetSize * 2}
height={targetSize * 2}
pointerEvents="all"
fill="red"
{...events}
/>
<rect
className="tl-corner-handle"
x={(isLeft ? -1 : bounds.width + 1) - 4}
y={(isTop ? -1 : bounds.height + 1) - 4}
width={8}
height={8}
x={(isLeft ? -1 : bounds.width + 1) - size / 2}
y={(isTop ? -1 : bounds.height + 1) - size / 2}
width={size}
height={size}
pointerEvents="none"
/>
</g>

View file

@ -9,24 +9,29 @@ const edgeClassnames = {
[TLBoundsEdge.Left]: 'tl-transparent tl-cursor-ew',
}
export const EdgeHandle = React.memo(
({ size, bounds, edge }: { size: number; bounds: TLBounds; edge: TLBoundsEdge }): JSX.Element => {
const events = useBoundsHandleEvents(edge)
interface EdgeHandleProps {
targetSize: number
size: number
bounds: TLBounds
edge: TLBoundsEdge
}
const isHorizontal = edge === TLBoundsEdge.Top || edge === TLBoundsEdge.Bottom
const isFarEdge = edge === TLBoundsEdge.Right || edge === TLBoundsEdge.Bottom
export const EdgeHandle = React.memo(({ size, bounds, edge }: EdgeHandleProps): JSX.Element => {
const events = useBoundsHandleEvents(edge)
const { height, width } = bounds
const isHorizontal = edge === TLBoundsEdge.Top || edge === TLBoundsEdge.Bottom
const isFarEdge = edge === TLBoundsEdge.Right || edge === TLBoundsEdge.Bottom
return (
<rect
className={edgeClassnames[edge]}
x={isHorizontal ? size / 2 : (isFarEdge ? width + 1 : -1) - size / 2}
y={isHorizontal ? (isFarEdge ? height + 1 : -1) - size / 2 : size / 2}
width={isHorizontal ? Math.max(0, width + 1 - size) : size}
height={isHorizontal ? size : Math.max(0, height + 1 - size)}
{...events}
/>
)
}
)
const { height, width } = bounds
return (
<rect
className={edgeClassnames[edge]}
x={isHorizontal ? size / 2 : (isFarEdge ? width + 1 : -1) - size / 2}
y={isHorizontal ? (isFarEdge ? height + 1 : -1) - size / 2 : size / 2}
width={isHorizontal ? Math.max(0, width + 1 - size) : size}
height={isHorizontal ? size : Math.max(0, height + 1 - size)}
{...events}
/>
)
})

View file

@ -2,18 +2,23 @@ import * as React from 'react'
import { useBoundsHandleEvents } from '+hooks'
import type { TLBounds } from '+types'
interface RotateHandleProps {
bounds: TLBounds
size: number
targetSize: number
}
export const RotateHandle = React.memo(
({ bounds, size }: { bounds: TLBounds; size: number }): JSX.Element => {
({ bounds, targetSize, size }: RotateHandleProps): JSX.Element => {
const events = useBoundsHandleEvents('rotate')
return (
<g cursor="grab">
<circle
className="tl-transparent"
cx={bounds.width / 2}
cy={size * -2}
r={size * 2}
fill="transparent"
stroke="none"
r={targetSize * 2}
pointerEvents="all"
{...events}
/>
@ -21,7 +26,7 @@ export const RotateHandle = React.memo(
className="tl-rotate-handle"
cx={bounds.width / 2}
cy={size * -2}
r={4}
r={size / 2}
pointerEvents="none"
/>
</g>

View file

@ -37,7 +37,8 @@ export function Canvas<T extends TLShape>({
}: CanvasProps<T>): JSX.Element {
const rCanvas = React.useRef<SVGSVGElement>(null)
const rContainer = React.useRef<HTMLDivElement>(null)
const rGroup = useCameraCss(pageState)
useResizeObserver(rCanvas)
useZoomEvents(rCanvas)
@ -47,7 +48,7 @@ export function Canvas<T extends TLShape>({
const events = useCanvasEvents()
useResizeObserver(rCanvas)
const rGroup = useCameraCss(rContainer, pageState)
return (
<div className="tl-container" ref={rContainer}>

View file

@ -29,8 +29,6 @@ export function Page<T extends TLShape>({
}: PageProps<T>): JSX.Element {
const { callbacks, shapeUtils, inputs } = useTLContext()
useRenderOnResize()
const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange)
const { shapeWithHandles } = useHandles(page, pageState)
@ -50,7 +48,13 @@ export function Page<T extends TLShape>({
<ShapeNode key={node.shape.id} utils={shapeUtils} {...node} />
))}
{bounds && !hideBounds && (
<Bounds zoom={zoom} bounds={bounds} isLocked={isLocked} rotation={rotation} />
<Bounds
zoom={zoom}
bounds={bounds}
viewportWidth={inputs.size[0]}
isLocked={isLocked}
rotation={rotation}
/>
)}
{!hideIndicators &&
selectedIds

View file

@ -1,12 +1,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import type { TLPageState } from '+types'
export function useCameraCss(pageState: TLPageState) {
export function useCameraCss(ref: React.RefObject<HTMLDivElement>, pageState: TLPageState) {
const rGroup = React.useRef<SVGGElement>(null)
// Update the tl-zoom CSS variable when the zoom changes
React.useEffect(() => {
document.documentElement.style.setProperty('--tl-zoom', pageState.camera.zoom.toString())
ref.current!.style.setProperty('--tl-zoom', pageState.camera.zoom.toString())
}, [pageState.camera.zoom])
// Update the group's position when the camera moves or zooms

View file

@ -4,14 +4,16 @@ import { Utils } from '+utils'
export function useResizeObserver<T extends HTMLElement | SVGElement>(ref: React.RefObject<T>) {
const { inputs } = useTLContext()
const forceUpdate = React.useReducer((x) => x + 1, 0)[1]
const updateOffsets = React.useCallback(() => {
const rect = ref.current?.getBoundingClientRect()
if (rect) {
inputs.offset = [rect.left, rect.top]
inputs.size = [rect.width, rect.height]
forceUpdate()
}
}, [ref])
}, [ref, forceUpdate])
React.useEffect(() => {
const debouncedUpdateOffsets = Utils.debounce(updateOffsets, 100)

View file

@ -108,15 +108,25 @@ const tlcss = css`
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
html,
* {
.tl-container {
--tl-zoom: 1;
--tl-scale: calc(1 / var(--tl-zoom));
position: relative;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 0px;
margin: 0px;
touch-action: none;
overscroll-behavior: none;
overscroll-behavior-x: none;
background-color: var(--tl-background);
}
.tl-container * {
user-select: none;
box-sizing: border-box;
}
:root {
--tl-zoom: 1;
--tl-scale: calc(1 / var(--tl-zoom));
}
.tl-counter-scaled {
transform: scale(var(--tl-scale));
}
@ -194,21 +204,6 @@ const tlcss = css`
touch-action: none;
pointer-events: all;
}
.tl-container {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 0px;
margin: 0px;
touch-action: none;
overscroll-behavior: none;
overscroll-behavior-x: none;
background-color: var(--tl-background);
}
.tl-container * {
user-select: none;
}
.tl-dot {
fill: var(--tl-background);
stroke: var(--tl-foreground);

View file

@ -93,7 +93,7 @@ export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.Ref
},
},
{
target: ref.current,
target: window,
eventOptions: { passive: false },
}
)

View file

@ -218,24 +218,11 @@ function InnerTldraw({
)
}
const Spacer = styled('div', {
flexGrow: 2,
})
const MenuButtons = styled('div', {
display: 'flex',
gap: 8,
})
const Layout = styled('div', {
overflow: 'hidden',
position: 'absolute',
height: '100%',
width: '100%',
top: 0,
right: 0,
bottom: 0,
left: 0,
padding: '8px 8px 0 8px',
display: 'flex',
alignItems: 'flex-start',
@ -243,6 +230,7 @@ const Layout = styled('div', {
boxSizing: 'border-box',
outline: 'none',
pointerEvents: 'none',
zIndex: 100,
'& > *': {
pointerEvents: 'all',
@ -254,5 +242,15 @@ const Layout = styled('div', {
left: 0,
width: '100%',
height: '100%',
zIndex: 100,
},
})
const Spacer = styled('div', {
flexGrow: 2,
})
const MenuButtons = styled('div', {
display: 'flex',
gap: 8,
})