squiggle with signature-pad and smoother lines + blossom #258
13
package-lock.json
generated
13
package-lock.json
generated
@ -47,6 +47,7 @@
|
||||
"react-singleton-hook": "^4.0.1",
|
||||
"react-toastify": "10.0.4",
|
||||
"redux": "5.0.1",
|
||||
"signature_pad": "^5.0.4",
|
||||
"svgo": "^3.3.2",
|
||||
"tseep": "1.2.1"
|
||||
},
|
||||
@ -4020,9 +4021,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
"version": "6.5.7",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
|
||||
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz",
|
||||
"integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -7866,6 +7867,12 @@
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/signature_pad": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.0.4.tgz",
|
||||
"integrity": "sha512-nngOixbwLAUOuH3QnZwlgwmynQblxmo4iWacKFwfymJfiY+Qt+9icNtcIe/okqXKun4hJ5QTFmHyC7dmv6lf2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
|
@ -57,6 +57,7 @@
|
||||
"react-singleton-hook": "^4.0.1",
|
||||
"react-toastify": "10.0.4",
|
||||
"redux": "5.0.1",
|
||||
"signature_pad": "^5.0.4",
|
||||
"svgo": "^3.3.2",
|
||||
"tseep": "1.2.1"
|
||||
},
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { MarkInputProps } from '../../types/mark'
|
||||
import { getOptimizedPaths, optimizeSVG } from '../../utils'
|
||||
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 } from '../../utils/const'
|
||||
|
||||
export const MarkInputSignature = ({
|
||||
value,
|
||||
@ -12,64 +15,44 @@ export const MarkInputSignature = ({
|
||||
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) : [])
|
||||
const signaturePad = useRef<SignaturePad | null>(null)
|
||||
|
||||
function update() {
|
||||
if (location && paths) {
|
||||
if (paths.length) {
|
||||
const optimizedSvg = optimizeSVG(location, paths)
|
||||
const extractedPaths = getOptimizedPaths(optimizedSvg)
|
||||
handler(JSON.stringify(extractedPaths))
|
||||
} else {
|
||||
handler('')
|
||||
}
|
||||
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))
|
||||
} else {
|
||||
handler('')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: React.PointerEvent) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
const x = event.clientX - rect.left
|
||||
const y = event.clientY - rect.top
|
||||
useEffect(() => {
|
||||
const handleEndStroke = () => {
|
||||
update()
|
||||
}
|
||||
if (location && canvasRef.current && signaturePad.current === null) {
|
||||
signaturePad.current = new SignaturePad(
|
||||
canvasRef.current,
|
||||
SIGNATURE_PAD_OPTIONS
|
||||
)
|
||||
signaturePad.current.addEventListener('endStroke', handleEndStroke)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('endStroke', handleEndStroke)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleReset = () => {
|
||||
setPaths([])
|
||||
setDrawing(false)
|
||||
signaturePad.current?.clear()
|
||||
update()
|
||||
const ctx = canvasRef.current?.getContext('2d')
|
||||
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -81,10 +64,6 @@ export const MarkInputSignature = ({
|
||||
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}>
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { MarkRenderProps } from '../../types/mark'
|
||||
|
||||
export const MarkRenderSignature = ({ value, mark }: MarkRenderProps) => {
|
||||
const paths = value ? JSON.parse(value) : []
|
||||
const segments: string[][] = 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
|
||||
viewBox={`0 0 ${mark.location.width} ${mark.location.height}`}
|
||||
strokeLinecap="round"
|
||||
>
|
||||
{segments.map(([path, width]) => (
|
||||
<path d={path} strokeWidth={width} stroke="black" fill="none" />
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
|
@ -117,7 +117,9 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
// setCurrentUserMarks(updatedCurrentUserMarks)
|
||||
// }
|
||||
|
||||
const handleChange = (value: string) => setSelectedMarkValue(value)
|
||||
const handleChange = (value: string) => {
|
||||
setSelectedMarkValue(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -112,3 +112,8 @@ export const MOST_COMMON_MEDIA_TYPES = new Map([
|
||||
['3g2', 'video/3gpp2'], // 3GPP2 audio/video container
|
||||
['7z', 'application/x-7z-compressed'] // 7-zip archive
|
||||
])
|
||||
|
||||
export const SIGNATURE_PAD_OPTIONS = {
|
||||
minWidth: 0.5,
|
||||
maxWidth: 3
|
||||
} as const
|
||||
|
@ -11,3 +11,4 @@ export * from './string'
|
||||
export * from './url'
|
||||
export * from './utils'
|
||||
export * from './zip'
|
||||
export * from './const'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CurrentUserMark, Mark, MarkLocation } from '../types/mark.ts'
|
||||
import { CurrentUserMark, Mark } from '../types/mark.ts'
|
||||
import { hexToNpub } from './nostr.ts'
|
||||
import { Meta, SignedEventContent } from '../types'
|
||||
import { Event } from 'nostr-tools'
|
||||
@ -24,7 +24,6 @@ import {
|
||||
faStamp,
|
||||
faTableCellsLarge
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Config, optimize } from 'svgo'
|
||||
|
||||
/**
|
||||
* Takes in an array of Marks already filtered by User.
|
||||
@ -266,22 +265,18 @@ export const getToolboxLabelByMarkType = (markType: MarkType) => {
|
||||
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
||||
}
|
||||
|
||||
export const optimizeSVG = (location: MarkLocation, paths: string[]) => {
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="${location.width}" height="${location.height}">${paths.map((path) => `<path d="${path}" stroke="black" fill="none" />`).join('')}</svg>`
|
||||
const optimizedSVG = optimize(svgContent, {
|
||||
multipass: true, // Optimize multiple times if needed
|
||||
floatPrecision: 2 // Adjust precision
|
||||
} as Config)
|
||||
export const getOptimizedPathsWithStrokeWidth = (svgString: string) => {
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(svgString, 'image/svg+xml')
|
||||
const paths = xmlDoc.querySelectorAll('path')
|
||||
const tuples: string[][] = []
|
||||
paths.forEach((path) => {
|
||||
const d = path.getAttribute('d') ?? ''
|
||||
const strokeWidth = path.getAttribute('stroke-width') ?? ''
|
||||
tuples.push([d, strokeWidth])
|
||||
})
|
||||
|
||||
return optimizedSVG.data
|
||||
}
|
||||
|
||||
export const getOptimizedPaths = (svgString: string) => {
|
||||
const regex = / d="([^"]*)"/g
|
||||
const matches = [...svgString.matchAll(regex)]
|
||||
const pathValues = matches.map((match) => match[1])
|
||||
|
||||
return pathValues
|
||||
return tuples
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { MarkType, PdfPage } from '../types/drawing.ts'
|
||||
import { PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib'
|
||||
import { LineCapStyle, PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib'
|
||||
import { Mark } from '../types/mark.ts'
|
||||
import * as PDFJS from 'pdfjs-dist'
|
||||
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'
|
||||
@ -132,7 +132,8 @@ export const addMarks = async (
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
||||
marksPerPage[i]?.forEach((mark) => {
|
||||
for (let j = 0; j < marksPerPage[i].length; j++) {
|
||||
const mark = marksPerPage[i][j]
|
||||
switch (mark.type) {
|
||||
case MarkType.SIGNATURE:
|
||||
drawSignatureText(mark, pages[i])
|
||||
@ -142,7 +143,7 @@ export const addMarks = async (
|
||||
drawMarkText(mark, pages[i], robotoFont)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -263,9 +264,14 @@ const drawSignatureText = (mark: Mark, page: PDFPage) => {
|
||||
const y = height - location.top
|
||||
|
||||
if (hasValue(mark)) {
|
||||
const paths = JSON.parse(mark.value!)
|
||||
paths.forEach((d: string) => {
|
||||
page.drawSvgPath(d, { x, y })
|
||||
const segments: string[][] = JSON.parse(mark.value!)
|
||||
segments.forEach(([d, w]) => {
|
||||
page.drawSvgPath(d, {
|
||||
x,
|
||||
y,
|
||||
borderWidth: parseFloat(w),
|
||||
borderLineCap: LineCapStyle.Round
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user