<canvas> at native resolution despite zooming, Retina - css

So I have a <canvas> element in which I am drawing things. Vectory things, not pixel-pushing. I would like this to display in the highest quality with the least resources under the largest range of viewing conditions. Specifically, I want to ensure that there is a 1:1 mapping between canvas pixels and device pixels, to eliminate blurry, time-consuming pixel scaling operations.
There are a
few things
I've seen about "Retina" displays (conclusion: devicePixelRatio is your friend), but they all assume viewing at 100% zoom. Equally I've seen things about detecting zoom level, but it seems messy and hacky and imprecise
(not to mention prone to breakage). In particular, imprecision is a problem as the entire point is to get a ratio of precisely 1:1. (Plus I don't actually care about the zoom level itself, only about addressing native pixels.)
So my questions, from pragmatic to philosophical:
What is the preferred way of getting a 1:1 canvas in modern (== released in the past 6 months) browsers? Do I really have to detect the device pixel ratio and the zoom (including checking for zoom changes) and calculate things from there?
If I don't care about resource consumption, is a 2:1 canvas (native res on "Retina" displays, upto 200% zoom elsewhere) worth considering? I'm assuming downscaling is less bad than upscaling.
What are browser vendors' take on this? Are there better solutions being worked on? Or is zoom a detail they think web developers should have no need to worry about? [Answered partially here: many consider it important. There seems some indecision on whether devicePixelRatio should change with zoom level or whether a new property that allows user-specified zoom detection as well as device-specific DPI detection is the way forward.]
How many people regularly browse the web at !100% zoom? How many of them are using high DPI displays? Are there stats? [Answered here: about 10% of gmail users.]

What i found (for Chrome and Internet Explorer) is the very tricky, subtle, differences between
canvas.width, and
canvas.style.width
They are not interchangeable. If you want to manipulate them you must do so very carefully to achieve what you want. Some values are measured in physical pixels. Others are measured in logical pixels (the pixels if the screen was still zoomed to 96 dpi).
In my case i want the Canvas to take up the entire window.
First get the size of the window in logical pixels (which i call "CSS pixels"):
//get size in CSS pixels (i.e. 96 dpi)
var styleWidth = window.innerWidth; //e.g. 420. Not document.body.clientWidth
var styleHeight = window.innerHeight; //e.g. 224. Not document.body.clientHeight
These values are in "logical pixels". When my browser is zoomed in, the amount of "logical pixels" decreases:
Zoom window.innerWidth x windows.innerHeight
==== ======================================
100% 1680 x 859
200% 840 x 448
400% 420 x 224
75% 2240 x 1193
What you then have to do is figure out the "real" size of the window; applying a zoom correction factor. For the moment we'll abstract the function that can figure out the current zoom level (using the TypeScript syntax):
function GetZoomLevel(): number
{
}
With the GetZoomLevel() function, we can calculate the real size of the window, in "physical pixels". When we need to set the width and height of the canvas to the size of the window in physical pixels:
//set canvas resolution to that of real pixels
var newZoomLevel = GetZoomLevel();
myCanvas.width = styleWidth * newZoomLevel; //e.g. 1680. Not myCanvas.clientWidth
myCanvas.height = styleHeight * newZoomLevel; //e.g. 859. Not myCanvas.clientHeight
The critical trick is that internally the canvas will render to a surface that is width by height pixels in size. After that rendering is complete, CSS can come along and shrink or stretch your canvas:
blurring it if CSS makes it larger
throwing away pixel data if CSS makes it smaller.
The final piece is to resize the canvas so that it takes up the entire client area window, using CSS length String:
myCanvas.style.width = styleWidth + "px"; //e.g. "420px"
myCanvas.style.height = styleHeight + "px"; //e.g. "224px"
So the canvas will be positioned and sized correctly in your browser window, yet internally is using full "real" pixel resolution.
GetZoomLevel()
Yes, you need the missing piece, GetZoomLevel. Unfortunately only IE supplies the information. Again, using TypeScript notation:
function GetZoomLevel(): number {
/*
Windows 7, running at 131 dpi (136% zoom = 131 / 96)
Internet Explorer's "default" zoom (pressing Ctrl+0) is 136%; matching the system DPI
screen.deviceYDPI: 130
screen.systemYDPI: 131
screen.logicalYDPI: 96
Set IE Zoom to 150%
screen.deviceYDPI: 144
screen.systemYDPI: 131
screen.logicalYDPI: 96
So a user running their system at 131 dpi, with IE at "normal" zoom,
and a user running their system at 96 dpi, with IE at 136% zoom,
both need to see the same thing; everything zoomed in from our default, assumed, 96dpi.
http://msdn.microsoft.com/en-us/library/cc849094(v=vs.85).aspx
Also note that the onresize event is triggered upon change of zoom factor, therefore you should make sure
that any code that takes into account DPI is executed when the onresize event is triggered, as well.
http://htmldoodads.appspot.com/dimensions.html
*/
var zoomLevel: number;
//If the browser supports the corrent API, then use that
if (screen && screen.deviceXDPI && screen.logicalXDPI)
{
//IE6 and above
zoomLevel = (screen.deviceYDPI / screen.logicalYDPI);
else
{
//Chrome (see http://htmldoodads.appspot.com/dimensions.html)
zoomLevel = window.outerWidth / window.innerWidth; //e.g. 1680 / 420
}
return zoomLevel;
}
Unfortunately no browsers besides IE support telling you:
device dpi
logical dpi
My full TS code is:
function OnWindowResize() {
if (drawingTool == null)
return;
//Zoom changes trigger window resize event
//get size in CSS pixels (i.e. 96 dpi)
var styleWidth = window.innerWidth; //myCanvas.clientWidth;
var styleHeight = window.innerHeight; //myCanvas.clientHeight;
var newZoomLevel = GetZoomLevel();
// myCanvas.width = document.body.clientWidth;
// myCanvas.height = document.body.clientHeight;
//set canvas resolution to that of real pixels
myCanvas.width = styleWidth * newZoomLevel;
myCanvas.height = styleHeight * newZoomLevel;
//myCanvas.clientWidth = styleWidth;
//myCanvas.clientHeight = styleHeight;
myCanvas.style.width = styleWidth + "px"; //styleWidth.toString() + "px";
myCanvas.style.height = styleHeight + "px"; //styleHeight.toString() + "px";
drawingTool.Width = myCanvas.width;
drawingTool.Height = myCanvas.height;
// Only take action if zoom factor has changed (won't be triggered by actual resize of window)
if (newZoomLevel != lastZoomLevel) {
lastZoomLevel = newZoomLevel;
drawingTool.CurrentScaleFactor = newZoomLevel;
}
}
It's also important to tell your drawing code the user's current zoom level, in case you try to hard-code any lengths. E.g. attempting to draw
a 32px box at (50,50)
is wrong. You need to draw a
128 px box at (200,200)
when the zoom factor is 4.
Note: Any code is released into the public domain. No attribution required.

You basically cannot detect zoom level on modern browser versions of Firefox and Chrome:
On Firefox 18+ Mozilla changes the devicePixelRatio value on manual zoom (cmd/ctrl +/-), making it impossible to know whether the browser is in zoom mode or is it a retina device, ignoring what the word DEVICE represents.
On Chrome 27 (Meaning WebKit and Blink) webkitTextSizeAdjust was deprecated on desktops versions of the browser. This was the only bullet proof way to detect zoom in desktop chrome that I am aware of.
There are couple of other ways, but they don't cover all the bases - one uses SVG but is not working in iFrames, the other uses window.inner/outerWidth and is not working when there is a sidebar or the DevTools are open on the side.
Source

Related

Prevent chrome from changing text size

There is an option in chrome that lets you change default font size (Small, Medium, Large, Very Large) and appearently the
-webkit-text-size-adjust:none;
line isn't supported anymore. Is there any other way I can prevent chrome from changing font size?
Chrome as any other browser has default values for how elements should look.
The best way to get what you want is to implement a css script that resets all the different elements to values you know, and from their set the elements to a new desired value.
h1,h2,h3,h4,p,a {
font-size: 50px;
}
You can also implement media-queries to change behavior as viewport change.
What you want to do, probably, is to operate backwards: instead of preventing the page to change the fonts size, you might want to re-calculate it based on the zoomed level in the user's window.
Have a look at this question, you can try to use the described methods to detect the zoom level in the browser and apply a "counter-zoom" to reset it to your default font-size: How to detect page zoom level in all modern browsers?
E.g.:
If the users zoom at 120% you want to set your font-size to 83.3333%
The formula is simply
function yourFontSize(zoomLevel){
return 100/zoomLevel*100
}
More examples:
If the users zoom at 110% yourFontSize(110) // returns 90.9090909090909
If the users zoom at 120% yourFontSize(120) // returns 83.33333333333334
If the users zoom at 150% yourFontSize(150) // returns 66.66666666666666
And so forth
Try this with jquery, I force the font-size to a specific size. Open your page, right click for Inspect. Minimize-maximize and when the text shrinks, at the top right corner you will see the screen size, force it like this:
$(window).resize(function() {
if ($(window).width() > 400 && $(window).width() < 1000)
$('#myParagraph').css('font-size', '40px');
});

Why is responsive images so off (my) reality?

There is one simple task I want to achieve.
I have an image in a variable width container.
The container can have a width of 300, 400, 700, or 900 pixels. This is done by the means of media-queries
The image should take up all the width of that container. So it will be also 300, 400, 700, or 900 pixels wide.
The image should have different sources for all that width values. So I can serve smaller images on mobile phones.
I thought that this could be done with the srcset attribute of the img element, maybe under help of the sizes attribute. width something like this
<img src="http://dummyimage.com/300x200/abc/000"
alt="dummy"
srcset="
http://dummyimage.com/900x200/abc/000 900w,
http://dummyimage.com/700x200/abc/000 700w,
http://dummyimage.com/400x200/abc/000 400w,
http://dummyimage.com/300x200/abc/000 300w
"
/>
But it's not working in that way, because the browser chooses the image in proportion to the width of the display port and not to that of the image itself.
Example with use of picturefill polyfill from http://scottjehl.github.io/picturefill/: http://codepen.io/HerrSerker/pen/itBJy . This does not work, because it will take the one image that is the next size.
I could of course take that into account and change my srcset to this
srcset="
http://dummyimage.com/900x200/abc/000 999999w,
http://dummyimage.com/700x200/abc/000 900w,
http://dummyimage.com/400x200/abc/000 700w,
http://dummyimage.com/300x200/abc/000 400w
"
This will work on the desktop, but fails on retina displays, because the device pixel ratio is taken into account here, but in a different way than with the media queries. And it is not useful, because the image should know about the width of the viewport and of the same width and that at compile time? No way. Image I use the image in a grid system. The image has different widthes if I'm in a 3 column grid on desktop devices and a 1 column grid on smart phones. That should not be in the responsibility of the image to calulate the ratio of width and viewport-width.
I did not have any luck with the sizes attribute as well (no example here). The reason is tha same as above. In the sizes attibute I say which amount of the viewport width should my image be wide according to media queries. This is so off. How should the image know?
So I came around with this solution. I setup a data-srcset attribute with the same syntax as the srcset attribute itself, but with a custom JavaScript programming. Example here: http://codepen.io/HerrSerker/pen/tCqJI
jQuery(function($){
var reg = /[\s\r\n]*(.*?)[\s\r\n]+([^\s\r\n]+w)[\s\r\n]*(,|$)/g;
var regw = /(.*)w/;
var sets, $set, set, myMatch, i, w, that, last;
var checkData = function() {
$('img[data-srcset]').each(function() {
that = $(this);
$set = that.data('srcset');
sets = [];
while(myMatch = reg.exec($set)) {
set = {};
set.src = myMatch[1];
set.w = (myMatch[2].match(regw))[1];
sets[set.w] = set;
}
w = that.width();
last = 0;
for (i in sets) {
last = i;
if (w <= i) {
that.attr('src', sets[i].src);
return;
}
}
that.attr('src', sets[last].src);
});
};
checkData();
$(window).on('resize', checkData);
});
This works, but it feels wrong. But maybe not, as the specifications says for responsive images to behave just in the way that it does. But I feel that it's the wrong way. 90 % of use cases for responsive images won't work with the spec.
So am I wrong? Didn't I use the srcset in the defined way? Did I understand the spec incorrectly? And do the W3C and Responsive Images Community Group think in such a way apart from reality?
Are the smaller images scaled down versions of the bigger image? Or are they cropped (art direction)? If the latter, you should use picture and source media.
The reason the browser only uses the viewport for deciding which image to download is that it's the only thing that is available when the browser wants to download an image. The CSS (probably) isn't downloaded yet. So if you use srcset+sizes, you have to repeat the breakpoints and image widths in sizes.
This question seems like a duplicate of Responsive full width image banner with fixed height using srcset
Like zcorpan said, what you are trying to do falls under the "art-direction" use-case (since the different images have different proportions), so you should use the <picture> element, rather than srcset. See the other question's answers for a syntax example.

Trying to zoom image based on mouse origin, yet my math is slightly off

I'm working on a full screen image viewer, I'll temporarily open a dev URL here:
http://www.jungledragon.org/apps/jd3/image/704/great_grey_owl.html/zoom
This viewer is responsive and scales to your browser width/height. One of its key features is being able to zoom in and out of the image using your mouse wheel. Rather than a center-based zoom, the idea is to zoom based on origin, meaning the coordinates of your mouse, allowing you to zoom into specific areas of the image.
How to reproduce the issue
If you open the above URL and have a quick play with your mouse wheel, it may appear to be working correctly. However, the math I am using is slightly off. Here is how you can reproduce the issue:
Open the above URL
Hover your mouse over the left eye of the Owl
Zoom one step using your mouse wheel, it should zoom exactly into the eye
Position your mouse on the owl's beak
Zoom one more step using your mouse wheel
You should now notice that the second zoom step did not go into the Owl's beak exactly, it seems to be slightly off, both horizontally and vertically. I'm thinking this is a result of bad math.
How it works
Here is the javascript that handles it all:
http://www.jungledragon.org/apps/jd3/js/jd3-slideshow.js
I am capturing the mousewheel event. Based upon its direction, I am increasing or decreasing the zoom level. The actual zooming is nothing more than applying a CSS class that scales the image using a CSS3 transform:
&.grow1 { #include jd-scale(1); }
&.grow2 { #include jd-scale(1.5); }
&.grow3 { #include jd-scale(2.0); }
&.grow4 { #include jd-scale(2.5); }
&.grow5 { #include jd-scale(3.0); }
Note: the above is a call to a SASS mixin that translates into the right vendor prefixes for transform:scale.
The above accomplishes the basic zooming without issues. To make origin-based zooming possible, however, a few more steps are needed. Upon doing the actual zooming, I first set the origin of the zoom in javascript, using transform-origin. Here is my helper function for setting it:
function zoomOrigin(selector, originStr) {
selector.css({'-webkit-transform-origin': originStr});
selector.css({'-moz-transform-origin': originStr});
selector.css({'-ms-transform-origin': originStr});
selector.css({'-o-transform-origin': originStr});
selector.css({'transform-origin': originStr});
}
The heart of this question is about calculating the correct origin. There are two things worthy to mention in calculating this value:
The absolute coordinates (meaning the X and Y) are relative to the image, not relative to the page
The calculation of the origin should take into account that the image has grown/shrunk based on the current zoom state
The origin calculation happens in realtime, based on the mousemove event. Here is the method that does so, with irrelevant parts removed:
$("#image-container img").mousemove(function(e) {
// user has moved their mouse. in case of zooming or panning, this means that the
// origin (center point) of those interactions need to be recalculated
// calculate the mouse offset within the zoomable object (which is different than the page-level offset)
// this relies on the parent of the element having position:relative set
var parentOffset = $(this).offset();
zoomOriginX = e.pageX - parentOffset.left;
zoomOriginY = e.pageY - parentOffset.top;
// recalculate the width and height of the image given the current zoom level
width = $(this).outerWidth() + (1 + ((zoomLevelCurrent - 1)*0.5) * $(this).outerWidth());
height = $(this).outerHeight() + (1 + ((zoomLevelCurrent - 1)*0.5) * $(this).outerHeight());
// calculate origin percentages based on zoomed width and height
// the zoom methods rely on these variables to be set
zoomOriginPercX = (zoomOriginX / width * 100);
zoomOriginPercY = (zoomOriginY / height * 100);
});
The main purpose of this method is to correctly set the global variables zoomOriginPercX and zoomOriginPercY, which are used to set the origin (percentage) prior to zooming.
From a math perspective, my idea was to simply calculate the zoomed width of the image, and to use the offset X and Y to come to a reliable origin percentage. As the problem statement shows, I am quite close to a correct calculation, yet something is off.
Although the zooming currently works well, I want it to be perfect. It would make for quite a powerful image viewer that is really easy to implement, also for others.
Desired Effect
To start answering your question I think it's worth first clarifying the desired effect. Essentially you're looking for the same effect you'd get if you pinched to zoom on an iPhone - the 'origin' of the pinch stays exactly the same, and everything around it stretches. You can imagine pinning some stretchy fabric at the origin, and pulling the corners.
Problem
This is working fine for you if you don't move the mouse between zooms, but if you do, the origin appears to move. The cause of the problem is exactly that - you are changing the origin of the transform every time you move the mouse. Of course you do need to do this, but you are calculating the origin based on the original (100% zoomed) position of the image. The actual origin needs to be somewhere between the origin of the first zoom and the new mouse position.
In other words, CSS is just doing one transform. If you set the origin to x,y then zoom to zoom level 2, this will give the same result as if you set the origin to x2,y2, zoom to level 1, then move to x,y, and go to level 2.
Solutions
I presume you could solve the issue in several ways:
Calculate a scaling factor for the 'new' origin on each zoom
this is likely a function of zoom level, mouse position and previous origin
Calculate and apply a translation each time the origin is moved
again will depend on the current origin, zoom level and mouse position
Find another way to 'stack' transforms on top of one another.
One way to do this may be to dynamically generate a new containing div each time you and apply a scale transform to that similar to the accepted solution in this question.
Unfortunately I don't have the time to go further than this, but hopefully it points you in the right direction?

How to attach a camera to fill a mobile's screen without zoom-in?

I have a flex video capturing mobile application that attaches its camera to a UIComponent as follows:
var camera:Camera = Camera.getCamera();
camera.setQuality(0,100);
video = new Video(width, height);
video.attachCamera(camera);
var uiVideo:UIComponent = new UIComponent();
uiVideo.width = video.width;
uiVideo.height = video.height;
uiVideo.addChild(video);
videoGroup.addElement(uiVideo);
*width and height are the mobile's screen size.
The problem with the above code is the video/camera the user sees when recording is zoomed-in when compared with the recorded video. That happens probably because, a camera with dimensions 20x30 is streched to a screen size of 60x90, thus resulting in a zoom of 3x.
How can I avoid that?
Is there a way to strech a camera to fill the mobile screen without zooming-in (distortion is okay, when the screen's ratio is different from the camera's ratio)?
Have you tried Camera.setMode()? You can pass in width, height, frames per second, and a 4th parameter. If the camera does not support that resolution you specify, it will try to use a supported resolution that is similar.

How do you set the proper width of a dynamic UITextField before assigning text?

In Flex 3.2, I'm creating a UITextField, then measuring text that I'm about to assign to that field's text property. I then use those metrics to set the size of the field. However, the calculated width is not wide enough to accommodate the text. Is there a different order to achieve proper measurement or am I seeing a problem with the measureText() function? How can I get accurate results?
// UITextField's default size appears to be 100x100
// Measure the text then set width and height
var tf:UITextFormat = uiTextField.getUITextFormat();
var tlm:TextLineMetrics = tf.measureText(this.labelText);
// Text within the field is clipped unless 'padding' is added to the size
// Flex Documentation specifies there is a 2 px gutter on each side, so
// the expected padding would be 4 px. However, clipping occurs, for
// "Hello, World" up to 9 px.
uiTextField.width = tlm.width + 9;
uiTextField.height = tlm.height + 4;
uiTextField.border = true;
uiTextField.name = "uiTextField";
uiTextField.text = this.labelText;
I've had all sorts of trouble with measuring the width and heights of textFields before. It looks like you just want to autosize the textField. Have you tried:
uiTextField.autoSize = TextFieldAutoSize.LEFT;
???
Unfortunately, Flex often gets very confused when it tries to dynamically get a textWidth. Even worse, it is difficult to reliably catch the error and have Flex update itself correctly. The best options I've found:
Hack it manually -- mostly reliable and has the benefit of happening before everything has finished rendering:
mx.controls.Text hack AS3 describes a way to do this.
Use callLater or an FlexEvent.CREATION_COMPLETE event listener -- this is less reliable, but it is definitely not a hack
Use setTimeout with a delay of less than 1/10 a second ( I like 25-50 milliseconds ) -- I have found this the most reliable, but it may cause a small "blip" on the screen (generally not terribly noticeable for short code).

Resources