How to change the img of a cloned div onClick - javascript

Right now I have a lovely code (that was initially for flowing snowflakes) that lets cloned divs fall from the top of the window, to the bottom and repeat. The thing is that I want the content to change on click. However, this is not yet working.
I know how to change content of a div without it being cloned, but I have serious trouble figuring out how to do the same with one div being cloned within the code.
Anyone have any tips what I should do, even a hint in which direction to go. I'm clueless.
This is the fiddle:
http://jsfiddle.net/4yaxvt7h/
the javascript:
function changeImage(){
document.getElementById('toChange').src='https://i.pinimg.com/originals/6e/19/56/6e195649034f042d1dea5230234570a8.gif';
}
// Array to store our Snowflake objects
var snowflakes = [];
// Global variables to store our browser's window size
var browserWidth;
var browserHeight;
// Specify the number of snowflakes you want visible
var numberOfSnowflakes = 15;
// Flag to reset the position of the snowflakes
var resetPosition = false;
// Handle accessibility
var enableAnimations = false;
var reduceMotionQuery = matchMedia("(prefers-reduced-motion)");
// Handle animation accessibility preferences
function setAccessibilityState() {
if (reduceMotionQuery.matches) {
enableAnimations = false;
} else {
enableAnimations = true;
}
}
setAccessibilityState();
reduceMotionQuery.addListener(setAccessibilityState);
//
// It all starts here...
//
function setup() {
if (enableAnimations) {
window.addEventListener("DOMContentLoaded", generateSnowflakes, false);
window.addEventListener("resize", setResetFlag, false);
}
}
setup();
//
// Constructor for our Snowflake object
//
function Snowflake(element, speed, xPos, yPos) {
// set initial snowflake properties
this.element = element;
this.speed = speed;
this.xPos = xPos;
this.yPos = yPos;
this.scale = 1;
// declare variables used for snowflake's motion
this.counter = 0;
this.sign = Math.random() < 0.5 ? 1 : -1;
// setting an initial opacity and size for our snowflake
this.element.style.opacity = 1;
}
//
// The function responsible for actually moving our snowflake
//
Snowflake.prototype.update = function () {
// using some trigonometry to determine our x and y position
this.counter += this.speed / 5000;
this.xPos += this.sign * this.speed * Math.cos(this.counter) / 40;
this.yPos += Math.sin(this.counter) / 40 + this.speed / 30;
this.scale = .5 + Math.abs(10 * Math.cos(this.counter) / 20);
// setting our snowflake's position
setTransform(Math.round(this.xPos), Math.round(this.yPos), this.scale, this.element);
// if snowflake goes below the browser window, move it back to the top
if (this.yPos > browserHeight) {
this.yPos = -50;
}
}
//
// A performant way to set your snowflake's position and size
//
function setTransform(xPos, yPos, scale, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0) scale(${scale}, ${scale})`;
}
//
// The function responsible for creating the snowflake
//
function generateSnowflakes() {
// get our snowflake element from the DOM and store it
var originalSnowflake = document.querySelector(".snowflake");
// access our snowflake element's parent container
var snowflakeContainer = originalSnowflake.parentNode;
snowflakeContainer.style.display = "block";
// get our browser's size
browserWidth = document.documentElement.clientWidth;
browserHeight = document.documentElement.clientHeight;
// create each individual snowflake
for (var i = 0; i < numberOfSnowflakes; i++) {
// clone our original snowflake and add it to snowflakeContainer
var snowflakeClone = originalSnowflake.cloneNode(true);
snowflakeContainer.appendChild(snowflakeClone);
// set our snowflake's initial position and related properties
var initialXPos = getPosition(50, browserWidth);
var initialYPos = getPosition(50, browserHeight);
var speed = 5 + Math.random() * 40;
// create our Snowflake object
var snowflakeObject = new Snowflake(snowflakeClone,
speed,
initialXPos,
initialYPos);
snowflakes.push(snowflakeObject);
}
// remove the original snowflake because we no longer need it visible
snowflakeContainer.removeChild(originalSnowflake);
moveSnowflakes();
}
//
// Responsible for moving each snowflake by calling its update function
//
function moveSnowflakes() {
if (enableAnimations) {
for (var i = 0; i < snowflakes.length; i++) {
var snowflake = snowflakes[i];
snowflake.update();
}
}
// Reset the position of all the snowflakes to a new value
if (resetPosition) {
browserWidth = document.documentElement.clientWidth;
browserHeight = document.documentElement.clientHeight;
for (var i = 0; i < snowflakes.length; i++) {
var snowflake = snowflakes[i];
snowflake.xPos = getPosition(50, browserWidth);
snowflake.yPos = getPosition(50, browserHeight);
}
resetPosition = false;
}
requestAnimationFrame(moveSnowflakes);
}
//
// This function returns a number between (maximum - offset) and (maximum + offset)
//
function getPosition(offset, size) {
return Math.round(-1 * offset + Math.random() * (size + 2 * offset));
}
//
// Trigger a reset of all the snowflakes' positions
//
function setResetFlag(e) {
resetPosition = true;
}
html
<div id="snowflakeContainer">
<div class="snowflake"><img src="element1.png"
</div>
css
body{
background-color: black;
}
#snowflakeContainer {
position: absolute;
left: 0px;
top: 0px;
display: none;
}
.snowflake {
position: fixed;
background-color: red;
user-select: none;
z-index: 1000;
pointer-events: none;
border-radius: 50%;
width: 200px;
height: 200px;
}
img{
max-width: 100%;
}
any tips are super welcome!!

You have several different problems here. First, your CSS sets pointer-events: none; for all .snowflake elements. This will stop any clicks from triggering, so you have to remove it if you want mouse interactivity.
Second, your changeImage() function uses document.getElementById('toChange') to get the element to change the source of. No such element exists and even if it did, that would mean that clicking on any of the snowflakes would just change the source of that one image. You need to reference the snowflake that you clicked on. An easy way to do this is by passing this as an argument to the function in the onclick attribute:
HTML:
<div class="snowflake"><img src="https://vignette.wikia.nocookie.net/dragonballfanon/images/7/70/Random.png/revision/latest?cb=20161221030547" onclick="changeImage(this)"></div>
JS:
function changeImage(img) {
img.src = 'https://i.pinimg.com/originals/6e/19/56/6e195649034f042d1dea5230234570a8.gif';
}
Finally, your JSFiddle is configured to delay execution of the JavaScript until after the DOM is ready. This means that the code will be wrapped in a callback function, and therefore the changeImage() function will no longer be in the global scope. This makes it inaccessible to the elements that want to call it in their handlers (the snowflakes). Because you are already listening for the DOMContentLoaded event on your own, you can just change the code to execute normally and it should work. Click on the dropdown that says "JavaScript + jQuery 3.2.1" above your JS code and change the load type to any "No wrap" option and it should work.

Related

Adding click event on a specific div with JavaScript

I have a snowfall effect in JavaScript, I want the create a click event on the 10th snowflake that falls down. I would also like to add a Class that says "clickable". We want to achieve this, so if user clicks the 10th snowflake only, then they will be redirected to a prize (we also want to add a Class so we can style this nicely via CSS).
I must admit, my JavaScript knowledge isn't as strong as my jQuery, so could someone please help? Here is the codepen that I am using:-
https://codepen.io/scottYg55/pen/GRRzOgO
#snowflakeContainer {
position: absolute;
left: 0px;
top: 0px;
bottom: 0;
right: 0;
background-color: #2c3e50;
}
.snowflake {
padding-left: 15px;
font-family: Cambria, Georgia, serif;
font-size: 14px;
line-height: 24px;
position: fixed;
color: #FFFFFF;
user-select: none;
z-index: 1000;
}
<div id="snowflakeContainer">
<p class="snowflake">*</p>
</div>
JS
// The star of every good animation
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
var transforms = ["transform",
"msTransform",
"webkitTransform",
"mozTransform",
"oTransform"];
var transformProperty = getSupportedPropertyName(transforms);
// Array to store our Snowflake objects
var snowflakes = [];
// Global variables to store our browser's window size
var browserWidth;
var browserHeight;
// Specify the number of snowflakes you want visible
var numberOfSnowflakes = 50;
// Flag to reset the position of the snowflakes
var resetPosition = false;
//
// It all starts here...
//
function setup() {
window.addEventListener("DOMContentLoaded", generateSnowflakes, false);
window.addEventListener("resize", setResetFlag, false);
}
setup();
//
// Vendor prefix management
//
function getSupportedPropertyName(properties) {
for (var i = 0; i < properties.length; i++) {
if (typeof document.body.style[properties[i]] != "undefined") {
return properties[i];
}
}
return null;
}
//
// Constructor for our Snowflake object
//
function Snowflake(element, radius, speed, xPos, yPos) {
// set initial snowflake properties
this.element = element;
this.radius = radius;
this.speed = speed;
this.xPos = xPos;
this.yPos = yPos;
// declare variables used for snowflake's motion
this.counter = 0;
this.sign = Math.random() < 0.5 ? 1 : -1;
// setting an initial opacity and size for our snowflake
this.element.style.opacity = .1 + Math.random();
this.element.style.fontSize = 12 + Math.random() * 50 + "px";
}
//
// The function responsible for actually moving our snowflake
//
Snowflake.prototype.update = function () {
// using some trigonometry to determine our x and y position
this.counter += this.speed / 5000;
this.xPos += this.sign * this.speed * Math.cos(this.counter) / 40;
this.yPos += Math.sin(this.counter) / 40 + this.speed / 30;
// setting our snowflake's position
setTranslate3DTransform(this.element, Math.round(this.xPos), Math.round(this.yPos));
// if snowflake goes below the browser window, move it back to the top
if (this.yPos > browserHeight) {
this.yPos = -50;
}
}
//
// A performant way to set your snowflake's position
//
function setTranslate3DTransform(element, xPosition, yPosition) {
var val = "translate3d(" + xPosition + "px, " + yPosition + "px" + ", 0)";
element.style[transformProperty] = val;
}
//
// The function responsible for creating the snowflake
//
function generateSnowflakes() {
// get our snowflake element from the DOM and store it
var originalSnowflake = document.querySelector(".snowflake");
// access our snowflake element's parent container
var snowflakeContainer = originalSnowflake.parentNode;
// get our browser's size
browserWidth = document.documentElement.clientWidth;
browserHeight = document.documentElement.clientHeight;
// create each individual snowflake
for (var i = 0; i < numberOfSnowflakes; i++) {
// clone our original snowflake and add it to snowflakeContainer
var snowflakeCopy = originalSnowflake.cloneNode(true);
snowflakeContainer.appendChild(snowflakeCopy);
// set our snowflake's initial position and related properties
var initialXPos = getPosition(50, browserWidth);
var initialYPos = getPosition(50, browserHeight);
var speed = 5+Math.random()*40;
var radius = 4+Math.random()*10;
// create our Snowflake object
var snowflakeObject = new Snowflake(snowflakeCopy,
radius,
speed,
initialXPos,
initialYPos);
snowflakes.push(snowflakeObject);
}
// remove the original snowflake because we no longer need it visible
snowflakeContainer.removeChild(originalSnowflake);
// call the moveSnowflakes function every 30 milliseconds
moveSnowflakes();
}
//
// Responsible for moving each snowflake by calling its update function
//
function moveSnowflakes() {
for (var i = 0; i < snowflakes.length; i++) {
var snowflake = snowflakes[i];
snowflake.update();
}
// Reset the position of all the snowflakes to a new value
if (resetPosition) {
browserWidth = document.documentElement.clientWidth;
browserHeight = document.documentElement.clientHeight;
for (var i = 0; i < snowflakes.length; i++) {
var snowflake = snowflakes[i];
snowflake.xPos = getPosition(50, browserWidth);
snowflake.yPos = getPosition(50, browserHeight);
}
resetPosition = false;
}
requestAnimationFrame(moveSnowflakes);
}
//
// This function returns a number between (maximum - offset) and (maximum + offset)
//
function getPosition(offset, size) {
return Math.round(-1*offset + Math.random() * (size+2*offset));
}
//
// Trigger a reset of all the snowflakes' positions
//
function setResetFlag(e) {
resetPosition = true;
}
Thank you in advance for your help!
Greatly appreciated
When generating the snowflakes, try to catch the 10th one and apply what you want
...
function generateSnowflakes() {
...
// create each individual snowflake
for (var i = 0; i < numberOfSnowflakes; i++) {
// clone our original snowflake and add it to snowflakeContainer
var snowflakeCopy = originalSnowflake.cloneNode(true);
snowflakeContainer.appendChild(snowflakeCopy);
// <------------------------------------------>
// catch the tenth snowflake and add your changes here
if (i === 9) {
snowflakeCopy.classList.add("clickable");
snowflakeCopy.addEventListener("click",function (e) {
//the stuff you want to do
//when the 10th snowflake is clicked
// for example :
window.location.replace("URL/OF/PRIZE");
})
}
....
}
...
```

