diff --git a/app/javascript/draw.js b/app/javascript/draw.js index a33d7ea80..5db282416 100644 --- a/app/javascript/draw.js +++ b/app/javascript/draw.js @@ -1,5 +1,6 @@ import SignaturePad from 'signature_pad' import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas' +import { setupCanvasSizing } from './submission_form/canvas_sizing' import { isValidSignatureCanvas } from './submission_form/validate_signature' window.customElements.define('draw-signature', class extends HTMLElement { @@ -46,103 +47,20 @@ window.customElements.define('draw-signature', class extends HTMLElement { }) }) - this.setupCanvasSizing() + this.canvasSizing = setupCanvasSizing({ + getCanvas: () => this.canvas, + getPad: () => this.pad, + scale: this.scale, + nextSize: (canvas) => ({ + width: canvas.parentNode.clientWidth * this.scale, + height: canvas.parentNode.clientHeight * this.scale + }), + perAxisStrokeRatios: true + }) } disconnectedCallback () { - this.teardownCanvasSizing() - } - - setupCanvasSizing () { - this.resizeCanvas() - - this.onResizeCanvas = () => { - if (this.resizeRaf) { - cancelAnimationFrame(this.resizeRaf) - } - - this.resizeRaf = requestAnimationFrame(() => { - this.resizeRaf = requestAnimationFrame(() => { - this.resizeCanvas() - }) - }) - } - - window.addEventListener('resize', this.onResizeCanvas) - screen?.orientation?.addEventListener('change', this.onResizeCanvas) - - if (typeof ResizeObserver !== 'undefined' && this.canvas?.parentNode) { - this.resizeObserver = new ResizeObserver(this.onResizeCanvas) - this.resizeObserver.observe(this.canvas.parentNode) - } - } - - teardownCanvasSizing () { - if (this.resizeRaf) { - cancelAnimationFrame(this.resizeRaf) - this.resizeRaf = null - } - - if (this.onResizeCanvas) { - window.removeEventListener('resize', this.onResizeCanvas) - screen?.orientation?.removeEventListener('change', this.onResizeCanvas) - } - - this.resizeObserver?.disconnect() - } - - resizeCanvas () { - if (!this.canvas?.parentNode) { - return - } - - const width = this.canvas.parentNode.clientWidth - const height = this.canvas.parentNode.clientHeight - - if (!width || !height) { - return - } - - const nextW = width * this.scale - const nextH = height * this.scale - - if (this.canvas.width === nextW && this.canvas.height === nextH) { - return - } - - const prevCssW = this.canvas.width / this.scale - const prevCssH = this.canvas.height / this.scale - const ratioX = prevCssW > 0 ? width / prevCssW : 1 - const ratioY = prevCssH > 0 ? height / prevCssH : 1 - - let data = [] - - if (this.pad) { - data = this.pad.toData() - - if (data.length && (ratioX !== 1 || ratioY !== 1)) { - data = data.map((group) => ({ - ...group, - points: group.points.map((point) => ({ - ...point, - x: point.x * ratioX, - y: point.y * ratioY - })) - })) - } - } - - this.canvas.width = nextW - this.canvas.height = nextH - this.canvas.getContext('2d').scale(this.scale, this.scale) - - if (this.pad) { - this.pad.clear() - - if (data.length) { - this.pad.fromData(data) - } - } + this.canvasSizing?.teardown() } clearSignaturePad () { diff --git a/app/javascript/elements/signature_form.js b/app/javascript/elements/signature_form.js index f0efad856..01a3b1569 100644 --- a/app/javascript/elements/signature_form.js +++ b/app/javascript/elements/signature_form.js @@ -1,5 +1,6 @@ import { target, targetable } from '@github/catalyst/lib/targetable' import { cropCanvasAndExportToPNG } from '../submission_form/crop_canvas' +import { setupCanvasSizing } from '../submission_form/canvas_sizing' export default targetable(class extends HTMLElement { static [target.static] = ['canvas', 'input', 'clear', 'button'] @@ -25,103 +26,20 @@ export default targetable(class extends HTMLElement { this.submit() }) - this.setupCanvasSizing() + this.canvasSizing = setupCanvasSizing({ + getCanvas: () => this.canvas, + getPad: () => this.pad, + scale: this.scale, + nextSize: (canvas) => ({ + width: canvas.parentNode.clientWidth * this.scale, + height: canvas.parentNode.clientHeight * this.scale + }), + perAxisStrokeRatios: true + }) } disconnectedCallback () { - this.teardownCanvasSizing() - } - - setupCanvasSizing () { - this.resizeCanvas() - - this.onResizeCanvas = () => { - if (this.resizeRaf) { - cancelAnimationFrame(this.resizeRaf) - } - - this.resizeRaf = requestAnimationFrame(() => { - this.resizeRaf = requestAnimationFrame(() => { - this.resizeCanvas() - }) - }) - } - - window.addEventListener('resize', this.onResizeCanvas) - screen?.orientation?.addEventListener('change', this.onResizeCanvas) - - if (typeof ResizeObserver !== 'undefined' && this.canvas?.parentNode) { - this.resizeObserver = new ResizeObserver(this.onResizeCanvas) - this.resizeObserver.observe(this.canvas.parentNode) - } - } - - teardownCanvasSizing () { - if (this.resizeRaf) { - cancelAnimationFrame(this.resizeRaf) - this.resizeRaf = null - } - - if (this.onResizeCanvas) { - window.removeEventListener('resize', this.onResizeCanvas) - screen?.orientation?.removeEventListener('change', this.onResizeCanvas) - } - - this.resizeObserver?.disconnect() - } - - resizeCanvas () { - if (!this.canvas?.parentNode) { - return - } - - const width = this.canvas.parentNode.clientWidth - const height = this.canvas.parentNode.clientHeight - - if (!width || !height) { - return - } - - const nextW = width * this.scale - const nextH = height * this.scale - - if (this.canvas.width === nextW && this.canvas.height === nextH) { - return - } - - const prevCssW = this.canvas.width / this.scale - const prevCssH = this.canvas.height / this.scale - const ratioX = prevCssW > 0 ? width / prevCssW : 1 - const ratioY = prevCssH > 0 ? height / prevCssH : 1 - - let data = [] - - if (this.pad) { - data = this.pad.toData() - - if (data.length && (ratioX !== 1 || ratioY !== 1)) { - data = data.map((group) => ({ - ...group, - points: group.points.map((point) => ({ - ...point, - x: point.x * ratioX, - y: point.y * ratioY - })) - })) - } - } - - this.canvas.width = nextW - this.canvas.height = nextH - this.canvas.getContext('2d').scale(this.scale, this.scale) - - if (this.pad) { - this.pad.clear() - - if (data.length) { - this.pad.fromData(data) - } - } + this.canvasSizing?.teardown() } async submit () { diff --git a/app/javascript/submission_form/canvas_sizing.js b/app/javascript/submission_form/canvas_sizing.js new file mode 100644 index 000000000..01c136e1c --- /dev/null +++ b/app/javascript/submission_form/canvas_sizing.js @@ -0,0 +1,115 @@ +function setupCanvasSizing (options) { + let resizeRaf = null + let resizeObserver = null + let intersectionObserver = null + + function resizeCanvas () { + const canvas = options.getCanvas() + + if (!canvas?.parentNode) { + return + } + + const { width: nextW, height: nextH } = options.nextSize(canvas) + + if (!nextW || !nextH) { + return + } + + if (canvas.width === nextW && canvas.height === nextH) { + return + } + + const ratioX = canvas.width > 0 ? nextW / canvas.width : 1 + const ratioY = options.perAxisStrokeRatios + ? (canvas.height > 0 ? nextH / canvas.height : 1) + : ratioX + + let data = [] + + const pad = options.getPad() + + if (pad) { + data = pad.toData() + + if (data.length && (ratioX !== 1 || ratioY !== 1)) { + data = data.map((group) => ({ + ...group, + points: group.points.map((point) => ({ + ...point, + x: point.x * ratioX, + y: point.y * ratioY + })) + })) + } + } + + canvas.width = nextW + canvas.height = nextH + canvas.getContext('2d').scale(options.scale, options.scale) + + if (pad) { + pad.clear() + + if (data.length) { + pad.fromData(data) + } else { + options.onNoStrokes?.(canvas) + } + } + } + + resizeCanvas() + + const onResizeCanvas = () => { + if (resizeRaf) { + cancelAnimationFrame(resizeRaf) + } + + // Double rAF: orientationchange often fires before layout has settled. + resizeRaf = requestAnimationFrame(() => { + resizeRaf = requestAnimationFrame(() => { + resizeCanvas() + }) + }) + } + + window.addEventListener('resize', onResizeCanvas) + screen?.orientation?.addEventListener('change', onResizeCanvas) + + const canvas = options.getCanvas() + + if (typeof ResizeObserver !== 'undefined' && canvas?.parentNode) { + resizeObserver = new ResizeObserver(onResizeCanvas) + resizeObserver.observe(canvas.parentNode) + } + + if (options.watchVisibility) { + intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + resizeCanvas() + } + }) + }) + + intersectionObserver.observe(canvas) + } + + function teardown () { + if (resizeRaf) { + cancelAnimationFrame(resizeRaf) + resizeRaf = null + } + + window.removeEventListener('resize', onResizeCanvas) + screen?.orientation?.removeEventListener('change', onResizeCanvas) + + resizeObserver?.disconnect() + intersectionObserver?.disconnect() + } + + return { resizeCanvas, teardown } +} + +export { setupCanvasSizing } diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index 5190329fe..0d99ea61c 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -144,6 +144,7 @@