skip to Main Content

How to make a signature pad canvas in signature-pad.js work responsively?

My challenge is the following:

  1. I either get the cursor positioning while drawing correctly but lose the input on resize

    // OR //

  2. I save the input while resizing but then the canvas is not calculated correctly and it does not track the cursor position anymore

Right now I am sure the issue comes from this line in my code:
signaturePad.fromData(signaturePad.toData());

Commenting this out or not decides the outcome of my two observed behaviors.

Does someone have an easy solution to this? I am getting crazy about it already. I also created a JS Fiddle for you to see what I have so far:
https://jsfiddle.net/xetrzi9/hort1ubj/3/

Additional info:
When scrolling on the phone it also resets the input for some reason which is really weird.

function initializeSignaturePad() {
  const wrapper = document.querySelector('.form_signature-wrapper');
  const canvas = wrapper.querySelector('#signature-pad');
  const clearButton = wrapper.querySelector('[data-action=clear]');

  let signaturePad;

  function resizeCanvas() {
    const ratio = Math.max(window.devicePixelRatio || 1, 1);
    const rect = wrapper.getBoundingClientRect();

    canvas.width = rect.width * ratio;
    canvas.height = rect.height * ratio;
    canvas.getContext('2d').scale(ratio, ratio);

    // signaturePad.fromData(signaturePad.toData());
  }

  function initPad() {
    signaturePad = new SignaturePad(canvas, {
      penColor: 'rgb(255, 255, 255)',
      minWidth: 0.5,
      maxWidth: 2.5,
      throttle: 0,
      minDistance: 0,
      velocityFilterWeight: 0.4,
    });

    updateMousePosition();
  }

  function updateMousePosition() {
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;

    function getPointFromEvent(event) {
      let x, y;
      if (event.touches && event.touches.length > 0) {
        const touch = event.touches[0];
        x = touch.clientX;
        y = touch.clientY;
      } else {
        x = event.clientX;
        y = event.clientY;
      }
      x = (x - rect.left) * scaleX;
      y = (y - rect.top) * scaleY;
      return new SignaturePad.Point(x, y);
    }

    const originalOnMouseDown = signaturePad._handleMouseDown;
    signaturePad._handleMouseDown = function(event) {
      const point = getPointFromEvent(event);
      originalOnMouseDown.call(signaturePad, event, point);
    };

    const originalOnMouseMove = signaturePad._handleMouseMove;
    signaturePad._handleMouseMove = function(event) {
      const point = getPointFromEvent(event);
      originalOnMouseMove.call(signaturePad, event, point);
    };

    const originalOnTouchStart = signaturePad._handleTouchStart;
    signaturePad._handleTouchStart = function(event) {
      const point = getPointFromEvent(event);
      originalOnTouchStart.call(signaturePad, event, point);
    };

    const originalOnTouchMove = signaturePad._handleTouchMove;
    signaturePad._handleTouchMove = function(event) {
      const point = getPointFromEvent(event);
      originalOnTouchMove.call(signaturePad, event, point);
    };
  }

  initPad();
  resizeCanvas();

  function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }

  window.addEventListener(
    'resize',
    debounce(() => {
      resizeCanvas();
    }, 250)
  );

  clearButton.addEventListener('click', function(event) {
    event.preventDefault();
    event.stopPropagation();
    signaturePad.clear();
  });

  return signaturePad;
}

document.addEventListener('DOMContentLoaded', function() {
  initializeSignaturePad();
});
html,
body {
  margin: 0;
  padding: 0;
}

body {
  background: #0c0c0c;
  font-family: sans-serif;
}

.form_field-wrapper {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem 0;
}

.form_label {
  color: #fff;
  margin-bottom: 0.5rem;
}

.form_signature-wrapper {
  width: 100%;
  height: auto;
  aspect-ratio: 5/3;
}

.form_signature-canvas {
  height: 100%;
  width: 100%;
  display: block;
  box-sizing: border-box;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.25rem 0.25rem 0 0;
}

.form_signature-wrapper {
  position: relative;
}

.form_signature-wrapper button {
  appearance: none;
  outline: none;
  border: none;
  box-shadow: none;
  font-weight: 600;
  color: #fff;
  background: rgba(255, 255, 255, 0.1);
  padding: 0.5rem;
  border-radius: 0.25rem;
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  transition: 100ms ease-in-out background;
}

.form_signature-wrapper button:hover {
  background: rgba(255, 255, 255, 0.2);
  cursor: pointer;
}

.form_info-wrapper {
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 0 0 0.25rem 0.25rem;
  font-size: 0.875rem;
}

.form_info-icon {
  height: 1rem;
  width: 1rem;
}

