|  | @@ -134,70 +134,70 @@ function exportAsPNG({
 | 
	
		
			
				|  |  |    // deselect & rerender
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    clearSelection();
 | 
	
		
			
				|  |  | -  drawScene();
 | 
	
		
			
				|  |  | +  ReactDOM.render(<App />, rootElement, () => {
 | 
	
		
			
				|  |  | +    // calculate visible-area coords
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  // calculate visible-area coords
 | 
	
		
			
				|  |  | +    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));
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | +    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 (exportBackground) {
 | 
	
		
			
				|  |  | -    tempCanvasCtx.fillStyle = viewBgColor;
 | 
	
		
			
				|  |  | -    tempCanvasCtx.fillRect(0, 0, canvas.width, canvas.height);
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | +    // create temporary canvas from which we'll export
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  // copy our original canvas onto the temp canvas
 | 
	
		
			
				|  |  | -  tempCanvasCtx.drawImage(
 | 
	
		
			
				|  |  | -    canvas, // source
 | 
	
		
			
				|  |  | -    exportVisibleOnly // sx
 | 
	
		
			
				|  |  | -      ? subCanvasX1 - exportPadding
 | 
	
		
			
				|  |  | -      : 0,
 | 
	
		
			
				|  |  | -    exportVisibleOnly // sy
 | 
	
		
			
				|  |  | -      ? subCanvasY1 - exportPadding
 | 
	
		
			
				|  |  | -      : 0,
 | 
	
		
			
				|  |  | -    exportVisibleOnly // sWidth
 | 
	
		
			
				|  |  | +    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,
 | 
	
		
			
				|  |  | -    exportVisibleOnly // sHeight
 | 
	
		
			
				|  |  | +      : canvas.width;
 | 
	
		
			
				|  |  | +    tempCanvas.height = exportVisibleOnly
 | 
	
		
			
				|  |  |        ? subCanvasY2 - subCanvasY1 + exportPadding * 2
 | 
	
		
			
				|  |  | -      : canvas.height,
 | 
	
		
			
				|  |  | -    0, // dx
 | 
	
		
			
				|  |  | -    0, // dy
 | 
	
		
			
				|  |  | -    exportVisibleOnly ? tempCanvas.width : canvas.width, // dWidth
 | 
	
		
			
				|  |  | -    exportVisibleOnly ? tempCanvas.height : canvas.height // dHeight
 | 
	
		
			
				|  |  | -  );
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  // create a temporary <a> elem which we'll use to download the image
 | 
	
		
			
				|  |  | -  const link = document.createElement("a");
 | 
	
		
			
				|  |  | -  link.setAttribute("download", "excalibur.png");
 | 
	
		
			
				|  |  | -  link.setAttribute("href", tempCanvas.toDataURL("image/png"));
 | 
	
		
			
				|  |  | -  link.click();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  // clean up the DOM
 | 
	
		
			
				|  |  | -  link.remove();
 | 
	
		
			
				|  |  | -  if (tempCanvas !== canvas) tempCanvas.remove();
 | 
	
		
			
				|  |  | +      : canvas.height;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (exportBackground) {
 | 
	
		
			
				|  |  | +      tempCanvasCtx.fillStyle = viewBgColor;
 | 
	
		
			
				|  |  | +      tempCanvasCtx.fillRect(0, 0, canvas.width, canvas.height);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // 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
 | 
	
		
			
				|  |  | +    );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // create a temporary <a> elem which we'll use to download the image
 | 
	
		
			
				|  |  | +    const link = document.createElement("a");
 | 
	
		
			
				|  |  | +    link.setAttribute("download", "excalibur.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) {
 | 
	
	
		
			
				|  | @@ -396,11 +396,11 @@ class App extends React.Component<{}, AppState> {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      if (event.key === "Escape") {
 | 
	
		
			
				|  |  |        clearSelection();
 | 
	
		
			
				|  |  | -      drawScene();
 | 
	
		
			
				|  |  | +      this.forceUpdate();
 | 
	
		
			
				|  |  |        event.preventDefault();
 | 
	
		
			
				|  |  |      } else if (event.key === "Backspace") {
 | 
	
		
			
				|  |  |        deleteSelectedElements();
 | 
	
		
			
				|  |  | -      drawScene();
 | 
	
		
			
				|  |  | +      this.forceUpdate();
 | 
	
		
			
				|  |  |        event.preventDefault();
 | 
	
		
			
				|  |  |      } else if (
 | 
	
		
			
				|  |  |        event.key === "ArrowLeft" ||
 | 
	
	
		
			
				|  | @@ -417,13 +417,13 @@ class App extends React.Component<{}, AppState> {
 | 
	
		
			
				|  |  |            else if (event.key === "ArrowDown") element.y += step;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  | -      drawScene();
 | 
	
		
			
				|  |  | +      this.forceUpdate();
 | 
	
		
			
				|  |  |        event.preventDefault();
 | 
	
		
			
				|  |  |      } else if (event.key === "a" && event.metaKey) {
 | 
	
		
			
				|  |  |        elements.forEach(element => {
 | 
	
		
			
				|  |  |          element.isSelected = true;
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  | -      drawScene();
 | 
	
		
			
				|  |  | +      this.forceUpdate();
 | 
	
		
			
				|  |  |        event.preventDefault();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    };
 | 
	
	
		
			
				|  | @@ -443,7 +443,7 @@ class App extends React.Component<{}, AppState> {
 | 
	
		
			
				|  |  |            onChange={() => {
 | 
	
		
			
				|  |  |              this.setState({ elementType: type });
 | 
	
		
			
				|  |  |              clearSelection();
 | 
	
		
			
				|  |  | -            drawScene();
 | 
	
		
			
				|  |  | +            this.forceUpdate();
 | 
	
		
			
				|  |  |            }}
 | 
	
		
			
				|  |  |          />
 | 
	
		
			
				|  |  |          {children}
 | 
	
	
		
			
				|  | @@ -453,7 +453,50 @@ class App extends React.Component<{}, AppState> {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    public render() {
 | 
	
		
			
				|  |  |      return (
 | 
	
		
			
				|  |  | -      <>
 | 
	
		
			
				|  |  | +      <div
 | 
	
		
			
				|  |  | +        onCut={e => {
 | 
	
		
			
				|  |  | +          e.clipboardData.setData(
 | 
	
		
			
				|  |  | +            "text/plain",
 | 
	
		
			
				|  |  | +            JSON.stringify(elements.filter(element => element.isSelected))
 | 
	
		
			
				|  |  | +          );
 | 
	
		
			
				|  |  | +          deleteSelectedElements();
 | 
	
		
			
				|  |  | +          this.forceUpdate();
 | 
	
		
			
				|  |  | +          e.preventDefault();
 | 
	
		
			
				|  |  | +        }}
 | 
	
		
			
				|  |  | +        onCopy={e => {
 | 
	
		
			
				|  |  | +          e.clipboardData.setData(
 | 
	
		
			
				|  |  | +            "text/plain",
 | 
	
		
			
				|  |  | +            JSON.stringify(elements.filter(element => element.isSelected))
 | 
	
		
			
				|  |  | +          );
 | 
	
		
			
				|  |  | +          e.preventDefault();
 | 
	
		
			
				|  |  | +        }}
 | 
	
		
			
				|  |  | +        onPaste={e => {
 | 
	
		
			
				|  |  | +          const paste = e.clipboardData.getData("text");
 | 
	
		
			
				|  |  | +          let parsedElements;
 | 
	
		
			
				|  |  | +          try {
 | 
	
		
			
				|  |  | +            parsedElements = JSON.parse(paste);
 | 
	
		
			
				|  |  | +          } catch (e) {}
 | 
	
		
			
				|  |  | +          if (
 | 
	
		
			
				|  |  | +            Array.isArray(parsedElements) &&
 | 
	
		
			
				|  |  | +            parsedElements.length > 0 &&
 | 
	
		
			
				|  |  | +            parsedElements[0].type // need to implement a better check here...
 | 
	
		
			
				|  |  | +          ) {
 | 
	
		
			
				|  |  | +            clearSelection();
 | 
	
		
			
				|  |  | +            parsedElements.forEach(parsedElement => {
 | 
	
		
			
				|  |  | +              parsedElement.x += 10;
 | 
	
		
			
				|  |  | +              parsedElement.y += 10;
 | 
	
		
			
				|  |  | +              generateDraw(
 | 
	
		
			
				|  |  | +                parsedElement,
 | 
	
		
			
				|  |  | +                this.state.itemStrokeColor,
 | 
	
		
			
				|  |  | +                this.state.itemBackgroundColor
 | 
	
		
			
				|  |  | +              );
 | 
	
		
			
				|  |  | +              elements.push(parsedElement);
 | 
	
		
			
				|  |  | +            });
 | 
	
		
			
				|  |  | +            this.forceUpdate();
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +          e.preventDefault();
 | 
	
		
			
				|  |  | +        }}
 | 
	
		
			
				|  |  | +      >
 | 
	
		
			
				|  |  |          <fieldset>
 | 
	
		
			
				|  |  |            <legend>Shapes</legend>
 | 
	
		
			
				|  |  |            {this.renderOption({ type: "rectangle", children: "Rectangle" })}
 | 
	
	
		
			
				|  | @@ -462,218 +505,171 @@ class App extends React.Component<{}, AppState> {
 | 
	
		
			
				|  |  |            {this.renderOption({ type: "text", children: "Text" })}
 | 
	
		
			
				|  |  |            {this.renderOption({ type: "selection", children: "Selection" })}
 | 
	
		
			
				|  |  |          </fieldset>
 | 
	
		
			
				|  |  | -        <div
 | 
	
		
			
				|  |  | -          onCut={e => {
 | 
	
		
			
				|  |  | -            e.clipboardData.setData(
 | 
	
		
			
				|  |  | -              "text/plain",
 | 
	
		
			
				|  |  | -              JSON.stringify(elements.filter(element => element.isSelected))
 | 
	
		
			
				|  |  | -            );
 | 
	
		
			
				|  |  | -            deleteSelectedElements();
 | 
	
		
			
				|  |  | -            drawScene();
 | 
	
		
			
				|  |  | -            e.preventDefault();
 | 
	
		
			
				|  |  | -          }}
 | 
	
		
			
				|  |  | -          onCopy={e => {
 | 
	
		
			
				|  |  | -            e.clipboardData.setData(
 | 
	
		
			
				|  |  | -              "text/plain",
 | 
	
		
			
				|  |  | -              JSON.stringify(elements.filter(element => element.isSelected))
 | 
	
		
			
				|  |  | -            );
 | 
	
		
			
				|  |  | -            e.preventDefault();
 | 
	
		
			
				|  |  | -          }}
 | 
	
		
			
				|  |  | -          onPaste={e => {
 | 
	
		
			
				|  |  | -            const paste = e.clipboardData.getData("text");
 | 
	
		
			
				|  |  | -            let parsedElements;
 | 
	
		
			
				|  |  | -            try {
 | 
	
		
			
				|  |  | -              parsedElements = JSON.parse(paste);
 | 
	
		
			
				|  |  | -            } catch (e) {}
 | 
	
		
			
				|  |  | -            if (
 | 
	
		
			
				|  |  | -              Array.isArray(parsedElements) &&
 | 
	
		
			
				|  |  | -              parsedElements.length > 0 &&
 | 
	
		
			
				|  |  | -              parsedElements[0].type // need to implement a better check here...
 | 
	
		
			
				|  |  | -            ) {
 | 
	
		
			
				|  |  | -              clearSelection();
 | 
	
		
			
				|  |  | -              parsedElements.forEach(parsedElement => {
 | 
	
		
			
				|  |  | -                parsedElement.x += 10;
 | 
	
		
			
				|  |  | -                parsedElement.y += 10;
 | 
	
		
			
				|  |  | -                generateDraw(
 | 
	
		
			
				|  |  | -                  parsedElement,
 | 
	
		
			
				|  |  | -                  this.state.itemStrokeColor,
 | 
	
		
			
				|  |  | -                  this.state.itemBackgroundColor
 | 
	
		
			
				|  |  | -                );
 | 
	
		
			
				|  |  | -                elements.push(parsedElement);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        <canvas
 | 
	
		
			
				|  |  | +          id="canvas"
 | 
	
		
			
				|  |  | +          width={window.innerWidth}
 | 
	
		
			
				|  |  | +          height={window.innerHeight - 200}
 | 
	
		
			
				|  |  | +          onMouseDown={e => {
 | 
	
		
			
				|  |  | +            const x = e.clientX - (e.target as HTMLElement).offsetLeft;
 | 
	
		
			
				|  |  | +            const y = e.clientY - (e.target as HTMLElement).offsetTop;
 | 
	
		
			
				|  |  | +            const element = newElement(this.state.elementType, x, y);
 | 
	
		
			
				|  |  | +            let isDraggingElements = false;
 | 
	
		
			
				|  |  | +            const cursorStyle = document.documentElement.style.cursor;
 | 
	
		
			
				|  |  | +            if (this.state.elementType === "selection") {
 | 
	
		
			
				|  |  | +              const hitElement = elements.find(element => {
 | 
	
		
			
				|  |  | +                return hitTest(element, x, y);
 | 
	
		
			
				|  |  |                });
 | 
	
		
			
				|  |  | -              drawScene();
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | -            e.preventDefault();
 | 
	
		
			
				|  |  | -          }}
 | 
	
		
			
				|  |  | -        >
 | 
	
		
			
				|  |  | -          <canvas
 | 
	
		
			
				|  |  | -            id="canvas"
 | 
	
		
			
				|  |  | -            width={window.innerWidth}
 | 
	
		
			
				|  |  | -            height={window.innerHeight - 200}
 | 
	
		
			
				|  |  | -            onMouseDown={e => {
 | 
	
		
			
				|  |  | -              const x = e.clientX - (e.target as HTMLElement).offsetLeft;
 | 
	
		
			
				|  |  | -              const y = e.clientY - (e.target as HTMLElement).offsetTop;
 | 
	
		
			
				|  |  | -              const element = newElement(this.state.elementType, x, y);
 | 
	
		
			
				|  |  | -              let isDraggingElements = false;
 | 
	
		
			
				|  |  | -              const cursorStyle = document.documentElement.style.cursor;
 | 
	
		
			
				|  |  | -              if (this.state.elementType === "selection") {
 | 
	
		
			
				|  |  | -                const hitElement = elements.find(element => {
 | 
	
		
			
				|  |  | -                  return hitTest(element, x, y);
 | 
	
		
			
				|  |  | -                });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                // If we click on something
 | 
	
		
			
				|  |  | -                if (hitElement) {
 | 
	
		
			
				|  |  | -                  if (hitElement.isSelected) {
 | 
	
		
			
				|  |  | -                    // If that element is not already selected, do nothing,
 | 
	
		
			
				|  |  | -                    // we're likely going to drag it
 | 
	
		
			
				|  |  | -                  } else {
 | 
	
		
			
				|  |  | -                    // We unselect every other elements unless shift is pressed
 | 
	
		
			
				|  |  | -                    if (!e.shiftKey) {
 | 
	
		
			
				|  |  | -                      clearSelection();
 | 
	
		
			
				|  |  | -                    }
 | 
	
		
			
				|  |  | -                    // No matter what, we select it
 | 
	
		
			
				|  |  | -                    hitElement.isSelected = true;
 | 
	
		
			
				|  |  | -                  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +              // If we click on something
 | 
	
		
			
				|  |  | +              if (hitElement) {
 | 
	
		
			
				|  |  | +                if (hitElement.isSelected) {
 | 
	
		
			
				|  |  | +                  // If that element is not already selected, do nothing,
 | 
	
		
			
				|  |  | +                  // we're likely going to drag it
 | 
	
		
			
				|  |  |                  } else {
 | 
	
		
			
				|  |  | -                  // If we don't click on anything, let's remove all the selected elements
 | 
	
		
			
				|  |  | -                  clearSelection();
 | 
	
		
			
				|  |  | +                  // We unselect every other elements unless shift is pressed
 | 
	
		
			
				|  |  | +                  if (!e.shiftKey) {
 | 
	
		
			
				|  |  | +                    clearSelection();
 | 
	
		
			
				|  |  | +                  }
 | 
	
		
			
				|  |  | +                  // No matter what, we select it
 | 
	
		
			
				|  |  | +                  hitElement.isSelected = true;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  | +              } else {
 | 
	
		
			
				|  |  | +                // If we don't click on anything, let's remove all the selected elements
 | 
	
		
			
				|  |  | +                clearSelection();
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                isDraggingElements = elements.some(
 | 
	
		
			
				|  |  | -                  element => element.isSelected
 | 
	
		
			
				|  |  | -                );
 | 
	
		
			
				|  |  | +              isDraggingElements = elements.some(element => element.isSelected);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                if (isDraggingElements) {
 | 
	
		
			
				|  |  | -                  document.documentElement.style.cursor = "move";
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | +              if (isDraggingElements) {
 | 
	
		
			
				|  |  | +                document.documentElement.style.cursor = "move";
 | 
	
		
			
				|  |  |                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              if (isTextElement(element)) {
 | 
	
		
			
				|  |  | -                const text = prompt("What text do you want?");
 | 
	
		
			
				|  |  | -                if (text === null) {
 | 
	
		
			
				|  |  | +            if (isTextElement(element)) {
 | 
	
		
			
				|  |  | +              const text = prompt("What text do you want?");
 | 
	
		
			
				|  |  | +              if (text === null) {
 | 
	
		
			
				|  |  | +                return;
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +              element.text = text;
 | 
	
		
			
				|  |  | +              element.font = "20px Virgil";
 | 
	
		
			
				|  |  | +              const font = context.font;
 | 
	
		
			
				|  |  | +              context.font = element.font;
 | 
	
		
			
				|  |  | +              const {
 | 
	
		
			
				|  |  | +                actualBoundingBoxAscent,
 | 
	
		
			
				|  |  | +                actualBoundingBoxDescent,
 | 
	
		
			
				|  |  | +                width
 | 
	
		
			
				|  |  | +              } = context.measureText(element.text);
 | 
	
		
			
				|  |  | +              element.actualBoundingBoxAscent = actualBoundingBoxAscent;
 | 
	
		
			
				|  |  | +              context.font = font;
 | 
	
		
			
				|  |  | +              const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
 | 
	
		
			
				|  |  | +              // Center the text
 | 
	
		
			
				|  |  | +              element.x -= width / 2;
 | 
	
		
			
				|  |  | +              element.y -= actualBoundingBoxAscent;
 | 
	
		
			
				|  |  | +              element.width = width;
 | 
	
		
			
				|  |  | +              element.height = height;
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            generateDraw(
 | 
	
		
			
				|  |  | +              element,
 | 
	
		
			
				|  |  | +              this.state.itemStrokeColor,
 | 
	
		
			
				|  |  | +              this.state.itemBackgroundColor
 | 
	
		
			
				|  |  | +            );
 | 
	
		
			
				|  |  | +            elements.push(element);
 | 
	
		
			
				|  |  | +            if (this.state.elementType === "text") {
 | 
	
		
			
				|  |  | +              this.setState({
 | 
	
		
			
				|  |  | +                draggingElement: null,
 | 
	
		
			
				|  |  | +                elementType: "selection"
 | 
	
		
			
				|  |  | +              });
 | 
	
		
			
				|  |  | +              element.isSelected = true;
 | 
	
		
			
				|  |  | +            } else {
 | 
	
		
			
				|  |  | +              this.setState({ draggingElement: element });
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            let lastX = x;
 | 
	
		
			
				|  |  | +            let lastY = y;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            const onMouseMove = (e: MouseEvent) => {
 | 
	
		
			
				|  |  | +              const target = e.target;
 | 
	
		
			
				|  |  | +              if (!(target instanceof HTMLElement)) {
 | 
	
		
			
				|  |  | +                return;
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +              if (isDraggingElements) {
 | 
	
		
			
				|  |  | +                const selectedElements = elements.filter(el => el.isSelected);
 | 
	
		
			
				|  |  | +                if (selectedElements.length) {
 | 
	
		
			
				|  |  | +                  const x = e.clientX - target.offsetLeft;
 | 
	
		
			
				|  |  | +                  const y = e.clientY - target.offsetTop;
 | 
	
		
			
				|  |  | +                  selectedElements.forEach(element => {
 | 
	
		
			
				|  |  | +                    element.x += x - lastX;
 | 
	
		
			
				|  |  | +                    element.y += y - lastY;
 | 
	
		
			
				|  |  | +                  });
 | 
	
		
			
				|  |  | +                  lastX = x;
 | 
	
		
			
				|  |  | +                  lastY = y;
 | 
	
		
			
				|  |  | +                  this.forceUpdate();
 | 
	
		
			
				|  |  |                    return;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  | -                element.text = text;
 | 
	
		
			
				|  |  | -                element.font = "20px Virgil";
 | 
	
		
			
				|  |  | -                const font = context.font;
 | 
	
		
			
				|  |  | -                context.font = element.font;
 | 
	
		
			
				|  |  | -                const {
 | 
	
		
			
				|  |  | -                  actualBoundingBoxAscent,
 | 
	
		
			
				|  |  | -                  actualBoundingBoxDescent,
 | 
	
		
			
				|  |  | -                  width
 | 
	
		
			
				|  |  | -                } = context.measureText(element.text);
 | 
	
		
			
				|  |  | -                element.actualBoundingBoxAscent = actualBoundingBoxAscent;
 | 
	
		
			
				|  |  | -                context.font = font;
 | 
	
		
			
				|  |  | -                const height =
 | 
	
		
			
				|  |  | -                  actualBoundingBoxAscent + actualBoundingBoxDescent;
 | 
	
		
			
				|  |  | -                // Center the text
 | 
	
		
			
				|  |  | -                element.x -= width / 2;
 | 
	
		
			
				|  |  | -                element.y -= actualBoundingBoxAscent;
 | 
	
		
			
				|  |  | -                element.width = width;
 | 
	
		
			
				|  |  | -                element.height = height;
 | 
	
		
			
				|  |  |                }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +              // It is very important to read this.state within each move event,
 | 
	
		
			
				|  |  | +              // otherwise we would read a stale one!
 | 
	
		
			
				|  |  | +              const draggingElement = this.state.draggingElement;
 | 
	
		
			
				|  |  | +              if (!draggingElement) return;
 | 
	
		
			
				|  |  | +              let width = e.clientX - target.offsetLeft - draggingElement.x;
 | 
	
		
			
				|  |  | +              let height = e.clientY - target.offsetTop - draggingElement.y;
 | 
	
		
			
				|  |  | +              draggingElement.width = width;
 | 
	
		
			
				|  |  | +              // Make a perfect square or circle when shift is enabled
 | 
	
		
			
				|  |  | +              draggingElement.height = e.shiftKey ? width : height;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                generateDraw(
 | 
	
		
			
				|  |  | -                element,
 | 
	
		
			
				|  |  | +                draggingElement,
 | 
	
		
			
				|  |  |                  this.state.itemStrokeColor,
 | 
	
		
			
				|  |  |                  this.state.itemBackgroundColor
 | 
	
		
			
				|  |  |                );
 | 
	
		
			
				|  |  | -              elements.push(element);
 | 
	
		
			
				|  |  | -              if (this.state.elementType === "text") {
 | 
	
		
			
				|  |  | -                this.setState({
 | 
	
		
			
				|  |  | -                  draggingElement: null,
 | 
	
		
			
				|  |  | -                  elementType: "selection"
 | 
	
		
			
				|  |  | -                });
 | 
	
		
			
				|  |  | -                element.isSelected = true;
 | 
	
		
			
				|  |  | -              } else {
 | 
	
		
			
				|  |  | -                this.setState({ draggingElement: element });
 | 
	
		
			
				|  |  | -              }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -              let lastX = x;
 | 
	
		
			
				|  |  | -              let lastY = y;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              const onMouseMove = (e: MouseEvent) => {
 | 
	
		
			
				|  |  | -                const target = e.target;
 | 
	
		
			
				|  |  | -                if (!(target instanceof HTMLElement)) {
 | 
	
		
			
				|  |  | -                  return;
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                if (isDraggingElements) {
 | 
	
		
			
				|  |  | -                  const selectedElements = elements.filter(el => el.isSelected);
 | 
	
		
			
				|  |  | -                  if (selectedElements.length) {
 | 
	
		
			
				|  |  | -                    const x = e.clientX - target.offsetLeft;
 | 
	
		
			
				|  |  | -                    const y = e.clientY - target.offsetTop;
 | 
	
		
			
				|  |  | -                    selectedElements.forEach(element => {
 | 
	
		
			
				|  |  | -                      element.x += x - lastX;
 | 
	
		
			
				|  |  | -                      element.y += y - lastY;
 | 
	
		
			
				|  |  | -                    });
 | 
	
		
			
				|  |  | -                    lastX = x;
 | 
	
		
			
				|  |  | -                    lastY = y;
 | 
	
		
			
				|  |  | -                    drawScene();
 | 
	
		
			
				|  |  | -                    return;
 | 
	
		
			
				|  |  | -                  }
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                // It is very important to read this.state within each move event,
 | 
	
		
			
				|  |  | -                // otherwise we would read a stale one!
 | 
	
		
			
				|  |  | -                const draggingElement = this.state.draggingElement;
 | 
	
		
			
				|  |  | -                if (!draggingElement) return;
 | 
	
		
			
				|  |  | -                let width = e.clientX - target.offsetLeft - draggingElement.x;
 | 
	
		
			
				|  |  | -                let height = e.clientY - target.offsetTop - draggingElement.y;
 | 
	
		
			
				|  |  | -                draggingElement.width = width;
 | 
	
		
			
				|  |  | -                // Make a perfect square or circle when shift is enabled
 | 
	
		
			
				|  |  | -                draggingElement.height = e.shiftKey ? width : height;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                generateDraw(
 | 
	
		
			
				|  |  | -                  draggingElement,
 | 
	
		
			
				|  |  | -                  this.state.itemStrokeColor,
 | 
	
		
			
				|  |  | -                  this.state.itemBackgroundColor
 | 
	
		
			
				|  |  | -                );
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                if (this.state.elementType === "selection") {
 | 
	
		
			
				|  |  | -                  setSelection(draggingElement);
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | -                drawScene();
 | 
	
		
			
				|  |  | -              };
 | 
	
		
			
				|  |  | +              if (this.state.elementType === "selection") {
 | 
	
		
			
				|  |  | +                setSelection(draggingElement);
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  | +              this.forceUpdate();
 | 
	
		
			
				|  |  | +            };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              const onMouseUp = (e: MouseEvent) => {
 | 
	
		
			
				|  |  | -                const { draggingElement, elementType } = this.state;
 | 
	
		
			
				|  |  | +            const onMouseUp = (e: MouseEvent) => {
 | 
	
		
			
				|  |  | +              const { draggingElement, elementType } = this.state;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                window.removeEventListener("mousemove", onMouseMove);
 | 
	
		
			
				|  |  | -                window.removeEventListener("mouseup", onMouseUp);
 | 
	
		
			
				|  |  | +              window.removeEventListener("mousemove", onMouseMove);
 | 
	
		
			
				|  |  | +              window.removeEventListener("mouseup", onMouseUp);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                document.documentElement.style.cursor = cursorStyle;
 | 
	
		
			
				|  |  | +              document.documentElement.style.cursor = cursorStyle;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                // if no element is clicked, clear the selection and redraw
 | 
	
		
			
				|  |  | -                if (draggingElement === null) {
 | 
	
		
			
				|  |  | -                  clearSelection();
 | 
	
		
			
				|  |  | -                  drawScene();
 | 
	
		
			
				|  |  | -                  return;
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | +              // if no element is clicked, clear the selection and redraw
 | 
	
		
			
				|  |  | +              if (draggingElement === null) {
 | 
	
		
			
				|  |  | +                clearSelection();
 | 
	
		
			
				|  |  | +                this.forceUpdate();
 | 
	
		
			
				|  |  | +                return;
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                if (elementType === "selection") {
 | 
	
		
			
				|  |  | -                  if (isDraggingElements) {
 | 
	
		
			
				|  |  | -                    isDraggingElements = false;
 | 
	
		
			
				|  |  | -                  }
 | 
	
		
			
				|  |  | -                  elements.pop();
 | 
	
		
			
				|  |  | -                } else {
 | 
	
		
			
				|  |  | -                  draggingElement.isSelected = true;
 | 
	
		
			
				|  |  | +              if (elementType === "selection") {
 | 
	
		
			
				|  |  | +                if (isDraggingElements) {
 | 
	
		
			
				|  |  | +                  isDraggingElements = false;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  | +                elements.pop();
 | 
	
		
			
				|  |  | +              } else {
 | 
	
		
			
				|  |  | +                draggingElement.isSelected = true;
 | 
	
		
			
				|  |  | +              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                this.setState({
 | 
	
		
			
				|  |  | -                  draggingElement: null,
 | 
	
		
			
				|  |  | -                  elementType: "selection"
 | 
	
		
			
				|  |  | -                });
 | 
	
		
			
				|  |  | -                drawScene();
 | 
	
		
			
				|  |  | -              };
 | 
	
		
			
				|  |  | +              this.setState({
 | 
	
		
			
				|  |  | +                draggingElement: null,
 | 
	
		
			
				|  |  | +                elementType: "selection"
 | 
	
		
			
				|  |  | +              });
 | 
	
		
			
				|  |  | +              this.forceUpdate();
 | 
	
		
			
				|  |  | +            };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              window.addEventListener("mousemove", onMouseMove);
 | 
	
		
			
				|  |  | -              window.addEventListener("mouseup", onMouseUp);
 | 
	
		
			
				|  |  | +            window.addEventListener("mousemove", onMouseMove);
 | 
	
		
			
				|  |  | +            window.addEventListener("mouseup", onMouseUp);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -              drawScene();
 | 
	
		
			
				|  |  | -            }}
 | 
	
		
			
				|  |  | -          />
 | 
	
		
			
				|  |  | -        </div>
 | 
	
		
			
				|  |  | +            this.forceUpdate();
 | 
	
		
			
				|  |  | +          }}
 | 
	
		
			
				|  |  | +        />
 | 
	
		
			
				|  |  |          <fieldset>
 | 
	
		
			
				|  |  |            <legend>Colors</legend>
 | 
	
		
			
				|  |  |            <label>
 | 
	
	
		
			
				|  | @@ -752,9 +748,10 @@ class App extends React.Component<{}, AppState> {
 | 
	
		
			
				|  |  |            />
 | 
	
		
			
				|  |  |            px)
 | 
	
		
			
				|  |  |          </fieldset>
 | 
	
		
			
				|  |  | -      </>
 | 
	
		
			
				|  |  | +      </div>
 | 
	
		
			
				|  |  |      );
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    componentDidUpdate() {
 | 
	
		
			
				|  |  |      const fillStyle = context.fillStyle;
 | 
	
		
			
				|  |  |      context.fillStyle = this.state.viewBgColor;
 | 
	
	
		
			
				|  | @@ -794,8 +791,4 @@ const context = canvas.getContext("2d")!;
 | 
	
		
			
				|  |  |  // https://stackoverflow.com/questions/13879322/drawing-a-1px-thick-line-in-canvas-creates-a-2px-thick-line/13879402#comment90766599_13879402
 | 
	
		
			
				|  |  |  context.translate(0.5, 0.5);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -function drawScene() {
 | 
	
		
			
				|  |  | -  ReactDOM.render(<App />, rootElement);
 | 
	
		
			
				|  |  | -}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -drawScene();
 | 
	
		
			
				|  |  | +ReactDOM.render(<App />, rootElement);
 |