]> git.proxmox.com Git - mirror_novnc.git/blobdiff - include/display.js
Support automatic resize [Part 1/4]: display.js
[mirror_novnc.git] / include / display.js
index b11e1aaa30b0c95398f91a1f44c1999b80df505d..d1278681fe8a1fbcc10e1992d592e834627ebea8 100644 (file)
 /*
  * noVNC: HTML5 VNC client
- * Copyright (C) 2011 Joel Martin
- * Licensed under LGPL-3 (see LICENSE.txt)
+ * Copyright (C) 2012 Joel Martin
+ * Licensed under MPL 2.0 (see LICENSE.txt)
  *
  * See README.md for usage and integration instructions.
  */
 
-/*jslint browser: true, white: false, bitwise: false */
+/*jslint browser: true, white: false */
 /*global Util, Base64, changeCursor */
 
-function Display(defaults) {
-"use strict";
+var Display;
 
-var that           = {},  // Public API methods
-    conf           = {},  // Configuration attributes
+(function () {
+    "use strict";
 
-    // Private Display namespace variables
-    c_ctx          = null,
-    c_forceCanvas  = false,
+    Display = function (defaults) {
+        this._drawCtx = null;
+        this._c_forceCanvas = false;
 
-    c_imageData, c_rgbxImage, c_cmapImage,
+        this._renderQ = [];  // queue drawing actions for in-oder rendering
 
-    // Predefine function variables (jslint)
-    imageDataCreate, imageDataGet, rgbxImageData, cmapImageData,
-    rgbxImageFill, cmapImageFill, setFillColor, rescale, flush,
+        // the full frame buffer (logical canvas) size
+        this._fb_width = 0;
+        this._fb_height = 0;
 
-    // The full frame buffer (logical canvas) size
-    fb_width        = 0,
-    fb_height       = 0,
-    // The visible "physical canvas" viewport
-    viewport       = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
-    cleanRect      = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
+        // the visible "physical canvas" viewport
+        this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 };
+        this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 };
 
-    c_prevStyle    = "",
+        this._prevDrawStyle = "";
+        this._tile = null;
+        this._tile16x16 = null;
+        this._tile_x = 0;
+        this._tile_y = 0;
 
-    c_webkit_bug   = false,
-    c_flush_timer  = null;
+        Util.set_defaults(this, defaults, {
+            'true_color': true,
+            'colourMap': [],
+            'scale': 1.0,
+            'viewport': false,
+            'render_mode': ''
+        });
 
-// Configuration attributes
-Util.conf_defaults(conf, that, defaults, [
-    ['target',      'wo', 'dom',  null, 'Canvas element for rendering'],
-    ['context',     'ro', 'raw',  null, 'Canvas 2D context for rendering (read-only)'],
-    ['logo',        'rw', 'raw',  null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'],
-    ['true_color',  'rw', 'bool', true, 'Use true-color pixel data'],
-    ['colourMap',   'rw', 'arr',  [], 'Colour map array (when not true-color)'],
-    ['scale',       'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'],
-    ['viewport',    'rw', 'bool', false, 'Use a viewport set with viewportChange()'],
-    ['width',       'rw', 'int', null, 'Display area width'],
-    ['height',      'rw', 'int', null, 'Display area height'],
+        Util.Debug(">> Display.constructor");
 
-    ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
+        if (!this._target) {
+            throw new Error("Target must be set");
+        }
 
-    ['prefer_js',   'rw', 'str', null, 'Prefer Javascript over canvas methods'],
-    ['cursor_uri',  'rw', 'raw', null, 'Can we render cursor using data URI']
-    ]);
+        if (typeof this._target === 'string') {
+            throw new Error('target must be a DOM element');
+        }
 
-// Override some specific getters/setters
-that.get_context = function () { return c_ctx; };
+        if (!this._target.getContext) {
+            throw new Error("no getContext method");
+        }
 
-that.set_scale = function(scale) { rescale(scale); };
+        if (!this._drawCtx) {
+            this._drawCtx = this._target.getContext('2d');
+        }
 
-that.set_width = function (val) { that.resize(val, fb_height); };
-that.get_width = function() { return fb_width; };
+        Util.Debug("User Agent: " + navigator.userAgent);
+        if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); }
+        if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); }
+        if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); }
+        if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); }
 
-that.set_height = function (val) { that.resize(fb_width, val); };
-that.get_height = function() { return fb_height; };
+        this.clear();
 
-that.set_prefer_js = function(val) {
-    if (val && c_forceCanvas) {
-        Util.Warn("Preferring Javascript to Canvas ops is not supported");
-        return false;
-    }
-    conf.prefer_js = val;
-    return true;
-};
+        // Check canvas features
+        if ('createImageData' in this._drawCtx) {
+            this._render_mode = 'canvas rendering';
+        } else {
+            throw new Error("Canvas does not support createImageData");
+        }
 
+        if (this._prefer_js === null) {
+            Util.Info("Prefering javascript operations");
+            this._prefer_js = true;
+        }
 
+        // Determine browser support for setting the cursor via data URI scheme
+        var curDat = [];
+        for (var i = 0; i < 8 * 8 * 4; i++) {
+            curDat.push(255);
+        }
+        try {
+            var curSave = this._target.style.cursor;
+            Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8);
+            if (this._target.style.cursor) {
+                if (this._cursor_uri === null || this._cursor_uri === undefined) {
+                    this._cursor_uri = true;
+                }
+                Util.Info("Data URI scheme cursor supported");
+                this._target.style.cursor = curSave;
+            } else {
+                if (this._cursor_uri === null || this._cursor_uri === undefined) {
+                    this._cursor_uri = false;
+                }
+                Util.Warn("Data URI scheme cursor not supported");
+                this._target.style.cursor = "none";
+            }
+        } catch (exc) {
+            Util.Error("Data URI scheme cursor test exception: " + exc);
+            this._cursor_uri = false;
+        }
 
-//
-// Private functions
-//
+        Util.Debug("<< Display.constructor");
+    };
 
