diff --git a/README.md b/README.md index cb9ebe6a9..c9458a507 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ Go to https://www.excalidraw.com to start sketching + + + ## Run the code diff --git a/src/element/handlerRectangles.ts b/src/element/handlerRectangles.ts index 6e01c9d86..985751109 100644 --- a/src/element/handlerRectangles.ts +++ b/src/element/handlerRectangles.ts @@ -1,11 +1,11 @@ -import { SceneState } from "../scene/types"; import { ExcalidrawElement } from "./types"; +import { SceneScroll } from "../scene/types"; type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se"; export function handlerRectangles( element: ExcalidrawElement, - sceneState: SceneState + { scrollX, scrollY }: SceneScroll ) { const elementX1 = element.x; const elementX2 = element.x + element.width; @@ -21,15 +21,15 @@ export function handlerRectangles( if (Math.abs(elementX2 - elementX1) > minimumSize) { handlers["n"] = [ - elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4, - elementY1 - margin + sceneState.scrollY + marginY, + elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4, + elementY1 - margin + scrollY + marginY, 8, 8 ]; handlers["s"] = [ - elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4, - elementY2 - margin + sceneState.scrollY - marginY, + elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4, + elementY2 - margin + scrollY - marginY, 8, 8 ]; @@ -37,41 +37,41 @@ export function handlerRectangles( if (Math.abs(elementY2 - elementY1) > minimumSize) { handlers["w"] = [ - elementX1 - margin + sceneState.scrollX + marginX, - elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4, + elementX1 - margin + scrollX + marginX, + elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4, 8, 8 ]; handlers["e"] = [ - elementX2 - margin + sceneState.scrollX - marginX, - elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4, + elementX2 - margin + scrollX - marginX, + elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4, 8, 8 ]; } handlers["nw"] = [ - elementX1 - margin + sceneState.scrollX + marginX, - elementY1 - margin + sceneState.scrollY + marginY, + elementX1 - margin + scrollX + marginX, + elementY1 - margin + scrollY + marginY, 8, 8 ]; // nw handlers["ne"] = [ - elementX2 - margin + sceneState.scrollX - marginX, - elementY1 - margin + sceneState.scrollY + marginY, + elementX2 - margin + scrollX - marginX, + elementY1 - margin + scrollY + marginY, 8, 8 ]; // ne handlers["sw"] = [ - elementX1 - margin + sceneState.scrollX + marginX, - elementY2 - margin + sceneState.scrollY - marginY, + elementX1 - margin + scrollX + marginX, + elementY2 - margin + scrollY - marginY, 8, 8 ]; // sw handlers["se"] = [ - elementX2 - margin + sceneState.scrollX - marginX, - elementY2 - margin + sceneState.scrollY - marginY, + elementX2 - margin + scrollX - marginX, + elementY2 - margin + scrollY - marginY, 8, 8 ]; // se diff --git a/src/element/resizeTest.ts b/src/element/resizeTest.ts index a74a2feca..51ecf7ef8 100644 --- a/src/element/resizeTest.ts +++ b/src/element/resizeTest.ts @@ -1,7 +1,7 @@ import { ExcalidrawElement } from "./types"; -import { SceneState } from "../scene/types"; import { handlerRectangles } from "./handlerRectangles"; +import { SceneScroll } from "../scene/types"; type HandlerRectanglesRet = keyof ReturnType; @@ -9,20 +9,20 @@ export function resizeTest( element: ExcalidrawElement, x: number, y: number, - sceneState: SceneState + { scrollX, scrollY }: SceneScroll ): HandlerRectanglesRet | false { if (!element.isSelected || element.type === "text") return false; - const handlers = handlerRectangles(element, sceneState); + const handlers = handlerRectangles(element, { scrollX, scrollY }); const filter = Object.keys(handlers).filter(key => { const handler = handlers[key as HandlerRectanglesRet]!; return ( - x + sceneState.scrollX >= handler[0] && - x + sceneState.scrollX <= handler[0] + handler[2] && - y + sceneState.scrollY >= handler[1] && - y + sceneState.scrollY <= handler[1] + handler[3] + x + scrollX >= handler[0] && + x + scrollX <= handler[0] + handler[2] && + y + scrollY >= handler[1] && + y + scrollY <= handler[1] + handler[3] ); }); @@ -32,3 +32,20 @@ export function resizeTest( return false; } + +export function getElementWithResizeHandler( + elements: ExcalidrawElement[], + { x, y }: { x: number; y: number }, + { scrollX, scrollY }: SceneScroll +) { + return elements.reduce((result, element) => { + if (result) { + return result; + } + const resizeHandle = resizeTest(element, x, y, { + scrollX, + scrollY + }); + return resizeHandle ? { element, resizeHandle } : null; + }, null as { element: ExcalidrawElement; resizeHandle: ReturnType } | null); +} diff --git a/src/index.tsx b/src/index.tsx index 0ab5716f7..81fde87eb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,7 +29,8 @@ import { hasStroke, getElementAtPosition, createScene, - getElementContainingPosition + getElementContainingPosition, + hasText } from "./scene"; import { renderScene } from "./renderer"; @@ -52,6 +53,7 @@ import { PanelCanvas } from "./components/panels/PanelCanvas"; import { Panel } from "./components/Panel"; import "./styles.scss"; +import { getElementWithResizeHandler } from "./element/resizeTest"; let { elements } = createScene(); const { history } = createHistory(); @@ -98,6 +100,15 @@ let lastCanvasHeight = -1; let lastMouseUp: ((e: any) => void) | null = null; +export function viewportCoordsToSceneCoords( + { clientX, clientY }: { clientX: number; clientY: number }, + { scrollX, scrollY }: { scrollX: number; scrollY: number } +) { + const x = clientX - CANVAS_WINDOW_OFFSET_LEFT - scrollX; + const y = clientY - CANVAS_WINDOW_OFFSET_TOP - scrollY; + return { x, y }; +} + export class App extends React.Component<{}, AppState> { canvas: HTMLCanvasElement | null = null; rc: RoughCanvas | null = null; @@ -265,7 +276,7 @@ export class App extends React.Component<{}, AppState> { const pastedElement = JSON.parse(copiedStyles); elements = elements.map(element => { if (element.isSelected) { - return { + const newElement = { ...element, backgroundColor: pastedElement?.backgroundColor, strokeWidth: pastedElement?.strokeWidth, @@ -274,6 +285,11 @@ export class App extends React.Component<{}, AppState> { opacity: pastedElement?.opacity, roughness: pastedElement?.roughness }; + if (isTextElement(newElement)) { + newElement.font = pastedElement?.font; + this.redrawTextBoundingBox(newElement); + } + return newElement; } return element; }); @@ -359,6 +375,13 @@ export class App extends React.Component<{}, AppState> { } }; + private redrawTextBoundingBox = (element: ExcalidrawTextElement) => { + const metrics = measureText(element.text, element.font); + element.width = metrics.width; + element.height = metrics.height; + element.baseline = metrics.baseline; + }; + public render() { const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT; const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP; @@ -490,6 +513,62 @@ export class App extends React.Component<{}, AppState> { )} + {hasText(elements) && ( + <> +
Font size
+ + isTextElement(element) && +element.font.split("px ")[0] + )} + onChange={value => + this.changeProperty(element => { + if (isTextElement(element)) { + element.font = `${value}px ${ + element.font.split("px ")[1] + }`; + this.redrawTextBoundingBox(element); + } + + return element; + }) + } + /> +
Font familly
+ + isTextElement(element) && element.font.split("px ")[1] + )} + onChange={value => + this.changeProperty(element => { + if (isTextElement(element)) { + element.font = `${ + element.font.split("px ")[0] + }px ${value}`; + this.redrawTextBoundingBox(element); + } + + return element; + }) + } + /> + + )} +
Opacity
{ onContextMenu={e => { e.preventDefault(); - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; + const { x, y } = viewportCoordsToSceneCoords(e, this.state); const element = getElementAtPosition(elements, x, y); if (!element) { @@ -652,9 +729,8 @@ export class App extends React.Component<{}, AppState> { this.state.scrollY ); - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; + const { x, y } = viewportCoordsToSceneCoords(e, this.state); + const element = newElement( this.state.elementType, x, @@ -666,28 +742,23 @@ export class App extends React.Component<{}, AppState> { 1, 100 ); - let resizeHandle: ReturnType = false; + type ResizeTestType = ReturnType; + let resizeHandle: ResizeTestType = false; let isDraggingElements = false; let isResizingElements = false; if (this.state.elementType === "selection") { - const resizeElement = elements.find(element => { - return resizeTest(element, x, y, { - scrollX: this.state.scrollX, - scrollY: this.state.scrollY, - viewBackgroundColor: this.state.viewBackgroundColor - }); - }); + const resizeElement = getElementWithResizeHandler( + elements, + { x, y }, + this.state + ); this.setState({ - resizingElement: resizeElement ? resizeElement : null + resizingElement: resizeElement ? resizeElement.element : null }); if (resizeElement) { - resizeHandle = resizeTest(resizeElement, x, y, { - scrollX: this.state.scrollX, - scrollY: this.state.scrollY, - viewBackgroundColor: this.state.viewBackgroundColor - }); + resizeHandle = resizeElement.resizeHandle; document.documentElement.style.cursor = `${resizeHandle}-resize`; isResizingElements = true; } else { @@ -696,7 +767,7 @@ export class App extends React.Component<{}, AppState> { // If we click on something if (hitElement) { if (hitElement.isSelected) { - // If that element is not already selected, do nothing, + // If that element is already selected, do nothing, // we're likely going to drag it } else { // We unselect every other elements unless shift is pressed @@ -811,10 +882,8 @@ export class App extends React.Component<{}, AppState> { const el = this.state.resizingElement; const selectedElements = elements.filter(el => el.isSelected); if (selectedElements.length === 1) { - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = - e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; + const { x, y } = viewportCoordsToSceneCoords(e, this.state); + selectedElements.forEach(element => { switch (resizeHandle) { case "nw": @@ -886,10 +955,8 @@ export class App extends React.Component<{}, AppState> { if (isDraggingElements) { const selectedElements = elements.filter(el => el.isSelected); if (selectedElements.length) { - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = - e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; + const { x, y } = viewportCoordsToSceneCoords(e, this.state); + selectedElements.forEach(element => { element.x += x - lastX; element.y += y - lastY; @@ -973,9 +1040,8 @@ export class App extends React.Component<{}, AppState> { this.forceUpdate(); }} onDoubleClick={e => { - const x = - e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; - const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; + const { x, y } = viewportCoordsToSceneCoords(e, this.state); + const elementAtPosition = getElementAtPosition(elements, x, y); const element = newElement( @@ -1047,6 +1113,34 @@ export class App extends React.Component<{}, AppState> { } }); }} + onMouseMove={e => { + const hasDeselectedButton = Boolean(e.buttons); + if (hasDeselectedButton || this.state.elementType !== "selection") { + return; + } + const { x, y } = viewportCoordsToSceneCoords(e, this.state); + const resizeElement = getElementWithResizeHandler( + elements, + { x, y }, + this.state + ); + if (resizeElement && resizeElement.resizeHandle) { + document.documentElement.style.cursor = `${resizeElement.resizeHandle}-resize`; + return; + } + const hitElement = getElementAtPosition(elements, x, y); + if (hitElement) { + const resizeHandle = resizeTest(hitElement, x, y, { + scrollX: this.state.scrollX, + scrollY: this.state.scrollY + }); + document.documentElement.style.cursor = resizeHandle + ? `${resizeHandle}-resize` + : `move`; + } else { + document.documentElement.style.cursor = ``; + } + }} /> ); diff --git a/src/scene/comparisons.ts b/src/scene/comparisons.ts index a1a3d41d6..30418a93b 100644 --- a/src/scene/comparisons.ts +++ b/src/scene/comparisons.ts @@ -21,6 +21,9 @@ export const hasStroke = (elements: ExcalidrawElement[]) => element.type === "arrow") ); +export const hasText = (elements: ExcalidrawElement[]) => + elements.some(element => element.isSelected && element.type === "text"); + export function getElementAtPosition( elements: ExcalidrawElement[], x: number, diff --git a/src/scene/index.ts b/src/scene/index.ts index 479c60efa..171315f40 100644 --- a/src/scene/index.ts +++ b/src/scene/index.ts @@ -18,6 +18,7 @@ export { hasBackground, hasStroke, getElementAtPosition, - getElementContainingPosition + getElementContainingPosition, + hasText } from "./comparisons"; export { createScene } from "./createScene"; diff --git a/src/scene/types.ts b/src/scene/types.ts index 41a2f1b2a..c8904dca5 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -7,6 +7,11 @@ export type SceneState = { viewBackgroundColor: string | null; }; +export type SceneScroll = { + scrollX: number; + scrollY: number; +}; + export interface Scene { elements: ExcalidrawTextElement[]; }