2

I am trying to render SVG images on a canvas the images are drawn one at a time to fill a given row, below is the code snippet of the same:

  function createSVGUrl(svg) {
    var svgBlob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'});
    return DOMURL.createObjectURL(svgBlob);
  };

/**
   * Renders svg tile on the given context.
   * @param {CanvasRenderingContext2D} ctx
   * @param {SVGElement} svg The svg tile.
   * @param {{x: number, y:number}} pos The position to draw the svg tile on.
   * @throws Error
   */
  function renderSVGTile(ctx, svg, pos) {
    var img = new Image();
    var url = createSVGUrl(svg);
    img.onload = function() {
      try {
        ctx.drawImage(img, pos.x, pos.y);
        ctx.imageSmoothingEnabled = false;
        ctx.mozImageSmoothingEnabled = false;
        DOMURL.revokeObjectURL(url);
      } catch (e) {
        throw new Error('Could not render image' + e);
      }
    };
    img.src = url;
  };

The problem is that I can see the partially filled rows which I don't want, is there any way to fill the whole row at once?

CodeYogi
  • 1,352
  • 1
  • 18
  • 41
  • Just wait for all your image have pre-loaded, then draw it. Btw, imageSmoothingEnabled should be set before you call drawImage, and avoid to set it in a loop. – Kaiido Jul 12 '16 at 23:35
  • Possible duplicate of [How do image preloaders work?](http://stackoverflow.com/questions/30578521/how-do-image-preloaders-work) – Kaiido Jul 12 '16 at 23:40
  • @Kaiido not exactly I tried the suggested solution but it doesn't work in my case, possibly due to very large number of images per row. Theoretically it should work but it doesn't, [here](http://codereview.stackexchange.com/q/133964/58341) you can see the example. – CodeYogi Jul 13 '16 at 05:06
  • o_o why do you pass through svg ? you know canvas has some drawing methods ? Your code would be waaaay simpler. – Kaiido Jul 13 '16 at 05:29
  • @Kaiido because of the same issue we developers suffer from yes, it is the requirement. – CodeYogi Jul 13 '16 at 06:13
  • @Kaiido I have posted a solution which is working fine for me, as I said its the hybrid approach which involves caching all images and rendering them to an offscreen canvas. – CodeYogi Jul 13 '16 at 06:59
  • I posted [a comment](http://codereview.stackexchange.com/questions/134633/svg-mosaic-creator#comment-252441) on your last question in CodeReview, with a [refactored code](https://jsfiddle.net/zwh2jq7r/). – Kaiido Jul 14 '16 at 04:45

3 Answers3

3

Yes. Draw your whole row of tiles to an offscreen Canvas first. When that's done, you can paint that offscreen canvas onto your main canvas.

Something like:

var offscreenCanvas = document.createElement('canvas');
offscreenCanvas .width = <whatever>;
offscreenCanvas .height = <whatever>;
var offscreenContext = offscreenCanvas.getContext('2d');

// Draw all your tiles
renderSVGTile(offscreenContext, svg, pos);
//... loop through all tiles etc

// When finished...
mainCanvasContext.drawImage(offscreenCanvas, 0, 0);

Demo:

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var image = document.getElementById("source");

var offscreenCanvas = document.createElement("canvas");
offscreenCanvas.width = 300;
offscreenCanvas.height = 150;
var offscreenContext = offscreenCanvas.getContext("2d");

offscreenContext.drawImage(image, 33, 71, 104, 124, 21, 20, 87, 104);
offscreenContext.drawImage(image, 33, 71, 104, 124, 108, 20, 87, 104);

ctx.drawImage(offscreenCanvas, 0, 0);
<canvas id="canvas"></canvas>
<div style="display:none;">
  <img id="source" src="http://placekitten.com/300/227"
       width="300" height="227">
</div>
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
  • DId you try this, because I tried this before but was unsuccessful. – CodeYogi Jul 12 '16 at 16:09
  • Added working demo (albeit with normal bitmap images). Tested in Chrome and Firefox. – Paul LeBeau Jul 12 '16 at 16:53
  • LGTM! I will try it myself. – CodeYogi Jul 12 '16 at 17:16
  • That won't work and only did in your test thanks to the caching of your image (and the fact you're using a single one) OP's method is async. He has to preload his images. – Kaiido Jul 12 '16 at 23:39
  • Yes you are right Kaido. I was assuming that the images were already cached. My example was more to do with demonstrating how to render a row at a time. Which was the OPs original request. – Paul LeBeau Jul 13 '16 at 03:07
  • I think I have to mix both of the solutions because I still can see half rows while rendering. – CodeYogi Jul 13 '16 at 05:12
0

This is how I do it, though I am not sure if it will serve your purpose as I have the image in the document at the time of rendering, it is just hidden.

var ctx = document.getElementById("canvasID").getContext("2d");
ctx.drawImage(document.getElementById("imageID"), x,y,w,h);
Dellirium
  • 1,362
  • 16
  • 30
  • No, the svg are dynamically generated. – CodeYogi Jul 12 '16 at 16:09
  • Then I am afraid it cannot work, unless you are willing to add a dynamically generated SVG file to an image in the document, in which case it would work. I am not sure if ctx.drawImage would accept a non-element image, you could try since you are using the same method. Also you do not actually have to have an element attached to the page, so long as it is created and referenced. – Dellirium Jul 12 '16 at 16:17
0

The solution below is working fine for me:

 /**
   * @param {CanvasRenderingContext2D} ctx
   * @param {!Array<!SVGTile>} tiles
   */
  function drawTiles(ctx, tiles) {
    var canvas  = document.createElement('canvas');
    var width   = tiles.length * TILE_WIDTH;
    var height  = TILE_HEIGHT;
    var y = tiles[0].y;
    canvas.width  = tiles.length * TILE_WIDTH;
    canvas.height = TILE_HEIGHT;
    var context = canvas.getContext("2d");
    tiles.forEach(function(tile, index) {
      renderTile(context, tile, function() {
        if (tiles.length === index + 1) {
          ctx.drawImage(canvas, 0, y);
        }
      });
    });
    // TODO: Below code is for testing purpose.
    var temp = document.createElement('div');
    temp.appendChild(canvas);
    document.body.appendChild(temp);
  };


  /**
   * Renders svg tile on the given context.
   * @param {CanvasRenderingContext2D} ctx
   * @param {!Tile} tile The tile to render.
   * @param {function()} callback To be called after image is loaded.
   * @throws Error
   */
  function renderTile(ctx, tile, callback) {
    var img = new Image();
    img.onload = function() {
      try {
        ctx.drawImage(this, tile.x, 0, tile.width, tile.height);
        ctx.imageSmoothingEnabled = false;
        ctx.mozImageSmoothingEnabled = false;
        DOMURL.revokeObjectURL(tile.svgURL);
        callback();
      } catch (e) {
        throw new Error('Could not render image' + e);
      }
    };
    img.src = tile.svgURL;
  };
CodeYogi
  • 1,352
  • 1
  • 18
  • 41