569 lines
17 KiB
JavaScript
569 lines
17 KiB
JavaScript
|
/*
|
||
|
* noVNC: HTML5 VNC client
|
||
|
* Copyright (C) 2011 Joel Martin
|
||
|
* Licensed under LGPL-3 (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,
|
||
|
|
||
|
c_imageData, c_rgbxImage, c_cmapImage,
|
||
|
|
||
|
// Predefine function variables (jslint)
|
||
|
imageDataCreate, imageDataGet, rgbxImageData, cmapImageData,
|
||
|
rgbxImageFill, cmapImageFill, setFillColor, rescale, flush,
|
||
|
|
||
|
c_width = 0,
|
||
|
c_height = 0,
|
||
|
|
||
|
c_prevStyle = "",
|
||
|
|
||
|
c_webkit_bug = false,
|
||
|
c_flush_timer = null;
|
||
|
|
||
|
// 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'],
|
||
|
['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, c_height); };
|
||
|
that.get_width = function() { return c_width; };
|
||
|
|
||
|
that.set_height = function (val) { that.resize(c_width, val); };
|
||
|
that.get_height = function() { return c_height; };
|
||
|
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
//
|
||
|
// Private functions
|
||
|
//
|
||
|
|
||
|
// Create the public API interface
|
||
|
function constructor() {
|
||
|
Util.Debug(">> Display.constructor");
|
||
|
|
||
|
var c, func, imgTest, tval, 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();
|
||
|
|
||
|
/*
|
||
|
* 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;
|
||
|
}
|
||
|
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 (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)";
|
||
|
};
|
||
|
|
||
|
// 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) {
|
||
|
var c = conf.target;
|
||
|
|
||
|
c_prevStyle = "";
|
||
|
|
||
|
c.width = width;
|
||
|
c.height = height;
|
||
|
|
||
|
c_width = c.offsetWidth;
|
||
|
c_height = c.offsetHeight;
|
||
|
|
||
|
rescale(conf.scale);
|
||
|
};
|
||
|
|
||
|
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, c_width, c_height);
|
||
|
}
|
||
|
|
||
|
// 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, y, width, height);
|
||
|
};
|
||
|
|
||
|
that.copyImage = function(old_x, old_y, new_x, new_y, width, height) {
|
||
|
c_ctx.drawImage(conf.target, old_x, old_y, width, height,
|
||
|
new_x, new_y, width, 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;
|
||
|
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, 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, 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, y); };
|
||
|
img.src = str;
|
||
|
};
|
||
|
|
||
|
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);
|
||
|
}
|