squiggle with signature-pad and smoother lines + blossom #258
@ -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}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user