GSAP/CSS transform origin and glitchy hover effect/ SVG Regions - javascript

I am attempting to animate an svg doughnut circle with gsap. After much testing with code and layering, I am stumped with a glitchy hover effect (which I tried to resolve with pointer events) and the transform origin is only applied to a few of the expanded tabs. I am wondering if this might be that the tabs may have a different bounding box?
Comments added per request:
Side Note: I've tried applying fill-box to entire svg instead, I'm wondering if I need a parent layer thats an exact square so I can apply the transform origin for the child "expandtabs" to the center of that?
I assumed I needed to iterate through an array of both to have the tabs correspond. Unless the tabs were children of each other?
TLDR; Tabs are not scaling from center of circle, and glitchy hover effect
CodePen Example
.expandtab {
pointer-events: none;
transform: fill-box;
transform-origin: -15px 25%;
}
Javascript:
const subTabs = gsap.utils.toArray(".subtab");
const expandTabs = gsap.utils.toArray(".expandtab");
const tl = gsap.timeline({ defaults: { duration: .05, } });
tl.set(expandTabs, {
visibility: "hidden",
opacity: 0,
scale: 0,
});
subTabs.forEach((subTab, index) => {
let expandTab = expandTabs[index];
// Event Listener Hover on
subTabs[index].addEventListener("mouseover", (event) => {
console.log("you clicked region number " + index);
tl.to(expandTab, {
visibility: "visible",
opacity: 1,
scale: 1,
});
});
// Event Listener Hover off
subTabs[index].addEventListener("mouseout", (event) => {
console.log("you exited region number " + index);
tl.to(expandTab, {
opacity: 0,
scale: 0,
visibility: "hidden",
});
});
});

About the glitchy hover effect, the mouseenter and mouseleave will do the job better. mouseover is firering way to much...
For the "growing" effect, it is more complex. The transform-origin CSS property won't be enought. Any way, you will need different values for each five parts of the circle.
Additionnaly, you will need to adjust a transition to "fit" or "keep" the inner part of the circle in place. I suggest you to look at the fromTo method of GSAP. That will allow you to specify explicitely the starting and the landing coordinates.
Be patient! ;)

Related

Any way to update the duration of an animation set via element.animate without restarting the animation?