-// Create the public API interface
-function constructor() {
-    Util.Debug(">> Display.constructor");
+    Display.prototype = {
+        // Public methods
+        viewportChangePos: function (deltaX, deltaY) {
+            var vp = this._viewportLoc;
 
-    var c, func, imgTest, tval, i, curDat, curSave,
-        has_imageData = false, UE = Util.Engine;
+            if (!this._viewport) {
+                deltaX = -vp.w;  // clamped later of out of bounds
+                deltaY = -vp.h;
+            }
 
-    if (! conf.target) { throw("target must be set"); }
+            var vx2 = vp.x + vp.w - 1;
+            var vy2 = vp.y + vp.h - 1;
 
-    if (typeof conf.target === 'string') {
-        throw("target must be a DOM element");
-    }
+            // Position change
 
-    c = conf.target;
+            if (deltaX < 0 && vp.x + deltaX < 0) {
+                deltaX = -vp.x;
+            }
+            if (vx2 + deltaX >= this._fb_width) {
+                deltaX -= vx2 + deltaX - this._fb_width + 1;
+            }
 
-    if (! c.getContext) { throw("no getContext method"); }
+            if (vp.y + deltaY < 0) {
+                deltaY = -vp.y;
+            }
+            if (vy2 + deltaY >= this._fb_height) {
+                deltaY -= (vy2 + deltaY - this._fb_height + 1);
+            }
 
-    if (! c_ctx) { c_ctx = c.getContext('2d'); }
+            if (deltaX === 0 && deltaY === 0) {
+                return;
+            }
+            Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
 
-    Util.Debug("User Agent: " + navigator.userAgent);
-    if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); }
-    if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); }
-    if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); }
-    if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); }
+            vp.x += deltaX;
+            vx2 += deltaX;
+            vp.y += deltaY;
+            vy2 += deltaY;
 
-    that.clear();
+            // Update the clean rectangle
+            var cr = this._cleanRect;
+            if (vp.x > cr.x1) {
+                cr.x1 = vp.x;
+            }
+            if (vx2 < cr.x2) {
+                cr.x2 = vx2;
+            }
+            if (vp.y > cr.y1) {
+                cr.y1 = vp.y;
+            }
+            if (vy2 < cr.y2) {
+                cr.y2 = vy2;
+            }
 
-    /*
-     * Determine browser Canvas feature support
-     * and select fastest rendering methods
-     */
-    tval = 0;
-    try {
-        imgTest = c_ctx.getImageData(0, 0, 1,1);
-        imgTest.data[0] = 123;
-        imgTest.data[3] = 255;
-        c_ctx.putImageData(imgTest, 0, 0);
-        tval = c_ctx.getImageData(0, 0, 1, 1).data[0];
-        if (tval === 123) {
-            has_imageData = true;
-        }
-    } catch (exc1) {}
-
-    if (has_imageData) {
-        Util.Info("Canvas supports imageData");
-        c_forceCanvas = false;
-        if (c_ctx.createImageData) {
-            // If it's there, it's faster
-            Util.Info("Using Canvas createImageData");
-            conf.render_mode = "createImageData rendering";
-            c_imageData = imageDataCreate;
-        } else if (c_ctx.getImageData) {
-            // I think this is mostly just Opera
-            Util.Info("Using Canvas getImageData");
-            conf.render_mode = "getImageData rendering";
-            c_imageData = imageDataGet;
-        }
-        Util.Info("Prefering javascript operations");
-        if (conf.prefer_js === null) {
-            conf.prefer_js = true;
-        }
-        c_rgbxImage = rgbxImageData;
-        c_cmapImage = cmapImageData;
-    } else {
-        Util.Warn("Canvas lacks imageData, using fillRect (slow)");
-        conf.render_mode = "fillRect rendering (slow)";
-        c_forceCanvas = true;
-        conf.prefer_js = false;
-        c_rgbxImage = rgbxImageFill;
-        c_cmapImage = cmapImageFill;
-    }
-
-    if (UE.webkit && UE.webkit >= 534.7 && UE.webkit <= 534.9) {
-        // Workaround WebKit canvas rendering bug #46319
-        conf.render_mode += ", webkit bug workaround";
-        Util.Debug("Working around WebKit bug #46319");
-        c_webkit_bug = true;
-        for (func in {"fillRect":1, "copyImage":1, "rgbxImage":1,
-                "cmapImage":1, "blitStringImage":1}) {
-            that[func] = (function() {
-                var myfunc = that[func]; // Save original function
-                //Util.Debug("Wrapping " + func);
-                return function() {
-                    myfunc.apply(this, arguments);
-                    if (!c_flush_timer) {
-                        c_flush_timer = setTimeout(flush, 100);
-                    }
-                };
-            }());
-        }
-    }
-
-    /*
-     * Determine browser support for setting the cursor via data URI
-     * scheme
-     */
-    curDat = [];
-    for (i=0; i < 8 * 8 * 4; i += 1) {
-        curDat.push(255);
-    }
-    try {
-        curSave = c.style.cursor;
-        changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8);
-        if (c.style.cursor) {
-            if (conf.cursor_uri === null) {
-                conf.cursor_uri = true;
-            }
-            Util.Info("Data URI scheme cursor supported");
-        } else {
-            if (conf.cursor_uri === null) {
-                conf.cursor_uri = false;
+            var x1, w;
+            if (deltaX < 0) {
+                // Shift viewport left, redraw left section
+                x1 = 0;
+                w = -deltaX;
+            } else {
+                // Shift viewport right, redraw right section
+                x1 = vp.w - deltaX;
+                w = deltaX;
             }
-            Util.Warn("Data URI scheme cursor not supported");
-        }
-        c.style.cursor = curSave;
-    } catch (exc2) { 
-        Util.Error("Data URI scheme cursor test exception: " + exc2);
-        conf.cursor_uri = false;
-    }
-
-    Util.Debug("<< Display.constructor");
-    return that ;
-}
-
-rescale = function(factor) {
-    var c, tp, x, y, 
-        properties = ['transform', 'WebkitTransform', 'MozTransform', null];
-    c = conf.target;
-    tp = properties.shift();
-    while (tp) {
-        if (typeof c.style[tp] !== 'undefined') {
-            break;
-        }
-        tp = properties.shift();
-    }
-
-    if (tp === null) {
-        Util.Debug("No scaling support");
-        return;
-    }
-
-
-    if (typeof(factor) === "undefined") {
-        factor = conf.scale;
-    } else if (factor > 1.0) {
-        factor = 1.0;
-    } else if (factor < 0.1) {
-        factor = 0.1;
-    }
-
-    if (conf.scale === factor) {
-        //Util.Debug("Display already scaled to '" + factor + "'");
-        return;
-    }
-
-    conf.scale = factor;
-    x = c.width - c.width * factor;
-    y = c.height - c.height * factor;
-    c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
-};
-
-that.viewportChange = function(deltaX, deltaY, width, height) {
-    var c = conf.target, v = viewport, cr = cleanRect,
-        saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h;
-
-    if (!conf.viewport) {
-        Util.Debug("Setting viewport to full display region");
-        deltaX = -v.w; // Clamped later if out of bounds
-        deltaY = -v.h; // Clamped later if out of bounds
-        width = fb_width;
-        height = fb_height;
-    }
-
-    if (typeof(deltaX) === "undefined") { deltaX = 0; }
-    if (typeof(deltaY) === "undefined") { deltaY = 0; }
-    if (typeof(width) === "undefined") { width = v.w; }
-    if (typeof(height) === "undefined") { height = v.h; }
-
-    // Size change
-
-    if (width > fb_width) { width = fb_width; }
-    if (height > fb_height) { height = fb_height; }
-
-    if ((v.w !== width) || (v.h !== height)) {
-        // Change width
-        if ((width < v.w) && (cr.x2 > v.x + width -1)) {
-            cr.x2 = v.x + width - 1;
-        }
-        v.w = width;
 
-        // Change height
-        if ((height < v.h) && (cr.y2 > v.y + height -1)) {
-            cr.y2 = v.y + height - 1;
-        }
-        v.h = height;
+            var y1, h;
+            if (deltaY < 0) {
+                // Shift viewport up, redraw top section
+                y1 = 0;
+                h = -deltaY;
+            } else {
+                // Shift viewport down, redraw bottom section
+                y1 = vp.h - deltaY;
+                h = deltaY;
+            }
 
+            // Copy the valid part of the viewport to the shifted location
+            var saveStyle = this._drawCtx.fillStyle;
+            var canvas = this._target;
+            this._drawCtx.fillStyle = "rgb(255,255,255)";
+            if (deltaX !== 0) {
+                this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h);
+                this._drawCtx.fillRect(x1, 0, w, vp.h);
+            }
+            if (deltaY !== 0) {
+                this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h);
+                this._drawCtx.fillRect(0, y1, vp.w, h);
+            }
+            this._drawCtx.fillStyle = saveStyle;
+        },
 
