CSS Zoom property not working with BoundingClientRectangle - javascript

I'm having trouble getting the coordinates of an element after it has been transformed using the "zoom" property. I need to know the coordinates of all 4 corners. I would typically accomplish this with the getBoundingClientRect property however, that does not seem to work correctly when the element is zoomed. I've attached a short jsfiddle link to show what doesn't seem to work. I'm using Chrome but the behavior is present on Firefox as well.
http://jsfiddle.net/GCam11489/0hu7kvqt/
HTML:
<div id="box" style="zoom:100%">Hello</div><div></div><div></div>
JS:
var div = document.getElementsByTagName("div");
div[1].innerHTML = "PreZoom Width: " + div[0].getBoundingClientRect().width;
div[0].style.zoom = "150%";
div[2].innerHTML = "PostZoom Width: " + div[0].getBoundingClientRect().width;
When I run that code, "100" is displayed for both the before and after zoom widths.
I have found a lot of information but everything I find seems to say that it was a known bug on Chrome and has since been fixed. Does anyone know how this can be corrected or what I might be doing wrong?

This comment on the bug report goes into some detail reg. the issue, but its status appears to be 'WontFix', so you're probably out of luck there.
Some alternatives:
If you were to use transform: scale(1.5) instead of zoom, you'd get the correct value in getBoundingClientRect(), but it would mess with the page layout.
You could use window.getComputedStyle(div[0]).zoom to get the zoom value of the element (in decimals) and multiply it with the width from getBoundingClientRect()

This is quite an old post but I've encountered the same problem, I have an angular material project on a small panel which has a pixel ratio of under 1 which makes everything very small. To fix that I've added a zoom on the body to counter this.
The angular material (7) slider uses getBoundingClientRect() to determine it's position, which made the slider go way further then I'd wish.
I've used a solution like eruditespirit mentioned above.
if (Element.prototype.getBoundingClientRect) {
const DefaultGetBoundingClientRect = Element.prototype.getBoundingClientRect;
Element.prototype.getBoundingClientRect = function () {
let zoom = +window.getComputedStyle(document.body).zoom;
let data = DefaultGetBoundingClientRect.apply(this, arguments);
if (zoom !== 1) {
data.x = data.x * zoom;
data.y = data.y * zoom;
data.top = data.top * zoom;
data.left = data.left * zoom;
data.right = data.right * zoom;
data.bottom = data.bottom * zoom;
data.width = data.width * zoom;
data.height = data.height * zoom;
}
return data;
};
}

Had the issue with a tooltip component.
Created this function to fetch all zooms applied to an element. Then I used that zoom to apply it to the global tooltip as well. Once done it aligned correctly.
Notice that it checks for parents parentel.parentElement?.parentElement, this is so it doesn't take the global browser zoom into account.
export const getZoomLevel = (el: HTMLElement) : number[] => {
const zooms = []
const getZoom = (el: HTMLElement) => {
const zoom = window.getComputedStyle(el).getPropertyValue('zoom')
const rzoom = zoom ? parseFloat(zoom) : 1
if (rzoom !== 1) zooms.push(rzoom)
if (el.parentElement?.parentElement) getZoom(el.parentElement)
}
getZoom(el)
zooms.reverse()
return zooms
}
If you don't want to to zoom the 'tooltip' component would be to get rect with zoom adjusted for, like this:
export const getRect = (el: HTMLElement) : Partial<DOMRect> => {
if (!el) return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 }
let rect = el?.getBoundingClientRect();
const zooms = getZoomLevel(el)
const rectWithZoom = {
bottom: zooms.reduce((a, b) => a * b, rect.bottom),
height: zooms.reduce((a, b) => a * b, rect.height),
left: zooms.reduce((a, b) => a * b, rect.left),
right: zooms.reduce((a, b) => a * b, rect.right),
top: zooms.reduce((a, b) => a * b, rect.top),
width: zooms.reduce((a, b) => a * b, rect.width),
x: zooms.reduce((a, b) => a * b, rect.x),
y: zooms.reduce((a, b) => a * b, rect.y),
}
return rectWithZoom
}

Related

Positioning absolute element relative to viewport

