Jump to content
Search Community

Draggable Question / Improvements, (snapIndex)

Julius Friedman 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

Quick question on the Draggable plugin...

 

The impetus for this is that I was creating to a Range type control which was supporting screen orientation change.

 

The snaps in my control act as a way to associate a value to the range, thus I suppose I could just use a function but I wanted to address this; Hopefully the points are simple enough but if not let me know and I will get a Pen created.

When not using a custom snap function but using the snaps and liveSnap array feature for example:

 

Draggable.create(this.handleGroup, {
                    type: 'y',
                    bounds: { //this.track, would cause transforms from that elements positions..
                        minY: 0,
                        maxY: this.track.height.baseVal.value
                    },
                    liveSnap: true,
                    //Direction specified to prevent movement along the other axis, on orientation change this must be changed to match.
                    snap: { y: [] },
                    //throwProps: true,
                    minDuration: 1,
                    overshootTolerance: 0,
                    dragClickables: true,
                    callbackScope: this,
                    onPress: this.onDrag,
                    onDrag: this.onDrag,
                    onThrowUpdate: this.onDrag,
                    onThrowComplete: this.onDragEnd,
                    onDragEnd: this.onDragEnd,
                    onRelease: this.onDragEnd
                });


You guys already have a feature that if an Array is provided the best point is selected from that array based on the direction of the drag.

 

What I find slightly annoying though and probably much more useful than you would think is that we don't record the last drag direction or the last snapIndex.

 

When dragging is occurring or ending one has to lookup the snap to provide that index if it's needed, e.g if your using the snaps as positions to indicate value something like this would work:

 

const value = this.dragger.vars.snap.y.length - this.dragger.vars.snap.y.indexOf(this.dragger.y) - 1;

 

Thus is you have only one direction your supporting and the snaps are the same then you can change the type and call apply and your done for orientation change, however you need to provide a supporting property for the snap in that axis e.g. you had snap.y and now you need snap.x or vice versa, still easy enough to alias in the event handler and your still done.


If you have more than one direction though you still need to determine the direction after it was already determined in the code...

 

So thus my gripes are as follows:

 

1) We don't record the snapIndex and the snapAxis in the vars, this causes users to have to repeat the same logic as the plugin just did to find the snap when it's needed.

 

2) If multiple directions are supported but only 1 direction at a time, one needs to define the snaps without the axis property (x, y) and just use a plain array from the start for the snap / liveSnap property and not an object with (x and or y)

 

3) If multiple directions are supported at the same time having the snapAxis which caused the snap from the plugin is much more efficient than again determining the direction and the snapIndex when it just was previously done.

 

Personally I then use the height of the track combined with the max values to set the snaps for example I have 4 default options here but you could specify an arbitrary number.
 

            const point = this.track.height.baseVal.value / (CS.context.Number.isFinite(this.options.maxValue) ? this.options.maxValue : 4);

            //Erase any existing points...
            this.dragger.vars.snap.y.length = 0;

            //Put the snaps in the dragger at the calculated space.
            for (let i = this.options.minValue; i <= this.options.maxValue; ++i)
                this.dragger.vars.snap.y.push(point * i);


So I supposed I could do this with a function and base it on the width or the height but I would still need to look the direction up again when it was just done previously by the Plugin to raise the event.

 

Let me know what you think or if there is already a way to get that information which I am somehow missing in the documentation.


Also checkout 

See the Pen mNzBpo by juliusfriedman (@juliusfriedman) on CodePen

 as I also had a question about relative properties...

 

It seems that relative properties only work with "+" and "-" and I was wondering why "*" and "/" as well as "%" are not implemented, maybe even "^"?

E.g. if I wanted to tween to the modulo of my elements x and the width of another element I would use '%' if I wanted to move in powers of the width I would use "^" etc.

