Jump to content
Search Community

Draggable Improvements - snapping and position properties

OSUblake test
Moderator Tag

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

This is kind of a follow up to my response about Draggable being an excellent tool for handling different types of mouse and touch interactivity.

https://greensock.com/forums/topic/12477-inspiring-html5-banner-examples-with-gsap/?p=68431

 

I would love to see Draggable expand out to support more types of interactivity, like with gestures. Development for Hammer.js has been on the decline for some time now, and there's not a lot of other options. Interact.js is probably the next best one. It's been around for awhile, and has all the bells and whistles, but it's not for beginners.

 

For example, I thought the SVG demo on their site was kind of neat, so I created a version on CodePen. Check it out...

 

Hope you like working SVG matrices. That is definitely not something your average user would be able to figure out. With Draggable, this is all that's needed for the dragging.

Draggable.create(handle, {
  onDrag: function() {
    point.x = this.x;
    point.y = this.y;
  }
});

Pretty simple, right?

 

See the Pen 02f36f1d867ba61abff89536414f5982?editors=0010 by osublake (@osublake) on CodePen

 

 

But wait. Something's not right. It's not snapping to the points. Using the current API for Draggable, that's going to be really hard to do. I've brought up needing x/y values at the same time with the ModfiersPlugin, but Draggable really needs this. I want to build a node editor like this, with draggable nodes and port connectors.

 

 

bX9P9FM.png

 

 

 

To see how xy snapping might work, I modified a line of code to pass in both values around line 1708.

x = snapX({ x: x, y: y });

That's enough to hack a demo together. Check out the simple point in circle test I'm doing. Draggable needs something like that. Pass in an array of points with an optional threshold.

 

See the Pen ?editors=0010 by osublake (@osublake) on CodePen

 

Now we're getting somewhere! But why stop there? It would be nice if Draggable could keep track of some additional properties, like the delta value between events. This would be crazy useful. Using the delta value, you can do stuff like move other objects alongside what you're dragging, or even mirror them, like a Bezier handle.

 

 

cVk6QZj.png

 

 

Check it out. The only thing I'm doing is setting the x and y properties to the negative delta value. That moves it in the opposite direction. Now you can do trigonometry without trigonometry!

 

See the Pen ?editors=0010 by osublake (@osublake) on CodePen

 

There's one other property that would be nice for Draggable to keep track of, and that's the last position. We already know how useful that is as that question gets asked a lot.

 

Here's something interesting you can do using the last and delta values. Connect circles. It's done by finding the mid point, which is the average of the last and current position. The diameter of the circle is the magnitude/length of the delta 

 

 

IoYDef3.png

 

 

The faster you drag, the larger the circles will be. It's almost like an event visualizer.

 

See the Pen ?editors=0010 by osublake (@osublake) on CodePen

 

 

 

 

 

 

What do you think? Hopefully you won't have to chew on this for too long. Here's the draggable file I modified.

https://s3-us-west-2.amazonaws.com/s.cdpn.io/106114/draggable.js

 

 

And these are the changes I made. Really simple stuff. Just enough to get my demos working.

// LINE 1122
this.last = { x: 0, y: 0 };
this.delta = { x: 0, y: 0 };

// LINE 1232
self.last.y = applyObj.data.y;
self.delta.y = y - applyObj.data.y;

// LINE 1242
self.last.x = applyObj.data.x;
self.delta.x = x - applyObj.data.x;

// LINE 1738
x = snapX({ x: x, y: y });

.

See the Pen 5008b47d7eff5ee86b30ba22cdbe4818?editors=0010 by osublake (@osublake) on CodePen

  • Like 6
Link to comment
Share on other sites

Sorry for the late response, Blake. These are great suggestions, of course. The delta is pretty easy. The "last" seems a bit redundant/unnecessary - couldn't anyone get that by just doing this.x - this.delta.x? What might be more useful is this.start.x and this.start.y (recording the original values when the drag started), right? 

 

