Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...

Search the Community

Showing results for 'overwrite'.

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Forums

  • GreenSock Forums
    • GSAP
    • Banner Animation
    • Jobs & Freelance
  • Flash / ActionScript Archive
    • GSAP (Flash)
    • Loading (Flash)
    • TransformManager (Flash)

Product Groups

  • Club GreenSock
  • TransformManager
  • Supercharge

Categories

There are no results to display.


Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


Personal Website


Twitter


CodePen


Company Website


Location


Interests

  1. That's asking a LOT of the browser to render all of that dynamically as vectors. I'm not at all surprised that performance is suffering. And to be clear, it has nothing to do with GSAP. I'd bet that less than 1% of the load is GSAP - the rest of graphics rendering. Also, I'd use overwrite: true instead of "auto" because it's cheaper. There's no need to do a pupil.forEach() to create a bunch of gsap.to() calls - just do a single gsap.to(pupil...) call (you can pass in multiple elements as the targets (Array). I'd recommend that you consider switching to something like PixiJS instead of using SVG. It'll render way faster but it is more work to set up.
  2. Hi all, I'm having issues with a simple GSAP3 tween code that I use inside an Adobe Animate banner. I have a movieclip that has a simple scaling animation that uses Adobe Timeline. Now, I want that movieclip to scale using GSAP 3 so I wrote a code right after the Timeline animation. The issue is that, the GSAP3 tween doesn't work but it only works when the code is in the last frame of the Adobe Timeline. I've also tried adding overwrite: true but it doesn't work.. I thought maybe I am missing some options that I need to add on my GSAP3 tween. You can check out my simple demo file attached. Thanks in advance! GSAP 3 Scale.zip
  3. Can you explain what you mean? What are you wishing for that immediateRender doesn't provide? And if you just want to get something to render at a new playhead position, you can easily do that by setting the progress() or time(). Yes, that's very intentional and it's also easy to fix. We explain it here: In the old (v2 and earlier) version, the default overwrite mode was "auto". Now it is false because: It's faster (performance) because in the vast majority of cases people aren't creating conflicting tweens, so we can skip the processing involved in hunting down conflicts. It confused people in some cases because they didn't even know overwriting was a thing. The new behavior in v3 ensures that people opt-in to overwriting so that it never catches them off-guard. The fix: either set overwrite: "auto" (or true) in the new/overwriting tweens or you can just set the default mode for all animations to one of those like: gsap.defaults({overwrite: "auto"}); Does that clear things up?
  4. Still wish there was a render method,Very helpful for me One more problem was found when upgrading the old project: Tween does not overwrite the previous Tween very well https://codepen.io/lzy100p/pen/XWaQKpx In gsap2 it will stop after 1 second This is very different from gsap2
  5. It looks to me like the problem is the fact that you're creating conflicting tweens and the "mouseEnter" animation is LONGER (0.3) than the "mouseLeave" (0.2), thus if you move fast enough, you could create a situation where the "mouseLeave" animation ends BEFORE the "mouseEnter" one does, thus the "mouseEnter" renders last. By default, overwrite is false. You could simply set overwrite:"auto" but if you want absolute best performance you could manage that overwriting manually like this: https://codesandbox.io/s/beautiful-leavitt-7o2tw?file=/src/App.js
  6. Thanks for the explanation. Setting overwrite: true I can still get the animation to go awry on very rare occasions. I've not been able to isolate the exact circumstances and will report back when I do. The manual overwrite approach works perfectly and I've settled on that. Thanks again.
  7. Welcome to the forums, @Olly Grunt. Yeah, that's not a bug - that's just a logic issue on your code. You've got overwrite set to "auto", so each tween will wait to execute any overwriting until the very first time it renders at which point it will look for any other CURRENTLY ACTIVE tweens of the same properties of the same targets and kill just those parts. But you've set up your staggers and delays in a way that makes it pretty easy to create a scenario where the old/stale tweens haven't even started yet when the new tweens render for the first time, so overwriting doesn't occur. Simple solution: set overwrite: true instead of overwrite: "auto". When you set overwrite to true, that IMMEDIATELY kills all animations of the same targets regardless of which properties are getting animated or if those other animations have started yet. There are other solutions too, like you could create a variable to store the current animation (onEnter or onLeave, whichever was last to fire) and simply .kill() that before you create your new tween. That's basically like a manual overwrite system Good luck!
  8. Hi all, In the CodePen I've set up a basic toggle that opens and closes a div. As part of the animation, the padding also expands from 0px to 20px. The 20px is however currently hard coded. What's the best way to store the divs original padding and refer to this for future animations? As currently if for example the button is toggled before the animation completes, the padding would return for example 10px if it wasn't hard coded. I was wondering if gsap had any way to store such properties? Or just storing it in a data attribute on the div is a fine solution.
  9. I'm trying to adapt a previous SVG demo... (here) ...to canvas and running into what I believe are fairly basic / newbie issues (both structurally and GSAP-wise). Relevant code begins around line 125 in the demo down below. Questions: Why are the circles tweening back to the bottom? I'm using a gsap.set with overwrite: true so shouldn't that reposition them instantaneously? Likewise, if I change it to a gsap.to and give it a duration (e.g. 10 secs), it will respect that duration. But if I set the duration to 0, it still tweens it as if it were set to 1 second. When I resize the window with the ScrollTrigger enabled, it seems to mess with my debounced resize event. If I comment out the ScrollTrigger, the issue goes away. Is there something I should be doing to avoid this unexpected behavior? I'm sure I'll figure this one out but I thought I'd ask in case it was something simple. Finally, on a structural level, I'm unclear on how to incorporate a ScrollTrigger that updates particle velocity (or timeScale) into this. I don't want to create a new ScrollTrigger every time the draw() or move() method is called. So should I be doing that work inside the ScrollTrigger itself (in an onUpdate)? Or should I be passing ScrollTrigger values into the methods instead? Is there a preferred or recommended style? I've pored over Codepens and this forum but nothing really stands out to me as "obviously correct." I'm trying to emulate as much of Blake's style as possible, but he's a hard act to follow.
  10. You can animate the timeScale of an animation... gsap.to(tl, { overwrite: true, timeScale: 1 }) For ideas on velocity scrolling, this demo might be a good one to learn from. https://codepen.io/GreenSock/pen/eYpGLYL
  11. If I were doing this, I'd probably use a simple function to create a randomized infinitely repeating tween (duration, position) that gets plugged into a timeline and then simply alter the timeScale() of the timeline to make your stuff speed up or slow down. I think that'd be a lot less CPU usage than what you're currently doing with using a modifier on every single element and an onUpdate that's creating a whole new tween every update (don't forget to overwrite: true so you don't end up with a bunch of animations trying to control the same thing). Honestly, I think that's exactly what I'd do. It's not "cheating" - it's a clever problem-solving technique that saves you from having to calculate all those opacity tweens. Is there some reason you don't want to use the current technique? Yes, in my experience <canvas> (especially if it's WebGL-enabled) can render much faster than SVG. Good luck!
  12. we can play animations when we click on the Nav "Slides 1 2 3..." and i want it to start as Slides[0] index array element that is Slide A as already Played or Finished animation, and then we can click on Nav slides to play the animation the issue im having is, i want when i click on Slide 2 3 4 or 5 it should Overwrite gsap.set property Slides[0] by GSAPSlideTL or in other words Slides[0] is not working when click animation to play GSAPSlideTL thank you
  13. GSAP is an animation engine, and doesn't do any rendering. Right now you are using HTML/CSS for rendering. For that warp effect, you would need to use a WebGL renderer like PixiJS or Three.js and probably a custom shader. GSAP could then animate whatever properties your WebGL objects/shader provides. Just follow the first demo you posted. Notice the use of forEach to create a ScrollTrigger for each element, and how it's getting the duration for each element from a data attribute... <div id="del2" class="delayed-section" data-scrub="0.2"> const scrub = gsap.to(imageAnim, { progress: 1, paused: true, ease: "power3", duration: parseFloat(section.dataset.scrub) || 0.1, // <-- reads data-scrub from element overwrite: true }); Using a different duration for each element is what creates the parallax effect.
  14. Hi guys, I am working on a banner animation. I have noticed that when I resize the browser window my video container class does not update. It is understandable because my variables that I passed to scrollTrigger and Timelines are defined once and they keep their values. I have tried to overwrite variables via: document.addEventListener('resize', myCallback) and inside myCallback function I tried to kill() the timelines and run them again but this made duplication of ".pin-spacer" inside a ".pin-spacer" and made a big mess in the DOM. I want to update these variables(videoYShift, videoWidth, videoHeight): searchBannerScrollAnim.to('.search_banner .file_video_wrap', { x: videoXShift, y: -videoYShift, width: videoWidth, height: videoHeight }); My question is what is the valid way to "refresh" the scrollTrigger and Timelines so the video stays centered and responsive on the resize event? The issue is the most visible when you resize the window from your current size to the smaller one. Thank you in advance.
  15. Hi guys! I come humbly in front of you with few drops of hope left, after 5 full days of switching between possible solutions to get a consistent ScrollTrigger behavior on a Gatsby site. Getting directly to you is my last resort, as every google and gsap forum link regarding ScrollTrigger and Gatsby is already visited. 😒 I cannot get a CodePen reproducing the exact issue so I'll try my best to describe it here. Shortly, the problem seems to be, as I suspect, that the ScrollTrigger does not refresh itself when Javascript pops into the browser on top of the SSR-ed html/css bundle. Here's what i did. I created several projects with different versions for dependencies, but i will stick to the simplest one with all dependencies up to date.It's a gatsby with material-ui plugin added, who's exact structure can be found here: https://github.com/mui-org/material-ui/tree/master/examples/gatsby There are no other plugins added, nor any other configs/plugins changed. I rendered the component that will contain the ScrollTrigger (AboutBlock) in the AboutPage page: about.js const AboutPage = () => { return ( <AboutBlock /> ) } export default AboutPage This is the component where i try to animate some elements on reveal when scrolled into view: aboutBlock.js import gsap from "gsap"; import ScrollTrigger from 'gsap/ScrollTrigger'; import animateReveal from "./gs_reveal"; export default function AboutBlock() { gsap.registerPlugin(ScrollTrigger) const revealRefs = useRef([]) revealRefs.current = [] useLayoutEffect(() => { let scrollTriggers = [] scrollTriggers = animateReveal(revealRefs.current) return () => { scrollTriggers.forEach(t => t.kill(true)) } }, []); const addToRevealRefs = el => { if (el && !revealRefs.current.includes(el)) { revealRefs.current.push(el); } }; return ( <Grid container> <Grid item width={{ xs: '100%', sm: '80%', md: '35%' }} pl={{ xs: 0, md: '2.5%' }} mt={{ xs: 60, sm: 0 }}> <Grid container direction="column" alignItems={{ xs: "flex-start", sm: "flex-end" }}> <Grid item mt={{ xs: 0, md: '10vh' }} id="acum"> <Typography variant="h5" textAlign={{ xs: "left", sm: "right" }} ref={addToRevealRefs} className='gs_reveal_fromRight'> NOW WE ARE IN </Typography> </Grid> <Grid item> <Typography variant="h6" textAlign={{ xs: "left", sm: "right" }} ref={addToRevealRefs} className='gs_reveal_fromRight'> LOCATION </Typography> </Grid> <Grid item mt="10vh" id="hi"> <Typography variant="h5" textAlign={{ xs: "left", sm: "right" }} ref={addToRevealRefs} className='gs_reveal_fromRight'> SAY HI </Typography> </Grid> <Grid item className='toughts'> <Typography variant="h6" textAlign={{ xs: "left", sm: "right" }} ref={addToRevealRefs} className='gs_reveal_fromRight'> TELL US YOUR THOUGHTS </Typography> </Grid> </Grid> </Grid> </Grid> } HTML is longer and crowded, I left a part to get the idea of the structure and styling approach (MUI's sx - emotion). And finally, this is the animateReveal function: gs_reveal.js import ScrollTrigger from 'gsap/ScrollTrigger'; import gsap from 'gsap'; export default function animateReveal(elements) { const triggers = [] elements.forEach(function (elem) { hide(elem) let tr = ScrollTrigger.create({ trigger: elem, id: elem.id, end: 'bottom top', markers: true, onEnter: function () { animateFrom(elem) }, onEnterBack: function () { animateFrom(elem, -1) }, onLeave: function () { hide(elem) } }); triggers.push(tr) }); return triggers; } function animateFrom(elem, direction) { direction = direction || 1; let x = 0, y = direction * 100; if (elem.classList.contains("gs_reveal_fromLeft")) { x = -100; y = 0; } else if (elem.classList.contains("gs_reveal_fromRight")) { x = 100; y = 0; } else if (elem.classList.contains("gs_reveal_fromBelow")) { y = -100 } elem.style.transform = "translate(" + x + "px, " + y + "px)"; elem.style.opacity = "0"; gsap.fromTo(elem, { x: x, y: y, autoAlpha: 0 }, { duration: 1.25, x: 0, y: 0, autoAlpha: 1, ease: "expo", overwrite: "auto", delay: elem.classList.contains("gs_delay") ? 0.3 : 0, }); } function hide(elem) { gsap.set(elem, { autoAlpha: 0 }); } The ScrollTrigger markers are misplaced when page loads, and might move (get more misplaced) on hard reloading page, depending on the current scroll position in the moment of reloading, even though the scroll position is not preserved on reload (always is scrolled on top). - The markers are placed on the correct position on resizing, as expected. I followed gsap official docs on react and react-advanced and tried: grabbing the html elements to animate on scroll inside animateReveal() by let elements = gsap.utils.toArray(".gs_reveal"); Assigning to each element a useRef() and use the .current value for each in animateReveal() grabbing html elements using gsap's selector utility gsap.utils.selector changing to simpler animation on scroll, like just a fade refreshing ScrollTrigger in different moments useLayoutEffect(() => { ScrollTrigger.refresh(true) // or ScrollTrigger.refresh() ... }, []); 6. Lifting ScrollTrigger logic to parent about.js page 7. Assigning scrollTrigger to a timeline triggered by the to-be-reveal element 8. Use useEffect() instead of useLayoutEffect() (recommended anyway for ScrollTrigger) 7. Other who-knows-what unsuccessful twists. I suspected a rehydration error, when the static generated code does not match the client side one. But the only JS that could cause a mismatch is the gsap related one, and it does not seem an SSR issue. I checked if the CSS and HTML elements are being properly SSR-ed, by preventing JS from running in the browser. All looking fine. This is both a SSR issue (gatsby build) and a development issue (no SSR). As i said on point 5, setting a ScrollTrigger.refresh() when component is mounted does not work, but delaying this with a 1-2 seconds in a setTimeout successfully solves the issue useLayoutEffect(() => { setTimeout(() => { ScrollTrigger.refresh(true) }, 2000); }, []); This is hard to be accepted as a solution, since i cannot rely on a fixed value to 'guess' when DOM is properly rendered in the eyes of the ScrollTrigger, not to mention the glitches that might occur. So, the question is 'WHY?', why animating with ScrollTrigger from within useLayoutEffect, which is not triggered on the server anyway and should mark the 'component is successfully mounted' moment, seems to not wait for the DOM being completely painted, even though nothing is generated dynamically! There are quite of threads on this forum regarding gatsby, and none seemed to have a clear cause-outcome-solution. Is this battle lost, should i move on? Do you have any suggestions? Thanks so much for your time reading this, it means so much to me!
  16. Hey guys, I'm running into problems when i scroll down or up too quickly. It must be something quite simple, I'm missing. If you scroll down slow, then box 1 appears, and then as you go further down, box-2 appears. But if you scroll down quickly, past both triggers, box 1 doesn't disappear quick enough and all the boxes appear on top of each other. I'm using fromTo's and I've tried "immediate render: false" and "overwrite" but, have not been able to work out what is causing this issue. Hope that all made sense. I've made a minimal codepen demo to show what's going on. Any help is greatly appreciated. Cheers
  17. Hi, In the animation, overwrite: true is not working with mask animation, works fine on other normal tweens. Works fine once animations are finished. Thanks for your help, always appreciated.
  18. Hi, I would like to reset the scroll position before navigating away from a page. ScrollPlugin seems to remember the position from the previous page when on a new page. I have a sample usage: gsap.to(window, { scrollTo: { y: this.triggers[i].trigger, autoKill: false }, overwrite: true, duration: 1, ease: 'expo.out' });
  19. @OSUblake for the case of Angular, I have the refresh in the OnInit hook method. This is just after registering the plugins and before using the scrollTo plugin. A brief below. ngOnInit() { gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollToPlugin); ScrollTrigger.refresh(); // refresh here ... gsap.to(window, { scrollTo: { y: this.triggers[i].trigger, autoKill: false }, overwrite: true, duration: 1, ease: 'expo.out' ... }); } I am guessing the ScrollTrigger.refresh() worked for my case because the elements used with the scroll plugin are the triggers on my ScrollTrigger config. For context, It is for the implementation I was assisted with here:
  20. Hi all, I cannot find what is causing my GSAP in Safari is buggy , laggy, choppy... well what else word can I describe haha. One of the forum links itself looks laggy in Safari. I attached the mp4 video file: left - chrome, right - safari. Here is my GSAP code for reference: function animateFrom(elem, direction) { direction = elem.getAttribute("data-reveal-direction") ?? 0.2; const delay = elem.getAttribute("data-reveal-delay") ?? 0; let x = 0, y = direction * 100; if (elem.classList.contains("js-reveal_fromLeft")) { x = -100; y = 0; } else if (elem.classList.contains("js-reveal_fromRight")) { x = 100; y = 0; } console.log("x: ", x); console.log("y: ", y); // gsap.set(elem, { x: x, y: y, autoAlpha: 0 }); elem.style.transform = "translate(" + x + "px, " + y + "px)"; elem.style.opacity = "0"; const tl = gsap.timeline({ scrollTrigger: { trigger: elem, once: true, markers: true, }, }); tl.to(elem, { duration: 1.25, x: 0, y: 0, autoAlpha: 1, ease: "power2.out", // overwrite: "auto", delay: delay, }); } function hide(elem) { gsap.set(elem, { autoAlpha: 0 }); } // Scroll reveal gsap.utils.toArray(".js-reveal").forEach(function (elem) { animateFrom(elem); }); gsap-safari-laggy.mp4
  21. Just one quick [untested] idea: liveSnap: {x: (value) => { // animate to the snapped value gsap.to(target, {x: gsap.utils.snap(100, value), duration: 0.3, overwrite: true}); // but return the unsnapped value return value; }},
  22. tldr. it feels that possibly timelines due to their nature of taking in element values straight away (not at the time of playing), might be the cause of glitch because values are constantly changing especially that its 3 timelines controlling elements. I am trying with overwrite both auto and true, but it's not working correctly. There are two problems in which focusing on the second is more important. The first one is the issue of inputing, in this case hovering, as Blake mentioned. There are methods to solve this, one being what Blake suggested. The result should strive to avoid triggering, hovering on/off , in a consecutive way. Here especially as hovering off reverses() the timeline. The second is really the overwriting issue. What seems a viable solution to me possibly presents a cornerstone mechanics for interactive control of multiple elements. On each hover, a timeline is created with .to(). At that moment timeline renders and picks up the current values in order to create the interpolation. And it starts interpolating. On a new hover, the same thing is repeated, the then-active timeline is paused/killed, and a new one is taking over. This seems plausible. In terms of Promise.All, it seems to me that it is better suited for chaining timelines/animations. Here it is taking over method, for multiple elements at the same time. Is this possibly not well suited for GSAP timelines? I pondered possibly trying to do this with JS/classes/CSS/transition. A bigger overkill would be canvas I presume.
  23. If you're animating the same properties in different timelines you can use overwrite:true Possibly even https://greensock.com/docs/v3/GSAP/Timeline/invalidate() Scaling/moving on hover is always a tricky one though because inevitably you'll find a spot whilst hovering where the element will get 'stuck' scaling up and down and in and out of your mouse 'hit area' Blake will be able to advise more on the react side of things.
  24. How do I overwrite scrollTrigger with an other timeline that also scrollTo the element on click, but keep the scrollTrigger alive to use the `onLeave` trigger? I have a page with videos and on scrollTrigger enter the videos should play and scale a bit (✅ working), but then when the user clicks the video should scale to 100% (❌ not working). I've set the property `overwrite: true` on the time line, but that doesn't seem to work in combination with scrollTrigger. In my demo you can see an orange box that scales to 100% on click, the video tries to do the same, but is taken over by the scrollTrigger timeline which I want to overwrite. I've tried killing all the scrollTriggers, but I need the `onLeave` trigger for when the user scrolls away again, so I can scale the video back and pause the video
  25. Well, CodePen has been down for a while so I can't provide a forked demo but here's how I'd do the JS: let n = 15; let parallax = []; // we'll store the animations in here. let clamp = gsap.utils.clamp(0, 1); let currentX = 0; let snap = gsap.utils.pipe(gsap.utils.snap(450), gsap.utils.clamp(n * -450, 0)); // Set #slides width for draggable bounds gsap.set('#slides', {width:n*450}); // Populate slide boxes for (var i=0; i<n; i++){ var box = document.createElement('div'), img = new Image(), link = document.createElement('a'); gsap.set(box, { width:400, height:600, overflow:'hidden', position:'absolute', top:50, left:i*450, attr:{ class:'box b'+i }, background:'#333' }); gsap.set(img, { position:'absolute', left:-300,//-i*50, attr:{src:'https://picsum.photos/id/'+(i+10)+'/700/600/'} }); parallax[i] = gsap.to(img, {x: 300, ease: "none", paused: true}); gsap.set(link, { position:'absolute', textAlign:'center', width:105, height:70, paddingTop:'7px', top:490, left:-25, rotation:90, fontSize:'45px', color:'#000', background:'#fff', mixBlendMode:'lighten', textDecoration:'none', innerHTML:'<span style="font-size:20px">IMG </span>'+(i+1), attr:{ class:'imgLink', href:'https://picsum.photos/id/'+(i+10)+'/700/600/', target:'_blank' }, }); box.appendChild(img); box.appendChild(link); slides.appendChild(box); } // Make #slides draggable Draggable.create('#slides', { type:'x', bounds: {left: innerWidth/2, width:1}, zIndexBoost: false, onDrag:updateParallax, inertia: true, onRelease: function() { currentX = this.endX }, onThrowUpdate: updateParallax, snap: snap }) function updateParallax() { // parallax should start from the right edge of the screen and we know that the #slides starts with its left edge centered, thus we add innerWidth/2 let x = gsap.getProperty('#slides', 'x') + window.innerWidth / 2, // convert the position in the viewport (right edge of viewport to -400 because that's when the right edge of the element would go off-screen to the left) into a progress value where it's 0 at the right edge and 1 when it reaches the left edge normalize = gsap.utils.mapRange(window.innerWidth, -400, 0, 1); // apply the clamped value to each animation parallax.forEach((animation, i) => animation.progress(clamp(normalize(x + i * 450)))); } updateParallax(); // Update draggable bounds onResize window.addEventListener('resize', ()=>{ Draggable.get("#slides").applyBounds({left: innerWidth/2, width:1}) }); // Previous & next buttons $('#prev, #next').on('click', function(e) { let nextX = snap(currentX + (e.currentTarget.id === "next" ? -450 : 450)); if (nextX !== currentX) { gsap.to("#slides", {x: nextX, duration: 0.3, onUpdate: updateParallax, overwrite: true}) currentX = nextX; } }); $('#prev, #next').on('mouseenter', (e)=>{ gsap.to('#'+e.currentTarget.id + ' circle', {attr:{r:22}, ease:'expo', overwrite: true}) }); $('#prev, #next').on('mouseleave', (e)=>{ gsap.to('#'+e.currentTarget.id + ' circle', {attr:{r:20}, ease:'expo', overwrite: true}) }); // Img Link rollover/out behavior $('.imgLink').on('mouseenter', (e)=>{ gsap.to(e.currentTarget, {x:10, duration:0.3, ease:'power3', overwrite: true}) }); $('.imgLink').on('mouseleave', (e)=>{ gsap.to(e.currentTarget, {x:0, duration:0.3, ease:'power4.inOut', overwrite: true}) }); The general idea is: You only want the parallax effect to exist while each individual element is inside the viewport (not the entire movement of the #slides). I created a simple linear animation of x from 0 to 300 for EACH element. Paused. Dumped them into an Array. The updateParallax() function loops through each one and sets the progress() according to its position (which we know because they're 450px apart). It's all based on the viewport so that progress would be 0 when it's on the far right edge of the screen and 1 when the element's right edge reaches the left edge of the viewport. I also made the following improvements: I applied inertia with snapping directly on the draggable so it's super smooth and users can flick it. The logic in the next/previous buttons allows users to click the buttons quickly and it still works (instead of ignoring clicks while animation is running). Sorry, that's my pet peeve when the interface ignores user clicks. I hope that helps, Tom!
×