How can I effectively pan an entire image programmatically? - javascript

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.

Related

How to make a collision detector for a class of items

Im working on a project game and I want to see if a div "rope" hits one of my pictures class='Fish1'.
I managed to get it to work by using document.GetElementsById("fish") but when I change it to document.getElementsByClassName('fish1') it gives me an error saying:
TypeError: Cannot read properties of undefined (reading
'getBoundingClientRect') at checkCollision
How can i fix this error? Also how can I check the height during the transition of a div and have it give me the height that it is at that point in the transition?
I've tried this:
function checkCollision(rope, fishy) {
var line = rope;
var fishy = document.getElementsByClassName('fish1');
var ropeRect = line.getBoundingClientRect();
for (var i = 0; i < fishy.length; i++) {
var fishyRect = fishy[i].getBoundingClientRect(i);
}
return (ropeRect.right >= fishyRect.left &&
ropeRect.left <= fishyRect.right) &&
(ropeRect.bottom >= fishyRect.top &&
ropeRect.top <= fishyRect.bottom);
}
And this is what worked for the id only:
function checkCollision(line, fishy) {
var line = rope;
var fishy = document.getElementById('fish');
var lineRect = line.getBoundingClientRect();
var fishyRect = fishy.getBoundingClientRect();
return (lineRect.right >= fishyRect.left &&
lineRect.left <= fishyRect.right) &&
(lineRect.bottom >= fishyRect.top &&
lineRect.top <= fishyRect.bottom);
}
Checking collision:
You can check if two elements overlap by using conditions that compare their x,y coords also involving their width, height:
//returns true if obj1 and obj2 collided, otherwise false
function checkCollision(obj1, obj2) {
const rect1 = obj1.getBoundingClientRect();
const rect2 = obj2.getBoundingClientRect();
return (rect1.x + rect1.width >= rect2.x && rect1.x <= rect2.x + rect2.width) &&
(rect1.y + rect1.height >= rect2.y && rect1.y <= rect2.y + rect2.height)
}
Moving the elements:
Once you have the function, you can call it inside your loop after changing the position of the elements at each iteration.
You didn't share any other detail apart the not working checkCollision function so I have no idea how did you perform the motion or probably you didn't at all yet.
Here I used Window.requestAnimationFrame()
https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
The window.requestAnimationFrame() method tells the browser that you
wish to perform an animation and requests that the browser calls a
specified function to update an animation before the next repaint. The
method takes a callback as an argument to be invoked before the
repaint.
Here in this example, I animate a group of .fish elements over a sin wave crossing a line in the middle. Every time an element overlaps with the rope, it calls the function collisionOccurred printing on console its id property.
Since you have the exact element partecipating to the event you are free to fetch anything about it like for example the exact position where it is at in that instant (or the height you were expecting to know).
Later I also added the events fishEnter and fishLeave that fire respectively when the element enters and leaves the rope area.
const container = document.querySelector('body');
const rope = document.getElementById('rope');
const fishes = document.getElementsByClassName('fish');
let start = null;
let containerWidth, containerHeight;
beginAnimation();
//begins the fishes animation
function beginAnimation() {
window.requestAnimationFrame(moveFish);
}
//returns the x,y coords of a sin function
function getSin(millisecondsPast, cycleDuration = 2000) {
//ranging 0-1 in the span of cycleDuration (ms)
const progress = (millisecondsPast % cycleDuration) / cycleDuration;
//=~6.28
const twoPi = 2 * Math.PI;
//x ranging from 0-6.28 in the span of cycleDuration
const x = twoPi * progress;
//sin(x) (radius=1)
const y = Math.sin(x);
return {
//[0-1] (in the span of cycleDuration ms)
x: progress,
//[-1,+1]
y: y
}
}
//moves all the fishes following a sin wave
function moveFish(timestamp) {
const containerWidth = parseInt(container.offsetWidth);
const containerHeight = parseInt(container.offsetHeight);
//keep track of the animation progress
if (!start) start = timestamp;
let progress = timestamp - start;
//for all the fishes
for (fish of fishes) {
//calculate the x and y of a sin wave
//spread across the container width in the 0-2pi space
const duration = parseInt(fish.dataset.duration);
const vertical = parseInt(fish.dataset.verticalspan);
const sin = getSin(progress, duration);
let x = sin.x * containerWidth;
let y = sin.y * (vertical/2) + (containerHeight / 2);
//change the position of the current fish
fish.style.left = `${x}px`;
fish.style.top = `${y}px`;
const didcollide = checkCollision(rope, fish);
const overlapping = (fish.dataset.overlapping == 'true') ? true : false;
//calls fishEnter if the fish entered in the space of the rope
if (!overlapping && didcollide) {
fish.dataset.overlapping = 'true';
fishEnter(fish);
}
//calls fishLeave if the fish left the space of the rope
else if (overlapping && !didcollide) {
fish.dataset.overlapping = 'false';
fishLeave(fish);
}
//calls collisionOccurred if the fish collided with the rope
if (didcollide)
collisionOccurred(fish);
}
//render next iteration
window.requestAnimationFrame(moveFish);
}
//returns true if obj1 and obj2 collided, otherwise false
function checkCollision(obj1, obj2) {
const rect1 = obj1.getBoundingClientRect();
const rect2 = obj2.getBoundingClientRect();
const didcollide =
(rect1.x + rect1.width >= rect2.x && rect1.x <= rect2.x + rect2.width) &&
(rect1.y + rect1.height >= rect2.y && rect1.y <= rect2.y + rect2.height);
return didcollide;
}
//gets fired when the fish enters in the space of the rope
function fishEnter(target) {
target.classList.add('crossing');
console.log(`The fish id: ${target.id} entered in the rope space!`);
}
//gets fired when the fish leaves in the space of the rope
function fishLeave(target) {
target.classList.remove('crossing');
console.log(`The fish id: ${target.id} left the rope space!`);
}
//gets fired when the collision event occurs
function collisionOccurred(target) {
//console.log(`The fish id: ${target.id} crossed the rope!`);
}
*{
box-sizing: border-box;
}
body {
position: relative;
height: 100vh;
padding: 0;
margin: 0;
overflow: hidden;
}
#rope {
--size: 80px;
--border: 5px;
position: absolute;
top: calc(50% + var(--border) - var(--size) / 2) ;
width: 100%;
height: var(--size);
background: brown;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
outline: solid var(--border) darkorange;
color: darkorange;
font-size: 2rem;
}
.fish {
position: absolute;
width: 80px;
line-height: 2rem;
background: blue;
font-weight: 600;
outline: solid darkblue;
text-align: center;
font-size: 1.5rem;
}
.fish::after {
content: attr(id);
color: white;
}
button {
padding: 1rem;
cursor: pointer;
}
.crossing{
background: yellow;
}
.crossing::after{
color: black !important;
}
<div id="rope">ROPE AREA</div>
<div id="fish1" class="fish" data-duration="5000" data-verticalspan="200"></div>
<div id="fish2" class="fish" data-duration="8000" data-verticalspan="300"></div>
<div id="fish3" class="fish" data-duration="10000" data-verticalspan="100"></div>
<div id="fish4" class="fish" data-duration="3000" data-verticalspan="500"></div>