I'm stuck in a project about snow animation in javascript

I have to create a script that simulates the animation of snow in javascript (I would prefer without using canvas if possible). I'm stuck at this point and I don't know where the mistake is. when I run the program google chrome crashes and the console does not give me errors, I think the error is due to a loop or some incorrect statements, I am attaching the code hoping for your help!
var direction=true; //true=right false =left
var active=true; //if true run the cycle,false stops the cycle
function startSnow() {
var _snowflakes = new Array(30);
var wid=50; //distance from a snowflake to another
var counterX; //var for editing the x position
var counterY; //var for editing the y position
for (var i=0; i< 30; i++) //cycle that initializes snowflakes
{
_snowflakes[i] = document.createElement("img");
_snowflakes[i].setAttribute("src","snowflake.png"); // setting the image
_snowflakes[i].setAttribute("class", "snowflake"); //setting the css style
_snowflakes[i].style.visibility ="inherit"; //when the function is running snowflakes have to be visible, when it finishes they have to be invisible
_snowflakes[i].style.right = (wid*i)+"px"; //set the distance from the left margin
_snowflakes[i].style.top = "30px"; // set the distance from the top
document.getElementById("background").appendChild(_snowflakes[i]);
}
while(active)
{
move();
}
function move() //function that moves the snowflake
{
for(;;)
{
if(counterY>=600) //when the snowflake reaches 600px from the top the function has to stop and hide snowflakes
{
for(var i=0;i<30;i++)
_snowflakes[i].style.visibility = "hidden";
active=false;
break;
}
else
{
if ((counterY%50)==0)
{
direction=!direction //every 50 Y pixels the snoflake change direction
}
counterY++;
for(var i=0;i<30;i++)
{
_snowflakes[i].style.top = counterY+"px"; //Y movement
if (direction==true)
{
_snowflakes[i].style.right = (_snowflakes[i].offsetLeft+counterX) + "px"; //x right movement
counterX++;
}
else
{
_snowflakes[i].style.right = (_snowflakes[i].offsetLeft+counterX) + "px"; //x left movement
counterX--;
}
}
}
}
}
}
Indeed, your code gets into an infinite loop because your variable counterY is not initialised and so it has the value undefined. Adding one to undefined gives NaN, and so you never get to that break statement.
But more importantly, you need a different approach, because even if you fix this, you'll never see an animation happening. For actually seeing the animation, you need to give the browser time to update the display. So a while and for(;;) loop are out of the question.
Instead call move once, and inside that function, in the else block, call requestAnimationFrame(move). This will give the browser time to repaint before move is called a gain. Remove the for(;;) loop, and remove the break.
There is also a logical error in how you move horizontally. As you read the current offsetLeft it makes no sense to increment counterX as that will lead to increasing (relative) jumps to the right (or left). Instead you should just add (or subtract) 1 to offsetLeft -- you can do away with counterX. Also, don't assign that to style.right, but to style.left.
So everything put together (cf. comments where there is a change):
var direction=true;
var active=true;
function startSnow() {
var _snowflakes = new Array(30);
var wid=50;
// var counterX = 0; -- not used.
var counterY = 0; // Need to initialise!
for (var i=0; i< 30; i++) {
_snowflakes[i] = document.createElement("img");
_snowflakes[i].setAttribute("src", "snowflake.png");
_snowflakes[i].setAttribute("alt", "❄"); // added for when image not found
_snowflakes[i].setAttribute("class", "snowflake");
_snowflakes[i].style.visibility = "inherit";
_snowflakes[i].style.left = (wid*i)+"px"; // assign to style.left
_snowflakes[i].style.top = "30px";
document.getElementById("background").appendChild(_snowflakes[i]);
}
// No while loop. Just call move
move();
function move() {
//No for (;;) loop
if (counterY>=600) {
for (var i=0;i<30;i++) {
_snowflakes[i].style.visibility = "hidden"; // you forgot the underscore
}
active=false;
// No break -- function will just return
} else {
if ((counterY%50)==0) {
direction=!direction
}
counterY++;
for (var i=0;i<30;i++) {
_snowflakes[i].style.top = counterY+"px";
if (direction==true) {
// assign to style.left,
_snowflakes[i].style.left = (_snowflakes[i].offsetLeft+1) + "px"; // add 1
} else {
_snowflakes[i].style.left = (_snowflakes[i].offsetLeft-1) + "px"; // sub 1
}
}
requestAnimationFrame(move); // schedule next run of this function
}
}
}
startSnow(); // Need to start it!
#background { width: 800px; height: 800px }
.snowflake { position: absolute; font-size: 40px }
<div id="background">
</div>

