Jump to content


Draggable - snapping to specific points with sensitivity

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Hi there,


In the demo for Draggable you show livesnap snapping to a grid.  I wondered if it was possible to have an array of x and y values and cause the dragged item to snap to the closest one when near?  


The livesnap appears to only work with either a range of x values or y values, not specific points in a container.  Also I'd like to be able to drag until I get close to a location and then it snaps.  In the flash days I used pythagarus to work out the distance.  I tried doing that in the ondrag function like below but it doesn't want to snap despite it getting the console log.


Any guidance would be much appreciated.  Thanks!

onDrag:function(e) {
          for(var i=0; i<targetLocations.length;i++){
            var xDistanceFromPoint = Math.abs(this.x - targetLocations[i].x);
            var yDistanceFromPoint = Math.abs(this.y - targetLocations[i].y);
            var distanceFromPoint = Math.sqrt((xDistanceFromPoint*xDistanceFromPoint)+(yDistanceFromPoint*yDistanceFromPoint));
            if(distanceFromPoint < 50){
              this.y = targetLocations[i].y;
              this.x = targetLocations[i].x;
Link to comment
Share on other sites

Hi and welcome to the GreenSock forums,


If you want to drag and snap to an array of x and y values, you can pass in an object for the livesnap value as noted in the docs:


liveSnap : Function | Boolean | Array - allows you to define rules that get applied WHILE the element is being dragged (whereas regular snap affects only the end value(s), where the element lands after the drag is released). For example, maybe you want the rotation to snap to 10-degree increments while dragging or you want the x and y values to snap to a grid (whichever cell is closest). You can define the liveSnap in any of the following ways:

  • as an object - if you'd like to use different logic for each property, like if type is "x,y" and you'd like to have the "x" part snap to one set of values, and the "y" part snap to a different set of values, you can use an object that has matching properties, like: liveSnap:{x:[5,20,80,400], y:[10,60,80,500]} or if type is "top,left" and you want to use a different function for each, you'd do liveSnap:{top:function(endValue) { return Math.round(endValue / 50) * 50; }, left:function(endValue) { return Math.round(endValue / 100) * 100; }}

If however you want to have your "sensitivity" applied, it looks like your onDrag is the way to go. As for why its not working, take a peak at the docs for Draggable's x property:


x property  

x:Number  [read-only]



The current x (horizontal) position of the Draggable instance. For a Draggable of type:"x,y", it would be the x transform translation, as in the css transform:translateX(...). Fortype:"top,left", the Draggable's x would refer to the css "left" value that's applied. This is not the global coordinate - it is the inline css-related value applied to the element. This value is updated each time the Draggable is dragged interactively and during the momentum-based tween that Draggable applies when the user releases their mouse/touch, but if you manually change (or tween) the element's position you can force Draggable to look at the "real" value and record it to its own "x" property by calling the Draggable's update() method. Basically that re-synchronizes it. Again, this is not necessary unless other code (outside Draggable) alters the target element's position.






Draggable.x is read only. This is why this.x wasn't working.

If you want an external force (your custom code) to alter the x of the Draggable, you will need to


1: set the x value of the target of the Draggable. Try using TweenLite.set(). Perhaps

TweenLite.set(this.target, {x:someValue})


2: call update() on the Draggable... this.update() or someDraggableInstance.update()


Its kind of tough to trouble code-snippets blind, so if none of the above works, please create a simple CodePen demo as shown here:



You can fork this pen which loads Draggable: http://codepen.io/GreenSock/pen/HjfsK



  • Like 2
Link to comment
Share on other sites

Thanks guys for responding to my query. 


Peleg, I looked at that codepend example but it only checks for points on one axis.




I've taken a bit of time to put a codepen together.  I'm not trying to force a livesnap now but used the codepen example that you guys had setup with the overlap threshold as a basis.


It's here


See the Pen zlxvg by antpearson (@antpearson) on CodePen


The tween into position on the target isn't quite going to plan - I think it's because of the absolute positioning on the drag items.  Any idea how I could get round that?  And any idea how I could send the drag items back to their starting positions when dragged off the targets?  It's kind of working but I think it won't if I sort out the snapping properly.


At some point I'm going to need to handle the logic that tests if the correct drag item is on the correct target.


Thanks again for your help

Link to comment
Share on other sites

The problem is that you're mixing x, y, top, and left. Remember, "x" and "y" are like offsets from the original layout position in the normal document flow (that's just how CSS transforms work - it's not a GSAP-specific thing). So simply changing the "type" of your Draggable to "top,left" should resolve things. However, I personally prefer using "x,y" because it usually performs better in most situations. You can still do that and then onDragEnd, just animate the x and y to 0 so that there's no offset from the normal position, and keep "top" and "left" for your positioning. Here's a demo:



We're also working on a tool that'll make it super easy to use x/y transforms to precisely position elements in relation to each other (like "make element1 animate exactly to element 2 so that their centers are aligned"). That may not launch for a while, though. "Shockingly Green" and "Business Green" members can request early access to that tool if they'd like. It's coming together nicely. 

  • Like 2
Link to comment
Share on other sites

Hi Jack,


That works a treat, thanks for making that clear.  I knew I was getting in a muddle somewhere but couldn't work out where.


Looking forward to the new tool - it sounds like it will be super handy.  This Draggable utility class is much easier to deal with than the jQueryUI/Touch Punch implementation of Drag & Drop!

  • Like 1
Link to comment
Share on other sites

Hi again,


I thought I was done with this one but wondered if I could refine it even better.  The snap is working great now (thanks for your input).  As TweenMax is now adding inline styles for position I wanted to know if there was a way to revert back to the original styles when the items are dragged off the targets - at the moment they are all heading back to the same spot?


See the Pen IaAfl by antpearson (@antpearson) on CodePen


I tried using the clearProps:"all" in the animation routine but this causes the object to return abruptly.  I guess I'm wondering if there is a way to animate the TweenMax locations off gradually - returning the item to its original absolutely positioned location?  Either that or a way to store the original positioning somehow on each dragged object so it can be recalled and added to the TweenMax animation that sends it back?


thanks again 

Link to comment
Share on other sites



What you could do is loop through the elements and store their original offset positions in the element and when the condition isn't met tween to that particular values:

var boxes = $(".box");
$.each(boxes, function(i,e)
  var position = $(e).offset;

  e.originalLeft = position.left - 8;
  e.originalTop = position.top - 8;

// then in the Draggabke instance
  var snapMade = false,
      targetEl = this.target;

  for(var i=0; i<targets.length;i++)
    if(this.hitTest(targets[i], overlapThreshold))
      var p = $(targets[i]).position();
      TweenLite.to(e.target,0.1, {left:p.left, top:p.top, x:0, y:0});
      snapMade = true;
    TweenLite.to(e.target,0.1, {x:10, y:10, top:targetEl.originalTop, left:targetEl.originalLeft});

That should return them to their original position when the page was rendered for the first time. Be aware though that if for any reason the positions change, window resize, other tweens, etc., you'll have to run the loop again after those events.


Also, in case you're wondering why there's a - 8 in the positions values, that has to do with the fact that in the codepen the <body> element has it's natural 8px margin. But that's in modern browsers, other browsers have different margins (if I recall correctly IE9 and some versions of Opera) so for the best crossbrowser behaviour I'd recommend you using margin:0; for the body element in the CSS. In that case remove the - 8 in those expressions.



Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.