Menu

Web Workers 5

Timo Kähkönen

Donate Javascript Clipper Project

D. Using Web Workers with Javascript Clipper

Javascript Clipper is now Web Worker compatible as of version 5.0.2.2.

Web workers is a new technology, that allows browser to run resource expensive tasks in background without affecting to the performance of the main page.

The downside of Web Workers is that certain features are not available: eg. window and document, which means that you cannot manipulate DOM eg. draw on canvas inside Worker task. The other downside is that you have to find a fallback solution for users that are using still older browsers that are not Web Worker capable.

Clipper can be used to handle thousands of polygons with thousands of vertices. In these cases, the browser is very likely to hang. To avoid these problems, the answer is Web Workers.

A Full code example of using Web Workers

Below is an example of drawing complex SVG using Web Workers. The program draws bars, that are randomly colored and sized. The task may take even few seconds, when the count of bars is over 10,000. Using Workers the main page remains responsive.

The example consists of two files: worker_main.html and worker_task.js.

Worker_main.html has the user interface and the functions related to these. Buttons are used to start and stop Web Worker and send drawing commands to worker_task.js.

Worker_task.js consists only of Javascript. Clipper library (clipper.js) is imported at the top of this file. Worker_task.js listens messages that are coming from worker_main.html and starts drawing tasks, generates SVG image source and sends it as a string back to the main page, which renders the image on the screen.

The timer indicates how long the different areas of the task took time. The areas are:

1) transfering the drawing command from main page to the worker
2) drawing SVG source
3) transfering SVG source string from worker to the main page
4) rendering SVG image

To run this example, please save both worker_main.html and worker_task.js below to your hard drive and navigate to worker_main.html. Press buttons on the page and see the results.

See the Worker example in action.


Screenshot of the Web Workers example:


worker_main.html:

    <!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Javascript Clipper Library / Drawing complex SVG using Web Workers</title>
    <style>
      body { overflow-y: scroll; }
      body, th, td, input, legend, fieldset, p, b, button, select, textarea { font-size:13px; font-family:Arial, Helvetica, sans-serif; }
    </style>
  </head>
  <body onload="document.getElementById('bar').style.height = window.innerHeight + 'px';">
    <button onclick="start()">Start worker</button>
    <button onclick="draw500()">Draw 500 bars</button>
    <button onclick="draw6000()">Draw 6000 bars</button>
    <button onclick="draw12000()">Draw 12000 bars</button>
    <button onclick="stop()">Stop worker</button>
    <br>
    <br>
    <output id="result"></output>
    <br>
    <h2>Javascript Clipper Library / Drawing complex SVG using Web Workers</h2>
    <p>This page shows a simple example of drawing random sized and random colored bars on SVG using Web Workers. Web Workers is a new technology to run Javascript in the background. The main goal of Web Workers is to avoid browser hanging, when time consuming operations are run. See <a href="http://caniuse.com/webworkers">the current browser support of Web Workers</a>.
    <p>Above buttons are used to instruct the Worker to create colored bars using Clipper's OffsetPolygons(). The worker creates SVG source as a string and sends it back to the main page, which renders finally the image.
    <p>To test Workers, press buttons above and try immediately after that to scroll the screen. You will see, that the browser's scroll bars can be used meanwhile Worker is working. Due to slow rendering of SVG, there is a little hang when SVG is actually drawn on the screen.
    <div id="svgcontainer"></div>
    <div id="bar" style="height:10000px"></div>
    <script>
      var cont = document.getElementById('svgcontainer');
      var ClickTime;
      var worker;
      var result = document.getElementById('result');
      var workerbusy = false;
      var spinner = '<svg width="800" height="400"><text font-family="Courier" font-size="40" x="400" y="160" fill="blue" text-anchor="middle">Please wait...<animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 400 155" to="360 400 155" dur="2s" repeatCount="indefinite"/></text></svg>';
      function stop()
      {
        if (typeof(worker) == "undefined") return;
        worker.terminate();
        result.textContent = 'Worker stopped (buttons will no longer work).';
        worker = undefined;
        workerbusy = false;
        cont.innerHTML = "";
      }
      function busy()
      {
        if (!workerbusy) return false;
        result.textContent = "Worker is busy!";
        return true;
      }
      function stopped()
      {
        if (typeof(worker) !== "undefined") return false;
        result.textContent = "Worker is stopped!";
        return true;
      }
      function draw500()
      {
        if (stopped() || busy()) return;
        workerbusy = true;
        cont.innerHTML = spinner;
        result.textContent = "Worker is running...";
        ClickTime = Date.now();
        worker.postMessage({'cmd': 'draw500', 'msg': 'Draw 500 pcs'});
      }
      function draw6000() {
        if (stopped() || busy()) return;
        workerbusy = true;
        cont.innerHTML = spinner;
        result.textContent = "Worker is running...";
        ClickTime = Date.now();
        worker.postMessage({'cmd': 'draw6000', 'msg': 'Draw 6000 pcs'});
      }
      function draw12000() {
        if (stopped() || busy()) return;
        workerbusy = true;
        cont.innerHTML = spinner;
        result.textContent = "Worker is running...";
        ClickTime = Date.now();
        worker.postMessage({'cmd': 'draw12000', 'msg': 'Draw 12000 pcs'});
      }
      function start()
      {
        if (typeof(Worker) === "undefined")
        {
          result.textContent = "Sorry, your browser does not support Web Workers...";
          return;
        }
        if(typeof(worker) !== "undefined")
        {
           result.textContent = "Worker is already started.";
           return;
        }
        worker = new Worker('worker_task.js');
        result.textContent = "Worker is now started.";
        worker.addEventListener('message', function(e)
        {
          if(!e.data.result) result.textContent = e.data;
          else
          {
            var TransferEnd = Date.now();
            var TransferStart =  Number(e.data.TransferStart);
            var TransferTime = TransferEnd - TransferStart;
            result.textContent = "Drawing SVG...";
            var RenderingStart = Date.now();
            document.getElementById('svgcontainer').innerHTML = e.data.result;
            var RenderingEnd = Date.now();
            var RenderingTime = RenderingEnd - RenderingStart;
            var EndTime = Date.now();
            var TotalTime = EndTime - ClickTime;
            var DrawingTime = Number(e.data.DrawingTime);
            result.textContent = "Total time: " + TotalTime + " ms (" + 
                                "ToWorkerTransferTime: " + (TotalTime-(DrawingTime+TransferTime+RenderingTime)) + " ms + " +
                                "DrawingTime: " + DrawingTime + " ms + " +
                                "FromWorkerTransferTime: " + TransferTime + " ms  + " +
                                "RenderingTime: " + RenderingTime + " ms)";
            workerbusy = false;
          }
        }, false);
      }
      start();
    </script>
  </body>
</html>

worker_task.js:

// Worker related functionalities:
if (typeof(document) === "undefined" && typeof(importScripts) !== "undefined") // test if we are in worker scope
{
  importScripts('clipper.js');

  self.addEventListener('message', function(e) // We listen messages from the main page
  {
    var data = e.data;
    // according to received commands (draw500, draw6000, draw12000) we call draw() to get svg data and send it to the main page
    switch (data.cmd)
    {
      case 'draw500':
        var DrawingStart = Date.now();
        var svg = draw(500);
        var DrawingEnd = Date.now();
        var DrawingTime = DrawingEnd - DrawingStart;
        var TransferStart = Date.now();
        self.postMessage({result:svg, DrawingTime:DrawingTime, TransferStart:TransferStart});
        break;
      case 'draw6000':
        var DrawingStart = Date.now();
        var svg = draw(6000);
        var DrawingEnd = Date.now();
        var DrawingTime = DrawingEnd - DrawingStart;
        var TransferStart = Date.now();
        self.postMessage({result:svg, DrawingTime:DrawingTime, TransferStart:TransferStart});
        break;
      case 'draw12000':
        var DrawingStart = Date.now();
        var svg = draw(12000);
        var DrawingEnd = Date.now();
        var DrawingTime = DrawingEnd - DrawingStart;
        var TransferStart = Date.now();
        self.postMessage({result:svg, DrawingTime:DrawingTime, TransferStart:TransferStart});
        break;
      default:
        self.postMessage('Unknown command: ' + data.msg);
    };
  }, false);
}

// SVG source is created here
function draw(count)
{
  var cpr = new ClipperLib.Clipper();
  var polygons;
  var scale = 100;
  var joinType = ClipperLib.JoinType.jtRound;
  var svg = "", offsetted_polygon, miterLimit = 2, AutoFix = false, i, xstart, ystart, xend, yend;  
  svg += '<svg style="margin-top:10px;margin-right:10px;margin-bottom:10px;background-color:#dddddd" width="800" height="320">';
  for(i = 0; i < count; i++)
  {
    xstart = Math.round(Math.random()*800);
    ystart = Math.round(Math.random()*320);
    xend = Math.round(Math.random()*800);
    yend = Math.round(Math.random()*320);
    polygons = new Array();
    polygons.push([{X:xstart, Y:ystart}, {X:xend, Y:yend }]);
    // We cannot use console.log for debugging, but self.postMessage:
    //self.postMessage(JSON.stringify(polygons));
    polygons = scaleup(polygons, scale);
    offsetted_polygon = cpr.OffsetPolygons(polygons, 5 * scale, joinType, miterLimit, AutoFix);
    //self.postMessage(JSON.stringify(JSON.stringify(offsetted_polygon)));
    svg += '<path stroke="black" fill="' + randomColor({red:255, green:255, blue:255}) + '" stroke-width="1" d="' + polys2path(offsetted_polygon, scale) + '"/>';
  }
  svg += '</svg>';
  return svg;
}

// Helper function to scale up polygon coordinates
function scaleup(poly, scale) {
  var i, j;
  if (!scale) scale = 1;
  for(i = 0; i < poly.length; i++) {
    for(j = 0; j < poly[i].length; j++) {
      poly[i][j].X *= scale;
      poly[i][j].Y *= scale;
    }
  }
  return poly;
}

// Helper function to convert Polygons object to SVG path string
function polys2path (poly, scale) {
  var path = "", i, j;
  if (!scale) scale = 1;
  for(i = 0; i < poly.length; i++) {
    for(j = 0; j < poly[i].length; j++) {
      if (!j) path += "M";
      else path += "L";
      path += (poly[i][j].X / scale) + "," + (poly[i][j].Y / scale);
    }
    path += "Z";
  }
  return path;
}

// Helpers function to get random color in hex
function randomColor(mix) {
  var rand = Math.random, round = Math.round;
  var red = round(rand()*255);
  var green = round(rand()*255);
  var blue = round(rand()*255);
  if (mix != null) {
    red = round((red + mix.red) / 2);
    green = round((green + mix.green) / 2);
    blue = round((blue + mix.blue) / 2);
  }
  function componentToHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
  }
  function rgbToHex(r, g, b) {
    return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
  }
  return (rgbToHex(red, green, blue));
}