Javascript roulette table, drag and drop - javascript

Good morning everyone,
I need to create a roulette table where you can drag a poker chip on the number(graphically) using only javascript/css/html.
Question 1: What would be the best approach for this?
Question 2: I thought of using 2 for loops to initialize divs with all the numbers in them and then use drag and drop(http://luke.breuer.com/tutorial/javascript-drag-and-drop-tutorial.aspx) to "snap" the poker chip image to the closest div when it is released. Euhm... How would I know where to snap the image without using hundreds of if/else?
Thank you very much! Have a nice day! :)
Update:
<!DOCTYPE HTML>
<html>
<head>
<style type="text/css">
#div1 {
width: 20px;
height: 40px;
padding: 10px;
border: 1px solid #aaaaaa;
}
#game {width:350px;height:70px;padding:10px;border:1px solid #aaaaaa;}
</style>
</head>
<body>
<p>Drag the W3Schools image into the rectangle:</p>
<br>
<div id="game">
</div>
<img id="drag1" src="images/c.png" draggable="true" ondragstart="drag(event)" width="336" height="69">
<script>
function allowDrop(ev)
{
ev.preventDefault();
}
function drag(ev)
{
ev.dataTransfer.setData("Text",ev.target.id);
}
function drop(ev)
{
ev.preventDefault();
var data=ev.dataTransfer.getData("Text");
ev.target.appendChild(document.getElementById(data));
}
function dragAlert() {
alert("Ondrag");
}
var numberCount = 1;
var divId = 0;
var div = document.createElement("div");
div.className = "div1";
/*divId = "id";
divId += numberCount;*/
div.id = "div1";
div.ondrop = function() {
drop(event);
};
div.ondragover = function() {
allowDrop(event);
};
document.getElementById("game").appendChild(div);
numberCount++;
</script>
</body>
</html>
Does anyone know why ondragover/ondrop doesn't work with this code?

I think you'd like interact.js. It's a standalone JavaScript drag and drop, resize and multitouch gesture module I wrote. I recently added snapping; it allows you to snap to a custom grid or more suitably in this case, custom anchors.
To make element draggable, you would call interact(element).draggable(true) then bind listeners to dragmove events:
interact(element).bind('dragmove', function (event) {
// use event.dx, event.dy and normal MouseEvent properties
// to move the element
});
And to configure the snapping to custom anchors:
interact.snap({
mode: 'anchors',
anchors: [
{x: 300, y: 100, range: 50},
{x: 100, y: 400, range: 40},
{x: 500, y: 400},
{x: 500, y: 500}
],
range: 30
});
I wrote a blog post explaining how to use it here: Drag and drop snap to grid with interact.js. The post also links to an interactive demo.

Related

How to get the viewport size excluding scrollbars in pure JavaScript?

