mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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:
parent
3e3ce18755
commit
65be7973be
23 changed files with 664 additions and 108 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue