Skip to content

Commit

Permalink
Merge pull request #9 from steveruizok/next
Browse files Browse the repository at this point in the history
0.3.3
  • Loading branch information
steveruizok authored Mar 6, 2021
2 parents d2adb83 + 2d10934 commit 5361285
Show file tree
Hide file tree
Showing 12 changed files with 580 additions and 173 deletions.
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

0 comments on commit 5361285

Please sign in to comment.