mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Better scrollbars
This commit is contained in:
parent
e7e676e1eb
commit
a59a2ff806
2 changed files with 144 additions and 35 deletions
113
src/index.tsx
113
src/index.tsx
|
@ -4,6 +4,7 @@ import rough from "roughjs/bin/wrappers/rough";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
|
|
||||||
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
|
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
|
||||||
|
import { roundRect } from "./roundRect";
|
||||||
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
|
|
||||||
|
@ -254,38 +255,72 @@ type SceneState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCROLLBAR_WIDTH = 6;
|
const SCROLLBAR_WIDTH = 6;
|
||||||
|
const SCROLLBAR_MIN_SIZE = 15;
|
||||||
const SCROLLBAR_MARGIN = 4;
|
const SCROLLBAR_MARGIN = 4;
|
||||||
const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||||
const CANVAS_WINDOW_OFFSET_LEFT = 250;
|
const CANVAS_WINDOW_OFFSET_LEFT = 250;
|
||||||
const CANVAS_WINDOW_OFFSET_TOP = 0;
|
const CANVAS_WINDOW_OFFSET_TOP = 0;
|
||||||
|
|
||||||
function getScrollbars(
|
function getScrollBars(
|
||||||
canvasWidth: number,
|
canvasWidth: number,
|
||||||
canvasHeight: number,
|
canvasHeight: number,
|
||||||
scrollX: number,
|
scrollX: number,
|
||||||
scrollY: number
|
scrollY: number
|
||||||
) {
|
) {
|
||||||
|
let minX = Infinity;
|
||||||
|
let maxX = 0;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxY = 0;
|
||||||
|
|
||||||
|
elements.forEach(element => {
|
||||||
|
minX = Math.min(minX, getElementAbsoluteX1(element));
|
||||||
|
maxX = Math.max(maxX, getElementAbsoluteX2(element));
|
||||||
|
minY = Math.min(minY, getElementAbsoluteY1(element));
|
||||||
|
maxY = Math.max(maxY, getElementAbsoluteY2(element));
|
||||||
|
});
|
||||||
|
|
||||||
|
minX += scrollX;
|
||||||
|
maxX += scrollX;
|
||||||
|
minY += scrollY;
|
||||||
|
maxY += scrollY;
|
||||||
|
const leftOverflow = Math.max(-minX, 0);
|
||||||
|
const rightOverflow = Math.max(-(canvasWidth - maxX), 0);
|
||||||
|
const topOverflow = Math.max(-minY, 0);
|
||||||
|
const bottomOverflow = Math.max(-(canvasHeight - maxY), 0);
|
||||||
|
|
||||||
// horizontal scrollbar
|
// horizontal scrollbar
|
||||||
const sceneWidth = canvasWidth + Math.abs(scrollX);
|
let horizontalScrollBar = null;
|
||||||
const scrollBarWidth = (canvasWidth * canvasWidth) / sceneWidth;
|
if (leftOverflow || rightOverflow) {
|
||||||
const scrollBarX = scrollX > 0 ? 0 : canvasWidth - scrollBarWidth;
|
horizontalScrollBar = {
|
||||||
const horizontalScrollBar = {
|
x: Math.min(
|
||||||
x: scrollBarX + SCROLLBAR_MARGIN,
|
leftOverflow + SCROLLBAR_MARGIN,
|
||||||
y: canvasHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
|
canvasWidth - SCROLLBAR_MIN_SIZE - SCROLLBAR_MARGIN
|
||||||
width: scrollBarWidth - SCROLLBAR_MARGIN * 2,
|
),
|
||||||
height: SCROLLBAR_WIDTH
|
y: canvasHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
|
||||||
};
|
width: Math.max(
|
||||||
|
canvasWidth - rightOverflow - leftOverflow - SCROLLBAR_MARGIN * 2,
|
||||||
|
SCROLLBAR_MIN_SIZE
|
||||||
|
),
|
||||||
|
height: SCROLLBAR_WIDTH
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// vertical scrollbar
|
// vertical scrollbar
|
||||||
const sceneHeight = canvasHeight + Math.abs(scrollY);
|
let verticalScrollBar = null;
|
||||||
const scrollBarHeight = (canvasHeight * canvasHeight) / sceneHeight;
|
if (topOverflow || bottomOverflow) {
|
||||||
const scrollBarY = scrollY > 0 ? 0 : canvasHeight - scrollBarHeight;
|
verticalScrollBar = {
|
||||||
const verticalScrollBar = {
|
x: canvasWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
|
||||||
x: canvasWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
|
y: Math.min(
|
||||||
y: scrollBarY + SCROLLBAR_MARGIN,
|
topOverflow + SCROLLBAR_MARGIN,
|
||||||
width: SCROLLBAR_WIDTH,
|
canvasHeight - SCROLLBAR_MIN_SIZE - SCROLLBAR_MARGIN
|
||||||
height: scrollBarHeight - SCROLLBAR_WIDTH * 2
|
),
|
||||||
};
|
width: SCROLLBAR_WIDTH,
|
||||||
|
height: Math.max(
|
||||||
|
canvasHeight - bottomOverflow - topOverflow - SCROLLBAR_WIDTH * 2,
|
||||||
|
SCROLLBAR_MIN_SIZE
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
horizontal: horizontalScrollBar,
|
horizontal: horizontalScrollBar,
|
||||||
|
@ -301,13 +336,14 @@ function isOverScrollBars(
|
||||||
scrollX: number,
|
scrollX: number,
|
||||||
scrollY: number
|
scrollY: number
|
||||||
) {
|
) {
|
||||||
const scrollBars = getScrollbars(canvasWidth, canvasHeight, scrollX, scrollY);
|
const scrollBars = getScrollBars(canvasWidth, canvasHeight, scrollX, scrollY);
|
||||||
|
|
||||||
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
|
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
|
||||||
scrollBars.horizontal,
|
scrollBars.horizontal,
|
||||||
scrollBars.vertical
|
scrollBars.vertical
|
||||||
].map(
|
].map(
|
||||||
scrollBar =>
|
scrollBar =>
|
||||||
|
scrollBar &&
|
||||||
scrollBar.x <= x &&
|
scrollBar.x <= x &&
|
||||||
x <= scrollBar.x + scrollBar.width &&
|
x <= scrollBar.x + scrollBar.width &&
|
||||||
scrollBar.y <= y &&
|
scrollBar.y <= y &&
|
||||||
|
@ -466,26 +502,30 @@ function renderScene(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (renderScrollbars) {
|
if (renderScrollbars) {
|
||||||
const scrollBars = getScrollbars(
|
const scrollBars = getScrollBars(
|
||||||
context.canvas.width / window.devicePixelRatio,
|
context.canvas.width / window.devicePixelRatio,
|
||||||
context.canvas.height / window.devicePixelRatio,
|
context.canvas.height / window.devicePixelRatio,
|
||||||
sceneState.scrollX,
|
sceneState.scrollX,
|
||||||
sceneState.scrollY
|
sceneState.scrollY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const strokeStyle = context.strokeStyle;
|
||||||
context.fillStyle = SCROLLBAR_COLOR;
|
context.fillStyle = SCROLLBAR_COLOR;
|
||||||
context.fillRect(
|
context.strokeStyle = "rgba(255,255,255,0.8)";
|
||||||
scrollBars.horizontal.x,
|
[scrollBars.horizontal, scrollBars.vertical].forEach(scrollBar => {
|
||||||
scrollBars.horizontal.y,
|
if (scrollBar)
|
||||||
scrollBars.horizontal.width,
|
roundRect(
|
||||||
scrollBars.horizontal.height
|
context,
|
||||||
);
|
scrollBar.x,
|
||||||
context.fillRect(
|
scrollBar.y,
|
||||||
scrollBars.vertical.x,
|
scrollBar.width,
|
||||||
scrollBars.vertical.y,
|
scrollBar.height,
|
||||||
scrollBars.vertical.width,
|
SCROLLBAR_WIDTH / 2,
|
||||||
scrollBars.vertical.height
|
true,
|
||||||
);
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
context.strokeStyle = strokeStyle;
|
||||||
context.fillStyle = fillStyle;
|
context.fillStyle = fillStyle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -889,7 +929,6 @@ const SHAPES = [
|
||||||
|
|
||||||
const shapesShortcutKeys = SHAPES.map(shape => shape.value[0]);
|
const shapesShortcutKeys = SHAPES.map(shape => shape.value[0]);
|
||||||
|
|
||||||
|
|
||||||
function capitalize(str: string) {
|
function capitalize(str: string) {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
}
|
}
|
||||||
|
@ -1134,7 +1173,11 @@ class App extends React.Component<{}, AppState> {
|
||||||
<h4>Shapes</h4>
|
<h4>Shapes</h4>
|
||||||
<div className="panelTools">
|
<div className="panelTools">
|
||||||
{SHAPES.map(({ value, icon }) => (
|
{SHAPES.map(({ value, icon }) => (
|
||||||
<label key={value} className="tool" title={`${capitalize(value)} - ${capitalize(value)[0]}`}>
|
<label
|
||||||
|
key={value}
|
||||||
|
className="tool"
|
||||||
|
title={`${capitalize(value)} - ${capitalize(value)[0]}`}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
checked={this.state.elementType === value}
|
checked={this.state.elementType === value}
|
||||||
|
|
66
src/roundRect.ts
Normal file
66
src/roundRect.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* https://stackoverflow.com/a/3368118
|
||||||
|
* Draws a rounded rectangle using the current state of the canvas.
|
||||||
|
* If you omit the last three params, it will draw a rectangle
|
||||||
|
* outline with a 5 pixel border radius
|
||||||
|
* @param {CanvasRenderingContext2D} ctx
|
||||||
|
* @param {Number} x The top left x coordinate
|
||||||
|
* @param {Number} y The top left y coordinate
|
||||||
|
* @param {Number} width The width of the rectangle
|
||||||
|
* @param {Number} height The height of the rectangle
|
||||||
|
* @param {Number} [radius = 5] The corner radius; It can also be an object
|
||||||
|
* to specify different radii for corners
|
||||||
|
* @param {Number} [radius.tl = 0] Top left
|
||||||
|
* @param {Number} [radius.tr = 0] Top right
|
||||||
|
* @param {Number} [radius.br = 0] Bottom right
|
||||||
|
* @param {Number} [radius.bl = 0] Bottom left
|
||||||
|
* @param {Boolean} [fill = false] Whether to fill the rectangle.
|
||||||
|
* @param {Boolean} [stroke = true] Whether to stroke the rectangle.
|
||||||
|
*/
|
||||||
|
export function roundRect(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
radius?: number | { tl: number; tr: number; br: number; bl: number },
|
||||||
|
fill?: boolean,
|
||||||
|
stroke?: boolean
|
||||||
|
) {
|
||||||
|
if (typeof stroke === "undefined") {
|
||||||
|
stroke = true;
|
||||||
|
}
|
||||||
|
if (typeof radius === "undefined") {
|
||||||
|
radius = 5;
|
||||||
|
}
|
||||||
|
if (typeof radius === "number") {
|
||||||
|
radius = { tl: radius, tr: radius, br: radius, bl: radius };
|
||||||
|
} else {
|
||||||
|
const sides = ["tl", "tr", "br", "bl"] as const;
|
||||||
|
for (const side of sides) {
|
||||||
|
radius[side] = radius[side] || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + radius.tl, y);
|
||||||
|
ctx.lineTo(x + width - radius.tr, y);
|
||||||
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
|
||||||
|
ctx.lineTo(x + width, y + height - radius.br);
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
x + width,
|
||||||
|
y + height,
|
||||||
|
x + width - radius.br,
|
||||||
|
y + height
|
||||||
|
);
|
||||||
|
ctx.lineTo(x + radius.bl, y + height);
|
||||||
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
|
||||||
|
ctx.lineTo(x, y + radius.tl);
|
||||||
|
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
|
||||||
|
ctx.closePath();
|
||||||
|
if (fill) {
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
if (stroke) {
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue