|
|
|
|
@ -351,10 +351,23 @@ function handlerRectangles(
@@ -351,10 +351,23 @@ function handlerRectangles(
|
|
|
|
|
|
|
|
|
|
function renderScene( |
|
|
|
|
rc: RoughCanvas, |
|
|
|
|
context: CanvasRenderingContext2D, |
|
|
|
|
sceneState: SceneState |
|
|
|
|
canvas: HTMLCanvasElement, |
|
|
|
|
sceneState: SceneState, |
|
|
|
|
// extra options, currently passed by export helper
|
|
|
|
|
{ |
|
|
|
|
offsetX, |
|
|
|
|
offsetY, |
|
|
|
|
renderScrollbars = true, |
|
|
|
|
renderSelection = true |
|
|
|
|
}: { |
|
|
|
|
offsetX?: number; |
|
|
|
|
offsetY?: number; |
|
|
|
|
renderScrollbars?: boolean; |
|
|
|
|
renderSelection?: boolean; |
|
|
|
|
} = {} |
|
|
|
|
) { |
|
|
|
|
if (!context) return; |
|
|
|
|
if (!canvas) return; |
|
|
|
|
const context = canvas.getContext("2d")!; |
|
|
|
|
|
|
|
|
|
const fillStyle = context.fillStyle; |
|
|
|
|
if (typeof sceneState.viewBackgroundColor === "string") { |
|
|
|
|
@ -367,9 +380,15 @@ function renderScene(
@@ -367,9 +380,15 @@ function renderScene(
|
|
|
|
|
|
|
|
|
|
const selectedIndices = getSelectedIndices(); |
|
|
|
|
|
|
|
|
|
sceneState = { |
|
|
|
|
...sceneState, |
|
|
|
|
scrollX: typeof offsetX === "number" ? offsetX : sceneState.scrollX, |
|
|
|
|
scrollY: typeof offsetY === "number" ? offsetY : sceneState.scrollY |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
elements.forEach(element => { |
|
|
|
|
element.draw(rc, context, sceneState); |
|
|
|
|
if (element.isSelected) { |
|
|
|
|
if (renderSelection && element.isSelected) { |
|
|
|
|
const margin = 4; |
|
|
|
|
|
|
|
|
|
const elementX1 = getElementAbsoluteX1(element); |
|
|
|
|
@ -405,119 +424,93 @@ function renderScene(
@@ -405,119 +424,93 @@ function renderScene(
|
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const scrollBars = getScrollbars( |
|
|
|
|
context.canvas.width, |
|
|
|
|
context.canvas.height, |
|
|
|
|
sceneState.scrollX, |
|
|
|
|
sceneState.scrollY |
|
|
|
|
); |
|
|
|
|
if (renderScrollbars) { |
|
|
|
|
const scrollBars = getScrollbars( |
|
|
|
|
context.canvas.width, |
|
|
|
|
context.canvas.height, |
|
|
|
|
sceneState.scrollX, |
|
|
|
|
sceneState.scrollY |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
context.fillStyle = SCROLLBAR_COLOR; |
|
|
|
|
context.fillRect( |
|
|
|
|
scrollBars.horizontal.x, |
|
|
|
|
scrollBars.horizontal.y, |
|
|
|
|
scrollBars.horizontal.width, |
|
|
|
|
scrollBars.horizontal.height |
|
|
|
|
); |
|
|
|
|
context.fillRect( |
|
|
|
|
scrollBars.vertical.x, |
|
|
|
|
scrollBars.vertical.y, |
|
|
|
|
scrollBars.vertical.width, |
|
|
|
|
scrollBars.vertical.height |
|
|
|
|
); |
|
|
|
|
context.fillStyle = fillStyle; |
|
|
|
|
context.fillStyle = SCROLLBAR_COLOR; |
|
|
|
|
context.fillRect( |
|
|
|
|
scrollBars.horizontal.x, |
|
|
|
|
scrollBars.horizontal.y, |
|
|
|
|
scrollBars.horizontal.width, |
|
|
|
|
scrollBars.horizontal.height |
|
|
|
|
); |
|
|
|
|
context.fillRect( |
|
|
|
|
scrollBars.vertical.x, |
|
|
|
|
scrollBars.vertical.y, |
|
|
|
|
scrollBars.vertical.width, |
|
|
|
|
scrollBars.vertical.height |
|
|
|
|
); |
|
|
|
|
context.fillStyle = fillStyle; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function exportAsPNG({ |
|
|
|
|
exportBackground, |
|
|
|
|
exportVisibleOnly, |
|
|
|
|
exportPadding = 10, |
|
|
|
|
viewBackgroundColor |
|
|
|
|
}: { |
|
|
|
|
exportBackground: boolean; |
|
|
|
|
exportVisibleOnly: boolean; |
|
|
|
|
exportPadding?: number; |
|
|
|
|
viewBackgroundColor: string; |
|
|
|
|
scrollX: number; |
|
|
|
|
scrollY: number; |
|
|
|
|
}) { |
|
|
|
|
if (!elements.length) return window.alert("Cannot export empty canvas."); |
|
|
|
|
|
|
|
|
|
// deselect & rerender
|
|
|
|
|
|
|
|
|
|
clearSelection(); |
|
|
|
|
ReactDOM.render(<App />, rootElement, () => { |
|
|
|
|
// calculate visible-area coords
|
|
|
|
|
// calculate smallest area to fit the contents in
|
|
|
|
|
|
|
|
|
|
let subCanvasX1 = Infinity; |
|
|
|
|
let subCanvasX2 = 0; |
|
|
|
|
let subCanvasY1 = Infinity; |
|
|
|
|
let subCanvasY2 = 0; |
|
|
|
|
let subCanvasX1 = Infinity; |
|
|
|
|
let subCanvasX2 = 0; |
|
|
|
|
let subCanvasY1 = Infinity; |
|
|
|
|
let subCanvasY2 = 0; |
|
|
|
|
|
|
|
|
|
elements.forEach(element => { |
|
|
|
|
subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element)); |
|
|
|
|
subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element)); |
|
|
|
|
subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element)); |
|
|
|
|
subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element)); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// create temporary canvas from which we'll export
|
|
|
|
|
|
|
|
|
|
const tempCanvas = document.createElement("canvas"); |
|
|
|
|
const tempCanvasCtx = tempCanvas.getContext("2d")!; |
|
|
|
|
tempCanvas.style.display = "none"; |
|
|
|
|
document.body.appendChild(tempCanvas); |
|
|
|
|
tempCanvas.width = exportVisibleOnly |
|
|
|
|
? subCanvasX2 - subCanvasX1 + exportPadding * 2 |
|
|
|
|
: canvas.width; |
|
|
|
|
tempCanvas.height = exportVisibleOnly |
|
|
|
|
? subCanvasY2 - subCanvasY1 + exportPadding * 2 |
|
|
|
|
: canvas.height; |
|
|
|
|
|
|
|
|
|
// if we're exporting without bg, we need to rerender the scene without it
|
|
|
|
|
// (it's reset again, below)
|
|
|
|
|
if (!exportBackground) { |
|
|
|
|
renderScene(rc, context, { |
|
|
|
|
viewBackgroundColor: null, |
|
|
|
|
scrollX: 0, |
|
|
|
|
scrollY: 0 |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
elements.forEach(element => { |
|
|
|
|
subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element)); |
|
|
|
|
subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element)); |
|
|
|
|
subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element)); |
|
|
|
|
subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element)); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// copy our original canvas onto the temp canvas
|
|
|
|
|
tempCanvasCtx.drawImage( |
|
|
|
|
canvas, // source
|
|
|
|
|
exportVisibleOnly // sx
|
|
|
|
|
? subCanvasX1 - exportPadding |
|
|
|
|
: 0, |
|
|
|
|
exportVisibleOnly // sy
|
|
|
|
|
? subCanvasY1 - exportPadding |
|
|
|
|
: 0, |
|
|
|
|
exportVisibleOnly // sWidth
|
|
|
|
|
? subCanvasX2 - subCanvasX1 + exportPadding * 2 |
|
|
|
|
: canvas.width, |
|
|
|
|
exportVisibleOnly // sHeight
|
|
|
|
|
? subCanvasY2 - subCanvasY1 + exportPadding * 2 |
|
|
|
|
: canvas.height, |
|
|
|
|
0, // dx
|
|
|
|
|
0, // dy
|
|
|
|
|
exportVisibleOnly ? tempCanvas.width : canvas.width, // dWidth
|
|
|
|
|
exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
|
|
|
|
|
); |
|
|
|
|
function distance(x: number, y: number) { |
|
|
|
|
return Math.abs(x > y ? x - y : y - x); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// reset transparent bg back to original
|
|
|
|
|
if (!exportBackground) { |
|
|
|
|
renderScene(rc, context, { viewBackgroundColor, scrollX: 0, scrollY: 0 }); |
|
|
|
|
const tempCanvas = document.createElement("canvas"); |
|
|
|
|
tempCanvas.style.display = "none"; |
|
|
|
|
document.body.appendChild(tempCanvas); |
|
|
|
|
tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2; |
|
|
|
|
tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2; |
|
|
|
|
|
|
|
|
|
renderScene( |
|
|
|
|
rough.canvas(tempCanvas), |
|
|
|
|
tempCanvas, |
|
|
|
|
{ |
|
|
|
|
viewBackgroundColor: exportBackground ? viewBackgroundColor : null, |
|
|
|
|
scrollX: 0, |
|
|
|
|
scrollY: 0 |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
offsetX: -subCanvasX1 + exportPadding, |
|
|
|
|
offsetY: -subCanvasY1 + exportPadding, |
|
|
|
|
renderScrollbars: false, |
|
|
|
|
renderSelection: false |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// create a temporary <a> elem which we'll use to download the image
|
|
|
|
|
const link = document.createElement("a"); |
|
|
|
|
link.setAttribute("download", "excalidraw.png"); |
|
|
|
|
link.setAttribute("href", tempCanvas.toDataURL("image/png")); |
|
|
|
|
link.click(); |
|
|
|
|
// create a temporary <a> elem which we'll use to download the image
|
|
|
|
|
const link = document.createElement("a"); |
|
|
|
|
link.setAttribute("download", "excalidraw.png"); |
|
|
|
|
link.setAttribute("href", tempCanvas.toDataURL("image/png")); |
|
|
|
|
link.click(); |
|
|
|
|
|
|
|
|
|
// clean up the DOM
|
|
|
|
|
link.remove(); |
|
|
|
|
if (tempCanvas !== canvas) tempCanvas.remove(); |
|
|
|
|
}); |
|
|
|
|
// clean up the DOM
|
|
|
|
|
link.remove(); |
|
|
|
|
if (tempCanvas !== canvas) tempCanvas.remove(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) { |
|
|
|
|
@ -723,8 +716,6 @@ type AppState = {
@@ -723,8 +716,6 @@ type AppState = {
|
|
|
|
|
resizingElement: ExcalidrawElement | null; |
|
|
|
|
elementType: string; |
|
|
|
|
exportBackground: boolean; |
|
|
|
|
exportVisibleOnly: boolean; |
|
|
|
|
exportPadding: number; |
|
|
|
|
currentItemStrokeColor: string; |
|
|
|
|
currentItemBackgroundColor: string; |
|
|
|
|
viewBackgroundColor: string; |
|
|
|
|
@ -821,9 +812,7 @@ class App extends React.Component<{}, AppState> {
@@ -821,9 +812,7 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
draggingElement: null, |
|
|
|
|
resizingElement: null, |
|
|
|
|
elementType: "selection", |
|
|
|
|
exportBackground: false, |
|
|
|
|
exportVisibleOnly: true, |
|
|
|
|
exportPadding: 10, |
|
|
|
|
exportBackground: true, |
|
|
|
|
currentItemStrokeColor: "#000000", |
|
|
|
|
currentItemBackgroundColor: "#ffffff", |
|
|
|
|
viewBackgroundColor: "#ffffff", |
|
|
|
|
@ -1052,12 +1041,7 @@ class App extends React.Component<{}, AppState> {
@@ -1052,12 +1041,7 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
<div className="panelColumn"> |
|
|
|
|
<button |
|
|
|
|
onClick={() => { |
|
|
|
|
exportAsPNG({ |
|
|
|
|
exportBackground: this.state.exportBackground, |
|
|
|
|
exportVisibleOnly: this.state.exportVisibleOnly, |
|
|
|
|
exportPadding: this.state.exportPadding, |
|
|
|
|
viewBackgroundColor: this.state.viewBackgroundColor |
|
|
|
|
}); |
|
|
|
|
exportAsPNG(this.state); |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
Export to png |
|
|
|
|
@ -1072,28 +1056,6 @@ class App extends React.Component<{}, AppState> {
@@ -1072,28 +1056,6 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
/> |
|
|
|
|
background |
|
|
|
|
</label> |
|
|
|
|
<label> |
|
|
|
|
<input |
|
|
|
|
type="checkbox" |
|
|
|
|
checked={this.state.exportVisibleOnly} |
|
|
|
|
onChange={e => { |
|
|
|
|
this.setState({ exportVisibleOnly: e.target.checked }); |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
visible area only |
|
|
|
|
</label> |
|
|
|
|
<div> |
|
|
|
|
(padding: |
|
|
|
|
<input |
|
|
|
|
type="number" |
|
|
|
|
value={this.state.exportPadding} |
|
|
|
|
onChange={e => { |
|
|
|
|
this.setState({ exportPadding: Number(e.target.value) }); |
|
|
|
|
}} |
|
|
|
|
disabled={!this.state.exportVisibleOnly} |
|
|
|
|
/> |
|
|
|
|
px) |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
{someElementIsSelected() && ( |
|
|
|
|
<> |
|
|
|
|
@ -1411,7 +1373,7 @@ class App extends React.Component<{}, AppState> {
@@ -1411,7 +1373,7 @@ class App extends React.Component<{}, AppState> {
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
componentDidUpdate() { |
|
|
|
|
renderScene(rc, context, { |
|
|
|
|
renderScene(rc, canvas, { |
|
|
|
|
scrollX: this.state.scrollX, |
|
|
|
|
scrollY: this.state.scrollY, |
|
|
|
|
viewBackgroundColor: this.state.viewBackgroundColor |
|
|
|
|
|