Skip to content

Commit f45310d

Browse files
CP-14191 - Fix signature offset when drawing in landscape on mobile
What Signature and initials pads recalculate canvas size on resize and orientation change so strokes follow the finger in landscape. Why On mobile landscape, the pad kept portrait canvas dimensions after rotation, so ink appeared offset from touch placement and made signing unreliable. How to test 1. Open a Docuseal signature field on a phone (or device emulation). 2. In portrait, tap the top-left of the pad — ink should match the finger. 3. Rotate to landscape, clear if needed, tap the top-left again — ink should still match (no inset down/right). 4. Draw left-to-right in landscape — stroke should track the finger. 5. Optionally repeat with initials draw mode.
1 parent 3e7e6ef commit f45310d

4 files changed

Lines changed: 409 additions & 57 deletions

File tree

app/javascript/draw.js

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
44

55
window.customElements.define('draw-signature', class extends HTMLElement {
66
connectedCallback () {
7-
const scale = 3
8-
9-
this.canvas.width = this.canvas.parentNode.clientWidth * scale
10-
this.canvas.height = this.canvas.parentNode.clientHeight * scale
11-
12-
this.canvas.getContext('2d').scale(scale, scale)
7+
this.scale = 3
138

149
this.pad = new SignaturePad(this.canvas)
1510

@@ -50,6 +45,104 @@ window.customElements.define('draw-signature', class extends HTMLElement {
5045
this.submitButton.disabled = false
5146
})
5247
})
48+
49+
this.setupCanvasSizing()
50+
}
51+
52+
disconnectedCallback () {
53+
this.teardownCanvasSizing()
54+
}
55+
56+
setupCanvasSizing () {
57+
this.resizeCanvas()
58+
59+
this.onResizeCanvas = () => {
60+
if (this.resizeRaf) {
61+
cancelAnimationFrame(this.resizeRaf)
62+
}
63+
64+
this.resizeRaf = requestAnimationFrame(() => {
65+
this.resizeRaf = requestAnimationFrame(() => {
66+
this.resizeCanvas()
67+
})
68+
})
69+
}
70+
71+
window.addEventListener('resize', this.onResizeCanvas)
72+
screen?.orientation?.addEventListener('change', this.onResizeCanvas)
73+
74+
if (typeof ResizeObserver !== 'undefined' && this.canvas?.parentNode) {
75+
this.resizeObserver = new ResizeObserver(this.onResizeCanvas)
76+
this.resizeObserver.observe(this.canvas.parentNode)
77+
}
78+
}
79+
80+
teardownCanvasSizing () {
81+
if (this.resizeRaf) {
82+
cancelAnimationFrame(this.resizeRaf)
83+
this.resizeRaf = null
84+
}
85+
86+
if (this.onResizeCanvas) {
87+
window.removeEventListener('resize', this.onResizeCanvas)
88+
screen?.orientation?.removeEventListener('change', this.onResizeCanvas)
89+
}
90+
91+
this.resizeObserver?.disconnect()
92+
}
93+
94+
resizeCanvas () {
95+
if (!this.canvas?.parentNode) {
96+
return
97+
}
98+
99+
const width = this.canvas.parentNode.clientWidth
100+
const height = this.canvas.parentNode.clientHeight
101+
102+
if (!width || !height) {
103+
return
104+
}
105+
106+
const nextW = width * this.scale
107+
const nextH = height * this.scale
108+
109+
if (this.canvas.width === nextW && this.canvas.height === nextH) {
110+
return
111+
}
112+
113+
const prevCssW = this.canvas.width / this.scale
114+
const prevCssH = this.canvas.height / this.scale
115+
const ratioX = prevCssW > 0 ? width / prevCssW : 1
116+
const ratioY = prevCssH > 0 ? height / prevCssH : 1
117+
118+
let data = []
119+
120+
if (this.pad) {
121+
data = this.pad.toData()
122+
123+
if (data.length && (ratioX !== 1 || ratioY !== 1)) {
124+
data = data.map((group) => ({
125+
...group,
126+
points: group.points.map((point) => ({
127+
...point,
128+
x: point.x * ratioX,
129+
y: point.y * ratioY
130+
}))
131+
}))
132+
}
133+
}
134+
135+
this.canvas.width = nextW
136+
this.canvas.height = nextH
137+
this.canvas.getContext('2d').scale(this.scale, this.scale)
138+
139+
if (this.pad) {
140+
this.pad.clear()
141+
142+
if (data.length) {
143+
this.pad.fromData(data)
144+
}
145+
}
53146
}
54147