I am now rewriting some old codes to work with modern browsers. One problem is persistent to me: the viewport size. I wish to put a DIV element in the center but with the scrollbar, it won't be right. Also, I wish to put a button near the right border but the scrollbar is rendered over my button.
I am using plain simple...
window.innerWidth
... to get the width, but won't consider vertical scrollbar, if there is. I have looked inside MDN documentation for something to give me the right number...
document.body.clientWidth (give a smaller number, about twice width of scrollbar)
Also this property has a special behavior for body, but to me is just wrong.
I did't find a way to get the size of each scrollbar, so I am now out of ideas.
I know this is asked a lot, as here, but I wish to go to the bottom of this subject thinking in new browsers and latest ECMAScript definitions, with some retro compatibility.
I did a webpage to play with those properties and it's values. It seems the only way to get what is needed in this case, the right green box positioning, is thought empiricism. So this is the code:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
#result { width:2000px; height:1600px; }
#bottomright {
position: fixed;
right: 0px;
bottom: 0px;
width: 64px;
height: 64px;
border: black solid 1px;
}
</style>
<script type="text/javascript">
function ini() {
var Sizes = [
{ method:"window.inner", color:"red", width:window.innerWidth, height:window.innerHeight },
{ method:"document.body.offset", color:"blue", width:document.body.offsetWidth, height:document.body.offsetHeight }
];
var html = "<h2>Methods do get de window size:</h2>";
for(let i=0; i<Sizes.length; i++) {
html += '<p style="color:'+Sizes[i].color+';">'+Sizes[i].method+'</p>';
document.body.appendChild(Box(0, 0, 64, 64, Sizes[i].color));
document.body.appendChild(Box(Sizes[i].width-66, 0, 64, 64, Sizes[i].color));
document.body.appendChild(Box(0, Sizes[i].height-66, 64, 64, Sizes[i].color));
document.body.appendChild(Box(Sizes[i].width-66, Sizes[i].height-66, 64, 64, Sizes[i].color));
}
const size = realSize();
document.body.appendChild(Box(size.width-34, size.height-34, 32, 32, "green"));
html += '<p style="color:green;">Real Size</p>';
document.getElementById("result").innerHTML = html;
}
function Box(x, y, w, h, color) {
const elm = document.createElement("div");
elm.style.border = color+" solid 1px";
elm.style.position = "fixed";
elm.style.left = x.toString()+"px";
elm.style.top = y.toString()+"px";
elm.style.width = w.toString()+"px";
elm.style.height = h.toString()+"px";
return elm;
}
function realSize() {
const elm = document.createElement("div");
elm.style.id = "sizeFinder"; // will look at later
elm.style.border = "0px"; // just in case
elm.style.position = "fixed";
elm.style.right = "0px"; // the real right
elm.style.bottom = "0px"; // the real bottom
elm.style.width = "10px"; // some width, just in case
elm.style.height = "10px"; // some height, just in case
document.body.appendChild(elm);
const box = elm.getBoundingClientRect()
return { width:box.x+10, height:box.y+10 };
}
</script>
<title>Window Size Test</title>
</head>
<body onload="ini();">
<h1>Window Size Test</h1>
<div id="result"></div>
<div id="bottomright"></div>
</body>
</html>
This is the result:
Note that I did the green box with half size to not cover the other boxes. Also, might be a good idea to have an invisible box fixed at the corner to get the actual window size when needed, thought is what I will do.
Please test and share comments, I am still worry if this will work with any browsers and have some retro compatibility.

How to drag and drop items between 2 columns with "Konva.JS"?

I'm looking for a way to drag and drop items between 2 columns with Konva.JS.
Since I found a sample code using Sortable.JS, I ported it and wrote the following code. With this code, I expected there were two vertical independent scroll bars, such as in this example image:
However, there aren't, as shown in this image from running my code:
.
My code:
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/konva#5.0.2/konva.min.js"></script>
<meta charset="utf-8" />
<style>
body {
margin: 0;
padding: 0;
background-color: #f0f0f0;
height:100%;
overflow: hidden;
}
#Leftcontainer {
overflow: auto;
}
#Rightcontainer {
overflow: auto;
}
</style>
</head>
<body>
<div id="container">
<div id="Leftcontainer"></div> <!-- Left Column -->
<div id="Rightcontainer"></div> <!-- Right Column -->
</div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: 60*1090,
});
//Layer 1
var layer = new Konva.Layer();
stage.add(layer);
//Layer 2
var tempLayer = new Konva.Layer();
stage.add(tempLayer);
//Load Image (Group of Left Column)
var leftGroup = new Konva.Group({
id: 'Leftcontainer',
});
layer.add(leftGroup);
for (let i = 0; i < 100; i++) {
var imageObj = new Image();
imageObj.src = './assets/apple.jpg';
imageObj.addEventListener('load', function() {
var dragImage = new Konva.Image({
x: 5,
y: 20+(5+100)*i,
image: imageObj,
width: 100,
height: 100,
draggable: false,
});
leftGroup.add(dragImage);
layer.draw();
});
};
//Load Text (Group of Right Column)
var rightGroup = new Konva.Group({
id: 'Rightcontainer',
});
layer.add(rightGroup);
for (var i = 0; i < 100; i++) {
var WordLabel = new Konva.Label({
x: 300,
y: 18+60*i,
opacity: 0.75,
draggable: true,
fill: 'green',
});
WordLabel.add(
new Konva.Tag({
fill: 'green',
lineJoin: 'round'
})
);
WordLabel.add(
new Konva.Text({
text: "Apple",
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white',
})
);
rightGroup.add(WordLabel);
};
layer.draw();
</script>
</body>
</html>
It would appear that you are intending there to be two columns on your page since you have the HTML for the container, Leftcontainer and Rightcontainer. You then appear to be setting a Konva stage in the 'container' div but later creating Konva groups to co-relate to left + right containers. You then make these groups long and expect there to be vertical scrollbars.
You are making a false assumption regarding the relationship between the HTML5 canvas (for which Konva is a wrapper) and its interaction with HTML elements. The basic principle is that an HTML5 canvas 'lives' inside a single HTML element. You cannot 'share' bits of it between HTML elements in the way that you are attempting.
[Aside: Under the covers, Konva DOES create a stage per layer, but that still does not allow placing those layers into other host containers than the main stage.]
Options:
1 - you do not specifically require a canvas-based solution to provide an image-based drag & drop. You would already have found this with sortable.js. But if you are simply using this as a learning activity to understand the canvas then well done you!
2 - continuing this the canvas approach, your basic architecture of a stage and a group per column is reasonable. But you have to take care of producing the scroll bars since in the world of canvas there are no such handy UI shortcuts.
3 - again following a canvas solution, ignore the main 'container' element, and create a stage in EACH of the left and right container elements to represent your left and right columns. Draw the content, then approach the problem as one of dragging an element from one canvas to another.