Can't keep hidden overflow when adding new images to div

I'm writing a function that add an image (from an array) inside a container div at the coordinates of there the user clicked in that moment.
The problem is that I can't keep the container dimension fixed, I don't know why it keeps enlarge whenever I add an image close to its border.
Another problem I'm facing is that I can't get the img height unless I've placed it in the div but I need to have that information while creating the image because I need to place it in the middle of the clicked point.
Can you help me figure out what I'm doing wrong?
document.addEventListener('DOMContentLoaded', () => {
//setup variables
var arrayImgs = ['https://media-cdn.tripadvisor.com/media/photo-s/0e/eb/ad/3d/crazy-cat-cafe.jpg','https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Chairman_Meow_Bao.jpg/1200px-Chairman_Meow_Bao.jpg','https://static.scientificamerican.com/sciam/cache/file/92E141F8-36E4-4331-BB2EE42AC8674DD3_source.jpg','https://cdn.britannica.com/91/181391-050-1DA18304/cat-toes-paw-number-paws-tiger-tabby.jpg'];
var imgIndex = 0;
document.querySelectorAll('.container').forEach(trigger => {
trigger.addEventListener('click', function(){
if (imgIndex >= arrayImgs.length){
//check index to loop array
imgIndex = 0;
}
var imgToAdd = document.createElement("img");
var container = document.getElementById("myCanvas");
imgToAdd.setAttribute("src", arrayImgs[imgIndex]);
imgToAdd.classList.add('class-img');
var x = event.clientX;
var y = event.clientY;
//generate a random width form the image
var rndInt = Math.floor(Math.random() * 33) + 20;
var imgWidth = ((window.innerWidth / 100) * rndInt);
//parse image width
imgWidth = Math.floor(imgWidth);
imgToAdd.setAttribute("width", imgWidth );
imgToAdd.setAttribute("height", "auto" );
var imgHeight = imgToAdd.height;
//place the image in the middle of mouse X and Y
imgToAdd.style.position = "absolute";
imgToAdd.style.left = (x - (imgWidth / 2))+'px';
imgToAdd.style.top = (y - (imgWidth / 2))+'px';
container.appendChild(imgToAdd);
imgIndex = imgIndex + 1;
});
});
});
#myCanvas {
margin:0;
padding: 0;
width: 100%;
max-width: 100%;
max-height: 100vh;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
*{
box-sizing: border-box;
padding: 0;
margin: 0;
}
.class-img {
overflow: hidden;
animation: bounce 1s;
-webkit-animation: bounce 1s;
-moz-animation: bounce 1s;
}
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<div id="myCanvas" class="container">CLICK ME</div>
The first problem is that the imgs are placed with position: absolute but their container does not have any position set so they are placed in relation to the nearest ancestor which does have a position set (all the way back to body if there is nothing else). So it is the body overflowing (or whatever the nearest positioned ancestor is) hence you get scrollbars.
You need to give #mycanvas (the container) a position. Then the imgs will be placed in relation to that and the overflow: hidden will work. This snippet gives it position: relative.
document.addEventListener('DOMContentLoaded', () => {
//setup variables
var arrayImgs = ['https://media-cdn.tripadvisor.com/media/photo-s/0e/eb/ad/3d/crazy-cat-cafe.jpg','https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Chairman_Meow_Bao.jpg/1200px-Chairman_Meow_Bao.jpg','https://static.scientificamerican.com/sciam/cache/file/92E141F8-36E4-4331-BB2EE42AC8674DD3_source.jpg','https://cdn.britannica.com/91/181391-050-1DA18304/cat-toes-paw-number-paws-tiger-tabby.jpg'];
var imgIndex = 0;
document.querySelectorAll('.container').forEach(trigger => {
trigger.addEventListener('click', function(){
if (imgIndex >= arrayImgs.length){
//check index to loop array
imgIndex = 0;
}
var imgToAdd = document.createElement("img");
var container = document.getElementById("myCanvas");
imgToAdd.setAttribute("src", arrayImgs[imgIndex]);
imgToAdd.classList.add('class-img');
var x = event.clientX;
var y = event.clientY;
//generate a random width form the image
var rndInt = Math.floor(Math.random() * 33) + 20;
var imgWidth = ((window.innerWidth / 100) * rndInt);
//parse image width
imgWidth = Math.floor(imgWidth);
imgToAdd.setAttribute("width", imgWidth );
imgToAdd.setAttribute("height", "auto" );
var imgHeight = imgToAdd.height;
//place the image in the middle of mouse X and Y
imgToAdd.style.position = "absolute";
imgToAdd.style.left = (x - (imgWidth / 2))+'px';
imgToAdd.style.top = (y - (imgWidth / 2))+'px';
container.appendChild(imgToAdd);
imgIndex = imgIndex + 1;
});
});
});
#myCanvas {
margin:0;
padding: 0;
width: 100%;
max-width: 100%;
max-height: 100vh;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
position: relative;
}
*{
box-sizing: border-box;
padding: 0;
margin: 0;
}
.class-img {
overflow: hidden;
animation: bounce 1s;
-webkit-animation: bounce 1s;
-moz-animation: bounce 1s;
}
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<div id="myCanvas" class="container">CLICK ME</div>
It is not clear from the code in the question whether the link is placed in the body of the document or in its head. It probably ought to be placed in the head before any of your own styling so you can if required overwrite the linked css's styling.
The second problem, working out the img's height, will need you to load the img (could be in the same place as now but with opacity: 0), and then look at its height and then reposition it and set opacity: 1 and set the animation.