Also, the snapping is quite a bit more tricky because:

  1. Changing the parameter from a simple number to an object with x/y properties means that all legacy code would break. That's a pretty massive problem. 
  2. Snapping can optionally also tie into the actual throwProps animation, and that plugin can handle ANY property (not just x and y), so it's not a simple "oh, just chuck x and y into the same place when updating" scenario, you know? They're totally distinct in the rendering pipeline. Changing that would require some substantial rewiring I suspect. 

I thought about just slapping on an additional parameter to the snapping function, like function(x, y) so that it doesn't break legacy code, but I'd have to reverse the order for the "y", like function(y, x) to keep legacy code working, but that seems a bit weird. And of course it doesn't really solve #2 from above. :(

 

Got any ideas/suggestions regarding the snapping? 

  • Like 1
Link to comment
Share on other sites

The start property is a good idea. I know the last point would be easy to calculate, but some peolpe are too lazy to calculate such things, and I was thinking that it might be useful to help figure out user intent.

 

I figured the snapping would require some work as I had hard time figuring out exactly where that even happened in the code.

 

Without breaking legacy code, what about introducing a new snap/liveSnap option called "point" or "xy"? So it might look like this...

Draggable.create(foo, {
  liveSnap: {
    point: function(endValue) {
      // endValue has x and y properties on it
      return endValue;
    }
  }
});

.

Link to comment
Share on other sites

Yeah, I like that. 

 

And I love a challenge.

 

So I took a crack at an overhaul that'd give you what you're asking for (and some extra):

  1. Draggable instances now have the following properties: deltaX, deltaY, startX, startY. I chose this syntax instead of delta.x/delta.y and start.x/start.y solely because we already had "endX" and "endY" properties so it'd be weird not to be consistent with that. Plus it saves a little memory :)
  2. You can define a "point" property in the snap (or liveSnap) that can be either:
    • a function - it'll get passed an object with x and y properties, like {x:201, y:500} and you return a similar object with the values tweaked however you want.
    • an array of point objects, like [{x:0, y:0}, {x:100, y:200}] and it'll snap to the closest one. 
  3. You can optionally define a "radius" property in the snap/liveSnap and it'll make the point snapping only effective within that radius of the point(s)!
  4. In order to make this all work seamlessly with momentum-based animation/flicking, I had to revamp some of the guts of ThrowPropsPlugin. Remember that it can handle an unlimited number of properties, not just "x" and "y", so I had to make it flexible. The new version recognizes a "linkedProps" special property that's a comma-delimited list of property names that should be linked/associated for the function-based or array-based end values. For example, ThrowPropsPlugin.to(... {throwProps:{x:{velocity:200, end:yourFunction}, y:{velocity:500, end:yourFunction}, linkedProps:"x,y"}}) will call yourFunction() and pass in an object like {x:321.021, y:400.23} (where it'd naturally end) and then you can spit back an edited object to define where it should end. Most people probably won't need this, but obviously it's critical for all this stuff to work in Draggable. 

So basically, your Draggable could look like this:

Draggable.create("#yourID", {
  type:"x,y",
  liveSnap:{
    point:[{x:0, y:0}, {x:100, y:200}],
    radius:10
  }
});

And it'll snap to the closest point in that array when it's within 10px. 

 

Or, for a function-based value, it could look like:

Draggable.create("#yourID", {
  type:"x,y",
  liveSnap:{
    point:myModifier
  }
});

function myModifier(point) {
  //run whatever logic you want here...
  point.x = 100;
  point.y = 200;
  return point;
}

Here's a forked version of your demo with super-easy snapping behavior that adds only a few lines:

http://codepen.io/GreenSock/pen/08895f3b352289b3ff3eca9c9a7f4016?editors=0010

 

Please kick the tires and let me know what you think. 

https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/Draggable-latest-beta.js

https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/ThrowPropsPlugin.min.js

 

If anyone has suggestions about the API, I'm all ears. Once we release this, it'll be locked-in so now's the time to chime in :)

  • Like 2
Link to comment
Share on other sites

  • 2 weeks later...

Just took a peak at how you are doing the point snapping. There's nothing wrong with it, but you might be able to optimize it some. 

 

