feat: signature squiggle #237
44
src/components/MarkInputs/Signature.module.scss
Normal file
44
src/components/MarkInputs/Signature.module.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
$padding: 5px;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: $padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
outline: 1px solid black;
|
||||||
|
background-color: $body-background-color;
|
||||||
|
cursor: crosshair;
|
||||||
|
|
||||||
|
// Disable panning/zooming when touching canvas element
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: $padding;
|
||||||
|
color: $primary-main;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-dark;
|
||||||
|
}
|
||||||
|
}
|
101
src/components/MarkInputs/Signature.tsx
Normal file
101
src/components/MarkInputs/Signature.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { MarkInputProps } from '../../types/mark'
|
||||||
|
import { getOptimizedPaths, optimizeSVG } from '../../utils'
|
||||||
|
import { faEraser } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import styles from './Signature.module.scss'
|
||||||
|
import { MarkRenderSignature } from '../MarkRender/Signature'
|
||||||
|
|
||||||
|
export const MarkInputSignature = ({
|
||||||
|
value,
|
||||||
|
handler,
|
||||||
|
userMark
|
||||||
|
}: MarkInputProps) => {
|
||||||
|
const location = userMark?.mark.location
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const [drawing, setDrawing] = useState(false)
|
||||||
|
const [paths, setPaths] = useState<string[]>(value ? JSON.parse(value) : [])
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (location && paths) {
|
||||||
|
if (paths.length) {
|
||||||
|
const optimizedSvg = optimizeSVG(location, paths)
|
||||||
|
const extractedPaths = getOptimizedPaths(optimizedSvg)
|
||||||
|
handler(JSON.stringify(extractedPaths))
|
||||||
|
} else {
|
||||||
|
handler('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerDown = (event: React.PointerEvent) => {
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect()
|
||||||
|
const x = event.clientX - rect.left
|
||||||
|
const y = event.clientY - rect.top
|
||||||
|
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
ctx?.beginPath()
|
||||||
|
ctx?.moveTo(x, y)
|
||||||
|
setPaths([...paths, `M ${x} ${y}`])
|
||||||
|
setDrawing(true)
|
||||||
|
}
|
||||||
|
const handlePointerUp = () => {
|
||||||
|
setDrawing(false)
|
||||||
|
update()
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
||||||
|
}
|
||||||
|
const handlePointerMove = (event: React.PointerEvent) => {
|
||||||
|
if (!drawing) return
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
const rect = canvasRef.current?.getBoundingClientRect()
|
||||||
|
const x = event.clientX - rect!.left
|
||||||
|
const y = event.clientY - rect!.top
|
||||||
|
|
||||||
|
ctx?.lineTo(x, y)
|
||||||
|
ctx?.stroke()
|
||||||
|
|
||||||
|
// Collect the path data
|
||||||
|
setPaths((prevPaths) => {
|
||||||
|
const newPaths = [...prevPaths]
|
||||||
|
newPaths[newPaths.length - 1] += ` L ${x} ${y}`
|
||||||
|
return newPaths
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setPaths([])
|
||||||
|
setDrawing(false)
|
||||||
|
update()
|
||||||
|
const ctx = canvasRef.current?.getContext('2d')
|
||||||
|
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<div className={styles.relative}>
|
||||||
|
<canvas
|
||||||
|
height={location?.height}
|
||||||
|
width={location?.width}
|
||||||
|
ref={canvasRef}
|
||||||
|
className={styles.canvas}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerOut={handlePointerUp}
|
||||||
|
></canvas>
|
||||||
|
{typeof userMark?.mark !== 'undefined' && (
|
||||||
|
<div className={styles.absolute}>
|
||||||
|
<MarkRenderSignature value={value} mark={userMark.mark} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.reset}>
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
13
src/components/MarkRender/Signature.tsx
Normal file
13
src/components/MarkRender/Signature.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { MarkRenderProps } from '../../types/mark'
|
||||||
|
|
||||||
|
export const MarkRenderSignature = ({ value, mark }: MarkRenderProps) => {
|
||||||
|
const paths = value ? JSON.parse(value) : []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg viewBox={`0 0 ${mark.location.width} ${mark.location.height}`}>
|
||||||
|
{paths.map((path: string) => (
|
||||||
|
<path d={path} stroke="black" fill="none" />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
16
src/components/getMarkComponents.tsx
Normal file
16
src/components/getMarkComponents.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { MarkType } from '../types/drawing'
|
||||||
|
import { MarkConfigs } from '../types/mark'
|
||||||
|
import { MarkInputSignature } from './MarkInputs/Signature'
|
||||||
|
import { MarkInputText } from './MarkInputs/Text'
|
||||||
|
import { MarkRenderSignature } from './MarkRender/Signature'
|
||||||
|
|
||||||
|
export const MARK_TYPE_CONFIG: MarkConfigs = {
|
||||||
|
[MarkType.TEXT]: {
|
||||||
|
input: MarkInputText,
|
||||||
|
render: ({ value }) => <>{value}</>
|
||||||
|
},
|
||||||
|
[MarkType.SIGNATURE]: {
|
||||||
|
input: MarkInputSignature,
|
||||||
|
render: MarkRenderSignature
|
||||||
|
}
|
||||||
|
}
|
@ -28,3 +28,24 @@ export interface MarkRect {
|
|||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MarkInputProps {
|
||||||
|
value: string
|
||||||
|
handler: (value: string) => void
|
||||||
|
placeholder?: string
|
||||||
|
userMark?: CurrentUserMark
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkRenderProps {
|
||||||
|
value?: string
|
||||||
|
mark: Mark
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkConfig {
|
||||||
|
input: React.FC<MarkInputProps>
|
||||||
|
render?: React.FC<MarkRenderProps>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkConfigs = {
|
||||||
|
[key in MarkType]?: MarkConfig
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user