From 85195eb807af5d80e9c0f9d48337e32f50ea6f84 Mon Sep 17 00:00:00 2001 From: Michael Lotz Date: Tue, 21 Nov 2017 21:51:39 +0100 Subject: [PATCH] Add a HTML5 client for the remote desktop protocol. It is implemented using websockets and canvas darwing. It directly speaks the remote desktop protocol. A websocket proxy that translates the TCP connection into a usable form is needed. Websockify works for this out of the box directly under Haiku. Note that not all drawing modes are implemented, and most of them don't have a good match on the canvas side. Fonts are also not properly supported yet. A sans serif font will be used on the client for everything and the metrics between the client and server will diverge and cause drawing artifacts. Usage: * Run an application with a target screen to create the desktop: TARGET_SCREEN=5001 Terminal * Use a websocket proxy to expose the port via websockets: websockify.py 5000 localhost:5001 * Open HaikuRemoteDesktop.html in a browser and connect. To get the full desktop experience you may want to run Tracker and Deskbar inside the virtual desktop. As they are both single launch and controlled by the launch_daemon, you have to stop them via: launch_roster stop x-vnd.be-trak launch_roster stop x-vnd.be-tskb And then start them manually from within the virtual desktop: /system/Tracker & /system/Deskbar & --- .../HaikuRemoteDesktop.html | 76 + .../HaikuRemoteDesktop.js | 2241 +++++++++++++++++ 2 files changed, 2317 insertions(+) create mode 100644 data/html5_remote_desktop/HaikuRemoteDesktop.html create mode 100644 data/html5_remote_desktop/HaikuRemoteDesktop.js 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); + }; +}