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// Font spacing and face
182const B_FIXED_SPACING = 3;
183
184const B_ITALIC_FACE = 0x0001;
185const B_BOLD_FACE = 0x0020;
186
187
188// modifiers
189const B_SHIFT_KEY = 0x00000001;
190const B_COMMAND_KEY = 0x00000002;
191const B_CONTROL_KEY = 0x00000004;
192const B_CAPS_LOCK = 0x00000008;
193const B_SCROLL_LOCK = 0x00000010;
194const B_NUM_LOCK = 0x00000020;
195const B_OPTION_KEY = 0x00000040;
196const B_MENU_KEY = 0x00000080;
197const B_LEFT_SHIFT_KEY = 0x00000100;
198const B_RIGHT_SHIFT_KEY = 0x00000200;
199const B_LEFT_COMMAND_KEY = 0x00000400;
200const B_RIGHT_COMMAND_KEY = 0x00000800;
201const B_LEFT_CONTROL_KEY = 0x00001000;
202const B_RIGHT_CONTROL_KEY = 0x00002000;
203const B_LEFT_OPTION_KEY = 0x00004000;
204const B_RIGHT_OPTION_KEY = 0x00008000;
205
206
207
208var gSession;
209var gSystemPalette;
210
211
212function StreamingDataView(buffer, littleEndian, byteOffset, byteLength)
213{
214	this.buffer = buffer;
215	this.dataView = new DataView(buffer.buffer, byteOffset, byteLength);
216	this.position = 0;
217	this.littleEndian = littleEndian;
218	this.textDecoder = new TextDecoder('utf-8');
219	this.textEncoder = new TextEncoder();
220}
221
222
223StreamingDataView.prototype.rewind = function()
224{
225	this.position = 0;
226}
227
228
229StreamingDataView.prototype.readInt8 = function()
230{
231	return this.dataView.getInt8(this.position++);
232}
233
234
235StreamingDataView.prototype.readUint8 = function()
236{
237	return this.dataView.getUint8(this.position++);
238}
239
240
241StreamingDataView.prototype.readInt16 = function()
242{
243	var result = this.dataView.getInt16(this.position, this.littleEndian);
244	this.position += 2;
245	return result;
246}
247
248
249StreamingDataView.prototype.readUint16 = function()
250{
251	var result = this.dataView.getUint16(this.position, this.littleEndian);
252	this.position += 2;
253	return result;
254}
255
256
257StreamingDataView.prototype.readInt32 = function()
258{
259	var result = this.dataView.getInt32(this.position, this.littleEndian);
260	this.position += 4;
261	return result;
262}
263
264
265StreamingDataView.prototype.readUint32 = function()
266{
267	var result = this.dataView.getUint32(this.position, this.littleEndian);
268	this.position += 4;
269	return result;
270}
271
272
273StreamingDataView.prototype.readFloat32 = function()
274{
275	var result = this.dataView.getFloat32(this.position, this.littleEndian);
276	this.position += 4;
277	return result;
278}
279
280
281StreamingDataView.prototype.readFloat64 = function()
282{
283	var result = this.dataView.getFloat64(this.position, this.littleEndian);
284	this.position += 8;
285	return result;
286}
287
288
289StreamingDataView.prototype.readString = function(length)
290{
291	var where = this.dataView.byteOffset + this.position;
292	var part = this.buffer.slice(where, where + length);
293	var result = this.textDecoder.decode(part);
294	this.position += length;
295	return result;
296}
297
298
299StreamingDataView.prototype.readInto = function(typedArray)
300{
301	var where = this.dataView.byteOffset + this.position;
302	typedArray.set(this.buffer.slice(where, where + typedArray.byteLength));
303	this.position += typedArray.byteLength;
304}
305
306
307StreamingDataView.prototype.writeInt8 = function(value)
308{
309	this.dataView.setInt8(this.position++, value);
310}
311
312
313StreamingDataView.prototype.writeUint8 = function(value)
314{
315	this.dataView.setUint8(this.position++, value);
316}
317
318
319StreamingDataView.prototype.writeInt16 = function(value)
320{
321	this.dataView.setInt16(this.position, value, this.littleEndian);
322	this.position += 2;
323}
324
325
326StreamingDataView.prototype.writeUint16 = function(value)
327{
328	this.dataView.setUint16(this.position, value, this.littleEndian);
329	this.position += 2;
330}
331
332
333StreamingDataView.prototype.writeInt32 = function(value)
334{
335	this.dataView.setInt32(this.position, value, this.littleEndian);
336	this.position += 4;
337}
338
339
340StreamingDataView.prototype.writeUint32 = function(value)
341{
342	this.dataView.setUint32(this.position, value, this.littleEndian);
343	this.position += 4;
344}
345
346
347StreamingDataView.prototype.writeFloat32 = function(value)
348{
349	this.dataView.setFloat32(this.position, value, this.littleEndian);
350	this.position += 4;
351}
352
353
354StreamingDataView.prototype.writeFloat64 = function(value)
355{
356	this.dataView.setFloat64(this.position, value, this.littleEndian);
357	this.position += 8;
358}
359
360
361StreamingDataView.prototype.writeString = function(string)
362{
363	var encoded = this.textEncoder.encode(string);
364	this.writeUint32(encoded.length);
365	this.buffer.set(encoded, this.position);
366	this.position += encoded.length;
367}
368
369
370StreamingDataView.prototype.setUint32 = function(byteOffset, value)
371{
372	this.dataView.setUint32(byteOffset, value, this.littleEndian);
373}
374
375
376StreamingDataView.prototype.pad = function(length)
377{
378	this.buffer.fill(0, this.position, this.position + length);
379	this.position += length;
380}
381
382
383function RemotePoint(remoteMessage)
384{
385	if (remoteMessage) {
386		this.readFrom(remoteMessage);
387		return;
388	}
389
390	this.x = 0;
391	this.y = 0;
392}
393
394
395RemotePoint.prototype.readFrom = function(remoteMessage)
396{
397	this.x = remoteMessage.dataView.readFloat32();
398	this.y = remoteMessage.dataView.readFloat32();
399	return this;
400}
401
402
403RemotePoint.prototype.writeTo = function(remoteMessage)
404{
405	remoteMessage.dataView.writeFloat32(this.x);
406	remoteMessage.dataView.writeFloat32(this.y);
407	return this;
408}
409
410
411function RemoteRect(remoteMessage)
412{
413	if (remoteMessage) {
414		this.readFrom(remoteMessage);
415		return;
416	}
417
418	this.left = 0;
419	this.top = 0;
420	this.right = -1;
421	this.bottom = -1;
422}
423
424
425RemoteRect.prototype.readFrom = function(remoteMessage)
426{
427	this.left = remoteMessage.dataView.readFloat32();
428	this.top = remoteMessage.dataView.readFloat32();
429	this.right = remoteMessage.dataView.readFloat32();
430	this.bottom = remoteMessage.dataView.readFloat32();
431	return this;
432}
433
434
435RemoteRect.prototype.width = function()
436{
437	return this.right - this.left + 1;
438}
439
440
441RemoteRect.prototype.height = function()
442{
443	return this.bottom - this.top + 1;
444}
445
446
447RemoteRect.prototype.integerWidth = function()
448{
449	return Math.ceil(this.right - this.left);
450}
451
452
453RemoteRect.prototype.integerHeight = function()
454{
455	return Math.ceil(this.bottom - this.top);
456}
457
458
459RemoteRect.prototype.centerX = function()
460{
461	return this.left + this.width() / 2;
462}
463
464
465RemoteRect.prototype.centerY = function()
466{
467	return this.top + this.height() / 2;
468}
469
470
471RemoteRect.prototype.apply = function(apply)
472{
473	var left = Math.floor(this.left);
474	var top = Math.floor(this.top);
475	var right = Math.ceil(this.right);
476	var bottom = Math.ceil(this.bottom);
477	apply(left, top, right - left + 1, bottom - top + 1);
478}
479
480
481RemoteRect.prototype.applyAsEllipse = function(context, which)
482{
483	context.beginPath();
484	context.ellipse(this.centerX(), this.centerY(), this.width() / 2,
485		this.height() / 2, 0, Math.PI * 2, false);
486	which.call(context);
487}
488
489
490function RemoteColor(remoteMessage)
491{
492	if (remoteMessage) {
493		this.readFrom(remoteMessage);
494		return;
495	}
496
497	this.red = 0;
498	this.green = 0;
499	this.blue = 0;
500	this.alpha = 0;
501}
502
503
504RemoteColor.prototype.readFrom = function(remoteMessage)
505{
506	this.red = remoteMessage.dataView.readUint8();
507	this.green = remoteMessage.dataView.readUint8();
508	this.blue = remoteMessage.dataView.readUint8();
509	this.alpha = remoteMessage.dataView.readUint8();
510	return this;
511}
512
513
514RemoteColor.prototype.fromUint32 = function(value)
515{
516	this.red = value & 0xff;
517	this.green = value >> 8 & 0xff;
518	this.blue = value >> 16 & 0xff;
519	this.alpha = value >> 24 & 0xff;
520	return this;
521}
522
523
524RemoteColor.prototype.toColor = function(unsetAlpha)
525{
526	return 'rgba(' + this.red + ', ' + this.green + ', ' + this.blue + ', '
527		+ (unsetAlpha ? 1 : this.alpha / 255) + ')';
528}
529
530
531RemoteColor.prototype.toUint32 = function(unsetAlpha)
532{
533	return this.red | this.green << 8 | this.blue << 16
534		| (unsetAlpha ? 255 : this.alpha) << 24;
535}
536
537
538function RemoteFont(remoteMessage)
539{
540	if (remoteMessage) {
541		this.readFrom(remoteMessage);
542		return;
543	}
544
545	this.direction = 0;
546	this.encoding = 0;
547	this.flags = 0;
548	this.spacing = 0;
549	this.shear = 0;
550	this.rotation = 0;
551	this.falseBoldWidth = 0;
552	this.size = 12;
553	this.face = 0;
554	this.family = 0;
555	this.style = 0;
556}
557
558
559RemoteFont.prototype.readFrom = function(remoteMessage)
560{
561	this.direction = remoteMessage.dataView.readUint8();
562	this.encoding = remoteMessage.dataView.readUint8();
563	this.flags = remoteMessage.dataView.readUint32();
564	this.spacing = remoteMessage.dataView.readUint8();
565	this.shear = remoteMessage.dataView.readFloat32();
566	this.rotation = remoteMessage.dataView.readFloat32();
567	this.falseBoldWidth = remoteMessage.dataView.readFloat32();
568	this.size = remoteMessage.dataView.readFloat32();
569	this.face = remoteMessage.dataView.readUint16();
570	this.family = remoteMessage.dataView.readUint16();
571	this.style = remoteMessage.dataView.readUint16();
572	return this;
573}
574
575
576function RemoteTransform(remoteMessage)
577{
578	if (remoteMessage) {
579		this.readFrom(remoteMessage);
580		return;
581	}
582
583	this.setIdentity();
584}
585
586
587RemoteTransform.prototype.readFrom = function(remoteMessage)
588{
589	var isIdentity = remoteMessage.dataView.readUint8();
590	if (isIdentity) {
591		this.setIdentity();
592		return;
593	}
594
595	this.sx = remoteMessage.dataView.readFloat64();
596	this.shy = remoteMessage.dataView.readFloat64();
597	this.shx = remoteMessage.dataView.readFloat64();
598	this.sy = remoteMessage.dataView.readFloat64();
599	this.tx = remoteMessage.dataView.readFloat64();
600	this.ty = remoteMessage.dataView.readFloat64();
601	return this;
602}
603
604
605RemoteTransform.prototype.setIdentity = function()
606{
607	this.sx = 1;
608	this.shy = 0;
609	this.shx = 0;
610	this.sy = 1;
611	this.tx = 0;
612	this.ty = 0;
613	return this;
614}
615
616
617RemoteTransform.prototype.isIdentity = function()
618{
619	return this.sx == 1 && this.shy == 0 && this.shx == 0 && this.sy == 1
620		&& this.tx == 0 && this.ty == 0;
621}
622
623
624RemoteTransform.prototype.apply = function(context)
625{
626	context.transform(this.sx, this.shy, this.shx, this.sy, this.tx,
627		this.ty);
628}
629
630
631function RemoteBitmap(remoteMessage, unsetAlpha, colorSpace, flags)
632{
633	if (remoteMessage) {
634		this.readFrom(remoteMessage, unsetAlpha, colorSpace, flags);
635		return;
636	}
637}
638
639
640RemoteBitmap.prototype.readFrom = function(remoteMessage, unsetAlpha,
641	colorSpace, flags)
642{
643	this.width = remoteMessage.dataView.readUint32();
644	this.height = remoteMessage.dataView.readUint32();
645	this.bytesPerRow = remoteMessage.dataView.readUint32();
646
647	if (colorSpace != undefined) {
648		this.colorSpace = colorSpace;
649		this.flags = flags;
650	} else {
651		this.colorSpace = remoteMessage.dataView.readUint32();
652		this.flags = remoteMessage.dataView.readUint32();
653	}
654
655	this.bitsLength = remoteMessage.dataView.readUint32();
656
657	this.canvas = document.createElement('canvas');
658	this.canvas.width = this.width;
659	this.canvas.height = this.height;
660
661	if (this.width == 0 || this.height == 0)
662		return;
663
664	var context = this.canvas.getContext('2d');
665	var imageData = context.createImageData(this.width, this.height);
666	switch (this.colorSpace) {
667		case B_RGBA32:
668			remoteMessage.dataView.readInto(imageData.data);
669			var output = new Uint32Array(imageData.data.buffer);
670
671			for (var i = 0; i < imageData.data.length / 4; i++) {
672				output[i] = (output[i] & 0xff) << 16 | (output[i] >> 16 & 0xff)
673					| (output[i] & 0xff00ff00);
674			}
675
676			if (unsetAlpha) {
677				for (var i = 0; i < imageData.data.length / 4; i++)
678					output[i] |= 0xff000000;
679			}
680
681			break;
682
683		case B_RGB32:
684			remoteMessage.dataView.readInto(imageData.data);
685			var output = new Uint32Array(imageData.data.buffer);
686
687			for (var i = 0; i < imageData.data.length / 4; i++) {
688				output[i] = (output[i] & 0xff) << 16 | (output[i] >> 16 & 0xff)
689					| (output[i] & 0xff00) | 0xff000000;
690
691				if (!unsetAlpha && output[i] == B_TRANSPARENT_MAGIC_RGBA32)
692					output[i] &= 0x00ffffff;
693			}
694			break;
695
696		case B_RGB24:
697			var line = new Uint8Array(this.bytesPerRow);
698			var position = 0;
699
700			for (var y = 0; y < this.height; y++) {
701				remoteMessage.dataView.readInto(line);
702
703				for (var x = 0; x < this.width; x++) {
704					imageData.data[position++] = line[x * 3 + 2];
705					imageData.data[position++] = line[x * 3 + 1];
706					imageData.data[position++] = line[x * 3 + 0];
707					imageData.data[position++] = 255;
708				}
709			}
710
711			break;
712
713		case B_RGB16:
714			var lineBuffer = new Uint8Array(this.bytesPerRow);
715			var line = new Uint16Array(lineBuffer.buffer);
716			var position = 0;
717
718			for (var y = 0; y < this.height; y++) {
719				remoteMessage.dataView.readInto(lineBuffer);
720
721				for (var x = 0; x < this.width; x++) {
722					imageData.data[position++] = (line[x] & 0xf800) >> 8;
723					imageData.data[position++] = (line[x] & 0x07e0) >> 3;
724					imageData.data[position++] = (line[x] & 0x001f) << 3;
725					imageData.data[position++] = 255;
726				}
727			}
728
729			break;
730
731		case B_CMAP8:
732			var line = new Uint8Array(this.bytesPerRow);
733			var output = new Uint32Array(imageData.data.buffer);
734			var position = 0;
735
736			for (var y = 0; y < this.height; y++) {
737				remoteMessage.dataView.readInto(line);
738
739				for (var x = 0; x < this.width; x++)
740					output[position++] = gSystemPalette[line[x]];
741			}
742
743			break;
744
745		case B_GRAY8:
746			var source = new Uint8Array(this.bitsLength);
747			remoteMessage.dataView.readInto(source);
748			for (var i = 0; i < imageData.data.length / 4; i++) {
749				imageData.data[i * 4 + 0] = source[i];
750				imageData.data[i * 4 + 1] = source[i];
751				imageData.data[i * 4 + 2] = source[i];
752				imageData.data[i * 4 + 3] = 255;
753			}
754			break;
755
756		case B_GRAY1:
757			var source = new Uint8Array(this.bitsLength);
758			remoteMessage.dataView.readInto(source);
759			for (var i = 0; i < imageData.data.length / 4; i++) {
760				var value = (source[Math.floor(i / 8)] >> i % 8) & 1 ? 255 : 0;
761				imageData.data[i * 4 + 0] = value;
762				imageData.data[i * 4 + 1] = value;
763				imageData.data[i * 4 + 2] = value;
764				imageData.data[i * 4 + 3] = 255;
765			}
766			break;
767
768		default:
769			console.warn('color space not implemented: ' + this.colorSpace);
770			break;
771	}
772
773	context.putImageData(imageData, 0, 0);
774	return this;
775}
776
777
778function RemotePattern(remoteMessage)
779{
780	this.data = new Uint8Array(8);
781
782	if (remoteMessage)
783		this.readFrom(remoteMessage);
784	else
785		this.data.fill(255);
786}
787
788
789RemotePattern.staticCanvas = document.createElement('canvas');
790RemotePattern.staticCanvas.width = RemotePattern.staticCanvas.height = 8;
791RemotePattern.staticContext = RemotePattern.staticCanvas.getContext('2d');
792RemotePattern.staticImageData
793	= RemotePattern.staticContext.createImageData(8, 8);
794RemotePattern.staticPixels
795	= new Uint32Array(RemotePattern.staticImageData.data.buffer);
796
797
798RemotePattern.prototype.readFrom = function(remoteMessage)
799{
800	remoteMessage.dataView.readInto(this.data);
801	return this;
802}
803
804
805RemotePattern.prototype.isSolid = function()
806{
807	var common = this.data[0];
808	return this.data.every(function(value) { return value == common; });
809}
810
811
812RemotePattern.prototype.toPattern = function(context, lowColor, highColor)
813{
814	for (var i = 0; i < this.data.length * 8; i++) {
815		RemotePattern.staticPixels[i]
816			= (this.data[i / 8 | 0] & 1 << 7 - i % 8) == 0
817				? lowColor : highColor;
818	}
819
820	// Apparently supplying ImageData to createPattern fails in Chrome.
821	RemotePattern.staticContext.putImageData(RemotePattern.staticImageData, 0,
822		0);
823	return context.createPattern(RemotePattern.staticCanvas, 'repeat');
824}
825
826
827function RemoteGradient(remoteMessage, context, unsetAlpha)
828{
829	if (remoteMessage) {
830		this.readFrom(remoteMessage, context, unsetAlpha);
831		return;
832	}
833
834	this.gradient = '#00000000';
835}
836
837
838RemoteGradient.prototype.readFrom = function(remoteMessage, context, unsetAlpha)
839{
840	this.type = remoteMessage.dataView.readUint32();
841	switch (this.type) {
842		case B_GRADIENT_TYPE_LINEAR:
843			var start = new RemotePoint(remoteMessage);
844			var end = new RemotePoint(remoteMessage);
845
846			this.gradient = context.createLinearGradient(start.x, start.y,
847				end.x, end.y);
848			break;
849
850		case B_GRADIENT_TYPE_RADIAL:
851			var center = new RemotePoint(remoteMessage);
852			var radius = remoteMessage.dataView.readFloat32();
853
854			this.gradient = context.createRadialGradient(center.x, center.y, 0,
855				center.x, center.y, radius);
856			break;
857
858		default:
859			console.warn('gradient type not implemented: ' + this.type);
860			this.gradient = 'black';
861			return this;
862	}
863
864	var stopCount = remoteMessage.dataView.readUint32();
865	for (var i = 0; i < stopCount; i++) {
866		var color = remoteMessage.readColor(unsetAlpha);
867		var offset = remoteMessage.dataView.readFloat32() / 255;
868		this.gradient.addColorStop(offset, color);
869	}
870
871	return this;
872}
873
874
875function RemoteShape(remoteMessage)
876{
877	if (remoteMessage) {
878		this.readFrom(remoteMessage);
879		return;
880	}
881
882	this.opCount = 0;
883	this.ops = [];
884	this.pointCount = 0;
885	this.points = [];
886}
887
888
889RemoteShape.prototype.readFrom = function(remoteMessage)
890{
891	this.bounds = new RemoteRect(remoteMessage);
892
893	this.opCount = remoteMessage.dataView.readUint32();
894	this.ops = new Array(this.opCount);
895	for (var i = 0; i < this.opCount; i++)
896		this.ops[i] = remoteMessage.dataView.readUint32();
897
898	this.pointCount = remoteMessage.dataView.readUint32();
899	this.points = new Array(this.pointCount);
900	for (var i = 0; i < this.pointCount; i++)
901		this.points[i] = new RemotePoint(remoteMessage);
902
903	return this;
904}
905
906
907RemoteShape.prototype.play = function(context)
908{
909	var pointIndex = 0;
910	for (var i = 0; i < this.opCount; i++) {
911		var op = this.ops[i] & 0xff000000;
912		var count = this.ops[i] & 0x00ffffff;
913
914		if (op & B_SHAPE_OP_MOVE_TO) {
915			var point = this.points[pointIndex++];
916			context.moveTo(point.x, point.y);
917		}
918
919		if (op & B_SHAPE_OP_LINE_TO) {
920			for (var j = 0; j < count; j++) {
921				var point = this.points[pointIndex++];
922				context.lineTo(point.x, point.y);
923			}
924		}
925
926		if (op & B_SHAPE_OP_BEZIER_TO) {
927			for (var j = 0; j < count / 3; j++) {
928				var control1 = this.points[pointIndex++];
929				var control2 = this.points[pointIndex++];
930				var to = this.points[pointIndex++];
931				context.bezierCurveTo(control1.x, control1.y, control2.x,
932					control2.y, to.x, to.y);
933			}
934		}
935
936		if (op & (B_SHAPE_OP_LARGE_ARC_TO_CW | B_SHAPE_OP_LARGE_ARC_TO_CCW
937				| B_SHAPE_OP_SMALL_ARC_TO_CW | B_SHAPE_OP_SMALL_ARC_TO_CCW)) {
938
939			console.warn('shape op arc to not implemented');
940			for (var j = 0; j < count / 3; j++)
941				pointIndex++;
942		}
943
944		if (op & B_SHAPE_OP_CLOSE)
945			context.closePath();
946	}
947}
948
949
950function RemoteMessage(socket)
951{
952	this.socket = socket;
953}
954
955
956RemoteMessage.staticRemoteColor = new RemoteColor();
957
958
959RemoteMessage.prototype.allocate = function(bufferSize)
960{
961	this.buffer = new Uint8Array(bufferSize);
962	this.dataView = new StreamingDataView(this.buffer, true);
963}
964
965
966RemoteMessage.prototype.ensureBufferSize = function(bufferSize)
967{
968	if (this.buffer.byteLength < bufferSize)
969		this.allocate(bufferSize);
970}
971
972
973RemoteMessage.prototype.attach = function(buffer, byteOffset)
974{
975	var bytesLeft = buffer.byteLength - byteOffset;
976	if (bytesLeft < 6)
977		return false;
978
979	this.buffer = buffer;
980	this.dataView = new StreamingDataView(this.buffer, true, byteOffset);
981	this.messageCode = this.dataView.readUint16();
982	this.messageSize = this.dataView.readUint32();
983	if (this.messageSize < 6)
984		throw false;
985
986	return this.messageSize <= bytesLeft;
987}
988
989
990RemoteMessage.prototype.code = function()
991{
992	return this.messageCode;
993}
994
995
996RemoteMessage.prototype.size = function()
997{
998	return this.messageSize;
999}
1000
1001
1002RemoteMessage.prototype.start = function(code)
1003{
1004	this.dataView.rewind();
1005	this.dataView.writeUint16(code);
1006	this.dataView.writeUint32(0);
1007		// Placeholder for size field.
1008}
1009
1010
1011RemoteMessage.prototype.flush = function()
1012{
1013	this.dataView.setUint32(2, this.dataView.position);
1014	this.socket.send(this.buffer.slice(0, this.dataView.position));
1015}
1016
1017
1018RemoteMessage.prototype.readColor = function(unsetAlpha)
1019{
1020	return RemoteMessage.staticRemoteColor.readFrom(this).toColor(unsetAlpha);
1021}
1022
1023
1024function RemoteState(session, token)
1025{
1026	this.session = session;
1027	this.token = token;
1028
1029	this.lowColor = new RemoteColor().fromUint32(0xffffffff);
1030	this.highColor = new RemoteColor().fromUint32(0xff000000);
1031
1032	this.penSize = 1.0;
1033	this.lineCap = 'butt';
1034	this.lineJoin = 'miter';
1035	this.miterLimit = B_DEFAULT_MITER_LIMIT;
1036	this.drawingMode = 'source-over';
1037
1038	this.pattern = new RemotePattern();
1039	this.font = new RemoteFont();
1040	this.transform = new RemoteTransform();
1041}
1042
1043
1044RemoteState.prototype.applyContext = function()
1045{
1046	var context = this.session.context;
1047	if (!this.invalidated && context.currentToken == this.token)
1048		return;
1049
1050	this.session.removeClipping();
1051
1052	if (this.blendModesEnabled && this.constantAlpha)
1053		context.globalAlpha = this.highColor.alpha / 255;
1054	else
1055		context.globalAlpha = 1;
1056
1057	var style;
1058	if (this.pattern.isSolid()) {
1059		style = this.pattern.data[0] == 0 ? this.lowColor : this.highColor;
1060		if (this.invert)
1061			style = style == this.lowColor ? 'transparent' : 'white';
1062		else
1063			style = style.toColor(this.unsetAlpha);
1064	} else {
1065		style = this.pattern.toPattern(context,
1066			this.invert ? 0x00000000 : this.lowColor.toUint32(this.unsetAlpha),
1067			this.invert
1068				? 0xffffffff : this.highColor.toUint32(this.unsetAlpha));
1069	}
1070
1071	context.fillStyle = context.strokeStyle = style;
1072
1073	context.font = (this.font.face & B_ITALIC_FACE ? "italic " : "")
1074		+ (this.font.face & B_BOLD_FACE ? "bold " : "")
1075		+ this.font.size + 'px '
1076		+ (this.font.spacing==B_FIXED_SPACING ? 'monospace' : 'Helvetica');
1077	context.globalCompositeOperation = this.drawingMode;
1078	context.lineWidth = this.penSize;
1079	context.lineCap = this.lineCap;
1080	context.lineJoin = this.lineJoin;
1081	context.miterLimit = this.miterLimit;
1082
1083	context.resetTransform();
1084
1085	this.session.applyClipping(this.clipRects);
1086
1087	if (!this.transform.isIdentity()) {
1088		context.translate(this.xOffset, this.yOffset);
1089		this.transform.apply(context);
1090		context.translate(-this.xOffset, -this.yOffset);
1091	}
1092
1093	context.currentToken = this.token;
1094	this.invalidated = false;
1095}
1096
1097
1098RemoteState.prototype.prepareForRect = function()
1099{
1100	this.session.context.lineJoin = 'miter';
1101	this.session.context.miterLimit = 10;
1102}
1103
1104
1105RemoteState.prototype.messageReceived = function(remoteMessage, reply)
1106{
1107	var context = this.session.context;
1108
1109	switch (remoteMessage.code()) {
1110		case RP_ENABLE_SYNC_DRAWING:
1111		case RP_DISABLE_SYNC_DRAWING:
1112			console.warn('sync drawing en-/disable not implemented');
1113			break;
1114
1115		case RP_SET_LOW_COLOR:
1116			this.lowColor.readFrom(remoteMessage);
1117			this.invalidated = true;
1118			break;
1119
1120		case RP_SET_HIGH_COLOR:
1121			this.highColor.readFrom(remoteMessage);
1122			this.invalidated = true;
1123			break;
1124
1125		case RP_SET_OFFSETS:
1126			this.xOffset = remoteMessage.dataView.readInt32();
1127			this.yOffset = remoteMessage.dataView.readInt32();
1128			this.invalidated = true;
1129			break;
1130
1131		case RP_SET_FONT:
1132			this.font = new RemoteFont(remoteMessage);
1133			this.invalidated = true;
1134			break;
1135
1136		case RP_SET_TRANSFORM:
1137			this.transform = new RemoteTransform(remoteMessage);
1138			this.invalidated = true;
1139			break;
1140
1141		case RP_SET_PATTERN:
1142			this.pattern = new RemotePattern(remoteMessage);
1143			this.invalidated = true;
1144			break;
1145
1146		case RP_SET_PEN_SIZE:
1147			this.penSize = remoteMessage.dataView.readFloat32();
1148			this.invalidated = true;
1149			break;
1150
1151		case RP_SET_STROKE_MODE:
1152			switch (remoteMessage.dataView.readUint32()) {
1153				case B_ROUND_CAP:
1154					this.lineCap = 'round';
1155					break;
1156
1157				case B_BUTT_CAP:
1158					this.lineCap = 'butt';
1159					break;
1160
1161				case B_SQUARE_CAP:
1162					this.lineCap = 'square';
1163					break;
1164			}
1165
1166			var lineJoin = remoteMessage.dataView.readUint32();
1167			switch (lineJoin) {
1168				case B_ROUND_JOIN:
1169					this.lineJoin = 'round';
1170					break;
1171
1172				case B_MITER_JOIN:
1173					this.lineJoin = 'miter';
1174					break;
1175
1176				case B_BEVEL_JOIN:
1177					this.lineJoin = 'bevel';
1178					break;
1179
1180				default:
1181					console.warn('line join not implemented: ' + join);
1182					break;
1183			}
1184
1185			this.miterLimit = remoteMessage.dataView.readFloat32();
1186			this.invalidated = true;
1187			break;
1188
1189		case RP_SET_BLENDING_MODE:
1190			var sourceAlpha = remoteMessage.dataView.readUint32();
1191			this.constantAlpha = sourceAlpha == B_CONSTANT_ALPHA;
1192			if (this.blendModesEnabled)
1193				this.unsetAlpha = this.constantAlpha;
1194
1195			var alphaFunction = remoteMessage.dataView.readUint32();
1196			if (alphaFunction != B_ALPHA_OVERLAY)
1197				console.warn('alpha function not supported: ' + alphaFunction);
1198
1199			this.invalidated = true;
1200			break;
1201
1202		case RP_SET_DRAWING_MODE:
1203			var drawingMode = remoteMessage.dataView.readUint32();
1204
1205			this.unsetAlpha = false;
1206			this.blendModesEnabled = false;
1207			this.invert = false;
1208
1209			switch (drawingMode) {
1210				case B_OP_COPY:
1211					this.unsetAlpha = true;
1212					this.drawingMode = 'source-over';
1213					break;
1214
1215				case B_OP_OVER:
1216					this.drawingMode = 'source-over';
1217					break;
1218
1219				case B_OP_ALPHA:
1220					this.blendModesEnabled = true;
1221					this.unsetAlpha = this.constantAlpha;
1222					this.drawingMode = 'source-over';
1223					break;
1224
1225				case B_OP_BLEND:
1226					this.drawingMode = 'lighter';
1227					break;
1228
1229				case B_OP_MIN:
1230					this.drawingMode = 'darken';
1231					break;
1232
1233				case B_OP_MAX:
1234					this.drawingMode = 'ligthen';
1235					break;
1236
1237				case B_OP_INVERT:
1238					this.drawingMode = 'difference';
1239					this.invert = true;
1240					break;
1241
1242				case B_OP_ADD:
1243					this.drawingMode = 'lighter';
1244					break;
1245
1246/*
1247				case B_OP_ERASE:
1248					this.drawingMode = 'destination-out';
1249					break;
1250
1251				case B_OP_SUBTRACT:
1252					this.drawingMode = 'difference';
1253					break;
1254*/
1255
1256				default:
1257					console.warn('drawing mode not implemented: '
1258						+ drawingMode);
1259					this.drawingMode = 'source-over';
1260					break;
1261			}
1262
1263			this.invalidated = true;
1264			break;
1265
1266		case RP_CONSTRAIN_CLIPPING_REGION:
1267			var rectCount = remoteMessage.dataView.readUint32();
1268			this.clipRects = new Array(rectCount);
1269			for (var i = 0; i < rectCount; i++)
1270				this.clipRects[i] = new RemoteRect(remoteMessage);
1271
1272			this.invalidated = true;
1273			break;
1274
1275		case RP_INVERT_RECT:
1276			this.applyContext();
1277
1278			var rect = new RemoteRect(remoteMessage);
1279
1280			context.save();
1281			context.globalCompositeOperation = 'difference';
1282			context.fillStyle = 'white';
1283			this.prepareForRect();
1284
1285			rect.apply(context.fillRect.bind(context));
1286
1287			context.restore();
1288			break;
1289
1290		case RP_DRAW_BITMAP:
1291			this.applyContext();
1292
1293			var bitmapRect = new RemoteRect(remoteMessage);
1294			var viewRect = new RemoteRect(remoteMessage);
1295			var options = remoteMessage.dataView.readUint32();
1296				// TODO: Implement options.
1297
1298			if (options != 0)
1299				console.warn('bitmap options not supported: ' + options);
1300
1301			var bitmap = new RemoteBitmap(remoteMessage, this.unsetAlpha);
1302			context.drawImage(bitmap.canvas, bitmapRect.left, bitmapRect.top,
1303				bitmapRect.width(), bitmapRect.height(), viewRect.left,
1304				viewRect.top, viewRect.width(), viewRect.height());
1305			break;
1306
1307		case RP_DRAW_BITMAP_RECTS:
1308			this.applyContext();
1309
1310			var options = remoteMessage.dataView.readUint32();
1311				// TODO: Implement options.
1312			var colorSpace = remoteMessage.dataView.readUint32();
1313			var flags = remoteMessage.dataView.readUint32();
1314
1315			if (options != 0)
1316				console.warn('bitmap options not supported: ' + options);
1317
1318			var rectCount = remoteMessage.dataView.readUint32();
1319			for (var i = 0; i < rectCount; i++) {
1320				var rect = new RemoteRect(remoteMessage);
1321				var bitmap = new RemoteBitmap(remoteMessage, this.unsetAlpha,
1322					colorSpace, flags);
1323
1324				context.drawImage(bitmap.canvas, 0, 0, bitmap.width,
1325					bitmap.height, rect.left, rect.top, rect.width(),
1326					rect.height());
1327			}
1328			break;
1329
1330		case RP_DRAW_STRING:
1331			this.applyContext();
1332
1333			var where = new RemotePoint(remoteMessage);
1334			var length = remoteMessage.dataView.readUint32();
1335			var string = remoteMessage.dataView.readString(length);
1336
1337			context.save();
1338			context.fillStyle = this.highColor.toColor(this.unsetAlpha);
1339			context.fillText(string, where.x, where.y);
1340
1341			var textMetric = context.measureText(string);
1342			where.x += textMetric.width;
1343
1344			context.restore();
1345
1346			reply.start(RP_DRAW_STRING_RESULT);
1347			reply.dataView.writeInt32(this.token);
1348			where.writeTo(reply);
1349			reply.flush();
1350			break;
1351
1352		case RP_DRAW_STRING_WITH_OFFSETS:
1353			this.applyContext();
1354
1355			var length = remoteMessage.dataView.readUint32();
1356			var string = remoteMessage.dataView.readString(length);
1357
1358			context.save();
1359			context.fillStyle = this.highColor.toColor(this.unsetAlpha);
1360
1361			var where;
1362			for (var i = 0; i < string.length; i++) {
1363				where = new RemotePoint(remoteMessage);
1364				context.fillText(string[i], where.x, where.y);
1365			}
1366
1367			var textMetric = context.measureText(string[string.length - 1]);
1368			where.x += textMetric.width;
1369
1370			context.restore();
1371
1372			reply.start(RP_DRAW_STRING_RESULT);
1373			reply.dataView.writeInt32(this.token);
1374			where.writeTo(reply);
1375			reply.flush();
1376			break;
1377
1378		case RP_STRING_WIDTH:
1379			this.applyContext();
1380
1381			var length = remoteMessage.dataView.readUint32();
1382			var string = remoteMessage.dataView.readString(length);
1383			var textMetric = context.measureText(string);
1384
1385			reply.start(RP_STRING_WIDTH_RESULT);
1386			reply.dataView.writeInt32(this.token);
1387			where.writeFloat32(textMetric.width);
1388			reply.flush();
1389			break;
1390
1391		case RP_STROKE_ARC:
1392		case RP_FILL_ARC:
1393			this.applyContext();
1394
1395			var rect = new RemoteRect(remoteMessage);
1396			var startAngle
1397				= remoteMessage.dataView.readFloat32() * Math.PI / 180;
1398			var invertStart = Math.PI * 2 - startAngle;
1399			startAngle += Math.PI / 2;
1400
1401			var span = remoteMessage.dataView.readFloat32() * Math.PI / 180;
1402			var centerX = Math.round(rect.centerX());
1403			var centerY = Math.round(rect.centerY());
1404			var radius = rect.width() / 2;
1405			var maxSpan
1406				= remoteMessage.code() != RP_STROKE_ARC ? Math.PI / 2 : span;
1407
1408			var arcStep = function(max) {
1409					max = Math.min(max, span);
1410
1411					context.beginPath();
1412					context.arc(centerX, centerY, radius, invertStart,
1413						invertStart - max, true);
1414
1415					switch (remoteMessage.code()) {
1416						case RP_STROKE_ARC:
1417							context.stroke();
1418							break;
1419
1420						case RP_FILL_ARC:
1421							context.moveTo(centerX, centerY);
1422							var endAngle = startAngle + max;
1423							context.lineTo(
1424								centerX + radius * Math.sin(startAngle),
1425								centerY + radius * Math.cos(startAngle));
1426							context.lineTo(
1427								centerX + radius * Math.sin(endAngle),
1428								centerY + radius * Math.cos(endAngle));
1429							context.fill();
1430							break;
1431					}
1432
1433					startAngle += max;
1434					invertStart -= max;
1435					span -= max;
1436				};
1437
1438			while (span > 0)
1439				arcStep(maxSpan);
1440
1441			break;
1442
1443		case RP_STROKE_RECT:
1444		case RP_STROKE_ELLIPSE:
1445		case RP_FILL_RECT:
1446		case RP_FILL_ELLIPSE:
1447			this.applyContext();
1448
1449			context.save();
1450			this.prepareForRect();
1451
1452			var rect = new RemoteRect(remoteMessage);
1453
1454			switch (remoteMessage.code()) {
1455				case RP_STROKE_RECT:
1456					rect.apply(context.strokeRect.bind(context));
1457					break;
1458				case RP_STROKE_ELLIPSE:
1459					rect.applyAsEllipse(context, context.stroke);
1460					break;
1461				case RP_FILL_RECT:
1462					rect.apply(context.fillRect.bind(context));
1463					break;
1464				case RP_FILL_ELLIPSE:
1465					rect.applyAsEllipse(context, context.fill);
1466					break;
1467			}
1468
1469			context.restore();
1470			break;
1471
1472		case RP_STROKE_ROUND_RECT:
1473		case RP_FILL_ROUND_RECT:
1474		case RP_FILL_ROUND_RECT_GRADIENT:
1475			this.applyContext();
1476
1477			context.save();
1478			this.prepareForRect();
1479
1480			var rect = new RemoteRect(remoteMessage);
1481			var xRadius = remoteMessage.dataView.readFloat32();
1482			var yRadius = remoteMessage.dataView.readFloat32();
1483
1484			if (remoteMessage.code() == RP_FILL_ROUND_RECT_GRADIENT) {
1485				context.save();
1486				var gradient = new RemoteGradient(remoteMessage, context,
1487					this.unsetAlpha);
1488				context.fillStyle = gradient.gradient;
1489			}
1490
1491			console.warn('round rects not implemented, falling back to rect');
1492			if (remoteMessage.code() == RP_STROKE_ROUND_RECT)
1493				rect.apply(context.strokeRect.bind(context));
1494			else
1495				rect.apply(context.fillRect.bind(context));
1496
1497			if (remoteMessage.code() == RP_FILL_ROUND_RECT_GRADIENT)
1498				context.restore();
1499
1500			context.restore();
1501			break;
1502
1503		case RP_STROKE_LINE:
1504			this.applyContext();
1505
1506			var from = new RemotePoint(remoteMessage);
1507			var to = new RemotePoint(remoteMessage);
1508
1509			context.beginPath();
1510			context.moveTo(from.x, from.y);
1511			context.lineTo(to.x, to.y);
1512			context.stroke();
1513			break;
1514
1515		case RP_STROKE_LINE_ARRAY:
1516			this.applyContext();
1517
1518			context.save();
1519			context.lineCap = 'square';
1520
1521			var numLines = remoteMessage.dataView.readUint32();
1522			for (var i = 0; i < numLines; i++) {
1523				var from = new RemotePoint(remoteMessage);
1524				var to = new RemotePoint(remoteMessage);
1525				context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1526				context.beginPath();
1527				context.moveTo(from.x + 0.5, from.y + 0.5);
1528				context.lineTo(to.x + 0.5, to.y + 0.5);
1529				context.stroke();
1530			}
1531
1532			context.restore();
1533			break;
1534
1535		case RP_STROKE_POINT_COLOR:
1536			this.applyContext();
1537
1538			var point = new RemotePoint(remoteMessage);
1539
1540			context.save();
1541			context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
1542
1543			context.fillRect(point.x, point.y, 1, 1);
1544			context.restore();
1545			break;
1546
1547		case RP_STROKE_LINE_1PX_COLOR:
1548			this.applyContext();
1549
1550			var from = new RemotePoint(remoteMessage);
1551			var to = new RemotePoint(remoteMessage);
1552
1553			context.save();
1554			context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1555			context.lineWidth = 1;
1556			context.lineCap = 'square';
1557
1558			context.beginPath();
1559			context.moveTo(from.x + 0.5, from.y + 0.5);
1560			context.lineTo(to.x + 0.5, to.y + 0.5);
1561			context.stroke();
1562
1563			context.restore();
1564			break;
1565
1566		case RP_STROKE_RECT_1PX_COLOR:
1567			this.applyContext();
1568
1569			var rect = new RemoteRect(remoteMessage);
1570
1571			context.save();
1572			this.prepareForRect();
1573
1574			context.strokeStyle = remoteMessage.readColor(this.unsetAlpha);
1575			context.lineWidth = 1;
1576
1577			rect.apply(context.strokeRect.bind(context));
1578
1579			context.restore();
1580			break;
1581
1582		case RP_STROKE_SHAPE:
1583		case RP_FILL_SHAPE:
1584		case RP_FILL_SHAPE_GRADIENT:
1585			this.applyContext();
1586
1587			var shape = new RemoteShape(remoteMessage);
1588			var offset = new RemotePoint(remoteMessage);
1589			var scale = remoteMessage.dataView.readFloat32();
1590
1591			context.save();
1592			if (remoteMessage.code() == RP_FILL_SHAPE_GRADIENT) {
1593				var gradient = new RemoteGradient(remoteMessage, context,
1594					this.unsetAlpha);
1595				context.fillStyle = gradient.gradient;
1596			}
1597
1598			context.translate(offset.x + 0.5, offset.y + 0.5);
1599			context.scale(scale, scale);
1600
1601			context.beginPath();
1602
1603			shape.play(context);
1604
1605			if (remoteMessage.code() == RP_STROKE_SHAPE)
1606				context.stroke();
1607			else
1608				context.fill();
1609
1610			context.restore();
1611			break;
1612
1613		case RP_STROKE_TRIANGLE:
1614		case RP_FILL_TRIANGLE:
1615		case RP_FILL_TRIANGLE_GRADIENT:
1616			this.applyContext();
1617
1618			if (remoteMessage.code() == RP_FILL_TRIANGLE_GRADIENT)
1619				context.save();
1620
1621			context.beginPath();
1622			var point = new RemotePoint(remoteMessage);
1623			context.moveTo(point.x + 0.5, point.y + 0.5);
1624
1625			for (var i = 0; i < 2; i++) {
1626				point = new RemotePoint(remoteMessage);
1627				context.lineTo(point.x + 0.5, point.y + 0.5);
1628			}
1629
1630			if (remoteMessage.code() == RP_FILL_TRIANGLE_GRADIENT) {
1631				var unusedBounds = new RemoteRect(remoteMessage);
1632				var gradient = new RemoteGradient(remoteMessage, context,
1633					this.unsetAlpha);
1634				context.fillStyle = gradient.gradient;
1635			}
1636
1637			switch (remoteMessage.code()) {
1638				case RP_STROKE_TRIANGLE:
1639					context.closePath();
1640					context.stroke();
1641					break;
1642
1643				case RP_FILL_TRIANGLE:
1644					context.fill();
1645					break;
1646
1647				case RP_FILL_TRIANGLE_GRADIENT:
1648					context.fill();
1649					context.restore();
1650					break;
1651			}
1652
1653			break;
1654
1655		case RP_FILL_RECT_COLOR:
1656			this.applyContext();
1657
1658			var rect = new RemoteRect(remoteMessage);
1659
1660			context.save();
1661			this.prepareForRect();
1662			context.fillStyle = remoteMessage.readColor(this.unsetAlpha);
1663
1664			rect.apply(context.fillRect.bind(context));
1665
1666			context.restore();
1667			break;
1668
1669		case RP_FILL_RECT_GRADIENT:
1670		case RP_FILL_ELLIPSE_GRADIENT:
1671			this.applyContext();
1672
1673			var rect = new RemoteRect(remoteMessage);
1674
1675			context.save();
1676			this.prepareForRect();
1677
1678			var gradient = new RemoteGradient(remoteMessage, context,
1679				this.unsetAlpha);
1680			context.fillStyle = gradient.gradient;
1681
1682			if (remoteMessage.code() == RP_FILL_RECT_GRADIENT)
1683				rect.apply(context.fillRect.bind(context));
1684			else
1685				rect.applyAsEllipse(context, context.fill);
1686
1687			context.restore();
1688			break;
1689
1690		case RP_FILL_REGION:
1691		case RP_FILL_REGION_GRADIENT:
1692			this.applyContext();
1693
1694			var rectCount = remoteMessage.dataView.readUint32();
1695			var rects = new Array(rectCount);
1696			for (var i = 0; i < rectCount; i++)
1697				rects[i] = new RemoteRect(remoteMessage);
1698
1699			if (remoteMessage.code() == RP_FILL_REGION_GRADIENT) {
1700				context.save();
1701				var gradient = new RemoteGradient(remoteMessage, context,
1702					this.unsetAlpha);
1703				context.fillStyle = gradient.gradient;
1704			}
1705
1706			for (var i = 0; i < rectCount; i++)
1707				rects[i].apply(context.fillRect.bind(context));
1708
1709			if (remoteMessage.code() == RP_FILL_REGION_GRADIENT)
1710				context.restore();
1711
1712			break;
1713
1714		case RP_READ_BITMAP:
1715			var bounds = new RemoteRect(remoteMessage);
1716			var drawCursor = remoteMessage.dataView.readUint8();
1717				// TODO: Support the drawCursor flag.
1718
1719			if (drawCursor)
1720				console.warn('draw cursor in read bitmap not supported');
1721
1722			var width = bounds.integerWidth() + 1;
1723			var height = bounds.integerHeight() + 1;
1724			var bytesPerPixel = 3;
1725			var bytesPerRow = (width * bytesPerPixel + 3) & ~7;
1726			var padding = bytesPerRow - width * bytesPerPixel;
1727			var bitsLength = height * bytesPerRow;
1728
1729			reply.ensureBufferSize(bitsLength + 1024);
1730
1731			reply.start(RP_READ_BITMAP_RESULT);
1732			reply.dataView.writeInt32(this.token);
1733
1734			reply.dataView.writeInt32(width);
1735			reply.dataView.writeInt32(height);
1736			reply.dataView.writeInt32(bytesPerRow);
1737			reply.dataView.writeUint32(B_RGB24);
1738			reply.dataView.writeUint32(0); // Flags
1739			reply.dataView.writeUint32(bitsLength);
1740
1741			var position = 0;
1742			var imageData
1743				= context.getImageData(bounds.left, bounds.top, width, height);
1744			for (var y = 0; y < height; y++) {
1745				for (var x = 0; x < width; x++, position += 4) {
1746					reply.dataView.writeUint8(imageData.data[position + 2]);
1747					reply.dataView.writeUint8(imageData.data[position + 1]);
1748					reply.dataView.writeUint8(imageData.data[position + 0]);
1749				}
1750
1751				reply.dataView.pad(padding);
1752			}
1753
1754			reply.flush();
1755			break;
1756
1757		default:
1758			console.warn('unhandled message: code: ' + remoteMessage.code()
1759				+ '; size: ' + remoteMessage.size());
1760			break;
1761	}
1762}
1763
1764
1765function RemoteDesktopSession(targetElement, width, height, targetAddress,
1766	disconnectCallback)
1767{
1768	this.websocket = new WebSocket(targetAddress, 'binary');
1769	this.websocket.binaryType = 'arraybuffer';
1770	this.websocket.onopen = this.onOpen.bind(this);
1771	this.websocket.onmessage = this.onMessage.bind(this);
1772	this.websocket.onerror = this.onError.bind(this);
1773	this.websocket.onclose = this.onClose.bind(this);
1774
1775	this.disconnectCallback = disconnectCallback;
1776
1777	this.sendMessage = new RemoteMessage(this.websocket);
1778	this.sendMessage.allocate(1024);
1779
1780	this.receiveMessage = new RemoteMessage();
1781
1782	this.container = document.createElement('div');
1783	this.container.className = 'session';
1784	this.container.style.position = 'relative';
1785	targetElement.appendChild(this.container);
1786
1787	this.canvas = document.createElement('canvas');
1788	this.canvas.className = 'session';
1789	this.canvas.width = width;
1790	this.canvas.height = height;
1791	this.container.appendChild(this.canvas);
1792
1793	this.canvas.tabIndex = 0;
1794	this.canvas.focus();
1795
1796	this.context = this.canvas.getContext('2d', { alpha: false });
1797	this.context.imageSmoothingEnabled = false;
1798
1799	this.cursorVisible = true;
1800	this.cursorPosition = { x: 0, y: 0 };
1801	this.cursorHotspot = { x: 0, y: 0 };
1802
1803	this.states = new Object();
1804	this.modifiers = 0;
1805
1806	this.canvas.onmousemove = this.onMouseMove.bind(this);
1807	this.canvas.onmousedown = this.onMouseDown.bind(this);
1808	this.canvas.onmouseup = this.onMouseUp.bind(this);
1809	this.canvas.onwheel = this.onWheel.bind(this);
1810
1811	this.canvas.onkeydown = this.onKeyDownUp.bind(this);
1812	this.canvas.onkeyup = this.onKeyDownUp.bind(this);
1813	this.canvas.onkeypress = this.onKeyPress.bind(this);
1814
1815	this.canvas.oncontextmenu = function(event) {
1816			event.preventDefault();
1817		};
1818
1819	this.canvas.onblur = function(event) {
1820			event.target.focus();
1821		};
1822}
1823
1824
1825RemoteDesktopSession.prototype.onOpen = function(open)
1826{
1827	console.log('open:', open);
1828	this.init();
1829}
1830
1831
1832RemoteDesktopSession.prototype.onMessage = function(message)
1833{
1834	var data = message.data;
1835	if (this.messageRemainder) {
1836		var combined = new Uint8Array(this.messageRemainder.byteLength
1837			+ data.byteLength);
1838		combined.set(new Uint8Array(this.messageRemainder), 0);
1839		combined.set(new Uint8Array(data), this.messageRemainder.byteLength);
1840		data = combined;
1841
1842		this.messageRemainder = null;
1843	} else
1844		data = new Uint8Array(data);
1845
1846	var byteOffset = 0;
1847	while (true) {
1848		try {
1849			if (!this.receiveMessage.attach(data, byteOffset))
1850				break;
1851		} catch (exception) {
1852			// Discard everything and hope for the best.
1853			console.error('stream invalid, discarding everything', exception,
1854				this.receiveMessage, data, byteOffset);
1855			return;
1856		}
1857
1858		try {
1859			this.messageReceived(this.receiveMessage, this.sendMessage);
1860		} catch (exception) {
1861			console.error('exception during message processing:', exception);
1862		}
1863
1864		byteOffset += this.receiveMessage.size();
1865	}
1866
1867	if (data.byteLength > byteOffset)
1868		this.messageRemainder = data.slice(byteOffset);
1869}
1870
1871
1872RemoteDesktopSession.prototype.messageReceived = function(remoteMessage, reply)
1873{
1874	switch (remoteMessage.code()) {
1875		case RP_INIT_CONNECTION:
1876			console.log('init connection reply');
1877			this.sendMessage.start(RP_UPDATE_DISPLAY_MODE);
1878			this.sendMessage.dataView.writeUint32(this.canvas.width);
1879			this.sendMessage.dataView.writeUint32(this.canvas.height);
1880			this.sendMessage.flush();
1881
1882			this.sendMessage.start(RP_GET_SYSTEM_PALETTE);
1883			this.sendMessage.flush();
1884			break;
1885
1886		case RP_GET_SYSTEM_PALETTE_RESULT:
1887			var count = remoteMessage.dataView.readUint32();
1888			gSystemPalette = new Uint32Array(count);
1889
1890			var color = new RemoteColor();
1891			for (var i = 0; i < gSystemPalette.length; i++)
1892				gSystemPalette[i] = color.readFrom(remoteMessage).toUint32();
1893
1894			break;
1895
1896		case RP_CREATE_STATE:
1897			var token = remoteMessage.dataView.readInt32();
1898			console.log('create state: ' + token);
1899
1900			if (this.states.hasOwnProperty(token))
1901				console.error('create state for existing token: ' + token);
1902
1903			this.states[token] = new RemoteState(this, token);
1904			break;
1905
1906		case RP_DELETE_STATE:
1907			var token = remoteMessage.dataView.readInt32();
1908			console.log('delete state: ' + token);
1909
1910			if (!this.states.hasOwnProperty(token)) {
1911				console.error('delete state for unknown token: ' + token);
1912				break;
1913			}
1914
1915			delete this.states[token];
1916			break;
1917
1918		case RP_INVALIDATE_RECT:
1919		case RP_INVALIDATE_REGION:
1920			break;
1921
1922		case RP_SET_CURSOR:
1923			this.cursorHotspot = new RemotePoint(remoteMessage);
1924			var bitmap = new RemoteBitmap(remoteMessage);
1925
1926			bitmap.canvas.style.position = 'absolute';
1927			if (this.cursorCanvas)
1928				this.cursorCanvas.remove();
1929
1930			this.cursorCanvas = bitmap.canvas;
1931			this.cursorCanvas.style.pointerEvents = 'none';
1932			this.container.appendChild(this.cursorCanvas);
1933			this.container.style.cursor = 'none';
1934			this.updateCursor();
1935			break;
1936
1937		case RP_MOVE_CURSOR_TO:
1938			this.cursorPosition.x = remoteMessage.dataView.readFloat32();
1939			this.cursorPosition.y = remoteMessage.dataView.readFloat32();
1940			this.updateCursor();
1941			break;
1942
1943		case RP_SET_CURSOR_VISIBLE:
1944			this.cursorVisible = remoteMessage.dataView.readUint8();
1945			if (this.cursorCanvas) {
1946				this.cursorCanvas.style.visibility
1947					= this.cursorVisible ? 'visible' : 'hidden';
1948			}
1949			break;
1950
1951		case RP_COPY_RECT_NO_CLIPPING:
1952			var xOffset = remoteMessage.dataView.readInt32();
1953			var yOffset = remoteMessage.dataView.readInt32();
1954			var rect = new RemoteRect(remoteMessage);
1955
1956			var imageData = this.context.getImageData(rect.left, rect.top,
1957				rect.width(), rect.height());
1958			this.context.putImageData(imageData, rect.left + xOffset,
1959				rect.top + yOffset);
1960			break;
1961
1962		case RP_FILL_REGION_COLOR_NO_CLIPPING:
1963			this.removeClipping();
1964			this.context.currentToken = -1;
1965			this.context.resetTransform();
1966			this.context.globalCompositeOperation = 'source-over';
1967
1968			var rectCount = remoteMessage.dataView.readUint32();
1969			var rects = new Array(rectCount);
1970			for (var i = 0; i < rectCount; i++)
1971				rects[i] = new RemoteRect(remoteMessage);
1972
1973			this.context.fillStyle = remoteMessage.readColor();
1974
1975			for (var i = 0; i < rectCount; i++)
1976				rects[i].apply(this.context.fillRect.bind(this.context));
1977
1978			break;
1979
1980		default:
1981			var token = remoteMessage.dataView.readInt32();
1982			if (!this.states.hasOwnProperty(token)) {
1983				console.warn('no state for token: ' + token);
1984				this.states[token] = new RemoteState(this, token);
1985			}
1986
1987			this.states[token].messageReceived(remoteMessage, reply);
1988			break;
1989	}
1990}
1991
1992
1993RemoteDesktopSession.prototype.onError = function(error)
1994{
1995	console.log('websocket error:', error);
1996	this.onDisconnect(error);
1997}
1998
1999
2000RemoteDesktopSession.prototype.onClose = function(close)
2001{
2002	console.log('websocket close:', close);
2003	this.onDisconnect(close);
2004}
2005
2006
2007RemoteDesktopSession.prototype.onDisconnect = function(reason)
2008{
2009	this.container.remove();
2010	if (this.disconnectCallback)
2011		this.disconnectCallback(reason);
2012}
2013
2014
2015RemoteDesktopSession.prototype.applyClipping = function(clipRects)
2016{
2017	this.removeClipping();
2018
2019	if (!clipRects || clipRects.length == 0)
2020		return;
2021
2022	this.context.save();
2023	this.context.beginPath();
2024
2025	this.context.save();
2026	this.context.lineJoin = 'miter';
2027	this.context.miterLimit = 10;
2028
2029	for (var i = 0; i < clipRects.length; i++)
2030		clipRects[i].apply(this.context.rect.bind(this.context));
2031
2032	this.context.restore();
2033
2034	this.context.clip();
2035	this.clippingApplied = true;
2036}
2037
2038
2039RemoteDesktopSession.prototype.removeClipping = function()
2040{
2041	if (!this.clippingApplied)
2042		return;
2043
2044	this.context.restore();
2045}
2046
2047
2048RemoteDesktopSession.prototype.init = function()
2049{
2050	this.sendMessage.start(RP_INIT_CONNECTION);
2051	this.sendMessage.flush();
2052}
2053
2054
2055RemoteDesktopSession.prototype.updateCursor = function()
2056{
2057	if (!this.cursorVisible || !this.cursorCanvas)
2058		return;
2059
2060	this.cursorCanvas.style.left
2061		= (this.cursorPosition.x - this.cursorHotspot.x) + 'px';
2062	this.cursorCanvas.style.top
2063		= (this.cursorPosition.y - this.cursorHotspot.y) + 'px';
2064}
2065
2066
2067RemoteDesktopSession.prototype.onMouseMove = function(event)
2068{
2069	this.sendMessage.start(RP_MOUSE_MOVED);
2070	this.sendMessage.dataView.writeFloat32(event.offsetX);
2071	this.sendMessage.dataView.writeFloat32(event.offsetY);
2072	this.sendMessage.flush();
2073	event.preventDefault();
2074}
2075
2076
2077RemoteDesktopSession.prototype.onMouseDown = function(event)
2078{
2079	this.canvas.focus();
2080	this.sendMessage.start(RP_MOUSE_DOWN);
2081	this.sendMessage.dataView.writeFloat32(event.offsetX);
2082	this.sendMessage.dataView.writeFloat32(event.offsetY);
2083	this.sendMessage.dataView.writeUint32(event.buttons);
2084	this.sendMessage.dataView.writeUint32(event.detail);
2085	this.sendMessage.flush();
2086	event.preventDefault();
2087}
2088
2089
2090RemoteDesktopSession.prototype.onMouseUp = function(event)
2091{
2092	this.sendMessage.start(RP_MOUSE_UP);
2093	this.sendMessage.dataView.writeFloat32(event.offsetX);
2094	this.sendMessage.dataView.writeFloat32(event.offsetY);
2095	this.sendMessage.dataView.writeUint32(event.buttons);
2096	this.sendMessage.flush();
2097	event.preventDefault();
2098}
2099
2100
2101RemoteDesktopSession.prototype.onKeyDownUp = function(event)
2102{
2103	var keyDown = event.type === 'keydown';
2104	var lockModifier = false;
2105	var modifiersChanged = 0;
2106	switch (event.code) {
2107		case 'ShiftLeft':
2108			modifiersChanged |= B_LEFT_SHIFT_KEY;
2109			if (event.shiftKey == keyDown)
2110				modifiersChanged |= B_SHIFT_KEY;
2111			break;
2112
2113		case 'ShiftRight':
2114			modifiersChanged |= B_RIGHT_SHIFT_KEY;
2115			if (event.shiftKey == keyDown)
2116				modifiersChanged |= B_SHIFT_KEY;
2117			break;
2118
2119		case 'ControlLeft':
2120			modifiersChanged |= B_LEFT_CONTROL_KEY;
2121			if (event.ctrlKey == keyDown)
2122				modifiersChanged |= B_CONTROL_KEY;
2123			break;
2124
2125		case 'ControlRight':
2126			modifiersChanged |= B_RIGHT_CONTROL_KEY;
2127			if (event.ctrlKey == keyDown)
2128				modifiersChanged |= B_CONTROL_KEY;
2129			break;
2130
2131		case 'AltLeft':
2132			modifiersChanged |= B_LEFT_COMMAND_KEY;
2133			if (event.altKey == keyDown)
2134				modifiersChanged |= B_COMMAND_KEY;
2135			break;
2136
2137		case 'AltRight':
2138			modifiersChanged |= B_RIGHT_COMMAND_KEY;
2139			if (event.altKey == keyDown)
2140				modifiersChanged |= B_COMMAND_KEY;
2141			break;
2142
2143		case 'ContextMenu':
2144			modifiersChanged |= B_MENU_KEY;
2145			break;
2146
2147		case 'CapsLock':
2148			modifiersChanged |= B_CAPS_LOCK;
2149			lockModifier = true;
2150			break;
2151
2152		case 'ScrollLock':
2153			modifiersChanged |= B_SCROLL_LOCK;
2154			lockModifier = true;
2155			break;
2156
2157		case 'NumLock':
2158			modifiersChanged |= B_NUM_LOCK;
2159			lockModifier = true;
2160			break;
2161	}
2162
2163	if (modifiersChanged != 0) {
2164		if (lockModifier) {
2165			if (((this.modifiers & modifiersChanged) == 0) == keyDown)
2166				this.modifiers ^= modifiersChanged;
2167		} else {
2168			if (keyDown)
2169				this.modifiers |= modifiersChanged;
2170			else
2171				this.modifiers &= ~modifiersChanged;
2172		}
2173
2174		this.sendMessage.start(RP_MODIFIERS_CHANGED);
2175		this.sendMessage.dataView.writeUint32(this.modifiers);
2176		this.sendMessage.flush();
2177		event.preventDefault();
2178		return;
2179	}
2180
2181	this.sendMessage.start(keyDown ? RP_KEY_DOWN : RP_KEY_UP);
2182	if (event.key.length == 1)
2183		this.sendMessage.dataView.writeString(event.key);
2184	else {
2185		this.sendMessage.dataView.writeUint32(1);
2186		this.sendMessage.dataView.writeUint8(event.keyCode);
2187	}
2188
2189	if (event.keyCode) {
2190		this.sendMessage.dataView.writeUint32(0);
2191		this.sendMessage.dataView.writeUint32(event.keyCode);
2192	}
2193
2194	this.sendMessage.flush();
2195	event.preventDefault();
2196}
2197
2198
2199RemoteDesktopSession.prototype.onKeyPress = function(event)
2200{
2201	this.sendMessage.start(RP_KEY_DOWN);
2202	this.sendMessage.dataView.writeUint32(1);
2203	this.sendMessage.dataView.writeUint8(event.which);
2204	this.sendMessage.flush();
2205	this.sendMessage.start(RP_KEY_UP);
2206	this.sendMessage.dataView.writeUint32(1);
2207	this.sendMessage.dataView.writeUint8(event.which);
2208	this.sendMessage.flush();
2209	event.preventDefault();
2210}
2211
2212
2213RemoteDesktopSession.prototype.onWheel = function(event)
2214{
2215	this.sendMessage.start(RP_MOUSE_WHEEL_CHANGED);
2216	this.sendMessage.dataView.writeFloat32(event.deltaX);
2217	this.sendMessage.dataView.writeFloat32(event.deltaY);
2218	this.sendMessage.flush();
2219	event.preventDefault();
2220}
2221
2222
2223function init()
2224{
2225	var targetAddressInput = document.querySelector('#targetAddress');
2226	var widthInput = document.querySelector('#width');
2227	var heightInput = document.querySelector('#height');
2228
2229	if (localStorage.targetAddress)
2230		targetAddressInput.value = localStorage.targetAddress;
2231	if (localStorage.width)
2232		widthInput.value = localStorage.width;
2233	if (localStorage.height)
2234		heightInput.value = localStorage.height;
2235
2236	var onDisconnect = function(reason) {
2237			document.body.classList.remove('connect');
2238			gSession = undefined;
2239		};
2240
2241	document.querySelector('#connectButton').onclick = function() {
2242			document.body.classList.add('connect');
2243
2244			localStorage.width = widthInput.value;
2245			localStorage.height = heightInput.value;
2246			localStorage.targetAddress = targetAddressInput.value;
2247
2248			gSession = new RemoteDesktopSession(document.body, widthInput.value,
2249				heightInput.value, targetAddressInput.value, onDisconnect);
2250		};
2251}
2252