How to change first view position

I use OpenSeadragon 1.2.1.
I want to show wide image(4096 x 2160),
and change first view position.
x: 640px;
y: 320px;
width: 1024px;
height:768px;
crip(320px, 1664px, 1088px, 640px);
HTML code
<div id="mycanvas" style="width:1024px;height:768px;"></div>
<script src="./openseadragon.min.js"></script>
<script>
var viewer = OpenSeadragon(
{
id: "mycanvas",
prefixUrl: "./images/",
tileSources: "./dzc_output_images/datas.xml"
});
viewer.addHandler('open', function()
{
// I want to change first view position.
// viewer.???
// viewer.viewport.applyConstraints();
}
</script>
Use "Class:Rect / Class:DisplayRect" or other Classes ?
https://openseadragon.github.io/docs/OpenSeadragon.Rect.html
https://openseadragon.github.io/docs/OpenSeadragon.DisplayRect.html
How to use these Classes ?
Your best bet is to figure out the location you want to feature, as a rectangle in viewport coordinates (where 0 is the left side of the image and 1 is the right side). For instance (inside the "open" handler you've written):
var box = new OpenSeadragon.Rect(0.25, 0.25, 0.5, 0.5);
viewer.viewport.fitBounds(box, true);

Creating an swf on an swf with jQuery

I have this javascript file which is a modified version of the VideoLightBox script:
jQuery(function(){
var $=jQuery;
var swfID = "video_overlay";
if(!document.getElementById("vcontainer")){
$("body").append($("<div id='voverlay'></div>"));
$("#voverlay").append($("<div id = 'vcontainer'></div>"));
}
$("#videogallery a[rel]").overlay({
api:true,
expose: (0?{
color:'#424542',
loadSpeed:400,
opacity:0
}:null),
effect:"apple",
onClose: function(){
swfobject.removeSWF(swfID);
},
// create video object for overlay
onBeforeLoad: function(){
// check and create overlay contaner
var c = document.getElementById(swfID);
if(!c){
var d = $("<div></div>");
d.attr({id: swfID});
$("#vcontainer").append(d);
};
var wmkText="© 2011 BORKH";
var wmkLink="http://borkh.co.uk";
c = wmkText? $('<div></div>'):0;
if (c) {
c.css({
position:'absolute',
right:'38px',
top:'38px',
padding:'0 0 0 0'
});
$("#vcontainer").append(c);
};
// for IE use iframe
if (c && document.all){
var f = $('<iframe src="javascript:false"></iframe>');
f.css({
position:'absolute',
left:0,
top:0,
width:'100%',
height:'100%',
filter:'alpha(opacity=0)'
});
f.attr({
scrolling:"no",
framespacing:0,
border:0,
frameBorder:"no"
});
c.append(f);
};
var d = c? $(document.createElement("A")):c;
if(d){
d.css({
position:'relative',
display:'block',
'background-color':'',
color:'#626d73',
'font-family': 'RegisterSansBTNDmRegular, Helvetica, Arial',
'font-size':'11px',
'font-weight':'normal',
'font-style':'normal',
'text-decoration': 'none',
padding:'1px 5px',
opacity:.7,
filter:'alpha(opacity=70)',
width:'auto',
height:'auto',
margin:'0 0 0 0',
outline:'none'
});
d.attr({href:wmkLink});
d.html(wmkText);
d.bind('contextmenu', function(eventObject){
return false;
});
c.append(d);
}
// create SWF
var src = this.getTrigger().attr("href");
if (typeof(d)!='number' && (!c || !c.html || !c.html())) return;
if (false){
var this_overlay = this;
// if local
window.videolb_complite_event = function (){ this_overlay.close() };
// if youtoube
window.onYouTubePlayerReady = function (playerId){
var player = $('#'+swfID).get(0);
if (player.addEventListener) player.addEventListener("onStateChange", "videolb_YTStateChange");
else player.attachEvent("onStateChange", "videolb_YTStateChange");
window.videolb_YTStateChange = function(newState){
if (!newState) this_overlay.close()
}
}
}
swfobject.createSWF(
{ data:src, width:"100%", height:"100%", wmode:"opaque" },
{ allowScriptAccess: "always", allowFullScreen: true, FlashVars: (false?"complete_event=videolb_complite_event()&enablejsapi=1":"") },
swfID
);
}
}); });
The script opens a flash swf file in a "popup" lightbox fashion and plays it either from youtube or via a player locally. I was however wondering if it was possible to create a secondary swf to float on top of the player (noting that this of course would have the wmode:"transparent") and hereby create an opening curtain effect revealing the first swf and the player. I've been trying for quite some time now to load the top clip via createSWF and to create an additional div to contain it and float it using absolute position however I can't seem to get it right.. I know that the div float perfectly on top of each other when using:
<head>
<style type="text/css" media="screen">
<!--
#bottom{
position:absolute;
width: 500px;
height: 400px;
}
#top{
position:absolute;
width:500px;
height:400px;
top: 0px;
left: 0px;
}
-->
</style>
</head>
<body>
<div id="bottom">
"MAIN CLIP"
<div id="top">
"CURTAIN EFFECT"
</div>
</div>
However I'm not strong enough in javascripting to transfer it.
Any help, ideas, hints or suggestions are much appreciated!
Thanks
Andreas
I think your idea will work, but could get quite tricky.
What I would recommend instead is creating a "curtain swf" that instead loads something like the Chromeless YouTube player inside of it. This way you can listen for when the video is done loading/buffering and reveal the curtains when that happens.

