squiggle with signature-pad and smoother lines + blossom #258

Merged
enes merged 12 commits from feat/signature-pad into staging 2024-11-26 09:35:44 +00:00
4 changed files with 84 additions and 60 deletions
Showing only changes of commit 3f081c1632 - Show all commits

View File

@ -86,6 +86,7 @@ const MarkFormField = ({
<form onSubmit={(e) => handleFormSubmit(e)}>
{typeof MarkInputComponent !== 'undefined' && (
<MarkInputComponent
key={selectedMark.id}
value={selectedMarkValue}
placeholder={markLabel}
handler={handleSelectedMarkValueChange}
@ -104,7 +105,7 @@ const MarkFormField = ({
return (
<div className={styles.pagination} key={index}>
<button
className={`${styles.paginationButton} ${isDone(mark) && styles.paginationButtonDone}`}
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
onClick={() => handleCurrentUserMarkChange(mark)}
>
{mark.id}

View File

@ -1,54 +1,67 @@
import { useEffect, useRef } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { MarkInputProps } from '../../types/mark'
import { getOptimizedPathsWithStrokeWidth } 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'
import SignaturePad from 'signature_pad'
import { Config, optimize } from 'svgo'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils/const'
import { BasicPoint } from 'signature_pad/dist/types/point'
export const MarkInputSignature = ({
value,
handler,
userMark
}: MarkInputProps) => {
const location = userMark?.mark.location
const canvasRef = useRef<HTMLCanvasElement>(null)
const signaturePad = useRef<SignaturePad | null>(null)
const update = () => {
if (signaturePad.current && signaturePad.current?.toData()) {
const svg = signaturePad.current.toSVG()
const optimizedSvg = optimize(svg, {
multipass: true, // Optimize multiple times if needed
floatPrecision: 2 // Adjust precision
} as Config).data
const extractedSegments = getOptimizedPathsWithStrokeWidth(optimizedSvg)
handler(JSON.stringify(extractedSegments))
const update = useCallback(() => {
const data = signaturePad.current?.toData()
const reduced = data?.map((pg) => pg.points)
const json = JSON.stringify(reduced)
if (signaturePad.current && !signaturePad.current?.isEmpty()) {
handler(json)
} else {
handler('')
}
}
}, [handler])
useEffect(() => {
const handleEndStroke = () => {
update()
}
if (location && canvasRef.current && signaturePad.current === null) {
signaturePad.current = new SignaturePad(
canvasRef.current,
SIGNATURE_PAD_OPTIONS
)
if (canvasRef.current) {
if (signaturePad.current === null) {
signaturePad.current = new SignaturePad(
canvasRef.current,
SIGNATURE_PAD_OPTIONS
)
}
signaturePad.current.addEventListener('endStroke', handleEndStroke)
}
return () => {
window.removeEventListener('endStroke', handleEndStroke)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [update])
useEffect(() => {
if (signaturePad.current) {
if (value) {
signaturePad.current.fromData(
JSON.parse(value).map((p: BasicPoint[]) => ({
points: p
}))
)
} else {
signaturePad.current?.clear()
}
}
update()
}, [update, value])
const handleReset = () => {
signaturePad.current?.clear()
@ -56,31 +69,29 @@ export const MarkInputSignature = ({
}
return (
<>
<div className={styles.wrapper}>
<div
className={styles.relative}
style={{
width: SIGNATURE_PAD_SIZE.width,
height: SIGNATURE_PAD_SIZE.height
}}
>
<canvas
width={SIGNATURE_PAD_SIZE.width}
height={SIGNATURE_PAD_SIZE.height}
ref={canvasRef}
className={styles.canvas}
></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 className={styles.wrapper}>
<div
className={styles.relative}
style={{
width: SIGNATURE_PAD_SIZE.width,
height: SIGNATURE_PAD_SIZE.height
}}
>
<canvas
width={SIGNATURE_PAD_SIZE.width}
height={SIGNATURE_PAD_SIZE.height}
ref={canvasRef}
className={styles.canvas}
></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

@ -1,4 +1,9 @@
.svg {
.img {
width: 100%;
height: 100%;
object-fit: contain;
overflow: hidden;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}

View File

@ -1,20 +1,27 @@
import { useEffect, useState } from 'react'
import SignaturePad from 'signature_pad'
import { MarkRenderProps } from '../../types/mark'
import { SIGNATURE_PAD_SIZE } from '../../utils'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils'
import { BasicPoint } from 'signature_pad/dist/types/point'
import styles from './Signature.module.scss'
export const MarkRenderSignature = ({ value }: MarkRenderProps) => {
const segments: string[][] = value ? JSON.parse(value) : []
const [dataUrl, setDataUrl] = useState<string | undefined>()
return (
<svg
preserveAspectRatio="xMidYMid meet"
viewBox={`0 0 ${SIGNATURE_PAD_SIZE.width} ${SIGNATURE_PAD_SIZE.height}`}
strokeLinecap="round"
className={styles.svg}
>
{segments.map(([path, width]) => (
<path d={path} strokeWidth={width} stroke="black" fill="none" />
))}
</svg>
)
useEffect(() => {
if (value) {
const canvas = document.createElement('canvas')
canvas.width = SIGNATURE_PAD_SIZE.width
canvas.height = SIGNATURE_PAD_SIZE.height
const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS)
pad.fromData(
JSON.parse(value).map((p: BasicPoint[]) => ({
points: p
}))
)
setDataUrl(canvas.toDataURL('image/webp'))
}
}, [value])
return dataUrl ? <img src={dataUrl} className={styles.img} alt="" /> : null
}