One can also pretty easily do this themselves I was just curious why not as I would have expected at least *= and /= to work, e.g. for when I want to move to a relative position but sometimes that position should be reset to 0 the *= works well, /= also lends itself well in the reverse where % and %= are more dependent but still useful.

 

I could also imagine some type of syntax which allows properties and objects to be scripted but that is well beyond the scope of what I imagined would come out of this question, an example would be something like:

 

"{width:'%=' + $1.width, x: '*=' + $2.x}"

Which is saying to set the width of the object to the modulo of the first objects width and the current width, also set the x of the x to the 2nd objects's x position....

 

Overall honestly not sure how much more useful that would be than just coding and calculating yourself  but the idea is whats important, and it would allow for adjusting positions quite easily especially those positions which are related to each other...

 

About to head to sleep, if I think of anything else I will update this post.

 

See the Pen BXqdMv by juliusfriedman (@juliusfriedman) on CodePen

Link to comment
Share on other sites

Ha, you're the king of super long, complicated posts today :)

 

Are you just making suggestions for potential feature additions to Draggable? Or was there a problem/bug you wanted to report? 

 

I'm not 100% sure I follow all that you wrote, but I'll mention a few things (none of which may be helpful): 

  • If you need both x and y involved in a snap, that's supported and here's a demo: 

    See the Pen wgapaY by GreenSock (@GreenSock) on CodePen

  • If you want to sense the direction of the drag, there's getDirection(): https://greensock.com/docs/Utilities/Draggable/getDirection()
  • We didn't implement *= or /= or %= because they're just not that useful and implementing them would force ALL users to pay the performance and kb hit for something that probably 0.00001% of the users may actually tap into. It just didn't seem like a good tradeoff. 
  • Your demo showing that scale:+=0.5 affected scaleX and scaleY seemed to work exactly as I'd expect. Am I missing something? Why would you not thing it'd affect scaleX and scaleY (scale, after all, is just a shortcut to affect scaleX and scaleY). 

If you still need some help with something, yes, it'd be REALLY good if you provided as reduced a test case as possible and please keep the threads focused on one question at a time if you don't mind. It's totally fine if you open a few different threads - it's just too convoluted and confusing for people to follow when a whole bunch of things are mashed into one really long post :)

 

Have a good night!

  • Like 4
Link to comment
Share on other sites

Definitely not my intent, was actually hoping that I was missing things but understood and that sounds good.

 

What about snapIndex and snapAxis being tracked by the plugin? I would hate to waste cycles myself looking up the snapIndex when it's known in the code as well as the direction before the callback is invoked.

 

The example above where I use indexOf to determine the snapIndex is what I am trying to just read from the vars of the plugin rather than have to call every callback especially since those events block.

 

I'm half tempted to just modify the code to store the index and axis as it already looks them up to facilitate the feature but I run the risk of hacking.... ?

 

Let me know if I can make that more clear as I'm sure it would improve perf for people especially when there are a lot of snaps, all I am essentially asking is that the snapIndex gets recorded in the vars of the dragger somewhere as state so I can read it rather than loop through all snaps again.

 

snapAxis would be helpful there also if there are 2 directions in use (with different snaps) especially since you guys already determine the axis first and then the snapIndex.

 

