@ -118,11 +118,13 @@ export interface ExcalidrawElementWithCanvas {
canvas : HTMLCanvasElement ;
theme : AppState [ "theme" ] ;
scale : number ;
angle : number ;
zoomValue : AppState [ "zoom" ] [ "value" ] ;
canvasOffsetX : number ;
canvasOffsetY : number ;
boundTextElementVersion : number | null ;
containingFrameOpacity : number ;
boundTextCanvas : HTMLCanvasElement ;
}
const cappedElementCanvasSize = (
@ -182,7 +184,7 @@ const cappedElementCanvasSize = (
const generateElementCanvas = (
element : NonDeletedExcalidrawElement ,
elementsMap : Renderabl eElementsMap,
elementsMap : NonDeletedScen eElementsMap,
zoom : Zoom ,
renderConfig : StaticCanvasRenderConfig ,
appState : StaticCanvasAppState ,
@ -234,8 +236,72 @@ const generateElementCanvas = (
}
drawElementOnCanvas ( element , rc , context , renderConfig , appState ) ;
context . restore ( ) ;
const boundTextElement = getBoundTextElement ( element , elementsMap ) ;
const boundTextCanvas = document . createElement ( "canvas" ) ;
const boundTextCanvasContext = boundTextCanvas . getContext ( "2d" ) ! ;
if ( isArrowElement ( element ) && boundTextElement ) {
const [ x1 , y1 , x2 , y2 ] = getElementAbsoluteCoords ( element , elementsMap ) ;
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math . max ( distance ( x1 , x2 ) , distance ( y1 , y2 ) ) ;
boundTextCanvas . width =
maxDim * window . devicePixelRatio * scale + padding * scale * 10 ;
boundTextCanvas . height =
maxDim * window . devicePixelRatio * scale + padding * scale * 10 ;
boundTextCanvasContext . translate (
boundTextCanvas . width / 2 ,
boundTextCanvas . height / 2 ,
) ;
boundTextCanvasContext . rotate ( element . angle ) ;
boundTextCanvasContext . drawImage (
canvas ! ,
- canvas . width / 2 ,
- canvas . height / 2 ,
canvas . width ,
canvas . height ,
) ;
const [ , , , , boundTextCx , boundTextCy ] = getElementAbsoluteCoords (
boundTextElement ,
elementsMap ,
) ;
boundTextCanvasContext . rotate ( - element . angle ) ;
const offsetX = ( boundTextCanvas . width - canvas ! . width ) / 2 ;
const offsetY = ( boundTextCanvas . height - canvas ! . height ) / 2 ;
const shiftX =
boundTextCanvas . width / 2 -
( boundTextCx - x1 ) * window . devicePixelRatio * scale -
offsetX -
padding * scale ;
const shiftY =
boundTextCanvas . height / 2 -
( boundTextCy - y1 ) * window . devicePixelRatio * scale -
offsetY -
padding * scale ;
boundTextCanvasContext . translate ( - shiftX , - shiftY ) ;
// Clear the bound text area
boundTextCanvasContext . clearRect (
- ( boundTextElement . width / 2 + BOUND_TEXT_PADDING ) *
window . devicePixelRatio *
scale ,
- ( boundTextElement . height / 2 + BOUND_TEXT_PADDING ) *
window . devicePixelRatio *
scale ,
( boundTextElement . width + BOUND_TEXT_PADDING * 2 ) *
window . devicePixelRatio *
scale ,
( boundTextElement . height + BOUND_TEXT_PADDING * 2 ) *
window . devicePixelRatio *
scale ,
) ;
}
return {
element ,
canvas ,
@ -248,6 +314,8 @@ const generateElementCanvas = (
getBoundTextElement ( element , elementsMap ) ? . version || null ,
containingFrameOpacity :
getContainingFrame ( element , elementsMap ) ? . opacity || 100 ,
boundTextCanvas ,
angle : element.angle ,
} ;
} ;
@ -423,7 +491,7 @@ export const elementWithCanvasCache = new WeakMap<
const generateElementWithCanvas = (
element : NonDeletedExcalidrawElement ,
elementsMap : Renderabl eElementsMap,
elementsMap : NonDeletedScen eElementsMap,
renderConfig : StaticCanvasRenderConfig ,
appState : StaticCanvasAppState ,
) = > {
@ -433,8 +501,8 @@ const generateElementWithCanvas = (
prevElementWithCanvas &&
prevElementWithCanvas . zoomValue !== zoom . value &&
! appState ? . shouldCacheIgnoreZoom ;
const boundTextElement Version =
getBoundTextElement ( element , elementsMap ) ? . version || null ;
const boundTextElement = getBoundTextElement ( element , elementsMap ) ;
const boundTextElementVersion = boundTextElement ? . version || null ;
const containingFrameOpacity =
getContainingFrame ( element , elementsMap ) ? . opacity || 100 ;
@ -444,7 +512,14 @@ const generateElementWithCanvas = (
shouldRegenerateBecauseZoom ||
prevElementWithCanvas . theme !== appState . theme ||
prevElementWithCanvas . boundTextElementVersion !== boundTextElementVersion ||
prevElementWithCanvas . containingFrameOpacity !== containingFrameOpacity
prevElementWithCanvas . containingFrameOpacity !== containingFrameOpacity ||
// since we rotate the canvas when copying from cached canvas, we don't
// regenerate the cached canvas. But we need to in case of labels which are
// cached alongside the arrow, and we want the labels to remain unrotated
// with respect to the arrow.
( isArrowElement ( element ) &&
boundTextElement &&
element . angle !== prevElementWithCanvas . angle )
) {
const elementWithCanvas = generateElementCanvas (
element ,
@ -481,75 +556,21 @@ const drawElementFromCanvas = (
const boundTextElement = getBoundTextElement ( element , allElementsMap ) ;
if ( isArrowElement ( element ) && boundTextElement ) {
const tempCanvas = document . createElement ( "canvas" ) ;
const tempCanvasContext = tempCanvas . getContext ( "2d" ) ! ;
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math . max ( distance ( x1 , x2 ) , distance ( y1 , y2 ) ) ;
tempCanvas . width =
maxDim * window . devicePixelRatio * zoom +
padding * elementWithCanvas . scale * 10 ;
tempCanvas . height =
maxDim * window . devicePixelRatio * zoom +
padding * elementWithCanvas . scale * 10 ;
const offsetX = ( tempCanvas . width - elementWithCanvas . canvas ! . width ) / 2 ;
const offsetY = ( tempCanvas . height - elementWithCanvas . canvas ! . height ) / 2 ;
tempCanvasContext . translate ( tempCanvas . width / 2 , tempCanvas . height / 2 ) ;
tempCanvasContext . rotate ( element . angle ) ;
tempCanvasContext . drawImage (
elementWithCanvas . canvas ! ,
- elementWithCanvas . canvas . width / 2 ,
- elementWithCanvas . canvas . height / 2 ,
elementWithCanvas . canvas . width ,
elementWithCanvas . canvas . height ,
) ;
const [ , , , , boundTextCx , boundTextCy ] = getElementAbsoluteCoords (
boundTextElement ,
allElementsMap ,
) ;
tempCanvasContext . rotate ( - element . angle ) ;
// Shift the canvas to the center of the bound text element
const shiftX =
tempCanvas . width / 2 -
( boundTextCx - x1 ) * window . devicePixelRatio * zoom -
offsetX -
padding * zoom ;
const shiftY =
tempCanvas . height / 2 -
( boundTextCy - y1 ) * window . devicePixelRatio * zoom -
offsetY -
padding * zoom ;
tempCanvasContext . translate ( - shiftX , - shiftY ) ;
// Clear the bound text area
tempCanvasContext . clearRect (
- ( boundTextElement . width / 2 + BOUND_TEXT_PADDING ) *
window . devicePixelRatio *
zoom ,
- ( boundTextElement . height / 2 + BOUND_TEXT_PADDING ) *
window . devicePixelRatio *
zoom ,
( boundTextElement . width + BOUND_TEXT_PADDING * 2 ) *
window . devicePixelRatio *
zoom ,
( boundTextElement . height + BOUND_TEXT_PADDING * 2 ) *
window . devicePixelRatio *
zoom ,
) ;
const offsetX =
( elementWithCanvas . boundTextCanvas . width -
elementWithCanvas . canvas ! . width ) /
2 ;
const offsetY =
( elementWithCanvas . boundTextCanvas . height -
elementWithCanvas . canvas ! . height ) /
2 ;
context . translate ( cx , cy ) ;
context . drawImage (
temp Canvas,
elementWithCanvas . boundTextCanvas ,
( - ( x2 - x1 ) / 2 ) * window . devicePixelRatio - offsetX / zoom - padding ,
( - ( y2 - y1 ) / 2 ) * window . devicePixelRatio - offsetY / zoom - padding ,
temp Canvas. width / zoom ,
temp Canvas. height / zoom ,
elementWithCanvas . boundTextCanvas . width / zoom ,
elementWithCanvas . boundTextCanvas . height / zoom ,
) ;
} else {
// we translate context to element center so that rotation and scale
@ -705,7 +726,7 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas (
element ,
e lementsMap,
allE lementsMap,
renderConfig ,
appState ,
) ;
@ -843,7 +864,7 @@ export const renderElement = (
} else {
const elementWithCanvas = generateElementWithCanvas (
element ,
e lementsMap,
allE lementsMap,
renderConfig ,
appState ,
) ;