I am doing this project for practice and I am achieving half the results but can't figure out how to the other half. Basically I have div1 with 0 width and 0 height inside a parent div2 with 280px width and height each with border radius of 50%. There is a sibling div3 of div1 with value inside. On clicking the div2 the div3 text converted into seconds and then set interval will run a function that increases the width and height proportionate to the seconds such that when seconds reach 0 height and width of div1 should be equal to parent div2. I am achieving this far and now I want to reassign seconds = 70 and decrease the width and height of div1 proportionally. My explanation might not be enough so please refer to the code snippet attached. Thanks for your help in advance.
var display, time, loader, width, height, reverse, seconds, running, interval;
display = document.getElementById("display");
time = document.getElementById("time");
loader = document.getElementById("loader");
width = 0;
height = 0;
reverse = false;
running = false;
seconds = parseInt(time.textContent)*60;
if ( width === 280 && height === 280 ) {
reverse = true;
time.textContent = 70;
}
display.addEventListener("click" , function(){
if ( running === false ) {
running = true;
interval = setInterval(function(){
if ( reverse === false ) {
width += (280-width)/seconds;
height += (280-height)/seconds;
}
else {
width -= (width/seconds);
height -= (height/seconds);
}
loader.style.width = Math.round(width) + "px";
loader.style.height = Math.round(height) + "px";
console.log(width + " " + height);
time.textContent = seconds;
if ( seconds > 0 ) {
seconds--;
}
},1000);
}
else {
running = false;
clearInterval(interval);
console.log("Inteval cleared");
}
});
#display {
width : 280px;
height: 280px;
border: 1px solid #5fcf80;
border-radius: 50%;
position: relative;
overflow: hidden;
font-family: sans-serif;
cursor: pointer;
}
#time {
font-size: 120px;
text-align: center;
line-height: 280px;
position: relative;
z-index: 9;
}
#loader {
z-index: 1;
width: 40px;
height: 40px;
background: #5fcf80;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div id="display">
<div id="time">1</div>
<div id="loader"></div>
</div>
</body>
</html>
There are some mistakes in your code:
1- The following if clause is useless because its contents never executes:
if ( width >= 280 && height >= 280 ) {
reverse = true;
time.textContent = 70;
}
It never executes because this if is outside addEventListener and setInterval. It will run one time at the beginning, when width and height are still 0.
2- The following code should be at the beginning of setInterval:
if ( seconds > 0 ) {
seconds--;
The reason is, imagine what happens when seconds reaches 0:
These lines will be executed: width += (280-width)/seconds and height += (280-height)/seconds.
seconds is 0, so a division by zero will happen!
width and height will be NaN (Not a Number).
3- The code in (2) should be:
if ( seconds > 1 ) {
seconds--;
} else if ( seconds == 1 ){
reverse = reverse ? false : true;
seconds = 70;
}
first, we check if seconds is above 1 not 0, to stop seconds from being 0, and in that case, we decrease by 1.
If seconds is 1, then we've reached the max (or min) width and height.
In this case, if we are increasing, we will reverse, and vice versa.
In addition, we will reset seconds to 70 based on your specification.
4- Based on the above changes, the initial value of seconds should be 61, not 60:
seconds = parseInt(time.textContent)*61;
That's because we will decrease seconds right away in the beginning of addEventListener.
Here is a working modified version on JSFiddle
Related
I want to show loading progress bar till the page load. If the internet might be slow and page take more to load, the progress bar show till the page fully load.
I attempted to add code, but because internet speeds vary, it is inaccurate. Could you please help me with this? I want to add a progress bar that starts at 0% while the page is loading and goes up to 100% after the page is completely loaded, dependent on the page loading speed.
$(window).on('load', function() {
$('#preloader').fadeOut(500);
$('body').removeClass('pre_loader');
});
var width = 100,
perfData = window.performance.timing, // The PerformanceTiming interface represents timing-related performance information for the given page.
EstimatedTime = -(perfData.loadEventEnd - perfData.navigationStart),
time = parseInt((EstimatedTime / 1000) % 60) * 100;
// Loadbar Animation
$(".loadbar").animate({
width: width + "%"
}, time);
// Percentage Increment Animation
function animateValue(id, start, end, duration) {
var range = end - start,
current = start,
increment = end > start ? 1 : -1,
stepTime = Math.abs(Math.floor(duration / range)),
obj = $(id);
var timer = setInterval(function() {
current += increment;
$(obj).text(current + "%");
//obj.innerHTML = current;
if (current == end) {
clearInterval(timer);
}
}, stepTime);
}
// Fading Out Loadbar on Finised
setTimeout(function() {
$('.preloader-wrap').fadeOut(100);
}, time);
<div class="preloader-wrap">
<div class="loader">
<div class="trackbar">
<div class="loadbar">
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
Sound like CSS (only) animation would be an option to consider. Inline your progress bar and it's <style> first thing as the page loads. Then remove then and make the body visible again on page load event. You can cheat time if you use some easing function that will never finish.
If you need numbers in your progress bar, then there is an options for that; even by a variable in modern browsers https://css-tricks.com/animating-number-counters/
For example (need to play with the percent values a bit):
<!-- almost first thing on page -->
<style>
.container {
width: 400px;
height: 50px;
position: relative;
border: 1px solid black;
}
.progress {
background: blue;
float: left;
color: white;
width: 100%;
height: 50px;
line-height: 50px;
animation-name: slideInFromLeft;
animation-duration: 30s;
animation-timing-function: cubic-bezier(0, .9, .9, .999);
text-align: center;
}
.percent::before {
content: counter(count);
animation-name: counter;
animation-duration: 30s;
animation-timing-function: cubic-bezier(0, .9, .9, .999);
counter-reset: count 0;
}
#keyframes slideInFromLeft {
0% {
width: 0%;
}
99% {
width: 99%;
}
}
#keyframes counter {
0% {
counter-increment: count 0;
}
10% {
counter-increment: count 50;
}
20% {
counter-increment: count 60;
}
30% {
counter-increment: count 70;
}
40% {
counter-increment: count 80;
}
50% {
counter-increment: count 90;
}
60% {
counter-increment: count 95;
}
70% {
counter-increment: count 98;
}
80% {
counter-increment: count 99;
}
90% {
counter-increment: count 90;
}
100% {
counter-increment: count 100;
}
}
</style>
<div class="container">
<div class="progress">
<span class="percent">%</span>
</div>
</div>
I have a red container which initially is at bottom right of black container. I have a scale function that gradually scales the container. I want to make the bottom right position of red container to be fixed and scale it towards top left. How can I do that?
const box = document.getElementById("box")
const initHeight = 200
const initWidth = 200
const centerX = initWidth / 2
const centerY = initHeight / 2
function transform(scale, translate) {
if (translate) {
translate[0] = -centerX + translate[0]
translate[1] = -centerY + translate[1]
}
box.style.transform = `scale(${scale})${
translate ? ` translate(${translate.map((x) => x + "px").toString()})` : ""
}`
}
let initX = initWidth
let initY = initHeight
let scaleVal = 0.5
transform(scaleVal, [initX, initY])
function scale() {
scaleVal = scaleVal + 0.01
transform(scaleVal, [
initX - scaleVal * initWidth,
initY - scaleVal * initHeight
])
if (scaleVal <= 1) {
setTimeout(() => {
requestAnimationFrame(scale)
}, 50)
}
}
scale()
* {
box-sizing: border-box;
}
.box {
height: 200px;
width: 200px;
background-color: black;
position: absolute;
}
.box:nth-child(2) {
background-color: red;
}
<div id="app">
<div class="box"></div>
<div class="box" id="box"></div>
</div>
To lock the bottom right corner of the red box to the bottom right of the black box this snippet does two things: positions red box right bottom relative to the parent app container and sets the transform-origin to that spot too (normally transform origin is at the center of an element). It then uses a CSS animation to expand the red box and contract it again using scale.
This method does not need JS as it is a simple scaling transform, but of course some of the subtleties of the original transformations are lost because of tying the corner down.
* {
box-sizing: border-box;
}
body {
position: relative;
width: 100vw;
height: 100vh;
}
#app {
position: absolute;
height: 200px;
width: 200px;
}
.box:nth-child(1) {
height: 200px;
width: 200px;
background-color: black;
position: absolute;
top: 0;
left: 0;
}
#box {
background-color: red;
width: 100px;
height: 100px;
position: absolute;
bottom: 0;
right: 0;
transform-origin: right bottom;
animation: scale 5s 1 ease-in-out;
}
#keyframes scale {
0% {
transform: scale(1);
}
50% {
transform: scale(2);
}
100% {
transform: scale(1);
}
}
<div id="app">
<div class="box"></div>
<div class="box" id="box"></div>
</div>
Okay so I finally figured it out,
const box = document.getElementById("box")
let scale = 0
const initWidth = 50
const initHeight = 50
function fixed(num, fix = 1) {
return Number(parseFloat(num).toFixed(fix))
}
function scaleBox() {
const [x, y] = [
fixed((initWidth - scale * initWidth) / 2),
fixed((initHeight - scale * initHeight) / 2)
]
box.style.transform = `translate(${x}px, ${y}px) scale(${scale})`
scale = scale + 0.1
if (scale < 1) {
setTimeout(() => {
requestAnimationFrame(scaleBox)
}, 500)
}
}
scaleBox()
* {
box-sizing: border-box;
}
.box {
height: 50px;
width: 50px;
background-color: black;
position: absolute;
}
.box:nth-child(2) {
background-color: red;
transform: translate(0, 0) scale(0);
}
<div id="app">
<div class="box"></div>
<div class="box" id="box"></div>
</div>
Explanation
The trick is to translate the container in such a way that when its scaled after the translation, it always places itself in the bottom right of purple container.
To figure out the translation amount, let's first scale the container to 0.5 without any translation. It looks like this,
As you can see the container's width is 25 as 0.5(scale) * 50(init_width)=25 and position from container from all sides(top left, bottom left, top right, bottom right) will be (25/2, 25/2)=(12.5,12.5) since the container is scaled equally from all sides.
Since the position from bottom right is (12.5,12.5), we need to translate the container to (+12.5,+12.5) and then scale it to exactly place it at bottom right.
You can achieve many things using display:flex, it's great!
This is how I would approach your problem:
const handleClick = () => {
const blackDiv = document.getElementById("black-div");
const redDiv = document.getElementById("red-div");
let widthRatio = 0;
let heightRatio = 0;
const scaleUpTimer = setInterval(() => {
if (widthRatio === 1 || heightRatio === 1) clearInterval(scaleUpTimer);
widthRatio = redDiv.offsetWidth / blackDiv.offsetWidth;
heightRatio = redDiv.offsetHeight / blackDiv.offsetHeight;
redDiv.style.width = widthRatio * 100 + 2 + "%";
redDiv.style.height = heightRatio * 100 + 2 + "%";
}, 10);
};
#black-div {
width: 200px;
height: 200px;
background-color: rgb(0, 0, 0);
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
#red-div {
background-color: red;
width: 50%;
height: 50%;
}
<div id='black-div'>
<div id='red-div' onclick={handleClick()}></div>
</div>
EDIT: I used onclick here but obviously you would have to handle the situations where someone clicks the red square and its already scaled up to avoid setting unnecessary timers. Or you could just call the function directly, without having to click anything.
I'm attempting to mimic the following widget with HTML/CSS/JavaScript:
https://gyazo.com/76bee875d35b571bd08edbe73ead12cb
The way that I have it set up is the following:
I have a bar with a background color that has a gradient from red to green which is static.
I then have two blinders that is supposed to represent the negative space to give the illusion that the colored bars are animating (in reality, the blinders are simply sliding away)
I did it this way because I figured it might be easier instead of trying to animate the bar going in both directions, but now I'm not so sure lol. One requirement that I'm trying to keep is that the animation only deals with transform or opacity to take advantage of optimizations the browser can do (as described here: https://hacks.mozilla.org/2016/08/animating-like-you-just-dont-care-with-element-animate/)
The example has a few buttons to help test various things. The "Random positive" works great, and is exactly what I want. I haven't quite hooked up the negative yet tho because I'm not sure how to approach the problem of transitioning from positive to negative and vice-versa.
Ideally, when going from a positive to a negative, the right blinder will finish at the middle, and the left blinder will pick up the animation and finish off where it needs to go.
So for example, if the values is initially set to 40%, and the then set to -30%, the right blinder should animate transform: translateX(40%) -> transform: translateX(0%) and then the left blinder should animate from transform: translateX(0%) -> transform: translateX(-30%) to expose the red.
Also, the easing should be seamless.
I'm not sure if this is possible with the setup (specifically keeping the easing seamless, since the easing would be per-element, I think, and can't "carry over" to another element?)
Looking for guidance on how I can salvage this to produce the expected results, or if there's a better way to deal with this.
Note: I'm using jquery simply for ease with click events and whatnot, but this will eventually be in an application that's not jquery aware.
Here's my current attempt: https://codepen.io/blitzmann/pen/vYLrqEW
let currentPercentageState = 0;
function animate(percentage) {
var animation = [{
transform: `translateX(${currentPercentageState}%)`,
easing: "ease-out"
},
{
transform: `translateX(${percentage}%)`
}
];
var timing = {
fill: "forwards",
duration: 1000
};
$(".blind.right")[0].animate(animation, timing);
// save the new value so that the next iteration has a proper from keyframe
currentPercentageState = percentage;
}
$(document).ready(function() {
$(".apply").click(function() {
animate($("#amount").val());
});
$(".reset").click(function() {
animate(0);
});
$(".random").click(function() {
var val = (Math.random() * 2 - 1) * 100;
$("#amount").val(val);
animate(val);
});
$(".randomPos").click(function() {
var val = Math.random() * 100;
$("#amount").val(val);
animate(val);
});
$(".randomNeg").click(function() {
var val = Math.random() * -100;
$("#amount").val(val);
animate(val);
});
$(".toggleBlinds").click(function() {
$(".blind").toggle();
});
$(".toggleLeft").click(function() {
$(".blind.left").toggle();
});
$(".toggleRight").click(function() {
$(".blind.right").toggle();
});
});
$(document).ready(function() {});
.wrapper {
margin: 10px;
height: 10px;
width: 800px;
background: linear-gradient(to right, red 50%, green 50%);
border: 1px solid black;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.blind {
height: 100%;
position: absolute;
top: 0;
background-color: rgb(51, 51, 51);
min-width: 50%;
}
.blind.right {
left: 50%;
border-left: 1px solid white;
transform-origin: left top;
}
.blind.left {
border-right: 1px solid white;
transform-origin: left top;
}
<div class="wrapper">
<div class='blind right'></div>
<div class='blind left'></div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js" type="text/javascript"></script>
<input id="amount" type="number" placeholder="Enter percentage..." value='40' />
<button class="apply">Apply</button>
<button class="random">Random</button>
<button class="randomPos">Random Positive</button>
<button class="randomNeg">Random Negative</button>
<button class="toggleBlinds">Toggle Blinds</button>
<button class="toggleLeft">Toggle L Blind</button>
<button class="toggleRight">Toggle R Blind</button>
<button class="reset" href="#">Reset</button>
I've modified your code. Have a look at the code.
let currentPercentageState = 0;
function animate(percentage) {
var animation = [{
transform: `translateX(${currentPercentageState}%)`,
easing: "ease-out"
},
{
transform: `translateX(${percentage}%)`
}
];
var timing = {
fill: "forwards",
duration: 1000
};
if (percentage < 0) {
$(".blind.right")[0].animate(
[{
transform: `translateX(0%)`,
easing: "ease-out"
},
{
transform: `translateX(0%)`
}
], timing);
$(".blind.left")[0].animate(animation, timing);
} else {
$(".blind.left")[0].animate(
[{
transform: `translateX(0%)`,
easing: "ease-out"
},
{
transform: `translateX(0%)`
}
], timing);
$(".blind.right")[0].animate(animation, timing);
}
// save the new value so that the next iteration has a proper from keyframe
//currentPercentageState = percentage;
}
$(document).ready(function() {
$(".apply").click(function() {
animate($("#amount").val());
});
$(".reset").click(function() {
animate(0);
});
$(".random").click(function() {
var val = (Math.random() * 2 - 1) * 100;
$("#amount").val(val);
animate(val);
});
$(".randomPos").click(function() {
var val = Math.random() * 100;
$("#amount").val(val);
animate(val);
});
$(".randomNeg").click(function() {
var val = Math.random() * -100;
$("#amount").val(val);
animate(val);
});
$(".toggleBlinds").click(function() {
$(".blind").toggle();
});
$(".toggleLeft").click(function() {
$(".blind.left").toggle();
});
$(".toggleRight").click(function() {
$(".blind.right").toggle();
});
});
$(document).ready(function() {});
.wrapper {
margin: 10px;
height: 10px;
width: 800px;
background: linear-gradient(to right, red 50%, green 50%);
border: 1px solid black;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.blind {
height: 100%;
position: absolute;
top: 0;
background-color: rgb(51, 51, 51);
min-width: 50%;
}
.blind.right {
left: 50%;
border-left: 1px solid white;
transform-origin: left top;
}
.blind.left {
border-right: 1px solid white;
transform-origin: left top;
}
<div class="wrapper">
<div class='blind right'></div>
<div class='blind left'></div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js" type="text/javascript"></script>
<input id="amount" type="number" placeholder="Enter percentage..." value='40' />
<button class="apply">Apply</button>
<button class="random">Random</button>
<button class="randomPos">Random Positive</button>
<button class="randomNeg">Random Negative</button>
<button class="toggleBlinds">Toggle Blinds</button>
<button class="toggleLeft">Toggle L Blind</button>
<button class="toggleRight">Toggle R Blind</button>
<button class="reset" href="#">Reset</button>
You need to animate the things in two steps. The first step is to reset the previous state to initial state(which should be set to 0) and in the second step, you need to run the other animation which will actually move it to the destination state.
In order to achive this you can do,
let currentPercentageState = 0;
const animationTiming = 300;
function animate(percentage) {
let defaultTranformVal = [{
transform: `translateX(${currentPercentageState}%)`,
easing: "ease-out"
}, {transform: `translateX(0%)`}];
var animation = [{
transform: `translateX(0%)`,
easing: "ease-out"
},{
transform: `translateX(${percentage}%)`,
easing: "ease-out"
}];
var timing = {
fill: "forwards",
duration: animationTiming
};
if (percentage < 0) {
if(currentPercentageState > 0) {
$(".blind.right")[0].animate(defaultTranformVal, timing);
setTimeout(() => {
$(".blind.left")[0].animate(animation, timing);
}, animationTiming);
} else {
$(".blind.left")[0].animate(animation, timing);
}
}
if(percentage > 0) {
if(currentPercentageState < 0) {
$(".blind.left")[0].animate(defaultTranformVal, timing);
setTimeout(() => {
$(".blind.right")[0].animate(animation, timing);
}, animationTiming);
} else {
$(".blind.right")[0].animate(animation, timing);
}
}
// save the new value so that the next iteration has a proper from keyframe
currentPercentageState = percentage;
}
Here, you will see we have two transformations. The first one defaultTranformVal will move the currentPercentageState to zero and then the other one which will move from 0 to percentage.
You need to handle a couple of conditions here. The first one is if you are running it the first time(means there is no currentPercentageState), you don't need to run defaultTranformVal. If you have currentPercentageState then you need to run defaultTranformVal and then run the second animation.
Note:- You also need to clear the timeout in order to prevent the memory leak. This can be handle by storing the setTimout return value and then when next time it's running clear the previous one with the help of clearTimeout.
Here is the updated codepen example:-
https://codepen.io/gauravsoni119/pen/yLeZBmb?editors=0011
EDIT: I actually did manage to solve this!
let easing = "cubic-bezier(0.5, 1, 0.89, 1)";
let duration = 1000;
let easeReversal = y => 1 - Math.sqrt((y-1)/-1)
https://codepen.io/blitzmann/pen/WNrBWpG
I gave it my own cubic-bezier function of which I know the reversal for. The post below and my explanation was based on an easing function using sin() which isn't easily reversible. Not only that, but the built in easing function for ease-out doesn't match the sin() one that I had a reference for (I'm not really sure what the build in one is based on). But I realized I could give it my own function that I knew the reversal for, and boom, works like a charm!
This has been a very informative experience for me, I'm glad that I've got a solution that works. I still think I'll dip my toes in the other ideas that I had to see which pans out better in the long term.
Historical post:
So, after a few nights of banging my head around on this, I've come to the conclusion that this either isn't possible the way I was thinking about doing it, or if it is possible then the solution is so contrived that it's probably not worth it and I'd be better off developing a new solution (of which I've thought of one or tow things that I'd like to try).
Please see this jsfiddle for my final "solution" and a post-mortem
https://jsfiddle.net/blitzmann/zc80p1n4/
let currentPercentageState = 0;
let easing = "linear";
let duration = 1000;
function animate(percentage) {
percentage = parseFloat(percentage);
// determine if we've crossed the 0 threshold, which would force us to do something else here
let threshold = currentPercentageState / percentage < 0;
console.log("Crosses 0: " + threshold);
if (!threshold && percentage != 0) {
// determine which blind we're animating
let blind = percentage < 0 ? "left" : "right";
$(`.blind.${blind}`)[0].animate(
[
{
transform: `translateX(${currentPercentageState}%)`,
easing: easing
},
{
transform: `translateX(${percentage}%)`
}
],
{
fill: "forwards",
duration: duration
}
);
} else {
// this happens when we cross the 0 boundry
// we'll have to create two animations - one for moving the currently offset blind back to 0, and then another to move the second blind
let firstBlind = percentage < 0 ? "right" : "left";
let secondBlind = percentage < 0 ? "left" : "right";
// get total travel distance
let delta = currentPercentageState - percentage;
// find the percentage of that travel that the first blind is responsible for
let firstTravel = currentPercentageState / delta;
let secondTravel = 1 - firstTravel;
console.log("delta; total values to travel: ", delta);
console.log(
"firstTravel; percentage of the total travel that should be done by the first blind: ",
firstTravel
);
console.log(
"secondTravel; percentage of the total travel that should be done by the second blind: ",
secondTravel
);
// animate the first blind.
$(`.blind.${firstBlind}`)[0].animate(
[
{
transform: `translateX(${currentPercentageState}%)`,
easing: easing
},
{
// we go towards the target value instead of 0 since we'll cut the animation short
transform: `translateX(${percentage}%)`
}
],
{
fill: "forwards",
duration: duration,
// cut the animation short, this should run the animation to this x value of the easing function
iterations: firstTravel
}
);
// animate the second blind
$(`.blind.${secondBlind}`)[0].animate(
[
{
transform: `translateX(${currentPercentageState}%)`,
easing: easing
},
{
transform: `translateX(${percentage}%)`
}
],
{
fill: "forwards",
duration: duration,
// start the iteration where the first should have left off. This should put up where the easing function left off
iterationStart: firstTravel,
// we only need to carry this aniamtion the rest of the way
iterations: 1-firstTravel,
// delay this animation until the first "meets" it
delay: duration * firstTravel
}
);
}
// save the new value so that the next iteration has a proper from keyframe
currentPercentageState = percentage;
}
// the following are just binding set ups for the buttons
$(document).ready(function () {
$(".apply").click(function () {
animate($("#amount").val());
});
$(".reset").click(function () {
animate(0);
});
$(".random").click(function () {
var val = (Math.random() * 2 - 1) * 100;
$("#amount").val(val);
animate(val);
});
$(".randomPos").click(function () {
var val = Math.random() * 100;
$("#amount").val(val);
animate(val);
});
$(".randomNeg").click(function () {
var val = Math.random() * -100;
$("#amount").val(val);
animate(val);
});
$(".flipSign").click(function () {
animate(currentPercentageState * -1);
});
$(".toggleBlinds").click(function () {
$(".blind").toggle();
});
$(".toggleLeft").click(function () {
$(".blind.left").toggle();
});
$(".toggleRight").click(function () {
$(".blind.right").toggle();
});
});
animate(50);
//setTimeout(()=>animate(-100), 1050)
$(function () {
// Build "dynamic" rulers by adding items
$(".ruler[data-items]").each(function () {
var ruler = $(this).empty(),
len = Number(ruler.attr("data-items")) || 0,
item = $(document.createElement("li")),
i;
for (i = -11; i < len - 11; i++) {
ruler.append(item.clone().text(i + 1));
}
});
// Change the spacing programatically
function changeRulerSpacing(spacing) {
$(".ruler")
.css("padding-right", spacing)
.find("li")
.css("padding-left", spacing);
}
changeRulerSpacing("30px");
});
.wrapper {
margin: 10px auto 2px;
height: 10px;
width: 600px;
background: linear-gradient(to right, red 50%, green 50%);
border: 1px solid black;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.blind {
height: 100%;
position: absolute;
top: 0;
background-color: rgb(51, 51, 51);
min-width: 50%;
}
.blind.right {
left: 50%;
border-left: 1px solid white;
transform-origin: left top;
}
.blind.left {
border-right: 1px solid white;
transform-origin: left top;
}
#buttons {
text-align: center;
}
/* Ruler crap */
.ruler-container {
text-align: center;
}
.ruler, .ruler li {
margin: 0;
padding: 0;
list-style: none;
display: inline-block;
}
/* IE6-7 Fix */
.ruler, .ruler li {
*display: inline;
}
.ruler {
display:inline-block;
margin: 0 auto;https://jsfiddle.net/user/login/
background: lightYellow;
box-shadow: 0 -1px 1em hsl(60, 60%, 84%) inset;
border-radius: 2px;
border: 1px solid #ccc;
color: #ccc;
height: 3em;
padding-right: 1cm;
white-space: nowrap;
margin-left: 1px;
}
.ruler li {
padding-left: 1cm;
width: 2em;
margin: .64em -1em -.64em;
text-align: center;
position: relative;
text-shadow: 1px 1px hsl(60, 60%, 84%);
}
.ruler li:before {
content: '';
position: absolute;
border-left: 1px solid #ccc;
height: .64em;
top: -.64em;
right: 1em;
}
<div class="wrapper">
<div class='blind right'></div>
<div class='blind left'></div>
</div>
<div class="ruler-container">
<ul class="ruler" data-items="21"></ul>
</div>
<div id="buttons">
<input id="amount" type="number" placeholder="Enter percentage..." value='-80' />
<button class="apply">Apply</button>
<button class="random">Random</button>
<button class="randomPos">Random Positive</button>
<button class="randomNeg">Random Negative</button>
<button class="flipSign">Flip Sign</button>
<button class="toggleBlinds">Toggle Blinds</button>
<button class="toggleLeft">Toggle L Blind</button>
<button class="toggleRight">Toggle R Blind</button>
<button class="reset" href="#">Reset</button>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js" type="text/javascript"></script>
<hr />
<p><strong>A note</strong> on the attempt made here:</p>
<p>
I was trying to animate a percentage bar that has both positive and negative values. But I set a challenge as well: I wanted to achieve this via animations utilizing only the compositor - which means animating opacity or transform <strong>only</strong> (no color, width, height, position, etc). The ideas presented here were based on the concept of blinds. I have a static element with a background gradient of red to green, then I have two elements that "blind" the user to the background. These blinds, being a simple element, simply slide into and out of place.
</p>
<p>The problem that I ran into was timing the two animations correctly when they switched signage. It's currently working (very well) for linear animation, but as soon as you introduce an easing function it gets wonky. The reason for this is due to the value that I'm using to set the first animation length (iteration, not duration), as well as the second animations start to pick up where the first left off. The value that I was using is the percentage of the total travel distance that each of the blinds will have to do.</p>
<p>So, for example, if you have a value of 50, and go to -80, that's a total travel distance of 130. The first blind travels <code>50 / 130 = ~0.3846</code> of the total distance, and the second blind will travel <code>1 - ~0.3846 = ~0.6154</code> of the total distance.</p>
<p>But, these are not the correct values for the <em>duration</em> of the animation. Instead, these are the percentages of the easing values (the y-axis). To get the duration for these, I would have to find the x value (given the known y value). eg, for an ease-out animation for a value going from 50 to -80, the animation crosses our 0 at ~0.03846, and we would have to solve for x given <code>0.03846 = sin((x * PI) / 2)</code>.</p>
<p>With the help of Wolfram Alpha, I was able to find a few test values this got me much closer to the actual animation, but the blinds always stopped slightly off the mark. I eventually chalked this up to one of two reasons: the fact that the valuess are always going to be approximate and the browser is never going to be 100% accurate, or / and 2) the browser is using a slightly different easing function than I was using for reference. Regardless, being so constrained by the fact that this "animation" relies on two different aniamtions lining up perfectly, I decided to leave this version be and go in a different direction.</p>
<p>
If anyone finds an actual solution to this, please post an answer here: https://stackoverflow.com/questions/62866844/how-to-animate-a-progress-bar-with-negatives-using-element-animate
</p>
Thanks to those that attempted this admittedly tricky problem
I tried to create a HTML5 video player from scratch. But I couldn't create a seekbar that slide nicely like vimeo(or youtube).
What I did is I added a extra div/span(that highlights the total viewed time) in the seekbar. Then I added mousedown & mousemove event.
When mousedown event get called I make a var(Boolean) true. While it is true and I change the width of the div/span according to the mousemove position.
It works, but It's unresponsive. I can't slide it seamlessly. So I wanted to know how can build a responsive seekbar like vimeo(youtube is same but youtube player has a dot like a range slider, but I don't want that). I prefer pure JavaScript without any frameware or API.
My Code:
HTML
<html>
<head>
<meta charset="utf-8" />
<title>Page Title</title>
</head>
<body>
<div class="container">
<video src="http://www.treatbd.com/Assets/vid.mp4" poster=""></video>
<div class="controls">
<!-- play/pause button -->
<div class="play"></div>
<!-- the seekbar -->
<div class="line">
<div class="elapsed"></div>
</div>
<div class="screen"></div>
</div>
</div>
</body>
</html>
CSS/SASS
html, body
margin: 0
padding: 0
background-color: #272b32
.container
height: 465px
width: 808px
margin: auto
margin-top: calc(50vh - 270px)
display: flex
flex-wrap: wrap
justify-content: center
position: relative
overflow: hidden
border-radius: 1px
video
background-color: aliceblue
object-position: center
width: 100%
height: 100%
display: block
object-fit: cover
.controls
background-color: transparent
width: 100%
height: 9%
position: absolute
left: 0
right: 0
bottom: 0
display: flex
transition: visibility 750ms
.controls *
box-sizing: border-box
border-left: 1px solid deepSkyblue
.play
display: inline-block
height: 100%
width: 7%
background: rgba(16,18,20, .4)
&:hover
background: rgba(16,18,20, .6)
.screen
display: inline-block
height: 100%
width: 7%
background: rgba(16,18,20, .4)
&:hover
background: rgba(16,18,20, .6)
.volume
display: none
.line
display: inline-block
height: 100%
width: 86%
background: rgba(0, 5, 7, 0.45)
position: relative
.elapsed
position: absolute
background: rgba(78, 144, 249, 0.4)
height: 100%
width: 0%
transition: width 200ms
JavaScript
var video = document.querySelector("video");
var playButton = document.querySelector(".play");
var elapsed = document.querySelector(".elapsed");
var line = document.querySelector(".line");
var duration = video.duration;
var playing = false;
var mouseDown = false;
// the play/pause button
playButton.addEventListener("click", function() {
if (playing) video.pause();
else video.play();
playing = !playing;
});
// click on the video to play/pause
video.addEventListener("click", function() {
if (playing) video.pause();
else video.play();
playing = !playing;
});
// seekbar
line.addEventListener("mousedown", function(e) {
// set current time according to mousedown position
video.currentTime = Number(
e.layerX / (line.clientWidth / 100) * (duration / 100)
).toFixed(1);
// change the width of elapsed bar.
elapsed.style.width = video.currentTime / (duration / 100) + "%";
mouseDown = true;
});
line.addEventListener("mousemove", function(e) {
if (mouseDown) {
video.currentTime = Number(
e.layerX / (line.clientWidth / 100) * (duration / 100)
).toFixed(1);
}
});
line.addEventListener("mouseup", () => (mouseDown = false));
// to let the metadata loaded
var dt = setInterval(function() {
if (video.readyState > 0) {
duration = Number(video.duration).toFixed(2);
clearTimeout(dt);
}
}, 50);
// highgligh the amount of played time. via a elapse bar
setInterval(function() {
elapsed.style.width = video.currentTime / (duration / 100) + "%";
}, 1000);
I was changing the currentTime according to mousemove.
line.addEventListener("mousemove", function(e) {
if (mouseDown) {
video.currentTime = Number(
e.layerX / (line.clientWidth / 100) * (duration / 100)
).toFixed(1);
}
});
and leaving the width of elapse bar to the currentTime. But I'm reading currentTime in every 1s.
setInterval(function() {
elapsed.style.width = video.currentTime / (duration / 100) + "%";
}, 1000);
That's why it's not responsive. Even if I slide quickly, the width of the elapse bar will only move after another second.
set the Interval to 10 (10 milliseconds)
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>