diff --git a/data/html5_remote_desktop/HaikuRemoteDesktop.html b/data/html5_remote_desktop/HaikuRemoteDesktop.html
new file mode 100644
index 0000000000..2b488ecf40
--- /dev/null
+++ b/data/html5_remote_desktop/HaikuRemoteDesktop.html
@@ -0,0 +1,76 @@
+
+
+
+ Haiku Remote Desktop
+
+
+
+
+
+
+
diff --git a/data/html5_remote_desktop/HaikuRemoteDesktop.js b/data/html5_remote_desktop/HaikuRemoteDesktop.js
new file mode 100644
index 0000000000..1d7deb9b38
--- /dev/null
+++ b/data/html5_remote_desktop/HaikuRemoteDesktop.js
@@ -0,0 +1,2241 @@
+'use strict';
+
+const RP_INIT_CONNECTION = 1;
+const RP_UPDATE_DISPLAY_MODE = 2;
+const RP_CLOSE_CONNECTION = 3;
+const RP_GET_SYSTEM_PALETTE = 4;
+const RP_GET_SYSTEM_PALETTE_RESULT = 5;
+
+const RP_CREATE_STATE = 20;
+const RP_DELETE_STATE = 21;
+const RP_ENABLE_SYNC_DRAWING = 22;
+const RP_DISABLE_SYNC_DRAWING = 23;
+const RP_INVALIDATE_RECT = 24;
+const RP_INVALIDATE_REGION = 25;
+
+const RP_SET_OFFSETS = 40;
+const RP_SET_HIGH_COLOR = 41;
+const RP_SET_LOW_COLOR = 42;
+const RP_SET_PEN_SIZE = 43;
+const RP_SET_STROKE_MODE = 44;
+const RP_SET_BLENDING_MODE = 45;
+const RP_SET_PATTERN = 46;
+const RP_SET_DRAWING_MODE = 47;
+const RP_SET_FONT = 48;
+const RP_SET_TRANSFORM = 49;
+
+const RP_CONSTRAIN_CLIPPING_REGION = 60;
+const RP_COPY_RECT_NO_CLIPPING = 61;
+const RP_INVERT_RECT = 62;
+const RP_DRAW_BITMAP = 63;
+const RP_DRAW_BITMAP_RECTS = 64;
+
+const RP_STROKE_ARC = 80;
+const RP_STROKE_BEZIER = 81;
+const RP_STROKE_ELLIPSE = 82;
+const RP_STROKE_POLYGON = 83;
+const RP_STROKE_RECT = 84;
+const RP_STROKE_ROUND_RECT = 85;
+const RP_STROKE_SHAPE = 86;
+const RP_STROKE_TRIANGLE = 87;
+const RP_STROKE_LINE = 88;
+const RP_STROKE_LINE_ARRAY = 89;
+
+const RP_FILL_ARC = 100;
+const RP_FILL_BEZIER = 101;
+const RP_FILL_ELLIPSE = 102;
+const RP_FILL_POLYGON = 103;
+const RP_FILL_RECT = 104;
+const RP_FILL_ROUND_RECT = 105;
+const RP_FILL_SHAPE = 106;
+const RP_FILL_TRIANGLE = 107;
+const RP_FILL_REGION = 108;
+
+const RP_FILL_ARC_GRADIENT = 120;
+const RP_FILL_BEZIER_GRADIENT = 121;
+const RP_FILL_ELLIPSE_GRADIENT = 122;
+const RP_FILL_POLYGON_GRADIENT = 123;
+const RP_FILL_RECT_GRADIENT = 124;
+const RP_FILL_ROUND_RECT_GRADIENT = 125;
+const RP_FILL_SHAPE_GRADIENT = 126;
+const RP_FILL_TRIANGLE_GRADIENT = 127;
+const RP_FILL_REGION_GRADIENT = 128;
+
+const RP_STROKE_POINT_COLOR = 140;
+const RP_STROKE_LINE_1PX_COLOR = 141;
+const RP_STROKE_RECT_1PX_COLOR = 142;
+
+const RP_FILL_RECT_COLOR = 160;
+const RP_FILL_REGION_COLOR_NO_CLIPPING = 161;
+
+const RP_DRAW_STRING = 180;
+const RP_DRAW_STRING_WITH_OFFSETS = 181;
+const RP_DRAW_STRING_RESULT = 182;
+const RP_STRING_WIDTH = 183;
+const RP_STRING_WIDTH_RESULT = 184;
+const RP_READ_BITMAP = 185;
+const RP_READ_BITMAP_RESULT = 186;
+
+const RP_SET_CURSOR = 200;
+const RP_SET_CURSOR_VISIBLE = 201;
+const RP_MOVE_CURSOR_TO = 202;
+
+const RP_MOUSE_MOVED = 220;
+const RP_MOUSE_DOWN = 221;
+const RP_MOUSE_UP = 222;
+const RP_MOUSE_WHEEL_CHANGED = 223;
+
+const RP_KEY_DOWN = 240;
+const RP_KEY_UP = 241;
+const RP_UNMAPPED_KEY_DOWN = 242;
+const RP_UNMAPPED_KEY_UP = 243;
+const RP_MODIFIERS_CHANGED = 244;
+
+
+// drawing_mode
+const B_OP_COPY = 0;
+const B_OP_OVER = 1;
+const B_OP_ERASE = 2;
+const B_OP_INVERT = 3;
+const B_OP_ADD = 4;
+const B_OP_SUBTRACT = 5;
+const B_OP_BLEND = 6;
+const B_OP_MIN = 7;
+const B_OP_MAX = 8;
+const B_OP_SELECT = 9;
+const B_OP_ALPHA = 10;
+
+
+// color_space
+const B_NO_COLOR_SPACE = 0x0000;
+const B_RGB32 = 0x0008; // BGR- 8:8:8:8
+const B_RGBA32 = 0x2008; // BGRA 8:8:8:8
+const B_RGB24 = 0x0003; // BGR 8:8:8
+const B_RGB16 = 0x0005; // BGR 5:6:5
+const B_RGB15 = 0x0010; // BGR- 5:5:5:1
+const B_RGBA15 = 0x2010; // BGRA 5:5:5:1
+const B_CMAP8 = 0x0004; // 256 color index table
+const B_GRAY8 = 0x0002; // 256 greyscale table
+const B_GRAY1 = 0x0001; // Each bit represents a single pixel
+const B_RGB32_BIG = 0x1008; // -RGB 8:8:8:8
+const B_RGBA32_BIG = 0x3008; // ARGB 8:8:8:8
+const B_RGB24_BIG = 0x1003; // RGB 8:8:8
+const B_RGB16_BIG = 0x1005; // RGB 5:6:5
+const B_RGB15_BIG = 0x1010; // -RGB 1:5:5:5
+const B_RGBA15_BIG = 0x3010; // ARGB 1:5:5:5
+
+const B_TRANSPARENT_MAGIC_CMAP8 = 0xff;
+const B_TRANSPARENT_MAGIC_RGBA15 = 0x39ce;
+const B_TRANSPARENT_MAGIC_RGBA15_BIG = 0xce39;
+const B_TRANSPARENT_MAGIC_RGBA32 = 0xff777477;
+const B_TRANSPARENT_MAGIC_RGBA32_BIG = 0x777477ff;
+
+
+// source_alpha
+const B_PIXEL_ALPHA = 0;
+const B_CONSTANT_ALPHA = 1;
+
+
+// alpha_function
+const B_ALPHA_OVERLAY = 0;
+const B_ALPHA_COMPOSITE = 1;
+
+
+// BGradient::Type
+const B_GRADIENT_TYPE_LINEAR = 0;
+const B_GRADIENT_TYPE_RADIAL = 1;
+const B_GRADIENT_TYPE_RADIAL_FOCUS = 2;
+const B_GRADIENT_TYPE_DIAMOND = 3;
+const B_GRADIENT_TYPE_CONIC = 4;
+const B_GRADIENT_TYPE_NONE = 5;
+
+
+// BShape ops
+const B_SHAPE_OP_MOVE_TO = 0x80000000;
+const B_SHAPE_OP_CLOSE = 0x40000000;
+const B_SHAPE_OP_BEZIER_TO = 0x20000000;
+const B_SHAPE_OP_LINE_TO = 0x10000000;
+const B_SHAPE_OP_SMALL_ARC_TO_CCW = 0x08000000;
+const B_SHAPE_OP_SMALL_ARC_TO_CW = 0x04000000;
+const B_SHAPE_OP_LARGE_ARC_TO_CCW = 0x02000000;
+const B_SHAPE_OP_LARGE_ARC_TO_CW = 0x01000000;
+
+
+// Line join_modes
+const B_ROUND_JOIN = 0;
+const B_MITER_JOIN = 1;
+const B_BEVEL_JOIN = 2;
+const B_BUTT_JOIN = 3;
+const B_SQUARE_JOIN = 4;
+
+
+// Line cap_modes
+const B_ROUND_CAP = B_ROUND_JOIN;
+const B_BUTT_CAP = B_BUTT_JOIN;
+const B_SQUARE_CAP = B_SQUARE_JOIN;
+
+
+const B_DEFAULT_MITER_LIMIT = 10;
+
+
+// modifiers
+const B_SHIFT_KEY = 0x00000001;
+const B_COMMAND_KEY = 0x00000002;
+const B_CONTROL_KEY = 0x00000004;
+const B_CAPS_LOCK = 0x00000008;
+const B_SCROLL_LOCK = 0x00000010;
+const B_NUM_LOCK = 0x00000020;
+const B_OPTION_KEY = 0x00000040;
+const B_MENU_KEY = 0x00000080;
+const B_LEFT_SHIFT_KEY = 0x00000100;
+const B_RIGHT_SHIFT_KEY = 0x00000200;
+const B_LEFT_COMMAND_KEY = 0x00000400;
+const B_RIGHT_COMMAND_KEY = 0x00000800;
+const B_LEFT_CONTROL_KEY = 0x00001000;
+const B_RIGHT_CONTROL_KEY = 0x00002000;
+const B_LEFT_OPTION_KEY = 0x00004000;
+const B_RIGHT_OPTION_KEY = 0x00008000;
+
+
+
+var gSession;
+var gSystemPalette;
+
+
+function StreamingDataView(buffer, littleEndian, byteOffset, byteLength)
+{
+ this.buffer = buffer;
+ this.dataView = new DataView(buffer.buffer, byteOffset, byteLength);
+ this.position = 0;
+ this.littleEndian = littleEndian;
+ this.textDecoder = new TextDecoder('utf-8');
+ this.textEncoder = new TextEncoder();
+}
+
+
+StreamingDataView.prototype.rewind = function()
+{
+ this.position = 0;
+}
+
+
+StreamingDataView.prototype.readInt8 = function()
+{
+ return this.dataView.getInt8(this.position++);
+}
+
+
+StreamingDataView.prototype.readUint8 = function()
+{
+ return this.dataView.getUint8(this.position++);
+}
+
+
+StreamingDataView.prototype.readInt16 = function()
+{
+ var result = this.dataView.getInt16(this.position, this.littleEndian);
+ this.position += 2;
+ return result;
+}
+
+
+StreamingDataView.prototype.readUint16 = function()
+{
+ var result = this.dataView.getUint16(this.position, this.littleEndian);
+ this.position += 2;
+ return result;
+}
+
+
+StreamingDataView.prototype.readInt32 = function()
+{
+ var result = this.dataView.getInt32(this.position, this.littleEndian);
+ this.position += 4;
+ return result;
+}
+
+
+StreamingDataView.prototype.readUint32 = function()
+{
+ var result = this.dataView.getUint32(this.position, this.littleEndian);
+ this.position += 4;
+ return result;
+}
+
+
+StreamingDataView.prototype.readFloat32 = function()
+{
+ var result = this.dataView.getFloat32(this.position, this.littleEndian);
+ this.position += 4;
+ return result;
+}
+
+
+StreamingDataView.prototype.readFloat64 = function()
+{
+ var result = this.dataView.getFloat64(this.position, this.littleEndian);
+ this.position += 8;
+ return result;
+}
+
+
+StreamingDataView.prototype.readString = function(length)
+{
+ var where = this.dataView.byteOffset + this.position;
+ var part = this.buffer.slice(where, where + length);
+ var result = this.textDecoder.decode(part);
+ this.position += length;
+ return result;
+}
+
+
+StreamingDataView.prototype.readInto = function(typedArray)
+{
+ var where = this.dataView.byteOffset + this.position;
+ typedArray.set(this.buffer.slice(where, where + typedArray.byteLength));
+ this.position += typedArray.byteLength;
+}
+
+
+StreamingDataView.prototype.writeInt8 = function(value)
+{
+ this.dataView.setInt8(this.position++, value);
+}
+
+
+StreamingDataView.prototype.writeUint8 = function(value)
+{
+ this.dataView.setUint8(this.position++, value);
+}
+
+
+StreamingDataView.prototype.writeInt16 = function(value)
+{
+ this.dataView.setInt16(this.position, value, this.littleEndian);
+ this.position += 2;
+}
+
+
+StreamingDataView.prototype.writeUint16 = function(value)
+{
+ this.dataView.setUint16(this.position, value, this.littleEndian);
+ this.position += 2;
+}
+
+
+StreamingDataView.prototype.writeInt32 = function(value)
+{
+ this.dataView.setInt32(this.position, value, this.littleEndian);
+ this.position += 4;
+}
+
+
+StreamingDataView.prototype.writeUint32 = function(value)
+{
+ this.dataView.setUint32(this.position, value, this.littleEndian);
+ this.position += 4;
+}
+
+
+StreamingDataView.prototype.writeFloat32 = function(value)
+{
+ this.dataView.setFloat32(this.position, value, this.littleEndian);
+ this.position += 4;
+}
+
+
+StreamingDataView.prototype.writeFloat64 = function(value)
+{
+ this.dataView.setFloat64(this.position, value, this.littleEndian);
+ this.position += 8;
+}
+
+
+StreamingDataView.prototype.writeString = function(string)
+{
+ var encoded = this.textEncoder.encode(string);
+ this.writeUint32(encoded.length);
+ this.buffer.set(encoded, this.position);
+ this.position += encoded.length;
+}
+
+
+StreamingDataView.prototype.setUint32 = function(byteOffset, value)
+{
+ this.dataView.setUint32(byteOffset, value, this.littleEndian);
+}
+
+
+StreamingDataView.prototype.pad = function(length)
+{
+ this.buffer.fill(0, this.position, this.position + length);
+ this.position += length;
+}
+
+
+function RemotePoint(remoteMessage)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage);
+ return;
+ }
+
+ this.x = 0;
+ this.y = 0;
+}
+
+
+RemotePoint.prototype.readFrom = function(remoteMessage)
+{
+ this.x = remoteMessage.dataView.readFloat32();
+ this.y = remoteMessage.dataView.readFloat32();
+ return this;
+}
+
+
+RemotePoint.prototype.writeTo = function(remoteMessage)
+{
+ remoteMessage.dataView.writeFloat32(this.x);
+ remoteMessage.dataView.writeFloat32(this.y);
+ return this;
+}
+
+
+function RemoteRect(remoteMessage)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage);
+ return;
+ }
+
+ this.left = 0;
+ this.top = 0;
+ this.right = -1;
+ this.bottom = -1;
+}
+
+
+RemoteRect.prototype.readFrom = function(remoteMessage)
+{
+ this.left = remoteMessage.dataView.readFloat32();
+ this.top = remoteMessage.dataView.readFloat32();
+ this.right = remoteMessage.dataView.readFloat32();
+ this.bottom = remoteMessage.dataView.readFloat32();
+ return this;
+}
+
+
+RemoteRect.prototype.width = function()
+{
+ return this.right - this.left + 1;
+}
+
+
+RemoteRect.prototype.height = function()
+{
+ return this.bottom - this.top + 1;
+}
+
+
+RemoteRect.prototype.integerWidth = function()
+{
+ return Math.ceil(this.right - this.left);
+}
+
+
+RemoteRect.prototype.integerHeight = function()
+{
+ return Math.ceil(this.bottom - this.top);
+}
+
+
+RemoteRect.prototype.centerX = function()
+{
+ return this.left + this.width() / 2;
+}
+
+
+RemoteRect.prototype.centerY = function()
+{
+ return this.top + this.height() / 2;
+}
+
+
+RemoteRect.prototype.apply = function(apply)
+{
+ var left = Math.floor(this.left);
+ var top = Math.floor(this.top);
+ var right = Math.ceil(this.right);
+ var bottom = Math.ceil(this.bottom);
+ apply(left, top, right - left + 1, bottom - top + 1);
+}
+
+
+RemoteRect.prototype.applyAsEllipse = function(context, which)
+{
+ context.beginPath();
+ context.ellipse(this.centerX(), this.centerY(), this.width() / 2,
+ this.height() / 2, 0, Math.PI * 2, false);
+ which.call(context);
+}
+
+
+function RemoteColor(remoteMessage)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage);
+ return;
+ }
+
+ this.red = 0;
+ this.green = 0;
+ this.blue = 0;
+ this.alpha = 0;
+}
+
+
+RemoteColor.prototype.readFrom = function(remoteMessage)
+{
+ this.red = remoteMessage.dataView.readUint8();
+ this.green = remoteMessage.dataView.readUint8();
+ this.blue = remoteMessage.dataView.readUint8();
+ this.alpha = remoteMessage.dataView.readUint8();
+ return this;
+}
+
+
+RemoteColor.prototype.fromUint32 = function(value)
+{
+ this.red = value & 0xff;
+ this.green = value >> 8 & 0xff;
+ this.blue = value >> 16 & 0xff;
+ this.alpha = value >> 24 & 0xff;
+ return this;
+}
+
+
+RemoteColor.prototype.toColor = function(unsetAlpha)
+{
+ return 'rgba(' + this.red + ', ' + this.green + ', ' + this.blue + ', '
+ + (unsetAlpha ? 1 : this.alpha / 255) + ')';
+}
+
+
+RemoteColor.prototype.toUint32 = function(unsetAlpha)
+{
+ return this.red | this.green << 8 | this.blue << 16
+ | (unsetAlpha ? 255 : this.alpha) << 24;
+}
+
+
+function RemoteFont(remoteMessage)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage);
+ return;
+ }
+
+ this.direction = 0;
+ this.encoding = 0;
+ this.flags = 0;
+ this.spacing = 0;
+ this.shear = 0;
+ this.rotation = 0;
+ this.falseBoldWidth = 0;
+ this.size = 12;
+ this.face = 0;
+ this.family = 0;
+ this.style = 0;
+}
+
+
+RemoteFont.prototype.readFrom = function(remoteMessage)
+{
+ this.direction = remoteMessage.dataView.readUint8();
+ this.encoding = remoteMessage.dataView.readUint8();
+ this.flags = remoteMessage.dataView.readUint32();
+ this.spacing = remoteMessage.dataView.readUint8();
+ this.shear = remoteMessage.dataView.readFloat32();
+ this.rotation = remoteMessage.dataView.readFloat32();
+ this.falseBoldWidth = remoteMessage.dataView.readFloat32();
+ this.size = remoteMessage.dataView.readFloat32();
+ this.face = remoteMessage.dataView.readUint16();
+ this.family = remoteMessage.dataView.readUint16();
+ this.style = remoteMessage.dataView.readUint16();
+ return this;
+}
+
+
+function RemoteTransform(remoteMessage)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage);
+ return;
+ }
+
+ this.setIdentity();
+}
+
+
+RemoteTransform.prototype.readFrom = function(remoteMessage)
+{
+ var isIdentity = remoteMessage.dataView.readUint8();
+ if (isIdentity) {
+ this.setIdentity();
+ return;
+ }
+
+ this.sx = remoteMessage.dataView.readFloat64();
+ this.shy = remoteMessage.dataView.readFloat64();
+ this.shx = remoteMessage.dataView.readFloat64();
+ this.sy = remoteMessage.dataView.readFloat64();
+ this.tx = remoteMessage.dataView.readFloat64();
+ this.ty = remoteMessage.dataView.readFloat64();
+ return this;
+}
+
+
+RemoteTransform.prototype.setIdentity = function()
+{
+ this.sx = 1;
+ this.shy = 0;
+ this.shx = 0;
+ this.sy = 1;
+ this.tx = 0;
+ this.ty = 0;
+ return this;
+}
+
+
+RemoteTransform.prototype.isIdentity = function()
+{
+ return this.sx == 1 && this.shy == 0 && this.shx == 0 && this.sy == 1
+ && this.tx == 0 && this.ty == 0;
+}
+
+
+RemoteTransform.prototype.apply = function(context)
+{
+ context.transform(this.sx, this.shy, this.shx, this.sy, this.tx,
+ this.ty);
+}
+
+
+function RemoteBitmap(remoteMessage, unsetAlpha, colorSpace, flags)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage, unsetAlpha, colorSpace, flags);
+ return;
+ }
+}
+
+
+RemoteBitmap.prototype.readFrom = function(remoteMessage, unsetAlpha,
+ colorSpace, flags)
+{
+ this.width = remoteMessage.dataView.readUint32();
+ this.height = remoteMessage.dataView.readUint32();
+ this.bytesPerRow = remoteMessage.dataView.readUint32();
+
+ if (colorSpace != undefined) {
+ this.colorSpace = colorSpace;
+ this.flags = flags;
+ } else {
+ this.colorSpace = remoteMessage.dataView.readUint32();
+ this.flags = remoteMessage.dataView.readUint32();
+ }
+
+ this.bitsLength = remoteMessage.dataView.readUint32();
+
+ this.canvas = document.createElement('canvas');
+ this.canvas.width = this.width;
+ this.canvas.height = this.height;
+
+ if (this.width == 0 || this.height == 0)
+ return;
+
+ var context = this.canvas.getContext('2d');
+ var imageData = context.createImageData(this.width, this.height);
+ switch (this.colorSpace) {
+ case B_RGBA32:
+ remoteMessage.dataView.readInto(imageData.data);
+ var output = new Uint32Array(imageData.data.buffer);
+
+ for (var i = 0; i < imageData.data.length / 4; i++) {
+ output[i] = (output[i] & 0xff) << 16 | (output[i] >> 16 & 0xff)
+ | (output[i] & 0xff00ff00);
+ }
+
+ if (unsetAlpha) {
+ for (var i = 0; i < imageData.data.length / 4; i++)
+ output[i] |= 0xff000000;
+ }
+
+ break;
+
+ case B_RGB32:
+ remoteMessage.dataView.readInto(imageData.data);
+ var output = new Uint32Array(imageData.data.buffer);
+
+ for (var i = 0; i < imageData.data.length / 4; i++) {
+ output[i] = (output[i] & 0xff) << 16 | (output[i] >> 16 & 0xff)
+ | (output[i] & 0xff00) | 0xff000000;
+
+ if (!unsetAlpha && output[i] == B_TRANSPARENT_MAGIC_RGBA32)
+ output[i] &= 0x00ffffff;
+ }
+ break;
+
+ case B_RGB24:
+ var line = new Uint8Array(this.bytesPerRow);
+ var position = 0;
+
+ for (var y = 0; y < this.height; y++) {
+ remoteMessage.dataView.readInto(line);
+
+ for (var x = 0; x < this.width; x++) {
+ imageData.data[position++] = line[x * 3 + 2];
+ imageData.data[position++] = line[x * 3 + 1];
+ imageData.data[position++] = line[x * 3 + 0];
+ imageData.data[position++] = 255;
+ }
+ }
+
+ break;
+
+ case B_RGB16:
+ var lineBuffer = new Uint8Array(this.bytesPerRow);
+ var line = new Uint16Array(lineBuffer.buffer);
+ var position = 0;
+
+ for (var y = 0; y < this.height; y++) {
+ remoteMessage.dataView.readInto(lineBuffer);
+
+ for (var x = 0; x < this.width; x++) {
+ imageData.data[position++] = (line[x] & 0xf800) >> 8;
+ imageData.data[position++] = (line[x] & 0x07e0) >> 3;
+ imageData.data[position++] = (line[x] & 0x001f) << 3;
+ imageData.data[position++] = 255;
+ }
+ }
+
+ break;
+
+ case B_CMAP8:
+ var line = new Uint8Array(this.bytesPerRow);
+ var output = new Uint32Array(imageData.data.buffer);
+ var position = 0;
+
+ for (var y = 0; y < this.height; y++) {
+ remoteMessage.dataView.readInto(line);
+
+ for (var x = 0; x < this.width; x++)
+ output[position++] = gSystemPalette[line[x]];
+ }
+
+ break;
+
+ case B_GRAY8:
+ var source = new Uint8Array(this.bitsLength);
+ remoteMessage.dataView.readInto(source);
+ for (var i = 0; i < imageData.data.length / 4; i++) {
+ imageData.data[i * 4 + 0] = source[i];
+ imageData.data[i * 4 + 1] = source[i];
+ imageData.data[i * 4 + 2] = source[i];
+ imageData.data[i * 4 + 3] = 255;
+ }
+ break;
+
+ case B_GRAY1:
+ var source = new Uint8Array(this.bitsLength);
+ remoteMessage.dataView.readInto(source);
+ for (var i = 0; i < imageData.data.length / 4; i++) {
+ var value = (source[Math.floor(i / 8)] >> i % 8) & 1 ? 255 : 0;
+ imageData.data[i * 4 + 0] = value;
+ imageData.data[i * 4 + 1] = value;
+ imageData.data[i * 4 + 2] = value;
+ imageData.data[i * 4 + 3] = 255;
+ }
+ break;
+
+ default:
+ console.warn('color space not implemented: ' + this.colorSpace);
+ break;
+ }
+
+ context.putImageData(imageData, 0, 0);
+ return this;
+}
+
+
+function RemotePattern(remoteMessage)
+{
+ this.data = new Uint8Array(8);
+
+ if (remoteMessage)
+ this.readFrom(remoteMessage);
+ else
+ this.data.fill(255);
+}
+
+
+RemotePattern.staticCanvas = document.createElement('canvas');
+RemotePattern.staticCanvas.width = RemotePattern.staticCanvas.height = 8;
+RemotePattern.staticContext = RemotePattern.staticCanvas.getContext('2d');
+RemotePattern.staticImageData
+ = RemotePattern.staticContext.createImageData(8, 8);
+RemotePattern.staticPixels
+ = new Uint32Array(RemotePattern.staticImageData.data.buffer);
+
+
+RemotePattern.prototype.readFrom = function(remoteMessage)
+{
+ remoteMessage.dataView.readInto(this.data);
+ return this;
+}
+
+
+RemotePattern.prototype.isSolid = function()
+{
+ var common = this.data[0];
+ return this.data.every(function(value) { return value == common; });
+}
+
+
+RemotePattern.prototype.toPattern = function(context, lowColor, highColor)
+{
+ for (var i = 0; i < this.data.length * 8; i++) {
+ RemotePattern.staticPixels[i]
+ = (this.data[i / 8 | 0] & 1 << 7 - i % 8) == 0
+ ? lowColor : highColor;
+ }
+
+ // Apparently supplying ImageData to createPattern fails in Chrome.
+ RemotePattern.staticContext.putImageData(RemotePattern.staticImageData, 0,
+ 0);
+ return context.createPattern(RemotePattern.staticCanvas, 'repeat');
+}
+
+
+function RemoteGradient(remoteMessage, context, unsetAlpha)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage, context, unsetAlpha);
+ return;
+ }
+
+ this.gradient = '#00000000';
+}
+
+
+RemoteGradient.prototype.readFrom = function(remoteMessage, context, unsetAlpha)
+{
+ this.type = remoteMessage.dataView.readUint32();
+ switch (this.type) {
+ case B_GRADIENT_TYPE_LINEAR:
+ var start = new RemotePoint(remoteMessage);
+ var end = new RemotePoint(remoteMessage);
+
+ this.gradient = context.createLinearGradient(start.x, start.y,
+ end.x, end.y);
+ break;
+
+ case B_GRADIENT_TYPE_RADIAL:
+ var center = new RemotePoint(remoteMessage);
+ var radius = remoteMessage.dataView.readFloat32();
+
+ this.gradient = context.createRadialGradient(center.x, center.y, 0,
+ center.x, center.y, radius);
+ break;
+
+ default:
+ console.warn('gradient type not implemented: ' + this.type);
+ this.gradient = 'black';
+ return this;
+ }
+
+ var stopCount = remoteMessage.dataView.readUint32();
+ for (var i = 0; i < stopCount; i++) {
+ var color = remoteMessage.readColor(unsetAlpha);
+ var offset = remoteMessage.dataView.readFloat32() / 255;
+ this.gradient.addColorStop(offset, color);
+ }
+
+ return this;
+}
+
+
+function RemoteShape(remoteMessage)
+{
+ if (remoteMessage) {
+ this.readFrom(remoteMessage);
+ return;
+ }
+
+ this.opCount = 0;
+ this.ops = [];
+ this.pointCount = 0;
+ this.points = [];
+}
+
+
+RemoteShape.prototype.readFrom = function(remoteMessage)
+{
+ this.bounds = new RemoteRect(remoteMessage);
+
+ this.opCount = remoteMessage.dataView.readUint32();
+ this.ops = new Array(this.opCount);
+ for (var i = 0; i < this.opCount; i++)
+ this.ops[i] = remoteMessage.dataView.readUint32();
+
+ this.pointCount = remoteMessage.dataView.readUint32();
+ this.points = new Array(this.pointCount);
+ for (var i = 0; i < this.pointCount; i++)
+ this.points[i] = new RemotePoint(remoteMessage);
+
+ return this;
+}
+
+
+RemoteShape.prototype.play = function(context)
+{
+ var pointIndex = 0;
+ for (var i = 0; i < this.opCount; i++) {
+ var op = this.ops[i] & 0xff000000;
+ var count = this.ops[i] & 0x00ffffff;
+
+ if (op & B_SHAPE_OP_MOVE_TO) {
+ var point = this.points[pointIndex++];
+ context.moveTo(point.x, point.y);
+ }
+
+ if (op & B_SHAPE_OP_LINE_TO) {
+ for (var j = 0; j < count; j++) {
+ var point = this.points[pointIndex++];
+ context.lineTo(point.x, point.y);
+ }
+ }
+
+ if (op & B_SHAPE_OP_BEZIER_TO) {
+ for (var j = 0; j < count / 3; j++) {
+ var control1 = this.points[pointIndex++];
+ var control2 = this.points[pointIndex++];
+ var to = this.points[pointIndex++];
+ context.bezierCurveTo(control1.x, control1.y, control2.x,
+ control2.y, to.x, to.y);
+ }
+ }
+
+ if (op & (B_SHAPE_OP_LARGE_ARC_TO_CW | B_SHAPE_OP_LARGE_ARC_TO_CCW
+ | B_SHAPE_OP_SMALL_ARC_TO_CW | B_SHAPE_OP_SMALL_ARC_TO_CCW)) {
+
+ console.warn('shape op arc to not implemented');
+ for (var j = 0; j < count / 3; j++)
+ pointIndex++;
+ }
+
+ if (op & B_SHAPE_OP_CLOSE)
+ context.closePath();
+ }
+}
+
+
+function RemoteMessage(socket)
+{
+ this.socket = socket;
+}
+
+
+RemoteMessage.staticRemoteColor = new RemoteColor();
+
+
+RemoteMessage.prototype.allocate = function(bufferSize)
+{
+ this.buffer = new Uint8Array(bufferSize);
+ this.dataView = new StreamingDataView(this.buffer, true);
+}
+
+
+RemoteMessage.prototype.ensureBufferSize = function(bufferSize)
+{
+ if (this.buffer.byteLength < bufferSize)
+ this.allocate(bufferSize);
+}
+
+
+RemoteMessage.prototype.attach = function(buffer, byteOffset)
+{
+ var bytesLeft = buffer.byteLength - byteOffset;
+ if (bytesLeft < 6)
+ return false;
+
+ this.buffer = buffer;
+ this.dataView = new StreamingDataView(this.buffer, true, byteOffset);
+ this.messageCode = this.dataView.readUint16();
+ this.messageSize = this.dataView.readUint32();
+ if (this.messageSize < 6)
+ throw false;
+
+ return this.messageSize <= bytesLeft;
+}
+
+
+RemoteMessage.prototype.code = function()
+{
+ return this.messageCode;
+}
+
+
+RemoteMessage.prototype.size = function()
+{
+ return this.messageSize;
+}
+
+
+RemoteMessage.prototype.start = function(code)
+{
+ this.dataView.rewind();
+ this.dataView.writeUint16(code);
+ this.dataView.writeUint32(0);
+ // Placeholder for size field.
+}
+
+
+RemoteMessage.prototype.flush = function()
+{
+ this.dataView.setUint32(2, this.dataView.position);
+ this.socket.send(this.buffer.slice(0, this.dataView.position));
+}
+
+
+RemoteMessage.prototype.readColor = function(unsetAlpha)
+{
+ return RemoteMessage.staticRemoteColor.readFrom(this).toColor(unsetAlpha);
+}
+
+
+function RemoteState(session, token)
+{
+ this.session = session;
+ this.token = token;
+
+ this.lowColor = new RemoteColor().fromUint32(0xffffffff);
+ this.highColor = new RemoteColor().fromUint32(0xff000000);
+
+ this.penSize = 1.0;
+ this.lineCap = 'butt';
+ this.lineJoin = 'miter';
+ this.miterLimit = B_DEFAULT_MITER_LIMIT;
+ this.drawingMode = 'source-over';
+
+ this.pattern = new RemotePattern();
+ this.font = new RemoteFont();
+ this.transform = new RemoteTransform();
+}
+
+
+RemoteState.prototype.applyContext = function()
+{
+ var context = this.session.context;
+ if (!this.invalidated && context.currentToken == this.token)
+ return;
+
+ this.session.removeClipping();
+
+ if (this.blendModesEnabled && this.constantAlpha)
+ context.globalAlpha = this.highColor.alpha / 255;
+ else
+ context.globalAlpha = 1;
+
+ var style;
+ if (this.pattern.isSolid()) {
+ style = this.pattern.data[0] == 0 ? this.lowColor : this.highColor;
+ if (this.invert)
+ style = style == this.lowColor ? 'transparent' : 'white';
+ else
+ style = style.toColor(this.unsetAlpha);
+ } else {
+ style = this.pattern.toPattern(context,
+ this.invert ? 0x00000000 : this.lowColor.toUint32(this.unsetAlpha),
+ this.invert
+ ? 0xffffffff : this.highColor.toUint32(this.unsetAlpha));
+ }
+
+ context.fillStyle = context.strokeStyle = style;
+
+ context.font = this.font.size + 'px sans';
+ context.globalCompositeOperation = this.drawingMode;
+ context.lineWidth = this.penSize;
+ context.lineCap = this.lineCap;
+ context.lineJoin = this.lineJoin;
+ context.miterLimit = this.miterLimit;
+
+ context.resetTransform();
+
+ this.session.applyClipping(this.clipRects);
+
+ if (!this.transform.isIdentity()) {
+ context.translate(this.xOffset, this.yOffset);
+ this.transform.apply(context);
+ context.translate(-this.xOffset, -this.yOffset);
+ }
+
+ context.currentToken = this.token;
+ this.invalidated = false;
+}
+
+
+RemoteState.prototype.prepareForRect = function()
+{
+ this.session.context.lineJoin = 'miter';
+ this.session.context.miterLimit = 10;
+}
+
+
+RemoteState.prototype.messageReceived = function(remoteMessage, reply)
+{
+ var context = this.session.context;
+
+ switch (remoteMessage.code()) {
+ case RP_ENABLE_SYNC_DRAWING:
+ case RP_DISABLE_SYNC_DRAWING:
+ console.warn('sync drawing en-/disable not implemented');
+ break;
+
+ case RP_SET_LOW_COLOR:
+ this.lowColor.readFrom(remoteMessage);
+ this.invalidated = true;
+ break;
+
+ case RP_SET_HIGH_COLOR:
+ this.highColor.readFrom(remoteMessage);
+ this.invalidated = true;
+ break;
+
+ case RP_SET_OFFSETS:
+ this.xOffset = remoteMessage.dataView.readInt32();
+ this.yOffset = remoteMessage.dataView.readInt32();
+ this.invalidated = true;
+ break;
+
+ case RP_SET_FONT:
+ this.font = new RemoteFont(remoteMessage);
+ this.invalidated = true;
+ break;
+
+ case RP_SET_TRANSFORM:
+ this.transform = new RemoteTransform(remoteMessage);
+ this.invalidated = true;
+ break;
+
+ case RP_SET_PATTERN:
+ this.pattern = new RemotePattern(remoteMessage);
+ this.invalidated = true;
+ break;
+
+ case RP_SET_PEN_SIZE:
+ this.penSize = remoteMessage.dataView.readFloat32();
+ this.invalidated = true;
+ break;
+
+ case RP_SET_STROKE_MODE:
+ switch (remoteMessage.dataView.readUint32()) {
+ case B_ROUND_CAP:
+ this.lineCap = 'round';
+ break;
+
+ case B_BUTT_CAP:
+ this.lineCap = 'butt';
+ break;
+
+ case B_SQUARE_CAP:
+ this.lineCap = 'square';
+ break;
+ }
+
+ var lineJoin = remoteMessage.dataView.readUint32();
+ switch (lineJoin) {
+ case B_ROUND_JOIN:
+ this.lineJoin = 'round';
+ break;
+
+ case B_MITER_JOIN:
+ this.lineJoin = 'miter';
+ break;
+
+ case B_BEVEL_JOIN:
+ this.lineJoin = 'bevel';
+ break;
+
+ default:
+ console.warn('line join not implemented: ' + join);
+ break;
+ }
+
+ this.miterLimit = remoteMessage.dataView.readFloat32();
+ this.invalidated = true;
+ break;
+
+ case RP_SET_BLENDING_MODE:
+ var sourceAlpha = remoteMessage.dataView.readUint32();
+ this.constantAlpha = sourceAlpha == B_CONSTANT_ALPHA;
+ if (this.blendModesEnabled)
+ this.unsetAlpha = this.constantAlpha;
+
+ var alphaFunction = remoteMessage.dataView.readUint32();
+ if (alphaFunction != B_ALPHA_OVERLAY)
+ console.warn('alpha function not supported: ' + alphaFunction);
+
+ this.invalidated = true;
+ break;
+
+ case RP_SET_DRAWING_MODE:
+ var drawingMode = remoteMessage.dataView.readUint32();
+
+ this.unsetAlpha = false;
+ this.blendModesEnabled = false;
+ this.invert = false;
+
+ switch (drawingMode) {
+ case B_OP_COPY:
+ this.unsetAlpha = true;
+ this.drawingMode = 'source-over';
+ break;
+
+ case B_OP_OVER:
+ this.drawingMode = 'source-over';
+ break;
+
+ case B_OP_ALPHA:
+ this.blendModesEnabled = true;
+ this.unsetAlpha = this.constantAlpha;
+ this.drawingMode = 'source-over';
+ break;
+
+ case B_OP_BLEND:
+ this.drawingMode = 'lighter';
+ break;
+
+ case B_OP_MIN:
+ this.drawingMode = 'darken';
+ break;
+
+ case B_OP_MAX:
+ this.drawingMode = 'ligthen';
+ break;
+
+ case B_OP_INVERT:
+ this.drawingMode = 'difference';
+ this.invert = true;
+ break;
+
+ case B_OP_ADD:
+ this.drawingMode = 'lighter';
+ break;
+
+/*
+ case B_OP_ERASE:
+ this.drawingMode = 'destination-out';
+ break;
+
+ case B_OP_SUBTRACT:
+ this.drawingMode = 'difference';
+ break;
+*/
+
+ default:
+ console.warn('drawing mode not implemented: '
+ + drawingMode);
+ this.drawingMode = 'source-over';
+ break;
+ }
+
+ this.invalidated = true;
+ break;
+
+ case RP_CONSTRAIN_CLIPPING_REGION:
+ var rectCount = remoteMessage.dataView.readUint32();
+ this.clipRects = new Array(rectCount);
+ for (var i = 0; i < rectCount; i++)
+ this.clipRects[i] = new RemoteRect(remoteMessage);
+
+ this.invalidated = true;
+ break;
+
+ case RP_INVERT_RECT:
+ this.applyContext();
+
+ var rect = new RemoteRect(remoteMessage);
+
+ context.save();
+ context.globalCompositeOperation = 'difference';
+ context.fillStyle = 'white';
+ this.prepareForRect();
+
+ rect.apply(context.fillRect.bind(context));
+
+ context.restore();
+ break;
+
+ case RP_DRAW_BITMAP:
+ this.applyContext();
+
+ var bitmapRect = new RemoteRect(remoteMessage);
+ var viewRect = new RemoteRect(remoteMessage);
+ var options = remoteMessage.dataView.readUint32();
+ // TODO: Implement options.
+
+ if (options != 0)
+ console.warn('bitmap options not supported: ' + options);
+
+ var bitmap = new RemoteBitmap(remoteMessage, this.unsetAlpha);
+ context.drawImage(bitmap.canvas, bitmapRect.left, bitmapRect.top,
+ bitmapRect.width(), bitmapRect.height(), viewRect.left,
+ viewRect.top, viewRect.width(), viewRect.height());
+ break;
+
+ case RP_DRAW_BITMAP_RECTS:
+ this.applyContext();
+
+ var options = remoteMessage.dataView.readUint32();
+ // TODO: Implement options.
+ var colorSpace = remoteMessage.dataView.readUint32();
+ var flags = remoteMessage.dataView.readUint32();
+
+ if (options != 0)
+ console.warn('bitmap options not supported: ' + options);
+
+ var rectCount = remoteMessage.dataView.readUint32();
+ for (var i = 0; i < rectCount; i++) {
+ var rect = new RemoteRect(remoteMessage);
+ var bitmap = new RemoteBitmap(remoteMessage, this.unsetAlpha,
+ colorSpace, flags);
+
+ context.drawImage(bitmap.canvas, 0, 0, bitmap.width,
+ bitmap.height, rect.left, rect.top, rect.width(),
+ rect.height());
+ }
+ break;
+
+ case RP_DRAW_STRING:
+ this.applyContext();
+
+ var where = new RemotePoint(remoteMessage);
+ var length = remoteMessage.dataView.readUint32();
+ var string = remoteMessage.dataView.readString(length);
+
+ context.save();
+ context.fillStyle = this.highColor.toColor(this.unsetAlpha);
+ context.fillText(string, where.x, where.y);
+
+ var textMetric = context.measureText(string);
+ where.x += textMetric.width;
+
+ context.restore();
+
+ reply.start(RP_DRAW_STRING_RESULT);
+ reply.dataView.writeInt32(this.token);
+ where.writeTo(reply);
+ reply.flush();
+ break;
+
+ case RP_DRAW_STRING_WITH_OFFSETS:
+ this.applyContext();
+
+ var length = remoteMessage.dataView.readUint32();
+ var string = remoteMessage.dataView.readString(length);
+
+ context.save();
+ context.fillStyle = this.highColor.toColor(this.unsetAlpha);
+
+ var where;
+ for (var i = 0; i < string.length; i++) {
+ where = new RemotePoint(remoteMessage);
+ context.fillText(string[i], where.x, where.y);
+ }
+
+ var textMetric = context.measureText(string[string.length - 1]);
+ where.x += textMetric.width;
+
+ context.restore();
+
+ reply.start(RP_DRAW_STRING_RESULT);
+ reply.dataView.writeInt32(this.token);
+ where.writeTo(reply);
+ reply.flush();
+ break;
+
+ case RP_STRING_WIDTH:
+ this.applyContext();
+
+ var length = remoteMessage.dataView.readUint32();
+ var string = remoteMessage.dataView.readString(length);
+ var textMetric = context.measureText(string);
+
+ reply.start(RP_STRING_WIDTH_RESULT);
+ reply.dataView.writeInt32(this.token);
+ where.writeFloat32(textMetric.width);
+ reply.flush();
+ break;
+
+ case RP_STROKE_ARC:
+ case RP_FILL_ARC:
+ this.applyContext();
+
+ var rect = new RemoteRect(remoteMessage);
+ var startAngle
+ = remoteMessage.dataView.readFloat32() * Math.PI / 180;
+ var invertStart = Math.PI * 2 - startAngle;
+ startAngle += Math.PI / 2;
+
+ var span = remoteMessage.dataView.readFloat32() * Math.PI / 180;
+ var centerX = Math.round(rect.centerX());
+ var centerY = Math.round(rect.centerY());
+ var radius = rect.width() / 2;
+ var maxSpan
+ = remoteMessage.code() != RP_STROKE_ARC ? Math.PI / 2 : span;
+
+ var arcStep = function(max) {
+ max = Math.min(max, span);
+
+ context.beginPath();
+ context.arc(centerX, centerY, radius, invertStart,
+ invertStart - max, true);
+
+ switch (remoteMessage.code()) {
+ case RP_STROKE_ARC:
+ context.stroke();
+ break;
+
+ case RP_FILL_ARC:
+ context.moveTo(centerX, centerY);
+ var endAngle = startAngle + max;
+ context.lineTo(
+ centerX + radius * Math.sin(startAngle),
+ centerY + radius * Math.cos(startAngle));
+ context.lineTo(
+ centerX + radius * Math.sin(endAngle),
+ centerY + radius * Math.cos(endAngle));
+ context.fill();
+ break;
+ }
+
+ startAngle += max;
+ invertStart -= max;
+ span -= max;
+ };
+
+ while (span > 0)
+ arcStep(maxSpan);
+
+ break;
+
+ case RP_STROKE_RECT:
+ case RP_STROKE_ELLIPSE:
+ case RP_FILL_RECT:
+ case RP_FILL_ELLIPSE:
+ this.applyContext();
+
+ context.save();
+ this.prepareForRect();
+
+ var rect = new RemoteRect(remoteMessage);
+
+ switch (remoteMessage.code()) {
+ case RP_STROKE_RECT:
+ rect.apply(context.strokeRect.bind(context));
+ break;
+ case RP_STROKE_ELLIPSE:
+ rect.applyAsEllipse(context, context.stroke);
+ break;
+ case RP_FILL_RECT:
+ rect.apply(context.fillRect.bind(context));
+ break;
+ case RP_FILL_ELLIPSE:
+ rect.applyAsEllipse(context, context.fill);
+ break;
+ }
+
+ context.restore();
+ break;
+
+ case RP_STROKE_ROUND_RECT:
+ case RP_FILL_ROUND_RECT:
+ case RP_FILL_ROUND_RECT_GRADIENT:
+ this.applyContext();
+
+ context.save();
+ this.prepareForRect();
+
+ var rect = new RemoteRect(remoteMessage);
+ var xRadius = remoteMessage.dataView.readFloat32();
+ var yRadius = remoteMessage.dataView.readFloat32();
+
+ if (remoteMessage.code() == RP_FILL_ROUND_RECT_GRADIENT) {
+ context.save();
+ var gradient = new RemoteGradient(remoteMessage, context,
+ this.unsetAlpha);
+ context.fillStyle = gradient.gradient;
+ }
+
+ console.warn('round rects not implemented, falling back to rect');
+ if (remoteMessage.code() == RP_STROKE_ROUND_RECT)
+ rect.apply(context.strokeRect.bind(context));
+ else
+ rect.apply(context.fillRect.bind(context));
+
+ if (remoteMessage.code() == RP_FILL_ROUND_RECT_GRADIENT)
+ context.restore();
+
+ context.restore();
+ break;
+
+ case RP_STROKE_LINE:
+ this.applyContext();
+
+ var from = new RemotePoint(remoteMessage);
+ var to = new RemotePoint(remoteMessage);
+
+ context.beginPath();
+ context.moveTo(from.x, from.y);
+ context.lineTo(to.x, to.y);
+ context.stroke();
+ break;
+
+ case RP_STROKE_LINE_ARRAY:
+ this.applyContext();
+
+ context.save();
+ context.lineCap = 'square';
+
+ var numLines = remoteMessage.dataView.readUint32();
+ for (var i = 0; i < numLines; i++) {
+ var from = new RemotePoint(remoteMessage);
+ var to = new RemotePoint(remoteMessage);
+ context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
+ context.beginPath();
+ context.moveTo(from.x + 0.5, from.y + 0.5);
+ context.lineTo(to.x + 0.5, to.y + 0.5);
+ context.stroke();
+ }
+
+ context.restore();
+ break;
+
+ case RP_STROKE_POINT_COLOR:
+ this.applyContext();
+
+ var point = new RemotePoint(remoteMessage);
+
+ context.save();
+ context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
+
+ context.fillRect(point.x, point.y, 1, 1);
+ context.restore();
+ break;
+
+ case RP_STROKE_LINE_1PX_COLOR:
+ this.applyContext();
+
+ var from = new RemotePoint(remoteMessage);
+ var to = new RemotePoint(remoteMessage);
+
+ context.save();
+ context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
+ context.lineWidth = 1;
+ context.lineCap = 'square';
+
+ context.beginPath();
+ context.moveTo(from.x + 0.5, from.y + 0.5);
+ context.lineTo(to.x + 0.5, to.y + 0.5);
+ context.stroke();
+
+ context.restore();
+ break;
+
+ case RP_STROKE_RECT_1PX_COLOR:
+ this.applyContext();
+
+ var rect = new RemoteRect(remoteMessage);
+
+ context.save();
+ this.prepareForRect();
+
+ context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
+ context.lineWidth = 1;
+
+ rect.apply(context.strokeRect.bind(context));
+
+ context.restore();
+ break;
+
+ case RP_STROKE_SHAPE:
+ case RP_FILL_SHAPE:
+ case RP_FILL_SHAPE_GRADIENT:
+ this.applyContext();
+
+ var shape = new RemoteShape(remoteMessage);
+ var offset = new RemotePoint(remoteMessage);
+ var scale = remoteMessage.dataView.readFloat32();
+
+ context.save();
+ if (remoteMessage.code() == RP_FILL_SHAPE_GRADIENT) {
+ var gradient = new RemoteGradient(remoteMessage, context,
+ this.unsetAlpha);
+ context.fillStyle = gradient.gradient;
+ }
+
+ context.translate(offset.x + 0.5, offset.y + 0.5);
+ context.scale(scale, scale);
+
+ context.beginPath();
+
+ shape.play(context);
+
+ if (remoteMessage.code() == RP_STROKE_SHAPE)
+ context.stroke();
+ else
+ context.fill();
+
+ context.restore();
+ break;
+
+ case RP_STROKE_TRIANGLE:
+ case RP_FILL_TRIANGLE:
+ case RP_FILL_TRIANGLE_GRADIENT:
+ this.applyContext();
+
+ if (remoteMessage.code() == RP_FILL_TRIANGLE_GRADIENT)
+ context.save();
+
+ context.beginPath();
+ var point = new RemotePoint(remoteMessage);
+ context.moveTo(point.x + 0.5, point.y + 0.5);
+
+ for (var i = 0; i < 2; i++) {
+ point = new RemotePoint(remoteMessage);
+ context.lineTo(point.x + 0.5, point.y + 0.5);
+ }
+
+ if (remoteMessage.code() == RP_FILL_TRIANGLE_GRADIENT) {
+ var unusedBounds = new RemoteRect(remoteMessage);
+ var gradient = new RemoteGradient(remoteMessage, context,
+ this.unsetAlpha);
+ context.fillStyle = gradient.gradient;
+ }
+
+ switch (remoteMessage.code()) {
+ case RP_STROKE_TRIANGLE:
+ context.closePath();
+ context.stroke();
+ break;
+
+ case RP_FILL_TRIANGLE:
+ context.fill();
+ break;
+
+ case RP_FILL_TRIANGLE_GRADIENT:
+ context.fill();
+ context.restore();
+ break;
+ }
+
+ break;
+
+ case RP_FILL_RECT_COLOR:
+ this.applyContext();
+
+ var rect = new RemoteRect(remoteMessage);
+
+ context.save();
+ this.prepareForRect();
+ context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
+
+ rect.apply(context.fillRect.bind(context));
+
+ context.restore();
+ break;
+
+ case RP_FILL_RECT_GRADIENT:
+ case RP_FILL_ELLIPSE_GRADIENT:
+ this.applyContext();
+
+ var rect = new RemoteRect(remoteMessage);
+
+ context.save();
+ this.prepareForRect();
+
+ var gradient = new RemoteGradient(remoteMessage, context,
+ this.unsetAlpha);
+ context.fillStyle = gradient.gradient;
+
+ if (remoteMessage.code() == RP_FILL_RECT_GRADIENT)
+ rect.apply(context.fillRect.bind(context));
+ else
+ rect.applyAsEllipse(context, context.fill);
+
+ context.restore();
+ break;
+
+ case RP_FILL_REGION:
+ case RP_FILL_REGION_GRADIENT:
+ this.applyContext();
+
+ var rectCount = remoteMessage.dataView.readUint32();
+ var rects = new Array(rectCount);
+ for (var i = 0; i < rectCount; i++)
+ rects[i] = new RemoteRect(remoteMessage);
+
+ if (remoteMessage.code() == RP_FILL_REGION_GRADIENT) {
+ context.save();
+ var gradient = new RemoteGradient(remoteMessage, context,
+ this.unsetAlpha);
+ context.fillStyle = gradient.gradient;
+ }
+
+ for (var i = 0; i < rectCount; i++)
+ rects[i].apply(context.fillRect.bind(context));
+
+ if (remoteMessage.code() == RP_FILL_REGION_GRADIENT)
+ context.restore();
+
+ break;
+
+ case RP_READ_BITMAP:
+ var bounds = new RemoteRect(remoteMessage);
+ var drawCursor = remoteMessage.dataView.readUint8();
+ // TODO: Support the drawCursor flag.
+
+ if (drawCursor)
+ console.warn('draw cursor in read bitmap not supported');
+
+ var width = bounds.integerWidth() + 1;
+ var height = bounds.integerHeight() + 1;
+ var bytesPerPixel = 3;
+ var bytesPerRow = (width * bytesPerPixel + 3) & ~7;
+ var padding = bytesPerRow - width * bytesPerPixel;
+ var bitsLength = height * bytesPerRow;
+
+ reply.ensureBufferSize(bitsLength + 1024);
+
+ reply.start(RP_READ_BITMAP_RESULT);
+ reply.dataView.writeInt32(this.token);
+
+ reply.dataView.writeInt32(width);
+ reply.dataView.writeInt32(height);
+ reply.dataView.writeInt32(bytesPerRow);
+ reply.dataView.writeUint32(B_RGB24);
+ reply.dataView.writeUint32(0); // Flags
+ reply.dataView.writeUint32(bitsLength);
+
+ var position = 0;
+ var imageData
+ = context.getImageData(bounds.left, bounds.top, width, height);
+ for (var y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++, position += 4) {
+ reply.dataView.writeUint8(imageData.data[position + 2]);
+ reply.dataView.writeUint8(imageData.data[position + 1]);
+ reply.dataView.writeUint8(imageData.data[position + 0]);
+ }
+
+ reply.dataView.pad(padding);
+ }
+
+ reply.flush();
+ break;
+
+ default:
+ console.warn('unhandled message: code: ' + remoteMessage.code()
+ + '; size: ' + remoteMessage.size());
+ break;
+ }
+}
+
+
+function RemoteDesktopSession(targetElement, width, height, targetAddress,
+ disconnectCallback)
+{
+ this.websocket = new WebSocket(targetAddress, 'binary');
+ this.websocket.binaryType = 'arraybuffer';
+ this.websocket.onopen = this.onOpen.bind(this);
+ this.websocket.onmessage = this.onMessage.bind(this);
+ this.websocket.onerror = this.onError.bind(this);
+ this.websocket.onclose = this.onClose.bind(this);
+
+ this.disconnectCallback = disconnectCallback;
+
+ this.sendMessage = new RemoteMessage(this.websocket);
+ this.sendMessage.allocate(1024);
+
+ this.receiveMessage = new RemoteMessage();
+
+ this.container = document.createElement('div');
+ this.container.className = 'session';
+ this.container.style.position = 'relative';
+ targetElement.appendChild(this.container);
+
+ this.canvas = document.createElement('canvas');
+ this.canvas.className = 'session';
+ this.canvas.width = width;
+ this.canvas.height = height;
+ this.container.appendChild(this.canvas);
+
+ this.canvas.tabIndex = 0;
+ this.canvas.focus();
+
+ this.context = this.canvas.getContext('2d', { alpha: false });
+ this.context.imageSmoothingEnabled = false;
+
+ this.cursorVisible = true;
+ this.cursorPosition = { x: 0, y: 0 };
+ this.cursorHotspot = { x: 0, y: 0 };
+
+ this.states = new Object();
+ this.modifiers = 0;
+
+ this.canvas.onmousemove = this.onMouseMove.bind(this);
+ this.canvas.onmousedown = this.onMouseDown.bind(this);
+ this.canvas.onmouseup = this.onMouseUp.bind(this);
+ this.canvas.onwheel = this.onWheel.bind(this);
+
+ this.canvas.onkeydown = this.onKeyDownUp.bind(this);
+ this.canvas.onkeyup = this.onKeyDownUp.bind(this);
+ this.canvas.onkeypress = this.onKeyPress.bind(this);
+
+ this.canvas.oncontextmenu = function(event) {
+ event.preventDefault();
+ };
+
+ this.canvas.onblur = function(event) {
+ event.target.focus();
+ };
+}
+
+
+RemoteDesktopSession.prototype.onOpen = function(open)
+{
+ console.log('open:', open);
+ this.init();
+}
+
+
+RemoteDesktopSession.prototype.onMessage = function(message)
+{
+ var data = message.data;
+ if (this.messageRemainder) {
+ var combined = new Uint8Array(this.messageRemainder.byteLength
+ + data.byteLength);
+ combined.set(new Uint8Array(this.messageRemainder), 0);
+ combined.set(new Uint8Array(data), this.messageRemainder.byteLength);
+ data = combined;
+
+ this.messageRemainder = null;
+ } else
+ data = new Uint8Array(data);
+
+ var byteOffset = 0;
+ while (true) {
+ try {
+ if (!this.receiveMessage.attach(data, byteOffset))
+ break;
+ } catch (exception) {
+ // Discard everything and hope for the best.
+ console.error('stream invalid, discarding everything', exception,
+ this.receiveMessage, data, byteOffset);
+ return;
+ }
+
+ try {
+ this.messageReceived(this.receiveMessage, this.sendMessage);
+ } catch (exception) {
+ console.error('exception during message processing:', exception);
+ }
+
+ byteOffset += this.receiveMessage.size();
+ }
+
+ if (data.byteLength > byteOffset)
+ this.messageRemainder = data.slice(byteOffset);
+}
+
+
+RemoteDesktopSession.prototype.messageReceived = function(remoteMessage, reply)
+{
+ switch (remoteMessage.code()) {
+ case RP_INIT_CONNECTION:
+ console.log('init connection reply');
+ this.sendMessage.start(RP_UPDATE_DISPLAY_MODE);
+ this.sendMessage.dataView.writeUint32(this.canvas.width);
+ this.sendMessage.dataView.writeUint32(this.canvas.height);
+ this.sendMessage.flush();
+
+ this.sendMessage.start(RP_GET_SYSTEM_PALETTE);
+ this.sendMessage.flush();
+ break;
+
+ case RP_GET_SYSTEM_PALETTE_RESULT:
+ var count = remoteMessage.dataView.readUint32();
+ gSystemPalette = new Uint32Array(count);
+
+ var color = new RemoteColor();
+ for (var i = 0; i < gSystemPalette.length; i++)
+ gSystemPalette[i] = color.readFrom(remoteMessage).toUint32();
+
+ break;
+
+ case RP_CREATE_STATE:
+ var token = remoteMessage.dataView.readInt32();
+ console.log('create state: ' + token);
+
+ if (this.states.hasOwnProperty(token))
+ console.error('create state for existing token: ' + token);
+
+ this.states[token] = new RemoteState(this, token);
+ break;
+
+ case RP_DELETE_STATE:
+ var token = remoteMessage.dataView.readInt32();
+ console.log('delete state: ' + token);
+
+ if (!this.states.hasOwnProperty(token)) {
+ console.error('delete state for unknown token: ' + token);
+ break;
+ }
+
+ delete this.states[token];
+ break;
+
+ case RP_INVALIDATE_RECT:
+ case RP_INVALIDATE_REGION:
+ break;
+
+ case RP_SET_CURSOR:
+ this.cursorHotspot = new RemotePoint(remoteMessage);
+ var bitmap = new RemoteBitmap(remoteMessage);
+
+ bitmap.canvas.style.position = 'absolute';
+ if (this.cursorCanvas)
+ this.cursorCanvas.remove();
+
+ this.cursorCanvas = bitmap.canvas;
+ this.cursorCanvas.style.pointerEvents = 'none';
+ this.container.appendChild(this.cursorCanvas);
+ this.container.style.cursor = 'none';
+ this.updateCursor();
+ break;
+
+ case RP_MOVE_CURSOR_TO:
+ this.cursorPosition.x = remoteMessage.dataView.readFloat32();
+ this.cursorPosition.y = remoteMessage.dataView.readFloat32();
+ this.updateCursor();
+ break;
+
+ case RP_SET_CURSOR_VISIBLE:
+ this.cursorVisible = remoteMessage.dataView.readUint8();
+ if (this.cursorCanvas) {
+ this.cursorCanvas.style.visibility
+ = this.cursorVisible ? 'visible' : 'hidden';
+ }
+ break;
+
+ case RP_COPY_RECT_NO_CLIPPING:
+ var xOffset = remoteMessage.dataView.readInt32();
+ var yOffset = remoteMessage.dataView.readInt32();
+ var rect = new RemoteRect(remoteMessage);
+
+ var imageData = this.context.getImageData(rect.left, rect.top,
+ rect.width(), rect.height());
+ this.context.putImageData(imageData, rect.left + xOffset,
+ rect.top + yOffset);
+ break;
+
+ case RP_FILL_REGION_COLOR_NO_CLIPPING:
+ this.removeClipping();
+ this.context.currentToken = -1;
+ this.context.resetTransform();
+ this.context.globalCompositeOperation = 'source-over';
+
+ var rectCount = remoteMessage.dataView.readUint32();
+ var rects = new Array(rectCount);
+ for (var i = 0; i < rectCount; i++)
+ rects[i] = new RemoteRect(remoteMessage);
+
+ this.context.fillStyle = remoteMessage.readColor();
+
+ for (var i = 0; i < rectCount; i++)
+ rects[i].apply(this.context.fillRect.bind(this.context));
+
+ break;
+
+ default:
+ var token = remoteMessage.dataView.readInt32();
+ if (!this.states.hasOwnProperty(token)) {
+ console.warn('no state for token: ' + token);
+ this.states[token] = new RemoteState(this, token);
+ }
+
+ this.states[token].messageReceived(remoteMessage, reply);
+ break;
+ }
+}
+
+
+RemoteDesktopSession.prototype.onError = function(error)
+{
+ console.log('websocket error:', error);
+ this.onDisconnect(error);
+}
+
+
+RemoteDesktopSession.prototype.onClose = function(close)
+{
+ console.log('websocket close:', close);
+ this.onDisconnect(close);
+}
+
+
+RemoteDesktopSession.prototype.onDisconnect = function(reason)
+{
+ this.container.remove();
+ if (this.disconnectCallback)
+ this.disconnectCallback(reason);
+}
+
+
+RemoteDesktopSession.prototype.applyClipping = function(clipRects)
+{
+ this.removeClipping();
+
+ if (!clipRects || clipRects.length == 0)
+ return;
+
+ this.context.save();
+ this.context.beginPath();
+
+ this.context.save();
+ this.context.lineJoin = 'miter';
+ this.context.miterLimit = 10;
+
+ for (var i = 0; i < clipRects.length; i++)
+ clipRects[i].apply(this.context.rect.bind(this.context));
+
+ this.context.restore();
+
+ this.context.clip();
+ this.clippingApplied = true;
+}
+
+
+RemoteDesktopSession.prototype.removeClipping = function()
+{
+ if (!this.clippingApplied)
+ return;
+
+ this.context.restore();
+}
+
+
+RemoteDesktopSession.prototype.init = function()
+{
+ this.sendMessage.start(RP_INIT_CONNECTION);
+ this.sendMessage.flush();
+}
+
+
+RemoteDesktopSession.prototype.updateCursor = function()
+{
+ if (!this.cursorVisible || !this.cursorCanvas)
+ return;
+
+ this.cursorCanvas.style.left
+ = (this.cursorPosition.x - this.cursorHotspot.x) + 'px';
+ this.cursorCanvas.style.top
+ = (this.cursorPosition.y - this.cursorHotspot.y) + 'px';
+}
+
+
+RemoteDesktopSession.prototype.onMouseMove = function(event)
+{
+ this.sendMessage.start(RP_MOUSE_MOVED);
+ this.sendMessage.dataView.writeFloat32(event.offsetX);
+ this.sendMessage.dataView.writeFloat32(event.offsetY);
+ this.sendMessage.flush();
+ event.preventDefault();
+}
+
+
+RemoteDesktopSession.prototype.onMouseDown = function(event)
+{
+ this.canvas.focus();
+ this.sendMessage.start(RP_MOUSE_DOWN);
+ this.sendMessage.dataView.writeFloat32(event.offsetX);
+ this.sendMessage.dataView.writeFloat32(event.offsetY);
+ this.sendMessage.dataView.writeUint32(event.buttons);
+ this.sendMessage.dataView.writeUint32(event.detail);
+ this.sendMessage.flush();
+ event.preventDefault();
+}
+
+
+RemoteDesktopSession.prototype.onMouseUp = function(event)
+{
+ this.sendMessage.start(RP_MOUSE_UP);
+ this.sendMessage.dataView.writeFloat32(event.offsetX);
+ this.sendMessage.dataView.writeFloat32(event.offsetY);
+ this.sendMessage.dataView.writeUint32(event.buttons);
+ this.sendMessage.flush();
+ event.preventDefault();
+}
+
+
+RemoteDesktopSession.prototype.onKeyDownUp = function(event)
+{
+ var keyDown = event.type === 'keydown';
+ var lockModifier = false;
+ var modifiersChanged = 0;
+ switch (event.code) {
+ case 'ShiftLeft':
+ modifiersChanged |= B_LEFT_SHIFT_KEY;
+ if (event.shiftKey == keyDown)
+ modifiersChanged |= B_SHIFT_KEY;
+ break;
+
+ case 'ShiftRight':
+ modifiersChanged |= B_RIGHT_SHIFT_KEY;
+ if (event.shiftKey == keyDown)
+ modifiersChanged |= B_SHIFT_KEY;
+ break;
+
+ case 'ControlLeft':
+ modifiersChanged |= B_LEFT_CONTROL_KEY;
+ if (event.ctrlKey == keyDown)
+ modifiersChanged |= B_CONTROL_KEY;
+ break;
+
+ case 'ControlRight':
+ modifiersChanged |= B_RIGHT_CONTROL_KEY;
+ if (event.ctrlKey == keyDown)
+ modifiersChanged |= B_CONTROL_KEY;
+ break;
+
+ case 'AltLeft':
+ modifiersChanged |= B_LEFT_COMMAND_KEY;
+ if (event.altKey == keyDown)
+ modifiersChanged |= B_COMMAND_KEY;
+ break;
+
+ case 'AltRight':
+ modifiersChanged |= B_RIGHT_COMMAND_KEY;
+ if (event.altKey == keyDown)
+ modifiersChanged |= B_COMMAND_KEY;
+ break;
+
+ case 'ContextMenu':
+ modifiersChanged |= B_MENU_KEY;
+ break;
+
+ case 'CapsLock':
+ modifiersChanged |= B_CAPS_LOCK;
+ lockModifier = true;
+ break;
+
+ case 'ScrollLock':
+ modifiersChanged |= B_SCROLL_LOCK;
+ lockModifier = true;
+ break;
+
+ case 'NumLock':
+ modifiersChanged |= B_NUM_LOCK;
+ lockModifier = true;
+ break;
+ }
+
+ if (modifiersChanged != 0) {
+ if (lockModifier) {
+ if (((this.modifiers & modifiersChanged) == 0) == keyDown)
+ this.modifiers ^= modifiersChanged;
+ } else {
+ if (keyDown)
+ this.modifiers |= modifiersChanged;
+ else
+ this.modifiers &= ~modifiersChanged;
+ }
+
+ this.sendMessage.start(RP_MODIFIERS_CHANGED);
+ this.sendMessage.dataView.writeUint32(this.modifiers);
+ this.sendMessage.flush();
+ event.preventDefault();
+ return;
+ }
+
+ this.sendMessage.start(keyDown ? RP_KEY_DOWN : RP_KEY_UP);
+ if (event.key.length == 1)
+ this.sendMessage.dataView.writeString(event.key);
+ else {
+ this.sendMessage.dataView.writeUint32(1);
+ this.sendMessage.dataView.writeUint8(event.keyCode);
+ }
+
+ if (event.keyCode) {
+ this.sendMessage.dataView.writeUint32(0);
+ this.sendMessage.dataView.writeUint32(event.keyCode);
+ }
+
+ this.sendMessage.flush();
+ event.preventDefault();
+}
+
+
+RemoteDesktopSession.prototype.onKeyPress = function(event)
+{
+ this.sendMessage.start(RP_KEY_DOWN);
+ this.sendMessage.dataView.writeUint32(1);
+ this.sendMessage.dataView.writeUint8(event.which);
+ this.sendMessage.flush();
+ this.sendMessage.start(RP_KEY_UP);
+ this.sendMessage.dataView.writeUint32(1);
+ this.sendMessage.dataView.writeUint8(event.which);
+ this.sendMessage.flush();
+ event.preventDefault();
+}
+
+
+RemoteDesktopSession.prototype.onWheel = function(event)
+{
+ this.sendMessage.start(RP_MOUSE_WHEEL_CHANGED);
+ this.sendMessage.dataView.writeFloat32(event.deltaX);
+ this.sendMessage.dataView.writeFloat32(event.deltaY);
+ this.sendMessage.flush();
+ event.preventDefault();
+}
+
+
+function init()
+{
+ var targetAddressInput = document.querySelector('#targetAddress');
+ var widthInput = document.querySelector('#width');
+ var heightInput = document.querySelector('#height');
+
+ if (localStorage.targetAddress)
+ targetAddressInput.value = localStorage.targetAddress;
+ if (localStorage.width)
+ widthInput.value = localStorage.width;
+ if (localStorage.height)
+ heightInput.value = localStorage.height;
+
+ var onDisconnect = function(reason) {
+ document.body.classList.remove('connect');
+ gSession = undefined;
+ };
+
+ document.querySelector('#connectButton').onclick = function() {
+ document.body.classList.add('connect');
+
+ localStorage.width = widthInput.value;
+ localStorage.height = heightInput.value;
+ localStorage.targetAddress = targetAddressInput.value;
+
+ gSession = new RemoteDesktopSession(document.body, widthInput.value,
+ heightInput.value, targetAddressInput.value, onDisconnect);
+ };
+}