-        if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) {
-            console.log("here1:",
-                    ((c.width < v.w) ? c.width : v.w),
-                    ((c.height < v.h) ? c.height : v.h));
-            saveImg = c_ctx.getImageData(0, 0,
-                    (c.width < v.w) ? c.width : v.w,
-                    (c.height < v.h) ? c.height : v.h);
-        }
+        viewportChangeSize: function(width, height) {
 
-        c.width = v.w;
-        c.height = v.h;
+            if (!this._viewport ||
+                typeof(width) === "undefined" || typeof(height) === "undefined") {
 
-        if (saveImg) {
-            c_ctx.putImageData(saveImg, 0, 0);
-        }
-    }
-
-    vx2 = v.x + v.w - 1;
-    vy2 = v.y + v.h - 1;
-
-
-    // Position change
-
-    if ((deltaX < 0) && ((v.x + deltaX) < 0)) {
-        deltaX = - v.x;
-    }
-    if ((vx2 + deltaX) >= fb_width) {
-        deltaX -= ((vx2 + deltaX) - fb_width + 1);
-    }
-
-    if ((v.y + deltaY) < 0) {
-        deltaY = - v.y;
-    }
-    if ((vy2 + deltaY) >= fb_height) {
-        deltaY -= ((vy2 + deltaY) - fb_height + 1);
-    }
-
-    if ((deltaX === 0) && (deltaY === 0)) {
-        //Util.Debug("skipping viewport change");
-        return;
-    }
-    Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
-
-    v.x += deltaX;
-    vx2 += deltaX;
-    v.y += deltaY;
-    vy2 += deltaY;
-
-    // Update the clean rectangle
-    if (v.x > cr.x1) {
-        cr.x1 = v.x;
-    }
-    if (vx2 < cr.x2) {
-        cr.x2 = vx2;
-    }
-    if (v.y > cr.y1) {
-        cr.y1 = v.y;
-    }
-    if (vy2 < cr.y2) {
-        cr.y2 = vy2;
-    }
-
-    if (deltaX < 0) {
-        // Shift viewport left, redraw left section
-        x1 = 0;
-        w = - deltaX;
-    } else {
-        // Shift viewport right, redraw right section
-        x1 = v.w - deltaX;
-        w = deltaX;
-    }
-    if (deltaY < 0) {
-        // Shift viewport up, redraw top section
-        y1 = 0;
-        h = - deltaY;
-    } else {
-        // Shift viewport down, redraw bottom section
-        y1 = v.h - deltaY;
-        h = deltaY;
-    }
-
-    // Copy the valid part of the viewport to the shifted location
-    saveStyle = c_ctx.fillStyle;
-    c_ctx.fillStyle = "rgb(255,255,255)";
-    if (deltaX !== 0) {
-        //that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
-        //that.fillRect(x1, 0, w, v.h, [255,255,255]);
-        c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h);
-        c_ctx.fillRect(x1, 0, w, v.h);
-    }
-    if (deltaY !== 0) {
-        //that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
-        //that.fillRect(0, y1, v.w, h, [255,255,255]);
-        c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h);
-        c_ctx.fillRect(0, y1, v.w, h);
-    }
-    c_ctx.fillStyle = saveStyle;
-};
-
-that.getCleanDirtyReset = function() {
-    var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [],
-        vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1;
-
-
-    // Copy the cleanRect
-    cleanBox = {'x': c.x1, 'y': c.y1,
-                'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1};
-
-    if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
-        // Whole viewport is dirty
-        dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h});
-    } else {
-        // Redraw dirty regions
-        if (v.x < c.x1) {
-            // left side dirty region
-            dirtyBoxes.push({'x': v.x, 'y': v.y,
-                             'w': c.x1 - v.x + 1, 'h': v.h});
-        }
-        if (vx2 > c.x2) {
-            // right side dirty region
-            dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y,
-                             'w': vx2 - c.x2, 'h': v.h});
-        }
-        if (v.y < c.y1) {
-            // top/middle dirty region
-            dirtyBoxes.push({'x': c.x1, 'y': v.y,
-                             'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y});
-        }
-        if (vy2 > c.y2) {
-            // bottom/middle dirty region
-            dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1,
-                             'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2});
-        }
-    }
-
-    // Reset the cleanRect to the whole viewport
-    cleanRect = {'x1': v.x, 'y1': v.y,
-                 'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1};
-
-    return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
-};
-
-that.absX = function(x) {
-    return x + viewport.x;
-}
-that.absY = function(y) {
-    return y + viewport.y;
-}
-
-
-// Force canvas redraw (for webkit bug #46319 workaround)
-flush = function() {
-    var old_val;
-    //Util.Debug(">> flush");
-    old_val = conf.target.style.marginRight;
-    conf.target.style.marginRight = "1px";
-    c_flush_timer = null;
-    setTimeout(function () {
-            conf.target.style.marginRight = old_val;
-        }, 1);
-};
-
-setFillColor = function(color) {
-    var rgb, newStyle;
-    if (conf.true_color) {
-        rgb = color;
-    } else {
-        rgb = conf.colourMap[color[0]];
-    }
-    newStyle = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
-    if (newStyle !== c_prevStyle) {
-        c_ctx.fillStyle = newStyle;
-        c_prevStyle = newStyle;
-    }
-};
-
-
-//
-// Public API interface functions
-//
-
-that.resize = function(width, height) {
-    c_prevStyle    = "";
-
-    fb_width = width;
-    fb_height = height;
-
-    rescale(conf.scale);
-    that.viewportChange();
-};
-
-that.clear = function() {
-
-    if (conf.logo) {
-        that.resize(conf.logo.width, conf.logo.height);
-        that.blitStringImage(conf.logo.data, 0, 0);
-    } else {
-        that.resize(640, 20);
-        c_ctx.clearRect(0, 0, viewport.w, viewport.h);
-    }
-
-    // No benefit over default ("source-over") in Chrome and firefox
-    //c_ctx.globalCompositeOperation = "copy";
-};
-
-that.fillRect = function(x, y, width, height, color) {
-    setFillColor(color);
-    c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height);
-};
-
-that.copyImage = function(old_x, old_y, new_x, new_y, w, h) {
-    var x1 = old_x - viewport.x, y1 = old_y - viewport.y,
-        x2 = new_x - viewport.x, y2 = new_y  - viewport.y;
-    c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h);
-};
+                Util.Debug("Setting viewport to full display region");
+                width = this._fb_width;
+                height = this._fb_height;
+            }
 
