Tldraw/packages/editor/src/lib/editor/shapeutils/DrawShapeUtil/DrawShapeUtil.tsx

314 wiersze
9.0 KiB
TypeScript
Czysty Zwykły widok Historia

2023-04-25 11:01:25 +00:00
/* eslint-disable react-hooks/rules-of-hooks */
import {
Box2d,
getStrokeOutlinePoints,
getStrokePoints,
linesIntersect,
pointInPolygon,
setStrokePointRadii,
toFixed,
2023-04-25 11:01:25 +00:00
Vec2d,
VecLike,
} from '@tldraw/primitives'
import { TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
2023-04-25 11:01:25 +00:00
import { last, rng } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer'
import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../utils/svg'
import { ShapeUtil, TLOnResizeHandler } from '../ShapeUtil'
2023-04-25 11:01:25 +00:00
import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill'
import { TLExportColors } from '../shared/TLExportColors'
import { useForceSolid } from '../shared/useForceSolid'
import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments } from './getPath'
/** @public */
export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
static override type = 'draw'
2023-04-25 11:01:25 +00:00
[3/3] Highlighter styling (#1490) This PR finalises the highlighter shape with new colors, sizing, and perfect freehand options. The colors are based on our existing colour palette, but take advantage of wide-gamut displays to make the highlighter highlightier. I used my [oklch color palette tool to pick the palette](https://alex.dytry.ch/toys/palette/?palette=%7B%22families%22:%5B%22black%22,%22grey%22,%22white%22,%22green%22,%22light-green%22,%22blue%22,%22light-blue%22,%22violet%22,%22light-violet%22,%22red%22,%22light-red%22,%22orange%22,%22yellow%22%5D,%22shades%22:%5B%22light-mode%22,%22dark-mode%22,%22hl-light%22,%22hl-dark%22%5D,%22colors%22:%5B%5B%5B0.2308,0,null%5D,%5B0.9097,0,null%5D,%5B0.2308,0,null%5D,%5B0.2308,0,null%5D%5D,%5B%5B0.7692,0.0145,248.02%5D,%5B0.6778,0.0118,256.72%5D,%5B0.7692,0.0145,248.02%5D,%5B0.7692,0.0145,248.02%5D%5D,%5B%5B1,0,null%5D,%5B0.2308,0,null%5D,%5B1,0,null%5D,%5B1,0,null%5D%5D,%5B%5B0.5851,0.1227,164.1%5D,%5B0.5319,0.0811,162.23%5D,%5B0.8729,0.2083,173.3%5D,%5B0.5851,0.152,173.3%5D%5D,%5B%5B0.7146,0.1835,146.44%5D,%5B0.6384,0.1262,143.36%5D,%5B0.8603,0.2438,140.11%5D,%5B0.6082,0.2286,140.11%5D%5D,%5B%5B0.5566,0.2082,268.35%5D,%5B0.4961,0.1644,270.65%5D,%5B0.7158,0.173,243.85%5D,%5B0.5573,0.178,243.85%5D%5D,%5B%5B0.718,0.1422,246.06%5D,%5B0.6366,0.1055,250.98%5D,%5B0.8615,0.1896,200.03%5D,%5B0.707,0.161,200.03%5D%5D,%5B%5B0.5783,0.2186,319.15%5D,%5B0.5043,0.1647,315.37%5D,%5B0.728,0.2001,307.45%5D,%5B0.5433,0.2927,307.45%5D%5D,%5B%5B0.7904,0.1516,319.77%5D,%5B0.6841,0.1139,315.99%5D,%5B0.812,0.21,327.8%5D,%5B0.5668,0.281,327.8%5D%5D,%5B%5B0.5928,0.2106,26.53%5D,%5B0.5112,0.1455,26.18%5D,%5B0.7326,0.21,20.59%5D,%5B0.554,0.2461,20.59%5D%5D,%5B%5B0.7563,0.146,21.1%5D,%5B0.6561,0.0982,20.86%5D,%5B0.7749,0.178,6.8%5D,%5B0.5565,0.2454,6.8%5D%5D,%5B%5B0.6851,0.1954,44.57%5D,%5B0.5958,0.1366,46.6%5D,%5B0.8207,0.175,68.62%5D,%5B0.6567,0.164,68.61%5D%5D,%5B%5B0.8503,0.1149,68.95%5D,%5B0.7404,0.0813,72.25%5D,%5B0.8939,0.2137,100.36%5D,%5B0.7776,0.186,100.36%5D%5D%5D%7D&selected=3). I'm not sure happy about these colors as they are right now - in particular, i think dark mode looks a bit rubbish and there are a few colors where the highlight and original version are much too similar (light-violet & light-red). Black uses yellow (like note shape) and grey uses light-blue. Exports are forced into srgb color space rather than P3 for maximum compatibility. ![image](https://github.com/tldraw/tldraw/assets/1489520/e3de762b-6ef7-4d17-87db-3e2b71dd8de1) ![image](https://github.com/tldraw/tldraw/assets/1489520/3bd90aa9-bdbc-4a2b-9e56-e3a83a2a877b) The size of a highlighter stroke is now based on the text size which works nicely for making the highlighter play well with text: ![image](https://github.com/tldraw/tldraw/assets/1489520/dd3184fc-decd-4db5-90ce-e9cc75edd3d6) Perfect freehands settings are very similar to the draw tool, but with the thinning turned way down. There is still some, but it's pretty minimal. ### The plan 1. initial highlighter shape/tool #1401 2. sandwich rendering for highlighter shapes #1418 3. shape styling - new colours and sizes, lightweight perfect freehand changes #1490 **>you are here<** ### Change Type - [x] `minor` — New Feature ### Test Plan 1. You can find the highlighter tool in the extended toolbar 2. You can activate the highlighter tool by pressing shift-D 3. Highlighter draws nice and vibrantly when over the page background or frame background 4. Highlighter is less vibrant but still visible when drawn over images / other fills 5. Highlighter size should nicely match the corresponding unscaled text size 6. Exports with highlighter look as expected ### Release Notes Highlighter pen is here! 🎉🎉🎉 --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2023-06-01 15:34:59 +00:00
hideResizeHandles = (shape: TLDrawShape) => getIsDot(shape)
hideRotateHandle = (shape: TLDrawShape) => getIsDot(shape)
hideSelectionBoundsBg = (shape: TLDrawShape) => getIsDot(shape)
hideSelectionBoundsFg = (shape: TLDrawShape) => getIsDot(shape)
2023-04-25 11:01:25 +00:00
override defaultProps(): TLDrawShape['props'] {
return {
segments: [],
color: 'black',
fill: 'none',
dash: 'draw',
size: 'm',
opacity: '1',
isComplete: false,
isClosed: false,
isPen: false,
}
}
isClosed = (shape: TLDrawShape) => shape.props.isClosed
getBounds(shape: TLDrawShape) {
return Box2d.FromPoints(this.outline(shape))
}
getOutline(shape: TLDrawShape) {
return getPointsFromSegments(shape.props.segments)
}
getCenter(shape: TLDrawShape): Vec2d {
return this.bounds(shape).center
}
hitTestPoint(shape: TLDrawShape, point: VecLike): boolean {
const outline = this.outline(shape)
const zoomLevel = this.editor.zoomLevel
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
2023-04-25 11:01:25 +00:00
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
if (shape.props.segments[0].points.some((pt) => Vec2d.Dist(point, pt) < offsetDist * 1.5)) {
return true
}
}
if (this.isClosed(shape)) {
return pointInPolygon(point, outline)
}
if (this.bounds(shape).containsPoint(point)) {
for (let i = 0; i < outline.length; i++) {
const C = outline[i]
const D = outline[(i + 1) % outline.length]
if (Vec2d.DistanceToLineSegment(C, D, point) < offsetDist) return true
}
}
return false
}
hitTestLineSegment(shape: TLDrawShape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape)
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
const zoomLevel = this.editor.zoomLevel
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
2023-04-25 11:01:25 +00:00
if (
shape.props.segments[0].points.some(
(pt) => Vec2d.DistanceToLineSegment(A, B, pt) < offsetDist * 1.5
)
) {
return true
}
}
if (this.isClosed(shape)) {
for (let i = 0; i < outline.length; i++) {
const C = outline[i]
const D = outline[(i + 1) % outline.length]
if (linesIntersect(A, B, C, D)) return true
}
} else {
for (let i = 0; i < outline.length - 1; i++) {
const C = outline[i]
const D = outline[i + 1]
if (linesIntersect(A, B, C, D)) return true
}
}
return false
}
render(shape: TLDrawShape) {
const forceSolid = useForceSolid()
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
2023-04-25 11:01:25 +00:00
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
let sw = strokeWidth
if (
!forceSolid &&
!shape.props.isPen &&
shape.props.dash === 'draw' &&
allPointsFromSegments.length === 1
) {
sw += rng(shape.id)() * (strokeWidth / 6)
}
const options = getFreehandOptions(shape.props, sw, showAsComplete, forceSolid)
2023-04-25 11:01:25 +00:00
const strokePoints = getStrokePoints(allPointsFromSegments, options)
const solidStrokePath =
strokePoints.length > 1
? getSvgPathFromStrokePoints(strokePoints, shape.props.isClosed)
: getDot(allPointsFromSegments[0], sw)
if ((!forceSolid && shape.props.dash === 'draw') || strokePoints.length < 2) {
setStrokePointRadii(strokePoints, options)
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options)
return (
<SVGContainer id={shape.id}>
<ShapeFill
fill={shape.props.isClosed ? shape.props.fill : 'none'}
color={shape.props.color}
d={solidStrokePath}
/>
<path
d={getSvgPathFromStroke(strokeOutlinePoints, true)}
strokeLinecap="round"
fill="currentColor"
/>
</SVGContainer>
)
}
return (
<SVGContainer id={shape.id}>
<ShapeFill
color={shape.props.color}
fill={shape.props.isClosed ? shape.props.fill : 'none'}
d={solidStrokePath}
/>
<path
d={solidStrokePath}
strokeLinecap="round"
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeDasharray={getDrawShapeStrokeDashArray(shape, strokeWidth)}
strokeDashoffset="0"
/>
</SVGContainer>
)
}
indicator(shape: TLDrawShape) {
const forceSolid = useForceSolid()
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
2023-04-25 11:01:25 +00:00
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
let sw = strokeWidth
if (
!forceSolid &&
!shape.props.isPen &&
shape.props.dash === 'draw' &&
allPointsFromSegments.length === 1
) {
sw += rng(shape.id)() * (strokeWidth / 6)
}
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
const options = getFreehandOptions(shape.props, sw, showAsComplete, true)
2023-04-25 11:01:25 +00:00
const strokePoints = getStrokePoints(allPointsFromSegments, options)
const solidStrokePath =
strokePoints.length > 1
? getSvgPathFromStrokePoints(strokePoints, shape.props.isClosed)
: getDot(allPointsFromSegments[0], sw)
return <path d={solidStrokePath} />
}
toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors) {
const { color } = shape.props
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
2023-04-25 11:01:25 +00:00
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
let sw = strokeWidth
if (!shape.props.isPen && shape.props.dash === 'draw' && allPointsFromSegments.length === 1) {
sw += rng(shape.id)() * (strokeWidth / 6)
}
const options = getFreehandOptions(shape.props, sw, showAsComplete, false)
2023-04-25 11:01:25 +00:00
const strokePoints = getStrokePoints(allPointsFromSegments, options)
const solidStrokePath =
strokePoints.length > 1
? getSvgPathFromStrokePoints(strokePoints, shape.props.isClosed)
: getDot(allPointsFromSegments[0], sw)
let foregroundPath: SVGPathElement | undefined
if (shape.props.dash === 'draw' || strokePoints.length < 2) {
setStrokePointRadii(strokePoints, options)
const strokeOutlinePoints = getStrokeOutlinePoints(strokePoints, options)
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
p.setAttribute('d', getSvgPathFromStroke(strokeOutlinePoints, true))
p.setAttribute('fill', colors.fill[color])
p.setAttribute('stroke-linecap', 'round')
foregroundPath = p
} else {
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
p.setAttribute('d', solidStrokePath)
p.setAttribute('stroke', colors.fill[color])
p.setAttribute('fill', 'none')
p.setAttribute('stroke-linecap', 'round')
p.setAttribute('stroke-width', strokeWidth.toString())
p.setAttribute('stroke-dasharray', getDrawShapeStrokeDashArray(shape, strokeWidth))
p.setAttribute('stroke-dashoffset', '0')
foregroundPath = p
}
const fillPath = getShapeFillSvg({
fill: shape.props.isClosed ? shape.props.fill : 'none',
d: solidStrokePath,
color: shape.props.color,
colors,
})
if (fillPath) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
g.appendChild(fillPath)
g.appendChild(foregroundPath)
return g
}
return foregroundPath
}
override onResize: TLOnResizeHandler<TLDrawShape> = (shape, info) => {
2023-04-25 11:01:25 +00:00
const { scaleX, scaleY } = info
const newSegments: TLDrawShapeSegment[] = []
for (const segment of shape.props.segments) {
newSegments.push({
...segment,
points: segment.points.map(({ x, y, z }) => {
return {
x: toFixed(scaleX * x),
y: toFixed(scaleY * y),
2023-04-25 11:01:25 +00:00
z,
}
}),
})
}
return {
props: {
segments: newSegments,
},
}
}
expandSelectionOutlinePx(shape: TLDrawShape): number {
const multiplier = shape.props.dash === 'draw' ? 1.6 : 1
return (this.editor.getStrokeWidth(shape.props.size) * multiplier) / 2
}
2023-04-25 11:01:25 +00:00
}
function getDot(point: VecLike, sw: number) {
const r = (sw + 1) * 0.5
return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${
r * 2
},0`
}
[3/3] Highlighter styling (#1490) This PR finalises the highlighter shape with new colors, sizing, and perfect freehand options. The colors are based on our existing colour palette, but take advantage of wide-gamut displays to make the highlighter highlightier. I used my [oklch color palette tool to pick the palette](https://alex.dytry.ch/toys/palette/?palette=%7B%22families%22:%5B%22black%22,%22grey%22,%22white%22,%22green%22,%22light-green%22,%22blue%22,%22light-blue%22,%22violet%22,%22light-violet%22,%22red%22,%22light-red%22,%22orange%22,%22yellow%22%5D,%22shades%22:%5B%22light-mode%22,%22dark-mode%22,%22hl-light%22,%22hl-dark%22%5D,%22colors%22:%5B%5B%5B0.2308,0,null%5D,%5B0.9097,0,null%5D,%5B0.2308,0,null%5D,%5B0.2308,0,null%5D%5D,%5B%5B0.7692,0.0145,248.02%5D,%5B0.6778,0.0118,256.72%5D,%5B0.7692,0.0145,248.02%5D,%5B0.7692,0.0145,248.02%5D%5D,%5B%5B1,0,null%5D,%5B0.2308,0,null%5D,%5B1,0,null%5D,%5B1,0,null%5D%5D,%5B%5B0.5851,0.1227,164.1%5D,%5B0.5319,0.0811,162.23%5D,%5B0.8729,0.2083,173.3%5D,%5B0.5851,0.152,173.3%5D%5D,%5B%5B0.7146,0.1835,146.44%5D,%5B0.6384,0.1262,143.36%5D,%5B0.8603,0.2438,140.11%5D,%5B0.6082,0.2286,140.11%5D%5D,%5B%5B0.5566,0.2082,268.35%5D,%5B0.4961,0.1644,270.65%5D,%5B0.7158,0.173,243.85%5D,%5B0.5573,0.178,243.85%5D%5D,%5B%5B0.718,0.1422,246.06%5D,%5B0.6366,0.1055,250.98%5D,%5B0.8615,0.1896,200.03%5D,%5B0.707,0.161,200.03%5D%5D,%5B%5B0.5783,0.2186,319.15%5D,%5B0.5043,0.1647,315.37%5D,%5B0.728,0.2001,307.45%5D,%5B0.5433,0.2927,307.45%5D%5D,%5B%5B0.7904,0.1516,319.77%5D,%5B0.6841,0.1139,315.99%5D,%5B0.812,0.21,327.8%5D,%5B0.5668,0.281,327.8%5D%5D,%5B%5B0.5928,0.2106,26.53%5D,%5B0.5112,0.1455,26.18%5D,%5B0.7326,0.21,20.59%5D,%5B0.554,0.2461,20.59%5D%5D,%5B%5B0.7563,0.146,21.1%5D,%5B0.6561,0.0982,20.86%5D,%5B0.7749,0.178,6.8%5D,%5B0.5565,0.2454,6.8%5D%5D,%5B%5B0.6851,0.1954,44.57%5D,%5B0.5958,0.1366,46.6%5D,%5B0.8207,0.175,68.62%5D,%5B0.6567,0.164,68.61%5D%5D,%5B%5B0.8503,0.1149,68.95%5D,%5B0.7404,0.0813,72.25%5D,%5B0.8939,0.2137,100.36%5D,%5B0.7776,0.186,100.36%5D%5D%5D%7D&selected=3). I'm not sure happy about these colors as they are right now - in particular, i think dark mode looks a bit rubbish and there are a few colors where the highlight and original version are much too similar (light-violet & light-red). Black uses yellow (like note shape) and grey uses light-blue. Exports are forced into srgb color space rather than P3 for maximum compatibility. ![image](https://github.com/tldraw/tldraw/assets/1489520/e3de762b-6ef7-4d17-87db-3e2b71dd8de1) ![image](https://github.com/tldraw/tldraw/assets/1489520/3bd90aa9-bdbc-4a2b-9e56-e3a83a2a877b) The size of a highlighter stroke is now based on the text size which works nicely for making the highlighter play well with text: ![image](https://github.com/tldraw/tldraw/assets/1489520/dd3184fc-decd-4db5-90ce-e9cc75edd3d6) Perfect freehands settings are very similar to the draw tool, but with the thinning turned way down. There is still some, but it's pretty minimal. ### The plan 1. initial highlighter shape/tool #1401 2. sandwich rendering for highlighter shapes #1418 3. shape styling - new colours and sizes, lightweight perfect freehand changes #1490 **>you are here<** ### Change Type - [x] `minor` — New Feature ### Test Plan 1. You can find the highlighter tool in the extended toolbar 2. You can activate the highlighter tool by pressing shift-D 3. Highlighter draws nice and vibrantly when over the page background or frame background 4. Highlighter is less vibrant but still visible when drawn over images / other fills 5. Highlighter size should nicely match the corresponding unscaled text size 6. Exports with highlighter look as expected ### Release Notes Highlighter pen is here! 🎉🎉🎉 --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2023-06-01 15:34:59 +00:00
function getIsDot(shape: TLDrawShape) {
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
}