Collecting signatures with an Anvil app

@neriksen85 asked about collecting signatures - that is, allowing a user to draw their signature onto a web page, using their finger on a mobile phone. So I put together an example:

A custom component for capturing drawn signatures, using a Canvas. Stripped down, so you can use it as a dependency: https://anvil.works/ide#clone:HSX5MT2GEPCSIK6V=G2IHIAU67JA7BMOI3U3AL2LS

A sample app using this component to create a guest-book of autographs:
https://anvil.works/ide#clone:LHP3M44ADYWDZAFS=DLR622NXP62PTQY5DBIJUB62

See it in action:


Thanks again to @neriksen85 for letting us share the fruits of his support contract with the forum.

Do you want guaranteed timely help and development assistance from the Anvil crew? Drop us a line at support@anvil.works.

6 Likes

The example given does not correctly handle resize events, typically raised by anvil apps and especially on mobile devices. I put together a little hack from multiple sources that seems to work. 1. Create html form and in the custom html put:

<style>
  #sig-canvas {
  border: 2px dotted #CCCCCC;
  border-radius: 15px;
  cursor: crosshair;
  touch-action: none;
}
  
  div.containercan {
    width:100;
    height:100px;
  }
</style>
  
<div class="containercan">
		 	<canvas id="sig-canvas" >
		 	</canvas>
</div>

<script>
(function() {
  window.requestAnimFrame = (function(callback) {
    return window.requestAnimationFrame ||
      window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame ||
      window.oRequestAnimationFrame ||
      window.msRequestAnimaitonFrame ||
      function(callback) {
        window.setTimeout(callback, 1000 / 60);
      };
  })();

  var canvas = document.getElementById("sig-canvas");
  var ctx = canvas.getContext("2d");
  fitToContainer(canvas);
  ctx.strokeStyle = "#222222";
  ctx.lineWidth = 2;

  var drawing = false;
  var mousePos = {
    x: 0,
    y: 0
  };
  var lastPos = mousePos;

  canvas.addEventListener("mousedown", function(e) {
    drawing = true;
    lastPos = getMousePos(canvas, e);
  }, false);

  canvas.addEventListener("mouseup", function(e) {
    drawing = false;
  }, false);

  canvas.addEventListener("mousemove", function(e) {
    mousePos = getMousePos(canvas, e);
  }, false);

  // Add touch event support for mobile
  canvas.addEventListener("touchstart", function(e) {

  }, false);

  canvas.addEventListener("touchmove", function(e) {
    var touch = e.touches[0];
    var me = new MouseEvent("mousemove", {
      clientX: touch.clientX,
      clientY: touch.clientY
    });
    canvas.dispatchEvent(me);
  }, false);

  canvas.addEventListener("touchstart", function(e) {
    mousePos = getTouchPos(canvas, e);
    var touch = e.touches[0];
    var me = new MouseEvent("mousedown", {
      clientX: touch.clientX,
      clientY: touch.clientY
    });
    canvas.dispatchEvent(me);
  }, false);

  canvas.addEventListener("touchend", function(e) {
    var me = new MouseEvent("mouseup", {});
    canvas.dispatchEvent(me);
  }, false);

  function getMousePos(canvasDom, mouseEvent) {
    var rect = canvasDom.getBoundingClientRect();
    return {
      x: mouseEvent.clientX - rect.left,
      y: mouseEvent.clientY - rect.top
    }
  }

  function getTouchPos(canvasDom, touchEvent) {
    var rect = canvasDom.getBoundingClientRect();
    return {
      x: touchEvent.touches[0].clientX - rect.left,
      y: touchEvent.touches[0].clientY - rect.top
    }
  }

  function renderCanvas() {
    if (drawing) {
      ctx.moveTo(lastPos.x, lastPos.y);
      ctx.lineTo(mousePos.x, mousePos.y);
      ctx.stroke();
      lastPos = mousePos;
    }
  }

  // Prevent scrolling when touching the canvas
  document.body.addEventListener("touchstart", function(e) {
    if (e.target == canvas) {
      e.preventDefault();
    }
  }, false);
  document.body.addEventListener("touchend", function(e) {
    if (e.target == canvas) {
      e.preventDefault();
    }
  }, false);
  document.body.addEventListener("touchmove", function(e) {
    if (e.target == canvas) {
      e.preventDefault();
    }
  }, false);

  document.getElementById( "sig-canvas" ).onwheel = function(event){
    event.preventDefault();
};

document.getElementById( "sig-canvas" ).onmousewheel = function(event){
    event.preventDefault();
};

document.getElementById( "sig-canvas" ).onmousewheel = function(event){
    event.preventDefault();
};
  
  (function drawLoop() {
    requestAnimFrame(drawLoop);
    renderCanvas();
  })();

  function clearCanvas() {
    canvas.width = canvas.width;
  }
  
   
  function fitToContainer(canvas){
  canvas.style.width='100%';
  canvas.style.height='100%';
  canvas.width  = canvas.offsetWidth;
  canvas.height = canvas.offsetHeight;
}
})();
</script>

Make the form a component and wala, you have a signature pad to place in any form.

5 Likes

@754214 I am still learning about how to structure components within Anvil. In your example custom HTML code above, how would you call the function clearCanvas() from a button within the Anvil form? I tried a number of things, ex. self.signature_image.call_js(‘clearCanvas’) but that didn’t work. Likewise, if I wanted to grab the signature image after a button click, how would I get that?

Thanks,
David

Thank you so much for this! It helped a bunch

@meredydd a bug i found though, using a mobile phone if you were to scroll up or down, sometimes the signature disappears I am not sure why that happens.

The canvas component can be cleared by various window events, e.g. resizing. Apparently scrolling on mobile sometimes does it, too.

There’s a reset event on the canvas that you should hook into to redraw the canvas in those situations. Is there a way to change the mouse pointer when the mouse moves over the canvas component? - #8

2 Likes

I’d like to add another example for those trying to get a clean mobile experience.

It doesn’t quite hold the signature on resizing like @754214 example, but I was having cursor issues upon resizing which led to me find another solution.

I am leveraging the signature_pad git hub repo by sourcing it in my signature.html. (In the assests section)

In the html have have the save,undo,clear functions defined. I call these function from the python using self.call_js('function name here'). You can see that for every button click event.

When the save button is clicked, an image url is passed to a mediaurl object which you can than use within your python code :slight_smile:

Here is the app:
https://anvil.works/build#clone:3KK6CYVIZAU775MZ=DFWXOZAA7Z63FJ4FK4N2CSTZ

Hope this helps!