When comparing distances, you don't have to call Math.sqrt. 

// This works out the same...
var dist = Math.sqrt(dx * dx + dy * dy);
var hit1 = dist <= radius;

// ... as this
var distSq = dx * dx + dy * dy;
var hit2 = distSq <= radius * radius;

A squared number will never be negative, and squaring a smaller number will never be greater than squaring a larger number, so you really only need to call Math.sqrt for display purposes.

 

The performance difference can be pretty dramatic, especially if it's getting called on every drag. That's why almost every vector library out there includes some sort of distance and distanceSq method.

 

Firefox has never really had a problem with sqrt calls. At one point Chrome did, but it looks like the difference is negligible now. Edge on the hand is pretty dismal...

 

https://jsperf.com/comparing-distance

 

 

qyZU9BG.png

 

.

  • Like 2
Link to comment
Share on other sites

@OSUblake, what's funny is that the day after I posted here with the updates, I had the EXACT same thought regarding the .sqrt() optimization (woke up in the morning with that thought...guess my brain noticed it and reminded me while asleep). I just got distracted and forgot to implement that, but I'll do it in the next release. I can't imagine it'd provide a noticeable speedup, but I'm certainly a fan of optimizing the snot out of as much as I can. :) Thanks for the suggestion.

  • Like 4
Link to comment
Share on other sites

  • 8 months later...

Back for a couple more requests. The fist one should be easy, not so sure about the second one. :wacko:

 

Multiple Cursors

It would be nice if there was an option to specify a cursor when a draggable instance is pressed. The grab/grabbing cursors are available in all modern browsers, so it would be great if that was now the default cursor for all types e.g. x, y, rotation. I think that is what most people would prefer. If the grab/grabbing cursor isn't available, have it default back to the way it currently is.

 

Detect Off-Screen Mouse Release

Except for Firefox, releasing a draggable instance off-screen with a mouse is not detected. This causes dragging to continue even though you are not pressing any mouse buttons. To get it to stop dragging, you have to click again. Releasing a draggable off-screen is usually unintentional, but it does happen, particularly when dragging something close to the edge or very fast. Is there anyway to fix this behavior in the other browsers?

 

  • Like 3
Link to comment
Share on other sites

Multiple cursors

Are you talking about having the cursor change at various events? Currently, you can already define any "cursor" that you want, like {cursor:"grab"}

 

Off-screen mouse release

This seems to be an issue only when the page you're dragging on is loaded inside an iframe (like on codepen), right? The really annoying thing is that you must pick your poison - either put up with the current behavior or set allowEventDefault:true and then if you press/drag, it can highlight things in some cases. The crux of the issue has to do with the fact that the mousedown/touchdown event must NOT call preventDefault() if you want the release to bubble up, but by default we DO call preventDefault() for a bunch of other reasons (like to prevent drag-scrolling on touch devices, and to prevent the selection stuff from happening, etc.). See what I mean? Or do you see any good solutions that don't involve this tradeoff? 

Link to comment
Share on other sites

On 9/25/2017 at 12:38 AM, GreenSock said:

Are you talking about having the cursor change at various events? Currently, you can already define any "cursor" that you want, like {cursor:"grab"}

 

Yeah. But I was only thinking about when its pressed. So something like this.

Draggable.create(foo, {
  cursor: "grab",
  activeCursor: "grabbing" // cursor when pressed
});

 

Being able to disable/prevent the cursor from being added inline would also be nice. Sometimes it's easier to manage custom cursors using CSS.

Draggable.create(foo, {
  cursor: false // disable adding inline cursor
});

 

 

On 9/25/2017 at 12:38 AM, GreenSock said:

This seems to be an issue only when the page you're dragging on is loaded inside an iframe (like on codepen), right?

 

Correct. I forgot to mention that.

 

I wasn't sure if it was possible, but wanted to ask anyways. It's no biggie if it can't be done. There a ways to work around it, like using mouseleave to record event.buttons, and calling endDrag if the buttons aren't the same on mouseenter.

 

  • Like 1
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.
×
×
  • Create New...