Object with fixed Y-axis that follows the mouse on X-axis - javascript

I need help for an effect I'm trying to create: I made a CSS triangle and I want it to be fixed on the Y-axis but follow the mouse on his X-axis (didn't you read the title ?!). If it's not clear, I want it to move only to the left/right but not up/down. I managed to apply a js script I found on the internet to my triangle but I can't figure out how to change it to stop it from moving on the Y-axis. When I try to change anything, the whole thing doesn't move anymore. Can some one help me ?
// Here get the Div that you want to follow the mouse
var div_moving = document.getElementById('div_moving');
// Here add the ID of the parent element
var parent_div = 'parent_div';
// object to make a HTML element to follow mouse cursor ( http://coursesweb.net/ )
var movingDiv = {
mouseXY: {}, // will contain the X, Y mouse coords inside its parent
// Get X and Y position of the elm (from: vishalsays.wordpress.com/ )
getXYpos: function(elm) {
x = elm.offsetLeft; // set x to elm’s offsetLeft
y = elm.offsetTop; // set y to elm’s offsetTop
elm = elm.offsetParent; // set elm to its offsetParent
//use while loop to check if elm is null
// if not then add current elm’s offsetLeft to x, offsetTop to y and set elm to its offsetParent
while(elm != null) {
x = parseInt(x) + parseInt(elm.offsetLeft);
y = parseInt(y) + parseInt(elm.offsetTop);
elm = elm.offsetParent;
}
// returns an object with "xp" (Left), "=yp" (Top) position
return {'xp':x, 'yp':y};
},
// Returns object with X, Y coords inside its parent
getCoords: function(e) {
var xy_pos = this.getXYpos(e.target);
// if IE
if(navigator.appVersion.indexOf("MSIE") != -1) {
var standardBody = (document.compatMode == 'CSS1Compat') ? document.documentElement : document.body;
x = event.clientX + standardBody.scrollLeft;
y = event.clientY + standardBody.scrollTop;
}
else {
x = e.pageX;
y = e.pageY;
}
x = x - xy_pos['xp'];
y = y - xy_pos['yp'];
return {'xp':x, 'yp':y};
}
};
// registers 'mousemove' event to parent_div
document.getElementById(parent_div).addEventListener('mousemove', function(e){
mouseXY = movingDiv.getCoords(e);
div_moving.style.left = mouseXY.xp + 8 +'px';
div_moving.style.top = mouseXY.yp - 8 +'px';
});
#parent_div {
position: relative;
width: 100%;
height: 800px;
margin: 1em auto;
border; 1px solid #333;
background: #fefebe;
}
#div_moving {
position: absolute;
width: 41em;
height: 31em;
margin: 0;
border: 1px solid #33f;
background: #88ee99;
overflow:hidden;
}
.container {
width: 37.5em;
height: 37.5em;
position: relative;
border-top: 20px solid #e74c3c;
left:3%;
}
.triangle {
position: relative;
margin: auto;
top: -20em;
left: 0;
right: 0;
width:31em;
height:31em;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-o-transform: rotate(45deg);
-ms-transform: rotate(45deg);
border-right: 20px solid #e74c3c;
border-bottom: 20px solid #e74c3c;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="parent_div">
<div id="div_moving">
<div class="container">
<div class="triangle"></div>
</div>
</div>
Content in parent ...
</div>

I just reformatted a little, then commented one line and it's working in Chrome on my machine. Is this what you're looking for?
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script language="javascript">
// object to make a HTML element to follow mouse cursor ( http://coursesweb.net/ )
var movingDiv = {
mouseXY: {}, // will contain the X, Y mouse coords inside its parent
// Get X and Y position of the elm (from: vishalsays.wordpress.com/ )
getXYpos: function(elm) {
x = elm.offsetLeft; // set x to elm’s offsetLeft
y = elm.offsetTop; // set y to elm’s offsetTop
elm = elm.offsetParent; // set elm to its offsetParent
//use while loop to check if elm is null
// if not then add current elm’s offsetLeft to x, offsetTop to y and set elm to its offsetParent
while(elm != null) {
x = parseInt(x) + parseInt(elm.offsetLeft);
y = parseInt(y) + parseInt(elm.offsetTop);
elm = elm.offsetParent;
}
// returns an object with "xp" (Left), "=yp" (Top) position
return {'xp':x, 'yp':y};
},
// Returns object with X, Y coords inside its parent
getCoords: function(e) {
var xy_pos = this.getXYpos(e.target);
// if IE
if(navigator.appVersion.indexOf("MSIE") != -1) {
var standardBody = (document.compatMode == 'CSS1Compat') ? document.documentElement : document.body;
x = event.clientX + standardBody.scrollLeft;
y = event.clientY + standardBody.scrollTop;
}
else {
x = e.pageX;
y = e.pageY;
}
x = x - xy_pos['xp'];
y = y - xy_pos['yp'];
return {'xp':x, 'yp':y};
}
};
$(document).ready(function() {
// Here get the Div that you want to follow the mouse
var div_moving = document.getElementById('div_moving');
// Here add the ID of the parent element
var parent_div = 'parent_div';
// registers 'mousemove' event to parent_div
document.getElementById(parent_div).addEventListener('mousemove', function(e){
mouseXY = movingDiv.getCoords(e);
div_moving.style.left = mouseXY.xp + 8 +'px';
//div_moving.style.top = mouseXY.yp - 8 +'px';
});
});
</script>
<style>
#parent_div {
position: relative;
width: 100%;
height: 800px;
margin: 1em auto;
border; 1px solid #333;
background: #fefebe;
}
#div_moving {
position: absolute;
width: 41em;
height: 31em;
margin: 0;
border: 1px solid #33f;
background: #88ee99;
overflow:hidden;
}
.container {
width: 37.5em;
height: 37.5em;
position: relative;
border-top: 20px solid #e74c3c;
left:3%;
}
.triangle {
position: relative;
margin: auto;
top: -20em;
left: 0;
right: 0;
width:31em;
height:31em;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-o-transform: rotate(45deg);
-ms-transform: rotate(45deg);
border-right: 20px solid #e74c3c;
border-bottom: 20px solid #e74c3c;
}
</style>
</head>
<body>
<div id="parent_div">
<div id="div_moving">
<div class="container">
<div class="triangle"></div>
</div>
</div>
Content in parent ...
</div>
</body>
</html>
Differences:
Load the JQuery script first
Process the listener in a ready function so all the content has been loaded
Comment out the Y positioning

