fix export to support scrolling (#133)
This commit is contained in:
parent
60b06dab73
commit
490438960d
230
src/index.tsx
230
src/index.tsx
@ -351,10 +351,23 @@ function handlerRectangles(
|
|||||||
|
|
||||||
function renderScene(
|
function renderScene(
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
canvas: HTMLCanvasElement,
|
||||||
sceneState: SceneState
|
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;
|
const fillStyle = context.fillStyle;
|
||||||
if (typeof sceneState.viewBackgroundColor === "string") {
|
if (typeof sceneState.viewBackgroundColor === "string") {
|
||||||
@ -367,9 +380,15 @@ function renderScene(
|
|||||||
|
|
||||||
const selectedIndices = getSelectedIndices();
|
const selectedIndices = getSelectedIndices();
|
||||||
|
|
||||||
|
sceneState = {
|
||||||
|
...sceneState,
|
||||||
|
scrollX: typeof offsetX === "number" ? offsetX : sceneState.scrollX,
|
||||||
|
scrollY: typeof offsetY === "number" ? offsetY : sceneState.scrollY
|
||||||
|
};
|
||||||
|
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
element.draw(rc, context, sceneState);
|
element.draw(rc, context, sceneState);
|
||||||
if (element.isSelected) {
|
if (renderSelection && element.isSelected) {
|
||||||
const margin = 4;
|
const margin = 4;
|
||||||
|
|
||||||
const elementX1 = getElementAbsoluteX1(element);
|
const elementX1 = getElementAbsoluteX1(element);
|
||||||
@ -405,119 +424,93 @@ function renderScene(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrollBars = getScrollbars(
|
if (renderScrollbars) {
|
||||||
context.canvas.width,
|
const scrollBars = getScrollbars(
|
||||||
context.canvas.height,
|
context.canvas.width,
|
||||||
sceneState.scrollX,
|
context.canvas.height,
|
||||||
sceneState.scrollY
|
sceneState.scrollX,
|
||||||
);
|
sceneState.scrollY
|
||||||
|
);
|
||||||
|
|
||||||
context.fillStyle = SCROLLBAR_COLOR;
|
context.fillStyle = SCROLLBAR_COLOR;
|
||||||
context.fillRect(
|
context.fillRect(
|
||||||
scrollBars.horizontal.x,
|
scrollBars.horizontal.x,
|
||||||
scrollBars.horizontal.y,
|
scrollBars.horizontal.y,
|
||||||
scrollBars.horizontal.width,
|
scrollBars.horizontal.width,
|
||||||
scrollBars.horizontal.height
|
scrollBars.horizontal.height
|
||||||
);
|
);
|
||||||
context.fillRect(
|
context.fillRect(
|
||||||
scrollBars.vertical.x,
|
scrollBars.vertical.x,
|
||||||
scrollBars.vertical.y,
|
scrollBars.vertical.y,
|
||||||
scrollBars.vertical.width,
|
scrollBars.vertical.width,
|
||||||
scrollBars.vertical.height
|
scrollBars.vertical.height
|
||||||
);
|
);
|
||||||
context.fillStyle = fillStyle;
|
context.fillStyle = fillStyle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportAsPNG({
|
function exportAsPNG({
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportVisibleOnly,
|
|
||||||
exportPadding = 10,
|
exportPadding = 10,
|
||||||
viewBackgroundColor
|
viewBackgroundColor
|
||||||
}: {
|
}: {
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportVisibleOnly: boolean;
|
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
}) {
|
}) {
|
||||||
if (!elements.length) return window.alert("Cannot export empty canvas.");
|
if (!elements.length) return window.alert("Cannot export empty canvas.");
|
||||||
|
|
||||||
// deselect & rerender
|
// calculate smallest area to fit the contents in
|
||||||
|
|
||||||
clearSelection();
|
let subCanvasX1 = Infinity;
|
||||||
ReactDOM.render(<App />, rootElement, () => {
|
let subCanvasX2 = 0;
|
||||||
// calculate visible-area coords
|
let subCanvasY1 = Infinity;
|
||||||
|
let subCanvasY2 = 0;
|
||||||
|
|
||||||
let subCanvasX1 = Infinity;
|
elements.forEach(element => {
|
||||||
let subCanvasX2 = 0;
|
subCanvasX1 = Math.min(subCanvasX1, getElementAbsoluteX1(element));
|
||||||
let subCanvasY1 = Infinity;
|
subCanvasX2 = Math.max(subCanvasX2, getElementAbsoluteX2(element));
|
||||||
let subCanvasY2 = 0;
|
subCanvasY1 = Math.min(subCanvasY1, getElementAbsoluteY1(element));
|
||||||
|
subCanvasY2 = Math.max(subCanvasY2, getElementAbsoluteY2(element));
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
);
|
|
||||||
|
|
||||||
// reset transparent bg back to original
|
|
||||||
if (!exportBackground) {
|
|
||||||
renderScene(rc, context, { viewBackgroundColor, scrollX: 0, scrollY: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function distance(x: number, y: number) {
|
||||||
|
return Math.abs(x > y ? x - y : y - x);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// clean up the DOM
|
||||||
|
link.remove();
|
||||||
|
if (tempCanvas !== canvas) tempCanvas.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
|
function rotate(x1: number, y1: number, x2: number, y2: number, angle: number) {
|
||||||
@ -723,8 +716,6 @@ type AppState = {
|
|||||||
resizingElement: ExcalidrawElement | null;
|
resizingElement: ExcalidrawElement | null;
|
||||||
elementType: string;
|
elementType: string;
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportVisibleOnly: boolean;
|
|
||||||
exportPadding: number;
|
|
||||||
currentItemStrokeColor: string;
|
currentItemStrokeColor: string;
|
||||||
currentItemBackgroundColor: string;
|
currentItemBackgroundColor: string;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
@ -821,9 +812,7 @@ class App extends React.Component<{}, AppState> {
|
|||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
elementType: "selection",
|
elementType: "selection",
|
||||||
exportBackground: false,
|
exportBackground: true,
|
||||||
exportVisibleOnly: true,
|
|
||||||
exportPadding: 10,
|
|
||||||
currentItemStrokeColor: "#000000",
|
currentItemStrokeColor: "#000000",
|
||||||
currentItemBackgroundColor: "#ffffff",
|
currentItemBackgroundColor: "#ffffff",
|
||||||
viewBackgroundColor: "#ffffff",
|
viewBackgroundColor: "#ffffff",
|
||||||
@ -1052,12 +1041,7 @@ class App extends React.Component<{}, AppState> {
|
|||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
exportAsPNG({
|
exportAsPNG(this.state);
|
||||||
exportBackground: this.state.exportBackground,
|
|
||||||
exportVisibleOnly: this.state.exportVisibleOnly,
|
|
||||||
exportPadding: this.state.exportPadding,
|
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Export to png
|
Export to png
|
||||||
@ -1072,28 +1056,6 @@ class App extends React.Component<{}, AppState> {
|
|||||||
/>
|
/>
|
||||||
background
|
background
|
||||||
</label>
|
</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>
|
</div>
|
||||||
{someElementIsSelected() && (
|
{someElementIsSelected() && (
|
||||||
<>
|
<>
|
||||||
@ -1411,7 +1373,7 @@ class App extends React.Component<{}, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
renderScene(rc, context, {
|
renderScene(rc, canvas, {
|
||||||
scrollX: this.state.scrollX,
|
scrollX: this.state.scrollX,
|
||||||
scrollY: this.state.scrollY,
|
scrollY: this.state.scrollY,
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor
|
viewBackgroundColor: this.state.viewBackgroundColor
|
||||||
|
Loading…
x
Reference in New Issue
Block a user