Fix elements (esp top right) to the visual viewport in chrome mobile, with very wide content?
Asked Answered
O

1

9

Is there a way to fix elements (esp top right) to the visual viewport in chrome mobile?

We want to show very large images that the website visitor will zoom in and out of. The viewer needs to be able to see the whole picture and then target small areas to inspect. We will load images much wider than the screen available. Say from 1500 to 6000px wide.

As well as the above we want fixed elements for navigation in the top left and top right.

On Webkit/Safari we get the behaviour we want.

enter image description here

Chrome/Android mobile (initial load):

enter image description here

Chrome/Android mobile (zooming all the way out):

enter image description here

Chrome on mobile seems to want to fix the top right div off screen to the layout viewport. Ie the green fixed top right div is off the screen till you zoom all the way out to the whole pic.

These specs seem to suggest this is by design in Chrome.

Setting minimum-scale=1 would superficially seem to resolve this, but it disables zooming in/out which makes this NOT the solution.

Is there a solution to get Chrome to behave the same as Webkit/Safari? Solutions involving Javascript probably acceptable.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Testing fixed width headers on mobile</title>
</head>

<style>
    .tl {
        position:fixed;
        top:0;
        left:0;
        background-color:red;
    }
    .tr {
        position:fixed;
        top:0;
        right:0;
        background-color:green;
    }
    body img {
        margin-top:110px;
    }
</style>

<body>
    <div class="tl">Fixed top left</div>
    <div class="tr">Fixed top right</div>

    <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/7e/In_the_Conservatory.jpg/1280px-In_the_Conservatory.jpg" />
</body>
</html>
Overword answered 24/7, 2019 at 16:35 Comment(1)
Can you please publish the <head> code of the html pagePenates
T
4

The cleanest solution would be to place the image inside an element with width: 100%; overflow: auto;

For height of the parent element you could use calc(100% - X) where X is the height of the navbar (this is assuming you want the image to take up the entire screen unless zoomed out).

See below for full example.

EDIT: I have updated the answer below with help from this codepen [https://codepen.io/techslides/pen/zowLd] for the zooming and panning inside a canvas element. You can also add an event listener to check for window resize and redraw the canvas as needed.

var canvas = document.getElementsByTagName('canvas')[0];
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - 20;

var canvasimage = new Image;

window.onload = function() {

  var ctx = canvas.getContext('2d');
  trackTransforms(ctx);

  function redraw() {

    // Clear the entire canvas
    var p1 = ctx.transformedPoint(0, 0);
    var p2 = ctx.transformedPoint(canvas.width, canvas.height);
    ctx.clearRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);

    ctx.save();
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.restore();

    ctx.drawImage(canvasimage, 0, 0);

  }
  redraw();

  var lastX = canvas.width / 2,
    lastY = canvas.height / 2;

  var dragStart, dragged;

  canvas.addEventListener('mousedown', function(evt) {
    document.body.style.mozUserSelect = document.body.style.webkitUserSelect = document.body.style.userSelect = 'none';
    lastX = evt.offsetX || (evt.pageX - canvas.offsetLeft);
    lastY = evt.offsetY || (evt.pageY - canvas.offsetTop);
    dragStart = ctx.transformedPoint(lastX, lastY);
    dragged = false;
  }, false);

  canvas.addEventListener('mousemove', function(evt) {
    lastX = evt.offsetX || (evt.pageX - canvas.offsetLeft);
    lastY = evt.offsetY || (evt.pageY - canvas.offsetTop);
    dragged = true;
    if (dragStart) {
      var pt = ctx.transformedPoint(lastX, lastY);
      ctx.translate(pt.x - dragStart.x, pt.y - dragStart.y);
      redraw();
    }
  }, false);

  canvas.addEventListener('mouseup', function(evt) {
    dragStart = null;
    if (!dragged) zoom(evt.shiftKey ? -1 : 1);
  }, false);

  var scaleFactor = 1.1;

  var zoom = function(clicks) {
    var pt = ctx.transformedPoint(lastX, lastY);
    ctx.translate(pt.x, pt.y);
    var factor = Math.pow(scaleFactor, clicks);
    ctx.scale(factor, factor);
    ctx.translate(-pt.x, -pt.y);
    redraw();
  }

  var handleScroll = function(evt) {
    var delta = evt.wheelDelta ? evt.wheelDelta / 40 : evt.detail ? -evt.detail : 0;
    if (delta) zoom(delta);
    return evt.preventDefault() && false;
  };

  canvas.addEventListener('DOMMouseScroll', handleScroll, false);
  canvas.addEventListener('mousewheel', handleScroll, false);
};

