Rotation support (#1099)

* rotate rectanble with fixed angle

* rotate dashed rectangle with fixed angle

* fix rotate handler rect

* fix canvas size with rotation

* angle in element base

* fix bug in calculating canvas size

* trial only for rectangle

* hitTest for rectangle rotation

* properly resize rotated rectangle

* fix canvas size calculation

* giving up... workaround for now

* **experimental** handler to rotate rectangle

* remove rotation on copy for debugging

* update snapshots

* better rotation handler with atan2

* rotate when drawImage

* add rotation handler

* hitTest for any shapes

* fix hitTest for curved lines

* rotate text element

* rotation locking

* hint messaage for rotating

* show proper handlers on mobile (a workaround, there should be a better way)

* refactor hitTest

* support exporting png

* support exporting svg

* fix rotating curved line

* refactor drawElementFromCanvas with getElementAbsoluteCoords

* fix export png and svg

* adjust resize positions for lines (N, E, S, W)

* do not make handlers big on mobile

* Update src/locales/en.json

Alright!

Co-Authored-By: Lipis <lipiridis@gmail.com>

* do not show rotation/resizing hints on mobile

* proper calculation for N and W positions

* simplify calculation

* use "rotation" as property name for clarification (may increase bundle size)

* update snapshots excluding rotation handle

* refactor with adjustPositionWithRotation

* refactor with adjustXYWithRotation

* forgot to rename rotation

* rename internal function

* initialize element angle on restore

* rotate wysiwyg editor

* fix shift-rotate around 270deg

* improve rotation locking

* refactor adjustXYWithRotation

* avoid rotation degree becomes >=360

* refactor with generateHandler

Co-authored-by: Lipis <lipiridis@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Daishi Kato 2020-04-02 17:40:26 +09:00 committed by GitHub
parent 3e3ce18755
commit 65be7973be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 664 additions and 108 deletions

View file

@ -263,30 +263,24 @@ function drawElementFromCanvas(
context: CanvasRenderingContext2D,
sceneState: SceneState,
) {
const element = elementWithCanvas.element;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
const cy = ((y1 + y2) / 2 + sceneState.scrollY) * window.devicePixelRatio;
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
context.translate(
-CANVAS_PADDING / elementWithCanvas.canvasZoom,
-CANVAS_PADDING / elementWithCanvas.canvasZoom,
);
context.translate(cx, cy);
context.rotate(element.angle);
context.drawImage(
elementWithCanvas.canvas!,
Math.floor(
-elementWithCanvas.canvasOffsetX +
(Math.floor(elementWithCanvas.element.x) + sceneState.scrollX) *
window.devicePixelRatio,
),
Math.floor(
-elementWithCanvas.canvasOffsetY +
(Math.floor(elementWithCanvas.element.y) + sceneState.scrollY) *
window.devicePixelRatio,
),
(-(x2 - x1) / 2) * window.devicePixelRatio -
CANVAS_PADDING / elementWithCanvas.canvasZoom,
(-(y2 - y1) / 2) * window.devicePixelRatio -
CANVAS_PADDING / elementWithCanvas.canvasZoom,
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
);
context.translate(
CANVAS_PADDING / elementWithCanvas.canvasZoom,
CANVAS_PADDING / elementWithCanvas.canvasZoom,
);
context.rotate(-element.angle);
context.translate(-cx, -cy);
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
@ -325,11 +319,18 @@ export function renderElement(
if (renderOptimizations) {
drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
} else {
const offsetX = Math.floor(element.x + sceneState.scrollX);
const offsetY = Math.floor(element.y + sceneState.scrollY);
context.translate(offsetX, offsetY);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2 + sceneState.scrollX;
const cy = (y1 + y2) / 2 + sceneState.scrollY;
const shiftX = (x2 - x1) / 2 - (element.x - x1);
const shiftY = (y2 - y1) / 2 - (element.y - y1);
context.translate(cx, cy);
context.rotate(element.angle);
context.translate(-shiftX, -shiftY);
drawElementOnCanvas(element, rc, context);
context.translate(-offsetX, -offsetY);
context.translate(shiftX, shiftY);
context.rotate(-element.angle);
context.translate(-cx, -cy);
}
break;
}
@ -347,6 +348,10 @@ export function renderElementToSvg(
offsetX?: number,
offsetY?: number,
) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x2 - x1) / 2 - (element.x - x1);
const cy = (y2 - y1) / 2 - (element.y - y1);
const degree = (180 * element.angle) / Math.PI;
const generator = rsvg.generator;
switch (element.type) {
case "selection": {
@ -366,7 +371,9 @@ export function renderElementToSvg(
}
node.setAttribute(
"transform",
`translate(${offsetX || 0} ${offsetY || 0})`,
`translate(${offsetX || 0} ${
offsetY || 0
}) rotate(${degree} ${cx} ${cy})`,
);
svgRoot.appendChild(node);
break;
@ -384,7 +391,9 @@ export function renderElementToSvg(
}
node.setAttribute(
"transform",
`translate(${offsetX || 0} ${offsetY || 0})`,
`translate(${offsetX || 0} ${
offsetY || 0
}) rotate(${degree} ${cx} ${cy})`,
);
group.appendChild(node);
});
@ -401,7 +410,9 @@ export function renderElementToSvg(
}
node.setAttribute(
"transform",
`translate(${offsetX || 0} ${offsetY || 0})`,
`translate(${offsetX || 0} ${
offsetY || 0
}) rotate(${degree} ${cx} ${cy})`,
);
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = element.height / lines.length;

View file

@ -17,6 +17,8 @@ import { getSelectedElements } from "../scene/selection";
import { renderElement, renderElementToSvg } from "./renderElement";
import colors from "../colors";
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
function colorsForClientId(clientId: string) {
// Naive way of getting an integer out of the clientId
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
@ -26,6 +28,40 @@ function colorsForClientId(clientId: string) {
};
}
function strokeRectWithRotation(
context: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
cx: number,
cy: number,
angle: number,
fill?: boolean,
) {
context.translate(cx, cy);
context.rotate(angle);
if (fill) {
context.fillRect(x - cx, y - cy, width, height);
}
context.strokeRect(x - cx, y - cy, width, height);
context.rotate(-angle);
context.translate(-cx, -cy);
}
function strokeCircle(
context: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
) {
context.beginPath();
context.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2);
context.fill();
context.stroke();
}
export function renderScene(
allElements: readonly ExcalidrawElement[],
appState: AppState,
@ -113,7 +149,7 @@ export function renderScene(
// Pain selected elements
if (renderSelection) {
const selectedElements = getSelectedElements(elements, appState);
const dashledLinePadding = 4 / sceneState.zoom;
const dashedLinePadding = 4 / sceneState.zoom;
context.translate(sceneState.scrollX, sceneState.scrollY);
selectedElements.forEach((element) => {
@ -131,11 +167,15 @@ export function renderScene(
context.setLineDash([8 / sceneState.zoom, 4 / sceneState.zoom]);
const lineWidth = context.lineWidth;
context.lineWidth = 1 / sceneState.zoom;
context.strokeRect(
elementX1 - dashledLinePadding,
elementY1 - dashledLinePadding,
elementWidth + dashledLinePadding * 2,
elementHeight + dashledLinePadding * 2,
strokeRectWithRotation(
context,
elementX1 - dashedLinePadding,
elementY1 - dashedLinePadding,
elementWidth + dashedLinePadding * 2,
elementHeight + dashedLinePadding * 2,
elementX1 + elementWidth / 2,
elementY1 + elementHeight / 2,
element.angle,
);
context.lineWidth = lineWidth;
context.setLineDash(initialLineDash);
@ -143,19 +183,39 @@ export function renderScene(
context.translate(-sceneState.scrollX, -sceneState.scrollY);
// Paint resize handlers
if (selectedElements.length === 1 && selectedElements[0].type !== "text") {
if (selectedElements.length === 1) {
context.translate(sceneState.scrollX, sceneState.scrollY);
context.fillStyle = "#fff";
const handlers = handlerRectangles(selectedElements[0], sceneState.zoom);
Object.values(handlers)
.filter((handler) => handler !== undefined)
.forEach((handler) => {
Object.keys(handlers).forEach((key) => {
const handler = handlers[key as HandlerRectanglesRet];
if (handler !== undefined) {
const lineWidth = context.lineWidth;
context.lineWidth = 1 / sceneState.zoom;
context.fillRect(handler[0], handler[1], handler[2], handler[3]);
context.strokeRect(handler[0], handler[1], handler[2], handler[3]);
if (key === "rotation") {
strokeCircle(
context,
handler[0],
handler[1],
handler[2],
handler[3],
);
} else if (selectedElements[0].type !== "text") {
strokeRectWithRotation(
context,
handler[0],
handler[1],
handler[2],
handler[3],
handler[0] + handler[2] / 2,
handler[1] + handler[3] / 2,
selectedElements[0].angle,
true, // fill before stroke
);
}
context.lineWidth = lineWidth;
});
}
});
context.translate(-sceneState.scrollX, -sceneState.scrollY);
}
}