title: Scaling canvas on iOS seems to crash Mobile Safari
body: <p>I've been running into an issue where it seems like scaling canvases is causing mobile safari to crash.  The crash logs indicate low memory, but other than that don't seem to be very helpful.</p>
  
  <p>To reproduce the issue zoom in using a pinch zooming gesture as far as you can go and then remove your fingers.  Wait for the new canvases to load in.  Use a pinch zooming gesture to zoom out on the canvas and just hold your fingers down until the canvas has finished tiling in.  On an iPad 3 this seems to crash while you're holding your fingers down.</p>
  
  <p>On an iPad 1 it seems to happen still if you set this.numPages = 2, this.numCanvases = 2, this.zoomFactor = 2.5.</p>
  
  <p>I've tested with various Android tablets and I can't reproduce a crash so it seems like it's iOS specific.  In the full code with images and other things drawn on the canvases I can reproduce this issue with fewer canvases.</p>
  
  <p>Has anyone run into this issue before, and if so are there any workarounds?  Am I possibly doing something wrong with the pinch zooming code?</p>
  
  <p>Here's a stripped down version of the javascript:</p>
  
  <pre><code>(function(exports) {
  "use strict";
  
  var ReaderControl = function() {
      this.numPages = 2;
      this.numCanvases = 8;
      this.zoomFactor = 5.2;
  
      this.pageWidth = 479.86;
      this.pageHeight = 621;
  
      this.isPinching = false;
      this.distRatio = 1;
      this.oldScale = 1;
      this.newScale = 1;
      this.oldPinchCenter = {};
  
      this.bindEvents();
      this.c = this.createPageWrapper();
      $('body').append(this.c.$e);
  };
  
  ReaderControl.prototype = {
  
      bindEvents: function() {
          this.currentPageZoom = parseFloat(document.documentElement.clientHeight) / this.pageHeight;
          this.currentPageMinZoom = this.currentPageZoom;
          this.currentPageMaxZoom = (800 * 800 * this.zoomFactor) / (this.pageWidth * this.pageHeight * window.devicePixelRatio);
  
          var tbind = function(func, context) {
              return function() {
                  func.apply(context, Array.prototype.slice.call(arguments));
              };
          };
  
          $(document).bind('touchstart', tbind(this.onTouchStart, this));
          $(document).bind('touchmove', tbind(this.onTouchMove, this));  
          $(document).bind('touchend', tbind(this.onTouchEnd, this));
  
          document.ontouchmove = function(e) { 
              e.preventDefault();
          };
      },
  
      createCanvas: function (width, height) {
          var canvas = document.createElement('canvas');
  
          var cWidth = width * window.devicePixelRatio;
          var cHeight = height * window.devicePixelRatio;
          canvas.setAttribute('width', cWidth);
          canvas.setAttribute('height', cHeight);
  
          $(canvas).css('width', width);
          $(canvas).css('height', height);                
  
          var ctx = canvas.getContext("2d");
          ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
          ctx.fillStyle = 'pink';
          ctx.fillRect(0, 0, width, height);
          return canvas;
      },
  
      createPageWrapper: function() {
          var $wrapper = $("&lt;div&gt;&lt;/div&gt;");
  
          var scWidth = this.pageWidth * this.currentPageZoom;
          var scHeight = this.pageHeight * this.currentPageZoom;
  
          var origWidth = scWidth;
          var origHeight = scHeight;
  
          var totalWidth = origWidth * this.numPages;
          var maxHeight = origHeight;
  
          for (var pageNum = 0; pageNum &lt; this.numPages; pageNum++){
              for (var j = 0; j &lt; this.numCanvases; j++) {
                  var canvas = this.createCanvas(scWidth, scHeight / this.numCanvases);
                  var ctx = canvas.getContext("2d");
  
                  if (pageNum % 2 === 0) {
                      ctx.fillStyle = 'blue';
                      ctx.fillRect(0, 0, scWidth, scHeight);
                  }
  
                  $wrapper.append($(canvas));
              }
          }
  
          $wrapper.css("width", totalWidth + "px");
          $wrapper.css("height", maxHeight + "px");
  
          var left = (exports.innerWidth - totalWidth) / 2;
          var top = (exports.innerHeight - maxHeight) / 2;
  
          this.transform($wrapper, left, top);
          return {$e:$wrapper, tX: left, tY: top};
      },
  
      onTouchStart: function(evt) { 
          if (evt.originalEvent.touches.length &gt; 1) {
              this.isPinching = true;
  
              var touch0 = evt.originalEvent.touches[0];
              var touch1 = evt.originalEvent.touches[1];
  
              var x1 = touch1.clientX;
              var y1 = touch1.clientY;
              var x0 = touch0.clientX;
              var y0 = touch0.clientY;
  
              this.oldPinchCenter.x = (x0 + x1) / 2;
              this.oldPinchCenter.y = (y0 + y1) / 2;            
  
              this.oldDist = Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1));
          }
      },
  
      transform: function($e, x, y, scale) {
          scale = scale || 1;
  
          $e.css('-webkit-transform', 'translate3d(' + x + 'px,' + y + 'px, 0) scale(' + scale + ')');
      },
  
      onTouchMove: function(evt) {
          var width = this.c.$e.width();
          var height = this.c.$e.height();
  
          if (evt.originalEvent.touches.length &gt; 1) {
              var touch0 = evt.originalEvent.touches[0];
              var touch1 = evt.originalEvent.touches[1];
  
              var x1 = touch1.clientX;
              var y1 = touch1.clientY;
              var x0 = touch0.clientX;
              var y0 = touch0.clientY;
  
              this.newDist = Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1));
              this.distRatio = this.newDist / this.oldDist;
  
              var newPinchCenter = {
                  x: (x0 + x1) / 2,
                  y: (y0 + y1) / 2
              };
  
              this.newScale = this.distRatio * this.oldScale;
  
              var actualZoom = this.newScale * this.currentPageZoom;
              if (actualZoom &gt; this.currentPageMaxZoom) {
                  this.newScale = this.currentPageMaxZoom / parseFloat(this.currentPageZoom);
              }
  
              var pcMidX = this.c.tX + width / 2;
              var pcMidY = this.c.tY + height / 2;
  
              var pcCenter = {
                  x: pcMidX, 
                  y: pcMidY
              };
  
              var scX = pcCenter.x - (this.newScale / this.oldScale) * (pcCenter.x - this.oldPinchCenter.x);
              var scY = pcCenter.y - (this.newScale / this.oldScale) * (pcCenter.y - this.oldPinchCenter.y);
  
              var scaledOldPinchCenter = {
                  x: scX, 
                  y: scY
              };
  
              var offsetX = newPinchCenter.x - scaledOldPinchCenter.x;
              var offsetY = newPinchCenter.y - scaledOldPinchCenter.y;
              this.c.tX = this.c.tX + offsetX;
              this.c.tY = this.c.tY + offsetY;
  
              this.transform(this.c.$e, this.c.tX, this.c.tY, this.newScale);
  
              this.oldScale = this.newScale;
              this.oldDist = this.newDist;
              this.oldPinchCenter.x = newPinchCenter.x;
              this.oldPinchCenter.y = newPinchCenter.y;
          }
      },
  
      onTouchEnd: function(evt) {
          if (evt.originalEvent.touches.length === 0) {
              var newPageZoom = this.newScale * this.currentPageZoom;
  
              if (newPageZoom &lt; this.currentPageMinZoom) {
                  newPageZoom = this.currentPageMinZoom;
                  this.newScale = newPageZoom / parseFloat(this.currentPageZoom);
                  this.oldScale = this.newScale;
              }
  
              this.c.tX = 31.37;
              this.c.tY = 0;
              this.transform(this.c.$e, this.c.tX, this.c.tY);
  
              if (this.isPinching) {
                  var zoomedIn = newPageZoom &gt; this.currentPageMinZoom;
                  var goingToMinZoom = (newPageZoom === this.currentPageMinZoom) &amp;&amp; (this.currentPageZoom !== this.currentPageMinZoom);
                  var shouldZoom = newPageZoom !== this.currentPageZoom &amp;&amp; (goingToMinZoom || zoomedIn);
  
                  if (shouldZoom) {
                      this.currentPageZoom = newPageZoom;
  
                      this.invisC &amp;&amp; this.invisC.$e.remove();
                      this.invisC = this.createPageWrapper();
                      $('body').append(this.invisC.$e);
                      this.c.$e.remove();
  
                      this.c = this.invisC;
                      this.invisC = null;
  
                      this.newScale = 1;
                      this.oldScale = 1;
                  } 
              }
  
              this.isPinching = false;
          }
      }
  };
  
  exports.ReaderControl = ReaderControl;
  })(window);
  
  $(function(){
      window.readerControl = new ReaderControl();
  });
  </code></pre>
  
  <p>And here's the HTML file:</p>
  
  <pre><code>&lt;html&gt;
  &lt;head&gt;
      &lt;title&gt;Canvas Crash Test&lt;/title&gt;
      &lt;meta name="apple-mobile-web-app-capable" content="yes" /&gt;
      &lt;meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"&gt;
  
      &lt;script src="http://code.jquery.com/jquery-1.8.2.min.js"&gt;&lt;/script&gt;
      &lt;script src="test.js"&gt;&lt;/script&gt;
  &lt;/head&gt;
  
  &lt;body&gt;
  &lt;/body&gt;
  &lt;/html&gt;
  </code></pre>