I'm developing a game engine in HTML5. Characters are div elements using an animated sprite for background. As sprite animation have fluid parameters and must be set by code, they can't be predefined in a static CSS definition, thus I use element.animate to set sprite animations to a given row at a given speed knowing my scales and frame counts.
// Applies the given frame and animation to the sprite
// Frame is an angle, clockwise direction: 0 = up, 1 = right, 2 = down, 3 = left
set_animation(frame, duration) {
const scale_x = this.settings.sprite.scale_x * this.settings.sprite.frames_x;
const pos_y = this.settings.sprite.scale_y * -frame;
// Cancel the existing animation
if(this.data_actors_self.anim) {
this.data_actors_self.anim.cancel();
this.data_actors_self.anim = null;
}
// Play the animation for this row or show the first frame if static
if(duration > 0) {
this.data_actors_self.anim = this.element.animate([
{
backgroundPosition: px([0, pos_y])
}, {
backgroundPosition: px([scale_x, pos_y])
}
], {
duration: duration * 1000,
direction: "normal",
easing: "steps(" + this.settings.sprite.frames_x + ")",
iterations: Infinity
});
this.data_actors_self.anim.play();
} else {
this.element.style.backgroundPosition = px([0, pos_y]);
}
}
Obviously that's a snippet from an actor class function: this.element is the div, this.settings is an object with parameters to be used who's names should make sense in this context, the px() function is a simple converter to turn arrays into pixel strings for HTML (eg: [0, 0] to "0px 0px").
The issue I'm having: While I can always run this function to set a new animation, I want the ability to change the speed of the animation without resetting it. It doesn't need to be a smooth transition, for all I care the new speed can be applied at the next iteration... I only want to avoid a visual snap or any kind of reset upon applying the change. Once an animation is set, I have no idea how to access and update its duration parameter. Does anyone have any suggestions?
When using console.log on this.data.anim I'm rightfully told it's an animation object. I tried using JSON.stringify to get more information but nothing relevant is printed. this.data.anim.duration returns undefined so the setting must be stored under some other property. Even if I know that property, I'd like to be sure web browsers will agree with me changing it like this.data.anim.options.duration = new_duration.
You can wait for the end of an iteration before changing the animation duration if that is what is required.
This snippet only sets an event listener for animationiteration event when you click the button to increase the speed.
function upthespeed() {
const div = document.querySelector('div');
div.addEventListener('animationiteration', function() {
div.style.animationDuration = '1s';
});
document.querySelector('button').style.display = 'none';
}
div {
width: 10vmin;
height: 10vmin;
background-color: magenta;
animation: move 10s linear infinite;
}
#keyframes move {
0% {
transform: translateX(50vw);
}
50% {
transform: translateX(0);
}
100% {
transform: translateX(50vw);
}
}
<div></div>
<button onclick="upthespeed()">Click me to increase the speed at the end of the next iteration (you may have to wait!)</button>
The value for the animation duration isn't in the Animation object itself but in the CSS animation-duration property for the Element: so this.data_actors_self.style.animationDuration = new_duration will do the job. It will however restart the animation if it is being played, but if I understand correctly that isn't a problem for you.
Edit: To change the animation's duration without restarting it, all you have to do is set the value of anim.startTime to what it was before. For example:
const startTime = anim.startTime;
this.data_actors_self.style.animationDuration = new_duration
anim.startTime = startTime;

Javascript animate Method Accumulation Problem

I am trying to do an animation example but there is a problem about accumulation. At first click on the button, the translate animation is done and the position of the element changes permanently. However, on second click it does the translate animation again but this time does not keep the last position. How to overcome this? I went through MDN document and applied the optional section but failed to complete this challenge. Regards,
https://jsfiddle.net/ja218pbr/19/
document.getElementsByTagName("button")[0].addEventListener("click", function() {
document.querySelectorAll("div")[0].animate([
{transform: 'translate(100%, 0)'}
], {
duration: 250,
composite: "accumulate",
iterationComposite: "accumulate",
fill: "forwards"
});
});
If I'm understanding this correctly, you want to be able to let the object slide again from the position it ended in earlier. To do this we can get the boundingClientRect each time the button is clicked and calculate the new translation distance by basically taking the width of the object and adding the left distance of the client rect, this will effectively allow the rectangle to keep moving from the distance it ended in before. Also I removed the composite property because it caused the rectangle to jump over the correct position.
document.getElementsByTagName("button")[0].addEventListener("click", function() {
const clientRect = document.querySelectorAll("div")[0].getBoundingClientRect();
const translationX = clientRect.width + clientRect.left;
document.querySelectorAll("div")[0].animate([{
transform: `translate(${translationX}px, 0)`
}], {
duration: 250,
iterationComposite: "accumulate",
fill: "forwards"
});
});
body {
margin: 0;
}
div {
position: relative;
height: 150px;
width: 150px;
background-color: yellow;
text-align: center;
line-height: 150px;
}
<div>Moving Object</div>
<button>Press</button>

How to animate between 2 scales in a zoom function in react?