Paperjs bounce animation

Theres some wierd behaviour when playing with Paperjs, i was trying to curve a line up with 7 points separately - which works fine once, but when trying to make the link overshoot and return to 3 different points (to create a bounce effect) doesn't seem to play ball. On the second if statement, the 'counter' variable doesnt seem to increase instead of decrease, '+ steps' instead of '- steps'.
Maybe i'm not using if statements properly in this case, or paperjs has some strange behaviour?
Heres the codepen for it in full, click above the blue line to trigger it off. . Following is one setInterval for one of the points of the segment.
var seg6first = true;
var seg6sec = false;
var seg6thir = false;
setInterval(function() {
if (seg6first == true) {
counter = counter - steps;
if (counter >= 230) {
path.segments[6].point.y = counter;
path.smooth(); }
else {
seg6first = false;
seg6sec = true;
}
}
if (seg6sec == true) {
counter = counter + steps;
if (counter <= 260) {
path.segments[6].point.y = counter;
path.smooth();}
else {
seg6sec = false;
seg6thir = true;
}
}
if (seg6sec == true) {
counter = counter - steps;
if (counter >= 250) {
path.segments[6].point.y = counter;
path.smooth(); }
else {
seg6thir = false;
}
}
}, mintiming);
Thanks!
Rather than manually building your bounce effect, you can use an animation library like GSAP.
It has a lot of features that will make your task easier (see easing documentation).
Here is an example of what you are trying to do (click on the canvas to animate the line).
html,
body {
margin: 0;
overflow: hidden;
height: 100%;
}
canvas[resize] {
width: 100%;
height: 100%;
}
<canvas id="canvas" resize></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.11.8/paper-full.min.js"></script>
<script type="text/paperscript" canvas="canvas">
// user defined constants
var SEGMENTS_COUNT = 6;
var CURVE_HEIGHT = 80;
var ANIMATION_DURATION = 2;
// init path
var path = new Path({
fillColor: 'orange',
selected: true
});
// add points
for (var i = 0; i <= SEGMENTS_COUNT; i++) {
path.add([view.bounds.width * i / SEGMENTS_COUNT, view.center.y]);
}
// on mouse down...
function onMouseDown() {
// ...animate points
for (var i = 0, l = path.segments.length; i < l; i++) {
// get a reference to the point
var point = path.segments[i].point;
// calculate offset using sine function to form a curve
var offset = CURVE_HEIGHT * Math.sin(point.x * Math.PI / view.bounds.width);
// register animation
TweenLite.fromTo(
// target
point,
// duration
ANIMATION_DURATION,
// initial value
{ y: view.center.y },
{
// final value
y: view.center.y - offset,
// easing
ease: Elastic.easeOut.config(1, 0.3),
// on update...
onUpdate: function() {
// ...smooth the path
path.smooth();
}
}
);
}
}
</script>