I'm making a slider from scratch and I'm having an issue with the absolute positioning which has to be relative to the viewport (whenever the window height changes). Basically the slider has a bar and a button (to slide) and I wrote code to get the relative button position to the bar:
const getRelativeCoordinates = (event, referenceElement) => {
const position = {
x: event.pageX,
y: event.pageY
};
const offset = {
left: referenceElement.offsetLeft,
top: referenceElement.offsetTop
};
return {
x: position.x - offset.left,
y: position.y - offset.top,
};
}
and I wrote code to know after a cursor slide where the button would change his position and what percentage 1-100 actually is:
const handleSlider = (e) => {
const sliderBar = document.getElementById('slider-bar')
const sliderBtn = document.getElementById('slider-button')
const sliderHeight = parseInt(sliderBar.getBoundingClientRect().height) - sliderBarOffsetY
const cursorY = getRelativeCoordinates(e, document.getElementById('slider-bar')).y
let slidingPerc = ((sliderHeight - cursorY) / sliderHeight) * 100
if (slidingPerc > 100) slidingPerc = 100
else if (slidingPerc < 0) slidingPerc = 0
let barPerc = sliderBarMaxY - ((slidingPerc * (sliderBarMaxY - sliderBarMinY)) / 100)
return {
barPerc,
slidingPerc
}
}
The problem is that sliderBarMaxY and sliderBarMinY (83 and 9) are constants of max and min values of the button absolute top position in percentage (83% and 9%). These values changing the viewport height would not work anymore and if I try to change them by window.innerHeight (83 : 460 = x : innerHeight) gives an offset-error (in my case of 17)

Prevent subpixel rendering with svg

I'm working with SVGs currently and came to a dead end.
The SVG has lines, which should scale together with zooming (so that they stay in balance: 100% width 10px --> 10% width 1px for example)
i scale all stroke-widths with this code:
var svgPath = this._svgContainer.find('svg [class*="style"]');
for (var i = 0; i < svgPath.length; ++i) {
var newStrokeWidth = this._oldStrokeWidth[i] * (1 / (width / imgData.w));
$(svgPath[i]).css(
'stroke-width', newStrokeWidth
);
}
Where width is the new width after zoom and imgData.w is the original unscaled width.
The problem with this is, if i zoom in to far. The stroke with becomes to small and leads to sub-pixel rendering. And supposedly black lines get grey-ish.
My Idea was to clip the value at a certain point to prevent it.
But as far as I know, I have to consider the Device Pixel ratio too, because of different screens (desktop, mobile, 4K)
Would be nice If someone can help me with an idea to fix my problem
We finally found a solution for this, in case anyone has the same problems:
1) Because of the panning of this._$svgElement and the calculation of vpx in a completely different section of the code the element is 'between' pixels. ( 100.88945px for x for example). This causes lines to blur.
I fixed this part with a simple Math.round().
this._hammerCanvas.on('panmove', (event: any) => {
const translate3d = 'translate3d(' + Math.round(this._oldDeltaX + ((vpx === imgData.x) ? 0 : vpx) + event.deltaX) + 'px, ' + Math.round(this._oldDeltaY + ((vpy === imgData.y) ? 0 : vpy) + event.deltaY) + 'px, 0)';
this._$svgElement.css({
transform: translate3d
});
}
2) To fix the problem between the SVG viewport and the line strength, I had to implement a method to calculate the strokewidth equal to 1 'real' pixel regarding the svgs dimension.
the updated code looks like this: (This is the inital code, after the SVG was loaded from the server. Inside the zooming, the old code from above is still the same)
const pixelRatio = devicePixelRatio || 1;
const widthRatio = this._initSVGWidth / svgContainerWidth;
const heightRatio = this._initSVGHeight / svgContainerHeight;
this._svgZoomFactor = Math.max(widthRatio, heightRatio);
const strokeWidth1px = this.computeStrokeWidth1px(widthRatio, heightRatio);
for (let i = 0; i < svgPaths.length; ++i) {
this._initalStrokeWidth[i] = parseFloat($(svgPaths[i]).css('stroke-width'));
const newStrokeWidth = Math.max(strokeWidth1px / pixelRatio, this._svgZoomFactor * this._initalStrokeWidth[i]);
$(svgPaths[i])[0].setAttribute('style', 'stroke-width:' + newStrokeWidth);
this._oldStrokeWidth[i] = newStrokeWidth;
}
and the compute:
protected computeStrokeWidth1px (widthRatio: number, heightRatio: number): number {
const viewBox = this._$svgElement[0].getAttribute('viewBox').split(' ');
const viewBoxWidthRatio = parseFloat(viewBox[2]) / this._$svgElement.width();
const viewBoxHeightRatio = parseFloat(viewBox[3]) / this._$svgElement.height();
return widthRatio > heightRatio ? viewBoxWidthRatio : viewBoxHeightRatio;
}
var newStrokeWidth = this._oldStrokeWidth[i] * (1 / (width / imgData.w));
newStrokeWidth = (newStrokeWidth < 1) ? 1 : newStrokeWidth;
newStrokeWidth will always be 1 or greater