I want to transition when my state changes. The result I currently have is that my entire component is reload during zoom, I would like a smoother zoom. How can I do this?
I use react-tappable for detect pinch.
Here is my function :
// Zoom on pinch until maxScale, else resize to initial scale
handlePinchStart = () => {
const { scale, maxScale } = this.state;
if (scale <= maxScale) {
this.setState(state => ({ scale: state.scale + 0.5 }));
} else {
this.resize();
}
}
You can add or remove CSS class to your element which you want to zoom in or out, and do this according to your state. For example,
return (
<div className={scale <= maxScale ? "my-element zoom-in" : "my-element"}>My Element</div>
)
It simply says, if your scale state is less than or equal to the maxScale, then make the className of your element my-element zoom-in, else make it just my-element. And in CSS, these classes can be like this:
.my-element {
/* Some stylings for your element */
transition: transform 0.5s;
}
.my-element.zoom-in {
transform: scale(2);
}
As long as your scale state is <= maxScale, your element will have .zoom-in CSS class attached to it. And .zoom-in class simply makes your element's scale doubled. And thanks to putting transition: transform 0.5s; in your actual element's class(.my-element), it will grow smoothly and when scale is bigger than myScale, .zoom-in class will be removed, your element will only have .my-element class, and shrink back to its original size smoothly.

Vanil Javascript .animate() hamburger menu disappears after completion

