Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.3.3 #9

Merged
merged 5 commits into from
Mar 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 0.3.4

- Adds the `easing` property. This property accepts an [easing function](https://gist.github.com/gre/1650294) that will apply to all pressure measurements (real or simulated). Defaults to a linear easing (`t => t`).

# 0.3.2

- Superficial changes.
Expand Down
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Perfect Freehand

Perfect freehand is a library for creating freehand paths by [@steveruizok](https://twitter.com/steveruizok).
Perfect freehand is a library for generating freehand strokes.

![Screenshot](https://github.com/steveruizok/perfect-freehand/raw/main/screenshot.png)

Expand All @@ -20,7 +20,7 @@ yarn add perfect-freehand

## Usage

The library exports a default function, `getStroke`, that:
This library's default export is a function that:

- accepts an array of points and an (optional) options object
- returns a stroke as an array of points formatted as `[x, y]`
Expand All @@ -29,7 +29,7 @@ The library exports a default function, `getStroke`, that:
import getStroke from 'perfect-freehand'
```

You may format your input points _either_ as an array or an object as shown below. In both cases, the pressure value is optional.
You may format your input points as array _or_ an object. In both cases, the value for pressure is optional (it will default to `.5`).

```js
getStroke([
Expand All @@ -49,24 +49,28 @@ getStroke([

The options object is optional, as are each of its properties.

| Property | Type | Default | Description |
| ------------------ | ------- | ------- | ----------------------------------------------- |
| `size` | number | 8 | The base size (diameter) of the stroke. |
| `thinning` | number | .5 | The effect of pressure on the stroke's size. |
| `smoothing` | number | .5 | How much to soften the stroke's edges. |
| `streamline` | number | .5 | How much to streamline the stroke. |
| `simulatePressure` | boolean | true | Whether to simulate pressure based on velocity. |
| Property | Type | Default | Description |
| ------------------ | -------- | ------- | ----------------------------------------------------- |
| `size` | number | 8 | The base size (diameter) of the stroke. |
| `thinning` | number | .5 | The effect of pressure on the stroke's size. |
| `smoothing` | number | .5 | How much to soften the stroke's edges. |
| `streamline` | number | .5 | How much to streamline the stroke. |
| `simulatePressure` | boolean | true | Whether to simulate pressure based on velocity. |
| `easing` | function | t => t | An easing function to apply to each point's pressure. |

```js
getStroke(myPoints, {
size: 8,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
easing: t => t * t * t,
simulatePressure: true,
})
```

> **Tip:** To create a stroke with a steady line, set the `thinning` option to `0`.

> **Tip:** To create a stroke that gets thinner with pressure instead of thicker, use a negative number for the `thinning` option.

### Rendering
Expand Down Expand Up @@ -130,6 +134,7 @@ export default function Example() {
thinning: 0.75,
smoothing: 0.5,
streamline: 0.5,
easing: t => t * t * t,
simulatePressure: currentMark.type !== 'pen',
})
: []
Expand Down Expand Up @@ -158,7 +163,10 @@ For advanced usage, the library also exports smaller functions that `getStroke`

#### `getStrokePoints`

Accepts an array of points (formatted either as `[x, y, pressure]` or `{ x: number, y: number, pressure: number}`) and returns a set of streamlined points as `[x, y, pressure, angle, distance, length]`. The path's total length will be the length of the last point in the array.
```js
```

Accepts an array of points (formatted either as `[x, y, pressure]` or `{ x: number, y: number, pressure: number}`) and a streamline value. Returns a set of streamlined points as `[x, y, pressure, angle, distance, lengthAtPoint]`. The path's total length will be the length of the last point in the array.

#### `getStrokeOutlinePoints`

Expand Down Expand Up @@ -199,3 +207,7 @@ function getFlatSvgPathFromStroke(stroke) {
return d.join(' ')
}
```

## Author

- [@steveruizok](https://twitter.com/steveruizok)
288 changes: 288 additions & 0 deletions example/components/bezier.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { useRef, useState, useEffect } from 'react'
import styled from 'styled-components'
import { useStateDesigner } from '@state-designer/react'

const PADDING = 32

export default function Bezier({
label,
value,
onChange,
}: {
label: string
value: number[]
onChange: (v: number[]) => void
}) {
function handleChange(v: number, i: number) {
let next = [...value]
next[i] = v
onChange(next)
}

const state = useStateDesigner({
data: {
value,
draggingKnob: 1,
pointer: {
x: 0,
y: 0,
},
},
on: {
UPDATED_VALUE_FROM_PROPS: 'setValue',
MOVED_POINTER: { secretlyDo: 'updatePointer' },
},
initial: 'idle',
states: {
idle: {
on: {
CLICKED_BACKGROUND: {
to: 'draggingKnob',
},
STARTED_DRAGGING_KNOB: {
do: 'setDraggingKnob',
to: 'draggingKnob',
},
RESET_KNOB: {
do: ['setDraggingKnob', 'resetDraggingKnob', 'shareValue'],
},
},
},
draggingKnob: {
onEnter: ['updateDraggingKnob', 'shareValue'],
on: {
MOVED_POINTER: {
do: ['updateDraggingKnob', 'shareValue'],
},
STOPPED_DRAGGING: {
to: 'idle',
},
},
},
},
actions: {
setDraggingKnob(data, payload: number) {
data.draggingKnob = payload
},
updateDraggingKnob(data) {
const {
value,
draggingKnob,
pointer: { x, y },
} = data
if (draggingKnob === 0 || draggingKnob === 3) {
value[draggingKnob * 2 + 1] = Math.max(Math.min(y, 1), 0)
} else {
value[draggingKnob * 2] = x
value[draggingKnob * 2 + 1] = y
}
},
resetDraggingKnob(data) {
data.value[data.draggingKnob * 2] = 0
data.value[data.draggingKnob * 2 + 1] = 0
},
updatePointer(data, payload: { x: number; y: number }) {
data.pointer = payload
},
setValue(data, payload: number[]) {
data.value = payload
},
shareValue(data) {
onChange([...data.value])
},
},
})

const rContainer = useRef<HTMLDivElement>(null)
const [rect, setRect] = useState([0, 0, 200, 128])

useEffect(() => {
state.send('UPDATED_VALUE_FROM_PROPS', value)

const elm = rContainer.current!
const rect = elm.getBoundingClientRect()
setRect([rect.x, rect.y, rect.width, rect.height])
}, [value])

const [x0, y0, x1, y1, x2, y2, x3, y3] = state.data.value

const w = rect[2] - PADDING * 2
const h = rect[3] - PADDING * 2

const cx0 = PADDING + x0 * w
const cy0 = PADDING + (1 - y0) * h

const cx1 = PADDING + x1 * w
const cy1 = PADDING + (1 - y1) * h

const cx2 = PADDING + x2 * w
const cy2 = PADDING + (1 - y2) * h

const cx3 = PADDING + x3 * w
const cy3 = PADDING + (1 - y3) * h

const path = `M ${cx0},${cy0} C ${cx1},${cy1} ${cx2},${cy2} ${cx3},${cy3}`

return (
<>
<label>{label}</label>
<Container ref={rContainer}>
<svg
width={rect[2]}
height={rect[3]}
viewBox={`0 0 ${rect[2]} ${rect[3]}`}
onPointerMove={({ pageX, pageY }) => {
state.send('MOVED_POINTER', {
x: (pageX - PADDING - rect[0]) / w,
y: 1 - (pageY - PADDING - rect[1]) / h,
})
}}
onPointerUp={() => state.send('STOPPED_DRAGGING')}
>
<rect
x={PADDING}
y={PADDING}
width={w}
height={h}
fill="transparent"
stroke="rgba(144, 144, 144, .2)"
/>
<g opacity=".5">
<text
x={PADDING}
y={'50%'}
textAnchor="left"
transform={`translate(-${PADDING * 1.7}, ${h *
1.25}) rotate(-90)`}
>
Size
</text>
<text x={'50%'} y={rect[3] - PADDING / 2} textAnchor="middle">
Pressure
</text>
</g>
<line x1={cx0} y1={cy0} x2={cx1} y2={cy1} />
<line x1={cx2} y1={cy2} x2={cx3} y2={cy3} />
<path opacity={0.5} d={path} strokeWidth={2} />
<circle
cx={cx0}
cy={cy0}
r={8}
onPointerDown={() => state.send('STARTED_DRAGGING_KNOB', 0)}
onDoubleClick={() => state.send('RESET_KNOB', 0)}
/>
<circle
cx={cx1}
cy={cy1}
r={8}
onPointerDown={() => state.send('STARTED_DRAGGING_KNOB', 1)}
onDoubleClick={() => state.send('RESET_KNOB', 1)}
/>
<circle
cx={cx2}
cy={cy2}
r={8}
onPointerDown={() => state.send('STARTED_DRAGGING_KNOB', 2)}
onDoubleClick={() => state.send('RESET_KNOB', 2)}
/>
<circle
cx={cx3}
cy={cy3}
r={8}
onPointerDown={() => state.send('STARTED_DRAGGING_KNOB', 3)}
onDoubleClick={() => state.send('RESET_KNOB', 3)}
/>
</svg>
</Container>
<Values>
<input
type="number"
min={0}
max={1}
step={0.01}
value={y0}
onChange={e => handleChange(Number(e.currentTarget.value), 1)}
/>
<input
type="number"
min={0}
max={1}
step={0.01}
value={x1}
onChange={e => handleChange(Number(e.currentTarget.value), 2)}
/>
<input
type="number"
min={0}
max={1}
step={0.01}
value={y1}
onChange={e => handleChange(Number(e.currentTarget.value), 3)}
/>
<input
type="number"
min={0}
max={1}
step={0.01}
value={x2}
onChange={e => handleChange(Number(e.currentTarget.value), 4)}
/>
<input
type="number"
min={0}
max={1}
step={0.01}
value={y2}
onChange={e => handleChange(Number(e.currentTarget.value), 5)}
/>
<input
type="number"
min={0}
max={1}
step={0.01}
value={y3}
onChange={e => handleChange(Number(e.currentTarget.value), 7)}
/>
</Values>
</>
)
}

const Container = styled.div`
height: 160px;
position: relative;
border-radius: 4px;
overflow: hidden;
border: 1px solid rgba(144, 144, 144, 0.5);
grid-column: span 2;

svg {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--color-background);
user-select: none;
}

text,
circle {
fill: var(--color-text);
}

line,
path {
stroke-width: 2px;
stroke: var(--color-text);
fill: transparent;
}
`

const Values = styled.div`
grid-column: 2 / span 2;
display: grid;
grid-template-columns: repeat(6, 1fr);

input {
width: 100%;
}
`
Loading