feat: signature squiggle #237

Merged
b merged 3 commits from feat/signature into staging 2024-10-28 16:23:29 +00:00
5 changed files with 195 additions and 0 deletions
Showing only changes of commit dfa2832e8d - Show all commits

View 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;
}
}

View 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>
</>
)
}

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

View 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
}
}

View File

@ -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
}