.form_info-icon svg {
  display: flex;
  height: auto;
  width: 100%;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/signature_pad.umd.min.js"></script>

<div class="form_field-wrapper">
  <div class="form_label">Signature</div>
  <div class="form_signature-wrapper">
    <canvas id="signature-pad" class="form_signature-canvas" width="" height="" style="touch-action: none"></canvas
      ><button
               data-action="clear"
               type="button"
               class="form_signature-btn is-clear">
    Delete
    </button>
  </div>
  <div class="form_info-wrapper is-signature form_label">
    <div class="form_info-icon">
      <svg
           fill="none"
           xmlns="http://www.w3.org/2000/svg"
           viewBox="0 0 14 14">
        <path
              d="M7 14a7 7 0 1 1 7-7 7.007 7.007 0 0 1-7 7ZM7 1.167A5.833 5.833 0 1 0 12.833 7 5.84 5.84 0 0 0 7 1.167Z"
              fill="currentColor"></path>
        <path
              d="M7.583 8.75H6.416v-.434a2.306 2.306 0 0 1 1.146-2.041A1.167 1.167 0 1 0 5.833 5.25H4.666a2.333 2.333 0 1 1 3.46 2.044 1.156 1.156 0 0 0-.543 1.022v.434ZM7.583 9.916H6.416v1.167h1.167V9.916Z"
              fill="currentColor"></path>
      </svg>
    </div>
    <div class="form_info">Please add your signature</div>
  </div>
</div>

2

Answers


  1. Chosen as BEST ANSWER

    I ended up building my own little library that also integrates with Webflow components super easily.

    If you want to check it out, you can find it here:

    https://github.com/vierless/attributes/tree/main/signature-pad


  2. Seems like the clearing of the canvas on resize is intended by the library.

    Certainly, an questionable design decision but you could bypass this behavior by creating your own point calculation adding a scaling logic.

    Basically we’re setting the canvas dimensions only on initialization (according to CSS rules) and scale/project the mouse/pointer coordinates to meet the current canvas scaling. So the canvas keeps its dimensions and is solely scaled by its CSS layout properties.

    const wrapper = document.querySelector('.form_signature-wrapper');
    const canvas = wrapper.querySelector('#signature-pad');
    const clearButton = wrapper.querySelector('[data-action=clear]');
    
    
    function initializeSignaturePad() {
    
      //set initial width and height
      let ratio = Math.max(window.devicePixelRatio || 1, 1);
      let bb = wrapper.getBoundingClientRect();
      let w = bb.width * ratio;
      let h = bb.height * ratio;
      canvas.width = w;
      canvas.height = h;
    
    
      const signaturePad = new SignaturePad(canvas, {
        penColor: 'rgb(255, 255, 255)',
        minWidth: 0.5,
        maxWidth: 2.5,
        throttle: 0,
        minDistance: 0,
        velocityFilterWeight: 0.4,
      });
    
      // add scaling to point creation
      signaturePad._createPoint = (x, y, pressure) => {
        let bb = canvas.getBoundingClientRect();
        let scale = w / bb.width;
        return new Point((x - bb.left) * scale, (y - bb.top) * scale, pressure, new Date().getTime());
      }
    
      class Point {
        constructor(x, y, pressure, time) {
          if (isNaN(x) || isNaN(y)) {
            throw new Error(`Point is invalid: (${x}, ${y})`);
          }
          this.x = +x;
          this.y = +y;
          this.pressure = pressure || 0;
          this.time = time || Date.now();
        }
        distanceTo(start) {
          return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
        }
        equals(other) {
          return (this.x === other.x &&
            this.y === other.y &&
            this.pressure === other.pressure &&
            this.time === other.time);
        }
        velocityFrom(start) {
          return this.time !== start.time ?
            this.distanceTo(start) / (this.time - start.time) :
            0;
        }
      }
    
    
    
      clearButton.addEventListener('click', function(event) {
        event.preventDefault();
        event.stopPropagation();
        signaturePad.clear();
      });
    
      return signaturePad;
    }
    
    document.addEventListener('DOMContentLoaded', function() {
      initializeSignaturePad();
    });
    html,
    body {
      margin: 0;
      padding: 0;
    }
    
    body {
      background: #0c0c0c;
      font-family: sans-serif;
    }
    
    .form_field-wrapper {
      max-width: 100%;
      margin: 0 auto;
      padding: 2rem 0;
    }
    
    .form_label {
      color: #fff;
      margin-bottom: 0.5rem;
    }
    
    .form_signature-wrapper {
      width: 100%;
      height: auto;
      aspect-ratio: 5/3;
      resize: horizontal;
      overflow: auto;
      outline: 1px solid red;
    }
    
    .form_signature-canvas {
      height: 100%;
      width: 100%;
      display: block;
      box-sizing: border-box;
      border: 1px solid rgba(255, 255, 255, 0.1);
      border-radius: 0.25rem 0.25rem 0 0;
    }
    
    .form_signature-wrapper {
      position: relative;
    }
    
    .form_signature-wrapper button {
      appearance: none;
      outline: none;
      border: none;
      box-shadow: none;
      font-weight: 600;
      color: #fff;
      background: rgba(255, 255, 255, 0.1);
      padding: 0.5rem;
      border-radius: 0.25rem;
      position: absolute;
      top: 0.75rem;
      right: 0.75rem;
      transition: 100ms ease-in-out background;
    }
    
    .form_signature-wrapper button:hover {
      background: rgba(255, 255, 255, 0.2);
      cursor: pointer;
    }
    
    .form_info-wrapper {
      display: flex;
      gap: 0.5rem;
      padding: 0.5rem;
      background: rgba(255, 255, 255, 0.1);
      border-radius: 0 0 0.25rem 0.25rem;
      font-size: 0.875rem;
    }
    
    .form_info-icon {
      height: 1rem;
      width: 1rem;
    }
    
    .form_info-icon svg {
      display: flex;
      height: auto;
      width: 100%;
    }
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/signature_pad.umd.min.js"></script>
    
    
    <div class="form_field-wrapper">
      <div class="form_label">Signature</div>
      <div class="form_signature-wrapper">
        <canvas id="signature-pad" class="form_signature-canvas" width="" height="" style="touch-action: none"></canvas
          ><button
                   data-action="clear"
                   type="button"
                   class="form_signature-btn is-clear">
        Delete
        </button>
      </div>
      <div class="form_info-wrapper is-signature form_label">
    
        <div class="form_info">Please add your signature</div>
      </div>
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search