I have an Svg hamburger menu which i'm using to target an event listener,
this invokes the javascript .animate() api & brings the menu pane in from the left,
however when the animation is completed it dissapears(it does not return to it's original position), it just vanishes. I would like it to stay put & then set-up a seperate function to fade it out or slide back.
My HTML is here:
<svg>`Hamburger Code`</svg>
<section id="menuPane" class="lftMenu"
style="position:absolute;
left:-888px;
z-index:18;">
<div class="test">
</div>
<div class="tst2">
</div>
</section>
My CSS is here:
.test{
display:block;
position:absolute;
width:calc(27vw - 7vmax);
height:calc(55vh + 4.8vmin);
top:48.4px; left:8px;
background-image:linear-gradient(-48deg,green,yellow);
border-radius:8.2px;
z-index:19;
}
.test:before{
content:"";
display:block;
position:relative;
width:inherit; height:inherit;
filter:opacity(0.55) hue-rotate(120deg);
background-image:radial-gradient(ellipse at top,blue,green);
border:solid inset 28px transparent;
border-radius:8.2px;
z-index:20;
}
.tst2{
display:block;
position:absolute;
top:48.4px; left:8px;
width:calc(27vw - 7vmax); height:calc(55vh + 4.8vmin);
background-image:url('GalvPlt1.jpg');
background-size:contain;
filter:opacity(0.2);
border-radius:8.2px;
z-index:21;
}
.tst2:hover{
filter:hue-rotate(208deg) opacity(0.44);
}
My Javascript is here:
const menu_pane = document.getElementById("menuPane");
//UiContr.animate([
// Animate opacity to fade in during page load.
//])
let Elem = document.getElementById("Elem"); // This El is the svg box above the menu pane.
Elem.style.position = "absolute";
//Elem.style.willChange = "auto";
Elem.animate({
opacity: ['1', '0.224', '1', '0.498', '1'],
filter: ['hue-rotate(0deg)', 'hue-rotate(-72deg)', 'hue-rotate(0deg)']
// borderRadius: ['8px', '14px']
},{ duration: 4864,
easing: 'ease-in-out',
iterations:7800
// 0.1 hours = 360,000 ms...
// fill: 'reverse'
});
Elem.addEventListener('click',(e) => {
// menu.style.animationFillMode = 'forwards';
menu_pane.animate({
left: ['-888px','8px']
},{ duration: 600,
easing: 'ease-in-out',
iterations:1,
// fill: forwards,
});/*.then(
menu_pane.style.left = "8px";
);*/
});
These are sitting ontop of 3 layered Canvases, one buffer, one menu & the main game window.
All with lower z-index values. I can't figure out where its going to...
I tried this:
menu.style.animationFillMode = 'forwards';
*.tried this in the CSS file aswell none camel case.[nope nothing.]
As a similar question stated animations always return to there initial state.
but that would mean it would slide back to the left, not just pop of the screen & down the pub.. & I also tried chaining a .then() on the end, but this failed also..
Also as an Aside the tutorial I watched, used fill: forwards, reverse, or both & also infinity for iterations as options of .animate() but now I trying these none are being recognised or working.
>.... Partially Solved, Infinity needed Capitolized first letter.
Also, Which is the best way to write these out of the following 2 methods;
Youtube Tutorial Version is:
.animate({property:['from','to']},{',' seperated options});
MDN version is:
.animate([{Property: 'from'},{Property: 'to'}],{',' seperated options});
both of these examples work & animate the same, achieveing about 58fps, but is there a "Correct" or Optimal way to write this.
I would lean more towards the MDN version being more 'Correct' or optimal,
but is there any reason for the two variants, which should I choose?
Although the MDN version you have to write the property name multiple times & with complex, multi parameter animations that might end up being quite a bit more code.? any inputs:
This is the example version of V. Javascript .animate() function.
document.getElementById("tunnel").animate([
// keyframes
{ transform: 'translateY(0px)' },
{ transform: 'translateY(-300px)' }
], {
// timing options
duration: 1000,
iterations: Infinity
});
Thanks in advance any who comments or answers..

How to read size and move hidden element before Vue transition starts on it?

How to read dimensions and move a div that is hidden before Vue transition starts? For example, a user clicks a button and I want to move a hidden div to appear under the button with a fade-in transition. I need to be able to both read the dimensions and move the top/left position of the hidden div before the transition starts.
Let's say I'm using v-show="active" on the div, where active is my reactive data property I want to set to true and be able to move the div before transition starts.
I've tried all these:
Move the div first, then on nextTick set active = true.
Use the javascript hook beforeEnter to try to move the div before transitions start.
Use the javascript hook enter (and 'done' callback) to try to move the div before transition starts.
Tried all the above with updating the DOM immediately with the new position before setting active = true. (In other words, not via data binding, but actually setting element style properties directly like this.$refs.content.style.top = '500px' to avoid any waiting on the virtual DOM.) However, ideally I would like to accomplish this without directly touching the DOM, but using nextTicks instead. Both approaches fail.
Tried with some success with a hacky transition: all .8ms ease-in, top 1ms, left 1ms.
Tried with success with moving the div first then setting active in a setTimeout. This is not the right solution though.
Update
Thanks to the accepted answer I was able to see that I can read dimensions on nextTick (by which time v-show has turned on display). However, it turns out I needed the transition to be all transition all .3s and that would cause the movement to be included. The DOM will gather up all the changes and apply them together, which means they get lumped into the transition that is later added by Vue. The solution ended up being that I needed to make the movements, then trigger the DOM to repaint first, then trigger the v-show to turn on. Here's an example method:
startTransition () {
this.$refs.content.offsetHeight // <-- Force DOM to repaint first.
this.isContentActive = true // <-- Turns on v-show.
},
Use v-bind:style to move your window and it all works as intended.
Update: To check the size of the popup itself, it has to be shown, so I'm using v-show instead of v-if. The first thing I do is make it visible; on the next tick, I can measure it and place it.
new Vue({
el: '.container',
data: {
top: 0,
left: 0,
width: 0,
show: false
},
methods: {
showFloater: function(evt) {
const t = evt.target;
this.show = true;
Vue.nextTick(() => {
const fEl = this.$el.querySelector('.floating');
this.top = t.offsetTop + 30;
this.left = t.offsetLeft;
this.width = fEl.offsetWidth;
setTimeout(() => this.show = false, 1000);
});
}
}
});
.container {
position: relative;
}
.floating {
border: thin solid black;
padding: 3em;
position: absolute;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to /* .fade-leave-active in <2.1.8 */ {
opacity: 0
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<div class="container">
<button #click="showFloater">Could go here</button>
<button #click="showFloater">Or here</button>
<transition name="fade">
<div v-show="show" class="floating" v-bind:style="{
top: top + 'px',
left: left + 'px'
}">
This window is {{width}}px wide.
</div>
</transition>
</div>

Categories