Finally if direction could be stored (couldn't definitely tell from the minimized code but I am pretty sure the direction is also known), that would save people from calling getDirection again.

 

I will also definitely try to keep threads separate, the only other thing I was questioning at this time was how to composite morphs into the same canvas if I wanted to or how to access the underlying canvas used for morphing when canvas was specified but that's save for now as I have to finish up the other logic needed for the controls and I will revisit that and improve the morphFromTo logic later on.

 

Let's focus on the draggable issue here which is that snapIndex and snapAxis should be stored after they are looked up so in the callbacks I can access them without doing the math / lookup again. If you can give snapDirection if you call it then that's also helpful.

 

Thank you and Blake for the demos very much!

In that example you guys don't look up the snaps after creating them, but I have seen others which use the snapIndex and to get it they calculate it each time just like I do using the x / y even though it was already previously created.

 

Giving a function of snap if definitely an improvement but only half of the work because it would still need to be coupled to something to store the state for the snapIndex / snapAxis etc and yet before the snap function is called logic is already run to determine the axis at least and would have determined the snap but found a function and not an array.

 

Let me know if I can be any more clear or if I am missing on how to retrieve that information from the plugin without looking it up every callback!

 

Also... while looking into the other thread where someone was have an issue with double events due to how they were using the plugin as well as some of my own event handling needs I was wondering what you thought about having the event state machine more exposed or at-least the current event..

This I thought would be enable one to use a single function for each of the events and simply switch on the type of event more similar to how vanilla code would be with e.type... The next step would be having a e.next so I can determine if there would be a next event and use possibly it's name or other state information to determine what that type of event is and act on it.

If you needed to call dragEnd or facilitate press / click etc that would then be possible by taking the state into account as you would already have access via the completionScope etc, or you could simply just call the corresponding method needed e.g. drag, press etc.

 

Let me know if you agree / disagree or I am missing something.

 

RE the operators, it seems like it's a small change from +=, -= and the code is already there to support it, things like centering to an element are typically always half the width and height so *=0.5 or /= 2 would be better than having to make a function just to do such small things, then again the parsing of the string to get the values is probably just as slow...

 

Thank you!

 

Link to comment
Share on other sites

Just so I can be sure I am making sense I updated my pen and the Draggable code to show you what I mean...

The pen is the same but the logic is updated to handle if there was a snapIndex available from the Plugin @ 

 

const value = this.dragger.vars.snapIndex ? this.dragger.vars.snapIndex : this.dragger.vars.snap.y.indexOf(this.dragger.y);
            this.setValue(value);

 

 

DraggablePlugin required the following modifications to facilitate this:

 

/Simple function declaration to save having to write function(a){return a};
                    λ = function () { return this; },
                    //Not 100% sure what this function does but it might be useful to store the result.
                    MUL = function (a, d) { return a * d },
                    //nb and ob could likely be combined especially the logic for the loop...
                    nb = function (a, b, c, d) {
                        return null == b && (b = -A), null == c && (c = A), "function" == typeof a ? function (e) {
                            var f = Ha.isPressed ? 1 - Ha.edgeResistance : 1;
                            //Here snapResult could be stored
                            return a.call(Ha, e > c ? c + (e - c) * f : b > e ? b + (e - b) * f : e) * d
                        } : a instanceof Array ? function (d) {
                            for (var e, f, g = a.length, h = 0, i = A; --g > -1;) e = a[g], f = e - d, 0 > f && (f = -f), i > f && e >= b && c >= e && (h = g, i = f);
                            //Here this.vars.snapIndex = h tracks the index and axis but might be transformed?
                                //snappedIndex and snappedAxis might be better names?
                            this.vars.snapIndex = h;
                            this.vars.snapAxis = 'y';
                            return a[h]
                        }.bind(Ha) : isNaN(a) ? λ.bind(Ha, a) : MUL.bind(Ha, a, d)
                    },
                    ob = function (a, b, c, d, e, f, g) {
                        return f = f && A > f ? f * f : A, "function" == typeof a ? function (h) {
                            var i, j, k, l = Ha.isPressed ? 1 - Ha.edgeResistance : 1,
                                m = h.x,
                                n = h.y;
                            //Here snapResult could be stored but we also check y?
                            return h.x = m = m > c ? c + (m - c) * l : b > m ? b + (m - b) * l : m, h.y = n = n > e ? e + (n - e) * l : d > n ? d + (n - d) * l : n, i = a.call(Ha, h), i !== h && (h.x = i.x, h.y = i.y), 1 !== g && (h.x *= g, h.y *= g), A > f && (j = h.x - m, k = h.y - n, j * j + k * k > f && (h.x = m, h.y = n)), h
                        } : a instanceof Array ? function (b) {
                            for (var c, d, e, g, h = a.length, i = 0, j = A; --h > -1;) e = a[h], c = e.x - b.x, d = e.y - b.y, g = c * c + d * d, j > g && (i = h, j = g);
                            //Here this.vars.snapIndex = i tracks the index and axis but might be transformed?
                                //snappedIndex and snappedAxis might be better names?
                            this.vars.snapIndex = i;
                            this.vars.snapAxis = 'x';
                            return f >= j ? a[i] : b
                        }.bind(Ha) : λ.bind(Ha, a)
                    },


I have tested the code to make sure it didn't interfere with anything in Draggable and that it works as expected.

Let me know what you think in regard to my hack / modification as well as the possibility of exposing that same data directly from the plugin and also adding the state information like I was explaining... I can take a crack at doing that but it's less useful for me at the moment as I am not needing to cancel the events or stop the propagation at the moment.

 

 

Link to comment
Share on other sites

I am building a simple range like control as shown in my pens.

 

In order to try to use the tools I have and make the best component possible I am trying to refactor my logic to not require a snap array at all....

This will allow me to only have to set the min and max values accordingly and then I can move the snapping logic to a function.


A) The snap callback is always in the scope of the dragger, even if you have a callback scope. That threw me off at first but it seems it's by design and I believe it's probably correct, a person can bind the function for the scope they needed....

 