55148
clearSignaturePad () {

app/javascript/elements/signature_form.js

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ export default targetable(class extends HTMLElement {
55
static [target.static] = ['canvas', 'input', 'clear', 'button']
66

77
async connectedCallback () {
8-
const scale = 3
9-
10-
this.canvas.width = this.canvas.parentNode.clientWidth * scale
11-
this.canvas.height = this.canvas.parentNode.clientHeight * scale
12-
13-
this.canvas.getContext('2d').scale(scale, scale)
8+
this.scale = 3
149

1510
const { default: SignaturePad } = await import('signature_pad')
1611

@@ -29,6 +24,104 @@ export default targetable(class extends HTMLElement {
2924

3025
this.submit()
3126
})
27+
28+
this.setupCanvasSizing()
29+
}
30+
31+
disconnectedCallback () {
32+
this.teardownCanvasSizing()
33+
}
34+
35+
setupCanvasSizing () {
36+
this.resizeCanvas()
37+
38+
this.onResizeCanvas = () => {
39+
if (this.resizeRaf) {
40+
cancelAnimationFrame(this.resizeRaf)
41+
}
42+
43+
this.resizeRaf = requestAnimationFrame(() => {
44+
this.resizeRaf = requestAnimationFrame(() => {
45+
this.resizeCanvas()
46+
})
47+
})
48+
}
49+
50+
window.addEventListener('resize', this.onResizeCanvas)
51+
screen?.orientation?.addEventListener('change', this.onResizeCanvas)
52+
53+
if (typeof ResizeObserver !== 'undefined' && this.canvas?.parentNode) {
54+
this.resizeObserver = new ResizeObserver(this.onResizeCanvas)
55+
this.resizeObserver.observe(this.canvas.parentNode)
56+
}
57+
}
58+
59+
teardownCanvasSizing () {
60+
if (this.resizeRaf) {
61+
cancelAnimationFrame(this.resizeRaf)
62+
this.resizeRaf = null
63+
}
64+
65+
if (this.onResizeCanvas) {
66+
window.removeEventListener('resize', this.onResizeCanvas)
67+
screen?.orientation?.removeEventListener('change', this.onResizeCanvas)
68+
}
69+
70+
this.resizeObserver?.disconnect()
71+
}
72+
73+
resizeCanvas () {
74+
if (!this.canvas?.parentNode) {
75+
return
76+
}
77+
78+
const width = this.canvas.parentNode.clientWidth
79+
const height = this.canvas.parentNode.clientHeight
80+
81+
if (!width || !height) {
82+
return
83+
}
84+
85+
const nextW = width * this.scale
86+
const nextH = height * this.scale
87+
88+
if (this.canvas.width === nextW && this.canvas.height === nextH) {
89+
return
90+
}
91+
92+
const prevCssW = this.canvas.width / this.scale
93+
const prevCssH = this.canvas.height / this.scale
94+
const ratioX = prevCssW > 0 ? width / prevCssW : 1
95+
const ratioY = prevCssH > 0 ? height / prevCssH : 1
96+
97+
let data = []
98+
99+
if (this.pad) {
100+
data = this.pad.toData()
101+
102+
if (data.length && (ratioX !== 1 || ratioY !== 1)) {
103+
data = data.map((group) => ({
104+
...group,
105+
points: group.points.map((point) => ({
106+
...point,
107+
x: point.x * ratioX,
108+
y: point.y * ratioY
109+
}))
110+
}))
111+
}
112+
}
113+
114+
this.canvas.width = nextW
115+
this.canvas.height = nextH
116+
this.canvas.getContext('2d').scale(this.scale, this.scale)
117+
118+
if (this.pad) {
119+
this.pad.clear()
120+
121+
if (data.length) {
122+
this.pad.fromData(data)
123+
}
124+
}
32125
}
33126

34127
async submit () {

app/javascript/submission_form/initials_step.vue

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,6 @@ export default {
218218
}
219219
},
220220
async mounted () {
221-
this.$nextTick(() => {
222-
if (this.$refs.canvas) {
223-
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
224-
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
225-
226-
this.$refs.canvas.getContext('2d').scale(scale, scale)
227-
}
228-
})
229-
230221
if (this.$refs.canvas) {
231222
this.pad = new SignaturePad(this.$refs.canvas)
232223
@@ -240,26 +231,118 @@ export default {
240231
this.$emit('start')
241232
})
242233
243-
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
244-
entries.forEach(entry => {
245-
if (entry.isIntersecting) {
246-
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
247-
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
234+
this.setupCanvasSizing()
235+
}
236+
},
237+
beforeUnmount () {
238+
this.teardownCanvasSizing()
239+
},
240+
methods: {
241+
canvasHeightForWidth (width) {
242+
return (width / 4.5) * scale
243+
},
244+
setupCanvasSizing () {
245+
this.resizeCanvas()
246+
247+
this.onResizeCanvas = () => {
248+
if (this.resizeRaf) {
249+
cancelAnimationFrame(this.resizeRaf)
250+
}
248251
249-
this.$refs.canvas.getContext('2d').scale(scale, scale)
252+
// Double rAF: orientationchange often fires before layout has settled.
253+
this.resizeRaf = requestAnimationFrame(() => {
254+
this.resizeRaf = requestAnimationFrame(() => {
255+
this.resizeCanvas()
256+
})
257+
})
258+
}
250259
251-
this.intersectionObserver?.disconnect()
260+
window.addEventListener('resize', this.onResizeCanvas)
261+
screen?.orientation?.addEventListener('change', this.onResizeCanvas)
262+
263+
if (typeof ResizeObserver !== 'undefined' && this.$refs.canvas?.parentNode) {
264+
this.resizeObserver = new ResizeObserver(this.onResizeCanvas)
265+
this.resizeObserver.observe(this.$refs.canvas.parentNode)
266+
}
267+
268+
this.intersectionObserver = new IntersectionObserver((entries) => {
269+
entries.forEach(entry => {
270+
if (entry.isIntersecting) {
271+
this.resizeCanvas()
252272
}
253273
})
254274
})
255275
256276
this.intersectionObserver.observe(this.$refs.canvas)
257-
}
258-
},
259-
beforeUnmount () {
260-
this.intersectionObserver?.disconnect()
261-
},
262-
methods: {
277+
},
278+
teardownCanvasSizing () {
279+
if (this.resizeRaf) {
280+
cancelAnimationFrame(this.resizeRaf)
281+
this.resizeRaf = null
282+
}
283+
284+
if (this.onResizeCanvas) {
285+
window.removeEventListener('resize', this.onResizeCanvas)
286+
screen?.orientation?.removeEventListener('change', this.onResizeCanvas)
287+
}
288+
289+
this.resizeObserver?.disconnect()
290+
this.intersectionObserver?.disconnect()
291+
},
292+
resizeCanvas () {
293+
const canvas = this.$refs.canvas
294+
295+
if (!canvas?.parentNode) {
296+
return
297+
}
298+
299+
const width = canvas.parentNode.clientWidth
300+
301+
if (!width) {
302+
return
303+
}
304+
305+
const nextW = width * scale
306+
const nextH = this.canvasHeightForWidth(width)
307+
308+
if (canvas.width === nextW && canvas.height === nextH) {
309+
return
310+
}
311+
312+
const prevCssW = canvas.width / scale
313+
const ratio = prevCssW > 0 ? width / prevCssW : 1
314+
315+
let data = []
316+
317+
if (this.pad) {
318+
data = this.pad.toData()
319+
320+
if (data.length && ratio !== 1) {
321+
data = data.map((group) => ({
322+
...group,
323+
points: group.points.map((point) => ({
324+
...point,
325+
x: point.x * ratio,
326+
y: point.y * ratio
327+
}))
328+
}))
329+
}
330+
}
331+
332+
canvas.width = nextW
333+
canvas.height = nextH
334+
canvas.getContext('2d').scale(scale, scale)
335+
336+
if (this.pad) {
337+
this.pad.clear()
338+
339+
if (data.length) {
340+
this.pad.fromData(data)
341+
} else if (!this.isDrawInitials && this.$refs.textInput?.value) {
342+
this.updateWrittenInitials({ target: this.$refs.textInput })
343+
}
344+
}
345+
},
263346
drawOnCanvas: SignatureStep.methods.drawOnCanvas,
264347
drawImage (event) {
265348
this.remove()

0 commit comments

Comments
 (0)