Improve rectangle rendering (#163)

This commit is contained in:
Steve Ruiz 2021-10-17 09:09:01 +01:00 committed by GitHub
parent db62005251
commit 01c824bbbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 68 additions and 48 deletions

View file

@ -1585,7 +1585,7 @@ left past the initial left edge) then swap points on that axis.
* @param arr * @param arr
* @param offset * @param offset
*/ */
static shuffleArr<T>(arr: T[], offset: number): T[] { static rotateArray<T>(arr: T[], offset: number): T[] {
return arr.map((_, i) => arr[(i + offset) % arr.length]) return arr.map((_, i) => arr[(i + offset) % arr.length])
} }

View file

@ -1,7 +1,7 @@
import * as React from 'react' import * as React from 'react'
import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core' import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import getStroke, { getStrokePoints } from 'perfect-freehand' import getStroke, { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles' import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types' import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared' import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
@ -173,70 +173,93 @@ function getRectangleDrawPoints(shape: RectangleShape) {
const getRandom = Utils.rng(shape.id) const getRandom = Utils.rng(shape.id)
const strokeWidth = +styles.strokeWidth const sw = styles.strokeWidth
const baseOffset = strokeWidth / 2 // Dimensions
const w = Math.max(0, shape.size[0])
const h = Math.max(0, shape.size[1])
const offsets = Array.from(Array(4)).map(() => [ // Random corner offsets
getRandom() * baseOffset, const offsets = Array.from(Array(4)).map(() => {
getRandom() * baseOffset, return [getRandom() * sw * 0.75, getRandom() * sw * 0.75]
]) })
const sw = strokeWidth
const w = Math.max(0, shape.size[0] - sw / 2)
const h = Math.max(0, shape.size[1] - sw / 2)
// Corners
const tl = Vec.add([sw / 2, sw / 2], offsets[0]) const tl = Vec.add([sw / 2, sw / 2], offsets[0])
const tr = Vec.add([w, sw / 2], offsets[1]) const tr = Vec.add([w - sw / 2, sw / 2], offsets[1])
const br = Vec.add([w, h], offsets[2]) const br = Vec.add([w - sw / 2, h - sw / 2], offsets[2])
const bl = Vec.add([sw / 2, h], offsets[3]) const bl = Vec.add([sw / 2, h - sw / 2], offsets[3])
const px = Math.max(8, Math.floor(w / 20)) // Which side to start drawing first
const py = Math.max(8, Math.floor(h / 20)) const rm = Math.round(Math.abs(getRandom() * 2 * 4))
const rm = Math.floor(5 + getRandom() * 4)
const lines = Utils.shuffleArr( // Corner radii
const rx = Math.min(w / 2, sw * 2)
const ry = Math.min(h / 2, sw * 2)
// Number of points per side
const px = Math.max(8, Math.floor(w / 16))
const py = Math.max(8, Math.floor(h / 16))
// Inset each line by the corner radii and let the freehand algo
// interpolate points for the corners.
const lines = Utils.rotateArray(
[ [
Vec.pointsBetween(tr, br, py, EASINGS.linear), Vec.pointsBetween(Vec.add(tl, [rx, 0]), Vec.sub(tr, [rx, 0]), px),
Vec.pointsBetween(br, bl, px, EASINGS.linear), Vec.pointsBetween(Vec.add(tr, [0, ry]), Vec.sub(br, [0, ry]), py),
Vec.pointsBetween(bl, tl, py, EASINGS.linear), Vec.pointsBetween(Vec.sub(br, [rx, 0]), Vec.add(bl, [rx, 0]), px),
Vec.pointsBetween(tl, tr, px, EASINGS.linear), Vec.pointsBetween(Vec.sub(bl, [0, ry]), Vec.add(tl, [0, ry]), py),
], ],
rm rm
) )
// For the final points, include the first half of the first line again,
// so that the line wraps around and avoids ending on a sharp corner.
// This has a bit of finesse and magic—if you change the points between
// function, then you'll likely need to change this one too.
const points = [...lines.flat(), ...lines[0]].slice(
5,
Math.floor((rm % 2 === 0 ? px : py) / -2) + 3
)
return { return {
points: [...lines.flat(), ...lines[0], ...lines[1]].slice( points,
4,
Math.floor((rm % 2 === 0 ? px : py) / -2) + 2
),
edgeDistance: rm % 2 === 0 ? px : py,
} }
} }
function getRectanglePath(shape: RectangleShape) { function getDrawStrokeInfo(shape: RectangleShape) {
const { points, edgeDistance } = getRectangleDrawPoints(shape) const { points } = getRectangleDrawPoints(shape)
const { strokeWidth } = getShapeStyle(shape.style) const { strokeWidth } = getShapeStyle(shape.style)
const stroke = getStroke(points, { const options = {
size: 1 + strokeWidth, size: strokeWidth,
thinning: 0.6, thinning: 0.65,
end: { taper: edgeDistance }, streamline: 0.3,
start: { taper: edgeDistance }, smoothing: 1,
simulatePressure: false, simulatePressure: false,
last: true, last: true,
}) }
return { points, options }
}
function getRectanglePath(shape: RectangleShape) {
const { points, options } = getDrawStrokeInfo(shape)
const stroke = getStroke(points, options)
return Utils.getSvgPathFromStroke(stroke) return Utils.getSvgPathFromStroke(stroke)
} }
function getRectangleIndicatorPathData(shape: RectangleShape) { function getRectangleIndicatorPathData(shape: RectangleShape) {
const { points } = getRectangleDrawPoints(shape) const { points, options } = getDrawStrokeInfo(shape)
const strokePoints = getStrokePoints(points, options)
return Utils.getSvgPathFromStroke( return Utils.getSvgPathFromStroke(
getStrokePoints(points).map((pt) => pt.point.slice(0, 2)), strokePoints.map((pt) => pt.point.slice(0, 2)),
false false
) )
} }

View file

@ -512,15 +512,12 @@ export class Vec {
* @param steps The number of points to return. * @param steps The number of points to return.
* @param ease An easing function to apply to the simulated pressure. * @param ease An easing function to apply to the simulated pressure.
*/ */
static pointsBetween = ( static pointsBetween = (A: number[], B: number[], steps = 6): number[][] => {
A: number[], return Array.from(Array(steps)).map((_, i) => {
B: number[], const t = i / (steps - 1)
steps = 6, const k = Math.min(1, 0.5 + Math.abs(0.5 - t))
ease = (t: number) => t * t * t * t return [...Vec.lrp(A, B, t), k]
): number[][] => { })
return Array.from(Array(steps)).map((_, i) =>
Vec.round([...Vec.lrp(A, B, i / steps), (1 - ease(i / steps)) / 2])
)
} }
} }