Set scale for export images (#416)

This commit is contained in:
Timur Khazamov 2020-01-17 15:19:56 +03:00 committed by GitHub
parent 71570a325b
commit 82f559f826
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 60 deletions

View File

@ -44,3 +44,9 @@
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
} }
.ExportDialog__scales {
display: flex;
align-items: baseline;
justify-content: flex-end;
}

View File

@ -18,6 +18,11 @@ const probablySupportsClipboard =
"write" in navigator.clipboard && "write" in navigator.clipboard &&
"ClipboardItem" in window; "ClipboardItem" in window;
const scales = [1, 2, 3];
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
type ExportCB = (elements: readonly ExcalidrawElement[], scale: number) => void;
export function ExportDialog({ export function ExportDialog({
elements, elements,
appState, appState,
@ -32,11 +37,12 @@ export function ExportDialog({
exportPadding?: number; exportPadding?: number;
actionManager: ActionsManagerInterface; actionManager: ActionsManagerInterface;
syncActionResult: UpdaterFn; syncActionResult: UpdaterFn;
onExportToPng(elements: readonly ExcalidrawElement[]): void; onExportToPng: ExportCB;
onExportToClipboard(elements: readonly ExcalidrawElement[]): void; onExportToClipboard: ExportCB;
}) { }) {
const someElementIsSelected = elements.some(element => element.isSelected); const someElementIsSelected = elements.some(element => element.isSelected);
const [modalIsShown, setModalIsShown] = useState(false); const [modalIsShown, setModalIsShown] = useState(false);
const [scale, setScale] = useState(defaultScale);
const [exportSelected, setExportSelected] = useState(someElementIsSelected); const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previeRef = useRef<HTMLDivElement>(null); const previeRef = useRef<HTMLDivElement>(null);
const { exportBackground, viewBackgroundColor } = appState; const { exportBackground, viewBackgroundColor } = appState;
@ -54,7 +60,8 @@ export function ExportDialog({
const canvas = getExportCanvasPreview(exportedElements, { const canvas = getExportCanvasPreview(exportedElements, {
exportBackground, exportBackground,
viewBackgroundColor, viewBackgroundColor,
exportPadding exportPadding,
scale
}); });
previewNode?.appendChild(canvas); previewNode?.appendChild(canvas);
return () => { return () => {
@ -65,7 +72,8 @@ export function ExportDialog({
exportedElements, exportedElements,
exportBackground, exportBackground,
exportPadding, exportPadding,
viewBackgroundColor viewBackgroundColor,
scale
]); ]);
function handleClose() { function handleClose() {
@ -98,7 +106,7 @@ export function ExportDialog({
icon={downloadFile} icon={downloadFile}
title="Export to PNG" title="Export to PNG"
aria-label="Export to PNG" aria-label="Export to PNG"
onClick={() => onExportToPng(exportedElements)} onClick={() => onExportToPng(exportedElements, scale)}
/> />
{probablySupportsClipboard && ( {probablySupportsClipboard && (
@ -107,7 +115,9 @@ export function ExportDialog({
icon={clipboard} icon={clipboard}
title="Copy to clipboard" title="Copy to clipboard"
aria-label="Copy to clipboard" aria-label="Copy to clipboard"
onClick={() => onExportToClipboard(exportedElements)} onClick={() =>
onExportToClipboard(exportedElements, scale)
}
/> />
)} )}
</Stack.Row> </Stack.Row>
@ -119,6 +129,21 @@ export function ExportDialog({
syncActionResult syncActionResult
)} )}
<Stack.Col gap={1}> <Stack.Col gap={1}>
<div className="ExportDialog__scales">
<Stack.Row gap={1} align="baseline">
{scales.map(s => (
<ToolIcon
size="s"
type="radio"
icon={"x" + s}
name="export-canvas-scale"
id="export-canvas-scale"
checked={scale === s}
onChange={() => setScale(s)}
/>
))}
</Stack.Row>
</div>
{actionManager.renderAction( {actionManager.renderAction(
"changeExportBackground", "changeExportBackground",
elements, elements,

View File

@ -5,7 +5,7 @@ import React from "react";
type StackProps = { type StackProps = {
children: React.ReactNode; children: React.ReactNode;
gap?: number; gap?: number;
align?: "start" | "center" | "end"; align?: "start" | "center" | "end" | "baseline";
}; };
function RowStack({ children, gap, align }: StackProps) { function RowStack({ children, gap, align }: StackProps) {

View File

@ -20,6 +20,12 @@
} }
} }
.ToolIcon_size_s .ToolIcon__icon {
width: 25px;
height: 25px;
font-size: 0.8em;
}
.ToolIcon_type_button { .ToolIcon_type_button {
padding: 0; padding: 0;
border: none; border: none;

View File

@ -2,6 +2,8 @@ import "./ToolIcon.scss";
import React from "react"; import React from "react";
type ToolIconSize = "s" | "m";
type ToolIconProps = type ToolIconProps =
| { | {
type: "button"; type: "button";
@ -11,6 +13,7 @@ type ToolIconProps =
name?: string; name?: string;
id?: string; id?: string;
onClick?(): void; onClick?(): void;
size?: ToolIconSize;
} }
| { | {
type: "radio"; type: "radio";
@ -20,12 +23,17 @@ type ToolIconProps =
id?: string; id?: string;
checked: boolean; checked: boolean;
onChange?(): void; onChange?(): void;
size?: ToolIconSize;
}; };
const DEFAULT_SIZE: ToolIconSize = "m";
export function ToolIcon(props: ToolIconProps) { export function ToolIcon(props: ToolIconProps) {
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
if (props.type === "button") if (props.type === "button")
return ( return (
<label className="ToolIcon" title={props.title}> <label className={`ToolIcon ${sizeCn}`} title={props.title}>
<button <button
className="ToolIcon_type_button" className="ToolIcon_type_button"
aria-label={props["aria-label"]} aria-label={props["aria-label"]}
@ -38,7 +46,7 @@ export function ToolIcon(props: ToolIconProps) {
); );
return ( return (
<label className="ToolIcon" title={props.title}> <label className={`ToolIcon ${sizeCn}`} title={props.title}>
<input <input
className="ToolIcon_type_radio" className="ToolIcon_type_radio"
type="radio" type="radio"

View File

@ -478,18 +478,23 @@ export class App extends React.Component<{}, AppState> {
appState={this.state} appState={this.state}
actionManager={this.actionManager} actionManager={this.actionManager}
syncActionResult={this.syncActionResult} syncActionResult={this.syncActionResult}
onExportToPng={exportedElements => { onExportToPng={(exportedElements, scale) => {
if (this.canvas) if (this.canvas)
exportCanvas("png", exportedElements, this.canvas, this.state); exportCanvas("png", exportedElements, this.canvas, {
exportBackground: this.state.exportBackground,
name: this.state.name,
viewBackgroundColor: this.state.viewBackgroundColor,
scale
});
}} }}
onExportToClipboard={exportedElements => { onExportToClipboard={(exportedElements, scale) => {
if (this.canvas) if (this.canvas)
exportCanvas( exportCanvas("clipboard", exportedElements, this.canvas, {
"clipboard", exportBackground: this.state.exportBackground,
exportedElements, name: this.state.name,
this.canvas, viewBackgroundColor: this.state.viewBackgroundColor,
this.state scale
); });
}} }}
/> />
{this.actionManager.renderAction( {this.actionManager.renderAction(

View File

@ -174,10 +174,12 @@ export function getExportCanvasPreview(
{ {
exportBackground, exportBackground,
exportPadding = 10, exportPadding = 10,
viewBackgroundColor viewBackgroundColor,
scale = 1
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
scale?: number;
viewBackgroundColor: string; viewBackgroundColor: string;
} }
) { ) {
@ -200,8 +202,13 @@ export function getExportCanvasPreview(
} }
const tempCanvas = document.createElement("canvas"); const tempCanvas = document.createElement("canvas");
tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2; const width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2; const height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
tempCanvas.style.width = width + "px";
tempCanvas.style.height = height + "px";
tempCanvas.width = width * scale;
tempCanvas.height = height * scale;
tempCanvas.getContext("2d")?.scale(scale, scale);
renderScene( renderScene(
elements, elements,
@ -230,58 +237,27 @@ export async function exportCanvas(
exportBackground, exportBackground,
exportPadding = 10, exportPadding = 10,
viewBackgroundColor, viewBackgroundColor,
name name,
scale = 1
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
viewBackgroundColor: string; viewBackgroundColor: string;
scrollX: number;
scrollY: number;
name: string; name: string;
scale?: number;
} }
) { ) {
if (!elements.length) return window.alert("Cannot export empty canvas."); if (!elements.length) return window.alert("Cannot export empty canvas.");
// calculate smallest area to fit the contents in // calculate smallest area to fit the contents in
let subCanvasX1 = Infinity; const tempCanvas = getExportCanvasPreview(elements, {
let subCanvasX2 = 0; exportBackground,
let subCanvasY1 = Infinity; viewBackgroundColor,
let subCanvasY2 = 0; exportPadding,
scale
elements.forEach(element => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
subCanvasX1 = Math.min(subCanvasX1, x1);
subCanvasY1 = Math.min(subCanvasY1, y1);
subCanvasX2 = Math.max(subCanvasX2, x2);
subCanvasY2 = Math.max(subCanvasY2, y2);
}); });
function distance(x: number, y: number) {
return Math.abs(x > y ? x - y : y - x);
}
const tempCanvas = document.createElement("canvas");
tempCanvas.style.display = "none"; tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas); document.body.appendChild(tempCanvas);
tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
renderScene(
elements,
rough.canvas(tempCanvas),
tempCanvas,
{
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: 0,
scrollY: 0
},
{
offsetX: -subCanvasX1 + exportPadding,
offsetY: -subCanvasY1 + exportPadding,
renderScrollbars: false,
renderSelection: false
}
);
if (type === "png") { if (type === "png") {
const fileName = `${name}.png`; const fileName = `${name}.png`;