'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; // Font spacing and face const B_FIXED_SPACING = 3; const B_ITALIC_FACE = 0x0001; const B_BOLD_FACE = 0x0020; // 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.face & B_ITALIC_FACE ? "italic " : "") + (this.font.face & B_BOLD_FACE ? "bold " : "") + this.font.size + 'px ' + (this.font.spacing==B_FIXED_SPACING ? 'monospace' : 'Helvetica'); 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); }; }