B) In a snap function it seems my logic must be to replace the iterative approach of the snaps and just calculate the value, that should always be more efficient that looping so it seems like a better approach however I had the following issues...

 

C) It seems that is the only way to know the value without looking it up without he snapIndex / snapAxis.

 

1) When using snaps the bounds are respected such that if the user exceeds the bounds the correct snap is chosen according to the direction of travel, when using a function you have to determine the direction of travel since your not running the iterative loop anymore. It also changes how you would react to the logic therein...

 

2) It seems that sometimes the logic wraps for getting the snap e.g. if the bounds are small and the object is large or the object is moving then you also have to take into account hit testing in the snap function with respect to the movement and that complicates things a bit as well as you might have to call update as well as apply bounds and it doesn't help that update only takes a Boolean and not the new bounds...

 

I am going to rework my math and logic see where I can get but I wanted to make sure that this is the correct approach and that I am understanding things correctly.

 

Let me know if you need a pen to better understand.

 

 

 

 

Link to comment
Share on other sites

Here is a pen @ 

See the Pen BXgaqO by juliusfriedman (@juliusfriedman) on CodePen



This pen contains the improvements I spoke of above.


I gave up trying to use the snapIndex which didn't exist and I just tried to do things like were available in the library. (And not because my change didn't work but because I want to show you I am willing to learn if I can)

 

You will see that because of the way the call was bound in the calling script I can't even bind my own context because the wall `call` is used to force the context...

 

The logic to run the snap function specifies the context...


`return a.call(Ha,e>c?c+(e-`

Thus my snap function will always have the scope of the dragger? Except when called from a dragging event, then the scope is the callbackScope...

This means I have to adjust my code to always use call when I am calling the snap function and I would also need the dragger instance to which it was bound...

 

That is slightly awkward as I have given a callback scope and I have bound the function yet surprisingly enough it is called with it's own scope. This means I have to call it from that same scope or determine the scope from the function call...

 

The only problem it seems at this point is that I can't approach the 0 boundary...

If you look at the pen and drag the dragger you will see the values in the console, I seem to get the values right for 4, 3, 2 and 1 but the zero boundary is eluding me for some reason right now...

 

In the lower slider there are only values 1 and 0 to make it easier.

 

It seems the snap is working correctly but how to correctly calculate the index is eluding me right now...

 

You will see for the slider with 2 options it works as expected but with 4 options I can't access index 3.


The pen is at 

See the Pen BXgaqO by juliusfriedman (@juliusfriedman) on CodePen

 and I am taking a break.