Animate an element in Javascript on mousemove

I have two DIV's of different widths on top of each other. The top DIV displayDIV is wider than the bottom DIV captureDIV.
In the displayDIV I'm drawing a dot who's X position is proportionate to the mouse position within captureDIV.
As you move the mouse in captureDIV the dot moves proportionately in DisplayDIV.
It makes much more sense if you look at this fiddle
My code is as follows...
let capture = document.getElementById('captureDIV');
let display = document.getElementById('displayDIV');
let circle = document.getElementById('circle');
capture.addEventListener('mousemove', handleMouseMove);
function handleMouseMove(event) {
const captureRect = capture.getBoundingClientRect();
const captureWidth = captureRect.right - captureRect.left;
const relativeX = event.x - captureRect.left;
let percent = (relativeX / captureWidth) * 100;
let roundedPercent = parseFloat(Math.round(percent * 100) / 100).toFixed(2);
moveDotTo(roundedPercent);
}
function moveDotTo(percentage) {
const displayRect = display.getBoundingClientRect();
const displayWidth = displayRect.right - displayRect.left;
const circleX = displayRect.left + displayWidth * (percentage / 100);
const circleY = displayRect.top + (displayRect.height / 2);
const style = `top:${circleY}px;left:${circleX}px;`;
circle.setAttribute('style', style);
}
I also have a number of buttons that can set the position of the dot within DisplayDIV such as...
let move20 = document.getElementById('move20');
move20.addEventListener('click', function(event) {
moveDotTo(20);
});
Using Vanilla JS not CSS tricks, how can I create a function to animate (rather than move) the dot from its existing position to the new position.
function animateDotTo(percentage) {
// clever code here
}
I need to be able to call the animateDotTo(percentage) function from either a button or from the mousemove event handler.
The dot should always animate to its new position regardless of how the move is triggered. For instance if the mouse is moved out of the left side of the captureDIV round the bottom and then into the right side of the captureDIV the dot should animate across the DisplayDIV not jump as it does now. Equally pressing one of the move to x% buttons should animate the dot from its current position to the new one.
If you are drawing a circle and moving it around, I would suggest drawing to a <canvas> element instead of moving a <div> by setting its top and left properties. Even using transform: translate(x, y) might be better.
In order to smoothly transition your dot from one location to another, using JavaScript, you will want:
The dot's current position as x and y coordinates,
The dot's target position as x and y coordinates, and
The speed at which the dot moves as a scalar.
Updating the current position is done at every animation frame with window.requestAnimationFrame. With these in hand, and a way of applying the resulting calculated position to the dot, you can use a method like this one: How to move an object using X and Y coordinates in JavaScript to move your dot (the example moves a canvas, but if you know the x and y, then you can set them to top and bottom).
Answering my own question, with thanks to Billy Brown for pointing me in the right direction. Using window.requestAnimationFrame is the way to go.
var currentPercentage;
var startPercentage;
var targetPercentage;
function animateDotTo(percentage) {
targetPercentage = percentage;
startPercentage = currentPercentage;
window.requestAnimationFrame(step);
}
function step(timestamp) {
var fps = 7;
var maxStep = 30;
var distStartToTarget = Math.abs(startPercentage - targetPercentage);
var stepSize = Math.min(distStartToTarget / fps, maxStep);
if (targetPercentage < startPercentage) {
currentPercentage -= stepSize,0;
if (currentPercentage > targetPercentage) {
window.requestAnimationFrame(step);
}
} else if (targetPercentage > startPercentage) {
currentPercentage += stepSize,100;
if (currentPercentage < targetPercentage) {
window.requestAnimationFrame(step);
}
} else {
return;
}
if (currentPercentage > 100 ) { currentPercentage = 100; }
if (currentPercentage < 0 ) { currentPercentage = 0; }
moveDotTo(currentPercentage);
}
Updated fiddle
A simple trick in css transition will fix this.
Of course. You don't want it to animate when you're actually moving the mouse. So what I did is that I separate the transition css property on another class and then remove that class on mouse move, re-attaching it when we click the move buttons.
CSS
#circle {
position: absolute;
left: -100px;
top: -100px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #000;
transition: none;
}
#circle.animate{
transition: 500ms ease;
}
JS
move20.addEventListener('click', function(event) {
moveDotTo(20); animateDotTo();
});
move60.addEventListener('click', function(event) {
moveDotTo(60);animateDotTo();
});
move80.addEventListener('click', function(event) {
moveDotTo(80);animateDotTo();
});
function moveDotTo(percentage) {
circle.classList.remove("animate");
const displayRect = display.getBoundingClientRect();
const displayWidth = displayRect.right - displayRect.left;
const circleX = displayRect.left + displayWidth * (percentage / 100);
const circleY = displayRect.top + (displayRect.height / 2);
const style = `top:${circleY}px;left:${circleX}px;`;
circle.setAttribute('style', style);
}
function animateDotTo(percentage) {
circle.classList.add("animate");
}
http://jsfiddle.net/8pm2grjd/
If you want it to animate even if you're triggering the movement using mousemove, you can disregard the class approach and just slap the transition property on the css. But this will simulate the annoying mouse delay effect similar to input delay on video games due to V-Sync.

