mirror of
https://github.com/novnc/noVNC.git
synced 2026-05-26 07:08:06 +00:00
The MPL 2.0 license is a "file-level" copyleft license vs the
"project-level" nature of the L/GPL. The intention of noVNC has
always been that it should be easy to incorporate into existing
projects and sites whether free/open or proprietary/commercial. The MPL
2.0 is designed for this sort of combination project but still
requires that any distributed modifications to noVNC source files must
also be published under the same license.
In addition, the MPL 2.0 allows the code to be used in L/GPL projects
(the secondary license clause). This means that any projects that are
already incorporating noVNC should not be impacted by this change and
in fact it should clarify the licensing situation (the exact
application of the L/GPL to web applications and interpreted code is
somewhat ambiguous).
The HTML, CSS, image and font files continue to be under more
permissive licenses (see LICENSE.txt). The included websockify python
code remains under a LGPLv3 license although the include/websock.js
file from the websockify component is now under MPL 2.0 as well.
Permission was received from other noVNC authors to make this change to their
code license on the following dates:
- Chris Gordon (UI): Jun 24, 2012
- Antoine Mercadal (DOM,*util.js): Oct 10, 2012
- William Lightning (UltraVNC repeater): Oct 10, 2012
- Mike Tinglof (tight encoding): Oct 15, 2012
758 lines
22 KiB
JavaScript
758 lines
22 KiB
JavaScript
/*
|
|
* noVNC: HTML5 VNC client
|
|
* 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 */
|
|
/*global Util, Base64, changeCursor */
|
|
|
|
function Display(defaults) {
|
|
"use strict";
|
|
|
|
var that = {}, // Public API methods
|
|
conf = {}, // Configuration attributes
|
|
|
|
// Private Display namespace variables
|
|
c_ctx = null,
|
|
c_forceCanvas = false,
|
|
|
|
// Queued drawing actions for in-order rendering
|
|
renderQ = [],
|
|
|
|
// Predefine function variables (jslint)
|
|
imageDataGet, rgbImageData, bgrxImageData, cmapImageData,
|
|
setFillColor, rescale, scan_renderQ,
|
|
|
|
// 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},
|
|
|
|
c_prevStyle = "",
|
|
tile = null,
|
|
tile16x16 = null,
|
|
tile_x = 0,
|
|
tile_y = 0;
|
|
|
|
|
|
// 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'],
|
|
|
|
['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'],
|
|
|
|
['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'],
|
|
['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI']
|
|
]);
|
|
|
|
// Override some specific getters/setters
|
|
that.get_context = function () { return c_ctx; };
|
|
|
|
that.set_scale = function(scale) { rescale(scale); };
|
|
|
|
that.set_width = function (val) { that.resize(val, fb_height); };
|
|
that.get_width = function() { return fb_width; };
|
|
|
|
that.set_height = function (val) { that.resize(fb_width, val); };
|
|
that.get_height = function() { return fb_height; };
|
|
|
|
|
|
|
|
//
|
|
// Private functions
|
|
//
|
|
|
|
// Create the public API interface
|
|
function constructor() {
|
|
Util.Debug(">> Display.constructor");
|
|
|
|
var c, func, i, curDat, curSave,
|
|
has_imageData = false, UE = Util.Engine;
|
|
|
|
if (! conf.target) { throw("target must be set"); }
|
|
|
|
if (typeof conf.target === 'string') {
|
|
throw("target must be a DOM element");
|
|
}
|
|
|
|
c = conf.target;
|
|
|
|
if (! c.getContext) { throw("no getContext method"); }
|
|
|
|
if (! c_ctx) { c_ctx = c.getContext('2d'); }
|
|
|
|
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); }
|
|
|
|
that.clear();
|
|
|
|
// Check canvas features
|
|
if ('createImageData' in c_ctx) {
|
|
conf.render_mode = "canvas rendering";
|
|
} else {
|
|
throw("Canvas does not support createImageData");
|
|
}
|
|
if (conf.prefer_js === null) {
|
|
Util.Info("Prefering javascript operations");
|
|
conf.prefer_js = true;
|
|
}
|
|
|
|
// Initialize cached tile imageData
|
|
tile16x16 = c_ctx.createImageData(16, 16);
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
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)";
|
|
};
|
|
|
|
setFillColor = function(color) {
|
|
var bgr, newStyle;
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")";
|
|
if (newStyle !== c_prevStyle) {
|
|
c_ctx.fillStyle = newStyle;
|
|
c_prevStyle = newStyle;
|
|
}
|
|
};
|
|
|
|
|
|
//
|
|
// Public API interface functions
|
|
//
|
|
|
|
// Shift and/or resize the visible viewport
|
|
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;
|
|
|
|
|
|
if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) {
|
|
saveImg = c_ctx.getImageData(0, 0,
|
|
(c.width < v.w) ? c.width : v.w,
|
|
(c.height < v.h) ? c.height : v.h);
|
|
}
|
|
|
|
c.width = v.w;
|
|
c.height = v.h;
|
|
|
|
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;
|
|
};
|
|
|
|
|
|
// 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}, ...]}
|
|
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};
|
|
};
|
|
|
|
// Translate viewport coordinates to absolute coordinates
|
|
that.absX = function(x) {
|
|
return x + viewport.x;
|
|
};
|
|
that.absY = function(y) {
|
|
return y + viewport.y;
|
|
};
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
renderQ = [];
|
|
|
|
// 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);
|
|
};
|
|
|
|
|
|
// Start updating a tile
|
|
that.startTile = function(x, y, width, height, color) {
|
|
var data, bgr, red, green, blue, i;
|
|
tile_x = x;
|
|
tile_y = y;
|
|
if ((width === 16) && (height === 16)) {
|
|
tile = tile16x16;
|
|
} else {
|
|
tile = c_ctx.createImageData(width, height);
|
|
}
|
|
data = tile.data;
|
|
if (conf.prefer_js) {
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
red = bgr[2];
|
|
green = bgr[1];
|
|
blue = bgr[0];
|
|
for (i = 0; i < (width * height * 4); i+=4) {
|
|
data[i ] = red;
|
|
data[i + 1] = green;
|
|
data[i + 2] = blue;
|
|
data[i + 3] = 255;
|
|
}
|
|
} else {
|
|
that.fillRect(x, y, width, height, color);
|
|
}
|
|
};
|
|
|
|
// Update sub-rectangle of the current tile
|
|
that.subTile = function(x, y, w, h, color) {
|
|
var data, p, bgr, red, green, blue, width, j, i, xend, yend;
|
|
if (conf.prefer_js) {
|
|
data = tile.data;
|
|
width = tile.width;
|
|
if (conf.true_color) {
|
|
bgr = color;
|
|
} else {
|
|
bgr = conf.colourMap[color[0]];
|
|
}
|
|
red = bgr[2];
|
|
green = bgr[1];
|
|
blue = bgr[0];
|
|
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;
|
|
data[p + 3] = 255;
|
|
}
|
|
}
|
|
} else {
|
|
that.fillRect(tile_x + x, tile_y + y, w, h, color);
|
|
}
|
|
};
|
|
|
|
// Draw the current tile to the screen
|
|
that.finishTile = function() {
|
|
if (conf.prefer_js) {
|
|
c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y);
|
|
}
|
|
// else: No-op, if not prefer_js then already done by setSubTile
|
|
};
|
|
|
|
rgbImageData = 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)) {
|
|
// Skipping because outside of viewport
|
|
return;
|
|
}
|
|
*/
|
|
img = c_ctx.createImageData(width, height);
|
|
data = img.data;
|
|
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) {
|
|
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);
|
|
};
|
|
|
|
bgrxImageData = 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)) {
|
|
// Skipping because outside of viewport
|
|
return;
|
|
}
|
|
*/
|
|
img = c_ctx.createImageData(width, height);
|
|
data = img.data;
|
|
for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) {
|
|
data[i ] = arr[j + 2];
|
|
data[i + 1] = arr[j + 1];
|
|
data[i + 2] = arr[j ];
|
|
data[i + 3] = 255; // Set Alpha
|
|
}
|
|
c_ctx.putImageData(img, x - v.x, y - v.y);
|
|
};
|
|
|
|
cmapImageData = function(x, y, width, height, arr, offset) {
|
|
var img, i, j, data, bgr, cmap;
|
|
img = c_ctx.createImageData(width, height);
|
|
data = img.data;
|
|
cmap = conf.colourMap;
|
|
for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) {
|
|
bgr = cmap[arr[j]];
|
|
data[i ] = bgr[2];
|
|
data[i + 1] = bgr[1];
|
|
data[i + 2] = bgr[0];
|
|
data[i + 3] = 255; // Set Alpha
|
|
}
|
|
c_ctx.putImageData(img, x - viewport.x, y - viewport.y);
|
|
};
|
|
|
|
that.blitImage = function(x, y, width, height, arr, offset) {
|
|
if (conf.true_color) {
|
|
bgrxImageData(x, y, width, height, arr, offset);
|
|
} else {
|
|
cmapImageData(x, y, width, height, arr, offset);
|
|
}
|
|
};
|
|
|
|
that.blitRgbImage = function(x, y, width, height, arr, offset) {
|
|
if (conf.true_color) {
|
|
rgbImageData(x, y, width, height, arr, offset);
|
|
} else {
|
|
// prolly wrong...
|
|
cmapImageData(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;
|
|
};
|
|
|
|
// Wrap ctx.drawImage but relative to viewport
|
|
that.drawImage = function(img, x, y) {
|
|
c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
|
|
};
|
|
|
|
that.renderQ_push = function(action) {
|
|
renderQ.push(action);
|
|
if (renderQ.length === 1) {
|
|
// If this can be rendered immediately it will be, otherwise
|
|
// the scanner will start polling the queue (every
|
|
// requestAnimationFrame interval)
|
|
scan_renderQ();
|
|
}
|
|
};
|
|
|
|
scan_renderQ = function() {
|
|
var a, ready = true;
|
|
while (ready && renderQ.length > 0) {
|
|
a = renderQ[0];
|
|
switch (a.type) {
|
|
case 'copy':
|
|
that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height);
|
|
break;
|
|
case 'fill':
|
|
that.fillRect(a.x, a.y, a.width, a.height, a.color);
|
|
break;
|
|
case 'blit':
|
|
that.blitImage(a.x, a.y, a.width, a.height, a.data, 0);
|
|
break;
|
|
case 'blitRgb':
|
|
that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0);
|
|
break;
|
|
case 'img':
|
|
if (a.img.complete) {
|
|
that.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) {
|
|
a = renderQ.shift();
|
|
}
|
|
}
|
|
if (renderQ.length > 0) {
|
|
requestAnimFrame(scan_renderQ);
|
|
}
|
|
};
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
that.defaultCursor = function() {
|
|
conf.target.style.cursor = "default";
|
|
};
|
|
|
|
return constructor(); // Return the public API interface
|
|
|
|
} // End of Display()
|
|
|
|
|
|
/* 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 );
|
|
};
|
|
|
|
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
|
|
} 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
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
url = "data:image/x-icon;base64," + Base64.encode(cur);
|
|
target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default";
|
|
//Util.Debug("<< changeCursor, cur.length: " + cur.length);
|
|
}
|