xref: /haiku/src/tools/html5_remote_desktop/HaikuRemoteDesktop.js (revision 1e60bdeab63fa7a57bc9a55b032052e95a18bd2c)
1'use strict';
2
3const RP_INIT_CONNECTION = 1;
4const RP_UPDATE_DISPLAY_MODE = 2;
5const RP_CLOSE_CONNECTION = 3;
6const RP_GET_SYSTEM_PALETTE = 4;
7const RP_GET_SYSTEM_PALETTE_RESULT = 5;
8
9const RP_CREATE_STATE = 20;
10const RP_DELETE_STATE = 21;
11const RP_ENABLE_SYNC_DRAWING = 22;
12const RP_DISABLE_SYNC_DRAWING = 23;
13const RP_INVALIDATE_RECT = 24;
14const RP_INVALIDATE_REGION = 25;
15
16const RP_SET_OFFSETS = 40;
17const RP_SET_HIGH_COLOR = 41;
18const RP_SET_LOW_COLOR = 42;
19const RP_SET_PEN_SIZE = 43;
20const RP_SET_STROKE_MODE = 44;
21const RP_SET_BLENDING_MODE = 45;
22const RP_SET_PATTERN = 46;
23const RP_SET_DRAWING_MODE = 47;
24const RP_SET_FONT = 48;
25const RP_SET_TRANSFORM = 49;
26
27const RP_CONSTRAIN_CLIPPING_REGION = 60;
28const RP_COPY_RECT_NO_CLIPPING = 61;
29const RP_INVERT_RECT = 62;
30const RP_DRAW_BITMAP = 63;
31const RP_DRAW_BITMAP_RECTS = 64;
32
33const RP_STROKE_ARC = 80;
34const RP_STROKE_BEZIER = 81;
35const RP_STROKE_ELLIPSE = 82;
36const RP_STROKE_POLYGON = 83;
37const RP_STROKE_RECT = 84;
38const RP_STROKE_ROUND_RECT = 85;
39const RP_STROKE_SHAPE = 86;
40const RP_STROKE_TRIANGLE = 87;
41const RP_STROKE_LINE = 88;
42const RP_STROKE_LINE_ARRAY = 89;
43
44const RP_FILL_ARC = 100;
45const RP_FILL_BEZIER = 101;
46const RP_FILL_ELLIPSE = 102;
47const RP_FILL_POLYGON = 103;
48const RP_FILL_RECT = 104;
49const RP_FILL_ROUND_RECT = 105;
50const RP_FILL_SHAPE = 106;
51const RP_FILL_TRIANGLE = 107;
52const RP_FILL_REGION = 108;
53
54const RP_FILL_ARC_GRADIENT = 120;
55const RP_FILL_BEZIER_GRADIENT = 121;
56const RP_FILL_ELLIPSE_GRADIENT = 122;
57const RP_FILL_POLYGON_GRADIENT = 123;
58const RP_FILL_RECT_GRADIENT = 124;
59const RP_FILL_ROUND_RECT_GRADIENT = 125;
60const RP_FILL_SHAPE_GRADIENT = 126;
61const RP_FILL_TRIANGLE_GRADIENT = 127;
62const RP_FILL_REGION_GRADIENT = 128;
63
64const RP_STROKE_POINT_COLOR = 140;
65const RP_STROKE_LINE_1PX_COLOR = 141;
66const RP_STROKE_RECT_1PX_COLOR = 142;
67
68const RP_FILL_RECT_COLOR = 160;
69const RP_FILL_REGION_COLOR_NO_CLIPPING = 161;
70
71const RP_DRAW_STRING = 180;
72const RP_DRAW_STRING_WITH_OFFSETS = 181;
73const RP_DRAW_STRING_RESULT = 182;
74const RP_STRING_WIDTH = 183;
75const RP_STRING_WIDTH_RESULT = 184;
76const RP_READ_BITMAP = 185;
77const RP_READ_BITMAP_RESULT = 186;
78
79const RP_SET_CURSOR = 200;
80const RP_SET_CURSOR_VISIBLE = 201;
81const RP_MOVE_CURSOR_TO = 202;
82
83const RP_MOUSE_MOVED = 220;
84const RP_MOUSE_DOWN = 221;
85const RP_MOUSE_UP = 222;
86const RP_MOUSE_WHEEL_CHANGED = 223;
87
88const RP_KEY_DOWN = 240;
89const RP_KEY_UP = 241;
90const RP_UNMAPPED_KEY_DOWN = 242;
91const RP_UNMAPPED_KEY_UP = 243;
92const RP_MODIFIERS_CHANGED = 244;
93
94
95// drawing_mode
96const B_OP_COPY = 0;
97const B_OP_OVER = 1;
98const B_OP_ERASE = 2;
99const B_OP_INVERT = 3;
100const B_OP_ADD = 4;
101const B_OP_SUBTRACT = 5;
102const B_OP_BLEND = 6;
103const B_OP_MIN = 7;
104const B_OP_MAX = 8;
105const B_OP_SELECT = 9;
106const B_OP_ALPHA = 10;
107
108
109// color_space
110const B_NO_COLOR_SPACE = 0x0000;
111const B_RGB32 = 0x0008;			// BGR- 8:8:8:8
112const B_RGBA32 = 0x2008;		// BGRA	8:8:8:8
113const B_RGB24 = 0x0003;			// BGR	8:8:8
114const B_RGB16 = 0x0005;			// BGR	5:6:5
115const B_RGB15 = 0x0010;			// BGR- 5:5:5:1
116const B_RGBA15 = 0x2010;		// BGRA 5:5:5:1
117const B_CMAP8 = 0x0004;			// 256 color index table
118const B_GRAY8 = 0x0002;			// 256 greyscale table
119const B_GRAY1 = 0x0001;			// Each bit represents a single pixel
120const B_RGB32_BIG = 0x1008;		// -RGB	8:8:8:8
121const B_RGBA32_BIG = 0x3008;	// ARGB	8:8:8:8
122const B_RGB24_BIG = 0x1003;		// RGB	8:8:8
123const B_RGB16_BIG = 0x1005;		// RGB	5:6:5
124const B_RGB15_BIG = 0x1010;		// -RGB	1:5:5:5
125const B_RGBA15_BIG = 0x3010;	// ARGB	1:5:5:5
126
127const B_TRANSPARENT_MAGIC_CMAP8 = 0xff;
128const B_TRANSPARENT_MAGIC_RGBA15 = 0x39ce;
129const B_TRANSPARENT_MAGIC_RGBA15_BIG = 0xce39;
130const B_TRANSPARENT_MAGIC_RGBA32 = 0xff777477;
131const B_TRANSPARENT_MAGIC_RGBA32_BIG = 0x777477ff;
132
133
134// source_alpha
135const B_PIXEL_ALPHA = 0;
136const B_CONSTANT_ALPHA = 1;
137
138
139// alpha_function
140const B_ALPHA_OVERLAY = 0;
141const B_ALPHA_COMPOSITE = 1;
142
143
144// BGradient::Type
145const B_GRADIENT_TYPE_LINEAR = 0;
146const B_GRADIENT_TYPE_RADIAL = 1;
147const B_GRADIENT_TYPE_RADIAL_FOCUS = 2;
148const B_GRADIENT_TYPE_DIAMOND = 3;
149const B_GRADIENT_TYPE_CONIC = 4;
150const B_GRADIENT_TYPE_NONE = 5;
151
152
153// BShape ops
154const B_SHAPE_OP_MOVE_TO = 0x80000000;
155const B_SHAPE_OP_CLOSE = 0x40000000;
156const B_SHAPE_OP_BEZIER_TO = 0x20000000;
157const B_SHAPE_OP_LINE_TO = 0x10000000;
158const B_SHAPE_OP_SMALL_ARC_TO_CCW = 0x08000000;
159const B_SHAPE_OP_SMALL_ARC_TO_CW = 0x04000000;
160const B_SHAPE_OP_LARGE_ARC_TO_CCW = 0x02000000;
161const B_SHAPE_OP_LARGE_ARC_TO_CW = 0x01000000;
162
163
164// Line join_modes
165const B_ROUND_JOIN = 0;
166const B_MITER_JOIN = 1;
167const B_BEVEL_JOIN = 2;
168const B_BUTT_JOIN = 3;
169const B_SQUARE_JOIN = 4;
170
171
172// Line cap_modes
173const B_ROUND_CAP = B_ROUND_JOIN;
174const B_BUTT_CAP = B_BUTT_JOIN;
175const B_SQUARE_CAP = B_SQUARE_JOIN;
176
177
178const B_DEFAULT_MITER_LIMIT = 10;
179
180
181// modifiers
182const B_SHIFT_KEY = 0x00000001;
183const B_COMMAND_KEY = 0x00000002;
184const B_CONTROL_KEY = 0x00000004;
185const B_CAPS_LOCK = 0x00000008;
186const B_SCROLL_LOCK = 0x00000010;
187const B_NUM_LOCK = 0x00000020;
188const B_OPTION_KEY = 0x00000040;
189const B_MENU_KEY = 0x00000080;
190const B_LEFT_SHIFT_KEY = 0x00000100;
191const B_RIGHT_SHIFT_KEY = 0x00000200;
192const B_LEFT_COMMAND_KEY = 0x00000400;
193const B_RIGHT_COMMAND_KEY = 0x00000800;
194const B_LEFT_CONTROL_KEY = 0x00001000;
195const B_RIGHT_CONTROL_KEY = 0x00002000;
196const B_LEFT_OPTION_KEY = 0x00004000;
197const B_RIGHT_OPTION_KEY = 0x00008000;
198
199
200
201var gSession;
202var gSystemPalette;
203
204
205function StreamingDataView(buffer, littleEndian, byteOffset, byteLength)
206{
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();
213}
214
215
216StreamingDataView.prototype.rewind = function()
217{
218	this.position = 0;
219}
220
221
222StreamingDataView.prototype.readInt8 = function()
223{
224	return this.dataView.getInt8(this.position++);
225}
226
227
228StreamingDataView.prototype.readUint8 = function()
229{
230	return this.dataView.getUint8(this.position++);
231}
232
233
234StreamingDataView.prototype.readInt16 = function()
235{
236	var result = this.dataView.getInt16(this.position, this.littleEndian);
237	this.position += 2;
238	return result;
239}
240
241
242StreamingDataView.prototype.readUint16 = function()
243{
244	var result = this.dataView.getUint16(this.position, this.littleEndian);
245	this.position += 2;
246	return result;
247}
248
249
250StreamingDataView.prototype.readInt32 = function()
251{
252	var result = this.dataView.getInt32(this.position, this.littleEndian);
253	this.position += 4;
254	return result;
255}
256
257
258StreamingDataView.prototype.readUint32 = function()
259{
260	var result = this.dataView.getUint32(this.position, this.littleEndian);
261	this.position += 4;
262	return result;
263}
264
265
266StreamingDataView.prototype.readFloat32 = function()
267{
268	var result = this.dataView.getFloat32(this.position, this.littleEndian);
269	this.position += 4;
270	return result;
271}
272
273
274StreamingDataView.prototype.readFloat64 = function()
275{
276	var result = this.dataView.getFloat64(this.position, this.littleEndian);
277	this.position += 8;
278	return result;
279}
280
281
282StreamingDataView.prototype.readString = function(length)
283{
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;
289}
290
291
292StreamingDataView.prototype.readInto = function(typedArray)
293{
294	var where = this.dataView.byteOffset + this.position;
295	typedArray.set(this.buffer.slice(where, where + typedArray.byteLength));
296	this.position += typedArray.byteLength;
297}
298
299
300StreamingDataView.prototype.writeInt8 = function(value)
301{
302	this.dataView.setInt8(this.position++, value);
303}
304
305
306StreamingDataView.prototype.writeUint8 = function(value)
307{
308	this.dataView.setUint8(this.position++, value);
309}
310
311
312StreamingDataView.prototype.writeInt16 = function(value)
313{
314	this.dataView.setInt16(this.position, value, this.littleEndian);
315	this.position += 2;
316}
317
318
319StreamingDataView.prototype.writeUint16 = function(value)
320{
321	this.dataView.setUint16(this.position, value, this.littleEndian);
322	this.position += 2;
323}
324
325
326StreamingDataView.prototype.writeInt32 = function(value)
327{
328	this.dataView.setInt32(this.position, value, this.littleEndian);
329	this.position += 4;
330}
331
332
333StreamingDataView.prototype.writeUint32 = function(value)
334{
335	this.dataView.setUint32(this.position, value, this.littleEndian);
336	this.position += 4;
337}
338
339
340StreamingDataView.prototype.writeFloat32 = function(value)
341{
342	this.dataView.setFloat32(this.position, value, this.littleEndian);
343	this.position += 4;
344}
345
346
347StreamingDataView.prototype.writeFloat64 = function(value)
348{
349	this.dataView.setFloat64(this.position, value, this.littleEndian);
350	this.position += 8;
351}
352
353
354StreamingDataView.prototype.writeString = function(string)
355{
356	var encoded = this.textEncoder.encode(string);
357	this.writeUint32(encoded.length);
358	this.buffer.set(encoded, this.position);
359	this.position += encoded.length;
360}
361
362
363StreamingDataView.prototype.setUint32 = function(byteOffset, value)
364{
365	this.dataView.setUint32(byteOffset, value, this.littleEndian);
366}
367
368
369StreamingDataView.prototype.pad = function(length)
370{
371	this.buffer.fill(0, this.position, this.position + length);
372	this.position += length;
373}
374
375
376function RemotePoint(remoteMessage)
377{
378	if (remoteMessage) {
379		this.readFrom(remoteMessage);
380		return;
381	}
382
383	this.x = 0;
384	this.y = 0;
385}
386
387
388RemotePoint.prototype.readFrom = function(remoteMessage)
389{
390	this.x = remoteMessage.dataView.readFloat32();
391	this.y = remoteMessage.dataView.readFloat32();
392	return this;
393}
394
395
396RemotePoint.prototype.writeTo = function(remoteMessage)
397{
398	remoteMessage.dataView.writeFloat32(this.x);
399	remoteMessage.dataView.writeFloat32(this.y);
400	return this;
401}
402
403
404function RemoteRect(remoteMessage)
405{
406	if (remoteMessage) {
407		this.readFrom(remoteMessage);
408		return;
409	}
410
411	this.left = 0;
412	this.top = 0;
413	this.right = -1;
414	this.bottom = -1;
415}
416
417
418RemoteRect.prototype.readFrom = function(remoteMessage)
419{
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;
425}
426
427
428RemoteRect.prototype.width = function()
429{
430	return this.right - this.left + 1;
431}
432
433
434RemoteRect.prototype.height = function()
435{
436	return this.bottom - this.top + 1;
437}
438
439
440RemoteRect.prototype.integerWidth = function()
441{
442	return Math.ceil(this.right - this.left);
443}
444
445
446RemoteRect.prototype.integerHeight = function()
447{
448	return Math.ceil(this.bottom - this.top);
449}
450
451
452RemoteRect.prototype.centerX = function()
453{
454	return this.left + this.width() / 2;
455}
456
457
458RemoteRect.prototype.centerY = function()
459{
460	return this.top + this.height() / 2;
461}
462
463
464RemoteRect.prototype.apply = function(apply)
465{
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);
471}
472
473
474RemoteRect.prototype.applyAsEllipse = function(context, which)
475{
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);
480}
481
482
483function RemoteColor(remoteMessage)
484{
485	if (remoteMessage) {
486		this.readFrom(remoteMessage);
487		return;
488	}
489
490	this.red = 0;
491	this.green = 0;
492	this.blue = 0;
493	this.alpha = 0;
494}
495
496
497RemoteColor.prototype.readFrom = function(remoteMessage)
498{
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;
504}
505
506
507RemoteColor.prototype.fromUint32 = function(value)
508{
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;
514}
515
516
517RemoteColor.prototype.toColor = function(unsetAlpha)
518{
519	return 'rgba(' + this.red + ', ' + this.green + ', ' + this.blue + ', '
520		+ (unsetAlpha ? 1 : this.alpha / 255) + ')';
521}
522
523
524RemoteColor.prototype.toUint32 = function(unsetAlpha)
525{
526	return this.red | this.green << 8 | this.blue << 16
527		| (unsetAlpha ? 255 : this.alpha) << 24;
528}
529
530
531function RemoteFont(remoteMessage)
532{
533	if (remoteMessage) {
534		this.readFrom(remoteMessage);
535		return;
536	}
537
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;
549}
550
551
552RemoteFont.prototype.readFrom = function(remoteMessage)
553{
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;
566}
567
568
569function RemoteTransform(remoteMessage)
570{
571	if (remoteMessage) {
572		this.readFrom(remoteMessage);
573		return;
574	}
575
576	this.setIdentity();
577}
578
579
580RemoteTransform.prototype.readFrom = function(remoteMessage)
581{
582	var isIdentity = remoteMessage.dataView.readUint8();
583	if (isIdentity) {
584		this.setIdentity();
585		return;
586	}
587
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;
595}
596
597
598RemoteTransform.prototype.setIdentity = function()
599{
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;
607}
608
609
610RemoteTransform.prototype.isIdentity = function()
611{
612	return this.sx == 1 && this.shy == 0 && this.shx == 0 && this.sy == 1
613		&& this.tx == 0 && this.ty == 0;
614}
615
616
617RemoteTransform.prototype.apply = function(context)
618{
619	context.transform(this.sx, this.shy, this.shx, this.sy, this.tx,
620		this.ty);
621}
622
623
624function RemoteBitmap(remoteMessage, unsetAlpha, colorSpace, flags)
625{
626	if (remoteMessage) {
627		this.readFrom(remoteMessage, unsetAlpha, colorSpace, flags);
628		return;
629	}
630}
631
632
633RemoteBitmap.prototype.readFrom = function(remoteMessage, unsetAlpha,
634	colorSpace, flags)
635{
636	this.width = remoteMessage.dataView.readUint32();
637	this.height = remoteMessage.dataView.readUint32();
638	this.bytesPerRow = remoteMessage.dataView.readUint32();
639
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();
646	}
647
648	this.bitsLength = remoteMessage.dataView.readUint32();
649
650	this.canvas = document.createElement('canvas');
651	this.canvas.width = this.width;
652	this.canvas.height = this.height;
653
654	if (this.width == 0 || this.height == 0)
655		return;
656
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);
663
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);
667			}
668
669			if (unsetAlpha) {
670				for (var i = 0; i < imageData.data.length / 4; i++)
671					output[i] |= 0xff000000;
672			}
673
674			break;
675
676		case B_RGB32:
677			remoteMessage.dataView.readInto(imageData.data);
678			var output = new Uint32Array(imageData.data.buffer);
679
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;
683
684				if (!unsetAlpha && output[i] == B_TRANSPARENT_MAGIC_RGBA32)
685					output[i] &= 0x00ffffff;
686			}
687			break;
688
689		case B_RGB24:
690			var line = new Uint8Array(this.bytesPerRow);
691			var position = 0;
692
693			for (var y = 0; y < this.height; y++) {
694				remoteMessage.dataView.readInto(line);
695
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;
701				}
702			}
703
704			break;
705
706		case B_RGB16:
707			var lineBuffer = new Uint8Array(this.bytesPerRow);
708			var line = new Uint16Array(lineBuffer.buffer);
709			var position = 0;
710
711			for (var y = 0; y < this.height; y++) {
712				remoteMessage.dataView.readInto(lineBuffer);
713
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;
719				}
720			}
721
722			break;
723
724		case B_CMAP8:
725			var line = new Uint8Array(this.bytesPerRow);
726			var output = new Uint32Array(imageData.data.buffer);
727			var position = 0;
728
729			for (var y = 0; y < this.height; y++) {
730				remoteMessage.dataView.readInto(line);
731
732				for (var x = 0; x < this.width; x++)
733					output[position++] = gSystemPalette[line[x]];
734			}
735
736			break;
737
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;
746			}
747			break;
748
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;
758			}
759			break;
760
761		default:
762			console.warn('color space not implemented: ' + this.colorSpace);
763			break;
764	}
765
766	context.putImageData(imageData, 0, 0);
767	return this;
768}
769
770
771function RemotePattern(remoteMessage)
772{
773	this.data = new Uint8Array(8);
774
775	if (remoteMessage)
776		this.readFrom(remoteMessage);
777	else
778		this.data.fill(255);
779}
780
781
782RemotePattern.staticCanvas = document.createElement('canvas');
783RemotePattern.staticCanvas.width = RemotePattern.staticCanvas.height = 8;
784RemotePattern.staticContext = RemotePattern.staticCanvas.getContext('2d');
785RemotePattern.staticImageData
786	= RemotePattern.staticContext.createImageData(8, 8);
787RemotePattern.staticPixels
788	= new Uint32Array(RemotePattern.staticImageData.data.buffer);
789
790
791RemotePattern.prototype.readFrom = function(remoteMessage)
792{
793	remoteMessage.dataView.readInto(this.data);
794	return this;
795}
796
797
798RemotePattern.prototype.isSolid = function()
799{
800	var common = this.data[0];
801	return this.data.every(function(value) { return value == common; });
802}
803
804
805RemotePattern.prototype.toPattern = function(context, lowColor, highColor)
806{
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;
811	}
812
813	// Apparently supplying ImageData to createPattern fails in Chrome.
814	RemotePattern.staticContext.putImageData(RemotePattern.staticImageData, 0,
815		0);
816	return context.createPattern(RemotePattern.staticCanvas, 'repeat');
817}
818
819
820function RemoteGradient(remoteMessage, context, unsetAlpha)
821{
822	if (remoteMessage) {
823		this.readFrom(remoteMessage, context, unsetAlpha);
824		return;
825	}
826
827	this.gradient = '#00000000';
828}
829
830
831RemoteGradient.prototype.readFrom = function(remoteMessage, context, unsetAlpha)
832{
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);
838
839			this.gradient = context.createLinearGradient(start.x, start.y,
840				end.x, end.y);
841			break;
842
843		case B_GRADIENT_TYPE_RADIAL:
844			var center = new RemotePoint(remoteMessage);
845			var radius = remoteMessage.dataView.readFloat32();
846
847			this.gradient = context.createRadialGradient(center.x, center.y, 0,
848				center.x, center.y, radius);
849			break;
850
851		default:
852			console.warn('gradient type not implemented: ' + this.type);
853			this.gradient = 'black';
854			return this;
855	}
856
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);
862	}
863
864	return this;
865}
866
867
868function RemoteShape(remoteMessage)
869{
870	if (remoteMessage) {
871		this.readFrom(remoteMessage);
872		return;
873	}
874
875	this.opCount = 0;
876	this.ops = [];
877	this.pointCount = 0;
878	this.points = [];
879}
880
881
882RemoteShape.prototype.readFrom = function(remoteMessage)
883{
884	this.bounds = new RemoteRect(remoteMessage);
885
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();
890
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);
895
896	return this;
897}
898
899
900RemoteShape.prototype.play = function(context)
901{
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;
906
907		if (op & B_SHAPE_OP_MOVE_TO) {
908			var point = this.points[pointIndex++];
909			context.moveTo(point.x, point.y);
910		}
911
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);
916			}
917		}
918
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);
926			}
927		}
928
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)) {
931
932			console.warn('shape op arc to not implemented');
933			for (var j = 0; j < count / 3; j++)
934				pointIndex++;
935		}
936
937		if (op & B_SHAPE_OP_CLOSE)
938			context.closePath();
939	}
940}
941
942
943function RemoteMessage(socket)
944{
945	this.socket = socket;
946}
947
948
949RemoteMessage.staticRemoteColor = new RemoteColor();
950
951
952RemoteMessage.prototype.allocate = function(bufferSize)
953{
954	this.buffer = new Uint8Array(bufferSize);
955	this.dataView = new StreamingDataView(this.buffer, true);
956}
957
958
959RemoteMessage.prototype.ensureBufferSize = function(bufferSize)
960{
961	if (this.buffer.byteLength < bufferSize)
962		this.allocate(bufferSize);
963}
964
965
966RemoteMessage.prototype.attach = function(buffer, byteOffset)
967{
968	var bytesLeft = buffer.byteLength - byteOffset;
969	if (bytesLeft < 6)
970		return false;
971
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;
978
979	return this.messageSize <= bytesLeft;
980}
981
982
983RemoteMessage.prototype.code = function()
984{
985	return this.messageCode;
986}
987
988
989RemoteMessage.prototype.size = function()
990{
991	return this.messageSize;
992}
993
994
995RemoteMessage.prototype.start = function(code)
996{
997	this.dataView.rewind();
998	this.dataView.writeUint16(code);
999	this.dataView.writeUint32(0);
1000		// Placeholder for size field.
1001}
1002
1003
1004RemoteMessage.prototype.flush = function()
1005{
1006	this.dataView.setUint32(2, this.dataView.position);
1007	this.socket.send(this.buffer.slice(0, this.dataView.position));
1008}
1009
1010
1011RemoteMessage.prototype.readColor = function(unsetAlpha)
1012{
1013	return RemoteMessage.staticRemoteColor.readFrom(this).toColor(unsetAlpha);
1014}
1015
1016
1017function RemoteState(session, token)
1018{
1019	this.session = session;
1020	this.token = token;
1021
1022	this.lowColor = new RemoteColor().fromUint32(0xffffffff);
1023	this.highColor = new RemoteColor().fromUint32(0xff000000);
1024
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';
1030
1031	this.pattern = new RemotePattern();
1032	this.font = new RemoteFont();
1033	this.transform = new RemoteTransform();
1034}
1035
1036
1037RemoteState.prototype.applyContext = function()
1038{
1039	var context = this.session.context;
1040	if (!this.invalidated && context.currentToken == this.token)
1041		return;
1042
1043	this.session.removeClipping();
1044
1045	if (this.blendModesEnabled && this.constantAlpha)
1046		context.globalAlpha = this.highColor.alpha / 255;
1047	else
1048		context.globalAlpha = 1;
1049
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));
1062	}
1063
1064	context.fillStyle = context.strokeStyle = style;
1065
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;
1072
1073	context.resetTransform();
1074
1075	this.session.applyClipping(this.clipRects);
1076
1077	if (!this.transform.isIdentity()) {
1078		context.translate(this.xOffset, this.yOffset);
1079		this.transform.apply(context);
1080		context.translate(-this.xOffset, -this.yOffset);
1081	}
1082
1083	context.currentToken = this.token;
1084	this.invalidated = false;
1085}
1086
1087
1088RemoteState.prototype.prepareForRect = function()
1089{
1090	this.session.context.lineJoin = 'miter';
1091	this.session.context.miterLimit = 10;
1092}
1093
1094
1095RemoteState.prototype.messageReceived = function(remoteMessage, reply)
1096{
1097	var context = this.session.context;
1098
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;
1104
1105		case RP_SET_LOW_COLOR:
1106			this.lowColor.readFrom(remoteMessage);
1107			this.invalidated = true;
1108			break;
1109
1110		case RP_SET_HIGH_COLOR:
1111			this.highColor.readFrom(remoteMessage);
1112			this.invalidated = true;
1113			break;
1114
1115		case RP_SET_OFFSETS:
1116			this.xOffset = remoteMessage.dataView.readInt32();
1117			this.yOffset = remoteMessage.dataView.readInt32();
1118			this.invalidated = true;
1119			break;
1120
1121		case RP_SET_FONT:
1122			this.font = new RemoteFont(remoteMessage);
1123			this.invalidated = true;
1124			break;
1125
1126		case RP_SET_TRANSFORM:
1127			this.transform = new RemoteTransform(remoteMessage);
1128			this.invalidated = true;
1129			break;
1130
1131		case RP_SET_PATTERN:
1132			this.pattern = new RemotePattern(remoteMessage);
1133			this.invalidated = true;
1134			break;
1135
1136		case RP_SET_PEN_SIZE:
1137			this.penSize = remoteMessage.dataView.readFloat32();
1138			this.invalidated = true;
1139			break;
1140
1141		case RP_SET_STROKE_MODE:
1142			switch (remoteMessage.dataView.readUint32()) {
1143				case B_ROUND_CAP:
1144					this.lineCap = 'round';
1145					break;
1146
1147				case B_BUTT_CAP:
1148					this.lineCap = 'butt';
1149					break;
1150
1151				case B_SQUARE_CAP:
1152					this.lineCap = 'square';
1153					break;
1154			}
1155
1156			var lineJoin = remoteMessage.dataView.readUint32();
1157			switch (lineJoin) {
1158				case B_ROUND_JOIN:
1159					this.lineJoin = 'round';
1160					break;
1161
1162				case B_MITER_JOIN:
1163					this.lineJoin = 'miter';
1164					break;
1165
1166				case B_BEVEL_JOIN:
1167					this.lineJoin = 'bevel';
1168					break;
1169
1170				default:
1171					console.warn('line join not implemented: ' + join);
1172					break;
1173			}
1174
1175			this.miterLimit = remoteMessage.dataView.readFloat32();
1176			this.invalidated = true;
1177			break;
1178
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;
1184
1185			var alphaFunction = remoteMessage.dataView.readUint32();
1186			if (alphaFunction != B_ALPHA_OVERLAY)
1187				console.warn('alpha function not supported: ' + alphaFunction);
1188
1189			this.invalidated = true;
1190			break;
1191
1192		case RP_SET_DRAWING_MODE:
1193			var drawingMode = remoteMessage.dataView.readUint32();
1194
1195			this.unsetAlpha = false;
1196			this.blendModesEnabled = false;
1197			this.invert = false;
1198
1199			switch (drawingMode) {
1200				case B_OP_COPY:
1201					this.unsetAlpha = true;
1202					this.drawingMode = 'source-over';
1203					break;
1204
1205				case B_OP_OVER:
1206					this.drawingMode = 'source-over';
1207					break;
1208
1209				case B_OP_ALPHA:
1210					this.blendModesEnabled = true;
1211					this.unsetAlpha = this.constantAlpha;
1212					this.drawingMode = 'source-over';
1213					break;
1214
1215				case B_OP_BLEND:
1216					this.drawingMode = 'lighter';
1217					break;
1218
1219				case B_OP_MIN:
1220					this.drawingMode = 'darken';
1221					break;
1222
1223				case B_OP_MAX:
1224					this.drawingMode = 'ligthen';
1225					break;
1226
1227				case B_OP_INVERT:
1228					this.drawingMode = 'difference';
1229					this.invert = true;
1230					break;
1231
1232				case B_OP_ADD:
1233					this.drawingMode = 'lighter';
1234					break;
1235
1236/*
1237				case B_OP_ERASE:
1238					this.drawingMode = 'destination-out';
1239					break;
1240
1241				case B_OP_SUBTRACT:
1242					this.drawingMode = 'difference';
1243					break;
1244*/
1245
1246				default:
1247					console.warn('drawing mode not implemented: '
1248						+ drawingMode);
1249					this.drawingMode = 'source-over';
1250					break;
1251			}
1252
1253			this.invalidated = true;
1254			break;
1255
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);
1261
1262			this.invalidated = true;
1263			break;
1264
1265		case RP_INVERT_RECT:
1266			this.applyContext();
1267
1268			var rect = new RemoteRect(remoteMessage);
1269
1270			context.save();
1271			context.globalCompositeOperation = 'difference';
1272			context.fillStyle = 'white';
1273			this.prepareForRect();
1274
1275			rect.apply(context.fillRect.bind(context));
1276
1277			context.restore();
1278			break;
1279
1280		case RP_DRAW_BITMAP:
1281			this.applyContext();
1282
1283			var bitmapRect = new RemoteRect(remoteMessage);
1284			var viewRect = new RemoteRect(remoteMessage);
1285			var options = remoteMessage.dataView.readUint32();
1286				// TODO: Implement options.
1287
1288			if (options != 0)
1289				console.warn('bitmap options not supported: ' + options);
1290
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;
1296
1297		case RP_DRAW_BITMAP_RECTS:
1298			this.applyContext();
1299
1300			var options = remoteMessage.dataView.readUint32();
1301				// TODO: Implement options.
1302			var colorSpace = remoteMessage.dataView.readUint32();
1303			var flags = remoteMessage.dataView.readUint32();
1304
1305			if (options != 0)
1306				console.warn('bitmap options not supported: ' + options);
1307
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);
1313
1314				context.drawImage(bitmap.canvas, 0, 0, bitmap.width,
1315					bitmap.height, rect.left, rect.top, rect.width(),
1316					rect.height());
1317			}
1318			break;
1319
1320		case RP_DRAW_STRING:
1321			this.applyContext();
1322
1323			var where = new RemotePoint(remoteMessage);
1324			var length = remoteMessage.dataView.readUint32();
1325			var string = remoteMessage.dataView.readString(length);
1326
1327			context.save();
1328			context.fillStyle = this.highColor.toColor(this.unsetAlpha);
1329			context.fillText(string, where.x, where.y);
1330
1331			var textMetric = context.measureText(string);
1332			where.x += textMetric.width;
1333
1334			context.restore();
1335
1336			reply.start(RP_DRAW_STRING_RESULT);
1337			reply.dataView.writeInt32(this.token);
1338			where.writeTo(reply);
1339			reply.flush();
1340			break;
1341
1342		case RP_DRAW_STRING_WITH_OFFSETS:
1343			this.applyContext();
1344
1345			var length = remoteMessage.dataView.readUint32();
1346			var string = remoteMessage.dataView.readString(length);
1347
1348			context.save();
1349			context.fillStyle = this.highColor.toColor(this.unsetAlpha);
1350
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);
1355			}
1356
1357			var textMetric = context.measureText(string[string.length - 1]);
1358			where.x += textMetric.width;
1359
1360			context.restore();
1361
1362			reply.start(RP_DRAW_STRING_RESULT);
1363			reply.dataView.writeInt32(this.token);
1364			where.writeTo(reply);
1365			reply.flush();
1366			break;
1367
1368		case RP_STRING_WIDTH:
1369			this.applyContext();
1370
1371			var length = remoteMessage.dataView.readUint32();
1372			var string = remoteMessage.dataView.readString(length);
1373			var textMetric = context.measureText(string);
1374
1375			reply.start(RP_STRING_WIDTH_RESULT);
1376			reply.dataView.writeInt32(this.token);
1377			where.writeFloat32(textMetric.width);
1378			reply.flush();
1379			break;
1380
1381		case RP_STROKE_ARC:
1382		case RP_FILL_ARC:
1383			this.applyContext();
1384
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;
1390
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;
1397
1398			var arcStep = function(max) {
1399					max = Math.min(max, span);
1400
1401					context.beginPath();
1402					context.arc(centerX, centerY, radius, invertStart,
1403						invertStart - max, true);
1404
1405					switch (remoteMessage.code()) {
1406						case RP_STROKE_ARC:
1407							context.stroke();
1408							break;
1409
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;
1421					}
1422
1423					startAngle += max;
1424					invertStart -= max;
1425					span -= max;
1426				};
1427
1428			while (span > 0)
1429				arcStep(maxSpan);
1430
1431			break;
1432
1433		case RP_STROKE_RECT:
1434		case RP_STROKE_ELLIPSE:
1435		case RP_FILL_RECT:
1436		case RP_FILL_ELLIPSE:
1437			this.applyContext();
1438
1439			context.save();
1440			this.prepareForRect();
1441
1442			var rect = new RemoteRect(remoteMessage);
1443
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;
1457			}
1458
1459			context.restore();
1460			break;
1461
1462		case RP_STROKE_ROUND_RECT:
1463		case RP_FILL_ROUND_RECT:
1464		case RP_FILL_ROUND_RECT_GRADIENT:
1465			this.applyContext();
1466
1467			context.save();
1468			this.prepareForRect();
1469
1470			var rect = new RemoteRect(remoteMessage);
1471			var xRadius = remoteMessage.dataView.readFloat32();
1472			var yRadius = remoteMessage.dataView.readFloat32();
1473
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;
1479			}
1480
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));
1486
1487			if (remoteMessage.code() == RP_FILL_ROUND_RECT_GRADIENT)
1488				context.restore();
1489
1490			context.restore();
1491			break;
1492
1493		case RP_STROKE_LINE:
1494			this.applyContext();
1495
1496			var from = new RemotePoint(remoteMessage);
1497			var to = new RemotePoint(remoteMessage);
1498
1499			context.beginPath();
1500			context.moveTo(from.x, from.y);
1501			context.lineTo(to.x, to.y);
1502			context.stroke();
1503			break;
1504
1505		case RP_STROKE_LINE_ARRAY:
1506			this.applyContext();
1507
1508			context.save();
1509			context.lineCap = 'square';
1510
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();
1520			}
1521
1522			context.restore();
1523			break;
1524
1525		case RP_STROKE_POINT_COLOR:
1526			this.applyContext();
1527
1528			var point = new RemotePoint(remoteMessage);
1529
1530			context.save();
1531			context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
1532
1533			context.fillRect(point.x, point.y, 1, 1);
1534			context.restore();
1535			break;
1536
1537		case RP_STROKE_LINE_1PX_COLOR:
1538			this.applyContext();
1539
1540			var from = new RemotePoint(remoteMessage);
1541			var to = new RemotePoint(remoteMessage);
1542
1543			context.save();
1544			context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1545			context.lineWidth = 1;
1546			context.lineCap = 'square';
1547
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();
1552
1553			context.restore();
1554			break;
1555
1556		case RP_STROKE_RECT_1PX_COLOR:
1557			this.applyContext();
1558
1559			var rect = new RemoteRect(remoteMessage);
1560
1561			context.save();
1562			this.prepareForRect();
1563
1564			context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1565			context.lineWidth = 1;
1566
1567			rect.apply(context.strokeRect.bind(context));
1568
1569			context.restore();
1570			break;
1571
1572		case RP_STROKE_SHAPE:
1573		case RP_FILL_SHAPE:
1574		case RP_FILL_SHAPE_GRADIENT:
1575			this.applyContext();
1576
1577			var shape = new RemoteShape(remoteMessage);
1578			var offset = new RemotePoint(remoteMessage);
1579			var scale = remoteMessage.dataView.readFloat32();
1580
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;
1586			}
1587
1588			context.translate(offset.x + 0.5, offset.y + 0.5);
1589			context.scale(scale, scale);
1590
1591			context.beginPath();
1592
1593			shape.play(context);
1594
1595			if (remoteMessage.code() == RP_STROKE_SHAPE)
1596				context.stroke();
1597			else
1598				context.fill();
1599
1600			context.restore();
1601			break;
1602
1603		case RP_STROKE_TRIANGLE:
1604		case RP_FILL_TRIANGLE:
1605		case RP_FILL_TRIANGLE_GRADIENT:
1606			this.applyContext();
1607
1608			if (remoteMessage.code() == RP_FILL_TRIANGLE_GRADIENT)
1609				context.save();
1610
1611			context.beginPath();
1612			var point = new RemotePoint(remoteMessage);
1613			context.moveTo(point.x + 0.5, point.y + 0.5);
1614
1615			for (var i = 0; i < 2; i++) {
1616				point = new RemotePoint(remoteMessage);
1617				context.lineTo(point.x + 0.5, point.y + 0.5);
1618			}
1619
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;
1625			}
1626
1627			switch (remoteMessage.code()) {
1628				case RP_STROKE_TRIANGLE:
1629					context.closePath();
1630					context.stroke();
1631					break;
1632
1633				case RP_FILL_TRIANGLE:
1634					context.fill();
1635					break;
1636
1637				case RP_FILL_TRIANGLE_GRADIENT:
1638					context.fill();
1639					context.restore();
1640					break;
1641			}
1642
1643			break;
1644
1645		case RP_FILL_RECT_COLOR:
1646			this.applyContext();
1647
1648			var rect = new RemoteRect(remoteMessage);
1649
1650			context.save();
1651			this.prepareForRect();
1652			context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
1653
1654			rect.apply(context.fillRect.bind(context));
1655
1656			context.restore();
1657			break;
1658
1659		case RP_FILL_RECT_GRADIENT:
1660		case RP_FILL_ELLIPSE_GRADIENT:
1661			this.applyContext();
1662
1663			var rect = new RemoteRect(remoteMessage);
1664
1665			context.save();
1666			this.prepareForRect();
1667
1668			var gradient = new RemoteGradient(remoteMessage, context,
1669				this.unsetAlpha);
1670			context.fillStyle = gradient.gradient;
1671
1672			if (remoteMessage.code() == RP_FILL_RECT_GRADIENT)
1673				rect.apply(context.fillRect.bind(context));
1674			else
1675				rect.applyAsEllipse(context, context.fill);
1676
1677			context.restore();
1678			break;
1679
1680		case RP_FILL_REGION:
1681		case RP_FILL_REGION_GRADIENT:
1682			this.applyContext();
1683
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);
1688
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;
1694			}
1695
1696			for (var i = 0; i < rectCount; i++)
1697				rects[i].apply(context.fillRect.bind(context));
1698
1699			if (remoteMessage.code() == RP_FILL_REGION_GRADIENT)
1700				context.restore();
1701
1702			break;
1703
1704		case RP_READ_BITMAP:
1705			var bounds = new RemoteRect(remoteMessage);
1706			var drawCursor = remoteMessage.dataView.readUint8();
1707				// TODO: Support the drawCursor flag.
1708
1709			if (drawCursor)
1710				console.warn('draw cursor in read bitmap not supported');
1711
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;
1718
1719			reply.ensureBufferSize(bitsLength + 1024);
1720
1721			reply.start(RP_READ_BITMAP_RESULT);
1722			reply.dataView.writeInt32(this.token);
1723
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);
1730
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]);
1739				}
1740
1741				reply.dataView.pad(padding);
1742			}
1743
1744			reply.flush();
1745			break;
1746
1747		default:
1748			console.warn('unhandled message: code: ' + remoteMessage.code()
1749				+ '; size: ' + remoteMessage.size());
1750			break;
1751	}
1752}
1753
1754
1755function RemoteDesktopSession(targetElement, width, height, targetAddress,
1756	disconnectCallback)
1757{
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);
1764
1765	this.disconnectCallback = disconnectCallback;
1766
1767	this.sendMessage = new RemoteMessage(this.websocket);
1768	this.sendMessage.allocate(1024);
1769
1770	this.receiveMessage = new RemoteMessage();
1771
1772	this.container = document.createElement('div');
1773	this.container.className = 'session';
1774	this.container.style.position = 'relative';
1775	targetElement.appendChild(this.container);
1776
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);
1782
1783	this.canvas.tabIndex = 0;
1784	this.canvas.focus();
1785
1786	this.context = this.canvas.getContext('2d', { alpha: false });
1787	this.context.imageSmoothingEnabled = false;
1788
1789	this.cursorVisible = true;
1790	this.cursorPosition = { x: 0, y: 0 };
1791	this.cursorHotspot = { x: 0, y: 0 };
1792
1793	this.states = new Object();
1794	this.modifiers = 0;
1795
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);
1800
1801	this.canvas.onkeydown = this.onKeyDownUp.bind(this);
1802	this.canvas.onkeyup = this.onKeyDownUp.bind(this);
1803	this.canvas.onkeypress = this.onKeyPress.bind(this);
1804
1805	this.canvas.oncontextmenu = function(event) {
1806			event.preventDefault();
1807		};
1808
1809	this.canvas.onblur = function(event) {
1810			event.target.focus();
1811		};
1812}
1813
1814
1815RemoteDesktopSession.prototype.onOpen = function(open)
1816{
1817	console.log('open:', open);
1818	this.init();
1819}
1820
1821
1822RemoteDesktopSession.prototype.onMessage = function(message)
1823{
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;
1831
1832		this.messageRemainder = null;
1833	} else
1834		data = new Uint8Array(data);
1835
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;
1846		}
1847
1848		try {
1849			this.messageReceived(this.receiveMessage, this.sendMessage);
1850		} catch (exception) {
1851			console.error('exception during message processing:', exception);
1852		}
1853
1854		byteOffset += this.receiveMessage.size();
1855	}
1856
1857	if (data.byteLength > byteOffset)
1858		this.messageRemainder = data.slice(byteOffset);
1859}
1860
1861
1862RemoteDesktopSession.prototype.messageReceived = function(remoteMessage, reply)
1863{
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();
1871
1872			this.sendMessage.start(RP_GET_SYSTEM_PALETTE);
1873			this.sendMessage.flush();
1874			break;
1875
1876		case RP_GET_SYSTEM_PALETTE_RESULT:
1877			var count = remoteMessage.dataView.readUint32();
1878			gSystemPalette = new Uint32Array(count);
1879
1880			var color = new RemoteColor();
1881			for (var i = 0; i < gSystemPalette.length; i++)
1882				gSystemPalette[i] = color.readFrom(remoteMessage).toUint32();
1883
1884			break;
1885
1886		case RP_CREATE_STATE:
1887			var token = remoteMessage.dataView.readInt32();
1888			console.log('create state: ' + token);
1889
1890			if (this.states.hasOwnProperty(token))
1891				console.error('create state for existing token: ' + token);
1892
1893			this.states[token] = new RemoteState(this, token);
1894			break;
1895
1896		case RP_DELETE_STATE:
1897			var token = remoteMessage.dataView.readInt32();
1898			console.log('delete state: ' + token);
1899
1900			if (!this.states.hasOwnProperty(token)) {
1901				console.error('delete state for unknown token: ' + token);
1902				break;
1903			}
1904
1905			delete this.states[token];
1906			break;
1907
1908		case RP_INVALIDATE_RECT:
1909		case RP_INVALIDATE_REGION:
1910			break;
1911
1912		case RP_SET_CURSOR:
1913			this.cursorHotspot = new RemotePoint(remoteMessage);
1914			var bitmap = new RemoteBitmap(remoteMessage);
1915
1916			bitmap.canvas.style.position = 'absolute';
1917			if (this.cursorCanvas)
1918				this.cursorCanvas.remove();
1919
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;
1926
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;
1932
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';
1938			}
1939			break;
1940
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);
1945
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;
1951
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';
1957
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);
1962
1963			this.context.fillStyle = remoteMessage.readColor();
1964
1965			for (var i = 0; i < rectCount; i++)
1966				rects[i].apply(this.context.fillRect.bind(this.context));
1967
1968			break;
1969
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);
1975			}
1976
1977			this.states[token].messageReceived(remoteMessage, reply);
1978			break;
1979	}
1980}
1981
1982
1983RemoteDesktopSession.prototype.onError = function(error)
1984{
1985	console.log('websocket error:', error);
1986	this.onDisconnect(error);
1987}
1988
1989
1990RemoteDesktopSession.prototype.onClose = function(close)
1991{
1992	console.log('websocket close:', close);
1993	this.onDisconnect(close);
1994}
1995
1996
1997RemoteDesktopSession.prototype.onDisconnect = function(reason)
1998{
1999	this.container.remove();
2000	if (this.disconnectCallback)
2001		this.disconnectCallback(reason);
2002}
2003
2004
2005RemoteDesktopSession.prototype.applyClipping = function(clipRects)
2006{
2007	this.removeClipping();
2008
2009	if (!clipRects || clipRects.length == 0)
2010		return;
2011
2012	this.context.save();
2013	this.context.beginPath();
2014
2015	this.context.save();
2016	this.context.lineJoin = 'miter';
2017	this.context.miterLimit = 10;
2018
2019	for (var i = 0; i < clipRects.length; i++)
2020		clipRects[i].apply(this.context.rect.bind(this.context));
2021
2022	this.context.restore();
2023
2024	this.context.clip();
2025	this.clippingApplied = true;
2026}
2027
2028
2029RemoteDesktopSession.prototype.removeClipping = function()
2030{
2031	if (!this.clippingApplied)
2032		return;
2033
2034	this.context.restore();
2035}
2036
2037
2038RemoteDesktopSession.prototype.init = function()
2039{
2040	this.sendMessage.start(RP_INIT_CONNECTION);
2041	this.sendMessage.flush();
2042}
2043
2044
2045RemoteDesktopSession.prototype.updateCursor = function()
2046{
2047	if (!this.cursorVisible || !this.cursorCanvas)
2048		return;
2049
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';
2054}
2055
2056
2057RemoteDesktopSession.prototype.onMouseMove = function(event)
2058{
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();
2064}
2065
2066
2067RemoteDesktopSession.prototype.onMouseDown = function(event)
2068{
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();
2077}
2078
2079
2080RemoteDesktopSession.prototype.onMouseUp = function(event)
2081{
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();
2088}
2089
2090
2091RemoteDesktopSession.prototype.onKeyDownUp = function(event)
2092{
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;
2102
2103		case 'ShiftRight':
2104			modifiersChanged |= B_RIGHT_SHIFT_KEY;
2105			if (event.shiftKey == keyDown)
2106				modifiersChanged |= B_SHIFT_KEY;
2107			break;
2108
2109		case 'ControlLeft':
2110			modifiersChanged |= B_LEFT_CONTROL_KEY;
2111			if (event.ctrlKey == keyDown)
2112				modifiersChanged |= B_CONTROL_KEY;
2113			break;
2114
2115		case 'ControlRight':
2116			modifiersChanged |= B_RIGHT_CONTROL_KEY;
2117			if (event.ctrlKey == keyDown)
2118				modifiersChanged |= B_CONTROL_KEY;
2119			break;
2120
2121		case 'AltLeft':
2122			modifiersChanged |= B_LEFT_COMMAND_KEY;
2123			if (event.altKey == keyDown)
2124				modifiersChanged |= B_COMMAND_KEY;
2125			break;
2126
2127		case 'AltRight':
2128			modifiersChanged |= B_RIGHT_COMMAND_KEY;
2129			if (event.altKey == keyDown)
2130				modifiersChanged |= B_COMMAND_KEY;
2131			break;
2132
2133		case 'ContextMenu':
2134			modifiersChanged |= B_MENU_KEY;
2135			break;
2136
2137		case 'CapsLock':
2138			modifiersChanged |= B_CAPS_LOCK;
2139			lockModifier = true;
2140			break;
2141
2142		case 'ScrollLock':
2143			modifiersChanged |= B_SCROLL_LOCK;
2144			lockModifier = true;
2145			break;
2146
2147		case 'NumLock':
2148			modifiersChanged |= B_NUM_LOCK;
2149			lockModifier = true;
2150			break;
2151	}
2152
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;
2162		}
2163
2164		this.sendMessage.start(RP_MODIFIERS_CHANGED);
2165		this.sendMessage.dataView.writeUint32(this.modifiers);
2166		this.sendMessage.flush();
2167		event.preventDefault();
2168		return;
2169	}
2170
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);
2177	}
2178
2179	if (event.keyCode) {
2180		this.sendMessage.dataView.writeUint32(0);
2181		this.sendMessage.dataView.writeUint32(event.keyCode);
2182	}
2183
2184	this.sendMessage.flush();
2185	event.preventDefault();
2186}
2187
2188
2189RemoteDesktopSession.prototype.onKeyPress = function(event)
2190{
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();
2200}
2201
2202
2203RemoteDesktopSession.prototype.onWheel = function(event)
2204{
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();
2210}
2211
2212
2213function init()
2214{
2215	var targetAddressInput = document.querySelector('#targetAddress');
2216	var widthInput = document.querySelector('#width');
2217	var heightInput = document.querySelector('#height');
2218
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;
2225
2226	var onDisconnect = function(reason) {
2227			document.body.classList.remove('connect');
2228			gSession = undefined;
2229		};
2230
2231	document.querySelector('#connectButton').onclick = function() {
2232			document.body.classList.add('connect');
2233
2234			localStorage.width = widthInput.value;
2235			localStorage.height = heightInput.value;
2236			localStorage.targetAddress = targetAddressInput.value;
2237
2238			gSession = new RemoteDesktopSession(document.body, widthInput.value,
2239				heightInput.value, targetAddressInput.value, onDisconnect);
2240		};
2241}
2242