Why is the distance between first and last element decreasing?

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.

Smooth movement with background mousemove script

I found out how to make a background image move with mouse movement.
I'm trying to figure out how to smooth the movement out or make it more fluid.
The most I've figured out is that by dividing by larger and larger numbers there is less background movement, but that's as far as I've gotten.
html
<body id="body">
</body>
css
html {
width: 100%;
}
body {
background-image: url("http://sherly.mobile9.com/download/media/656/49_ybQFKMAV.png");
background-repeat: no-repeat;
background-size: cover;
height:4400px;
}
jquery
$(document).ready(function(){
$('#body').css('background-position', 'calc(45% - 0px)');
$('#body').mousemove(function(e){
var x = -(e.pageX + this.offsetLeft) / 205;
var y = -(e.pageY + this.offsetTop) / 100;
$(this).css('background-position', "calc( 45% - " + x + 'px' + ")" + y + 'px');
});
});
A good example would be from flickr after zooming in on an image.
Or this guy's site: http://ericportfolio.com/
You should separate out the background into an element of its own.
Style the element with:
.background
{
will-change: transform;
}
To prime the rending engine to promote it to its own compositing layer. This makes transform changes more cheap.
Animate transform via translateX(...) translateY(...) instead of background position.
To smooth movement you can keep track of the last several position changes and average them.
Example with 10 sample smoothing:
const root = document.querySelector(".root");
const bg = document.querySelector(".background");
const positions = [];
root.addEventListener("mousemove", e => {
const x = -(e.pageX + bg.offsetLeft) / 50;
const y = -(e.pageY + bg.offsetTop) / 50;
positions.push({ x, y });
const averageCount = 10;
if (positions.length > averageCount)
positions.splice(0, 1);
const current = positions.reduce((acc, e) => { acc.x += e.x; acc.y += e.y; return acc }, { x: 0, y: 0 });
current.x /= positions.length;
current.y /= positions.length;
bg.style.transform = `translateX(${current.x}px) translateY(${current.y}px)`;
});
.root
{
position: relative;
}
.background
{
will-change: transform;
width: 100%;
height: 100%;
}
.overlay
{
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
<div class="root">
<img class="background" src="https://i.stack.imgur.com/35oj3.png"/>
<h1 class="overlay">Lorem Ipsum</h1>
</div>

2D Infinitely looping Array of elements

The Goal :
The idea is to create an element grid (image gallery for exemple) that would infinitely loop on itself scrolling on two axes.
There should be no holes nor too much randomness (avoid having the same element randomly falling aside from itself). And this no matter how many element there is in the first place (it seems easy to infinite loop through a grid of 16 (4*4) elements, not that much over 17 (17*1). (My guess is that any prime number of elements is by definition a pain to make a grid of).
So I actually found a wonderful working exemple :
http://www.benstockley.com/
It's actually really close (probably better) than what I was imagining. Now it's using canvas and i tried looking at the javascript and it's a 30000 minified lines long script so I really can't read any core logic behind it.
Math side / Problem solving :
This is the logic and theory behind the problem, the math involved and the mindset.
How the program should process the list of elements so we have no holes, infinite grid, best repartion of the elements over all the axes.
My guess is that it somehow has to be procedural. I'm not sure if we should create grids or loop through the list on every axes (kind of like sudoku ? i don't know);
Pratical side / UI / UX :
Any advice on the technologies involved, pieces of code. I'm guessing it classic DOM is out of the way and that somehow canvas or 2D webgl will be mandatory. But I would love to hear any advice on this side.
Besides all the elements grid processing. The UI and UX involved in exploring a 2D infinite or vast layout in DOM or renderer is somehow not classical. The best technologies or advice on doing this are welcome.
Exemples :
I would welcome any working exemple that somewhat share an aspect of this problem.
I've got a fiddle that's set up to arrange your 2d grid.
It functions by using horizontal and vertical "step sizes". So, moving one step right in the grid advances the horizontal step size in the list. Moving one step down advances the vertical step size in the list (and they accumulate).
We allow the advances in the list to loop back to zero when the end is reached.
It likely makes sense to use a horizontal step size of 1 (so a row of your grid will maintain your list order). For the vertical step size, you want an integer that shares no common divisors with the list length. Though it's no guarantee, I used the (rounded) square root of the list length as something that will work in lots of cases.
I'll reproduce the fiddle here:
var list = ['red','green','blue','cyan','orange','yellow','pink'];
var hstep = 1;
var vstep = Math.ceil(Math.sqrt(list.length));
function getListItem(x,y) {
var index = x * hstep + y * vstep;
return list[index % list.length];
}
var elementSize = 30;
var gutterSize = 10;
function getOffset(x,y) {
return [10 + (elementSize + gutterSize) * x, 10 + (elementSize + gutterSize) * y];
}
var frame = $('.frame');
function drawElement(x,y) {
var listItem = getListItem(x,y);
var offsets = getOffset(x,y);
var element = $('<div></div>').addClass('element').css({
left: offsets[0] + 'px',
top: offsets[1] + 'px',
'background-color': listItem
});
frame.append(element);
}
function drawElements() {
var x = 0, y = 0;
while (10 + (elementSize + gutterSize) * x < frame.width()) {
while (10 + (elementSize + gutterSize) * y < frame.height()) {
drawElement(x,y);
y++;
}
y = 0;
x++;
}
}
drawElements();
.frame {
border: 2px solid black;
margin: 40px auto;
height: 300px;
width: 300px;
position: relative;
overflow: hidden;
}
.frame .element {
position: absolute;
width: 30px;
height: 30px;
}
.buttons {
position: absolute;
top: 0px;
width: 100%;
}
.buttons button {
position: absolute;
width: 30px;
height: 30px;
padding: 5px;
}
button.up {top: 0px; left: 46%;}
button.down {top: 355px; left: 46%;}
button.left {top: 160px; left: 15px;}
button.right {top: 160px; right: 15px;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="frame">
</div>
<div class="buttons">
<button class="up">↑</button>
<button class="down">↓</button>
<button class="left">←</button>
<button class="right">→</button>
</div>
You can see I've left some simple buttons to implement movement, but they are not functional yet. If you wanted to continue implementation along the lines of what I've done here, you could render your elements to a certain range beyond the visible frame, then implement some sort of animated repositioning. The renderElements function here only renders what is visible, so you can use something like that and not get stuck in rendering infinite elements, even though there's no theoretical limit to how far you can "scroll".
#arbuthnott I edited your code to implement the exploration via decrementing relativeX and relativeY variables. Also I inserted an "origin" div (1x1 px, overflow visible). This DOM element will represent the X and Y origin. I'm not sure it's essential but it's really convenient.
Now my function currently remove all elements and reinsert all elements on each update (every 500ms for now).
The idear would be to find a way to compare which elements I need versus which one already exists.
Maybe storing existing elements into an array, and compare the array with the "query" array. Than see just the elements that are missing.
This is the idear, not sure about the implementation (I suck at handling arrays).
https://jsfiddle.net/bnv6mumd/64/
var sources = ['red','green','blue','cyan','orange','yellow','pink','purple'];
var frame = $('.frame'),
origin = $('.origin');
var fWidth = 600,
fHeight = 300,
srcTotal = sources.length,
srcSquare = Math.ceil(Math.sqrt(srcTotal)),
rX = 0,
rY = 0;
var gridSize = 30,
gutterSize = 5,
elementSize = gridSize - gutterSize;
function getSourceItem(x,y) {
var index = x + y * srcSquare;
return sources[Math.abs(index) % srcTotal];
}
function getOffset(x,y) {
return [gridSize * x,gridSize * y];
}
function drawElement(x,y) {
var sourceItem = getSourceItem(x,y);
var offsets = getOffset(x,y);
var element = $('<div></div>').addClass('element').css({
left: offsets[0] + 'px',
top: offsets[1] + 'px',
'background-color': sourceItem,
});
origin.append(element);
}
function init() {
var x = 0, y = 0;
while ( gridSize * x < fWidth) {
while ( gridSize * y < fHeight) {
drawElement(x,y);
y++;
}
y = 0;
x++;
}
}
function updateElements() {
origin.empty();
var x = -Math.trunc(rX / gridSize) -1, y = - Math.trunc(rY / gridSize) -1;
while ( gridSize * x + rX < fWidth) {
while ( gridSize * y + rY < fHeight) {
drawElement(x,y);
y++;
}
y = -Math.ceil(rY / gridSize);
x++;
}
}
function animate() {
rX -= 5;
rY -= 5;
origin.css({left: rX, top: rY})
updateElements();
console.log("relative X : " + rX + " | relative Y : " + rY);
}
setInterval(animate, 500)
init();
.frame {
border: 2px solid black;
margin: 40px auto;
height: 300px;
width: 600px;
position: relative;
overflow: hidden;
}
.origin {
height: 1px;
width: 1px;
position: absolute;
overflow: visible;
}
.frame .element {
position: absolute;
width: 25px;
height: 25px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="frame">
<div class="origin" style="top:0;left:0;"></div>
</div>
This is my final snippet version (i will start to work on real implementation specific to my case now).
I think I optimized in a decent way DOM operations, code structure etc (I am very well open to suggestions though).
I now only update the elements that needs to be updated (click near the frame to show overflow)
https://jsfiddle.net/bnv6mumd/81/
var sources = ['red', 'green', 'blue', 'cyan', 'orange', 'yellow', 'pink', 'purple'];
var frame = $('.frame'),
origin = $('.origin');
var srcTotal = sources.length,
srcSquare = Math.round(Math.sqrt(srcTotal)),
fWidth = 200,
fHeight = 200,
cellSize = 50,
gutterSize = 20,
gridSize = [Math.floor(fWidth / cellSize) + 1, Math.floor(fHeight / cellSize) + 1],
aX = 0, // Absolute/Applied Coordinates
aY = 0,
rX = 0, // Relative/frame Coordinates
rY = 0;
function getSrcItem(x, y) {
var index = x + y * srcSquare;
return sources[Math.abs(index) % srcTotal];
}
function getOffset(x, y) {
return [cellSize * x, cellSize * y];
}
function getY() {
return Math.floor(-rY / cellSize);
}
function getX() {
return Math.floor(-rX / cellSize);
}
function drawElement(x, y) {
var srcItem = getSrcItem(x, y),
offsets = getOffset(x, y),
element = $('<div></div>').addClass('element').css({
left: offsets[0] + 'px',
top: offsets[1] + 'px',
'background-color': srcItem,
}).attr({
"X": x,
"Y": y
});
origin.append(element);
}
function drawCol(x, y) {
var maxY = y + gridSize[1];
while (y <= maxY + 1) {
drawElement(x - 1, y - 1);
y++;
}
}
function drawLign(x, y) {
var maxX = x + gridSize[0];
while (x <= maxX + 1) {
drawElement(x - 1, y - 1);
x++;
}
}
function drawGrid() {
origin.empty();
var x = getX(),
y = getY(),
maxX = x + gridSize[0],
maxY = y + gridSize[1];
while (y <= maxY + 1) {
drawLign(x, y);
x = getX();
y++;
}
}
function updateX(x, y, diffX, diffY) {
if (Math.sign(diffX) == -1) {
drawCol(aX - 1, y);
$('[x=' + (aX + gridSize[0]) + ']').remove();
aX--;
} else if (Math.sign(diffY) == 1) {
drawCol(aX + gridSize[0] + 2, y);
$('[x=' + (aX - 1) + ']').remove();
aX++;
}
}
function updateY(x, y, diffX, diffY) {
if (Math.sign(diffY) == -1) {
drawLign(x, aY - 1);
$('[y=' + (aY + gridSize[0]) + ']').remove();
aY--;
} else if (Math.sign(diffY) == 1) {
drawLign(x, aY + gridSize[0] + 2);
$('[y=' + (aY - 1) + ']').remove();
aY++;
}
}
function animate() {
rX += 1;
rY += 1;
origin.css({
left: rX,
top: rY
});
var x = getX(),
y = getY(),
diffX = x - aX,
diffY = y - aY;
if (diffX) {
updateX(x, y, diffX, diffY)
};
if (diffY) {
updateY(x, y, diffX, diffY)
};
requestAnimationFrame(animate);
}
$('body').click(function() {
$(frame).toggleClass("overflow");
})
drawGrid();
animate();
.frame {
border: 2px solid black;
margin: 100px auto;
height: 200px;
width: 200px;
position: relative;
}
.overflow{
overflow:hidden;
}
.origin {
height: 1px;
width: 1px;
position: absolute;
overflow: visible;
}
.frame .element {
position: absolute;
width: 30px;
height: 30px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="frame overflow">
<div class="origin" style="top:0;left:0;"></div>
</div>

Categories