How can I get jquery to execute animations in exact parallel?

I'm trying to create an accordion widget in jquery similar to jquery's accordion plugin, with the difference that I want the handles to appear below their respective content instead of above. My accordion works by decreasing the height of the open content section while at the same time increasing the height of the clicked content section. I've posted an example here. My problem is that the animations aren't started at exactly the same time, and there is a noticeable "jump" due to the slight delay before the second animation is started.
Scriptaculous has a function called Effect.Parallel that allows you to create an array of animation effects and execute them in parallel. Unfortunately, I can't seem to find something similar with jquery.
Is there a way I can run precise parallel animations on separate divs in jquery?
Edit: I'm as much interested in alternative methods of coding this accordion widget. So if there is any other method people think would work I'm open to that.
One more answer, hopefully my last one...
Unfortunately, John Resig's syncAnimate method is not quite up to snuff for the accordion-type animation I want to do. While it works great on Firefox, I couldn't get it working smoothly on IE or Safari.
With that said, I decided to bite the bullet and write my own animation engine that does simple parallel animations. The class-code uses jquery functions but is not a jquery plugin. Also, I've only set it up to do size/position animations, which is all I need.
ParallelAnimations = function(animations, opts){
this.init(animations, opts);
};
$.extend(ParallelAnimations.prototype, {
options: {
duration: 250
},
rules: {},
init: function(animations, opts){
// Overwrite the default options
$.extend(this.options, opts);
// Create a set of rules to follow in our animation
for(var i in animations){
this.rules[i] = {
element: animations[i].element,
changes: new Array()
};
for(var style in animations[i].styles){
// Calculate the start and end point values for the given style change
var from = this.parse_style_value(animations[i].element, style, "");
var to = this.parse_style_value(animations[i].element, style, animations[i].styles[style]);
this.rules[i].changes.push({
from: from,
to: to,
style: style
});
}
}
this.start()
},
/*
* Does some parsing of the given and real style values
* Allows for pixel and percentage-based animations
*/
parse_style_value: function(element, style, given_value){
var real_value = element.css(style);
if(given_value.indexOf("px") != -1){
return {
amount: given_value.substring(0, (given_value.length - 2)),
unit: "px"
};
}
if(real_value == "auto"){
return {
amount: 0,
unit: "px"
};
}
if(given_value.indexOf("%") != -1){
var fraction = given_value.substring(0, given_value.length - 1) / 100;
return {
amount: (real_value.substring(0, real_value.length - 2) * fraction),
unit: "px"
};
}
if(!given_value){
return {
amount: real_value.substring(0, real_value.length - 2),
unit: "px"
};
}
},
/*
* Start the animation
*/
start: function(){
var self = this;
var start_time = new Date().getTime();
var freq = (1 / this.options.duration);
var interval = setInterval(function(){
var elapsed_time = new Date().getTime() - start_time;
if(elapsed_time < self.options.duration){
var f = elapsed_time * freq;
for(var i in self.rules){
for(var j in self.rules[i].changes){
self.step(self.rules[i].element, self.rules[i].changes[j], f);
}
}
}
else{
clearInterval(interval);
for(var i in self.rules){
for(var j in self.rules[i].changes)
self.step(self.rules[i].element, self.rules[i].changes[j], 1);
}
}
}, 10);
},
/*
* Perform an animation step
* Only works with position-based animations
*/
step: function(element, change, fraction){
var new_value;
switch(change.style){
case 'height':
case 'width':
case 'top':
case 'bottom':
case 'left':
case 'right':
case 'marginTop':
case 'marginBottom':
case 'marginLeft':
case 'marginRight':
new_value = Math.round(change.from.amount - (fraction * (change.from.amount - change.to.amount))) + change.to.unit;
break;
}
if(new_value)
element.css(change.style, new_value);
}
});
Then the original Accordion class only needs to be modified in the animate method to make use of the new call.
Accordion = function(container_id, options){
this.init(container_id, options);
}
$.extend(Accordion.prototype, {
container_id: '',
options: {},
active_tab: 0,
animating: false,
button_position: 'below',
duration: 250,
height: 100,
handle_class: ".handle",
section_class: ".section",
init: function(container_id, options){
var self = this;
this.container_id = container_id;
this.button_position = this.get_button_position();
// The height of each section, use the height specified in the stylesheet if possible
this.height = $(this.container_id + " " + this.section_class).css("height");
if(options && options.duration) this.duration = options.duration;
if(options && options.active_tab) this.active_tab = options.active_tab;
// Set the first section to have a height and be "open"
// All the rest of the sections should have 0px height
$(this.container_id).children(this.section_class).eq(this.active_tab)
.addClass("open")
.css("height", this.height)
.siblings(this.section_class)
.css("height", "0px");
// figure out the state of the handles
this.do_handle_logic($(this.container_id).children(this.handle_class).eq(this.active_tab));
// Set up an event handler to animate each section
$(this.container_id + " " + this.handle_class).mouseover(function(){
if(self.animating)
return;
self.animate($(this));
});
},
/*
* Determines whether handles are above or below their associated section
*/
get_button_position: function(){
return ($(this.container_id).children(":first").hasClass(this.handle_class) ? 'above' : 'below');
},
/*
* Animate the accordion from one node to another
*/
animate: function(handle){
var active_section = (this.button_position == 'below' ? handle.prev() : handle.next());
var open_section = handle.siblings().andSelf().filter(".open");
if(active_section.hasClass("open"))
return;
this.animating = true;
// figure out the state of the handles
this.do_handle_logic(handle);
// Close the open section
var arr = new Array();
arr.push({
element: open_section,
styles: {
"height": "0px"
}
});
arr.push({
element: active_section,
styles: {
"height": this.height
}
});
new ParallelAnimations(arr, {duration: this.duration});
var self = this;
window.setTimeout(function(){
open_section.removeClass("open");
active_section.addClass("open");
self.animating = false;
}, this.duration);
},
/*
* Update the current class or "state" of each handle
*/
do_handle_logic: function(handle){
var all_handles = handle.siblings(".handle").andSelf();
var above_handles = handle.prevAll(this.handle_class);
var below_handles = handle.nextAll(this.handle_class);
// Remove all obsolete handles
all_handles
.removeClass("handle_on_above")
.removeClass("handle_on_below")
.removeClass("handle_off_below")
.removeClass("handle_off_above");
// Apply the "on" state to the current handle
if(this.button_position == 'below'){
handle
.addClass("handle_on_below");
}
else{
handle
.addClass("handle_on_above");
}
// Apply the off above/below state to the rest of the handles
above_handles
.addClass("handle_off_above");
below_handles
.addClass("handle_off_below");
}
});
The HTML is still called the same way:
<html>
<head>
<title>Parallel Accordion Animation</title>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="ui.js"></script>
<script type="text/javascript">
$(document).ready(function(){
new Accordion("#accordion");
});
</script>
<style type="text/css">
#accordion{
position: relative;
}
#accordion .handle{
width: 260px;
height: 30px;
background-color: orange;
}
#accordion .section{
width: 260px;
height: 445px;
background-color: #a9a9a9;
overflow: hidden;
position: relative;
}
</style>
</head>
<body>
<div id="accordion">
<div class="section"><!-- --></div>
<div class="handle">handle 1</div>
<div class="section"><!-- --></div>
<div class="handle">handle 2</div>
<div class="section"><!-- --></div>
<div class="handle">handle 3</div>
<div class="section"><!-- --></div>
<div class="handle">handle 4</div>
<div class="section"><!-- --></div>
<div class="handle">handle 5</div>
</div>
</body>
</html>
There are a few things I may add in the future:
- Queued Animations
- Animations for other types of styles (colors,etc)
John Resig posted a synchronized animation sample (no instructions, click a colored box). It might take some work to figure out how to apply it to your control, but it could be a good place to start.
This does not solve running animations in parallel however it reproduces your expected behavior without the jitter. I placed section inside of handle to reduce the number of animations. You could use andSelf() to make the code smaller but it would be harder to read. You will need to make some style tweaks.
<html>
<head>
<title>Accordion Test</title>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$("#accordion .handle").click(function(){
var open = $(this).parent().children(".section, .open");
var active = $(this);
if (!active.hasClass("open"))
{
if (active.hasClass("up"))
{
console.log("up");
active.animate({top:"+=100"}).removeClass("up");
active.nextAll(".handle").andSelf().filter(".up").animate({top:"+=100"}).removeClass("up");
$(".section", active).slideUp();
$(".section", active.nextAll()).slideUp();
$(".section", active.prev()).slideDown();
}
else
{
active.prevAll(".handle").not(".up").animate({top:"-=100"}).addClass("up");
$(".section", active.prev()).slideDown();
}
open.removeClass("open");
active.addClass("open");
}
});
});
</script>
<style type="text/css">
#accordion{
width: 200px;
position:relative;
}
#accordion .section{
width: 196px;
margin-left: 2px;
height: 100px;
background-color: #b9b9b9;
display:none;
}
#accordion .handle{
width: 200px;
height: 30px;
background-color: #d9d9d9;
border: 1px solid black;
cursor: pointer;
cursor: hand;
position: absolute;
}
#accordion .handle .header {
height: 30px;
}
</style>
</head>
<body>
<div id="accordion">
<div id="s1" class="section open" style="display:block">This is section 1</div>
<div class="handle open" style="top:100;">
<div class="header">handle 1</div>
<div class="section">This is section 2</div>
</div>
<div class="handle" style="top:130;">
<div class="header">handle 2</div>
<div class="section">This is section 3</div>
</div>
<div class="handle" style="top:160;">
<div class="header">handle 3</div>
<div class="section">This is section 4</div>
</div>
<div class="handle" style="top:190;">
<div class="header">handle 4</div>
<div class="section">This is section 5</div>
</div>
<div class="handle" style="top:220;">
<div class="content">handle 5</div>
</div>
</div>
</body>
</html>
Thanks Adam Plumb for a really great solution to parallel animations. I had a small problem with it though and that was that it somehow saved roles from earlier animations i fixed that by setting the rules to {} before adding them in the init function. It can probably be done in a better way though. I also added a callback function that is called when the animation have finished.
ParallelAnimations = function(animations, opts){
this.init(animations, opts);
};
$.extend(ParallelAnimations.prototype, {
options: {
duration: 250,
callback: null
},
rules: {},
init: function(animations, opts){
// Overwrite the default options
$.extend(this.options, opts);
// Create a set of rules to follow in our animation
this.rules = {}; // Empty the rules.
for(var i in animations){
this.rules[i] = {
element: animations[i].element,
changes: new Array()
};
for(var style in animations[i].styles){
// Calculate the start and end point values for the given style change
var from = this.parse_style_value(animations[i].element, style, "");
var to = this.parse_style_value(animations[i].element, style, animations[i].styles[style]);
this.rules[i].changes.push({
from: from,
to: to,
style: style
});
}
}
this.start()
},
/*
* Does some parsing of the given and real style values
* Allows for pixel and percentage-based animations
*/
parse_style_value: function(element, style, given_value){
var real_value = element.css(style);
if(given_value.indexOf("px") != -1){
return {
amount: given_value.substring(0, (given_value.length - 2)),
unit: "px"
};
}
if(real_value == "auto"){
return {
amount: 0,
unit: "px"
};
}
if(given_value.indexOf("%") != -1){
var fraction = given_value.substring(0, given_value.length - 1) / 100;
return {
amount: (real_value.substring(0, real_value.length - 2) * fraction),
unit: "px"
};
}
if(!given_value){
return {
amount: real_value.substring(0, real_value.length - 2),
unit: "px"
};
}
},
/*
* Start the animation
*/
start: function(){
var self = this;
var start_time = new Date().getTime();
var freq = (1 / this.options.duration);
var interval = setInterval(function(){
var elapsed_time = new Date().getTime() - start_time;
if(elapsed_time < self.options.duration){
var f = elapsed_time * freq;
for(var i in self.rules){
for(var j in self.rules[i].changes){
self.step(self.rules[i].element, self.rules[i].changes[j], f);
}
}
}
else{
clearInterval(interval);
for(var i in self.rules){
for(var j in self.rules[i].changes)
self.step(self.rules[i].element, self.rules[i].changes[j], 1);
}
if(self.options.callback != null) {
self.options.callback(); // Do Callback
}
}
}, 10);
},
/*
* Perform an animation step
* Only works with position-based animations
*/
step: function(element, change, fraction){
var new_value;
switch(change.style){
case 'height':
case 'width':
case 'top':
case 'bottom':
case 'left':
case 'right':
case 'marginTop':
case 'marginBottom':
case 'marginLeft':
case 'marginRight':
new_value = Math.round(change.from.amount - (fraction * (change.from.amount - change.to.amount))) + change.to.unit;
break;
}
if(new_value)
element.css(change.style, new_value);
}
});
I think your problem isn't timing but fractional division of a pixel. If you try this code it looks smooth for handle 1 and 2 but not others in Firefox 3 but still looks jumpy in chrome.
active
.animate({ height: "100px" })
.siblings(".section")
.animate({ height: "0px" });
Have you thought about making the position of the elements static or absolute? If your only moving the position of two elements you don't have to worry about the other ones jumping. Give me a second and I'll try to make an example.
Update: I'm no longer using John Resig's syncAnimate plugin. See my later answer for the final solution
I just wanted to supply the final working solution that I'm employing on my project. It uses the syncAnimate plugin that John Resig wrote (posted by Corbin March).
This code will:
Read and use the section height from CSS
Allow you to set the animation duration, and default active section through an options object.
Automatically detect handle position relative to section and adjusts accordingly. So you move the handles above or below a section in the markup and not have to change the js code.
HTML
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="ui.js"></script>
<script type="text/javascript">
$(document).ready(function(){
new Accordion("#accordion", {active_tab: 0});
});
</script>
<style type="text/css">
#accordion .handle{
width: 260px;
height: 30px;
background-color: orange;
}
#accordion .section{
width: 260px;
height: 445px;
background-color: #a9a9a9;
overflow: hidden;
position: relative;
}
</style>
<div id="accordion">
<div class="section">Section Code</div>
<div class="handle">handle 1</div>
<div class="section">Section Code</div>
<div class="handle">handle 2</div>
<div class="section">Section Code</div>
<div class="handle">handle 3</div>
<div class="section">Section Code</div>
<div class="handle">handle 4</div>
<div class="section">Section Code</div>
<div class="handle">handle 5</div>
</div>
ui.js
Accordion = function(container_id, options){
this.init(container_id, options);
}
$.extend(Accordion.prototype, {
container_id: '',
options: {},
active_tab: 0,
animating: false,
button_position: 'below',
duration: 250,
height: 100,
handle_class: ".handle",
section_class: ".section",
init: function(container_id, options){
var self = this;
this.container_id = container_id;
this.button_position = this.get_button_position();
// The height of each section, use the height specified in the stylesheet if possible
this.height = $(this.container_id + " " + this.section_class).css("height");
if(options && options.duration) this.duration = options.duration;
if(options && options.active_tab) this.active_tab = options.active_tab;
// Set the first section to have a height and be "open"
// All the rest of the sections should have 0px height
$(this.container_id).children(this.section_class).eq(this.active_tab)
.addClass("open")
.css("height", this.height)
.siblings(this.section_class)
.css("height", "0px");
// figure out the state of the handles
this.do_handle_logic($(this.container_id).children(this.handle_class).eq(this.active_tab));
// Set up an event handler to animate each section
$(this.container_id + " " + this.handle_class).mouseover(function(){
if(self.animating)
return;
self.animate($(this));
});
},
/*
* Determines whether handles are above or below their associated section
*/
get_button_position: function(){
return ($(this.container_id).children(":first").hasClass(this.handle_class) ? 'above' : 'below');
},
/*
* Animate the accordion from one node to another
*/
animate: function(handle){
var active_section = (this.button_position == 'below' ? handle.prev() : handle.next());
var open_section = handle.siblings().andSelf().filter(".open");
if(active_section.hasClass("open"))
return;
this.animating = true;
// figure out the state of the handles
this.do_handle_logic(handle);
// Close the open section
open_section
.syncAnimate(active_section, {"height": "0px"}, {queue:false, duration:this.duration}, '')
.removeClass("open");
// Open the new section
active_section
.syncAnimate(open_section, {"height": this.height}, {queue:false, duration:this.duration}, '')
.addClass("open");
var self = this;
window.setTimeout(function(){
self.animating = false;
}, this.duration);
},
/*
* Update the current class or "state" of each handle
*/
do_handle_logic: function(handle){
var all_handles = handle.siblings(".handle").andSelf();
var above_handles = handle.prevAll(this.handle_class);
var below_handles = handle.nextAll(this.handle_class);
// Remove all obsolete handles
all_handles
.removeClass("handle_on_above")
.removeClass("handle_on_below")
.removeClass("handle_off_below")
.removeClass("handle_off_above");
// Apply the "on" state to the current handle
if(this.button_position == 'below'){
handle
.addClass("handle_on_below");
}
else{
handle
.addClass("handle_on_above");
}
// Apply the off above/below state to the rest of the handles
above_handles
.addClass("handle_off_above");
below_handles
.addClass("handle_off_below");
}
});
You can't do a parallel effect in jquery with proper queue and scope. Scriptaculous got it right with queue and scope where jQuery on the other hand has .queue and .animate that are basically useless combined. The only thing jQuery is good for out of the box is pushing some style attributes around on the dom whereas Scriptaculous covers the whole spectrum of what's possible with effects.
You need to use Scriptaculous and John Resig should rethink jQuery.fx, he should have a look at scripty2.com while he's at it.

Categories