I'm trying to make an image slider. But as you can see the distance between the first and last element is not consistent. If you keep on dragging to left, the distance decreases and if you keep on dragging to right, the distance increases. Looks like the code is behaving differently on different zoom levels (sometimes?) and hence distance between every elements is changing at times.
//project refers to placeholder rectangular divs
projectContainer = document.querySelector(".project-container")
projects = document.querySelectorAll(".project")
elementAOffset = projects[0].offsetLeft;
elementBOffset = projects[1].offsetLeft;
elementAWidth = parseInt(getComputedStyle(projects[0]).width)
margin = (elementBOffset - (elementAOffset + elementAWidth))
LeftSideBoundary = -(elementAWidth)
RightSideBoundary = (elementAWidth * (projects.length)) + (margin * (projects.length))
RightSidePosition = RightSideBoundary - elementAWidth;
initialPosition = 0; //referring to mouse
mouseIsDown = false
projectContainer.addEventListener("mousedown", e => {
mouseIsDown = true
initialPosition = e.clientX;
})
projectContainer.addEventListener("mouseup", e => {
mouseExit(e)
})
projectContainer.addEventListener("mouseleave", e => {
mouseExit(e);
})
function mouseExit(e) {
mouseIsDown = false
//updates translateX value of transform
projects.forEach(project => {
var style = window.getComputedStyle(project)
project.currentTranslationX = (new WebKitCSSMatrix(style.webkitTransform)).m41
project.style.transform = 'translateX(' + (project.currentTranslationX) + 'px)'
})
}
projectContainer.addEventListener("mousemove", e => {
if (!mouseIsDown) { return };
// adds mousemovement to translateX
projects.forEach(project => {
project.style.transform = 'translateX(' + ((project.currentTranslationX ?? 0) + (e.clientX - initialPosition)) + 'px)'
shiftPosition(e, project)
})
})
//teleports div if it hits left or right boundary to make an infinite loop
function shiftPosition(e, project) {
projectStyle = window.getComputedStyle(project)
projectTranslateX = (new WebKitCSSMatrix(projectStyle.webkitTransform)).m41
//projectVisualPosition is relative to the left border of container div
projectVisualPosition = project.offsetLeft + projectTranslateX
if (projectVisualPosition <= LeftSideBoundary) {
project.style.transform = "translateX(" + ((RightSidePosition - project.offsetLeft)) + "px)"
updateTranslateX(e);
}
if (projectVisualPosition >= RightSidePosition) {
newPosition = -1 * (project.offsetLeft + elementAWidth)
project.style.transform = "translateX(" + newPosition + "px)"
updateTranslateX(e);
}
}
function updateTranslateX(e) {
projects.forEach(project => {
style = window.getComputedStyle(project)
project.currentTranslationX = (new WebKitCSSMatrix(style.webkitTransform)).m41
project.style.transform = 'translateX(' + (project.currentTranslationX) + 'px)'
initialPosition = e.clientX
})
}
*, *::before, *::after{
margin:0px;
padding:0px;
box-sizing: border-box;
font-size:0px;
user-select: none;
}
.project-container{
font-size: 0px;
position: relative;
width:1500px;
height:400px;
background-color: rgb(15, 207, 224);
margin:auto;
margin-top:60px;
white-space: nowrap;
overflow: hidden;
padding-left:40px;
padding-right:40px;
}
.project{
font-size:100px;
margin:40px;
display: inline-block;
height:300px;
width:350px;
background-color:red;
border: black 3px solid;
user-select: none;
}
<div class="project-container">
<div class="project">1</div>
<div class="project">2</div>
<div class="project">3</div>
<div class="project">4</div>
<div class="project">5</div>
<div class="project">6</div>
<div class="project">7</div>
<div class="project">8</div>
</div>
I'm not sure exactly how you would go about fixing your implementation. I played around with it for a while and discovered a few things; dragging more quickly makes the displacement worse, and the displacement seems to happen mainly when the elements are teleported at each end of the container.
I would guess that the main reason for this is that you are looping over all the elements and spacing them individually. Mouse move events generally happen under 20ms apart, and you are relying on all the DOM elements being repainted with their new transform positions before the next move is registered.
I did come up with a different approach using absolutely placed elements and the IntersectionObserver API, which is now supported in all modern browsers. The idea here is basically that when each element intersects with the edge of the container, it triggers an array lookup to see if the next element in the sequence is on the correct end and moves it there if not. Elements are only ever spaced by a static variable, while the job of sliding them is passed up to a new parent wrapper .project-slider.
window.addEventListener('DOMContentLoaded', () => {
// Style variables
const styles = {
width: 350,
margin: 40
};
const space = styles.margin*2 + styles.width;
// Document variables
const projectContainer = document.querySelector(".project-container");
const projectSlider = document.querySelector(".project-slider");
const projects = Array.from(document.querySelectorAll(".project"));
// Mouse interactions
let dragActive = false;
let prevPos = 0;
projectContainer.addEventListener('mousedown', e => {
dragActive = true;
prevPos = e.clientX;
});
projectContainer.addEventListener('mouseup', () => dragActive = false);
projectContainer.addEventListener('mouseleave', () => dragActive = false);
projectContainer.addEventListener('mousemove', e => {
if (!dragActive) return;
const newTrans = projectSlider.currentTransX + e.clientX - prevPos;
projectSlider.style.transform = `translateX(${newTrans}px)`;
projectSlider.currentTransX = newTrans;
prevPos = e.clientX;
});
// Generate initial layout
function init() {
let workingLeft = styles.margin;
projects.forEach((project, i) => {
if (i === projects.length - 1) {
project.style.left = `-${space - styles.margin}px`;
} else {
i !== 0 && (workingLeft += space);
project.style.left = `${workingLeft}px`;
};
});
projectSlider.currentTransX = 0;
};
// Intersection observer
function observe() {
const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Find intersecting edge
const { left } = entry.boundingClientRect;
const isLeftEdge = left < projectContainer.clientWidth - left;
// Test and reposition next element
const targetIdx = projects.findIndex(project => project === entry.target);
let nextIdx = null;
const nextEl = () => projects[nextIdx];
const targetLeft = parseInt(entry.target.style.left);
const nextLeft = () => parseInt(nextEl().style.left);
if (isLeftEdge) {
nextIdx = targetIdx === 0 ? projects.length-1 : targetIdx - 1;
nextLeft() > targetLeft && (nextEl().style.left = `${targetLeft - space}px`);
} else {
nextIdx = targetIdx === projects.length-1 ? 0 : targetIdx + 1;
nextLeft() < targetLeft && (nextEl().style.left = `${targetLeft + space}px`);
};
};
});
};
const observer = new IntersectionObserver(callback, {root: projectContainer});
projects.forEach(project => observer.observe(project));
};
init();
observe();
});
*, *::before, *::after{
margin:0px;
padding:0px;
box-sizing: border-box;
font-size:0px;
user-select: none;
}
.project-container {
font-size: 0px;
width: 100%;
height: 400px;
background-color: rgb(15, 207, 224);
margin:auto;
margin-top:60px;
white-space: nowrap;
overflow: hidden;
}
.project-slider {
position: relative;
}
.project {
font-size:100px;
display: block;
position: absolute;
top: 40px;
height:300px;
width:350px;
background-color:red;
border: black 3px solid;
user-select: none;
}
<div class="project-container">
<div class="project-slider">
<div class="project">1</div>
<div class="project">2</div>
<div class="project">3</div>
<div class="project">4</div>
<div class="project">5</div>
<div class="project">6</div>
<div class="project">7</div>
<div class="project">8</div>
</div>
</div>
There is still an issue here which is how to resize the elements for smaller screens, and on browser resizes. You would have to add another event listener for window resizes which resets the positions and styles at certain breakpoints, and also determine the style variables programmatically when the page first loads. I believe this would still have been a partial issue with the original implementation so you'd have to address it at some point either way.
Related
I'm interested in how I can make a grid with an undetermined amount of columns and rows that I can put inside another div and have it not spill into others objects or mess with the parent size.
I want it to be square and I'm using Tailwind CSS but I can adapt to SCSS or vanilla CSS. Also I want it to be touchable/moveable with a mouse on desktop and touch capable devices.
How would I go about accomplishing this?
Assuming I've understood your question correctly, here is one way you could do it. I haven't tested it with a touch device but it shouldn't be hard to modify it to also respond to touch events.
const items = [
['a0', 'a1', 'a2'],
['b0', 'b1', 'b2'],
['c0', 'c1', 'c2']
];
let html = '';
for (let rowItems of items) {
html += '<div class="row">';
for (let item of rowItems) {
html += '<div class="item">';
html += item;
html += '</div>';
}
html += '</div>';
}
const viewElem = document.querySelector('#view');
const outputElem = document.querySelector('#output');
outputElem.innerHTML = html;
let mouseStartPos = null;
let startOffset = null;
outputElem.addEventListener('mousedown', e => {
outputElem.classList.remove('animate');
mouseStartPos = {
x: e.clientX,
y: e.clientY
};
startOffset = {
x: outputElem.offsetLeft - viewElem.offsetLeft,
y: outputElem.offsetTop - viewElem.offsetTop
};
});
window.addEventListener('mouseup', e => {
mouseStartPos = null;
startOffset = null;
outputElem.classList.add('animate');
const xGridOffset = -1 * Math.max(0, Math.min(Math.round((outputElem.offsetLeft - viewElem.offsetLeft) / -100), items.length - 1));
const yGridOffset = -1 * Math.max(0, Math.min(Math.round((outputElem.offsetTop - viewElem.offsetTop) / -100), items[0].length - 1));
outputElem.style.left = `${xGridOffset * 100}px`;
outputElem.style.top = `${yGridOffset * 100}px`;
});
window.addEventListener('mousemove', e => {
if (mouseStartPos) {
const xOffset = mouseStartPos.x - e.clientX;
const yOffset = mouseStartPos.y - e.clientY;
outputElem.style.left = `${-1 * xOffset + startOffset.x}px`;
outputElem.style.top = `${-1 * yOffset + startOffset.y}px`;
}
});
#view {
width: 100px;
height: 100px;
overflow: hidden;
border: 2px solid blue;
}
#output {
position: relative;
}
.row {
display: flex;
}
.item {
display: flex;
min-width: 100px;
width: 100px;
height: 100px;
box-sizing: border-box;
justify-content: center;
align-items: center;
border: 1px solid red;
}
#output.animate {
transition: left 1s ease 0s, top 1s ease 0s;
}
Drag it!<br/>
<br/>
<div id="view">
<div id="output"></div>
</div>
I have a container that is expanded and collapsed on click of chevron icon. The code to collapse/expand the container is in the function transformAnimation. The code of transformAnimation is similar to the code on MDN web docs for requestAnimationFrame. The code to animate (scale) the container has been developed on the guidelines of this article on Building performant expand & collapse animations on Chrome Developers website.
I am not able to figure out how to calculate yScale value (which is nothing but y scale values for collapse/expand animation) as a function of the time elapsed since the start of the animation.
To elaborate what I mean, let's assume that the container is in expanded state. In this state the scaleY value of the container is 6. Now when user clicks on the toggle button, in the transformAnimation function for each animation frame, i.e, execution of the requestAnimationFrame callback step function, the value of scaleY should decrease from 6 (the expanded state) to 1 (the collapsed state) in the exact duration that I want the animation to run for.
In the present state, the code to calculate yScale is not working as expected.
const dragExpandableContainer = document.querySelector('.drag-expandable-container');
const dragExpandableContents = document.querySelector('.drag-expandable__contents');
const resizeableControlEl = document.querySelector('.drag-expandable__resize-control');
const content = document.querySelector(`.content`);
const toggleEl = document.querySelector(`.toggle`);
const collapsedHeight = calculateCollapsedHeight();
/* This height is used as the basis for calculating all the scales for the component.
* It acts as proxy for collapsed state.
*/
dragExpandableContainer.style.height = `${collapsedHeight}px`;
// Apply iniial transform to expand
dragExpandableContainer.style.transformOrigin = 'bottom left';
dragExpandableContainer.style.transform = `scale(1, 10)`;
// Apply iniial reverse transform on the contents
dragExpandableContents.style.transformOrigin = 'bottom left';
dragExpandableContents.style.transform = `scale(1, calc(1/10))`;
let isOpen = true;
const togglePopup = () => {
if (isOpen) {
collapsedAnimation();
toggleEl.classList.remove('toggle-open');
isOpen = false;
} else {
expandAnimation();
toggleEl.classList.add('toggle-open');
isOpen = true
};
};
function calculateCollapsedHeight() {
const collapsedHeight = content.offsetHeight + resizeableControlEl.offsetHeight;
return collapsedHeight;
}
const calculateCollapsedScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = dragExpandableContainer.getBoundingClientRect().height;
return {
/* Since we are not dealing with scaling on X axis, we keep it 1.
* It can be inverse to if required */
x: 1,
y: expandedHeight / collapsedHeight,
};
};
const calculateExpandScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = 100;
return {
x: 1,
y: expandedHeight / collapsedHeight,
};
};
function expandAnimation() {
const {
x,
y
} = calculateExpandScale();
transformAnimation('expand', {
x,
y
});
}
function collapsedAnimation() {
const {
x,
y
} = calculateCollapsedScale();
transformAnimation('collapse', {
x,
y
});
}
function transformAnimation(animationType, scale) {
let start, previousTimeStamp;
let done = false;
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimeStamp !== timestamp) {
const count = Math.min(0.1 * elapsed, 200);
//console.log('count', count);
let yScale;
if (animationType === 'expand') {
yScale = (scale.y / 100) * count;
} else yScale = scale.y - (scale.y / 100) * count;
//console.log('yScale', yScale);
if (yScale < 1) yScale = 1;
dragExpandableContainer.style.transformOrigin = 'bottom left';
dragExpandableContainer.style.transform = `scale(${scale.x}, ${yScale})`;
const inverseXScale = 1;
const inverseYScale = 1 / yScale;
dragExpandableContents.style.transformOrigin = 'bottom left';
dragExpandableContents.style.transform = `scale(${inverseXScale}, ${inverseYScale})`;
if (count === 200) done = true;
//console.log('elapsed', elapsed);
if (elapsed < 1000) {
// Stop the animation after 2 seconds
previousTimeStamp = timestamp;
if (!done) requestAnimationFrame(step);
}
}
}
requestAnimationFrame(step);
}
.drag-expandable-container {
position: absolute;
bottom: 0px;
display: block;
overflow: hidden;
width: 100%;
background-color: #f3f7f7;
}
.drag-expandable__contents {
height: 0;
}
.toggle {
position: absolute;
top: 2px;
right: 15px;
height: 10px;
width: 10px;
transition: transform 0.2s linear;
}
.toggle-open {
transform: rotate(180deg);
}
.drag-expandable__resize-control {
background-color: #e7eeef;
}
.burger-icon {
width: 12px;
margin: 0 auto;
padding: 2px 0;
}
.burger-icon__line {
height: 1px;
background-color: #738F93;
margin: 2px 0;
}
.drag-expandable__resize-control:hover {
border-top: 1px solid #4caf50;
cursor: ns-resize;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css.css">
</head>
<body>
<div class="drag-expandable-container">
<div class="drag-expandable__contents">
<div class="drag-expandable__resize-control">
<div class="burger-icon">
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
</div>
</div>
<div class="content" />
<div>
<div class="toggle toggle-open" onclick="togglePopup()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.1.1 by #fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z"/></svg>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript" src="js.js"></script>
</html>
I have a 11500x11500 div that consists of 400 images, that obviously overflows the viewport.
I would like to pan around the whole div programmatically.
I want to generate an animation and by the time the animation is over, the whole of the div must have been panned across the viewport, top to bottom, left to right.
Right now, I am "splitting" my 11500x1500 div into tiles. The maximum width and height of each tile is the width and height of the viewport.
I store the coordinates of each tile and then I randomly choose one, pan it left-to-right and then move on to the next one.
I would like to know:
whether my method is correct or whether I am missing something in my calculations/approach and it could be improved. Given the size, it is hard for me to tell whether I'm actually panning the whole of the div after all
whether I can make the panning effect feel more "organic"/"natural". In order to be sure that the whole div is eventually panned, I pick each tile and pan it left-to-right, move on to the next one etc. This feels kind of rigid and too formalised. Is there a way to pan at let's say an angle or with a movement that is even more random and yet be sure that the whole div will eventually be panned ?
Thank in advance for any help.
This is the jsfiddle and this is the code (for the sake of the example/test every "image" is actually a div containing its index as text):
function forMs(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
}
let container = document.getElementById('container')
let {
width,
height
} = container.getBoundingClientRect()
let minLeft = window.innerWidth - width
let minTop = window.innerHeight - height
let i = 0
while (i < 400) {
// adding "image" to the container
let image = document.createElement('div')
// add some text to the "image"
// to know what we're looking at while panning
image.innerHTML = ''
let j = 0
while (j < 100) {
image.innerHTML += ` ${i + 1}`
j++
}
container.appendChild(image)
i++
}
let coords = []
let x = 0
while (x < width) {
let y = 0
while (y < height) {
coords.push({
x,
y
})
y += window.innerHeight
}
x += window.innerWidth
}
async function pan() {
if (!coords.length) {
return;
}
let randomIdx = Math.floor(Math.random() * coords.length)
let [randomCoord] = coords.splice(randomIdx, 1);
console.log(coords.length)
container.classList.add('fast')
// update style in new thread so new transition-duration is applied
await forMs(10)
// move to new yet-unpanned area
container.style.top = Math.max(-randomCoord.y, minTop) + 'px'
container.style.left = Math.max(-randomCoord.x, minLeft) + 'px'
// wait (approx.) for transition to end
await forMs(2500)
container.classList.remove('fast')
// update style in new thread so new transition-duration is applied
await forMs(10)
//pan that area
let newLeft = -(randomCoord.x + window.innerWidth)
if (newLeft < minLeft) {
newLeft = minLeft
}
container.style.left = newLeft + 'px'
// wait (approx.) for transition to end
await forMs(4500)
// move on to next random area
await pan()
}
pan()
html,
body {
position: relative;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
* {
margin: 0;
padding: 0;
}
#container {
position: absolute;
top: 0;
left: 0;
text-align: left;
width: 11500px;
height: 11500px;
transition: all 4s ease-in-out;
transition-property: top left;
font-size: 0;
}
#container.fast {
transition-duration: 2s;
}
#container div {
display: inline-block;
height: 575px;
width: 575px;
border: 1px solid black;
box-sizing: border-box;
font-size: 45px;
overflow: hidden;
word-break: break-all;
}
<div id="container"></div>
I think following improvements can be made:
Hide overflow on html and body so user can not move scrollbar and disturb the flow.
Calculate minLeft and minTop every time to account for window resizing. You might need ResizeObserver to recalculate things.
Increase transition times to avoid Cybersickness. In worse case RNG will pick bottom right tile first so your container will move the longest in 2seconds! Maybe, you can zoom-out and move then zoom-in then perform pan. Or use any serpentine path which will make shorter jumps.
Performance improvements:
Use transform instead of top, left for animation.
Use will-change: transform;. will-change will let browser know what to optimize.
Use translate3D() instead of translate(). ref
Use requestAnimationFrame. Avoid setTimeout, setInterval.
This is an old but good article: https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/
Modified code to use transform:
function forMs(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
}
let container = document.getElementById('container')
let stat = document.getElementById('stats');
let {
width,
height
} = container.getBoundingClientRect()
let minLeft = window.innerWidth - width
let minTop = window.innerHeight - height
let i = 0
while (i < 400) {
// adding "image" to the container
let image = document.createElement('div')
// add some text to the "image"
// to know what we're looking at while panning
image.innerHTML = ''
let j = 0
while (j < 100) {
image.innerHTML += ` ${i + 1}`
j++
}
container.appendChild(image)
i++
}
let coords = []
let x = 0
while (x < width) {
let y = 0
while (y < height) {
coords.push({
x,
y
})
y += window.innerHeight
}
x += window.innerWidth
}
let count = 0;
async function pan() {
if (!coords.length) {
stat.innerText = 'iteration: ' +
(++count) + '\n tile# ' + randomIdx + ' done!!';
stat.style.backgroundColor = 'red';
return;
}
let minLeft = window.innerWidth - width
let minTop = window.innerHeight - height
let randomIdx = Math.floor(Math.random() * coords.length);
randomIdx = 1; //remove after debugging
let [randomCoord] = coords.splice(randomIdx, 1);
stat.innerText = 'iteration: ' +
(++count) + '\n tile# ' + randomIdx;
console.log(coords.length + ' - ' + randomIdx)
container.classList.add('fast')
// update style in new thread so new transition-duration is applied
await forMs(10)
// move to new yet-unpanned area
let yy = Math.max(-randomCoord.y, minTop);
let xx = Math.max(-randomCoord.x, minLeft);
move(xx, yy);
// wait (approx.) for transition to end
await forMs(2500)
container.classList.remove('fast')
// update style in new thread so new transition-duration is applied
await forMs(10)
//pan that area
let newLeft = -(randomCoord.x + window.innerWidth)
if (newLeft < minLeft) {
newLeft = minLeft
}
xx = newLeft;
//container.style.left = newLeft + 'px'
move(xx, yy);
// wait (approx.) for transition to end
await forMs(4500)
// move on to next random area
await pan()
}
pan()
function move(xx, yy) {
container.style.transform = "translate3D(" + xx + "px," + yy + "px,0px)";
}
html,
body {
position: relative;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#container {
text-align: left;
width: 11500px;
height: 11500px;
transition: all 4s ease-in-out;
transition-property: transform;
font-size: 0;
will-change: transform;
}
#container.fast {
transition-duration: 2s;
}
#container div {
display: inline-block;
height: 575px;
width: 575px;
border: 1px solid black;
box-sizing: border-box;
font-size: 45px;
overflow: hidden;
word-break: break-all;
}
#stats {
border: 2px solid green;
width: 100px;
background-color: lightgreen;
position: fixed;
opacity: 1;
top: 0;
left: 0;
z-index: 10;
}
<div id=stats>iteration: 1 tile# 11</div>
<div id="container"></div>
Note I haven't implemented everything in above snippet.
I have a large background image and some much smaller images for the user to drag around on the background. I need this to be efficient in terms of performance, so i'm trying to avoid libraries. I'm fine with drag 'n' drop if it work's well, but im trying to get drag.
Im pretty much trying to do this. But after 8 years there must be a cleaner way to do this right?
I currently have a drag 'n' drop system that almost works, but when i drop the smaller images, they are just a little off and it's very annoying. Is there a way to fix my code, or do i need to take a whole different approach?
This is my code so far:
var draggedPoint;
function dragStart(event) {
draggedPoint = event.target; // my global var
}
function drop(event) {
event.preventDefault();
let xDiff = draggedPoint.x - event.pageX;
let yDiff = draggedPoint.y - event.pageY;
let left = draggedPoint.style.marginLeft; // get margins
let top = draggedPoint.style.marginTop;
let leftNum = Number(left.substring(0, left.length - 2)); // cut off px from the end
let topNum = Number(top.substring(0, top.length - 2));
let newLeft = leftNum - xDiff + "px" // count new margins and put px back to the end
let newTop = topNum - yDiff + "px"
draggedPoint.style.marginLeft = newLeft;
draggedPoint.style.marginTop = newTop;
}
function allowDrop(event) {
event.preventDefault();
}
let imgs = [
"https://upload.wikimedia.org/wikipedia/commons/6/67/Orange_juice_1_edit1.jpg",
"https://upload.wikimedia.org/wikipedia/commons/f/ff/Solid_blue.svg",
"https://upload.wikimedia.org/wikipedia/commons/b/b4/Litoria_infrafrenata_-_Julatten.jpg"
]
/* my smaller images: */
for (let i = 0; i < 6; i++) {
let sensor = document.createElement("img");
sensor.src = imgs[i % imgs.length];
sensor.alt = i;
sensor.draggable = true;
sensor.classList.add("sensor");
sensor.style.marginLeft = `${Math.floor(Math.random() * 900)}px`
sensor.style.marginTop = `${Math.floor(Math.random() * 500)}px`
sensor.onclick = function() {
sensorClick(logs[i].id)
};
sensor.addEventListener("dragstart", dragStart, null);
let parent = document.getElementsByClassName("map")[0];
parent.appendChild(sensor);
}
<!-- my html: -->
<style>
.map {
width: 900px;
height: 500px;
align-content: center;
margin: 150px auto 150px auto;
}
.map .base {
position: absolute;
width: inherit;
height: inherit;
}
.map .sensor {
position: absolute;
width: 50px;
height: 50px;
}
</style>
<div class="map" onDrop="drop(event)" ondragover="allowDrop(event)">
<img src='https://upload.wikimedia.org/wikipedia/commons/f/f7/Plan-Oum-el-Awamid.jpg' alt="pohja" class="base" draggable="false">
<div>
With the answers from here and some time i was able to get a smooth drag and click with pure js.
Here is a JSFiddle to see it in action.
let maxLeft;
let maxTop;
const minLeft = 0;
const minTop = 0;
let timeDelta;
let imgs = [
"https://upload.wikimedia.org/wikipedia/commons/6/67/Orange_juice_1_edit1.jpg",
"https://upload.wikimedia.org/wikipedia/commons/f/ff/Solid_blue.svg",
"https://upload.wikimedia.org/wikipedia/commons/b/b4/Litoria_infrafrenata_-_Julatten.jpg"
]
var originalX;
var originalY;
window.onload = function() {
document.onmousedown = startDrag;
document.onmouseup = stopDrag;
}
function sensorClick () {
if (Date.now() - timeDelta < 150) { // check that we didn't drag
createPopup(this);
}
}
// create a popup when we click
function createPopup(parent) {
let p = document.getElementById("popup");
if (p) {
p.parentNode.removeChild(p);
}
let popup = document.createElement("div");
popup.id = "popup";
popup.className = "popup";
popup.style.top = parent.y - 110 + "px";
popup.style.left = parent.x - 75 + "px";
let text = document.createElement("span");
text.textContent = parent.id;
popup.appendChild(text);
var map = document.getElementsByClassName("map")[0];
map.appendChild(popup);
}
// when our base is loaded
function baseOnLoad() {
var map = document.getElementsByClassName("map")[0];
let base = document.getElementsByClassName("base")[0];
maxLeft = base.width - 50;
maxTop = base.height - 50;
/* my smaller images: */
for (let i = 0; i < 6; i++) {
let sensor = document.createElement("img");
sensor.src = imgs[i % imgs.length];
sensor.alt = i;
sensor.id = i;
sensor.draggable = true;
sensor.classList.add("sensor");
sensor.classList.add("dragme");
sensor.style.left = `${Math.floor(Math.random() * 900)}px`
sensor.style.top = `${Math.floor(Math.random() * 500)}px`
sensor.onclick = sensorClick;
let parent = document.getElementsByClassName("map")[0];
parent.appendChild(sensor);
}
}
function startDrag(e) {
timeDelta = Date.now(); // get current millis
// determine event object
if (!e) var e = window.event;
// prevent default event
if(e.preventDefault) e.preventDefault();
// IE uses srcElement, others use target
targ = e.target ? e.target : e.srcElement;
originalX = targ.style.left;
originalY = targ.style.top;
// check that this is a draggable element
if (!targ.classList.contains('dragme')) return;
// calculate event X, Y coordinates
offsetX = e.clientX;
offsetY = e.clientY;
// calculate integer values for top and left properties
coordX = parseInt(targ.style.left);
coordY = parseInt(targ.style.top);
drag = true;
document.onmousemove = dragDiv; // move div element
return false; // prevent default event
}
function dragDiv(e) {
if (!drag) return;
if (!e) var e = window.event;
// move div element and check for borders
let newLeft = coordX + e.clientX - offsetX;
if (newLeft < maxLeft && newLeft > minLeft) targ.style.left = newLeft + 'px'
let newTop = coordY + e.clientY - offsetY;
if (newTop < maxTop && newTop > minTop) targ.style.top = newTop + 'px'
return false; // prevent default event
}
function stopDrag() {
if (typeof drag == "undefined") return;
if (drag) {
if (Date.now() - timeDelta > 150) { // we dragged
let p = document.getElementById("popup");
if (p) {
p.parentNode.removeChild(p);
}
} else {
targ.style.left = originalX;
targ.style.top = originalY;
}
}
drag = false;
}
.map {
width: 900px;
height: 500px;
margin: 50px
position: relative;
}
.map .base {
position: absolute;
width: inherit;
height: inherit;
}
.map .sensor {
display: inline-block;
position: absolute;
width: 50px;
height: 50px;
}
.dragme {
cursor: move;
left: 0px;
top: 0px;
}
.popup {
position: absolute;
display: inline-block;
width: 200px;
height: 100px;
background-color: #9FC990;
border-radius: 10%;
}
.popup::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -10px;
border-width: 10px;
border-style: solid;
border-color: #9FC990 transparent transparent transparent;
}
.popup span {
width: 90%;
margin: 10px;
display: inline-block;
text-align: center;
}
<div class="map" width="950px" height="500px">
<img src='https://upload.wikimedia.org/wikipedia/commons/f/f7/Plan-Oum-el-Awamid.jpg' alt="pohja" class="base" draggable="false" onload="baseOnLoad()">
<div>
See the following snippet of code.
It creates a loader on click of a button. But the animation is not smooth.
I have recently read about requestAnimationFrame function which can do this job. But how can I use it to replace setInterval altogether since there is no way to specify time in requestAnimationFrame function.
Can it be used in conjunction with setInterval ?
let idx = 1;
const timetoEnd = 5000;
function ProgressBar(width){
this.width = width || 0;
this.id = `pBar-${idx++}`;
this.create = () => {
let pBar = document.createElement('div');
pBar.id = this.id;
pBar.className = `p-bar`;
pBar.innerHTML = `<div class="loader"></div>`;
return pBar;
};
this.animator = () => {
let element = document.querySelector(`#${(this.id)} div`);
if(this.width < 100){
this.width++;
element.style.width = `${this.width}%`;
} else {
clearInterval(this.interval);
}
};
this.animate = () => {
this.interval = setInterval(this.animator, timetoEnd/100);
}
}
function addLoader (){
let bar1 = new ProgressBar(40);
let container = document.querySelector("#container");
container.appendChild(bar1.create());
bar1.animate();
}
.p-bar{
width: 400px;
height: 20px;
background: 1px solid #ccc;
margin: 10px;
overflow:hidden;
border-radius: 4px;
}
.p-bar .loader{
width: 0;
background: #1565C0;
height: 100%;
}
<input type="button" value="Add loader" onclick="addLoader()" />
<div id="container"></div>
You are right, requestAnimationFrame is the recommended way to avoid UI jam when doing animation.
You can remember the absolute starting time at start instead of trying to do this at each frame. Then it's just a matter of computing a width based on the delta time between start and current time.
Also, document.querySelector is considered a relatively "heavy" operation so I added this.element to avoid doing it at each frame.
Here is how to new width is computed: ((100 - this.startWidth) / timetoEnd) * deltaT + this.startWidth
100 - this.startWidth is the total amount of width we have to animate
(100 - this.startWidth) / timetoEnd is how much width each second must add to (1)
((100 - this.startWidth) / timetoEnd) * deltaT is how much width we have to add to (1)
We just have to shift the whole thing this.startWidth px to have the frame's width
Also notice that some of this computation is constant and do not have to be computed on each frame, which I left as an exercise :)
Here is your slightly adapted code:
let idx = 1;
const timetoEnd = 5000;
function ProgressBar(startWidth){
this.startWidth = startWidth || 0;
this.id = `pBar-${idx++}`;
this.create = () => {
let pBar = document.createElement('div');
pBar.id = this.id;
pBar.className = `p-bar`;
pBar.innerHTML = `<div class="loader"></div>`;
return pBar;
};
this.animator = () => {
const deltaT = Math.min(new Date().getTime() - this.start, timetoEnd);
if(deltaT < timetoEnd){
const width = ((100 - this.startWidth) / timetoEnd) * deltaT + this.startWidth;
this.element.style.width = `${width}%`;
requestAnimationFrame(this.animator.bind(this))
}
};
this.animate = () => {
this.element = document.querySelector(`#${(this.id)} div`);
this.start = new Date().getTime();
this.animator();
}
}
function addLoader (){
let bar1 = new ProgressBar(40);
let container = document.querySelector("#container");
container.appendChild(bar1.create());
bar1.animate();
}
.p-bar{
width: 400px;
height: 20px;
background: 1px solid #ccc;
margin: 10px;
overflow:hidden;
border-radius: 4px;
}
.p-bar .loader{
width: 0;
background: #1565C0;
height: 100%;
}
<input type="button" value="Add loader" onclick="addLoader()" />
<div id="container"></div>