I am pretty sure that is something with my math though and not your libraries code, specifically @ getSnap where I am probably not understanding how to handle the edge cases...

That logic has been extrapolated here for review:
 

function getSnap(endValue) {
            if (false === Number.isFinite(endValue)) return;
            const max = this.vars.type === 'x' ? this.vars.bounds.maxX : this.vars.bounds.maxY;
            if (endValue >= max) return max;
            const min = this.vars.type === 'x' ? this.vars.bounds.minX : this.vars.bounds.minY;
            if (endValue <= min) return min;            
            const mean = this.vars.mean;            
            return CS.context.Math.round(endValue / mean) * mean;
        };

 

Please do let me know if you see something wrong with that.

You can move the slider manually with code like

 

range.setValue(3, true)

 

You will notice that I can get to the correct position that way...

However when snapping I can't, I run into values 0, 1, 2 and 4 but never 3 and in other examples there are also holes but not 0 and 1....

 

I am going to step away for a bit as I can do nothing else outside of rewrite the logic or go back to using snaps like I was at this point.

Please do let me know if you see anything as this resolution will relate to how my isTweening questions follow.

 

I just updated the pen to log more and check my math.

 

See the Pen BXgaqO by juliusfriedman (@juliusfriedman) on CodePen



I noticed that getSnap is sometimes called just as many if not more times then there are snaps in the array, this depend on the users movement but now I am questioning on if snaps as an array was a better design choice...

 

It seems if the user exceeds the bounds and then comes back in bounds with or without pressing then it can make the issue worse as getSnap may be called more for an unknown reason....

 

I think this is an issue with the way offset location are being determined to stop the movement when the cursor is outside the bounds but again I only have the minified code to look at.

 

The important things to take away is that I can position the slider handle correctly with the setValue method, yet while I cannot when dragging especially when I perform awkward movements e.g. slow and then fast or circular especially when exceeding the bounds.

 

This is easily replicate-able in the various pens I have so let me know if theres anything that I can do better.

 

I like that the snap function is more efficient but if it's going to be called more times than there are snaps in the sequence then it doesn't make sense for me or I imagine many others...

 

We should only have to snap once and if the value exceeds the bounds at that point then the position is given by the min or max of the corresponding axis... Yes it's a little tricky at the bounds when dragging starts because theres nothing to compare to get a previous position to determine the direction so I think it also has something to do with timing as another user was saying but I am even more so confident that my is right for the application and that the problem lies somewhere in Draggable at this point.

 

One confirmed point is that when the position is at the awkward  one and you put a break point, the snap function runs and returns the same value as it did previously however after that breakpoint the element's position can be seen to be changed but the snap function was not called again... 

 

Thus I definitely think this is something to do with timing and how the event mechanism for clicks is able to run one more than 1 thread and sometimes that thread has an event which cannot be canceled and thus Draggable thinks that the event was cancelled yet the browser still has av event dispatched in that state.

 

I am quite sure you know all about how events are implemented in browsers so I will leave it at that but if you happen to see something wrong in my code please do punch me in the face :P

 

Taking the rest of the night to chill as I really don't think there is anything I can do other than go back to using snaps and my hacked up Draggable to save a few cycles which I am hopeful is not the final resolution!

 

Good luck and thank you in advance!

 

 

 

 

Regards!

Link to comment
Share on other sites

I haven't had time to read through your entire post or all the code in your demo, but I wanted to mention a few things and then you can let me know if you still need some help/input: 

  1. In terms of snapIndex, wouldn't it be as simple as feeding the value into indexOf() to find what you need? 
  2. There are actually a deltaX and deltaY values attached to the Draggable instance - would that help you determine the direction that you're after? 
  3. Your getSnap() function seems to assume the type will always be either "x" or "y" (never "x,y" or anything else) which may be totally fine in your use case. I didn't follow what CS.context was (I did a search in the codepen and didn't find anything), nor do I understand what the "mean" was for. Are you trying to make it snap to increments of mean (starting at 0)? If so, that math looks right to me. 
