Commit 54e7cbdf authored by Joel Martin's avatar Joel Martin

Viewport handling in include/display.js

Part of mobile device support:
https://github.com/kanaka/noVNC/issues/48

The Display object is redefined as a larger display region with
an equal or smaller visible viewport. The size of the full display
region is set/changed using resize(). The viewport is set/changed
using viewportChange().

All exposed routines that draw on the display now take coordinates
that are absolute (relative to the full display region). For example,
the result of fillRect(100, 100, 10, 10, [255,0,0]) will appear in the
canvas at (0,0) if the viewport is set to (100,100).

Details:

- Move the generic part of the viewport code from tests/viewport.html
  into include/display.

- Add two new routines to the Display interface:

    - viewportChange(deltaX, deltaY, width, height)
        - This adjusts the position of the visible viewport and/or the
          size of the viewport.

        - deltaX and deltaY specify how the position of the viewport
          should be shifted. The position of the viewport is clamped
          to the full region size (i.e. cannot outside the display
          region).

        - The clean and dirty regions of the display are updated based
          on calls to this routine. For example, if the viewport width
          is increased, then there is now a dirty box on the right
          side of the viewport. Another example, if the viewport is
          shifted down and to the left over the display region, there
          are now two dirty boxes: one on the left side and one
          on the bottom of the viewport.

    - getCleanDirtyReset()
        - This returns an object with the clean box and a list of
          dirty boxes (that need to be redrawn).

            {'cleanBox':
                {'x': x, 'y': y, 'w': w, 'h': h},
             'dirtyBoxes':
                [{'x': x, 'y': y, 'w': w, 'h': h}, ...]
             }

        - The coordinates in the clean and dirty boxes are absolute
          coordinates (relative to the full display region) but they
          are clipped to the visible viewport.

        - Calling this function also resets the clean rectangle to be
          the whole viewport (i.e. nothing visible needs to be redrawn
          dirty) so the caller of this routine is responsible for
          redrawing any
