vfs: check userland buffers before reading them.
[haiku.git] / src / tools / html5_remote_desktop / HaikuRemoteDesktop.js
blob1d7deb9b38a1149ceda58bdba5cd12d9634e86b0
1 'use strict';
3 const RP_INIT_CONNECTION = 1;
4 const RP_UPDATE_DISPLAY_MODE = 2;
5 const RP_CLOSE_CONNECTION = 3;
6 const RP_GET_SYSTEM_PALETTE = 4;
7 const RP_GET_SYSTEM_PALETTE_RESULT = 5;
9 const RP_CREATE_STATE = 20;
10 const RP_DELETE_STATE = 21;
11 const RP_ENABLE_SYNC_DRAWING = 22;
12 const RP_DISABLE_SYNC_DRAWING = 23;
13 const RP_INVALIDATE_RECT = 24;
14 const RP_INVALIDATE_REGION = 25;
16 const RP_SET_OFFSETS = 40;
17 const RP_SET_HIGH_COLOR = 41;
18 const RP_SET_LOW_COLOR = 42;
19 const RP_SET_PEN_SIZE = 43;
20 const RP_SET_STROKE_MODE = 44;
21 const RP_SET_BLENDING_MODE = 45;
22 const RP_SET_PATTERN = 46;
23 const RP_SET_DRAWING_MODE = 47;
24 const RP_SET_FONT = 48;
25 const RP_SET_TRANSFORM = 49;
27 const RP_CONSTRAIN_CLIPPING_REGION = 60;
28 const RP_COPY_RECT_NO_CLIPPING = 61;
29 const RP_INVERT_RECT = 62;
30 const RP_DRAW_BITMAP = 63;
31 const RP_DRAW_BITMAP_RECTS = 64;
33 const RP_STROKE_ARC = 80;
34 const RP_STROKE_BEZIER = 81;
35 const RP_STROKE_ELLIPSE = 82;
36 const RP_STROKE_POLYGON = 83;
37 const RP_STROKE_RECT = 84;
38 const RP_STROKE_ROUND_RECT = 85;
39 const RP_STROKE_SHAPE = 86;
40 const RP_STROKE_TRIANGLE = 87;
41 const RP_STROKE_LINE = 88;
42 const RP_STROKE_LINE_ARRAY = 89;
44 const RP_FILL_ARC = 100;
45 const RP_FILL_BEZIER = 101;
46 const RP_FILL_ELLIPSE = 102;
47 const RP_FILL_POLYGON = 103;
48 const RP_FILL_RECT = 104;
49 const RP_FILL_ROUND_RECT = 105;
50 const RP_FILL_SHAPE = 106;
51 const RP_FILL_TRIANGLE = 107;
52 const RP_FILL_REGION = 108;
54 const RP_FILL_ARC_GRADIENT = 120;
55 const RP_FILL_BEZIER_GRADIENT = 121;
56 const RP_FILL_ELLIPSE_GRADIENT = 122;
57 const RP_FILL_POLYGON_GRADIENT = 123;
58 const RP_FILL_RECT_GRADIENT = 124;
59 const RP_FILL_ROUND_RECT_GRADIENT = 125;
60 const RP_FILL_SHAPE_GRADIENT = 126;
61 const RP_FILL_TRIANGLE_GRADIENT = 127;
62 const RP_FILL_REGION_GRADIENT = 128;
64 const RP_STROKE_POINT_COLOR = 140;
65 const RP_STROKE_LINE_1PX_COLOR = 141;
66 const RP_STROKE_RECT_1PX_COLOR = 142;
68 const RP_FILL_RECT_COLOR = 160;
69 const RP_FILL_REGION_COLOR_NO_CLIPPING = 161;
71 const RP_DRAW_STRING = 180;
72 const RP_DRAW_STRING_WITH_OFFSETS = 181;
73 const RP_DRAW_STRING_RESULT = 182;
74 const RP_STRING_WIDTH = 183;
75 const RP_STRING_WIDTH_RESULT = 184;
76 const RP_READ_BITMAP = 185;
77 const RP_READ_BITMAP_RESULT = 186;
79 const RP_SET_CURSOR = 200;
80 const RP_SET_CURSOR_VISIBLE = 201;
81 const RP_MOVE_CURSOR_TO = 202;
83 const RP_MOUSE_MOVED = 220;
84 const RP_MOUSE_DOWN = 221;
85 const RP_MOUSE_UP = 222;
86 const RP_MOUSE_WHEEL_CHANGED = 223;
88 const RP_KEY_DOWN = 240;
89 const RP_KEY_UP = 241;
90 const RP_UNMAPPED_KEY_DOWN = 242;
91 const RP_UNMAPPED_KEY_UP = 243;
92 const RP_MODIFIERS_CHANGED = 244;
95 // drawing_mode
96 const B_OP_COPY = 0;
97 const B_OP_OVER = 1;
98 const B_OP_ERASE = 2;
99 const B_OP_INVERT = 3;
100 const B_OP_ADD = 4;
101 const B_OP_SUBTRACT = 5;
102 const B_OP_BLEND = 6;
103 const B_OP_MIN = 7;
104 const B_OP_MAX = 8;
105 const B_OP_SELECT = 9;
106 const B_OP_ALPHA = 10;
109 // color_space
110 const B_NO_COLOR_SPACE = 0x0000;
111 const B_RGB32 = 0x0008; // BGR- 8:8:8:8
112 const B_RGBA32 = 0x2008; // BGRA 8:8:8:8
113 const B_RGB24 = 0x0003; // BGR 8:8:8
114 const B_RGB16 = 0x0005; // BGR 5:6:5
115 const B_RGB15 = 0x0010; // BGR- 5:5:5:1
116 const B_RGBA15 = 0x2010; // BGRA 5:5:5:1
117 const B_CMAP8 = 0x0004; // 256 color index table
118 const B_GRAY8 = 0x0002; // 256 greyscale table
119 const B_GRAY1 = 0x0001; // Each bit represents a single pixel
120 const B_RGB32_BIG = 0x1008; // -RGB 8:8:8:8
121 const B_RGBA32_BIG = 0x3008; // ARGB 8:8:8:8
122 const B_RGB24_BIG = 0x1003; // RGB 8:8:8
123 const B_RGB16_BIG = 0x1005; // RGB 5:6:5
124 const B_RGB15_BIG = 0x1010; // -RGB 1:5:5:5
125 const B_RGBA15_BIG = 0x3010; // ARGB 1:5:5:5
127 const B_TRANSPARENT_MAGIC_CMAP8 = 0xff;
128 const B_TRANSPARENT_MAGIC_RGBA15 = 0x39ce;
129 const B_TRANSPARENT_MAGIC_RGBA15_BIG = 0xce39;
130 const B_TRANSPARENT_MAGIC_RGBA32 = 0xff777477;
131 const B_TRANSPARENT_MAGIC_RGBA32_BIG = 0x777477ff;
134 // source_alpha
135 const B_PIXEL_ALPHA = 0;
136 const B_CONSTANT_ALPHA = 1;
139 // alpha_function
140 const B_ALPHA_OVERLAY = 0;
141 const B_ALPHA_COMPOSITE = 1;
144 // BGradient::Type
145 const B_GRADIENT_TYPE_LINEAR = 0;
146 const B_GRADIENT_TYPE_RADIAL = 1;
147 const B_GRADIENT_TYPE_RADIAL_FOCUS = 2;
148 const B_GRADIENT_TYPE_DIAMOND = 3;
149 const B_GRADIENT_TYPE_CONIC = 4;
150 const B_GRADIENT_TYPE_NONE = 5;
153 // BShape ops
154 const B_SHAPE_OP_MOVE_TO = 0x80000000;
155 const B_SHAPE_OP_CLOSE = 0x40000000;
156 const B_SHAPE_OP_BEZIER_TO = 0x20000000;
157 const B_SHAPE_OP_LINE_TO = 0x10000000;
158 const B_SHAPE_OP_SMALL_ARC_TO_CCW = 0x08000000;
159 const B_SHAPE_OP_SMALL_ARC_TO_CW = 0x04000000;
160 const B_SHAPE_OP_LARGE_ARC_TO_CCW = 0x02000000;
161 const B_SHAPE_OP_LARGE_ARC_TO_CW = 0x01000000;
164 // Line join_modes
165 const B_ROUND_JOIN = 0;
166 const B_MITER_JOIN = 1;
167 const B_BEVEL_JOIN = 2;
168 const B_BUTT_JOIN = 3;
169 const B_SQUARE_JOIN = 4;
172 // Line cap_modes
173 const B_ROUND_CAP = B_ROUND_JOIN;
174 const B_BUTT_CAP = B_BUTT_JOIN;
175 const B_SQUARE_CAP = B_SQUARE_JOIN;
178 const B_DEFAULT_MITER_LIMIT = 10;
181 // modifiers
182 const B_SHIFT_KEY = 0x00000001;
183 const B_COMMAND_KEY = 0x00000002;
184 const B_CONTROL_KEY = 0x00000004;
185 const B_CAPS_LOCK = 0x00000008;
186 const B_SCROLL_LOCK = 0x00000010;
187 const B_NUM_LOCK = 0x00000020;
188 const B_OPTION_KEY = 0x00000040;
189 const B_MENU_KEY = 0x00000080;
190 const B_LEFT_SHIFT_KEY = 0x00000100;
191 const B_RIGHT_SHIFT_KEY = 0x00000200;
192 const B_LEFT_COMMAND_KEY = 0x00000400;
193 const B_RIGHT_COMMAND_KEY = 0x00000800;
194 const B_LEFT_CONTROL_KEY = 0x00001000;
195 const B_RIGHT_CONTROL_KEY = 0x00002000;
196 const B_LEFT_OPTION_KEY = 0x00004000;
197 const B_RIGHT_OPTION_KEY = 0x00008000;
201 var gSession;
202 var gSystemPalette;
205 function StreamingDataView(buffer, littleEndian, byteOffset, byteLength)
207 this.buffer = buffer;
208 this.dataView = new DataView(buffer.buffer, byteOffset, byteLength);
209 this.position = 0;
210 this.littleEndian = littleEndian;
211 this.textDecoder = new TextDecoder('utf-8');
212 this.textEncoder = new TextEncoder();
216 StreamingDataView.prototype.rewind = function()
218 this.position = 0;
222 StreamingDataView.prototype.readInt8 = function()
224 return this.dataView.getInt8(this.position++);
228 StreamingDataView.prototype.readUint8 = function()
230 return this.dataView.getUint8(this.position++);
234 StreamingDataView.prototype.readInt16 = function()
236 var result = this.dataView.getInt16(this.position, this.littleEndian);
237 this.position += 2;
238 return result;
242 StreamingDataView.prototype.readUint16 = function()
244 var result = this.dataView.getUint16(this.position, this.littleEndian);
245 this.position += 2;
246 return result;
250 StreamingDataView.prototype.readInt32 = function()
252 var result = this.dataView.getInt32(this.position, this.littleEndian);
253 this.position += 4;
254 return result;
258 StreamingDataView.prototype.readUint32 = function()
260 var result = this.dataView.getUint32(this.position, this.littleEndian);
261 this.position += 4;
262 return result;
266 StreamingDataView.prototype.readFloat32 = function()
268 var result = this.dataView.getFloat32(this.position, this.littleEndian);
269 this.position += 4;
270 return result;
274 StreamingDataView.prototype.readFloat64 = function()
276 var result = this.dataView.getFloat64(this.position, this.littleEndian);
277 this.position += 8;
278 return result;
282 StreamingDataView.prototype.readString = function(length)
284 var where = this.dataView.byteOffset + this.position;
285 var part = this.buffer.slice(where, where + length);
286 var result = this.textDecoder.decode(part);
287 this.position += length;
288 return result;
292 StreamingDataView.prototype.readInto = function(typedArray)
294 var where = this.dataView.byteOffset + this.position;
295 typedArray.set(this.buffer.slice(where, where + typedArray.byteLength));
296 this.position += typedArray.byteLength;
300 StreamingDataView.prototype.writeInt8 = function(value)
302 this.dataView.setInt8(this.position++, value);
306 StreamingDataView.prototype.writeUint8 = function(value)
308 this.dataView.setUint8(this.position++, value);
312 StreamingDataView.prototype.writeInt16 = function(value)
314 this.dataView.setInt16(this.position, value, this.littleEndian);
315 this.position += 2;
319 StreamingDataView.prototype.writeUint16 = function(value)
321 this.dataView.setUint16(this.position, value, this.littleEndian);
322 this.position += 2;
326 StreamingDataView.prototype.writeInt32 = function(value)
328 this.dataView.setInt32(this.position, value, this.littleEndian);
329 this.position += 4;
333 StreamingDataView.prototype.writeUint32 = function(value)
335 this.dataView.setUint32(this.position, value, this.littleEndian);
336 this.position += 4;
340 StreamingDataView.prototype.writeFloat32 = function(value)
342 this.dataView.setFloat32(this.position, value, this.littleEndian);
343 this.position += 4;
347 StreamingDataView.prototype.writeFloat64 = function(value)
349 this.dataView.setFloat64(this.position, value, this.littleEndian);
350 this.position += 8;
354 StreamingDataView.prototype.writeString = function(string)
356 var encoded = this.textEncoder.encode(string);
357 this.writeUint32(encoded.length);
358 this.buffer.set(encoded, this.position);
359 this.position += encoded.length;
363 StreamingDataView.prototype.setUint32 = function(byteOffset, value)
365 this.dataView.setUint32(byteOffset, value, this.littleEndian);
369 StreamingDataView.prototype.pad = function(length)
371 this.buffer.fill(0, this.position, this.position + length);
372 this.position += length;
376 function RemotePoint(remoteMessage)
378 if (remoteMessage) {
379 this.readFrom(remoteMessage);
380 return;
383 this.x = 0;
384 this.y = 0;
388 RemotePoint.prototype.readFrom = function(remoteMessage)
390 this.x = remoteMessage.dataView.readFloat32();
391 this.y = remoteMessage.dataView.readFloat32();
392 return this;
396 RemotePoint.prototype.writeTo = function(remoteMessage)
398 remoteMessage.dataView.writeFloat32(this.x);
399 remoteMessage.dataView.writeFloat32(this.y);
400 return this;
404 function RemoteRect(remoteMessage)
406 if (remoteMessage) {
407 this.readFrom(remoteMessage);
408 return;
411 this.left = 0;
412 this.top = 0;
413 this.right = -1;
414 this.bottom = -1;
418 RemoteRect.prototype.readFrom = function(remoteMessage)
420 this.left = remoteMessage.dataView.readFloat32();
421 this.top = remoteMessage.dataView.readFloat32();
422 this.right = remoteMessage.dataView.readFloat32();
423 this.bottom = remoteMessage.dataView.readFloat32();
424 return this;
428 RemoteRect.prototype.width = function()
430 return this.right - this.left + 1;
434 RemoteRect.prototype.height = function()
436 return this.bottom - this.top + 1;
440 RemoteRect.prototype.integerWidth = function()
442 return Math.ceil(this.right - this.left);
446 RemoteRect.prototype.integerHeight = function()
448 return Math.ceil(this.bottom - this.top);
452 RemoteRect.prototype.centerX = function()
454 return this.left + this.width() / 2;
458 RemoteRect.prototype.centerY = function()
460 return this.top + this.height() / 2;
464 RemoteRect.prototype.apply = function(apply)
466 var left = Math.floor(this.left);
467 var top = Math.floor(this.top);
468 var right = Math.ceil(this.right);
469 var bottom = Math.ceil(this.bottom);
470 apply(left, top, right - left + 1, bottom - top + 1);
474 RemoteRect.prototype.applyAsEllipse = function(context, which)
476 context.beginPath();
477 context.ellipse(this.centerX(), this.centerY(), this.width() / 2,
478 this.height() / 2, 0, Math.PI * 2, false);
479 which.call(context);
483 function RemoteColor(remoteMessage)
485 if (remoteMessage) {
486 this.readFrom(remoteMessage);
487 return;
490 this.red = 0;
491 this.green = 0;
492 this.blue = 0;
493 this.alpha = 0;
497 RemoteColor.prototype.readFrom = function(remoteMessage)
499 this.red = remoteMessage.dataView.readUint8();
500 this.green = remoteMessage.dataView.readUint8();
501 this.blue = remoteMessage.dataView.readUint8();
502 this.alpha = remoteMessage.dataView.readUint8();
503 return this;
507 RemoteColor.prototype.fromUint32 = function(value)
509 this.red = value & 0xff;
510 this.green = value >> 8 & 0xff;
511 this.blue = value >> 16 & 0xff;
512 this.alpha = value >> 24 & 0xff;
513 return this;
517 RemoteColor.prototype.toColor = function(unsetAlpha)
519 return 'rgba(' + this.red + ', ' + this.green + ', ' + this.blue + ', '
520 + (unsetAlpha ? 1 : this.alpha / 255) + ')';
524 RemoteColor.prototype.toUint32 = function(unsetAlpha)
526 return this.red | this.green << 8 | this.blue << 16
527 | (unsetAlpha ? 255 : this.alpha) << 24;
531 function RemoteFont(remoteMessage)
533 if (remoteMessage) {
534 this.readFrom(remoteMessage);
535 return;
538 this.direction = 0;
539 this.encoding = 0;
540 this.flags = 0;
541 this.spacing = 0;
542 this.shear = 0;
543 this.rotation = 0;
544 this.falseBoldWidth = 0;
545 this.size = 12;
546 this.face = 0;
547 this.family = 0;
548 this.style = 0;
552 RemoteFont.prototype.readFrom = function(remoteMessage)
554 this.direction = remoteMessage.dataView.readUint8();
555 this.encoding = remoteMessage.dataView.readUint8();
556 this.flags = remoteMessage.dataView.readUint32();
557 this.spacing = remoteMessage.dataView.readUint8();
558 this.shear = remoteMessage.dataView.readFloat32();
559 this.rotation = remoteMessage.dataView.readFloat32();
560 this.falseBoldWidth = remoteMessage.dataView.readFloat32();
561 this.size = remoteMessage.dataView.readFloat32();
562 this.face = remoteMessage.dataView.readUint16();
563 this.family = remoteMessage.dataView.readUint16();
564 this.style = remoteMessage.dataView.readUint16();
565 return this;
569 function RemoteTransform(remoteMessage)
571 if (remoteMessage) {
572 this.readFrom(remoteMessage);
573 return;
576 this.setIdentity();
580 RemoteTransform.prototype.readFrom = function(remoteMessage)
582 var isIdentity = remoteMessage.dataView.readUint8();
583 if (isIdentity) {
584 this.setIdentity();
585 return;
588 this.sx = remoteMessage.dataView.readFloat64();
589 this.shy = remoteMessage.dataView.readFloat64();
590 this.shx = remoteMessage.dataView.readFloat64();
591 this.sy = remoteMessage.dataView.readFloat64();
592 this.tx = remoteMessage.dataView.readFloat64();
593 this.ty = remoteMessage.dataView.readFloat64();
594 return this;
598 RemoteTransform.prototype.setIdentity = function()
600 this.sx = 1;
601 this.shy = 0;
602 this.shx = 0;
603 this.sy = 1;
604 this.tx = 0;
605 this.ty = 0;
606 return this;
610 RemoteTransform.prototype.isIdentity = function()
612 return this.sx == 1 && this.shy == 0 && this.shx == 0 && this.sy == 1
613 && this.tx == 0 && this.ty == 0;
617 RemoteTransform.prototype.apply = function(context)
619 context.transform(this.sx, this.shy, this.shx, this.sy, this.tx,
620 this.ty);
624 function RemoteBitmap(remoteMessage, unsetAlpha, colorSpace, flags)
626 if (remoteMessage) {
627 this.readFrom(remoteMessage, unsetAlpha, colorSpace, flags);
628 return;
633 RemoteBitmap.prototype.readFrom = function(remoteMessage, unsetAlpha,
634 colorSpace, flags)
636 this.width = remoteMessage.dataView.readUint32();
637 this.height = remoteMessage.dataView.readUint32();
638 this.bytesPerRow = remoteMessage.dataView.readUint32();
640 if (colorSpace != undefined) {
641 this.colorSpace = colorSpace;
642 this.flags = flags;
643 } else {
644 this.colorSpace = remoteMessage.dataView.readUint32();
645 this.flags = remoteMessage.dataView.readUint32();
648 this.bitsLength = remoteMessage.dataView.readUint32();
650 this.canvas = document.createElement('canvas');
651 this.canvas.width = this.width;
652 this.canvas.height = this.height;
654 if (this.width == 0 || this.height == 0)
655 return;
657 var context = this.canvas.getContext('2d');
658 var imageData = context.createImageData(this.width, this.height);
659 switch (this.colorSpace) {
660 case B_RGBA32:
661 remoteMessage.dataView.readInto(imageData.data);
662 var output = new Uint32Array(imageData.data.buffer);
664 for (var i = 0; i < imageData.data.length / 4; i++) {
665 output[i] = (output[i] & 0xff) << 16 | (output[i] >> 16 & 0xff)
666 | (output[i] & 0xff00ff00);
669 if (unsetAlpha) {
670 for (var i = 0; i < imageData.data.length / 4; i++)
671 output[i] |= 0xff000000;
674 break;
676 case B_RGB32:
677 remoteMessage.dataView.readInto(imageData.data);
678 var output = new Uint32Array(imageData.data.buffer);
680 for (var i = 0; i < imageData.data.length / 4; i++) {
681 output[i] = (output[i] & 0xff) << 16 | (output[i] >> 16 & 0xff)
682 | (output[i] & 0xff00) | 0xff000000;
684 if (!unsetAlpha && output[i] == B_TRANSPARENT_MAGIC_RGBA32)
685 output[i] &= 0x00ffffff;
687 break;
689 case B_RGB24:
690 var line = new Uint8Array(this.bytesPerRow);
691 var position = 0;
693 for (var y = 0; y < this.height; y++) {
694 remoteMessage.dataView.readInto(line);
696 for (var x = 0; x < this.width; x++) {
697 imageData.data[position++] = line[x * 3 + 2];
698 imageData.data[position++] = line[x * 3 + 1];
699 imageData.data[position++] = line[x * 3 + 0];
700 imageData.data[position++] = 255;
704 break;
706 case B_RGB16:
707 var lineBuffer = new Uint8Array(this.bytesPerRow);
708 var line = new Uint16Array(lineBuffer.buffer);
709 var position = 0;
711 for (var y = 0; y < this.height; y++) {
712 remoteMessage.dataView.readInto(lineBuffer);
714 for (var x = 0; x < this.width; x++) {
715 imageData.data[position++] = (line[x] & 0xf800) >> 8;
716 imageData.data[position++] = (line[x] & 0x07e0) >> 3;
717 imageData.data[position++] = (line[x] & 0x001f) << 3;
718 imageData.data[position++] = 255;
722 break;
724 case B_CMAP8:
725 var line = new Uint8Array(this.bytesPerRow);
726 var output = new Uint32Array(imageData.data.buffer);
727 var position = 0;
729 for (var y = 0; y < this.height; y++) {
730 remoteMessage.dataView.readInto(line);
732 for (var x = 0; x < this.width; x++)
733 output[position++] = gSystemPalette[line[x]];
736 break;
738 case B_GRAY8:
739 var source = new Uint8Array(this.bitsLength);
740 remoteMessage.dataView.readInto(source);
741 for (var i = 0; i < imageData.data.length / 4; i++) {
742 imageData.data[i * 4 + 0] = source[i];
743 imageData.data[i * 4 + 1] = source[i];
744 imageData.data[i * 4 + 2] = source[i];
745 imageData.data[i * 4 + 3] = 255;
747 break;
749 case B_GRAY1:
750 var source = new Uint8Array(this.bitsLength);
751 remoteMessage.dataView.readInto(source);
752 for (var i = 0; i < imageData.data.length / 4; i++) {
753 var value = (source[Math.floor(i / 8)] >> i % 8) & 1 ? 255 : 0;
754 imageData.data[i * 4 + 0] = value;
755 imageData.data[i * 4 + 1] = value;
756 imageData.data[i * 4 + 2] = value;
757 imageData.data[i * 4 + 3] = 255;
759 break;
761 default:
762 console.warn('color space not implemented: ' + this.colorSpace);
763 break;
766 context.putImageData(imageData, 0, 0);
767 return this;
771 function RemotePattern(remoteMessage)
773 this.data = new Uint8Array(8);
775 if (remoteMessage)
776 this.readFrom(remoteMessage);
777 else
778 this.data.fill(255);
782 RemotePattern.staticCanvas = document.createElement('canvas');
783 RemotePattern.staticCanvas.width = RemotePattern.staticCanvas.height = 8;
784 RemotePattern.staticContext = RemotePattern.staticCanvas.getContext('2d');
785 RemotePattern.staticImageData
786 = RemotePattern.staticContext.createImageData(8, 8);
787 RemotePattern.staticPixels
788 = new Uint32Array(RemotePattern.staticImageData.data.buffer);
791 RemotePattern.prototype.readFrom = function(remoteMessage)
793 remoteMessage.dataView.readInto(this.data);
794 return this;
798 RemotePattern.prototype.isSolid = function()
800 var common = this.data[0];
801 return this.data.every(function(value) { return value == common; });
805 RemotePattern.prototype.toPattern = function(context, lowColor, highColor)
807 for (var i = 0; i < this.data.length * 8; i++) {
808 RemotePattern.staticPixels[i]
809 = (this.data[i / 8 | 0] & 1 << 7 - i % 8) == 0
810 ? lowColor : highColor;
813 // Apparently supplying ImageData to createPattern fails in Chrome.
814 RemotePattern.staticContext.putImageData(RemotePattern.staticImageData, 0,
816 return context.createPattern(RemotePattern.staticCanvas, 'repeat');
820 function RemoteGradient(remoteMessage, context, unsetAlpha)
822 if (remoteMessage) {
823 this.readFrom(remoteMessage, context, unsetAlpha);
824 return;
827 this.gradient = '#00000000';
831 RemoteGradient.prototype.readFrom = function(remoteMessage, context, unsetAlpha)
833 this.type = remoteMessage.dataView.readUint32();
834 switch (this.type) {
835 case B_GRADIENT_TYPE_LINEAR:
836 var start = new RemotePoint(remoteMessage);
837 var end = new RemotePoint(remoteMessage);
839 this.gradient = context.createLinearGradient(start.x, start.y,
840 end.x, end.y);
841 break;
843 case B_GRADIENT_TYPE_RADIAL:
844 var center = new RemotePoint(remoteMessage);
845 var radius = remoteMessage.dataView.readFloat32();
847 this.gradient = context.createRadialGradient(center.x, center.y, 0,
848 center.x, center.y, radius);
849 break;
851 default:
852 console.warn('gradient type not implemented: ' + this.type);
853 this.gradient = 'black';
854 return this;
857 var stopCount = remoteMessage.dataView.readUint32();
858 for (var i = 0; i < stopCount; i++) {
859 var color = remoteMessage.readColor(unsetAlpha);
860 var offset = remoteMessage.dataView.readFloat32() / 255;
861 this.gradient.addColorStop(offset, color);
864 return this;
868 function RemoteShape(remoteMessage)
870 if (remoteMessage) {
871 this.readFrom(remoteMessage);
872 return;
875 this.opCount = 0;
876 this.ops = [];
877 this.pointCount = 0;
878 this.points = [];
882 RemoteShape.prototype.readFrom = function(remoteMessage)
884 this.bounds = new RemoteRect(remoteMessage);
886 this.opCount = remoteMessage.dataView.readUint32();
887 this.ops = new Array(this.opCount);
888 for (var i = 0; i < this.opCount; i++)
889 this.ops[i] = remoteMessage.dataView.readUint32();
891 this.pointCount = remoteMessage.dataView.readUint32();
892 this.points = new Array(this.pointCount);
893 for (var i = 0; i < this.pointCount; i++)
894 this.points[i] = new RemotePoint(remoteMessage);
896 return this;
900 RemoteShape.prototype.play = function(context)
902 var pointIndex = 0;
903 for (var i = 0; i < this.opCount; i++) {
904 var op = this.ops[i] & 0xff000000;
905 var count = this.ops[i] & 0x00ffffff;
907 if (op & B_SHAPE_OP_MOVE_TO) {
908 var point = this.points[pointIndex++];
909 context.moveTo(point.x, point.y);
912 if (op & B_SHAPE_OP_LINE_TO) {
913 for (var j = 0; j < count; j++) {
914 var point = this.points[pointIndex++];
915 context.lineTo(point.x, point.y);
919 if (op & B_SHAPE_OP_BEZIER_TO) {
920 for (var j = 0; j < count / 3; j++) {
921 var control1 = this.points[pointIndex++];
922 var control2 = this.points[pointIndex++];
923 var to = this.points[pointIndex++];
924 context.bezierCurveTo(control1.x, control1.y, control2.x,
925 control2.y, to.x, to.y);
929 if (op & (B_SHAPE_OP_LARGE_ARC_TO_CW | B_SHAPE_OP_LARGE_ARC_TO_CCW
930 | B_SHAPE_OP_SMALL_ARC_TO_CW | B_SHAPE_OP_SMALL_ARC_TO_CCW)) {
932 console.warn('shape op arc to not implemented');
933 for (var j = 0; j < count / 3; j++)
934 pointIndex++;
937 if (op & B_SHAPE_OP_CLOSE)
938 context.closePath();
943 function RemoteMessage(socket)
945 this.socket = socket;
949 RemoteMessage.staticRemoteColor = new RemoteColor();
952 RemoteMessage.prototype.allocate = function(bufferSize)
954 this.buffer = new Uint8Array(bufferSize);
955 this.dataView = new StreamingDataView(this.buffer, true);
959 RemoteMessage.prototype.ensureBufferSize = function(bufferSize)
961 if (this.buffer.byteLength < bufferSize)
962 this.allocate(bufferSize);
966 RemoteMessage.prototype.attach = function(buffer, byteOffset)
968 var bytesLeft = buffer.byteLength - byteOffset;
969 if (bytesLeft < 6)
970 return false;
972 this.buffer = buffer;
973 this.dataView = new StreamingDataView(this.buffer, true, byteOffset);
974 this.messageCode = this.dataView.readUint16();
975 this.messageSize = this.dataView.readUint32();
976 if (this.messageSize < 6)
977 throw false;
979 return this.messageSize <= bytesLeft;
983 RemoteMessage.prototype.code = function()
985 return this.messageCode;
989 RemoteMessage.prototype.size = function()
991 return this.messageSize;
995 RemoteMessage.prototype.start = function(code)
997 this.dataView.rewind();
998 this.dataView.writeUint16(code);
999 this.dataView.writeUint32(0);
1000 // Placeholder for size field.
1004 RemoteMessage.prototype.flush = function()
1006 this.dataView.setUint32(2, this.dataView.position);
1007 this.socket.send(this.buffer.slice(0, this.dataView.position));
1011 RemoteMessage.prototype.readColor = function(unsetAlpha)
1013 return RemoteMessage.staticRemoteColor.readFrom(this).toColor(unsetAlpha);
1017 function RemoteState(session, token)
1019 this.session = session;
1020 this.token = token;
1022 this.lowColor = new RemoteColor().fromUint32(0xffffffff);
1023 this.highColor = new RemoteColor().fromUint32(0xff000000);
1025 this.penSize = 1.0;
1026 this.lineCap = 'butt';
1027 this.lineJoin = 'miter';
1028 this.miterLimit = B_DEFAULT_MITER_LIMIT;
1029 this.drawingMode = 'source-over';
1031 this.pattern = new RemotePattern();
1032 this.font = new RemoteFont();
1033 this.transform = new RemoteTransform();
1037 RemoteState.prototype.applyContext = function()
1039 var context = this.session.context;
1040 if (!this.invalidated && context.currentToken == this.token)
1041 return;
1043 this.session.removeClipping();
1045 if (this.blendModesEnabled && this.constantAlpha)
1046 context.globalAlpha = this.highColor.alpha / 255;
1047 else
1048 context.globalAlpha = 1;
1050 var style;
1051 if (this.pattern.isSolid()) {
1052 style = this.pattern.data[0] == 0 ? this.lowColor : this.highColor;
1053 if (this.invert)
1054 style = style == this.lowColor ? 'transparent' : 'white';
1055 else
1056 style = style.toColor(this.unsetAlpha);
1057 } else {
1058 style = this.pattern.toPattern(context,
1059 this.invert ? 0x00000000 : this.lowColor.toUint32(this.unsetAlpha),
1060 this.invert
1061 ? 0xffffffff : this.highColor.toUint32(this.unsetAlpha));
1064 context.fillStyle = context.strokeStyle = style;
1066 context.font = this.font.size + 'px sans';
1067 context.globalCompositeOperation = this.drawingMode;
1068 context.lineWidth = this.penSize;
1069 context.lineCap = this.lineCap;
1070 context.lineJoin = this.lineJoin;
1071 context.miterLimit = this.miterLimit;
1073 context.resetTransform();
1075 this.session.applyClipping(this.clipRects);
1077 if (!this.transform.isIdentity()) {
1078 context.translate(this.xOffset, this.yOffset);
1079 this.transform.apply(context);
1080 context.translate(-this.xOffset, -this.yOffset);
1083 context.currentToken = this.token;
1084 this.invalidated = false;
1088 RemoteState.prototype.prepareForRect = function()
1090 this.session.context.lineJoin = 'miter';
1091 this.session.context.miterLimit = 10;
1095 RemoteState.prototype.messageReceived = function(remoteMessage, reply)
1097 var context = this.session.context;
1099 switch (remoteMessage.code()) {
1100 case RP_ENABLE_SYNC_DRAWING:
1101 case RP_DISABLE_SYNC_DRAWING:
1102 console.warn('sync drawing en-/disable not implemented');
1103 break;
1105 case RP_SET_LOW_COLOR:
1106 this.lowColor.readFrom(remoteMessage);
1107 this.invalidated = true;
1108 break;
1110 case RP_SET_HIGH_COLOR:
1111 this.highColor.readFrom(remoteMessage);
1112 this.invalidated = true;
1113 break;
1115 case RP_SET_OFFSETS:
1116 this.xOffset = remoteMessage.dataView.readInt32();
1117 this.yOffset = remoteMessage.dataView.readInt32();
1118 this.invalidated = true;
1119 break;
1121 case RP_SET_FONT:
1122 this.font = new RemoteFont(remoteMessage);
1123 this.invalidated = true;
1124 break;
1126 case RP_SET_TRANSFORM:
1127 this.transform = new RemoteTransform(remoteMessage);
1128 this.invalidated = true;
1129 break;
1131 case RP_SET_PATTERN:
1132 this.pattern = new RemotePattern(remoteMessage);
1133 this.invalidated = true;
1134 break;
1136 case RP_SET_PEN_SIZE:
1137 this.penSize = remoteMessage.dataView.readFloat32();
1138 this.invalidated = true;
1139 break;
1141 case RP_SET_STROKE_MODE:
1142 switch (remoteMessage.dataView.readUint32()) {
1143 case B_ROUND_CAP:
1144 this.lineCap = 'round';
1145 break;
1147 case B_BUTT_CAP:
1148 this.lineCap = 'butt';
1149 break;
1151 case B_SQUARE_CAP:
1152 this.lineCap = 'square';
1153 break;
1156 var lineJoin = remoteMessage.dataView.readUint32();
1157 switch (lineJoin) {
1158 case B_ROUND_JOIN:
1159 this.lineJoin = 'round';
1160 break;
1162 case B_MITER_JOIN:
1163 this.lineJoin = 'miter';
1164 break;
1166 case B_BEVEL_JOIN:
1167 this.lineJoin = 'bevel';
1168 break;
1170 default:
1171 console.warn('line join not implemented: ' + join);
1172 break;
1175 this.miterLimit = remoteMessage.dataView.readFloat32();
1176 this.invalidated = true;
1177 break;
1179 case RP_SET_BLENDING_MODE:
1180 var sourceAlpha = remoteMessage.dataView.readUint32();
1181 this.constantAlpha = sourceAlpha == B_CONSTANT_ALPHA;
1182 if (this.blendModesEnabled)
1183 this.unsetAlpha = this.constantAlpha;
1185 var alphaFunction = remoteMessage.dataView.readUint32();
1186 if (alphaFunction != B_ALPHA_OVERLAY)
1187 console.warn('alpha function not supported: ' + alphaFunction);
1189 this.invalidated = true;
1190 break;
1192 case RP_SET_DRAWING_MODE:
1193 var drawingMode = remoteMessage.dataView.readUint32();
1195 this.unsetAlpha = false;
1196 this.blendModesEnabled = false;
1197 this.invert = false;
1199 switch (drawingMode) {
1200 case B_OP_COPY:
1201 this.unsetAlpha = true;
1202 this.drawingMode = 'source-over';
1203 break;
1205 case B_OP_OVER:
1206 this.drawingMode = 'source-over';
1207 break;
1209 case B_OP_ALPHA:
1210 this.blendModesEnabled = true;
1211 this.unsetAlpha = this.constantAlpha;
1212 this.drawingMode = 'source-over';
1213 break;
1215 case B_OP_BLEND:
1216 this.drawingMode = 'lighter';
1217 break;
1219 case B_OP_MIN:
1220 this.drawingMode = 'darken';
1221 break;
1223 case B_OP_MAX:
1224 this.drawingMode = 'ligthen';
1225 break;
1227 case B_OP_INVERT:
1228 this.drawingMode = 'difference';
1229 this.invert = true;
1230 break;
1232 case B_OP_ADD:
1233 this.drawingMode = 'lighter';
1234 break;
1237 case B_OP_ERASE:
1238 this.drawingMode = 'destination-out';
1239 break;
1241 case B_OP_SUBTRACT:
1242 this.drawingMode = 'difference';
1243 break;
1246 default:
1247 console.warn('drawing mode not implemented: '
1248 + drawingMode);
1249 this.drawingMode = 'source-over';
1250 break;
1253 this.invalidated = true;
1254 break;
1256 case RP_CONSTRAIN_CLIPPING_REGION:
1257 var rectCount = remoteMessage.dataView.readUint32();
1258 this.clipRects = new Array(rectCount);
1259 for (var i = 0; i < rectCount; i++)
1260 this.clipRects[i] = new RemoteRect(remoteMessage);
1262 this.invalidated = true;
1263 break;
1265 case RP_INVERT_RECT:
1266 this.applyContext();
1268 var rect = new RemoteRect(remoteMessage);
1270 context.save();
1271 context.globalCompositeOperation = 'difference';
1272 context.fillStyle = 'white';
1273 this.prepareForRect();
1275 rect.apply(context.fillRect.bind(context));
1277 context.restore();
1278 break;
1280 case RP_DRAW_BITMAP:
1281 this.applyContext();
1283 var bitmapRect = new RemoteRect(remoteMessage);
1284 var viewRect = new RemoteRect(remoteMessage);
1285 var options = remoteMessage.dataView.readUint32();
1286 // TODO: Implement options.
1288 if (options != 0)
1289 console.warn('bitmap options not supported: ' + options);
1291 var bitmap = new RemoteBitmap(remoteMessage, this.unsetAlpha);
1292 context.drawImage(bitmap.canvas, bitmapRect.left, bitmapRect.top,
1293 bitmapRect.width(), bitmapRect.height(), viewRect.left,
1294 viewRect.top, viewRect.width(), viewRect.height());
1295 break;
1297 case RP_DRAW_BITMAP_RECTS:
1298 this.applyContext();
1300 var options = remoteMessage.dataView.readUint32();
1301 // TODO: Implement options.
1302 var colorSpace = remoteMessage.dataView.readUint32();
1303 var flags = remoteMessage.dataView.readUint32();
1305 if (options != 0)
1306 console.warn('bitmap options not supported: ' + options);
1308 var rectCount = remoteMessage.dataView.readUint32();
1309 for (var i = 0; i < rectCount; i++) {
1310 var rect = new RemoteRect(remoteMessage);
1311 var bitmap = new RemoteBitmap(remoteMessage, this.unsetAlpha,
1312 colorSpace, flags);
1314 context.drawImage(bitmap.canvas, 0, 0, bitmap.width,
1315 bitmap.height, rect.left, rect.top, rect.width(),
1316 rect.height());
1318 break;
1320 case RP_DRAW_STRING:
1321 this.applyContext();
1323 var where = new RemotePoint(remoteMessage);
1324 var length = remoteMessage.dataView.readUint32();
1325 var string = remoteMessage.dataView.readString(length);
1327 context.save();
1328 context.fillStyle = this.highColor.toColor(this.unsetAlpha);
1329 context.fillText(string, where.x, where.y);
1331 var textMetric = context.measureText(string);
1332 where.x += textMetric.width;
1334 context.restore();
1336 reply.start(RP_DRAW_STRING_RESULT);
1337 reply.dataView.writeInt32(this.token);
1338 where.writeTo(reply);
1339 reply.flush();
1340 break;
1342 case RP_DRAW_STRING_WITH_OFFSETS:
1343 this.applyContext();
1345 var length = remoteMessage.dataView.readUint32();
1346 var string = remoteMessage.dataView.readString(length);
1348 context.save();
1349 context.fillStyle = this.highColor.toColor(this.unsetAlpha);
1351 var where;
1352 for (var i = 0; i < string.length; i++) {
1353 where = new RemotePoint(remoteMessage);
1354 context.fillText(string[i], where.x, where.y);
1357 var textMetric = context.measureText(string[string.length - 1]);
1358 where.x += textMetric.width;
1360 context.restore();
1362 reply.start(RP_DRAW_STRING_RESULT);
1363 reply.dataView.writeInt32(this.token);
1364 where.writeTo(reply);
1365 reply.flush();
1366 break;
1368 case RP_STRING_WIDTH:
1369 this.applyContext();
1371 var length = remoteMessage.dataView.readUint32();
1372 var string = remoteMessage.dataView.readString(length);
1373 var textMetric = context.measureText(string);
1375 reply.start(RP_STRING_WIDTH_RESULT);
1376 reply.dataView.writeInt32(this.token);
1377 where.writeFloat32(textMetric.width);
1378 reply.flush();
1379 break;
1381 case RP_STROKE_ARC:
1382 case RP_FILL_ARC:
1383 this.applyContext();
1385 var rect = new RemoteRect(remoteMessage);
1386 var startAngle
1387 = remoteMessage.dataView.readFloat32() * Math.PI / 180;
1388 var invertStart = Math.PI * 2 - startAngle;
1389 startAngle += Math.PI / 2;
1391 var span = remoteMessage.dataView.readFloat32() * Math.PI / 180;
1392 var centerX = Math.round(rect.centerX());
1393 var centerY = Math.round(rect.centerY());
1394 var radius = rect.width() / 2;
1395 var maxSpan
1396 = remoteMessage.code() != RP_STROKE_ARC ? Math.PI / 2 : span;
1398 var arcStep = function(max) {
1399 max = Math.min(max, span);
1401 context.beginPath();
1402 context.arc(centerX, centerY, radius, invertStart,
1403 invertStart - max, true);
1405 switch (remoteMessage.code()) {
1406 case RP_STROKE_ARC:
1407 context.stroke();
1408 break;
1410 case RP_FILL_ARC:
1411 context.moveTo(centerX, centerY);
1412 var endAngle = startAngle + max;
1413 context.lineTo(
1414 centerX + radius * Math.sin(startAngle),
1415 centerY + radius * Math.cos(startAngle));
1416 context.lineTo(
1417 centerX + radius * Math.sin(endAngle),
1418 centerY + radius * Math.cos(endAngle));
1419 context.fill();
1420 break;
1423 startAngle += max;
1424 invertStart -= max;
1425 span -= max;
1428 while (span > 0)
1429 arcStep(maxSpan);
1431 break;
1433 case RP_STROKE_RECT:
1434 case RP_STROKE_ELLIPSE:
1435 case RP_FILL_RECT:
1436 case RP_FILL_ELLIPSE:
1437 this.applyContext();
1439 context.save();
1440 this.prepareForRect();
1442 var rect = new RemoteRect(remoteMessage);
1444 switch (remoteMessage.code()) {
1445 case RP_STROKE_RECT:
1446 rect.apply(context.strokeRect.bind(context));
1447 break;
1448 case RP_STROKE_ELLIPSE:
1449 rect.applyAsEllipse(context, context.stroke);
1450 break;
1451 case RP_FILL_RECT:
1452 rect.apply(context.fillRect.bind(context));
1453 break;
1454 case RP_FILL_ELLIPSE:
1455 rect.applyAsEllipse(context, context.fill);
1456 break;
1459 context.restore();
1460 break;
1462 case RP_STROKE_ROUND_RECT:
1463 case RP_FILL_ROUND_RECT:
1464 case RP_FILL_ROUND_RECT_GRADIENT:
1465 this.applyContext();
1467 context.save();
1468 this.prepareForRect();
1470 var rect = new RemoteRect(remoteMessage);
1471 var xRadius = remoteMessage.dataView.readFloat32();
1472 var yRadius = remoteMessage.dataView.readFloat32();
1474 if (remoteMessage.code() == RP_FILL_ROUND_RECT_GRADIENT) {
1475 context.save();
1476 var gradient = new RemoteGradient(remoteMessage, context,
1477 this.unsetAlpha);
1478 context.fillStyle = gradient.gradient;
1481 console.warn('round rects not implemented, falling back to rect');
1482 if (remoteMessage.code() == RP_STROKE_ROUND_RECT)
1483 rect.apply(context.strokeRect.bind(context));
1484 else
1485 rect.apply(context.fillRect.bind(context));
1487 if (remoteMessage.code() == RP_FILL_ROUND_RECT_GRADIENT)
1488 context.restore();
1490 context.restore();
1491 break;
1493 case RP_STROKE_LINE:
1494 this.applyContext();
1496 var from = new RemotePoint(remoteMessage);
1497 var to = new RemotePoint(remoteMessage);
1499 context.beginPath();
1500 context.moveTo(from.x, from.y);
1501 context.lineTo(to.x, to.y);
1502 context.stroke();
1503 break;
1505 case RP_STROKE_LINE_ARRAY:
1506 this.applyContext();
1508 context.save();
1509 context.lineCap = 'square';
1511 var numLines = remoteMessage.dataView.readUint32();
1512 for (var i = 0; i < numLines; i++) {
1513 var from = new RemotePoint(remoteMessage);
1514 var to = new RemotePoint(remoteMessage);
1515 context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1516 context.beginPath();
1517 context.moveTo(from.x + 0.5, from.y + 0.5);
1518 context.lineTo(to.x + 0.5, to.y + 0.5);
1519 context.stroke();
1522 context.restore();
1523 break;
1525 case RP_STROKE_POINT_COLOR:
1526 this.applyContext();
1528 var point = new RemotePoint(remoteMessage);
1530 context.save();
1531 context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
1533 context.fillRect(point.x, point.y, 1, 1);
1534 context.restore();
1535 break;
1537 case RP_STROKE_LINE_1PX_COLOR:
1538 this.applyContext();
1540 var from = new RemotePoint(remoteMessage);
1541 var to = new RemotePoint(remoteMessage);
1543 context.save();
1544 context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1545 context.lineWidth = 1;
1546 context.lineCap = 'square';
1548 context.beginPath();
1549 context.moveTo(from.x + 0.5, from.y + 0.5);
1550 context.lineTo(to.x + 0.5, to.y + 0.5);
1551 context.stroke();
1553 context.restore();
1554 break;
1556 case RP_STROKE_RECT_1PX_COLOR:
1557 this.applyContext();
1559 var rect = new RemoteRect(remoteMessage);
1561 context.save();
1562 this.prepareForRect();
1564 context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1565 context.lineWidth = 1;
1567 rect.apply(context.strokeRect.bind(context));
1569 context.restore();
1570 break;
1572 case RP_STROKE_SHAPE:
1573 case RP_FILL_SHAPE:
1574 case RP_FILL_SHAPE_GRADIENT:
1575 this.applyContext();
1577 var shape = new RemoteShape(remoteMessage);
1578 var offset = new RemotePoint(remoteMessage);
1579 var scale = remoteMessage.dataView.readFloat32();
1581 context.save();
1582 if (remoteMessage.code() == RP_FILL_SHAPE_GRADIENT) {
1583 var gradient = new RemoteGradient(remoteMessage, context,
1584 this.unsetAlpha);
1585 context.fillStyle = gradient.gradient;
1588 context.translate(offset.x + 0.5, offset.y + 0.5);
1589 context.scale(scale, scale);
1591 context.beginPath();
1593 shape.play(context);
1595 if (remoteMessage.code() == RP_STROKE_SHAPE)
1596 context.stroke();
1597 else
1598 context.fill();
1600 context.restore();
1601 break;
1603 case RP_STROKE_TRIANGLE:
1604 case RP_FILL_TRIANGLE:
1605 case RP_FILL_TRIANGLE_GRADIENT:
1606 this.applyContext();
1608 if (remoteMessage.code() == RP_FILL_TRIANGLE_GRADIENT)
1609 context.save();
1611 context.beginPath();
1612 var point = new RemotePoint(remoteMessage);
1613 context.moveTo(point.x + 0.5, point.y + 0.5);
1615 for (var i = 0; i < 2; i++) {
1616 point = new RemotePoint(remoteMessage);
1617 context.lineTo(point.x + 0.5, point.y + 0.5);
1620 if (remoteMessage.code() == RP_FILL_TRIANGLE_GRADIENT) {
1621 var unusedBounds = new RemoteRect(remoteMessage);
1622 var gradient = new RemoteGradient(remoteMessage, context,
1623 this.unsetAlpha);
1624 context.fillStyle = gradient.gradient;
1627 switch (remoteMessage.code()) {
1628 case RP_STROKE_TRIANGLE:
1629 context.closePath();
1630 context.stroke();
1631 break;
1633 case RP_FILL_TRIANGLE:
1634 context.fill();
1635 break;
1637 case RP_FILL_TRIANGLE_GRADIENT:
1638 context.fill();
1639 context.restore();
1640 break;
1643 break;
1645 case RP_FILL_RECT_COLOR:
1646 this.applyContext();
1648 var rect = new RemoteRect(remoteMessage);
1650 context.save();
1651 this.prepareForRect();
1652 context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
1654 rect.apply(context.fillRect.bind(context));
1656 context.restore();
1657 break;
1659 case RP_FILL_RECT_GRADIENT:
1660 case RP_FILL_ELLIPSE_GRADIENT:
1661 this.applyContext();
1663 var rect = new RemoteRect(remoteMessage);
1665 context.save();
1666 this.prepareForRect();
1668 var gradient = new RemoteGradient(remoteMessage, context,
1669 this.unsetAlpha);
1670 context.fillStyle = gradient.gradient;
1672 if (remoteMessage.code() == RP_FILL_RECT_GRADIENT)
1673 rect.apply(context.fillRect.bind(context));
1674 else
1675 rect.applyAsEllipse(context, context.fill);
1677 context.restore();
1678 break;
1680 case RP_FILL_REGION:
1681 case RP_FILL_REGION_GRADIENT:
1682 this.applyContext();
1684 var rectCount = remoteMessage.dataView.readUint32();
1685 var rects = new Array(rectCount);
1686 for (var i = 0; i < rectCount; i++)
1687 rects[i] = new RemoteRect(remoteMessage);
1689 if (remoteMessage.code() == RP_FILL_REGION_GRADIENT) {
1690 context.save();
1691 var gradient = new RemoteGradient(remoteMessage, context,
1692 this.unsetAlpha);
1693 context.fillStyle = gradient.gradient;
1696 for (var i = 0; i < rectCount; i++)
1697 rects[i].apply(context.fillRect.bind(context));
1699 if (remoteMessage.code() == RP_FILL_REGION_GRADIENT)
1700 context.restore();
1702 break;
1704 case RP_READ_BITMAP:
1705 var bounds = new RemoteRect(remoteMessage);
1706 var drawCursor = remoteMessage.dataView.readUint8();
1707 // TODO: Support the drawCursor flag.
1709 if (drawCursor)
1710 console.warn('draw cursor in read bitmap not supported');
1712 var width = bounds.integerWidth() + 1;
1713 var height = bounds.integerHeight() + 1;
1714 var bytesPerPixel = 3;
1715 var bytesPerRow = (width * bytesPerPixel + 3) & ~7;
1716 var padding = bytesPerRow - width * bytesPerPixel;
1717 var bitsLength = height * bytesPerRow;
1719 reply.ensureBufferSize(bitsLength + 1024);
1721 reply.start(RP_READ_BITMAP_RESULT);
1722 reply.dataView.writeInt32(this.token);
1724 reply.dataView.writeInt32(width);
1725 reply.dataView.writeInt32(height);
1726 reply.dataView.writeInt32(bytesPerRow);
1727 reply.dataView.writeUint32(B_RGB24);
1728 reply.dataView.writeUint32(0); // Flags
1729 reply.dataView.writeUint32(bitsLength);
1731 var position = 0;
1732 var imageData
1733 = context.getImageData(bounds.left, bounds.top, width, height);
1734 for (var y = 0; y < height; y++) {
1735 for (var x = 0; x < width; x++, position += 4) {
1736 reply.dataView.writeUint8(imageData.data[position + 2]);
1737 reply.dataView.writeUint8(imageData.data[position + 1]);
1738 reply.dataView.writeUint8(imageData.data[position + 0]);
1741 reply.dataView.pad(padding);
1744 reply.flush();
1745 break;
1747 default:
1748 console.warn('unhandled message: code: ' + remoteMessage.code()
1749 + '; size: ' + remoteMessage.size());
1750 break;
1755 function RemoteDesktopSession(targetElement, width, height, targetAddress,
1756 disconnectCallback)
1758 this.websocket = new WebSocket(targetAddress, 'binary');
1759 this.websocket.binaryType = 'arraybuffer';
1760 this.websocket.onopen = this.onOpen.bind(this);
1761 this.websocket.onmessage = this.onMessage.bind(this);
1762 this.websocket.onerror = this.onError.bind(this);
1763 this.websocket.onclose = this.onClose.bind(this);
1765 this.disconnectCallback = disconnectCallback;
1767 this.sendMessage = new RemoteMessage(this.websocket);
1768 this.sendMessage.allocate(1024);
1770 this.receiveMessage = new RemoteMessage();
1772 this.container = document.createElement('div');
1773 this.container.className = 'session';
1774 this.container.style.position = 'relative';
1775 targetElement.appendChild(this.container);
1777 this.canvas = document.createElement('canvas');
1778 this.canvas.className = 'session';
1779 this.canvas.width = width;
1780 this.canvas.height = height;
1781 this.container.appendChild(this.canvas);
1783 this.canvas.tabIndex = 0;
1784 this.canvas.focus();
1786 this.context = this.canvas.getContext('2d', { alpha: false });
1787 this.context.imageSmoothingEnabled = false;
1789 this.cursorVisible = true;
1790 this.cursorPosition = { x: 0, y: 0 };
1791 this.cursorHotspot = { x: 0, y: 0 };
1793 this.states = new Object();
1794 this.modifiers = 0;
1796 this.canvas.onmousemove = this.onMouseMove.bind(this);
1797 this.canvas.onmousedown = this.onMouseDown.bind(this);
1798 this.canvas.onmouseup = this.onMouseUp.bind(this);
1799 this.canvas.onwheel = this.onWheel.bind(this);
1801 this.canvas.onkeydown = this.onKeyDownUp.bind(this);
1802 this.canvas.onkeyup = this.onKeyDownUp.bind(this);
1803 this.canvas.onkeypress = this.onKeyPress.bind(this);
1805 this.canvas.oncontextmenu = function(event) {
1806 event.preventDefault();
1809 this.canvas.onblur = function(event) {
1810 event.target.focus();
1815 RemoteDesktopSession.prototype.onOpen = function(open)
1817 console.log('open:', open);
1818 this.init();
1822 RemoteDesktopSession.prototype.onMessage = function(message)
1824 var data = message.data;
1825 if (this.messageRemainder) {
1826 var combined = new Uint8Array(this.messageRemainder.byteLength
1827 + data.byteLength);
1828 combined.set(new Uint8Array(this.messageRemainder), 0);
1829 combined.set(new Uint8Array(data), this.messageRemainder.byteLength);
1830 data = combined;
1832 this.messageRemainder = null;
1833 } else
1834 data = new Uint8Array(data);
1836 var byteOffset = 0;
1837 while (true) {
1838 try {
1839 if (!this.receiveMessage.attach(data, byteOffset))
1840 break;
1841 } catch (exception) {
1842 // Discard everything and hope for the best.
1843 console.error('stream invalid, discarding everything', exception,
1844 this.receiveMessage, data, byteOffset);
1845 return;
1848 try {
1849 this.messageReceived(this.receiveMessage, this.sendMessage);
1850 } catch (exception) {
1851 console.error('exception during message processing:', exception);
1854 byteOffset += this.receiveMessage.size();
1857 if (data.byteLength > byteOffset)
1858 this.messageRemainder = data.slice(byteOffset);
1862 RemoteDesktopSession.prototype.messageReceived = function(remoteMessage, reply)
1864 switch (remoteMessage.code()) {
1865 case RP_INIT_CONNECTION:
1866 console.log('init connection reply');
1867 this.sendMessage.start(RP_UPDATE_DISPLAY_MODE);
1868 this.sendMessage.dataView.writeUint32(this.canvas.width);
1869 this.sendMessage.dataView.writeUint32(this.canvas.height);
1870 this.sendMessage.flush();
1872 this.sendMessage.start(RP_GET_SYSTEM_PALETTE);
1873 this.sendMessage.flush();
1874 break;
1876 case RP_GET_SYSTEM_PALETTE_RESULT:
1877 var count = remoteMessage.dataView.readUint32();
1878 gSystemPalette = new Uint32Array(count);
1880 var color = new RemoteColor();
1881 for (var i = 0; i < gSystemPalette.length; i++)
1882 gSystemPalette[i] = color.readFrom(remoteMessage).toUint32();
1884 break;
1886 case RP_CREATE_STATE:
1887 var token = remoteMessage.dataView.readInt32();
1888 console.log('create state: ' + token);
1890 if (this.states.hasOwnProperty(token))
1891 console.error('create state for existing token: ' + token);
1893 this.states[token] = new RemoteState(this, token);
1894 break;
1896 case RP_DELETE_STATE:
1897 var token = remoteMessage.dataView.readInt32();
1898 console.log('delete state: ' + token);
1900 if (!this.states.hasOwnProperty(token)) {
1901 console.error('delete state for unknown token: ' + token);
1902 break;
1905 delete this.states[token];
1906 break;
1908 case RP_INVALIDATE_RECT:
1909 case RP_INVALIDATE_REGION:
1910 break;
1912 case RP_SET_CURSOR:
1913 this.cursorHotspot = new RemotePoint(remoteMessage);
1914 var bitmap = new RemoteBitmap(remoteMessage);
1916 bitmap.canvas.style.position = 'absolute';
1917 if (this.cursorCanvas)
1918 this.cursorCanvas.remove();
1920 this.cursorCanvas = bitmap.canvas;
1921 this.cursorCanvas.style.pointerEvents = 'none';
1922 this.container.appendChild(this.cursorCanvas);
1923 this.container.style.cursor = 'none';
1924 this.updateCursor();
1925 break;
1927 case RP_MOVE_CURSOR_TO:
1928 this.cursorPosition.x = remoteMessage.dataView.readFloat32();
1929 this.cursorPosition.y = remoteMessage.dataView.readFloat32();
1930 this.updateCursor();
1931 break;
1933 case RP_SET_CURSOR_VISIBLE:
1934 this.cursorVisible = remoteMessage.dataView.readUint8();
1935 if (this.cursorCanvas) {
1936 this.cursorCanvas.style.visibility
1937 = this.cursorVisible ? 'visible' : 'hidden';
1939 break;
1941 case RP_COPY_RECT_NO_CLIPPING:
1942 var xOffset = remoteMessage.dataView.readInt32();
1943 var yOffset = remoteMessage.dataView.readInt32();
1944 var rect = new RemoteRect(remoteMessage);
1946 var imageData = this.context.getImageData(rect.left, rect.top,
1947 rect.width(), rect.height());
1948 this.context.putImageData(imageData, rect.left + xOffset,
1949 rect.top + yOffset);
1950 break;
1952 case RP_FILL_REGION_COLOR_NO_CLIPPING:
1953 this.removeClipping();
1954 this.context.currentToken = -1;
1955 this.context.resetTransform();
1956 this.context.globalCompositeOperation = 'source-over';
1958 var rectCount = remoteMessage.dataView.readUint32();
1959 var rects = new Array(rectCount);
1960 for (var i = 0; i < rectCount; i++)
1961 rects[i] = new RemoteRect(remoteMessage);
1963 this.context.fillStyle = remoteMessage.readColor();
1965 for (var i = 0; i < rectCount; i++)
1966 rects[i].apply(this.context.fillRect.bind(this.context));
1968 break;
1970 default:
1971 var token = remoteMessage.dataView.readInt32();
1972 if (!this.states.hasOwnProperty(token)) {
1973 console.warn('no state for token: ' + token);
1974 this.states[token] = new RemoteState(this, token);
1977 this.states[token].messageReceived(remoteMessage, reply);
1978 break;
1983 RemoteDesktopSession.prototype.onError = function(error)
1985 console.log('websocket error:', error);
1986 this.onDisconnect(error);
1990 RemoteDesktopSession.prototype.onClose = function(close)
1992 console.log('websocket close:', close);
1993 this.onDisconnect(close);
1997 RemoteDesktopSession.prototype.onDisconnect = function(reason)
1999 this.container.remove();
2000 if (this.disconnectCallback)
2001 this.disconnectCallback(reason);
2005 RemoteDesktopSession.prototype.applyClipping = function(clipRects)
2007 this.removeClipping();
2009 if (!clipRects || clipRects.length == 0)
2010 return;
2012 this.context.save();
2013 this.context.beginPath();
2015 this.context.save();
2016 this.context.lineJoin = 'miter';
2017 this.context.miterLimit = 10;
2019 for (var i = 0; i < clipRects.length; i++)
2020 clipRects[i].apply(this.context.rect.bind(this.context));
2022 this.context.restore();
2024 this.context.clip();
2025 this.clippingApplied = true;
2029 RemoteDesktopSession.prototype.removeClipping = function()
2031 if (!this.clippingApplied)
2032 return;
2034 this.context.restore();
2038 RemoteDesktopSession.prototype.init = function()
2040 this.sendMessage.start(RP_INIT_CONNECTION);
2041 this.sendMessage.flush();
2045 RemoteDesktopSession.prototype.updateCursor = function()
2047 if (!this.cursorVisible || !this.cursorCanvas)
2048 return;
2050 this.cursorCanvas.style.left
2051 = (this.cursorPosition.x - this.cursorHotspot.x) + 'px';
2052 this.cursorCanvas.style.top
2053 = (this.cursorPosition.y - this.cursorHotspot.y) + 'px';
2057 RemoteDesktopSession.prototype.onMouseMove = function(event)
2059 this.sendMessage.start(RP_MOUSE_MOVED);
2060 this.sendMessage.dataView.writeFloat32(event.offsetX);
2061 this.sendMessage.dataView.writeFloat32(event.offsetY);
2062 this.sendMessage.flush();
2063 event.preventDefault();
2067 RemoteDesktopSession.prototype.onMouseDown = function(event)
2069 this.canvas.focus();
2070 this.sendMessage.start(RP_MOUSE_DOWN);
2071 this.sendMessage.dataView.writeFloat32(event.offsetX);
2072 this.sendMessage.dataView.writeFloat32(event.offsetY);
2073 this.sendMessage.dataView.writeUint32(event.buttons);
2074 this.sendMessage.dataView.writeUint32(event.detail);
2075 this.sendMessage.flush();
2076 event.preventDefault();
2080 RemoteDesktopSession.prototype.onMouseUp = function(event)
2082 this.sendMessage.start(RP_MOUSE_UP);
2083 this.sendMessage.dataView.writeFloat32(event.offsetX);
2084 this.sendMessage.dataView.writeFloat32(event.offsetY);
2085 this.sendMessage.dataView.writeUint32(event.buttons);
2086 this.sendMessage.flush();
2087 event.preventDefault();
2091 RemoteDesktopSession.prototype.onKeyDownUp = function(event)
2093 var keyDown = event.type === 'keydown';
2094 var lockModifier = false;
2095 var modifiersChanged = 0;
2096 switch (event.code) {
2097 case 'ShiftLeft':
2098 modifiersChanged |= B_LEFT_SHIFT_KEY;
2099 if (event.shiftKey == keyDown)
2100 modifiersChanged |= B_SHIFT_KEY;
2101 break;
2103 case 'ShiftRight':
2104 modifiersChanged |= B_RIGHT_SHIFT_KEY;
2105 if (event.shiftKey == keyDown)
2106 modifiersChanged |= B_SHIFT_KEY;
2107 break;
2109 case 'ControlLeft':
2110 modifiersChanged |= B_LEFT_CONTROL_KEY;
2111 if (event.ctrlKey == keyDown)
2112 modifiersChanged |= B_CONTROL_KEY;
2113 break;
2115 case 'ControlRight':
2116 modifiersChanged |= B_RIGHT_CONTROL_KEY;
2117 if (event.ctrlKey == keyDown)
2118 modifiersChanged |= B_CONTROL_KEY;
2119 break;
2121 case 'AltLeft':
2122 modifiersChanged |= B_LEFT_COMMAND_KEY;
2123 if (event.altKey == keyDown)
2124 modifiersChanged |= B_COMMAND_KEY;
2125 break;
2127 case 'AltRight':
2128 modifiersChanged |= B_RIGHT_COMMAND_KEY;
2129 if (event.altKey == keyDown)
2130 modifiersChanged |= B_COMMAND_KEY;
2131 break;
2133 case 'ContextMenu':
2134 modifiersChanged |= B_MENU_KEY;
2135 break;
2137 case 'CapsLock':
2138 modifiersChanged |= B_CAPS_LOCK;
2139 lockModifier = true;
2140 break;
2142 case 'ScrollLock':
2143 modifiersChanged |= B_SCROLL_LOCK;
2144 lockModifier = true;
2145 break;
2147 case 'NumLock':
2148 modifiersChanged |= B_NUM_LOCK;
2149 lockModifier = true;
2150 break;
2153 if (modifiersChanged != 0) {
2154 if (lockModifier) {
2155 if (((this.modifiers & modifiersChanged) == 0) == keyDown)
2156 this.modifiers ^= modifiersChanged;
2157 } else {
2158 if (keyDown)
2159 this.modifiers |= modifiersChanged;
2160 else
2161 this.modifiers &= ~modifiersChanged;
2164 this.sendMessage.start(RP_MODIFIERS_CHANGED);
2165 this.sendMessage.dataView.writeUint32(this.modifiers);
2166 this.sendMessage.flush();
2167 event.preventDefault();
2168 return;
2171 this.sendMessage.start(keyDown ? RP_KEY_DOWN : RP_KEY_UP);
2172 if (event.key.length == 1)
2173 this.sendMessage.dataView.writeString(event.key);
2174 else {
2175 this.sendMessage.dataView.writeUint32(1);
2176 this.sendMessage.dataView.writeUint8(event.keyCode);
2179 if (event.keyCode) {
2180 this.sendMessage.dataView.writeUint32(0);
2181 this.sendMessage.dataView.writeUint32(event.keyCode);
2184 this.sendMessage.flush();
2185 event.preventDefault();
2189 RemoteDesktopSession.prototype.onKeyPress = function(event)
2191 this.sendMessage.start(RP_KEY_DOWN);
2192 this.sendMessage.dataView.writeUint32(1);
2193 this.sendMessage.dataView.writeUint8(event.which);
2194 this.sendMessage.flush();
2195 this.sendMessage.start(RP_KEY_UP);
2196 this.sendMessage.dataView.writeUint32(1);
2197 this.sendMessage.dataView.writeUint8(event.which);
2198 this.sendMessage.flush();
2199 event.preventDefault();
2203 RemoteDesktopSession.prototype.onWheel = function(event)
2205 this.sendMessage.start(RP_MOUSE_WHEEL_CHANGED);
2206 this.sendMessage.dataView.writeFloat32(event.deltaX);
2207 this.sendMessage.dataView.writeFloat32(event.deltaY);
2208 this.sendMessage.flush();
2209 event.preventDefault();
2213 function init()
2215 var targetAddressInput = document.querySelector('#targetAddress');
2216 var widthInput = document.querySelector('#width');
2217 var heightInput = document.querySelector('#height');
2219 if (localStorage.targetAddress)
2220 targetAddressInput.value = localStorage.targetAddress;
2221 if (localStorage.width)
2222 widthInput.value = localStorage.width;
2223 if (localStorage.height)
2224 heightInput.value = localStorage.height;
2226 var onDisconnect = function(reason) {
2227 document.body.classList.remove('connect');
2228 gSession = undefined;
2231 document.querySelector('#connectButton').onclick = function() {
2232 document.body.classList.add('connect');
2234 localStorage.width = widthInput.value;
2235 localStorage.height = heightInput.value;
2236 localStorage.targetAddress = targetAddressInput.value;
2238 gSession = new RemoteDesktopSession(document.body, widthInput.value,
2239 heightInput.value, targetAddressInput.value, onDisconnect);