-/*
- * Tile rendering functions optimized for rendering engines.
- *
- * - In Chrome/webkit, Javascript image data array manipulations are
- *   faster than direct Canvas fillStyle, fillRect rendering. In
- *   gecko, Javascript array handling is much slower.
- */
-that.getTile = function(x, y, width, height, color) {
-    var img, data = [], rgb, red, green, blue, i;
-    img = {'x': x, 'y': y, 'width': width, 'height': height,
-           'data': data};
-    if (conf.prefer_js) {
-        if (conf.true_color) {
-            rgb = color;
-        } else {
-            rgb = conf.colourMap[color[0]];
-        }
-        red = rgb[0];
-        green = rgb[1];
-        blue = rgb[2];
-        for (i = 0; i < (width * height * 4); i+=4) {
-            data[i    ] = red;
-            data[i + 1] = green;
-            data[i + 2] = blue;
-        }
-    } else {
-        that.fillRect(x, y, width, height, color);
-    }
-    return img;
-};
-
-that.setSubTile = function(img, x, y, w, h, color) {
-    var data, p, rgb, red, green, blue, width, j, i, xend, yend;
-    if (conf.prefer_js) {
-        data = img.data;
-        width = img.width;
-        if (conf.true_color) {
-            rgb = color;
-        } else {
-            rgb = conf.colourMap[color[0]];
-        }
-        red = rgb[0];
-        green = rgb[1];
-        blue = rgb[2];
-        xend = x + w;
-        yend = y + h;
-        for (j = y; j < yend; j += 1) {
-            for (i = x; i < xend; i += 1) {
-                p = (i + (j * width) ) * 4;
-                data[p    ] = red;
-                data[p + 1] = green;
-                data[p + 2] = blue;
-            }   
-        } 
-    } else {
-        that.fillRect(img.x + x, img.y + y, w, h, color);
-    }
-};
-
-that.putTile = function(img) {
-    if (conf.prefer_js) {
-        c_rgbxImage(img.x, img.y, img.width, img.height, img.data, 0);
-    }
-    // else: No-op, under gecko already done by setSubTile
-};
-
-imageDataGet = function(width, height) {
-    return c_ctx.getImageData(0, 0, width, height);
-};
-imageDataCreate = function(width, height) {
-    return c_ctx.createImageData(width, height);
-};
-
-rgbxImageData = function(x, y, width, height, arr, offset) {
-    var img, i, j, data, v = viewport;
-    /*
-    if ((x - v.x >= v.w) || (y - v.y >= v.h) ||
-        (x - v.x + width < 0) || (y - v.y + height < 0)) {
-        //console.log("skipping, out of bounds: ", x, y);
-        // Skipping because outside of viewport
-        return;
-    }
-    */
-    img = c_imageData(width, height);
-    data = img.data;
-    for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
-        data[i    ] = arr[j    ];
-        data[i + 1] = arr[j + 1];
-        data[i + 2] = arr[j + 2];
-        data[i + 3] = 255; // Set Alpha
-    }
-    c_ctx.putImageData(img, x - v.x, y - v.y);
-};
-
-// really slow fallback if we don't have imageData
-rgbxImageFill = function(x, y, width, height, arr, offset) {
-    var i, j, sx = 0, sy = 0;
-    for (i=0, j=offset; i < (width * height); i+=1, j+=4) {
-        that.fillRect(x+sx, y+sy, 1, 1, [arr[j], arr[j+1], arr[j+2]]);
-        sx += 1;
-        if ((sx % width) === 0) {
-            sx = 0;
-            sy += 1;
-        }
-    }
-};
-
-cmapImageData = function(x, y, width, height, arr, offset) {
-    var img, i, j, data, rgb, cmap;
-    img = c_imageData(width, height);
-    data = img.data;
-    cmap = conf.colourMap;
-    for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
-        rgb = cmap[arr[j]];
-        data[i    ] = rgb[0];
-        data[i + 1] = rgb[1];
-        data[i + 2] = rgb[2];
-        data[i + 3] = 255; // Set Alpha
-    }
-    c_ctx.putImageData(img, x - viewport.x, y - viewport.y);
-};
-
-cmapImageFill = function(x, y, width, height, arr, offset) {
-    var i, j, sx = 0, sy = 0, cmap;
-    cmap = conf.colourMap;
-    for (i=0, j=offset; i < (width * height); i+=1, j+=1) {
-        that.fillRect(x+sx, y+sy, 1, 1, [arr[j]]);
-        sx += 1;
-        if ((sx % width) === 0) {
-            sx = 0;
-            sy += 1;
-        }
-    }
-};
-
-
-that.blitImage = function(x, y, width, height, arr, offset) {
-    if (conf.true_color) {
-        c_rgbxImage(x, y, width, height, arr, offset);
-    } else {
-        c_cmapImage(x, y, width, height, arr, offset);
-    }
-};
-
-that.blitStringImage = function(str, x, y) {
-    var img = new Image();
-    img.onload = function () {
-        c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
-    };
-    img.src = str;
-};
+            var vp = this._viewportLoc;
+            if (vp.w !== width || vp.h !== height) {
+
+                var cr = this._cleanRect;
+
+                if (width < vp.w &&  cr.x2 > vp.x + width - 1) {
+                    cr.x2 = vp.x + width - 1;
+                }
+
+                if (height < vp.h &&  cr.y2 > vp.y + height - 1) {
+                    cr.y2 = vp.y + height - 1;
+                }
+
+                if (this.fbuClip()) {
+                    // clipping
+                    vp.w = window.innerWidth;
+                    var cb = document.getElementById('noVNC-control-bar');
+                    var controlbar_h = (cb !== null) ? cb.offsetHeight : 0;
+                    vp.h = window.innerHeight - controlbar_h - 5;
+                } else {
+                    // scrollbars
+                    vp.w = width;
+                    vp.h = height;
+                }
+
+                var saveImg = null;
+                var canvas = this._target;
+                if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) {
+                    var img_width = canvas.width < vp.w ? canvas.width : vp.w;
+                    var img_height = canvas.height < vp.h ? canvas.height : vp.h;
+                    saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height);
+                }
+
+                canvas.width = vp.w;
+                canvas.height = vp.h;
+
+                if (saveImg) {
+                    this._drawCtx.putImageData(saveImg, 0, 0);
+                }
+            }
+        },
+
+        // Return a map of clean and dirty areas of the viewport and reset the
+        // tracking of clean and dirty areas
+        //
+        // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h},
+        //            'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] }
+        getCleanDirtyReset: function () {
+            var vp = this._viewportLoc;
+            var cr = this._cleanRect;
+
+            var cleanBox = { 'x': cr.x1, 'y': cr.y1,
+                             'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 };
+
+            var dirtyBoxes = [];
+            if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) {
+                // Whole viewport is dirty
+                dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h });
+            } else {
+                // Redraw dirty regions
+                var vx2 = vp.x + vp.w - 1;
+                var vy2 = vp.y + vp.h - 1;
+
+                if (vp.x < cr.x1) {
+                    // left side dirty region
+                    dirtyBoxes.push({'x': vp.x, 'y': vp.y,
+                                     'w': cr.x1 - vp.x + 1, 'h': vp.h});
+                }
+                if (vx2 > cr.x2) {
+                    // right side dirty region
+                    dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y,
+                                     'w': vx2 - cr.x2, 'h': vp.h});
+                }
+                if(vp.y < cr.y1) {
+                    // top/middle dirty region
+                    dirtyBoxes.push({'x': cr.x1, 'y': vp.y,
+                                     'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y});
+                }
+                if (vy2 > cr.y2) {
+                    // bottom/middle dirty region
+                    dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1,
+                                     'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2});
+                }
+            }
 
-that.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
-    if (conf.cursor_uri === false) {
-        Util.Warn("changeCursor called but no cursor data URI support");
-        return;
-    }
+            this._cleanRect = {'x1': vp.x, 'y1': vp.y,
+                               'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1};
 
-    if (conf.true_color) {
-        changeCursor(conf.target, pixels, mask, hotx, hoty, w, h);
-    } else {
-        changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap);
-    }
-};
+            return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
+        },
 