d3 scaleLinear update and zoom conflict

i have a problem combining manual update and zoom functionality in d3.
Here is a small demo code cut off out of a larger module which creates only a linear scale.
https://jsfiddle.net/superkamil/x3v2yc7j/2
class LinearScale {
constructor(element, options) {
this.element = d3.select(element);
this.options = options;
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale = this._create();
}
update(options) {
this.options = Object.assign(this.options, options);
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale.call(this.axis);
}
_create() {
const scale = this.element
.append('g')
.attr('class', 'linearscale')
.call(this.axis);
this.zoom = d3.zoom().on('zoom', () => this._zoomed());
this.element.append('rect')
.style('visibility', 'hidden')
.style('width', this.options.width)
.style('height', this.options.height)
.attr('pointer-events', 'all')
.call(this.zoom);
return scale;
}
_createScale() {
let range = this.options.width;
this.scale = this.scale || d3.scaleLinear();
this.scale.domain([
this.options.from,
this.options.to
]).range([0, range]);
return this.scale;
}
_createAxis() {
if (this.axis) {
this.axis.scale(this.scale);
return this.axis;
}
return d3.axisBottom(this.scale);
}
_zoomed() {
this.linearscale
.call(this.axis.scale(d3.event.transform.rescaleX(this.scale)));
let domain = this.axis.scale().domain();
this.element.dispatch('zoomed', {
detail: {
from: domain[0],
to: domain[1],
},
});
}
}
const scale = new LinearScale(document.getElementById('axis'), {
from: 0,
to: 600,
width: 600,
height: 100
});
document.getElementById('set').addEventListener('click', () => {
scale.update({
from: 0,
to: 100
});
});
Zoom x axis out to 0 - 10000 (random numbers)
Click on the "set" button
X axis sets the domain from 0 - 100
Start zooming out again
-> Expected: Zoom starts from the domain 0 - 100
-> Result: Zoom jumps back to the previous zoom level 0 - 10000
I know, that d3 is working with a scale copy and i'm updating the original scale but i don't find a way how to combine them or how to set the zoom level to the original scale.
https://github.com/d3/d3-zoom/blob/master/README.md#transform_rescaleX
Thanks!
You either recreate the zoom behaviour or just reset the current zoom transforms.
You should be able to reset each parameter( zoom and translate) or just reset them all. The following example also triggers the zoom specific events, but as you already have the domain set it should not be a visual problem.
Ex:
Add
this.element.select("rect").call(this.zoom.transform, d3.zoomIdentity);
in your update function.
More information regarding the zoom api in https://github.com/d3/d3-zoom/blob/master/README.md#zoom_transform . Also I found an example using a transition animation at https://bl.ocks.org/mbostock/db6b4335bf1662b413e7968910104f0f .
I also updated the fiddle:
update(options) {
this.options = Object.assign(this.options, options);
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale.call(this.axis);
this.element.select("rect").call(this.zoom.transform, d3.zoomIdentity);
}
https://jsfiddle.net/x3v2yc7j/8/

