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)}> <form onSubmit={(e) => handleFormSubmit(e)}>
{typeof MarkInputComponent !== 'undefined' && ( {typeof MarkInputComponent !== 'undefined' && (
<MarkInputComponent <MarkInputComponent
key={selectedMark.id}
value={selectedMarkValue} value={selectedMarkValue}
placeholder={markLabel} placeholder={markLabel}
handler={handleSelectedMarkValueChange} handler={handleSelectedMarkValueChange}
@ -104,7 +105,7 @@ const MarkFormField = ({
return ( return (
<div className={styles.pagination} key={index}> <div className={styles.pagination} key={index}>
<button <button
className={`${styles.paginationButton} ${isDone(mark) && styles.paginationButtonDone}`} className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
onClick={() => handleCurrentUserMarkChange(mark)} onClick={() => handleCurrentUserMarkChange(mark)}
> >
{mark.id} {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 { MarkInputProps } from '../../types/mark'
import { getOptimizedPathsWithStrokeWidth } from '../../utils'
import { faEraser } from '@fortawesome/free-solid-svg-icons' import { faEraser } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import styles from './Signature.module.scss' import styles from './Signature.module.scss'
import { MarkRenderSignature } from '../MarkRender/Signature' import { MarkRenderSignature } from '../MarkRender/Signature'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import { Config, optimize } from 'svgo'
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils/const' import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils/const'
import { BasicPoint } from 'signature_pad/dist/types/point'
export const MarkInputSignature = ({ export const MarkInputSignature = ({
value, value,
handler, handler,
userMark userMark
}: MarkInputProps) => { }: MarkInputProps) => {
const location = userMark?.mark.location
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null)
const signaturePad = useRef<SignaturePad | null>(null) const signaturePad = useRef<SignaturePad | null>(null)
const update = () => { const update = useCallback(() => {
if (signaturePad.current && signaturePad.current?.toData()) { const data = signaturePad.current?.toData()
const svg = signaturePad.current.toSVG() const reduced = data?.map((pg) => pg.points)
const optimizedSvg = optimize(svg, { const json = JSON.stringify(reduced)
multipass: true, // Optimize multiple times if needed
floatPrecision: 2 // Adjust precision if (signaturePad.current && !signaturePad.current?.isEmpty()) {
} as Config).data handler(json)
const extractedSegments = getOptimizedPathsWithStrokeWidth(optimizedSvg)
handler(JSON.stringify(extractedSegments))
} else { } else {
handler('') handler('')
} }
} }, [handler])
useEffect(() => { useEffect(() => {
const handleEndStroke = () => { const handleEndStroke = () => {
update() update()
} }
if (location && canvasRef.current && signaturePad.current === null) { if (canvasRef.current) {
signaturePad.current = new SignaturePad( if (signaturePad.current === null) {
canvasRef.current, signaturePad.current = new SignaturePad(
SIGNATURE_PAD_OPTIONS canvasRef.current,
) SIGNATURE_PAD_OPTIONS
)
}
signaturePad.current.addEventListener('endStroke', handleEndStroke) signaturePad.current.addEventListener('endStroke', handleEndStroke)
} }
return () => { return () => {
window.removeEventListener('endStroke', handleEndStroke) 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 = () => { const handleReset = () => {
signaturePad.current?.clear() signaturePad.current?.clear()
@ -56,31 +69,29 @@ export const MarkInputSignature = ({
} }
return ( return (
<> <div className={styles.wrapper}>
<div className={styles.wrapper}> <div
<div className={styles.relative}
className={styles.relative} style={{
style={{ width: SIGNATURE_PAD_SIZE.width,
width: SIGNATURE_PAD_SIZE.width, height: SIGNATURE_PAD_SIZE.height
height: SIGNATURE_PAD_SIZE.height }}
}} >
> <canvas
<canvas width={SIGNATURE_PAD_SIZE.width}
width={SIGNATURE_PAD_SIZE.width} height={SIGNATURE_PAD_SIZE.height}
height={SIGNATURE_PAD_SIZE.height} ref={canvasRef}
ref={canvasRef} className={styles.canvas}
className={styles.canvas} ></canvas>
></canvas> {typeof userMark?.mark !== 'undefined' && (
{typeof userMark?.mark !== 'undefined' && ( <div className={styles.absolute}>
<div className={styles.absolute}> <MarkRenderSignature value={value} mark={userMark.mark} />
<MarkRenderSignature value={value} mark={userMark.mark} />
</div>
)}
<div className={styles.reset}>
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
</div> </div>
)}
<div className={styles.reset}>
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
</div> </div>
</div> </div>
</> </div>
) )
} }

View File

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