parent afecb839
......@@ -25,8 +25,12 @@ var that = {}, // Public API methods
imageDataCreate, imageDataGet, rgbxImageData, cmapImageData,
rgbxImageFill, cmapImageFill, setFillColor, rescale, flush,
c_width = 0,
c_height = 0,
// The full frame buffer (logical canvas) size
fb_width = 0,
fb_height = 0,
// The visible "physical canvas" viewport
viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1},
c_prevStyle = "",
......@@ -55,11 +59,11 @@ that.get_context = function () { return c_ctx; };
that.set_scale = function(scale) { rescale(scale); };
that.set_width = function (val) { that.resize(val, c_height); };
that.get_width = function() { return c_width; };
that.set_width = function (val) { that.resize(val, fb_height); };
that.get_width = function() { return fb_width; };
that.set_height = function (val) { that.resize(c_width, val); };
that.get_height = function() { return c_height; };
that.set_height = function (val) { that.resize(fb_width, val); };
that.get_height = function() { return fb_height; };
that.set_prefer_js = function(val) {
if (val && c_forceCanvas) {
......@@ -217,7 +221,10 @@ rescale = function(factor) {
return;
}
if (factor > 1.0) {
if (typeof(factor) === "undefined") {
factor = conf.scale;
} else if (factor > 1.0) {
factor = 1.0;
} else if (factor < 0.1) {
factor = 0.1;
......@@ -234,6 +241,174 @@ rescale = function(factor) {
c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)";
};
that.viewportChange = function(deltaX, deltaY, width, height) {
var c = conf.target, v = viewport, cr = cleanRect,
saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h;
if (typeof(deltaX) === "undefined") { deltaX = 0; }
if (typeof(deltaY) === "undefined") { deltaY = 0; }
if (typeof(width) === "undefined") { width = v.w; }
if (typeof(height) === "undefined") { height = v.h; }
// Size change
if (width > fb_width) { width = fb_width; }
if (height > fb_height) { height = fb_height; }
if ((v.w !== width) || (v.h !== height)) {
// Change width
if ((width < v.w) && (cr.x2 > v.x + width -1)) {
cr.x2 = v.x + width - 1;
}
v.w = width;
// Change height
if ((height < v.h) && (cr.y2 > v.y + height -1)) {
cr.y2 = v.y + height - 1;
}
v.h = height;
if (v.w > 0 && v.h > 0) {
saveImg = c_ctx.getImageData(0, 0,
(c.width < v.w) ? c.width : v.w,
(c.height < v.h) ? c.height : v.h);
}
c.width = v.w;
c.height = v.h;
if (saveImg) {
c_ctx.putImageData(saveImg, 0, 0);
}
}
vx2 = v.x + v.w - 1;
vy2 = v.y + v.h - 1;
// Position change
if ((deltaX < 0) && ((v.x + deltaX) < 0)) {
deltaX = - v.x;
}
if ((vx2 + deltaX) >= fb_width) {
deltaX -= ((vx2 + deltaX) - fb_width + 1);
}
if ((v.y + deltaY) < 0) {
deltaY = - v.y;
}
if ((vy2 + deltaY) >= fb_height) {
deltaY -= ((vy2 + deltaY) - fb_height + 1);
}
if ((deltaX === 0) && (deltaY === 0)) {
//message("skipping");
return;
}
message("deltaX: " + deltaX + ", deltaY: " + deltaY);
v.x += deltaX;
vx2 += deltaX;
v.y += deltaY;
vy2 += deltaY;
// Update the clean rectangle
if (v.x > cr.x1) {
cr.x1 = v.x;
}
if (vx2 < cr.x2) {
cr.x2 = vx2;
}
if (v.y > cr.y1) {
cr.y1 = v.y;
}
if (vy2 < cr.y2) {
cr.y2 = vy2;
}
if (deltaX < 0) {
// Shift viewport left, redraw left section
x1 = 0;
w = - deltaX;
} else {
// Shift viewport right, redraw right section
x1 = v.w - deltaX;
w = deltaX;
}
if (deltaY < 0) {
// Shift viewport up, redraw top section
y1 = 0;
h = - deltaY;
} else {
// Shift viewport down, redraw bottom section
y1 = v.h - deltaY;
h = deltaY;
}
// Copy the valid part of the viewport to the shifted location
saveStyle = c_ctx.fillStyle;
c_ctx.fillStyle = "rgb(255,255,255)";
if (deltaX !== 0) {
//that.copyImage(0, 0, -deltaX, 0, v.w, v.h);
//that.fillRect(x1, 0, w, v.h, [255,255,255]);
c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h);
c_ctx.fillRect(x1, 0, w, v.h);
}
if (deltaY !== 0) {
//that.copyImage(0, 0, 0, -deltaY, v.w, v.h);
//that.fillRect(0, y1, v.w, h, [255,255,255]);
c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h);
c_ctx.fillRect(0, y1, v.w, h);
}
c_ctx.fillStyle = saveStyle;
}
that.getCleanDirtyReset = function() {
var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [],
vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1;
// Copy the cleanRect
cleanBox = {'x': c.x1, 'y': c.y1,
'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1};
if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
// Whole viewport is dirty
dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h});
} else {
// Redraw dirty regions
if (v.x < c.x1) {
// left side dirty region
dirtyBoxes.push({'x': v.x, 'y': v.y,
'w': c.x1 - v.x + 1, 'h': v.h});
}
if (vx2 > c.x2) {
// right side dirty region
dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y,
'w': vx2 - c.x2, 'h': v.h});
}
if (v.y < c.y1) {
// top/middle dirty region
dirtyBoxes.push({'x': c.x1, 'y': v.y,
'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y});
}
if (vy2 > c.y2) {
// bottom/middle dirty region
dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1,
'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2});
}
}
// Reset the cleanRect to the whole viewport
cleanRect = {'x1': v.x, 'y1': v.y,
'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1};
return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes};
}
// Force canvas redraw (for webkit bug #46319 workaround)
flush = function() {
var old_val;
......@@ -266,27 +441,25 @@ setFillColor = function(color) {
//
that.resize = function(width, height) {
var c = conf.target;
c_prevStyle = "";
c.width = width;
c.height = height;
c_width = c.offsetWidth;
c_height = c.offsetHeight;
fb_width = width;
fb_height = height;
rescale(conf.scale);
that.viewportChange();
};
that.clear = function() {
if (conf.logo) {
that.resize(conf.logo.width, conf.logo.height);
that.viewportChange(0, 0, conf.logo.width, conf.logo.height);
that.blitStringImage(conf.logo.data, 0, 0);
} else {
that.resize(640, 20);
c_ctx.clearRect(0, 0, c_width, c_height);
that.viewportChange(0, 0, 640, 20);
c_ctx.clearRect(0, 0, viewport.w, viewport.h);
}
// No benefit over default ("source-over") in Chrome and firefox
......@@ -295,12 +468,13 @@ that.clear = function() {
that.fillRect = function(x, y, width, height, color) {
setFillColor(color);
c_ctx.fillRect(x, y, width, height);
c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height);
};
that.copyImage = function(old_x, old_y, new_x, new_y, width, height) {
c_ctx.drawImage(conf.target, old_x, old_y, width, height,
new_x, new_y, width, height);
that.copyImage = function(old_x, old_y, new_x, new_y, w, h) {
var x1 = old_x - viewport.x, y1 = old_y - viewport.y,
x2 = new_x - viewport.x, y2 = new_y - viewport.y;
c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h);
};
/*
......@@ -386,7 +560,7 @@ rgbxImageData = function(x, y, width, height, arr, offset) {
data[i + 2] = arr[j + 2];
data[i + 3] = 255; // Set Alpha
}
c_ctx.putImageData(img, x, y);
c_ctx.putImageData(img, x - viewport.x, y - viewport.y);
};
// really slow fallback if we don't have imageData
......@@ -414,7 +588,7 @@ cmapImageData = function(x, y, width, height, arr, offset) {
data[i + 2] = rgb[2];
data[i + 3] = 255; // Set Alpha
}
c_ctx.putImageData(img, x, y);
c_ctx.putImageData(img, x - viewport.x, y - viewport.y);
};
cmapImageFill = function(x, y, width, height, arr, offset) {
......@@ -441,7 +615,9 @@ that.blitImage = function(x, y, width, height, arr, offset) {
that.blitStringImage = function(str, x, y) {
var img = new Image();
img.onload = function () { c_ctx.drawImage(img, x, y); };
img.onload = function () {
c_ctx.drawImage(img, x - viewport.x, y - viewport.y);
};
img.src = str;
};
......
.fullscreen {
display: block;
position: absolute;
top: 0px;
left: 0px;
html,body {
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
z-index: 9999;
margin: 0;
padding: 0;
}
.flex-layout {
width: 100%;
height: 100%;
display: box;
display: -webkit-box;
display: -moz-box;
display: -ms-box;
box-orient: vertical;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;
box-align: stretch;
-webkit-box-align: stretch;
-moz-box-align: stretch;
-ms-box-align: stretch;
}
.flex-box {
box-flex: 1;
......@@ -27,3 +31,13 @@
-ms-box-flex: 1;
}
.container {
margin: 0px;
padding: 0px;
}
.canvas {
position: absolute;
border-style: dotted;
border-width: 1px;
}
......@@ -782,6 +782,7 @@ init_msg = function() {
display.set_true_color(conf.true_color);
display.resize(fb_width, fb_height);
display.viewportChange(0, 0, fb_width, fb_height);
keyboard.grab();
mouse.grab();
......@@ -1312,6 +1313,7 @@ encHandlers.DesktopSize = function set_desktopsize() {
fb_width = FBU.width;
fb_height = FBU.height;
display.resize(fb_width, fb_height);
display.viewportChange(0, 0, fb_width, fb_height);
timing.fbu_rt_start = (new Date()).getTime();
// Send a new non-incremental request
ws.send(fbUpdateRequest(0));
......
......@@ -9,17 +9,15 @@
<meta name=viewport content="width=device-width, initial-scale=1.0, user-scalable=no">
</head>
<body>
<div class="fullscreen flex-layout">
<div class="flex-layout">
<div>
Canvas:
<input id="move-selector" type="button" value="Move"
onclick="toggleMove();">
<br>
</div>
<div id="container" class="flex-box">
<canvas id="canvas"
style="border-style: dotted; border-width: 1px;">
<div class="container flex-box">
<canvas id="canvas" class="canvas">
Canvas not supported.
</canvas>
<br>
......@@ -29,9 +27,7 @@
Results:<br>
<textarea id="messages" style="font-size: 9;" cols=80 rows=8></textarea>
</div>
</div>
</body>
<!--
......@@ -45,13 +41,10 @@
<script src="../include/display.js"></script>
<script>
var msg_cnt = 0, iterations,
fb_width = 800,
fb_height = 768,
viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 },
cleanRect = {},
penDown = false, doMove = false,
inMove = false, lastPos = {},
canvas, ctx, keyboard, mouse;
padW = 0, padH = 0,
display, ctx, keyboard, mouse;
var newline = "\n";
if (Util.Engine.trident) {
......@@ -75,14 +68,9 @@
if (down && !inMove) {
inMove = true;
lastPos = {'x': x, 'y': y};
cleanRect = {
'x1': viewport.x,
'y1': viewport.y,
'x2': viewport.x + viewport.w - 1,
'y2': viewport.y + viewport.h - 1};
} else if (!down && inMove) {
inMove = false;
dirtyRedraw();
//dirtyRedraw();
}
return;
}
......@@ -101,140 +89,54 @@
var deltaX, deltaY, x1, y1;
if (inMove) {
viewportMove(x, y);
return;
}
if (penDown) {
ctx.lineTo(x, y);
ctx.stroke();
}
}
function viewportMove(x, y) {
var v = viewport, c = cleanRect,
vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1,
deltaX, deltaY, w, h;
//deltaX = x - lastPos.x; // drag viewport
deltaX = lastPos.x - x; // drag frame buffer
//deltaY = y - lastPos.y; // drag viewport
deltaY = lastPos.y - y; // drag frame buffer
lastPos = {'x': x, 'y': y};
if ((deltaX < 0) && ((v.x + deltaX) < 0)) {
deltaX = - v.x;
}
if ((vx2 + deltaX) >= fb_width) {
deltaX -= ((vx2 + deltaX) - fb_width + 1);
display.viewportChange(deltaX, deltaY);
return;
}
if ((v.y + deltaY) < 0) {
deltaY = - v.y;
if (penDown) {
ctx.lineTo(x, y);
ctx.stroke();
}
if ((vy2 + deltaY) >= fb_height) {
deltaY -= ((vy2 + deltaY) - fb_height + 1);
}
if ((deltaX === 0) && (deltaY === 0)) {
//message("skipping");
function dirtyRedraw() {
if (inMove) {
// Wait for user to stop moving viewport
return;
}
message("deltaX: " + deltaX + ", deltaY: " + deltaY);
v.x += deltaX;
vx2 += deltaX;
v.y += deltaY;
vy2 += deltaY;
// Update the clean rectangle
if (v.x > c.x1) {
c.x1 = v.x;
}
if (vx2 < c.x2) {
c.x2 = vx2;
}
if (v.y > c.y1) {
c.y1 = v.y;
}
if (vy2 < c.y2) {
c.y2 = vy2;
}
var d = display.getCleanDirtyReset();
if (deltaX < 0) {
// Shift viewport left, redraw left section
x1 = 0;
w = - deltaX;
} else {
// Shift viewport right, redraw right section
x1 = v.w - deltaX;
w = deltaX;
}
if (deltaY < 0) {
// Shift viewport up, redraw top section
y1 = 0;
h = - deltaY;
} else {
// Shift viewport down, redraw bottom section
y1 = v.h - deltaY;
h = deltaY;
}
if (deltaX !== 0) {
canvas.copyImage(0, 0, -deltaX, 0, v.w, v.h);
canvas.fillRect(x1, 0, w, v.h, [255,255,255]);
}
if (deltaY !== 0) {
canvas.copyImage(0, 0, 0, -deltaY, v.w, v.h);
canvas.fillRect(0, y1, v.w, h, [255,255,255]);
for (i = 0; i < d.dirtyBoxes.length; i++) {
//showBox(d.dirtyBoxes[i], "dirty[" + i + "]: ");
drawArea(d.dirtyBoxes[i]);
}
}
function dirtyRedraw() {
var v = viewport, c = cleanRect,
vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1;
if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) {
// Nothing clean, redraw everything
drawArea(0, 0, v.w, v.h);
} else {
// Redraw dirty regions
if (v.x < c.x1) {
// redraw left side dirty region
drawArea(0, 0, c.x1 - v.x, v.h);
}
if (vx2 > c.x2) {
// redraw right side dirty region
drawArea(v.w - (vx2 - c.x2), 0, vx2 - c.x2, v.h);
}
if (v.y < c.y1) {
// redraw top/middle dirty region
drawArea(c.x1 - v.x, 0, c.x2 - c.x1 + 1, c.y1 - v.y);
}
if (vy2 > c.y2) {
// redraw bottom/middle dirty region
drawArea(c.x1 - v.x, c.y2 - v.y, c.x2 - c.x1 + 1, v.h - (c.y2 - v.y));
}
}
}
function drawArea(b) {
var data = [], pixel, x, y;
function drawArea(x, y, w, h) {
message("draw "+x+","+y+" ("+w+","+h+")");
var imgData = ctx.createImageData(w, h),
data = imgData.data, pixel, realX, realY;
//message("draw "+b.x+","+b.y+" ("+b.w+","+b.h+")");
for (var i = 0; i < w; i++) {
realX = viewport.x + x + i;
for (var j = 0; j < h; j++) {
realY = viewport.y + y + j;
pixel = (j * w * 4 + i * 4);
data[pixel + 0] = ((realX * realY) / 13) % 256;
data[pixel + 1] = ((realX * realY) + 392) % 256;
data[pixel + 2] = ((realX + realY) + 256) % 256;
for (var i = 0; i < b.w; i++) {
x = b.x + i;
for (var j = 0; j < b.h; j++) {
y = b.y + j;
pixel = (j * b.w * 4 + i * 4);
data[pixel + 0] = ((x * y) / 13) % 256;
data[pixel + 1] = ((x * y) + 392) % 256;
data[pixel + 2] = ((x + y) + 256) % 256;
data[pixel + 3] = 255;
}
}
//message("i: " + i + ", j: " + j + ", pixel: " + pixel);
ctx.putImageData(imgData, x, y);
display.blitImage(b.x, b.y, b.w, b.h, data, 0);
}
function toggleMove() {
......@@ -249,36 +151,37 @@
}
}
window.onresize = function() {
var v = viewport,
cw = $D('container').offsetWidth,
ch = $D('container').offsetHeight;
message("container: " + cw + "," + ch);
if (cw > fb_width) {
cw = fb_width;
}
if (ch > fb_height) {
ch = fb_height;
function detectPad() {
var c = $D('canvas'), p = c.parentNode;
c.width = 10;
c.height = 10;
padW = c.offsetWidth - 10;
padH = c.offsetHeight - 10;
message("padW: " + padW + ", padH: " + padH);
}
if ((cw !== v.w) || (ch !== v.h)) {
v.w = cw;
v.h = ch;
message("new viewport: " + v.w + "," + v.h);
canvas.resize(v.w, v.h);
drawArea(0, 0, v.w, v.h);
function doResize() {
var p = $D('canvas').parentNode;
message("doResize1: [" + (p.offsetWidth - padW) +
"," + (p.offsetHeight - padH) + "]");
display.viewportChange(0, 0,
p.offsetWidth - padW, p.offsetHeight - padH);
}
};
window.onload = function() {
canvas = new Display({'target' : $D('canvas')});
ctx = canvas.get_context();
detectPad();
display = new Display({'target': $D('canvas')});
display.resize(1600, 1024);
//display.resize(800, 600);
ctx = display.get_context();
mouse = new Mouse({'target': $D('canvas'),
'onMouseButton': mouseButton,
'onMouseMove': mouseMove});
window.onresize();
Util.addEvent(window, 'resize', doResize);
//doResize();
setTimeout(doResize, 1);
setInterval(dirtyRedraw, 50);
mouse.grab();
message("Display initialized");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment