Adds independent zooms, prevents elements from effecting root or body
This commit is contained in:
parent
b00e0d3a95
commit
44e1c7dfdc
11 changed files with 148 additions and 94 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -93,7 +93,7 @@ export function useZoomEvents<T extends HTMLElement | SVGElement>(ref: React.Ref
|
|||
},
|
||||
},
|
||||
{
|
||||
target: ref.current,
|
||||
target: window,
|
||||
eventOptions: { passive: false },
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue