@ -34,6 +34,7 @@ import {
loadScene ,
calculateScrollCenter ,
loadFromBlob ,
getZoomOrigin ,
} from "./scene" ;
import { renderScene } from "./renderer" ;
@ -77,6 +78,8 @@ import {
actionChangeFontFamily ,
actionChangeViewBackgroundColor ,
actionClearCanvas ,
actionZoomIn ,
actionZoomOut ,
actionChangeProjectName ,
actionChangeExportBackground ,
actionLoadScene ,
@ -127,17 +130,53 @@ const MOUSE_BUTTON = {
SECONDARY : 2 ,
} ;
let lastCanvasWidth = - 1 ;
let lastCanvasHeight = - 1 ;
let lastMouseUp : ( ( e : any ) = > void ) | null = null ;
export function viewportCoordsToSceneCoords (
{ clientX , clientY } : { clientX : number ; clientY : number } ,
{ scrollX , scrollY } : { scrollX : number ; scrollY : number } ,
{
scrollX ,
scrollY ,
zoom ,
} : {
scrollX : number ;
scrollY : number ;
zoom : number ;
} ,
canvas : HTMLCanvasElement | null ,
) {
const x = clientX - scrollX ;
const y = clientY - scrollY ;
const zoomOrigin = getZoomOrigin ( canvas ) ;
const clientXWithZoom = zoomOrigin . x + ( clientX - zoomOrigin . x ) / zoom ;
const clientYWithZoom = zoomOrigin . y + ( clientY - zoomOrigin . y ) / zoom ;
const x = clientXWithZoom - scrollX ;
const y = clientYWithZoom - scrollY ;
return { x , y } ;
}
export function sceneCoordsToViewportCoords (
{ sceneX , sceneY } : { sceneX : number ; sceneY : number } ,
{
scrollX ,
scrollY ,
zoom ,
} : {
scrollX : number ;
scrollY : number ;
zoom : number ;
} ,
canvas : HTMLCanvasElement | null ,
) {
const zoomOrigin = getZoomOrigin ( canvas ) ;
const sceneXWithZoomAndScroll =
zoomOrigin . x - ( zoomOrigin . x - sceneX - scrollX ) * zoom ;
const sceneYWithZoomAndScroll =
zoomOrigin . y - ( zoomOrigin . y - sceneY - scrollY ) * zoom ;
const x = sceneXWithZoomAndScroll ;
const y = sceneYWithZoomAndScroll ;
return { x , y } ;
}
@ -320,6 +359,20 @@ const LayerUI = React.memo(
) ;
}
function renderZoomActions() {
return (
< Stack.Col gap = { 1 } >
< Stack.Row gap = { 1 } align = "center" >
{ actionManager . renderAction ( "zoomIn" ) }
{ actionManager . renderAction ( "zoomOut" ) }
< div style = { { marginLeft : 4 } } >
{ ( appState . zoom * 100 ) . toFixed ( 0 ) } %
< / div >
< / Stack.Row >
< / Stack.Col >
) ;
}
return (
< >
< FixedSideContainer side = "top" >
@ -370,6 +423,16 @@ const LayerUI = React.memo(
< / section >
< div / >
< / div >
< div className = "App-menu App-menu_bottom" >
< Stack.Col gap = { 2 } >
< section aria - labelledby = "canvas-zoom-actions-title" >
< h2 className = "visually-hidden" id = "canvas-zoom-actions-title" >
{ t ( "headings.canvasActions" ) }
< / h2 >
< Island padding = { 1 } > { renderZoomActions ( ) } < / Island >
< / section >
< / Stack.Col >
< / div >
< / FixedSideContainer >
< footer role = "contentinfo" >
< HintViewer
@ -459,6 +522,8 @@ export class App extends React.Component<any, AppState> {
this . actionManager . registerAction ( actionChangeViewBackgroundColor ) ;
this . actionManager . registerAction ( actionClearCanvas ) ;
this . actionManager . registerAction ( actionZoomIn ) ;
this . actionManager . registerAction ( actionZoomOut ) ;
this . actionManager . registerAction ( actionChangeProjectName ) ;
this . actionManager . registerAction ( actionChangeExportBackground ) ;
@ -684,8 +749,6 @@ export class App extends React.Component<any, AppState> {
}
} ;
private removeWheelEventListener : ( ( ) = > void ) | undefined ;
private copyToAppClipboard = ( ) = > {
copyToAppClipboard ( elements ) ;
} ;
@ -708,6 +771,7 @@ export class App extends React.Component<any, AppState> {
const { x , y } = viewportCoordsToSceneCoords (
{ clientX : cursorX , clientY : cursorY } ,
this . state ,
this . canvas ,
) ;
const element = newTextElement (
@ -759,8 +823,13 @@ export class App extends React.Component<any, AppState> {
} ;
public render() {
const canvasWidth = window . innerWidth ;
const canvasHeight = window . innerHeight ;
const canvasDOMWidth = window . innerWidth ;
const canvasDOMHeight = window . innerHeight ;
const canvasScale = window . devicePixelRatio ;
const canvasWidth = canvasDOMWidth * canvasScale ;
const canvasHeight = canvasDOMHeight * canvasScale ;
return (
< div className = "container" >
@ -777,46 +846,43 @@ export class App extends React.Component<any, AppState> {
< canvas
id = "canvas"
style = { {
width : canvas Width,
height : canvas Height,
width : canvas DOM Width,
height : canvas DOM Height,
} }
width = { canvasWidth * window . devicePixelRatio }
height = { canvasHeight * window . devicePixelRatio }
width = { canvasWidth }
height = { canvasHeight }
ref = { canvas = > {
if ( this . canvas === null ) {
// canvas is null when unmounting
if ( canvas !== null ) {
this . canvas = canvas ;
this . rc = rough . canvas ( this . canvas ! ) ;
}
if ( this . removeWheelEventListener ) {
this . removeWheelEventListener ( ) ;
this . removeWheelEventListener = undefined ;
}
if ( canvas ) {
canvas . addEventListener ( "wheel" , this . handleWheel , {
this . rc = rough . canvas ( this . canvas ) ;
this . canvas . addEventListener ( "wheel" , this . handleWheel , {
passive : false ,
} ) ;
this . removeWheelEventListener = ( ) = >
canvas . removeEventListener ( "wheel" , this . handleWheel ) ;
// Whenever React sets the width/height of the canvas element,
// the context loses the scale transform. We need to re-apply it
if (
canvasWidth !== lastCanvasWidth ||
canvasHeight !== lastCanvasHeight
) {
lastCanvasWidth = canvasWidth ;
lastCanvasHeight = canvasHeight ;
canvas
. getContext ( "2d" ) !
. scale ( window . devicePixelRatio , window . devicePixelRatio ) ;
}
this . canvas
. getContext ( "2d" )
? . setTransform ( canvasScale , 0 , 0 , canvasScale , 0 , 0 ) ;
} else {
this . canvas ? . removeEventListener ( "wheel" , this . handleWheel ) ;
}
} }
onContextMenu = { e = > {
e . preventDefault ( ) ;
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
const element = getElementAtPosition ( elements , x , y ) ;
const element = getElementAtPosition (
elements ,
x ,
y ,
this . state . zoom ,
) ;
if ( ! element ) {
ContextMenu . push ( {
options : [
@ -887,8 +953,8 @@ export class App extends React.Component<any, AppState> {
lastY = e . clientY ;
this . setState ( {
scrollX : this.state.scrollX - deltaX ,
scrollY : this.state.scrollY - deltaY ,
scrollX : this.state.scrollX - deltaX / this . state . zoom ,
scrollY : this.state.scrollY - deltaY / this . state . zoom ,
} ) ;
} ;
const teardown = ( lastMouseUp = ( ) = > {
@ -933,11 +999,14 @@ export class App extends React.Component<any, AppState> {
e . clientY / window . devicePixelRatio ,
canvasWidth / window . devicePixelRatio ,
canvasHeight / window . devicePixelRatio ,
this . state . scrollX ,
this . state . scrollY ,
this . state ,
) ;
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
const originX = x ;
const originY = y ;
@ -972,7 +1041,7 @@ export class App extends React.Component<any, AppState> {
const resizeElement = getElementWithResizeHandler (
elements ,
{ x , y } ,
this . state ,
this . state .zoom ,
) ;
this . setState ( {
resizingElement : resizeElement ? resizeElement.element : null ,
@ -985,7 +1054,12 @@ export class App extends React.Component<any, AppState> {
) ;
isResizingElements = true ;
} else {
hitElement = getElementAtPosition ( elements , x , y ) ;
hitElement = getElementAtPosition (
elements ,
x ,
y ,
this . state . zoom ,
) ;
// clear selection if shift is not clicked
if ( ! hitElement ? . isSelected && ! e . shiftKey ) {
elements = clearSelection ( elements ) ;
@ -1061,6 +1135,7 @@ export class App extends React.Component<any, AppState> {
strokeColor : this.state.currentItemStrokeColor ,
opacity : this.state.currentItemOpacity ,
font : this.state.currentItemFont ,
zoom : this.state.zoom ,
onSubmit : text = > {
if ( text ) {
elements = [
@ -1211,7 +1286,9 @@ export class App extends React.Component<any, AppState> {
if ( isOverHorizontalScrollBar ) {
const x = e . clientX ;
const dx = x - lastX ;
this . setState ( { scrollX : this.state.scrollX - dx } ) ;
this . setState ( {
scrollX : this.state.scrollX - dx / this . state . zoom ,
} ) ;
lastX = x ;
return ;
}
@ -1219,7 +1296,9 @@ export class App extends React.Component<any, AppState> {
if ( isOverVerticalScrollBar ) {
const y = e . clientY ;
const dy = y - lastY ;
this . setState ( { scrollY : this.state.scrollY - dy } ) ;
this . setState ( {
scrollY : this.state.scrollY - dy / this . state . zoom ,
} ) ;
lastY = y ;
return ;
}
@ -1233,7 +1312,11 @@ export class App extends React.Component<any, AppState> {
( this . state . elementType === "arrow" ||
this . state . elementType === "line" )
) {
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
if ( distance2d ( x , y , originX , originY ) < DRAGGING_THRESHOLD ) {
return ;
}
@ -1244,7 +1327,11 @@ export class App extends React.Component<any, AppState> {
const el = this . state . resizingElement ;
const selectedElements = elements . filter ( el = > el . isSelected ) ;
if ( selectedElements . length === 1 ) {
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
const deltaX = x - lastX ;
const deltaY = y - lastY ;
const element = selectedElements [ 0 ] ;
@ -1467,7 +1554,11 @@ export class App extends React.Component<any, AppState> {
draggingOccurred = true ;
const selectedElements = elements . filter ( el = > el . isSelected ) ;
if ( selectedElements . length ) {
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
selectedElements . forEach ( element = > {
element . x += x - lastX ;
@ -1487,7 +1578,11 @@ export class App extends React.Component<any, AppState> {
return ;
}
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
let width = distance ( originX , x ) ;
let height = distance ( originY , y ) ;
@ -1581,7 +1676,11 @@ export class App extends React.Component<any, AppState> {
this . setState ( { } ) ;
}
if ( ! draggingOccurred && draggingElement && ! multiElement ) {
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
draggingElement . points . push ( [
x - draggingElement . x ,
y - draggingElement . y ,
@ -1696,9 +1795,18 @@ export class App extends React.Component<any, AppState> {
onDoubleClick = { e = > {
resetCursor ( ) ;
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
const elementAtPosition = getElementAtPosition ( elements , x , y ) ;
const elementAtPosition = getElementAtPosition (
elements ,
x ,
y ,
this . state . zoom ,
) ;
const element =
elementAtPosition && isTextElement ( elementAtPosition )
@ -1730,18 +1838,26 @@ export class App extends React.Component<any, AppState> {
) ;
this . setState ( { } ) ;
textX =
this . state . scrollX +
elementAtPosition . x +
elementAtPosition . width / 2 ;
textY =
this . state . scrollY +
elementAtPosition . y +
elementAtPosition . height / 2 ;
const centerElementX =
elementAtPosition . x + elementAtPosition . width / 2 ;
const centerElementY =
elementAtPosition . y + elementAtPosition . height / 2 ;
const {
x : centerElementXInViewport ,
y : centerElementYInViewport ,
} = sceneCoordsToViewportCoords (
{ sceneX : centerElementX , sceneY : centerElementY } ,
this . state ,
this . canvas ,
) ;
textX = centerElementXInViewport ;
textY = centerElementYInViewport ;
// x and y will change after calling newTextElement function
element . x = elementAtPosition . x + elementAtPosition . width / 2 ;
element . y = elementAtPosition . y + elementAtPosition . height / 2 ;
element . x = centerElementX ;
element . y = centerElementY ;
} else if ( ! e . altKey ) {
const snappedToCenterPosition = this . getTextWysiwygSnappedToCenterPosition (
x ,
@ -1770,6 +1886,7 @@ export class App extends React.Component<any, AppState> {
strokeColor : element.strokeColor ,
font : element.font ,
opacity : this.state.currentItemOpacity ,
zoom : this.state.zoom ,
onSubmit : text = > {
if ( text ) {
elements = [
@ -1796,7 +1913,11 @@ export class App extends React.Component<any, AppState> {
}
const hasDeselectedButton = Boolean ( e . buttons ) ;
const { x , y } = viewportCoordsToSceneCoords ( e , this . state ) ;
const { x , y } = viewportCoordsToSceneCoords (
e ,
this . state ,
this . canvas ,
) ;
if ( this . state . multiElement ) {
const { multiElement } = this . state ;
const originX = multiElement . x ;
@ -1823,7 +1944,7 @@ export class App extends React.Component<any, AppState> {
const resizeElement = getElementWithResizeHandler (
elements ,
{ x , y } ,
this . state ,
this . state .zoom ,
) ;
if ( resizeElement && resizeElement . resizeHandle ) {
document . documentElement . style . cursor = getCursorForResizingElement (
@ -1832,7 +1953,12 @@ export class App extends React.Component<any, AppState> {
return ;
}
}
const hitElement = getElementAtPosition ( elements , x , y ) ;
const hitElement = getElementAtPosition (
elements ,
x ,
y ,
this . state . zoom ,
) ;
document . documentElement . style . cursor = hitElement ? "move" : "" ;
} }
onDrop = { e = > {
@ -1858,8 +1984,8 @@ export class App extends React.Component<any, AppState> {
const { deltaX , deltaY } = e ;
this . setState ( {
scrollX : this.state.scrollX - deltaX ,
scrollY : this.state.scrollY - deltaY ,
scrollX : this.state.scrollX - deltaX / this . state . zoom ,
scrollY : this.state.scrollY - deltaY / this . state . zoom ,
} ) ;
} ;
@ -1873,8 +1999,14 @@ export class App extends React.Component<any, AppState> {
const elementsCenterX = distance ( minX , maxX ) / 2 ;
const elementsCenterY = distance ( minY , maxY ) / 2 ;
const dx = cursorX - this . state . scrollX - elementsCenterX ;
const dy = cursorY - this . state . scrollY - elementsCenterY ;
const { x , y } = viewportCoordsToSceneCoords (
{ clientX : cursorX , clientY : cursorY } ,
this . state ,
this . canvas ,
) ;
const dx = x - elementsCenterX ;
const dy = y - elementsCenterY ;
elements = [
. . . elements ,
@ -1933,6 +2065,7 @@ export class App extends React.Component<any, AppState> {
scrollX : this.state.scrollX ,
scrollY : this.state.scrollY ,
viewBackgroundColor : this.state.viewBackgroundColor ,
zoom : this.state.zoom ,
} ,
) ;
const scrolledOutside = ! atLeastOneVisibleElement && elements . length > 0 ;