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