-that.defaultCursor = function() {
-    conf.target.style.cursor = "default";
-};
+        absX: function (x) {
+            return x + this._viewportLoc.x;
+        },
 
-return constructor();  // Return the public API interface
+        absY: function (y) {
+            return y + this._viewportLoc.y;
+        },
 
-}  // End of Display()
+        resize: function (width, height) {
+            this._prevDrawStyle = "";
 
+            this._fb_width = width;
+            this._fb_height = height;
 
-/* Set CSS cursor property using data URI encoded cursor file */
-function changeCursor(target, pixels, mask, hotx, hoty, w, h, cmap) {
-    "use strict";
-    var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y;
-    //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h);
-    
-    // Push multi-byte little-endian values
-    cur.push16le = function (num) {
-        this.push((num     ) & 0xFF,
-                  (num >> 8) & 0xFF  );
-    };
-    cur.push32le = function (num) {
-        this.push((num      ) & 0xFF,
-                  (num >>  8) & 0xFF,
-                  (num >> 16) & 0xFF,
-                  (num >> 24) & 0xFF  );
-    };
+            this._rescale(this._scale);
+
+            this.viewportChangeSize();
+        },
+
+        clear: function () {
+            if (this._logo) {
+                this.resize(this._logo.width, this._logo.height);
+                this.blitStringImage(this._logo.data, 0, 0);
+            } else {
+                if (Util.Engine.trident === 6) {
+                    // NB(directxman12): there's a bug in IE10 where we can fail to actually
+                    //                   clear the canvas here because of the resize.
+                    //                   Clearing the current viewport first fixes the issue
+                    this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h);
+                }
+                this.resize(240, 20);
+                this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h);
+            }
+
+            this._renderQ = [];
+        },
+
+        fillRect: function (x, y, width, height, color) {
+            this._setFillColor(color);
+            this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height);
+        },
+
+        copyImage: function (old_x, old_y, new_x, new_y, w, h) {
+            var x1 = old_x - this._viewportLoc.x;
+            var y1 = old_y - this._viewportLoc.y;
+            var x2 = new_x - this._viewportLoc.x;
+            var y2 = new_y - this._viewportLoc.y;
+
+            this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h);
+        },
+
+        // start updating a tile
+        startTile: function (x, y, width, height, color) {
+            this._tile_x = x;
+            this._tile_y = y;
+            if (width === 16 && height === 16) {
+                this._tile = this._tile16x16;
+            } else {
+                this._tile = this._drawCtx.createImageData(width, height);
+            }
+
+            if (this._prefer_js) {
+                var bgr;
+                if (this._true_color) {
+                    bgr = color;
+                } else {
+                    bgr = this._colourMap[color[0]];
+                }
+                var red = bgr[2];
+                var green = bgr[1];
+                var blue = bgr[0];
+
+                var data = this._tile.data;
+                for (var i = 0; i < width * height * 4; i += 4) {
+                    data[i] = red;
+                    data[i + 1] = green;
+                    data[i + 2] = blue;
+                    data[i + 3] = 255;
+                }
+            } else {
+                this.fillRect(x, y, width, height, color);
+            }
+        },
+
+        // update sub-rectangle of the current tile
+        subTile: function (x, y, w, h, color) {
+            if (this._prefer_js) {
+                var bgr;
+                if (this._true_color) {
+                    bgr = color;
+                } else {
+                    bgr = this._colourMap[color[0]];
+                }
+                var red = bgr[2];
+                var green = bgr[1];
+                var blue = bgr[0];
+                var xend = x + w;
+                var yend = y + h;
+
+                var data = this._tile.data;
+                var width = this._tile.width;
+                for (var j = y; j < yend; j++) {
+                    for (var i = x; i < xend; i++) {
+                        var p = (i + (j * width)) * 4;
+                        data[p] = red;
+                        data[p + 1] = green;
+                        data[p + 2] = blue;
+                        data[p + 3] = 255;
+                    }
+                }
+            } else {
+                this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color);
+            }
+        },
+
+        // draw the current tile to the screen
+        finishTile: function () {
+            if (this._prefer_js) {
+                this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x,
+                                           this._tile_y - this._viewportLoc.y);
+            }
+            // else: No-op -- already done by setSubTile
+        },
 
