Jump to content
Search Community

Snap to dynamic hit element using GSAP/draggable? - Nuxt 3/Vue 3 Friendly

SamuelMb test
Moderator Tag

Go to solution Solved by SamuelMb,

Recommended Posts

Intro:
Theres probably not a simple anwser to this but I wanted to see if someone more gsap experienced than me knew of some cool trick that could help me before I go on to do some overly complicated way of solving my issue. Whatver the case, thanks to anyone who is willing to give my issue a shot one way or another. 

Context:
So, I want to create a component builder that lets users drag and drop blocks in place. As part of this I am using GSAP Draggable to drag and drop an element. The riddle im stuck on at the moment is I want the element to snap into place when its dragged over an area that can receive it. The simple solution is to just use the hitTest() function to detect if eligeble element is hit and then get either the points off that element to use in the livesnap, or get the transforms to match that element or something like that. Problem is that in a more complex example, for my use case, I dont actually want to manually create a uniqe function for every possible snappable area since there might be a lot. The ultimate solution would be if I could somehow dynamically fetch whatever snappable element we have hit. But as far as I can tell, from the docs, the hitTest() doesnt actually return the hit element, it just returns a boolean which kinda forces me to do something uniqley for each individual element that can get hit.

Question:
Is there a straight forward way to dynamically get whatever element I hit with the hitTest()?

Codepen:
Ive provided a codepen that acts like a massive simplification of what I am trying to do just to narrow it down to exactly what I am asking and making easy to experiement with solutions. Hope that makes it simpler to understand. 

See the Pen WNzpgmg by carelesscourage (@carelesscourage) on CodePen

Link to comment
Share on other sites

2 hours ago, SamuelMb said:

But as far as I can tell, from the docs, the hitTest() doesnt actually return the hit element, it just returns a boolean which kinda forces me to do something uniqley for each individual element that can get hit.

I'm a bit confused by this - why would you need to get the element back if you have it to feed in in the first place? In other words...

if (draggable.hitTest(element)) {
  // we know element is what hit
}

I must be missing something. 

 

Efficient hit testing of LOTS of elements is tricky. There are many strategies you could employ, but if I were you I would not be calling .hitTest() on every single element on every single onDrag(). That seems wasteful. A few ideas:

  • onPress or onDragStart, you could loop through every potential element that it could hit and create the exact x/y ranges that would trigger a hit for each so that onDrag, you're only looping through and comparing the current x/y to numeric ranges. That's much faster than calling getClientBoundingRect() on every element every time. At the very least, you could cache the getBoundingClientRect() for all the non-dragging elements for fast comparison. 
  • Tricky idea: figure out how far the pointer is from the center of the dragging element, then place an invisible <div> with opacity: 0 on top of every potential hitTest element such that all you need to do is listen for a "mouseenter" to know that it's intersecting. This allows you to do ZERO comparisons while dragging, and only rely on a simple event listener. I haven't personally tried this, so there may be some gotchas. I'm just offering an out-of-the-box potential solution. 

Good luck!

  • Like 1
Link to comment
Share on other sites

  • Solution
On 7/19/2022 at 11:03 PM, GreenSock said:

why would you need to get the element back if you have it to feed in in the first place?

Explanation
I was under the wrong impression that If I passed a selector into the hitTest then it would do the hit test against every element that fit the selector. In which case I dont neccesarily have the element when I execute the hitTest() I just have a selector which might fit many diffirent elements and I want to return the element of that selector that was actually hit. Anyway, I realize now that this is not actually how the hitTest() works and Ive finished my solution. Thanks for your help. Your suggestions where very helpfull for much of my experimentation. 

Issue Resolved
I experimented with a lot of diffirent ways of doing it using the liveSnap property. But nothing quite did all the things I wanted with the flexibility I was looking for. Instead I ended up bringing in the flip plugin and just using that to create my own snapping to complement the draggable plugin. Which turned out to be the perfect solution. I have a ton of flexibility now. 

Record
For the record, in case anyone else is trying to do something similar ill post another codepen with the approach I came up with for my solution. 

See the Pen VwXbLRx by carelesscourage (@carelesscourage) on CodePen



The Code
My real source code is doing all this in Nuxt 3 which has do do some extra juggling because of SSR. Heres how I did it in Nuxt 3 if anyone is spesifically dealing with that approach. Also, for the record, the below code will only work in Nuxt 3 if you transpile GSAP. Otherwise you have to import the gsap dependecies using require() inside the process.client check. Heres the Nuxt 3 docs to explain how to transpile dependencies: docs

<script setup>
import { gsap } from "gsap"
import { Flip } from "gsap/Flip";
import { Draggable } from "gsap/Draggable";

const fragElement = ref(null)
let zones = []
let fragRect = null

let draggable = null

onMounted(() => {
  if(process.client) {
    gsap.registerPlugin(Flip, Draggable);

    zones = document.querySelectorAll('[data-dropzone]')
    fragRect = fragElement.value.getBoundingClientRect()

    draggable = new Draggable(fragElement.value, {
      onDrag: onDrag,     
      onRelease: onRelease,
    })
  }
})

function zoneHit(el, {hit, mis = () => {}, dud = () => {}}) {
  let noHit = true
  function onHit(zone) {
    noHit = false
    hit(zone, el)
  }

  zones.forEach((zone) => {
    el.hitTest(zone)
      ? onHit(zone)
      : mis(zone, el)
  })
  
  noHit && dud(el)
}

function landHit(zone, el) {
  zone.classList.remove("drag-hit");
  zone.classList.add("land-hit");
  Flip.fit(el.target, zone, {
    duration: 0.1,
  });
}

function landMis(zone, el) {
  zone.classList.remove("drag-hit");
  zone.classList.remove("land-hit");
}

function onDrag(e) {
  fragElement.value.classList.add("drag")
  zoneHit(this, {
    hit: (zone) => zone.classList.add("drag-hit"),
    mis: (zone) => zone.classList.remove("drag-hit")
  })
}

function onRelease(e) {
  fragElement.value.classList.remove("drag")
  zoneHit(this, {
    hit: landHit,
    mis: landMis,
    dud: () => gsap.to(this.target, {duration: 0.5, x:0, y:0})
  }) 
}

</script>


<template>
<div 
  ref="fragElement"
  class="frag u-panel" 
>
  <slot></slot>
</div>
</template>

<style lang="scss" scoped>
.frag {
  background-color: var(--background);
  padding: var(--space-familiar);
  border: solid 3px red;

  transition: all none !important;
  transition: max-width 0.4s !important;

  max-width: 700px;
}
</style>



 

  • Like 1
Link to comment
Share on other sites

  • SamuelMb changed the title to Snap to dynamic hit element using GSAP/draggable? - Nuxt 3/Vue 3 Friendly

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...