EDIT: I found a solution to my problem.
So here the problems and what I did:
I wanted the object to move only on X-axis and not Y: IgnusFast found out the line to delete was "div_moving.style.top = mouseXY.yp - 8 +'px';"
I wanted it to stop staggering when the mouse passed over it: deleted "parseInt(x) +" in "while(elm != null) {x = parseInt(x) + parseInt(elm.offsetLeft); elm = elm.offsetParent;}" (makes the div stay where it is when not sure.
I wanted it to center with the mouse instead of being on its right: original was " div_moving.style.left = mouseXY.xp + 8 +'px';" wich made it go 8 pixels to the right of the current mouse's coordinates so I just used a negative number and place like this :" div_moving.style.left = mouseXY.xp + -350 +'px';"

Related

Using jQuery and CSS3's TranslateX() to reveal an element while scrolling parent element into view

I would like to change the position of a circle when it's parent section is scrolled into view.
While scrolling down after the parent is in view, it should move to the right and when scrolling up, it should move back to where it was originally. (-200px to the left) It should only be moved while the user is actively scrolling.
If the user scrolls all the way down to the very bottom of the circle's parent section, or if they have already scrolled down to the bottom and reload the page, the circle should appear in it's fully-revealed position.
My current code partially works, but I'm having trouble with getting the entire element to appear based on how much of the parent element is visible and also getting it to display in it's final position when reloading the page after having scrolled to the very bottom.
JSFiddle: https://jsfiddle.net/thebluehorse/gu2rvnsw/
var $window = $(window),
$sectionFour = $('.section-four'),
$circle = $sectionFour.find('.circle'),
lastScrollTop = 0,
position = -200;
function revealCircle() {
var isVisible,
st = $window.scrollTop();
isVisible = isInView($sectionFour);
if (isVisible) {
// console.log('section four is in view, so lets do stuff!');
if (st > lastScrollTop) {
if (position === 0) {
return false
}
$circle.css('transform', 'translateX(' + position + 'px')
position++;
} else {
if (position === -200) {
return false
}
$circle.css('transform', 'translateX(' + position + 'px')
position--;
}
}
}
function isInView(node) {
var rect;
if (typeof jQuery === 'function' && node instanceof jQuery) {
node = node[0];
}
rect = node.getBoundingClientRect();
return (
(rect.height > 0 || rect.width > 0) &&
rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.left <= (window.innerWidth || document.documentElement.clientWidth)
);
}
$window.on('scroll', revealCircle);
.circle {
width: 400px;
height: 400px;
background: #fff;
-webkit-border-radius: 200px;
-moz-border-radius: 200px;
border-radius: 200px;
transform: translateX(-200px); }
.section {
min-height: 400px; }
.section-one {
background-color: red; }
.section-two {
background-color: orange; }
.section-three {
background-color: yellow; }
.section-four {
background-color: green; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<section class="section section-one"></section>
<section class="section section-two"></section>
<section class="section section-three"></section>
<section class="section section-four">
<div class="circle"></div>
</section>
Your code can be simplified a bit. The only value you need to keep track of as the page is scrolled is scrollTop(). Because the geometry of $sectionFour never changes, you can cache its getBoundingClientRect() right away.
Once you know that $sectionFour is in view, you want to figure out how many pixels of its total height are in view, convert that to a percentage, and then apply that percentage to the initial position of -200. Essentially, when only a few pixels are showing, that's a small percentage, such as 10% and -200 becomes -180. When the element is fully in view, the percentage should be near 100%, and -200 becomes 0. This means you're not keeping track of the last position or which direction the scroll was, you're just computing what the value should be based on the current viewport (scrollTop).
var $window = $(window),
$sectionFour = $('.section-four'),
$circle = $sectionFour.find('.circle');
rect = $sectionFour[0].getBoundingClientRect();
function revealCircle() {
var scrollTop = $window.scrollTop();
var windowHeight = $window[0].innerHeight;
if (scrollTop + windowHeight > rect.top) {
var percentVisible = (scrollTop - (rect.top - windowHeight)) / rect.height;
var position = 200 - (percentVisible * 200);
$circle.css('transform', 'translateX(-' + position + 'px');
}
}
$window.on('scroll', revealCircle);
body { margin:0;}
.circle {
width: 400px;
height: 400px;
background: #fff;
-webkit-border-radius: 200px;
-moz-border-radius: 200px;
border-radius: 200px;
transform: translateX(-200px); }
.section {
min-height: 400px; }
.section-one {
background-color: red; }
.section-two {
background-color: orange; }
.section-three {
background-color: yellow; }
.section-four {
background-color: green; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<section class="section section-one"></section>
<section class="section section-two"></section>
<section class="section section-three"></section>
<section class="section section-four">
<div class="circle"></div>
</section>
It could be more simpler to use css variables for pure JS solution :
const sectFour = document.querySelector('#section-four')
, divCircle = sectFour.querySelector('.circle')
function percentVisible(elm)
{
let rect = elm.getBoundingClientRect()
, viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight)
, visu = viewHeight-rect.top
return (visu<0) ? -1 : Math.min( 100, (visu/rect.height*100))
// return !(rect.bottom < 0 || rect.top - viewHeight >= 0) => checkVisible
}
window.onscroll=_=>
{
let circlePos = percentVisible(sectFour) *2
if (circlePos>=0)
{
divCircle.style.setProperty('--circle-pos', `-${200-circlePos}px`)
// IE 11 : // divCircle.style=('transform:translateX(-'+(200-circlePos)+'px')
}
}
* { margin: 0 }
.circle {
--circle-pos :-200px;
width : 400px;
height : 400px;
background-color : #fff;
-webkit-border-radius: 200px;
-moz-border-radius: 200px;
border-radius: 200px;
transform : translateX(var(--circle-pos));
/* IE 11 ........... : translateX(-200px); */
}
section { min-height: 400px; }
section:nth-of-type(1) { background-color: red; }
section:nth-of-type(2) { background-color: orange; }
section:nth-of-type(3) { background-color: yellow; }
section:nth-of-type(4) { background-color: green; }
<section></section>
<section></section>
<section></section>
<section id="section-four"> <div class="circle"></div> </section>
You should have a look at Intersection Observer (IO), this was designed to solve problems like yours. Listening to scroll event and calculating the position can result in bad performance.
First, you have to define the options for the IO:
let options = {
root: document.querySelectorAll('.section-four'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
After defining the options you have to tell the observer which elements to observe, I guess in your case this would be .section-four:
let targets = document.querySelectorAll('.section-four');
targets.forEach(target => {
observer.observe(target) }
)
Final step is to define the callback function that should be executed once .section-four is getting into view:
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element
// here you can do something like $(entry.target).find('circle') to get your circle
});
};
Have a look at this demo, depending on how much the element is visible the background-color changes. I think this comes close to your problem, you just don't change the bg-color you animate the circle inside the element.
There is also another demo on the site that displays how much of an element is visible on the screen, maybe this suits you better.
You can also use this polyfill from w3c to support older browsers.

Centering a position absolute element that has been cloned

I'm trying to clone a tooltip div and append it to the document body while also centering around it's original parent.
window.addEventListener("load", function() {
const tooltipActivator = document.querySelectorAll("[data-onsitetooltip]");
for(let i = 0; i < tooltipActivator.length; i++) {
if(tooltipActivator[i] != undefined) {
let added = false;
tooltipActivator[i].addEventListener("mouseenter", function(e) {
const clone = tooltipActivator[i].querySelector(".onsiteTooltip").cloneNode(true),
elemRect = tooltipActivator[i].getBoundingClientRect(),
offset_top = elemRect.top - document.body.getBoundingClientRect().top,
offset_left = elemRect.left;
clone.style.visibility = "visible";
clone.style.opacity = "1";
clone.style.top = offset_top + parseInt(tooltipActivator[i].dataset.onsitetooltiptop) + "px";
clone.style.left = offset_left + "px";
clone.style.transform = "translateX(-"+tooltipActivator[i].offsetWidth/2+"px)";
clone.style.pointerEvents = "none";
document.body.appendChild(clone);
});
tooltipActivator[i].addEventListener("mouseleave", function(e) {
const tooltipToRemove = document.querySelectorAll("body > .onsiteTooltip");
for(let z = 0; z < tooltipToRemove.length; z++) {
if(tooltipToRemove[z] != undefined) {
document.body.removeChild(tooltipToRemove[z]);
}
}
});
}
}
});
.onsiteTooltip {
visibility: hidden;
opacity: 0;
width: 25em;
cursor: auto;
background: #0c1633;
padding: 1em;
#include border-radius(0.25rem);
#include box-shadow($shadow-big);
position: absolute;
z-index: 4;
border: 1px solid rgba(0, 0, 0, 0.05);
#include transition(visibility 0s, opacity 0.2s linear);
font-size: 0.9em;
}
.myDiv { height: 100px; background: red; }
<div class="myDiv" data-onsitetooltip data-onsitetooltiptop="70">
<div class="onsiteTooltip">Hello World</div>
</div>
The goal is to make it so when appended to the body, it should be below and centered around the element with the data-onsitetooltip property, without appending it as a child to that element.
Depending on the element that hold the tooltip, it works sometimes, but on other elements it sometimes isn't centered and misaligned.
Like this: http://prntscr.com/op0wuk
How can I accomplish this?
EDIT: I've tried to set the offset left to 50% of the parent and then -50% on the tooltip like you would do in CSS but that did not work either.

move a large div opposite to mouse movement

I have a table full of data that tends to be larger than the screen.
I put the table in a DIV and set the "overflow" to "auto" in CSS
div.scrolling-comps {
width : 970px;
height : 800px;
overflow : auto;
}
So the DIV can be scrolled up/down, left right using the browser's built-in scroll bars.
Problem is, the table can be WAAY bigger than the screen. And while the mousewheel will scroll it up/down, scrolling left/right is a pain in the hooch.
So, looking for a javascript/jquery or CSS way to scroll the div NATURALLY.
In other words, when someone viewing the huuuge table moves their mouse to the right, the DIV goes to the left (thus scrolling without using the scroll bars).
Something similar to this, but instead of following the mouse, the div would move opposite the mouse...
window.onload = function() {
var bsDiv = document.getElementById("box-shadow-div");
var x, y;
// On mousemove use event.clientX and event.clientY to set the location of the div to the location of the cursor:
window.addEventListener('mousemove', function(event) {
x = event.clientX;
y = event.clientY;
if (typeof x !== 'undefined') {
bsDiv.style.left = x + "px";
bsDiv.style.top = y + "px";
}
}, false);
}
#box-shadow-div {
position: fixed;
width: 1000px;
height: 800px;
border-radius: 0%;
background-color: black;
box-shadow: 0 0 10px 10px black;
top: 49%;
left: 48.85%;
}
<div id="box-shadow-div"></div>
The example you have about using the mouse position is interesting... But it is not what you need to achieve what you described.
In fact... What you need to know is the "ratio" between the div wrapping the table and its scrollWidth
Then, using the X position of the mouse, you can apply a scroll to the div in order to make it "move".
I used jQuery to do it using very few lines.
// Just to fake a big table
var fakeCell = $("<td>Some data</td>");
for(i=0;i<100;i++){
var fakeRow = $("<tr>");
for(k=0;k<50;k++){
fakeRow.append(fakeCell.clone().append(" "+k));
}
$("#test").append(fakeRow.clone());
}
// ---------------------------------------------------
// Calculate the "ratio" of the box-div width versus its scrollable width
var ratio = $("#box-div")[0].scrollWidth / $("#box-div").width();
console.log("Ratio: "+ratio);
// Scroll left/rigth based on mouse movement
$(window).on("mousemove", function(e){
var X = ratio * e.pageX;
// Scroll the div using the mouse position multiplyed by the ratio
$("#box-div").scrollLeft(X);
});
td{
white-space: nowrap;
border: 1px solid black;
}
#box-div{
overflow:auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<div id="box-div">
<table id="test">
</table>
</div>
</body>
So while the user moves the mouse over the div's width, you apply a scroll multiplied by the ratio... The effect is the user can scroll it all from the most left to most right ends easilly.
How about this?
wrap a table in div (i.e. parent-div) which is relatively positioned
Give position absolute to the target div.
And change left & top position of target div on mousemove event.
window.onload = function() {
var bsDiv = document.getElementById("box-shadow-div");
var x, y;
// On mousemove use event.clientX and event.clientY to set the location of the div to the location of the cursor:
window.addEventListener('mousemove', function(event) {
x = event.clientX;
y = event.clientY;
if (typeof x !== 'undefined') {
bsDiv.style.left = -x + "px";
bsDiv.style.top = -y + "px";
}
}, false);
}
.parent-div {
position: relative;
}
#box-shadow-div {
position: absolute;
width: 1000px;
height: 800px;
border-radius: 0%;
background-color: black;
box-shadow: 0 0 10px 10px black;
top: 0;
left: 0;
}
<div class="parent-div">
<div id="box-shadow-div"></div>
</div>
Have you tried changing x to -x? this will technically "invert" the effect.
window.onload = function() {
var bsDiv = document.getElementById("box-shadow-div");
var x, y;
// On mousemove use event.clientX and event.clientY to set the location of the div to the location of the cursor:
window.addEventListener('mousemove', function(event) {
x = event.clientX;
y = event.clientY;
if (typeof x !== 'undefined') {
bsDiv.style.left = -x + "px";
bsDiv.style.top = -y + "px";
}
}, false);
}
#box-shadow-div {
position: fixed;
width: 1000px;
height: 800px;
border-radius: 0%;
background-color: black;
box-shadow: 0 0 10px 10px black;
top: 49%;
left: 48.85%;
}
<div id="box-shadow-div"></div>

