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 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])
}

View file

@ -1,7 +1,7 @@
import * as React from 'react'
import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core'
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 { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
@ -173,70 +173,93 @@ function getRectangleDrawPoints(shape: RectangleShape) {
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(() => [
getRandom() * baseOffset,
getRandom() * baseOffset,
])
const sw = strokeWidth
const w = Math.max(0, shape.size[0] - sw / 2)
const h = Math.max(0, shape.size[1] - sw / 2)
// Random corner offsets
const offsets = Array.from(Array(4)).map(() => {
return [getRandom() * sw * 0.75, getRandom() * sw * 0.75]
})
// Corners
const tl = Vec.add([sw / 2, sw / 2], offsets[0])
const tr = Vec.add([w, sw / 2], offsets[1])
const br = Vec.add([w, h], offsets[2])
const bl = Vec.add([sw / 2, h], offsets[3])
const tr = Vec.add([w - sw / 2, sw / 2], offsets[1])
const br = Vec.add([w - sw / 2, h - sw / 2], offsets[2])
const bl = Vec.add([sw / 2, h - sw / 2], offsets[3])
const px = Math.max(8, Math.floor(w / 20))
const py = Math.max(8, Math.floor(h / 20))
const rm = Math.floor(5 + getRandom() * 4)
// Which side to start drawing first
const rm = Math.round(Math.abs(getRandom() * 2 * 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(br, bl, px, EASINGS.linear),
Vec.pointsBetween(bl, tl, py, EASINGS.linear),
Vec.pointsBetween(tl, tr, px, EASINGS.linear),
Vec.pointsBetween(Vec.add(tl, [rx, 0]), Vec.sub(tr, [rx, 0]), px),
Vec.pointsBetween(Vec.add(tr, [0, ry]), Vec.sub(br, [0, ry]), py),
Vec.pointsBetween(Vec.sub(br, [rx, 0]), Vec.add(bl, [rx, 0]), px),
Vec.pointsBetween(Vec.sub(bl, [0, ry]), Vec.add(tl, [0, ry]), py),
],
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 {
points: [...lines.flat(), ...lines[0], ...lines[1]].slice(
4,
Math.floor((rm % 2 === 0 ? px : py) / -2) + 2
),
edgeDistance: rm % 2 === 0 ? px : py,
points,
}
}
function getRectanglePath(shape: RectangleShape) {
const { points, edgeDistance } = getRectangleDrawPoints(shape)
function getDrawStrokeInfo(shape: RectangleShape) {
const { points } = getRectangleDrawPoints(shape)
const { strokeWidth } = getShapeStyle(shape.style)
const stroke = getStroke(points, {
size: 1 + strokeWidth,
thinning: 0.6,
end: { taper: edgeDistance },
start: { taper: edgeDistance },
const options = {
size: strokeWidth,
thinning: 0.65,
streamline: 0.3,
smoothing: 1,
simulatePressure: false,
last: true,
})
}
return { points, options }
}
function getRectanglePath(shape: RectangleShape) {
const { points, options } = getDrawStrokeInfo(shape)
const stroke = getStroke(points, options)
return Utils.getSvgPathFromStroke(stroke)
}
function getRectangleIndicatorPathData(shape: RectangleShape) {
const { points } = getRectangleDrawPoints(shape)
const { points, options } = getDrawStrokeInfo(shape)
const strokePoints = getStrokePoints(points, options)
return Utils.getSvgPathFromStroke(
getStrokePoints(points).map((pt) => pt.point.slice(0, 2)),
strokePoints.map((pt) => pt.point.slice(0, 2)),
false
)
}

View file

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