Quote

I like that the snap function is more efficient but if it's going to be called more times than there are snaps in the sequence then it doesn't make sense for me or I imagine many others...

I was confused by this comment - wouldn't you expect it to run on every move event so that your custom snapping logic can be implemented in whatever way you want? Like...we can't necessarily assume that if it's outside bounds, that your snapping logic won't apply some other rule. Of course that seems like it'd be pretty uncommon, but when we let users define a snap function, we've gotta keep things open, you know? And I doubt that there's gonna be some noticeable performance hit from running that logic between snaps or outside bounds. Perhaps I misunderstood what you meant though. 

Link to comment
Share on other sites

The CS is not open source YET, that is something I am cooking up maybe...

It shouldn't be in the pen and if you see it let me know but I just checked and I didn't see it... Perhaps cached somehow... Not sure...

 

If the snapFunction runs more than one time there is something wrong no?.... regardless of x or y or xyz, a snap to a single axis across any dimension would technically be acceptable if you need two snaps you should have two separate functions or the function should call that snap function with the information for the axis...

 

I am going on another tangent... :p

Let me know what I can do to better assist you with this issue. As you can see in the pen with the snap function I can get to index 3 by my setValue but can you ever drag the handle and get to all index's for both input and change?

If so just modify / fork the pen and my apologies.

Regards...

Link to comment
Share on other sites

26 minutes ago, Julius Friedman said:

If the snapFunction runs more than one time there is something wrong no?

No. I'm not sure why you'd think that. The purpose of the snap function is to run logic every time the pointer moves when dragging, so that it can apply any snapping necessary at that point. 

 

28 minutes ago, Julius Friedman said:

As you can see in the pen with the snap function I can get to index 3 by my setValue but can you ever drag the handle and get to all index's for both input and change?

I'm not really sure what I'm looking for, but I see 3 different sliders. I assume you're talking about the bottom one maybe? I can drag it just fine all the way up and down. 

 

29 minutes ago, Julius Friedman said:

Let me know what I can do to better assist you with this issue.

It would be amazing if you could provide a very reduced test case without all the custom code in there - just the most basic thing you can possibly provide that demonstrates the issue. Hopefully less than 20 lines of code total would be ideal. Thanks so much!

Link to comment
Share on other sites

11 minutes ago, GreenSock said:

No. I'm not sure why you'd think that. The purpose of the snap function is to run logic every time the pointer moves when dragging, so that it can apply any snapping necessary at that point. 

 

I'm not really sure what I'm looking for, but I see 3 different sliders. I assume you're talking about the bottom one maybe? I can drag it just fine all the way up and down. 

 

It would be amazing if you could provide a very reduced test case without all the custom code in there - just the most basic thing you can possibly provide that demonstrates the issue. Hopefully less than 20 lines of code total would be ideal. Thanks so much!

 

Yea but one snap should only ever be called per movement right? If I snap I should never call the snap function again? right? I should just be able to rotate the FOV except for the most extreme cases but we are digressing...

What custom code is in the snapFunction pen?

 

Link to comment
Share on other sites

11 minutes ago, Julius Friedman said:

What custom code is in the snapFunction pen?

Oh, I didn't mean that your most recent demo had customizations made to GreenSock tools - I was just trying to be clear about what would make the ideal reduced test case. You've posted some other stuff that did include customizations, so I figured I'd mention it, that's all. 

 

11 minutes ago, Julius Friedman said:

FYI, this is the pen which has an issue

That's what triggered my request for the most simplistic, basic reduced test case. Your codepen has a ton of other code that might (or might not) be causing issues. The goal was to isolate things as much as possible so we can get you an answer quickly and identify things accurately. It's just tough to parse through 400+ lines of code and try to understand what exactly your issue is, that's all. 

  • Like 2
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...