diff options
Diffstat (limited to 'webclients/novnc/include/display.js')
-rw-r--r-- | webclients/novnc/include/display.js | 671 |
1 files changed, 671 insertions, 0 deletions
diff --git a/webclients/novnc/include/display.js b/webclients/novnc/include/display.js new file mode 100644 index 0000000..2cf262d --- /dev/null +++ b/webclients/novnc/include/display.js @@ -0,0 +1,671 @@ +/* + * 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, + + // Predefine function variables (jslint) + imageDataGet, rgbxImageData, cmapImageData, + setFillColor, rescale, + + // 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 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 +// + +// 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); + } + + // 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, rgb, 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) { + 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; + 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, rgb, red, green, blue, width, j, i, xend, yend; + if (conf.prefer_js) { + data = tile.data; + width = tile.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; + 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 +}; + +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)) { + // 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 ]; + 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); +}; + +cmapImageData = function(x, y, width, height, arr, offset) { + var img, i, j, data, rgb, 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) { + 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); +}; + +that.blitImage = function(x, y, width, height, arr, offset) { + if (conf.true_color) { + rgbxImageData(x, y, width, height, arr, offset); + } else { + 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; +}; + +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); +} |