Using CSS transform scale() to zoom into an element without cropping, maintaining scrolling

Live example: https://jsfiddle.net/b8vLg0ny/
It's possible to use the CSS scale and translate functions to zoom into element.
Take this example, of 4 boxes in a 2x2 grid.
HTML:
<div id="container">
<div id="zoom-container">
<div class="box red">A</div>
<div class="box blue">B</div>
<div class="box green">C</div>
<div class="box black">D</div>
</div>
</div>
CSS:
* { margin: 0; }
body, html { height: 100%; }
#container {
height: 100%;
width: 50%;
margin: 0 auto;
}
#zoom-container {
height: 100%;
width: 100%;
transition: all 0.2s ease-in-out;
}
.box {
float: left;
width: 50%;
height: 50%;
color: white;
text-align: center;
display: block;
}
.red { background: red; }
.blue { background: blue; }
.green { background: green; }
.black { background: black; }
JavaScript:
window.zoomedIn = false;
$(".box").click(function(event) {
var el = this;
var zoomContainer = $("#zoom-container");
if (window.zoomedIn) {
console.log("resetting zoom");
zoomContainer.css("transform", "");
$("#container").css("overflow", "auto");
window.zoomedIn = false;
} else {
console.log("applying zoom");
var top = el.offsetTop;
var left = el.offsetLeft - 0.25*zoomContainer[0].clientWidth;
var translateY = 0.5*zoomContainer[0].clientHeight - top;
var translateX = 0.5*zoomContainer[0].clientWidth - left;
$("#container").css("overflow", "scroll");
zoomContainer.css("transform", "translate(" + 2 * translateX + "px, " + 2 * translateY + "px) scale(2)");
window.zoomedIn = true;
}
});
By controlling the value of translateX and translateY, you can change how the zooming works.
The initial rendered view looks something like this:
Clicking on the A box will zoom you in appropriately:
(Note that clicking D at the end is just showing the reset by zooming back out.)
The problem is: zooming to box D will scale the zoom container such that scrolling to the top and left doesn't work, because the contents overflow. The same happens when zooming to boxes B (the left half is cropped) and C (the top half is cropped). Only with A does the content not overflow outside the container.
In similar situations related to scaling (see CSS3 Transform Scale and Container with Overflow), one possible solution is to specify transform-origin: top left (or 0 0). Because of the way the scaling works relative to the top left, the scrolling functionality stays. That doesn't seem to work here though, because it means you're no longer repositioning the contents to be focused on the clicked box (A, B, C or D).
Another possible solution is to add a margin-left and a margin-top to the zoom container, which adds enough space to make up for the overflowed contents. But again: the translate values no longer line up.
So: is there a way to both zoom in on a given element, and overflow with a scroll so that contents aren't cropped?
Update: There's a rough almost-solution by animating scrollTop and scrollLeft, similar to https://stackoverflow.com/a/31406704/528044 (see the jsfiddle example), but it's not quite a proper solution because it first zooms to the top left, not the intended target. I'm beginning to suspect this isn't actually possible, because it's probably equivalent to asking for scrollLeft to be negative.
Why not just to reposition the TransformOrigin to 0 0 and to use proper scrollTop/scrollLeft after the animation?
https://jsfiddle.net/b8vLg0ny/7/
Updated: https://jsfiddle.net/b8vLg0ny/13/
If you do not need the animation, the TransformOrigin can always stays 0 0 and only the scrolling is used to show the box.
To make the animation less jumpy use transition only for transform porperty, otherwise the transform-origin gets animated also. I have edited the example with 4x4 elements, but I think it makes sense to zoom a box completely into view, thats why I changed the zoom level. But if you stay by zoom level 2 and the grid size 15x15 for instance, then with this approach really precise origin should be calculated for transform, and then also the correct scrolling.
Anyway I don't know, if you find this approach useful.
Stack snippet
var zoomedIn = false;
var zoomContainer = $("#zoom-container");
$(".box").click(function(event) {
var el = this;
if (zoomedIn) {
zoomContainer.css({
transform: "scale(1)",
transformOrigin: "0 0"
});
zoomContainer.parent().scrollTop(0).scrollLeft(0);
zoomedIn = false;
return;
}
zoomedIn = true;
var $el = $(el);
animate($el);
zoomContainer.on('transitionend', function(){
zoomContainer.off('transitionend');
reposition($el);
})
});
var COLS = 4, ROWS = 4,
COLS_STEP = 100 / (COLS - 1), ROWS_STEP = 100 / (ROWS - 1),
ZOOM = 4;
function animate($box) {
var cell = getCell($box);
var col = cell.col * COLS_STEP + '%',
row = cell.row * ROWS_STEP + '%';
zoomContainer.parent().css('overflow', 'hidden');
zoomContainer.css({
transition: 'transform 0.2s ease-in-out',
transform: "scale(" + ZOOM + ")",
transformOrigin: col + " " + row
});
}
function reposition($box) {
zoomContainer.css({
transition: 'none',
transform: "scale(" + ZOOM + ")",
transformOrigin: '0 0'
});
zoomContainer.parent().css('overflow', 'auto');
$box.get(0).scrollIntoView();
}
function getCell ($box) {
var idx = $box.index();
var col = idx % COLS,
row = (idx / ROWS) | 0;
return { col: col, row: row };
}
* { margin: 0; }
body, html { height: 100%; }
#container {
height: 100%;
width: 50%;
margin: 0 auto;
overflow: hidden;
}
#zoom-container {
height: 100%;
width: 100%;
will-change: transform;
}
.box {
float: left;
width: 25%;
height: 25%;
color: white;
text-align: center;
}
.red { background: red; }
.blue { background: blue; }
.green { background: green; }
.black { background: black; }
.l { opacity: .3 }
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<div id="container">
<div id="zoom-container">
<div class="box red">A</div>
<div class="box blue">B</div>
<div class="box green">C</div>
<div class="box black">D</div>
<div class="box red l">E</div>
<div class="box blue l">F</div>
<div class="box green l">G</div>
<div class="box black l">H</div>
<div class="box red">I</div>
<div class="box blue">J</div>
<div class="box green">K</div>
<div class="box black">L</div>
<div class="box red l">M</div>
<div class="box blue l">N</div>
<div class="box green l">O</div>
<div class="box black l">P</div>
</div>
</div>
I'm answering my own question, since I'm fairly confident that it's actually not possible with the given requirements. At least not without some hackery that would cause problems visually, e.g., jumpy scrolling by animating scrollTop after switching transform-origin to 0, 0 (which removes the cropping by bringing everything back into the container).
I'd love for someone to prove me wrong, but it seems equivalent to asking for scrollLeft = -10, something that MDN will tell you is not possible. ("If set to a value less than 0 [...], scrollLeft is set to 0.")
If, however, it's acceptable to change the UI from scrolling, to zooming and dragging/panning, then it's achievable: https://jsfiddle.net/jegn4x0f/5/
Here's the solution with the same context as my original problem:
HTML:
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<button id="zoom-out">Zoom out</button>
<div id="container">
<div id="inner-container">
<div id="zoom-container">
<div class="box red">A</div>
<div class="box blue">B</div>
<div class="box green">C</div>
<div class="box black">D</div>
</div>
</div>
</div>
JavaScript:
//
// credit for the approach goes to
//
// https://stackoverflow.com/questions/35252249/move-drag-pan-and-zoom-object-image-or-div-in-pure-js#comment58224460_35253567
//
// and the corresponding example:
//
// https://jsfiddle.net/j8kLz6wm/1/
//
// in a real-world setting, you
// wouldn't keep this information
// on window. this is just for
// the demonstration.
window.zoomedIn = false;
// stores the initial translate values after clicking on a box
window.translateY = null;
window.translateX = null;
// stores the incremental translate values based on
// applying the initial translate values + delta
window.lastTranslateY = null;
window.lastTranslateX = null;
// cursor position relative to the container, at
// the time the drag started
window.dragStartX = null;
window.dragStartY = null;
var handleDragStart = function(element, xCursor, yCursor) {
window.dragStartX = xCursor - element.offsetLeft;
window.dragStartY = yCursor - element.offsetTop;
// disable transition animations, since we're starting a drag
$("#zoom-container").css("transition", "none");
};
var handleDragEnd = function() {
window.dragStartX = null;
window.dragStartY = null;
// remove the individual element's styling for transitions
// which brings back the stylesheet's default of animating.
$("#zoom-container").css("transition", "");
// keep track of the translate values we arrived at
window.translateY = window.lastTranslateY;
window.translateX = window.lastTranslateX;
};
var handleDragMove = function(xCursor, yCursor) {
var deltaX = xCursor - window.dragStartX;
var deltaY = yCursor - window.dragStartY;
var translateY = window.translateY + (deltaY / 2);
// the subtracted value here is to keep the letter in the center
var translateX = window.translateX + (deltaX / 2) - (0.25 * $("#inner-container")[0].clientWidth);
// fudge factor, probably because of percentage
// width/height problems. couldn't really trace down
// the underlying cause. hopefully the general approach
// is clear, though.
translateY -= 9;
translateX -= 4;
var innerContainer = $("#inner-container")[0];
// cap all values to prevent infinity scrolling off the page
if (translateY > 0.5 * innerContainer.clientHeight) {
translateY = 0.5 * innerContainer.clientHeight;
}
if (translateX > 0.5 * innerContainer.clientWidth) {
translateX = 0.5 * innerContainer.clientWidth;
}
if (translateY < -0.5 * innerContainer.clientHeight) {
translateY = -0.5 * innerContainer.clientHeight;
}
if (translateX < -0.5 * innerContainer.clientWidth) {
translateX = -0.5 * innerContainer.clientWidth;
}
// update the zoom container's translate values
// based on the original + delta, capped to the
// container's width and height.
$("#zoom-container").css("transform", "translate(" + (2*translateX) + "px, " + (2*translateY) + "px) scale(2)");
// keep track of the updated values for the next
// touchmove event.
window.lastTranslateX = translateX;
window.lastTranslateY = translateY;
};
// Drag start -- touch version
$("#container").on("touchstart", function(event) {
if (!window.zoomedIn) {
return true;
}
var xCursor = event.originalEvent.changedTouches[0].clientX;
var yCursor = event.originalEvent.changedTouches[0].clientY;
handleDragStart(this, xCursor, yCursor);
});
// Drag start -- mouse version
$("#container").on("mousedown", function(event) {
if (!window.zoomedIn) {
return true;
}
var xCursor = event.clientX;
var yCursor = event.clientY;
handleDragStart(this, xCursor, yCursor);
});
// Drag end -- touch version
$("#inner-container").on("touchend", function(event) {
if (!window.zoomedIn) {
return true;
}
handleDragEnd();
});
// Drag end -- mouse version
$("#inner-container").on("mouseup", function(event) {
if (!window.zoomedIn) {
return true;
}
handleDragEnd();
});
// Drag move -- touch version
$("#inner-container").on("touchmove", function(event) {
// prevent pull-to-refresh. could be smarter by checking
// if the page's scroll y-offset is 0, and even smarter
// by checking if we're pulling down, not up.
event.preventDefault();
if (!window.zoomedIn) {
return true;
}
var xCursor = event.originalEvent.changedTouches[0].clientX;
var yCursor = event.originalEvent.changedTouches[0].clientY;
handleDragMove(xCursor, yCursor);
});
// Drag move -- click version
$("#inner-container").on("mousemove", function(event) {
// prevent pull-to-refresh. could be smarter by checking
// if the page's scroll y-offset is 0, and even smarter
// by checking if we're pulling down, not up.
event.preventDefault();
// if we aren't dragging from anywhere, don't move
if (!window.zoomedIn || !window.dragStartX) {
return true;
}
var xCursor = event.clientX;
var yCursor = event.clientY;
handleDragMove(xCursor, yCursor);
});
var zoomInTo = function(element) {
console.log("applying zoom");
var top = element.offsetTop;
// the subtracted value here is to keep the letter in the center
var left = element.offsetLeft - (0.25 * $("#inner-container")[0].clientWidth);
var translateY = 0.5 * $("#zoom-container")[0].clientHeight - top;
var translateX = 0.5 * $("#zoom-container")[0].clientWidth - left;
$("#container").css("overflow", "scroll");
$("#zoom-container").css("transform", "translate(" + (2*translateX) + "px, " + (2*translateY) + "px) scale(2)");
window.translateY = translateY;
window.translateX = translateX;
window.zoomedIn = true;
}
var zoomOut = function() {
console.log("resetting zoom");
window.zoomedIn = false;
$("#zoom-container").css("transform", "");
$("#zoom-container").css("transition", "");
window.dragStartX = null;
window.dragStartY = null;
window.dragMoveJustHappened = null;
window.translateY = window.lastTranslateY;
window.translateX = window.lastTranslateX;
window.lastTranslateX = null;
window.lastTranslateY = null;
}
$(".box").click(function(event) {
var element = this;
var zoomContainer = $("#zoom-container");
if (!window.zoomedIn) {
zoomInTo(element);
}
});
$("#zoom-out").click(function(event) {
zoomOut();
});
CSS:
* {
margin: 0;
}
body,
html {
height: 100%;
}
#container {
height: 100%;
width: 50%;
margin: 0 auto;
}
#inner-container {
width: 100%;
height: 100%;
}
#zoom-container {
height: 100%;
width: 100%;
transition: transform 0.2s ease-in-out;
}
.box {
float: left;
width: 50%;
height: 50%;
color: white;
text-align: center;
display: block;
}
.red {
background: red;
}
.blue {
background: blue;
}
.green {
background: green;
}
.black {
background: black;
}
I pieced this together from another question (Move (drag/pan) and zoom object (image or div) in pure js), where the width and height are being changed. That doesn't quite apply in my case, because I need to zoom into a specific element on the page (with a lot boxes than in a 2x2 grid). The solution from that question (https://jsfiddle.net/j8kLz6wm/1/) shows the basic approach in pure JavaScript. If you have jQuery available, you can probably just use jquery.panzoom.
Update
I got stuck on scroll bars not showing all the time, so I need to investigating that part, so that code is commented out and instead I use a delay to move the clicked box into view.
Here is my fiddle demo, which I use to play with, to figure out how to solve the scroll bar issue.
Side note: In a comment made by #AVAVT, I would like to link to his post here, as that might help someone else, which I find as an interesting alternative in some cases.
(function(zoomed) {
$(".box").click(function(event) {
var el = this, elp = el.parentElement;
if (zoomed) {
zoomed = false;
$("#zoom-container").css({'transform': ''});
} else {
zoomed = true;
/* this zooms correct but show 1 or none scroll for B,C,D so need to figure out why
var tro = (Math.abs(elp.offsetTop - el.offsetTop) > 0) ? 'bottom' : 'top';
tro += (Math.abs(elp.offsetLeft - el.offsetLeft) > 0) ? ' right' : ' left';
$("#zoom-container").css({'transform-origin': tro, 'transform': 'scale(2)'});
*/
$("#zoom-container").css({'transform-origin': '0 0', 'transform': 'scale(2)'});
/* delay needed before scroll into view */
setTimeout(function() {
el.scrollIntoView();
},250);
}
});
})();
* { margin: 0; }
body, html { height: 100%; }
#container {
height: 100%;
width: 50%;
overflow: auto;
margin: 0 auto;
}
#zoom-container {
height: 100%;
width: 100%;
transition: all 0.2s ease-in-out;
}
.box {
float: left;
width: 50%;
height: 50%;
color: white;
text-align: center;
display: block;
}
.red {
background: red;
}
.blue {
background: blue;
}
.green {
background: green;
}
.black {
background: black;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<div id="container">
<div id="zoom-container">
<div class="box red">A</div>
<div class="box blue">B</div>
<div class="box green">C</div>
<div class="box black">D</div>
</div>
</div>

Modifying cursor property for select portion of an element

I have this HTML that renders a simple arrow sign pointing towards the right:
<!DOCTYPE html>
<html>
<head>
<style>
div { width: 0px; height: 0px; border-left: 20px solid black; border-top: 20px solid transparent; border-bottom: 20px solid transparent; border-right: 20px solid transparent; position: absolute; left: 35px; top: 53px; cursor: pointer; }
</style>
<body>
<div></div>
</body>
</html>
If you hover of it, the cursor turns to pointer. But because it is actually a square div, the cursor turns pointer even if you are just outside the arrow within the perimeter of the div.
So I wrote this Javascript addition such that the cursor turns pointer only when the mouse is hovering over that arrow. For this purpose, I figured the coordinates of the three vertices of the triangle from Firebug ((35,53),(55,73),(35,93) clockwise from top). Then I check whether the point in question lies inside the triangle formed by these 3 vertices. This I do by checking whether the point and the opposite vertex for each edge lies on the same side of that edge or not (if they do, the product of the values obtained by substituting the coordinates of that point for x and y in that equation will be positive).
<!DOCTYPE html>
<html>
<head>
<style>
div { width: 0px; height: 0px; border-left: 20px solid black; border-top: 20px solid transparent; border-bottom: 20px solid transparent; border-right: 20px solid transparent; position: absolute; left: 35px; top: 53px; }
.hoverclass { cursor: pointer; }
</style>
<script src="jquery.js">
</script>
<script>
$(document).ready(function(){
$("div").click(function(e) { alert(e.pageX + " " + e.pageY); });
function l1(x,y) { return y - x - 18; }
function l2(x,y) { return x+y-128; }
function l3(x,y) { return x-35; }
$("div").hover(function(e) {
var x = e.pageX;
var y = e.pageY;
if (l1(x,y)*l1(35,93) >= 0 && l1(x,y)*l1(35,93) >= 0 && l1(x,y)*l1(35,93) >= 0 ) {
$(this).addClass('hoverclass');
}
else { $(this).removeClass('hoverclass'); }
},
function() {
$(this).removeClass('hoverclass');
});
});
</script>
<body>
<div></div>
</body>
</html>
However, the results are not predictable. Sometimes the cursor turns pointer within the triangle only, sometimes outside as well (just as before), and sometimes not at all. I suspect that this is probably due to the hover function working overtime, that may be temporarily hanging the script. Is there any other way to achieve this?
This could be done using HTML5 canvas. Basic idea is to check for pixel color on mousemove on canvas element. This way, your element can be of any form as you wish. Of course, you should make some optimization of following code:
SEE WORKING DEMO
function findPos(obj) {
var curleft = 0, curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return { x: curleft, y: curtop };
}
return undefined;
}
// set up triangle
var example = document.getElementById('example');
var context = example.getContext('2d');
context.fillStyle = '#000';
context.strokeStyle = '#f00';
context.lineWidth = 1;
context.beginPath();
// Start from the top-left point.
context.moveTo(10, 10); // give the (x,y) coordinates
context.lineTo(60, 60);
context.lineTo(10, 120);
context.lineTo(10, 10);
// Done! Now fill the shape, and draw the stroke.
// Note: your shape will not be visible until you call any of the two methods.
context.fill();
context.stroke();
context.closePath();
$('#example').mousemove(function(e) {
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
var coord = "x=" + x + ", y=" + y;
var c = this.getContext('2d');
var p = c.getImageData(x, y, 1, 1).data;
if(p[3]!='0') $(this).css({cursor:'pointer'});
else $(this).css({cursor:'default'});
});
You'd better use CSS instead. With :before and :after pseudo classes you can do magic. Check out this Pure CSS GUI icons by Nicolas Gallagher.
If you use any CSS pre-processor, these icons can be wrapped up as a mixin, this way required properties can be assigned like this:
#icon > .close(16px, #fff, #E83921);
You can make any shape have cursor pointer with CSS only. The idea is to rotate wrapper container which has overflow: hidden (you can have several of them depending on the shape you need). In case of OP problem this code does a trick:
<div class="arrow"><i></i></div>
.arrow {
margin: 100px;
border_: 1px red solid;
width: 40px;
height: 40px;
overflow: hidden;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.arrow i {
height: 65px;
width: 65px;
background-color: green;
content: '';
display: block;
cursor: pointer;
margin: -35px 0 0 11px;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
See this demo: http://cssdesk.com/PaB5n
True that this requires CSS transform support so it's not cross browser.

Categories