canvasimage.src = 'https://i.sstatic.net/ITUHK.png';

// Adds ctx.getTransform() - returns an SVGMatrix
// Adds ctx.transformedPoint(x,y) - returns an SVGPoint
function trackTransforms(ctx) {
  var svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
  var xform = svg.createSVGMatrix();
  ctx.getTransform = function() {
    return xform;
  };

  var savedTransforms = [];
  var save = ctx.save;
  ctx.save = function() {
    savedTransforms.push(xform.translate(0, 0));
    return save.call(ctx);
  };

  var restore = ctx.restore;
  ctx.restore = function() {
    xform = savedTransforms.pop();
    return restore.call(ctx);
  };

  var scale = ctx.scale;
  ctx.scale = function(sx, sy) {
    xform = xform.scaleNonUniform(sx, sy);
    return scale.call(ctx, sx, sy);
  };

  var rotate = ctx.rotate;
  ctx.rotate = function(radians) {
    xform = xform.rotate(radians * 180 / Math.PI);
    return rotate.call(ctx, radians);
  };

  var translate = ctx.translate;
  ctx.translate = function(dx, dy) {
    xform = xform.translate(dx, dy);
    return translate.call(ctx, dx, dy);
  };

  var transform = ctx.transform;
  ctx.transform = function(a, b, c, d, e, f) {
    var m2 = svg.createSVGMatrix();
    m2.a = a;
    m2.b = b;
    m2.c = c;
    m2.d = d;
    m2.e = e;
    m2.f = f;
    xform = xform.multiply(m2);
    return transform.call(ctx, a, b, c, d, e, f);
  };

  var setTransform = ctx.setTransform;
  ctx.setTransform = function(a, b, c, d, e, f) {
    xform.a = a;
    xform.b = b;
    xform.c = c;
    xform.d = d;
    xform.e = e;
    xform.f = f;
    return setTransform.call(ctx, a, b, c, d, e, f);
  };

  var pt = svg.createSVGPoint();
  ctx.transformedPoint = function(x, y) {
    pt.x = x;
    pt.y = y;
    return pt.matrixTransform(xform.inverse());
  }
}
body {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

.wide {
  width: 200vw;
}

.nav {
  width: 100%;
  height: 20px;
}

.clearfix::after {
  content: "";
  clear: both;
  display: table;
}

.left-nav {
  background-color: red;
}

.right-nav {
  float: right;
  background-color: green;
}
<div class="nav clearfix">
  <span class="left-nav">Top Left</span>
  <span class="right-nav">Top Right</span>
</div>
<canvas>
        </canvas>
Teamwork answered 1/8, 2019 at 23:51 Comment(3)
Thanks @Nathan, that does get us close. And +1 for the attempt. But, a bit like setting minimum-scale=1 this solution prevents zooming out to see the whole picture. Which our audience needs to be able to do. They want to zoom right in, and zoom right out. And keep the top right and top left nav bars in position and always visible.Overword
Please accept this answer if the edit worked for youTeamwork
Please note that flexbox has better (usage relative) support than calc now according to caniuse.com. Wouldn't this be preferable since it would eliminate the need to refer to the height of the top bar in another element?Phillips

© 2022 - 2024 — McMap. All rights reserved.