-    IHDRsz = 40;
-    RGBsz = w * h * 4;
-    XORsz = Math.ceil( (w * h) / 8.0 );
-    ANDsz = Math.ceil( (w * h) / 8.0 );
-
-    // Main header
-    cur.push16le(0);      // 0: Reserved
-    cur.push16le(2);      // 2: .CUR type
-    cur.push16le(1);      // 4: Number of images, 1 for non-animated ico
-
-    // Cursor #1 header (ICONDIRENTRY)
-    cur.push(w);          // 6: width
-    cur.push(h);          // 7: height
-    cur.push(0);          // 8: colors, 0 -> true-color
-    cur.push(0);          // 9: reserved
-    cur.push16le(hotx);   // 10: hotspot x coordinate
-    cur.push16le(hoty);   // 12: hotspot y coordinate
-    cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
-                          // 14: cursor data byte size
-    cur.push32le(22);     // 18: offset of cursor data in the file
-
-
-    // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
-    cur.push32le(IHDRsz); // 22: Infoheader size
-    cur.push32le(w);      // 26: Cursor width
-    cur.push32le(h*2);    // 30: XOR+AND height
-    cur.push16le(1);      // 34: number of planes
-    cur.push16le(32);     // 36: bits per pixel
-    cur.push32le(0);      // 38: Type of compression
-
-    cur.push32le(XORsz + ANDsz); // 43: Size of Image
-                                 // Gimp leaves this as 0
-
-    cur.push32le(0);      // 46: reserved
-    cur.push32le(0);      // 50: reserved
-    cur.push32le(0);      // 54: reserved
-    cur.push32le(0);      // 58: reserved
-
-    // 62: color data (RGBQUAD icColors[])
-    for (y = h-1; y >= 0; y -= 1) {
-        for (x = 0; x < w; x += 1) {
-            idx = y * Math.ceil(w / 8) + Math.floor(x/8);
-            alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
-
-            if (cmap) {
-                idx = (w * y) + x;
-                rgb = cmap[pixels[idx]];
-                cur.push(rgb[2]);          // blue
-                cur.push(rgb[1]);          // green
-                cur.push(rgb[0]);          // red
-                cur.push(alpha);           // alpha
+        blitImage: function (x, y, width, height, arr, offset) {
+            if (this._true_color) {
+                this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
             } else {
-                idx = ((w * y) + x) * 4;
-                cur.push(pixels[idx + 2]); // blue
-                cur.push(pixels[idx + 1]); // green
-                cur.push(pixels[idx    ]); // red
-                cur.push(alpha);           // alpha
+                this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
+            }
+        },
+
+        blitRgbImage: function (x, y , width, height, arr, offset) {
+            if (this._true_color) {
+                this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
+            } else {
+                // probably wrong?
+                this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset);
+            }
+        },
+
+        blitStringImage: function (str, x, y) {
+            var img = new Image();
+            img.onload = function () {
+                this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y);
+            }.bind(this);
+            img.src = str;
+            return img; // for debugging purposes
+        },
+
+        // wrap ctx.drawImage but relative to viewport
+        drawImage: function (img, x, y) {
+            this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y);
+        },
+
+        renderQ_push: function (action) {
+            this._renderQ.push(action);
+            if (this._renderQ.length === 1) {
+                // If this can be rendered immediately it will be, otherwise
+                // the scanner will start polling the queue (every
+                // requestAnimationFrame interval)
+                this._scan_renderQ();
             }
+        },
+
+        changeCursor: function (pixels, mask, hotx, hoty, w, h) {
+            if (this._cursor_uri === false) {
+                Util.Warn("changeCursor called but no cursor data URI support");
+                return;
+            }
+
+            if (this._true_color) {
+                Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
+            } else {
+                Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap);
+            }
+        },
+
+        defaultCursor: function () {
+            this._target.style.cursor = "default";
+        },
+
+        disableLocalCursor: function () {
+            this._target.style.cursor = "none";
+        },
+
+        fbuClip: function () {
+            var cb = document.getElementById('noVNC-control-bar');
+            var controlbar_h = (cb !== null) ? cb.offsetHeight : 0;
+            return (this._viewport &&
+                    (this._fb_width > window.innerWidth
+                     || this._fb_height > window.innerHeight - controlbar_h - 5));
+        },
+
+        // Overridden getters/setters
+        get_context: function () {
+            return this._drawCtx;
+        },
+
+        set_scale: function (scale) {
+            this._rescale(scale);
+        },
+
+        set_width: function (w) {
+            this._fb_width = w;
+        },
+        get_width: function () {
+            return this._fb_width;
+        },
+
+        set_height: function (h) {
+            this._fb_height =  h;
+        },
+        get_height: function () {
+            return this._fb_height;
+        },
+
+        // Private Methods
+        _rescale: function (factor) {
+            var canvas = this._target;
+            var properties = ['transform', 'WebkitTransform', 'MozTransform'];
+            var transform_prop;
+            while ((transform_prop = properties.shift())) {
+                if (typeof canvas.style[transform_prop] !== 'undefined') {
+                    break;
+                }
+            }
+
+            if (transform_prop === null) {
+                Util.Debug("No scaling support");
+                return;
+            }
+
+            if (typeof(factor) === "undefined") {
+                factor = this._scale;
+            } else if (factor > 1.0) {
+                factor = 1.0;
+            } else if (factor < 0.1) {
+                factor = 0.1;
+            }
+
+            if (this._scale === factor) {
+                return;
+            }
+
+            this._scale = factor;
+            var x = canvas.width - (canvas.width * factor);
+            var y = canvas.height - (canvas.height * factor);
+            canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)';
+        },
+
+        _setFillColor: function (color) {
+            var bgr;
+            if (this._true_color) {
+                bgr = color;
+            } else {
+                bgr = this._colourMap[color[0]];
+            }
+
+            var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')';
+            if (newStyle !== this._prevDrawStyle) {
+                this._drawCtx.fillStyle = newStyle;
+                this._prevDrawStyle = newStyle;
+            }
+        },
+
+        _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) {
+            var img = this._drawCtx.createImageData(width, height);
+            var data = img.data;
+            for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
+                data[i]     = arr[j];
+                data[i + 1] = arr[j + 1];
+                data[i + 2] = arr[j + 2];
+                data[i + 3] = 255;  // Alpha
+            }
+            this._drawCtx.putImageData(img, x - vx, y - vy);
+        },
+
+        _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) {
+            var img = this._drawCtx.createImageData(width, height);
+            var data = img.data;
+            for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
+                data[i]     = arr[j + 2];
+                data[i + 1] = arr[j + 1];
+                data[i + 2] = arr[j];
+                data[i + 3] = 255;  // Alpha
+            }
+            this._drawCtx.putImageData(img, x - vx, y - vy);
+        },
+
+        _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) {
+            var img = this._drawCtx.createImageData(width, height);
+            var data = img.data;
+            var cmap = this._colourMap;
+            for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) {
+                var bgr = cmap[arr[j]];
+                data[i]     = bgr[2];
+                data[i + 1] = bgr[1];
+                data[i + 2] = bgr[0];
+                data[i + 3] = 255;  // Alpha
+            }
+            this._drawCtx.putImageData(img, x - vx, y - vy);
+        },
+
+        _scan_renderQ: function () {
+            var ready = true;
+            while (ready && this._renderQ.length > 0) {
+                var a = this._renderQ[0];
+                switch (a.type) {
+                    case 'copy':
+                        this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
+                        break;
+                    case 'fill':
+                        this.fillRect(a.x, a.y, a.width, a.height, a.color);
+                        break;
+                    case 'blit':
+                        this.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
+                        break;
+                    case 'blitRgb':
+                        this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
+                        break;
+                    case 'img':
+                        if (a.img.complete) {
+                            this.drawImage(a.img, a.x, a.y);
+                        } else {
+                            // We need to wait for this image to 'load'
+                            // to keep things in-order
+                            ready = false;
+                        }
+                        break;
+                }
+
+                if (ready) {
+                    this._renderQ.shift();
+                }
+            }
+
+            if (this._renderQ.length > 0) {
+                requestAnimFrame(this._scan_renderQ.bind(this));
+            }
+        },
+    };
+
+    Util.make_properties(Display, [
+        ['target', 'wo', 'dom'],       // Canvas element for rendering
+        ['context', 'ro', 'raw'],      // Canvas 2D context for rendering (read-only)
+        ['logo', 'rw', 'raw'],         // Logo to display when cleared: {"width": w, "height": h, "data": data}
+        ['true_color', 'rw', 'bool'],  // Use true-color pixel data
+        ['colourMap', 'rw', 'arr'],    // Colour map array (when not true-color)
+        ['scale', 'rw', 'float'],      // Display area scale factor 0.0 - 1.0
+        ['viewport', 'rw', 'bool'],    // Use a viewport set with viewportChange()
+        ['width', 'rw', 'int'],        // Display area width
+        ['height', 'rw', 'int'],       // Display area height
+
+        ['render_mode', 'ro', 'str'],  // Canvas rendering mode (read-only)
+
+        ['prefer_js', 'rw', 'str'],    // Prefer Javascript over canvas methods
+        ['cursor_uri', 'rw', 'raw']    // Can we render cursor using data URI
+    ]);
+
+    // Class Methods
+    Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) {
+        var w = w0;
+        var h = h0;
+        if (h < w) {
+            h = w;  // increase h to make it square
+        } else {
+            w = h;  // increase w to make it square
         }
-    }
 
-    // XOR/bitmask data (BYTE icXOR[])
-    // (ignored, just needs to be right size)
-    for (y = 0; y < h; y += 1) {
-        for (x = 0; x < Math.ceil(w / 8); x += 1) {
-            cur.push(0x00);
+        var cur = [];
+
+        // Push multi-byte little-endian values
+        cur.push16le = function (num) {
+            this.push(num & 0xFF, (num >> 8) & 0xFF);
+        };
+        cur.push32le = function (num) {
+            this.push(num & 0xFF,
+                      (num >> 8) & 0xFF,
+                      (num >> 16) & 0xFF,
+                      (num >> 24) & 0xFF);
+        };
+
+        var IHDRsz = 40;
+        var RGBsz = w * h * 4;
+        var XORsz = Math.ceil((w * h) / 8.0);
+        var ANDsz = Math.ceil((w * h) / 8.0);
+
+        cur.push16le(0);        // 0: Reserved
+        cur.push16le(2);        // 2: .CUR type
+        cur.push16le(1);        // 4: Number of images, 1 for non-animated ico
+
+        // Cursor #1 header (ICONDIRENTRY)
+        cur.push(w);            // 6: width
+        cur.push(h);            // 7: height
+        cur.push(0);            // 8: colors, 0 -> true-color
+        cur.push(0);            // 9: reserved
+        cur.push16le(hotx);     // 10: hotspot x coordinate
+        cur.push16le(hoty);     // 12: hotspot y coordinate
+        cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
+                                // 14: cursor data byte size
+        cur.push32le(22);       // 18: offset of cursor data in the file
+
+        // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
+        cur.push32le(IHDRsz);   // 22: InfoHeader size
+        cur.push32le(w);        // 26: Cursor width
+        cur.push32le(h * 2);    // 30: XOR+AND height
+        cur.push16le(1);        // 34: number of planes
+        cur.push16le(32);       // 36: bits per pixel
+        cur.push32le(0);        // 38: Type of compression
+
+        cur.push32le(XORsz + ANDsz);
+                                // 42: Size of Image
+        cur.push32le(0);        // 46: reserved
+        cur.push32le(0);        // 50: reserved
+        cur.push32le(0);        // 54: reserved
+        cur.push32le(0);        // 58: reserved
+
+        // 62: color data (RGBQUAD icColors[])
+        var y, x;
+        for (y = h - 1; y >= 0; y--) {
+            for (x = 0; x < w; x++) {
+                if (x >= w0 || y >= h0) {
+                    cur.push(0);  // blue
+                    cur.push(0);  // green
+                    cur.push(0);  // red
+                    cur.push(0);  // alpha
+                } else {
+                    var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8);
+                    var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
+                    if (cmap) {
+                        idx = (w0 * y) + x;
+                        var rgb = cmap[pixels[idx]];
+                        cur.push(rgb[2]);  // blue
+                        cur.push(rgb[1]);  // green
+                        cur.push(rgb[0]);  // red
+                        cur.push(alpha);   // alpha
+                    } else {
+                        idx = ((w0 * y) + x) * 4;
+                        cur.push(pixels[idx + 2]); // blue
+                        cur.push(pixels[idx + 1]); // green
+                        cur.push(pixels[idx]);     // red
+                        cur.push(alpha);           // alpha
+                    }
+                }
+            }
         }
-    }
 
-    // AND/bitmask data (BYTE icAND[])
-    // (ignored, just needs to be right size)
-    for (y = 0; y < h; y += 1) {
-        for (x = 0; x < Math.ceil(w / 8); x += 1) {
-            cur.push(0x00);
+        // XOR/bitmask data (BYTE icXOR[])
+        // (ignored, just needs to be the right size)
+        for (y = 0; y < h; y++) {
+            for (x = 0; x < Math.ceil(w / 8); x++) {
+                cur.push(0);
+            }
         }
-    }
 
-    url = "data:image/x-icon;base64," + Base64.encode(cur);
-    target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
-    //Util.Debug("<< changeCursor, cur.length: " + cur.length);
-}
+        // AND/bitmask data (BYTE icAND[])
+        // (ignored, just needs to be the right size)
+        for (y = 0; y < h; y++) {
+            for (x = 0; x < Math.ceil(w / 8); x++) {
+                cur.push(0);
+            }
+        }
+
+        var url = 'data:image/x-icon;base64,' + Base64.encode(cur);
+        target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
+    };
+})();