diff --git a/packages/core/src/utils/utils.ts b/packages/core/src/utils/utils.ts index b1aa22f02..ea305aa75 100644 --- a/packages/core/src/utils/utils.ts +++ b/packages/core/src/utils/utils.ts @@ -1101,7 +1101,7 @@ export class Utils { y: number, rx: number, ry: number, - rotation: number + rotation = 0 ): TLBounds { const c = Math.cos(rotation) const s = Math.sin(rotation) @@ -1649,29 +1649,34 @@ left past the initial left edge) then swap points on that axis. } } + // Regex to trim numbers to 2 decimal places + static TRIM_NUMBERS = /(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g + /** * Turn an array of points into a path of quadradic curves. * @param stroke ; */ - static getSvgPathFromStroke(stroke: number[][]): string { - if (!stroke.length) return '' + static getSvgPathFromStroke(points: number[][]): string { + if (!points.length) { + return '' + } - const max = stroke.length - 1 + const max = points.length - 1 - const d = stroke.reduce( - (acc, [x0, y0], i, arr) => { - if (i === max) return acc - const [x1, y1] = arr[i + 1] - acc.push(` ${x0},${y0} ${(x0 + x1) / 2},${(y0 + y1) / 2}`) - return acc - }, - ['M ', `${stroke[0][0]},${stroke[0][1]}`, ' Q'] - ) - - return d - .concat('Z') - .join('') - .replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1') + return points + .reduce( + (acc, point, i, arr) => { + if (i === max) { + acc.push('Z') + } else { + acc.push(point, Vec.med(point, arr[i + 1])) + } + return acc + }, + ['M', points[0], 'Q'] + ) + .join(' ') + .replaceAll(this.TRIM_NUMBERS, '$1') } /* -------------------------------------------------- */ diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index cbf39b74a..139c166ef 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -68,7 +68,7 @@ "@tldraw/core": "^0.0.95", "@tldraw/intersect": "^0.0.95", "@tldraw/vec": "^0.0.95", - "perfect-freehand": "^1.0.4", + "perfect-freehand": "^1.0.6", "react-hotkeys-hook": "^3.4.0", "rko": "^0.5.25" }, diff --git a/packages/tldraw/src/shape/shape-styles.ts b/packages/tldraw/src/shape/shape-styles.ts index 5751e9d6f..ad464d589 100644 --- a/packages/tldraw/src/shape/shape-styles.ts +++ b/packages/tldraw/src/shape/shape-styles.ts @@ -51,9 +51,9 @@ const strokeWidths = { } const fontSizes = { - [SizeStyle.Small]: 24, - [SizeStyle.Medium]: 48, - [SizeStyle.Large]: 72, + [SizeStyle.Small]: 40, + [SizeStyle.Medium]: 64, + [SizeStyle.Large]: 140, auto: 'auto', } diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index f25dc1d50..71f58a55a 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -5,13 +5,11 @@ import { intersectBoundsBounds, intersectBoundsPolyline } from '@tldraw/intersec import getStroke, { getStrokePoints } from 'perfect-freehand' import { defaultStyle, getShapeStyle } from '~shape/shape-styles' import { DrawShape, DashStyle, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types' +import { EASINGS } from '~state/utils' const pointsBoundsCache = new WeakMap([]) const shapeBoundsCache = new Map() const rotatedCache = new WeakMap([]) -const drawPathCache = new WeakMap([]) -const simplePathCache = new WeakMap([]) -const polygonCache = new WeakMap([]) const pointCache = new WeakSet([]) export const Draw = new ShapeUtil(() => ({ @@ -34,6 +32,16 @@ export const Draw = new ShapeUtil(() => ({ Component({ shape, meta, events, isEditing }, ref) { const { points, style } = shape + const polygonPathData = React.useMemo(() => { + return getFillPath(shape) + }, [points, style.size, isEditing]) + + const pathData = React.useMemo(() => { + return style.dash === DashStyle.Draw + ? getDrawStrokePath(shape, isEditing) + : getSolidStrokePath(shape) + }, [points, style.size, style.dash, isEditing]) + const styles = getShapeStyle(style, meta.isDarkMode) const strokeWidth = styles.strokeWidth @@ -64,15 +72,7 @@ export const Draw = new ShapeUtil(() => ({ points.length > 3 && Vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2 - // For drawn lines, draw a line from the path cache - if (shape.style.dash === DashStyle.Draw) { - const polygonPathData = Utils.getFromCache(polygonCache, points, () => getFillPath(shape)) - - const drawPathData = isEditing - ? getDrawStrokePath(shape, true) - : Utils.getFromCache(drawPathCache, points, () => getDrawStrokePath(shape, false)) - return ( {shouldFill && ( @@ -86,7 +86,7 @@ export const Draw = new ShapeUtil(() => ({ /> )} (() => ({ [DashStyle.Dashed]: `-${strokeWidth}`, }[style.dash] - const path = Utils.getFromCache(simplePathCache, points, () => getSolidStrokePath(shape)) - const sw = strokeWidth * 1.618 return ( (() => ({ pointerEvents={shouldFill ? 'all' : 'stroke'} /> (() => ({ Indicator({ shape }) { const { points } = shape + const pathData = React.useMemo(() => { + return getSolidStrokePath(shape) + }, [points]) + const bounds = this.getBounds(shape) const verySmall = bounds.width < 4 && bounds.height < 4 @@ -155,9 +157,7 @@ export const Draw = new ShapeUtil(() => ({ return } - const path = Utils.getFromCache(simplePathCache, points, () => getSolidStrokePath(shape)) - - return + return }, getBounds(shape: DrawShape): TLBounds { @@ -300,12 +300,11 @@ function getDrawStrokePath(shape: DrawShape, isEditing: boolean) { const options = shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings const stroke = getStroke(shape.points.slice(2), { - size: 1 + styles.strokeWidth * 2, - thinning: 0.7, + size: 1 + styles.strokeWidth * 1.618, + thinning: 0.6, streamline: 0.7, smoothing: 0.5, - end: { taper: +styles.strokeWidth * 50 }, - start: { taper: +styles.strokeWidth * 50 }, + end: { taper: styles.strokeWidth * 10, easing: EASINGS.easeOutQuad }, easing: (t) => Math.sin((t * Math.PI) / 2), ...options, last: !isEditing, diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index a7954fccb..a02e489ef 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -149,13 +149,19 @@ export const Ellipse = new ShapeUtil(() shape.point[1], shape.radius[0], shape.radius[1], - shape.rotation || 0 + 0 ) }) }, getRotatedBounds(shape) { - return Utils.getBoundsFromPoints(Utils.getRotatedCorners(this.getBounds(shape), shape.rotation)) + return Utils.getRotatedEllipseBounds( + shape.point[0], + shape.point[1], + shape.radius[0], + shape.radius[1], + shape.rotation + ) }, getCenter(shape): number[] { @@ -321,25 +327,30 @@ function getEllipsePath(shape: EllipseShape, boundsCenter: number[]) { const ry = radiusY + getRandom() * strokeWidth * 2 const points: number[][] = [] + const start = Math.PI + Math.PI * getRandom() + const extra = Math.abs(getRandom()) - for (let i = 0; i < 32; i++) { - const t = EASINGS.easeInOutSine(i / 32) + const perimeter = Utils.perimeterOfEllipse(rx, ry) + + const count = Math.max(16, perimeter / 10) + + for (let i = 0; i < count; i++) { + const t = EASINGS.easeInOutSine(i / (count + 1)) const rads = start * 2 + Math.PI * (2 + extra) * t - const x = rx * Math.cos(rads) + center[0] - const y = ry * Math.sin(rads) + center[1] - points.push([x, y, t + 0.5 + getRandom() / 2]) + const c = Math.cos(rads) + const s = Math.sin(rads) + points.push([rx * c + center[0], ry * s + center[1], t + 0.5 + getRandom() / 2]) } const stroke = getStroke(points, { - size: 1 + strokeWidth, - thinning: 0.6, - easing: EASINGS.easeInOutSine, - end: { taper: strokeWidth * 20 }, - start: { taper: strokeWidth * 20 }, + size: 1 + strokeWidth * 2, + thinning: 0.5, + end: { taper: perimeter / 8 }, + start: { taper: perimeter / 12 }, streamline: 0, - simulatePressure: false, + simulatePressure: true, }) return Utils.getSvgPathFromStroke(stroke) diff --git a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx index ea0a99ca9..0b310ddd8 100644 --- a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx +++ b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx @@ -201,22 +201,26 @@ function getRectanglePath(shape: RectangleShape) { const lines = Utils.shuffleArr( [ - Vec.pointsBetween(tr, br, py, EASINGS.easeInOutCubic), - Vec.pointsBetween(br, bl, px, EASINGS.easeInOutCubic), - Vec.pointsBetween(bl, tl, py, EASINGS.easeInOutCubic), - Vec.pointsBetween(tl, tr, px, EASINGS.easeInOutCubic), + Vec.pointsBetween(tr, br, py, EASINGS.linear), + Vec.pointsBetween(br, bl, px, EASINGS.linear), + Vec.pointsBetween(bl, tl, py, EASINGS.linear), + Vec.pointsBetween(tl, tr, px, EASINGS.linear), ], rm ) + const edgeDist = rm % 2 === 0 ? px : py + const stroke = getStroke( - [...lines.flat(), ...lines[0], ...lines[1]].slice(4, Math.floor((rm % 2 === 0 ? px : py) / -2)), + [...lines.flat(), ...lines[0], ...lines[1]].slice( + 4, + Math.floor((rm % 2 === 0 ? px : py) / -2) + 2 + ), { - size: 1 + styles.strokeWidth, - thinning: 0.6, - easing: EASINGS.easeOutSine, - end: { cap: true }, - start: { cap: true }, + size: 1 + styles.strokeWidth * 2, + thinning: 0.5, + end: { taper: edgeDist }, + start: { taper: edgeDist }, simulatePressure: false, last: true, } diff --git a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts index 979144c7f..3f5059d70 100644 --- a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts +++ b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts @@ -3,9 +3,6 @@ import { Vec } from '@tldraw/vec' import { Data, DrawShape, Session, TLDrawStatus } from '~types' import { TLDR } from '~state/tldr' -// TODO -// [ ] - Solve flat lines at corners on perfectly straight lines - export class DrawSession implements Session { id = 'draw' status = TLDrawStatus.Creating @@ -30,7 +27,7 @@ export class DrawSession implements Session { // Add a first point but don't update the shape yet. We'll update // when the draw session ends; if the user hasn't added additional // points, this single point will be interpreted as a "dot" shape. - this.points = [] + this.points = [[0, 0, point[2] || 0.5]] } start = () => void null @@ -51,7 +48,7 @@ export class DrawSession implements Session { // Drawing while holding shift will "lock" the pen to either the // x or y axis, depending on the locking direction. if (isLocked) { - if (!this.isLocked && this.points.length > 1) { + if (!this.isLocked && this.points.length > 2) { // If we're locking before knowing what direction we're in, set it // early based on the bigger dimension. if (!this.lockedDirection) { @@ -174,8 +171,6 @@ export class DrawSession implements Session { const { snapshot } = this const pageId = data.appState.currentPageId - this.points.push(this.last) - return { id: 'create_draw', before: { diff --git a/yarn.lock b/yarn.lock index a7d3605ed..cc96ef3b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10730,10 +10730,10 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" -perfect-freehand@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.4.tgz#1c5318164b10a74a4e6664b8519ff68799b8ab52" - integrity sha512-feJV7C2LSiz6czFZuexYzxh8GHzaQ32bU4Vx+Y4xdCZYxnPFFyCoMVbVL3797zmk0v5rzGJkVfMQRUkLb/uZIg== +perfect-freehand@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.6.tgz#feeb25450241f036ec13b43fa84bbb16f8e78e0f" + integrity sha512-wWkFwpgUirsfBDTb9nG6+VnFR0ge119QKU2Nu96vR4MHZMPGfOsQRD7cUk+9CK5P+TUmnrtX8yOEzUrQ6KHJoA== performance-now@^2.1.0: version "2.1.0"