How to add elements fast but not instantly in javascript?

I would like to fill an element with dots in random order. I have managed to write all the functionality, but I am not satisfied with the execution speed.
If I add all of the points using a while loop, the points just seem to appear all at the same time.
Therefore I add points one by one using a function that I call recursively with a timeout. This, on the other hand, appears too slow. Is there any chance to run a sequence of actions slower than in a loop but faster than setTimeout() can?
var dotCellSize;
var initialOffset;
var slotsHorizontally;
var slotsVertically;
var container;
var redDots;
var dots;
var newDotElement = $('<div class="dot">');
function randomInteger(min,max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function addDots()
{
if (!dots.length)
return;
var dotIndex = randomInteger(0, dots.length - 1);
var dot = dots[dotIndex];
dots.splice(dotIndex, 1);
var column = dot % slotsHorizontally;
var row = Math.floor(dot/slotsHorizontally);
var position = {
left: initialOffset + column*dotCellSize,
top: initialOffset + row*dotCellSize
};
var dotElement = newDotElement.clone().css(position);
if (-1 != redDots.indexOf(dot))
dotElement.addClass('red');
dotElement.appendTo(container);
setTimeout(function() {
addDots();
}, 1);
}
function generateDots(dotContainer, cellSize, numberOfRedDots)
{
container = dotContainer;
dotCellSize = cellSize;
dots = [];
redDots = [];
container.find('div.dot').remove();
numberOfRedDots = typeof numberOfRedDots !== 'undefined' ? numberOfRedDots : 3;
initialOffset = Math.floor(dotCellSize/2);
slotsHorizontally = Math.ceil(container.width()/dotCellSize);
slotsVertically = Math.ceil(container.height()/dotCellSize);
var numberOfSlots = slotsHorizontally*slotsVertically;
while (dots.length < numberOfSlots)
dots.push(dots.length);
while (redDots.length < numberOfRedDots)
{
var newRedDot = randomInteger(0, numberOfSlots - 1);
if (-1 == redDots.indexOf(newRedDot))
redDots.push(newRedDot);
}
addDots();
}
generateDots($('.dot-container'), 18, 15);
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #C0E3EA;
position: absolute;
z-index: 1;
}
.dot.red {
background-color: #EF3D48;
}
.dot-container {
width: 420px;
height: 280px;
background-color: #333;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="dot-container"></div>
Unfortunately, not really, this is because of how the browser engine decides to repaint the screen. Without the timeout, the browser engine recognizes it's going to do a bunch of updates (adding the dots to the DOM). Because repainting the screen is expensive, it waits to do as much as possible at one time, and, in your case, all of the dots show up at once. With the timeout added, each call to your function gets "deferred" for future execution.
This may or may not happen "right away" and is non-trivial to explain in detail so I would recommend watching this video https://www.youtube.com/watch?v=8aGhZQkoFbQ which explains the JS event loop or read some articles on browser reflow:
Minimizing browser reflow
What is Layout Thrashing?
Without changing much of what you've already done, one solution is to batch a few of the dots to be drawn together. I've added a for loop to your function which will make five dots get drawn together. Adjust this to 10, 20, or higher and you'll see the dots get painted even faster. I hope there is a number that you'll find suitable. I understand you may want to just speed up the drawing of every dot individually, but bear in mind that screens have refresh rates, so the faster you want the routine to finish the more they will appear in batches any way.
var dotCellSize;
var initialOffset;
var slotsHorizontally;
var slotsVertically;
var container;
var redDots;
var dots;
var newDotElement = $('<div class="dot">');
function randomInteger(min,max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function addDots()
{
if (!dots.length)
return;
for (let i = 0; i < 5; i++) {
var dotIndex = randomInteger(0, dots.length - 1);
var dot = dots[dotIndex];
dots.splice(dotIndex, 1);
var column = dot % slotsHorizontally;
var row = Math.floor(dot/slotsHorizontally);
var position = {
left: initialOffset + column*dotCellSize,
top: initialOffset + row*dotCellSize
};
var dotElement = newDotElement.clone().css(position);
if (-1 != redDots.indexOf(dot))
dotElement.addClass('red');
dotElement.appendTo(container);
}
setTimeout(function() {
addDots();
}, 1);
}
function generateDots(dotContainer, cellSize, numberOfRedDots)
{
container = dotContainer;
dotCellSize = cellSize;
dots = [];
redDots = [];
container.find('div.dot').remove();
numberOfRedDots = typeof numberOfRedDots !== 'undefined' ? numberOfRedDots : 3;
initialOffset = Math.floor(dotCellSize/2);
slotsHorizontally = Math.ceil(container.width()/dotCellSize);
slotsVertically = Math.ceil(container.height()/dotCellSize);
var numberOfSlots = slotsHorizontally*slotsVertically;
while (dots.length < numberOfSlots)
dots.push(dots.length);
while (redDots.length < numberOfRedDots)
{
var newRedDot = randomInteger(0, numberOfSlots - 1);
if (-1 == redDots.indexOf(newRedDot))
redDots.push(newRedDot);
}
addDots();
}
generateDots($('.dot-container'), 18, 15);
.dot {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #C0E3EA;
position: absolute;
z-index: 1;
}
.dot.red {
background-color: #EF3D48;
}
.dot-container {
width: 420px;
height: 280px;
background-color: #333;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="dot-container"></div>
Quickly profiling on my i7 3770k revealed that newDotElement.clone().css(position) took about .1 seconds. If you are running at 30 FPS, your frame time is .03 seconds. So you can see that Jquery clone is somewhat of a bottleneck.
However, your initial approach of drawing all the dots at once is sound, if you flag their styles to be "hidden". Then, when all the dots are added to the DOM, but are not visible, retrieve a list of their nodes (forgive the vanilla JS):
Array.from(document.getElementsByClassName("dot-container")[0].childNodes);
Now you can iterate over them and simply change their visibility style from "hidden" to "visible". As skyline3000 points out, the limit with setTimeout (or even requestAnimationFrame) is in the browser, and looping and setting one dot per iteration will take a little over 1 frame, which is actually a little slow. So you can write yourself a little abraction which per call will set a certain number of elements' visibility styles to "visible". By adjusting the quantity of dots you make visible per call, you will speed up or slow down the animation.
function showDots() {
var list = Array.from(document.getElementsByClassName("dot-container")[0].childNodes);
function draw(q) {
var e;
for (var i = 0; i < q; i++) {
if (list.length == 0) {
return;
}
e = list.shift();
e.style.visibility = "visible";
}
}
function callback() {
if (list.length == 0) {
return;
}
draw(4);
setTimeout(callback);
}
callback();
}

Ordering a graphical list via mouse dragging using JavaScript

NOTE: Exact description of question follows CSS below. Sample code can be seen in this fiddle.
I have a parent div with a list of child divs within it, that looks like the following:
HTML for said container and children is:
<div class="categories_container">
<div class="category one">One</div>
<div class="category two">Two</div>
<div class="category three">Three</div>
<div class="category four">Four</div>
<div class="category five">Five</div>
<div class="category six">Six</div>
</div>
Where the classes .one, .two, .three, etc... are their relative position in the list.
The children elements are positioned with absolute positioning, within their parent.
CSS as follows (some properties not shown for simplicity):
.categories_container {
height: 324px;
width: 100%;
position: relative;
}
.category {
height: 50px;
width: 98%;
position: absolute;
left: 0px;
z-index: 0;
}
.one {
top: 0px;
}
.two {
top: 54px;
}
.three {
top: 108px;
}
.four {
top: 162px;
}
.five {
top: 216px;
}
.six {
top: 270px;
}
As can be seen in this fiddle, you can click (and hold) on any one of the child elements and move it up and down within the parent div. When you release the mouse, the selected child snaps back to its original position.
Question:
How can I detect if the selected element has been dragged overtop of another? I don't only want to know if they are overlapping, but would like to put a range on it. Something like...
if(center of current child is overtop a set range within another child){
do stuff...
}
What I'd like to do for now (as a proof of concept) is to have the underneath child's background color change WHILE the vertical center of the selected child is within the range 0.4-0.6 of the bottom child's height. If the selected child is dragged out of said region, the background should change back.
I've tried something like:
$('.category').mouseover(function(){
if(dragging){
... execute code...
}
});
But it seems that if I am dragging one element over the other, the bottom element cannot see the mouse, and so the function is never executed.
Also:
I've tried a few different methods to keep the cursor as a pointer while dragging, but no matter what it switches to the text cursor whilst dragging. So any help with that would also be appreciated.
For the pointer thing I've tried putting $(this).css('cursor', 'pointer'); in the mousedown and mouse move functions, but to no avail.
Thanks in advance! Sorry if any of this is confusing.
Here is the solution I came up with, purely with JS and JQuery, with no external libraries required and without using JQueryUI Sortables.
HTML:
<div class="list_container">
<div class="list_item">One</div>
<div class="list_item">Two</div>
<div class="list_item">Three</div>
<div class="list_item">Four</div>
<div class="list_item">Five</div>
<div class="list_item">Six</div>
</div>
where list_container holds the individual list_item elements. Is it the latter of the two which can be moved around to create your sorted list. You can put just about anything you'd like within list_item and it'll still work just fine.
CSS:
.list_container {
position: relative;
}
.list_item {
position: absolute;
z-index: 0;
left: 0px;
}
.list_item.selected {
z-index: 1000;
}
Please visit this fiddle for the full list of CSS rules (only necessary ones are shown above).
JavaScript:
I'll go through this bit-by-bit and then show the full code at the bottom.
First off, I defined an array that matches up index numbers with their written counterparts
var classes = new Array("one", "two", "three", ...);
This is used to create classes dynamically (upon page load). These classes are used to order the list. You are only required to populate this array with as many items as you will have in your list. This is the one downfall of the code I have written and am unsure of how to overcome this issue (would be VERY tedious to enter in the elements for a list of hundreds of items, or more!)
Next, a few other variables:
var margin = 2; // Space desired between each list item
var $el; // Used to hold the ID of the element that has been selected
var oldPos = 0; // The position of the selected element BEFORE animation
var newPos = 0; // The position of the selected element AFTER animation (also current position)
var dragging = false; // Whether or not an item is being moved
var numElements = $('.list_container > div').length;
// selectionHeight is the height of each list element (assuming all the same height)
// It includes the div height, the height of top and bottom borders, and the desired margin
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = ''; // classInfo will be populated with the information that is used to dynamically create classes upon page load
When page loads, go through each list_item and assign it a class according to its initial position in the list. Also add to classInfo the location of the TOP of the list item.
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
Now, using classInfo that was created above, dynamically write the classes to the page.
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
The above bit of code will write the required classes into the HTML of the page. If you view the source of the page, you can see the classes in the head of the page.
Now for the ordering part. First, mousedown
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY; // Gets the current mouse position
startT = parseInt($el.css('top')); // Gets the current position of the TOP of the item
$el.addClass('selected'); // Adding class brings it to top (z-index) and changes color of list item
});
Next, the mousemove and mouseup functions are tied together
$(window).mousemove(function (ev) { // Use $(window) so mouse can leave parent div and still work
if (dragging) {
$el.attr('class', 'list_item') // Remove the numbered class (.one, .two, etc)
$el.addClass('selected'); // Add this class back for aesthetics
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight); // Determine what the current position of the selected list item is
// If the position of the list item has changed, move the position's current element out of the way and reassign oldPos to newPos
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false; // User is no longer dragging
$el.removeClass('selected'); // Element is no longer selected
setNewClass($el, newPos); // Set the new class of the moved list item
$el.css('top', (newPos - 1) * selectionHeight); // Position the moved element where it belongs. Otherwise it'll come to rest where you release it, not in its correct position.
});
Finally, the three functions getPos, moveThings and setNewClass are as follows:
function getPos(a, b) { // a == newTop, b == selectionHeight
return Math.round( (a/b) + 1 );
}
getPos works by finding out which region the selected element is currently in. If newTop is less than .5b, then it is in region 1. If between .5b and 1.5b, then it is region 2. If between 1.5b and 2.5b, then in region 3. And so on. Write out a few cases on a piece of paper and it'll make sense what is happening.
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1]; // What is the current class of the item that will be moved
var $newEl = $('.list_container .' + first); // ID of element that will be moved
if (a < b) { // oldPos less than newPos
var second = classes[b - 2]; // The new class of the moved element will be one less
var newTop = parseInt($newEl.css('top')) - c; // Top of element will move up
} else { // oldPos more than newPos
var second = classes[b]; // The new class of the moved element will be one more
var newTop = parseInt($newEl.css('top')) + c; // Top of element will move down
}
// The following line of code is required, otherwise the following animation
// will animate of from top=0px to the new position (opposed to from top=currentPosition)
// Try taking it out and seeing
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first); // Remove the current numbered class of element to move
// Move element and remove the added style tags (or future animations will get buggy)
$newEl.animate({top: newTop}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second); // Add the new numbered class
return false; // Cleans up animations
}
The function above is what does the actual animation part and moves the list items around to accommodate the selected list item.
function setNewClass(e, a) { // e == selected element, a == newPos
// Remove 'selected' class, then add back the 'list_item' class and the new numbered class
e.attr('class', 'list_item').addClass(classes[a-1]);
}
** All JavaScript together: **
var classes = new Array("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeem", "eighteen", "nineteen", "twenty", "twentyone", "twentytwo", "twentythree", "twentyfour");
$(document).ready(function () {
var margin = 2;
var $el;
var oldPos = 0;
var newPos = 0;
var dragging = false;
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = '';
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY;
startT = parseInt($el.css('top'));
$el.addClass('selected');
});
$(window).mousemove(function (ev) {
if (dragging) {
$el.attr('class', 'list_item')
$el.addClass('selected');
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight);
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false;
$el.removeClass('selected');
setNewClass($el, newPos);
$el.css('top', (newPos - 1) * selectionHeight);
});
});
function getPos(a, b) { // a == topPos, b == selectionHeight
return Math.round((a / b) + 1);
}
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1];
var $newEl = $('.list_container .' + first);
if (a < b) { // oldPos less than newPos
var second = classes[b - 2];
var newTop = parseInt($newEl.css('top')) - c;
} else { // oldPos more than newPos
var second = classes[b];
var newTop = parseInt($newEl.css('top')) + c;
}
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first);
$newEl.animate({
top: newTop
}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second);
return false; // Cleans up animations
}
function setNewClass(e, a) { // e == selected element, a == newPos
e.attr('class', 'list_item').addClass(classes[a - 1]);
}

Categories

Resources