Canvas: Rectangles -- Snap to grid / Snap to objects

I managed to manipulate Fabric.js to add a snap and scale to grid functionality by:
var grid = 100;
//Snap to Grid
canvas.on('object:moving', function (options) {
options.target.set({
left: Math.round(options.target.left / grid) * grid,
top: Math.round(options.target.top / grid) * grid
});
});
canvas.on('object:scaling', function (options) {
options.target.set({
left: Math.round(options.target.left / grid) * grid,
top: Math.round(options.target.top / grid) * grid
});
});
Now I want to add snap to objects functionality. My idea was to check intersection of two objects and then lock somehow the movement. I know its not the best attempt, but at least it snaps to it, but does not allow to move the object away anymore. And: right now it is not implemented well. See: http://jsfiddle.net/gcollect/y9kyq/
I have three issues:
The "snap" doesn't work well, because the object left attribute depends on the pointer somehow. Replication by dragging object and watching my controls output. For example when moving the red rectangle to position left:62, the rectangle isn't intersected with the blue rectangle and can still moved away. How can I reload the actual left value of the rectangle. Because of my snap to grid lines, it is at left:100 and not at left:62.
Any idea how I can add a snap to object functionality? And prohibit intersection?
How can I check this for n objects and not only for two?
Thanks for your comments.
PS:
The jsfiddle example doesn't show the scale to grid functionality, because it needed Fabric.js manipulation in line: 11100
var distroundedforgrid = Math.round(dist/100)*100;
transform.newScaleX = Math.round((transform.original.scaleX * distroundedforgrid / lastDist)*10)/10;
transform.newScaleY = Math.round((transform.original.scaleY * distroundedforgrid / lastDist)*10)/10;
target.set('scaleX', transform.newScaleX);
target.set('scaleY', transform.newScaleY);
}
For those who are still interested in the solution:
I solved it here: https://stackoverflow.com/a/22649022/3207478
See jsfiddle: http://jsfiddle.net/gcollect/FD53A/
Working with .oCoords.tl .tr .bl. and .br solved it.
For rescaling based on the grid see this JSfiddle
function snapScaling(options) {
var target = options.target;
var type = canvas.getActiveObject().get('type');
var corner = target.__corner;
var w = target.getWidth();
var h = target.getHeight();
var snap = { // Closest snapping points
top: Math.round(target.top / grid) * grid,
left: Math.round(target.left / grid) * grid,
bottom: Math.round((target.top + h) / grid) * grid,
right: Math.round((target.left + w) / grid) * grid,
};
snap.height = snap.top - snap.bottom;
if(snap.height < 0) {
snap.height *= - 1;
}
snap.width = snap.left - snap.right;
if(snap.width < 0) {
snap.width *= - 1;
}
switch (corner) {
case 'mt':
case 'mb':
target.top = snap.top;
target.height = snap.height;
target.scaleY = 1;
break;
case 'ml':
case 'mr':
target.left = snap.left;
target.width = snap.width;
target.scaleX = 1;
break;
case 'tl':
case 'bl':
case 'tr':
case 'br':
target.top = snap.top;
target.left = snap.left;
target.height = snap.height;
target.width = snap.width;
target.scaleY = 1;
target.scaleX = 1;
}
if(type == 'ellipse') {
target.rx = (target.width / 2);
target.ry = (target.height / 2);
}
}
I figured it out to solve the snap to objects on x-axis and will work on a solution for the y-axis. See JSfiddle here.
I did this by setting a new "left"-value for the active object, when I detect an intersection.
if (options.target.isContainedWithinObject(obj)||options.target.intersectsWithObject(obj)||obj.isContainedWithinObject(options.target)) {
var distx = ((obj.left + obj.width)/2) - ((options.target.left + options.target.width)/2);
var disty = ((obj.top + obj.height)/2) - ((options.target.top + options.target.height)/2);
if (distx > 0){
options.target.left = obj.left - options.target.width;
} else {
options.target.left = obj.left + obj.width;
}
}

Categories

Resources