A Reasonable Approach for Getting Comfortable With Command Line

Considering how much the command line is an integral part of the developer's workflow, it should not be thought overly difficult or tedious to learn.
At one time I avoided it myself, but one day began teaching myself ways to make the difficult as easy as it should be. I got over the hurdle, and you can too. It was worth investing my time to increase my command line comfort-level, and I am going to share a few tips and resources that I found helpful in this post.
The intended reader is the type who typically avoids the command line, or perhaps uses it occasionally but not as a regular or essential tool.

Tip #1: Maintain a Pragmatic Mindset
The way to get more comfortable with the command line is this: practice. You practice, and you get better. There is no secret trick to this; study and repetition of skills will turn into understanding and mastery. There is no use in the mindset that you cannot do this; it will only keep you from your goal. You may as well discard such thoughts and get down to it.
Tip #2: Keep a Cheat sheet
Don't be afraid to keep a cheat sheet. I find that a thin, spiral-bound notebook kept next to my keyboard is perfect; writing the command down helps commit it to memory; having it in a place where I can refer to it while I am typing is convenient to the process. Do not permit yourself merely to copy and paste; you will not learn this way. Until you know the command, make yourself type it out.
Tip #3: Peruse languages outside of the one(s) you normally use

Spend time looking at commands in various languages, looking at the commands even if you don't immediately absorb, use, or remember them. It is worth it to invest a bit of time regularly, looking at these commands; patterns will eventually emerge. Some of them may even come back to you at an unexpected time and give you an extra eureka moment!
Skimming through books with lots of CLI commands can prove interestingly useful for recognizing patterns in commands. I even take this one step further by getting my favorites spiral-bound. I am a big fan of spiral binding; a place like FedEx offers coil binding services at a surprisingly low cost.

Tip #4: Practice... safely
When I am advising someone who is new to contributing to open source, they are inevitably a bit nervous about it. I think this is perfectly natural if only to comfort myself that my own nervousness about it was perfectly natural. A good way to practice, though, is to set up your own repository for a project and regularly commit to it. Simply using common Git commands in a terminal window to commit inconsequential changes to a project of your own, will establish the "muscle memory" so that when it does come time to actually commit code of consequence, you won't be held back by still being nervous about the commands themselves.
These are the commands I have noticed most common to use in the practical day-to-day of development. It's perfectly acceptable to expect yourself to learn these and to be able to do any of them without a second thought. Do not use a GUI tool (they make weird merge choices). Learn how to write these commands yourself.

Check status
Create a new branch and switch to it
Add files

Add all the changes
Just add one of the changes

Commit
Push to a remote branch
Get a list of your branches
Checkout a branch
Delete a branch
Delete a branch even if there are changes
Fetch and merge the changes to a branch

Syncing a fork took longer to learn- I don't often spend my work hours writing code for a repository that I don't have access to. While contributing to open source software, however, I had to learn how to do this. The GitHub article about the topic is sufficient; even now I still have it bookmarked.
Tip #5: Level Up!
I really enjoy using Digital Ocean to level up my skills. Their step-by-step guides are really useful, and for $5 USD per month, "Droplets" are a cost-effective way to do so.
Here's a suggested self-learning path (which, feel free to choose your own adventure! There are over 1700 tutorials in the Digital Ocean community):

Create a Droplet with Ghost pre-installed. There is a little command line work to finalize the installation, which makes it a good candidate. It's not completely done for you but there's not so much to do that it's overwhelming. There's even an excellent tutorial already written by Melissa Anderson.
Set up a GitHub repo to work on a little theming for Ghost, making small changes and practice your command line work.

It would be remiss of me to write any guide without mentioning Ember, as the ember-cli is undoubtedly one of the strongest there is. Feel free to head over to the docs and read that list!
Conclusion
There may be some that find this brief guide too simplistic. However, as famously said by S. Thompson in Calculus Made Easy- "What one fool can do, other fools can do also." Don't let anyone else make you think that using the command line is horribly difficult, or that they are a genius because they can. With practice, you will be able to do it, and it will soon be a simple thing.

A Reasonable Approach for Getting Comfortable With Command Line is a post from CSS-Tricks
Source: CssTricks


How to Market Your Event on Facebook

Hosting an event is always a challenge for a company, it means investing time, money and effort to succeed. But it’s worth it! During important occasions, or even as a regular part of your marketing strategy, events would help you to connect to your customers and give them an experience that will make your brand Read more
Source: https://adespresso.com/feed/


XR: VR, AR, MR—What's the Difference?

What is XR?

Extended Reality (XR) refers to all real-and-virtual environments generated by computer graphics and wearables. The 'X' in XR is simply a variable that can stand for any letter. XR is the umbrella category that covers all the various forms of computer-altered reality, including: Augmented Reality (AR), Mixed Reality (MR), and Virtual Reality (VR).
Virtual Reality
For ease, let’s start with a topic many of us are already familiar with—Virtual Reality (VR). VR encompasses all virtually immersive experiences. These could be created using purely real-world content (360 Video), purely synthetic content (Computer Generated), or a hybrid of both. This medium requires the use of a Head-Mounted Device (HMD) like the Oculus Rift, HTC Vive, or Google Cardboard.
VR has its own spectrum in and of itself. On one end you have WebVR, the simplest and most accessible form, and on the other you have Fully-Immersive VR, like Multi-sensory Cinema. Don't ask me how they incorporate taste, but apparently "Virtual Vineyards" are a thing now.

Augmented Reality
Augmented Reality (AR) is an overlay of computer generated content on the real world. The key note here is that the augmented content doesn't recognize the physical objects within a real-world environment. In other words, the CG content and the real-world content are not able to respond to one another.

​ Google Translate’s AR feature “Word Lens” uses your camera to translate signs, menus, and similar items in real-time from one language to another. Source: Google

Using Google Translate as an example, we can identify images and detect planes to place computer generated content, but the graphics can’t interact with the environment beyond what the camera captures. Let’s look at another example.

IKEA's latest mobile app, IKEA Place, uses AR to make a profound impact on the way we shop for furniture at home. The basic premise is, shoppers select an item from the catalogue, then, using the camera on their mobile device, can place digital furniture anywhere in a given room. The product is automatically sized to fit the space (which IKEA claims is 98% accurate) and can be moved or rotated within view. Amazing, right?
Where IKEA and AR generally fall short, though, is that the computer generated content is only anchored to the camera view. Using IKEA Place as an example, if I crouched behind a physical table or a chair to get a better look, the render would not 'disappear' behind the real-world object. That's where Mixed Reality comes in.
Mixed Reality
Mixed Reality (MR) removes the boundaries between real and virtual interaction via occlusion. Occlusion means the computer-generated objects can be visibly obscured by objects in the physical environment—like a virtual robot scurrying under your coffee table. 
This is where things get interesting, because "hey, isn't all of this technically 'reality' that has been 'augmented' with computer graphics?" Technically, sure. But there is a key distinction in user experience (and developmental complexity) that does not afford these terms to be interchangeable. 

Occipital, a Boulder-based spatial computing startup, is advancing the field of computer vision. Their premier product, Bridge, is a Mixed Reality headset that gives users the ability to map any given room and place computer generated objects within it.
In the Bridge introduction video, we meet a friendly virtual robot named Bridget. In the Bridget application, available on iTunes, Bridget can fetch a ball and navigate around physical objects in the room (Occlusion!). With accurate room mapping, MR offers something AR doesn't—a whole new level of real-virtual interaction.
In conclusion, Augmented Reality and Mixed Reality are not interchangeable terms. The general distinction is: all MR is AR, but not all AR is MR. AR is a composite. MR is interactive.

To recap, a quick glossary:
Extended Reality (XR) refers to all real-and-virtual environments generated by computer technology and wearables. The 'X' in XR is a variable that can stand for any letter.Virtual Reality (VR) encompasses all immersive experiences. These could be created using purely real-world content (360 Video), purely synthetic content (Computer Generated), or a hybrid of both.Augmented Reality (AR) is an overlay of computer generated content on the real world that can superficially interact with the environment in real-time. With AR, there is no occlusion between CG content and the real-world.Mixed Reality (MR) is an overlay of synthetic content that is anchored to and interacts with objects in the real world—in real time. Mixed Reality experiences exhibit occlusion, in that the computer-generated objects are visibly obscured by objects in the physical environment.


Source: VigetInspire


Acquia Engage 2017 keynote

This October, Acquia welcomed over 650 people to the fourth annual Acquia Engage conference. In my opening keynote, I talked about the evolution of Acquia's product strategy and the move from building websites to creating customer journeys. You can watch a recording of my keynote (30 minutes) or download a copy of my slides (54 MB).

I shared that a number of new technology trends have emerged, such as conversational interfaces, beacons, augmented reality, artificial intelligence and more. These trends give organizations the opportunity to re-imagine their customer experience. Existing customer experiences can be leapfrogged by taking advantage of more channels and more data (e.g. be more intelligent, be more personalized, and be more contextualized).

I gave an example of this in a blog post last week, which showed how augmented reality can improve the shopping experience and help customers make better choices. It's just one example of how these new technologies advance existing customer experiences and move our industry from website management to customer journey management.

This is actually good news for DrupalCoin Blockchain as organizations will have to create and manage even more content. This content will need to be optimized for different channels and audience segments. However, it puts more emphasis on content modeling, content workflows, media management and web service integrations.

I believe that the transition from web content management to data-driven customer journeys is full of opportunity, and it has become clear that customers and partners are excited to take advantage of these new technology trends. This year's Acquia Engage showed how our own transformation will empower organizations to take advantage of new technology and transform how they do business.
Source: Dries Buytaert www.buytaert.net


How to Be Good at Being New

Starting a new job is hard. You have to get to know new people, follow new processes, and learn new tools all within an unfamiliar environment. You have to learn what is expected of you, how expectations are communicated, and how to gauge your progress. On top of all the other stress, there is the challenge of navigating the dynamic of simply being new. I’ve noticed some people are more comfortable than others in this dynamic. I think it’s a skillset worth considering more closely.
Being good at being new isn’t the same as being a great teammate, or being great at your specific discipline.  If you’re good at being new, you can accelerate the pace at which you build trust and connect to the team. You bring positive, inspiring energy to the people around you (which we love). You solidify your reputation faster and, by extension, get to ditch that awkward feeling of being new sooner. 
Here are some ideas for the next time you’re new:
Connect through difference.As a new hire, your perspective is valuable, in part, because it’s different from everyone else’s. When we’re out of ideas for how to approach something, you might see a path forward. When we’re on autopilot, you can wake us up to opportunities for improvement.
At the same time, don’t just share, but connect through your different experiences and perspectives. Whatever new idea, opinion, or approach you’re offering, try to frame it as something that could ultimately strengthen the team. While sharing something like, “that’s not how I’ve done it,” look for ways to add the sentiment, “I want to understand why you do it your way,” or “I wonder which way makes sense in this situation.”
Even feelings of inexperience can be chances to build connections and a stronger team. Seek out the person who joined most recently before you and catalog your shared lessons learned. Ask peers who have a particular expertise that you lack to tell you how they got to that level of expertise. Or document questions you have (and answers you find), so that the next new person can benefit. 
Find ways to show that the differences in your background can be understood and used to the advantage of the whole team.
Think tiny.You want to prove you’re awesome, and that you’re capable of doing big things. We want that too. But in the early days, the best place to show that you’re awesome might be in tiny ways. RSVP to meeting invites swiftly. Arrive to meetings early. Ask a question when presenters invite questions. Take notes. Push your chair in as you leave. Put your glass in the dishwasher. Follow up with someone the next day, asking more about some nugget of insight they said in the meeting. Reference that nugget days later to someone else entirely.
All these tiny behaviors are manifestations of someone being thoughtful, conscientious, self-aware, invested, curious, optimistic. A single one of these actions doesn’t mean much, but when seen in combination, they add up.
We wouldn’t have hired you if we didn’t think you were capable of doing big things. But when you’re new, find tiny ways to reflect your potential. The big stuff will come with time.
Give us something to talk about.People talk about you when you’re new. On the People Team, we’re focused on doing everything we can to make your onboarding experience positive. “How is she doing?” we ask each other. “Does she seem happy?” Your manager and teammates are also eager to see you engaged. “Do you think she’s feeling challenged?” they might ask. Be proactive about building your reputation. Do your part to influence these conversations.
What do you want your reputation to be? Maybe you want us to say, “I think she’s finding the onboarding sessions engaging.” Come to those sessions prepared to engage. Ask questions or share observations; show you’re listening and interested. 
Maybe you want us to say, “She seems focused on ramping up as quickly as possible.” Be deliberate about getting exposure to people, processes, tools, and knowledge. You could start a training log for yourself and share it with others. Or you could ask for project retrospective documents or team meeting recap notes, so you can catch up on recent lessons learned.
If you simply show up, do as you’re asked, and don’t say much, I predict we’ll be saying, “I’m not sure how she’s doing, it’s hard to tell,” which isn’t bad, but could be so much better.
Err on the side of caring too much.When you’re new, you might notice that your coworkers are impressive, talented, and smart … but they may also be quite laid-back. It might seem like they’re doing awesome work without trying all that hard. You may not have immediate opportunities to show how smart and capable you are, so you may be tempted to show that you fit in by asserting your casualness.
Resist the temptation. Instead, show that you care very much about doing great work, even if it means revealing some stress. Acknowledging your drive to perform well isn’t a contradiction to our casual culture. I believe most successful professionals are much more protective of their commitment to high quality work than they care about the appearance of being nonchalant. The new person who admits feeling nervous about a presentation makes a stronger impression than one who acts aloof and above it.
As Viget alum Anna Lewis wrote, “Our casual environment is effective only because, at our core, we maintain high standards of professionalism in our interactions with each other and in our work.” When you’re new, you may need to demonstrate those high standards for professionalism overtly until we have a chance to seem them evident in your daily work. Don’t worry; nobody will ding you for caring too much. 
Be curious.This is the most important one: be curious. Study up on case studies; dig back into old threads; read old proposals (ones we won as well as ones we didn’t); sit in on all kinds of meetings. Don’t just be curious about the things directly related to you or your role, be curious about the whole company. Don’t just focus on right now, try to gain perspective on our past and our future. Ask questions. Follow up on things you hear or notice that don’t make sense. Ask for clarification on why things are the way they are. 
When we welcome a new person to the team, we’re hoping he or she will make a mark and, over time, influence the company. It takes time to have that kind of impact, of course, and it’s wise to get solid footing before you start rocking the boat. By being curious you’re telling us that you’re seeking that footing. Time will tell whether your curiosity leads you to being a champion of upending the status quo, or a champion of fine-tuning existing processes. Either way, we’re encouraged by your desire to know our work and all the thought behind it. A new person’s lack of curiosity is easily mistaken as indifference, apathy, or even arrogance.

Being new is hard no matter how good you might be at the dynamic. By acknowledging the circumstances, being self-aware, and attempting a deliberate approach to being new, I expect you’ll do great. I really do! 
We are excited to get to know you and to see how your contributions will make us stronger.


Source: VigetInspire


Web Developer - George Washington University - Foggy Bottom, MD

DrupalCoin Blockchain 7 & DrupalCoin Blockchain 8 integrationexperience. Manage and extend existing custom DrupalCoin Blockchain modules on multiple DrupalCoin Blockchain (7 & 8) installs....
From George Washington University - Tue, 31 Oct 2017 14:07:06 GMT - View all Foggy Bottom, MD jobs
Source: http://rss.indeed.com/rss?q=DrupalCoin Blockchain+Developer


The Language of Work

The language that you choose to use will always depend and change depending on who you’re working with.
In other words, the lingua franca is transformed by the people, for the people.

In the context of work there is a clear difference between a bunch of relative strangers trying to get things done and a bunch of friends.
The former requires formalized methodologies and workflows to get things done, which isn’t necessarily bad but can feel mechanical and cold.
The latter doesn’t require as rigid workflows and methodologies to get things done; things happen because of the underlying bound of mutual understanding and trust and the language allows for the free passage of ideas, criticism, admonishment, and encouragement.
This is what high-performance teams are made out of and it’s yet another truly fundamental competitive advantage for the younger but more-agile software startups (besides velocity).
You see, terms and methodologies like scrum and waterfall are a common language for strangers. It is not necessary for friends.
The post The Language of Work appeared first on John Saddington.
Source: https://john.do/


Business Consultant position is open

Source: https://jobs.drupal.org/all-jobs/feed


How to influence culture

“How do you actually influence company culture?”This is the big, hairy question I often get asked by CEOs, managers, and employees alike. The other week, I posed it at the end of my most recent piece, The Culture Cliché.In that piece, I shared how the fundamental, core element of culture are our basic underlying assumptions. That is, the things we actually believe — but might not always say or outwardly show — are what determines our company culture.As a result, the key to influencing culture is tapping into those basic underlying assumptions: Listening to them, responding to them, and acting according to them.But what does that tangibly mean? Can you affect or influence others’ basic underlying assumptions, to begin with?Yes and no.Yes, basic underlying assumptions can be affected.But no, they can’t be outright changed. You can’t manipulate someone else’s basic underlying assumptions. Employees are not malleable objects for leaders to shape. Each person has her own intrinsic, worthwhile desires and beliefs — and that’s not for you as a leader to try to mess with.Instead, what you can do is to focus on creating an environment where employees can choose to shift their basic underlying assumptions in line with what feels right to them.Let’s talk about what this practically looks like.There are three parts to creating an environment where the kind of basic underlying assumptions you want — and ultimately, the kind of company culture you want — can come into fruition:Personal accountabilityYou must model the behaviors and basic underlying assumptions you want to be true. One of the greatest shortcomings of a leader is wanting others to do something when she doesn’t practice it herself. For instance, a manager often doesn’t admit her own weaknesses, but expects employees to be upfront and forthcoming about mistakes that happen in the company. See the disconnect? Whatever basic underlying assumptions you desire to be deeply rooted in your company, you must exhibit those basic underlying assumptions yourself, first.As the leader, you should be the living embodiment of the basic underlying assumptions you want your culture to have. You should be consciously and intentionally speaking and acting in a way that shows people, “This is important to me.”ConsistencyYour desired basic underlying assumptions won’t be made true unless they are consistent. Basic underlying assumptions are solidified when you act consistently upon them.Say you want honesty and transparency to be a basic underlying assumptions within your company’s culture. You decide to hold an all-hands meeting every month where you cover high-level financials, company goals, accomplishments, and answer employees’ questions. Seems like it’d be an effective initiative to establish that honesty and transparency are a basic underlying assumptions within the company.But here’s the missing piece: Is it consistent? Do you hold those all-hands meetings every month, regularly? Or has the frequency of those meetings tapered off so now it’s every 2 or 3 months…or just whenever you remember to have them.If it’s the latter, you’re falling short of creating an environment where those basic underlying assumptions can be strengthened.Consistency determines whether or not this tenet of your culture is fleeting, or here to stay.RichnessBeing consistent alone in how you demonstrate your basic underlying assumptions isn’t enough. You must think about the richness of how you’re communicating these basic underlying assumptions. Are you using a variety of channels, means, and mediums to show that this is something that’s important for the team to embrace and embody? Or are you just relying on one?For example, it’s not enough to just hold the all-hands meetings and assume that’s the only way to foster honesty and transparency within the company. Consider that some people might not feel comfortable speaking up in front of the rest of the company… so you’ll want to make sure you’re also doing one-on-ones. But don’t also merely assume that a one-on-one is enough of an opportunity for someone to speak their mind. Perhaps that person doesn’t always feel comfortable directly addressing that particular manager. Make sure you provide other opportunities for your employees to speak up, be it at group lunches, coffees get-togethers, CEO office hours, or an all-company survey.The richness of how you communicate, the varying formats and mediums, helps dictate if the basic underlying assumption is truly what it should be: foundational.Now this is no grand formula by any means for creating the culture that you want. Shaping a company’s culture and tapping into a team’s basic underlying assumptions is more art than it is science.But consider these three elements — personal accountability, consistency, and richness — in how you’re upholding the basic underlying assumptions you want to make more real.Pick one, commit to it, and see progress build over time. Slowly, but surely, you’ll see the difference.I wrote this piece as the latest chapter in our Knowledge Center. Each week, we release a new chapter on how to create an open, honest company culture. To get each chapter sent straight to your inbox, sign up below…https://medium.com/media/d44dd2a6a03c83b35a6dd9495abb813b/hrefP.S.: Please feel free to share + give this piece 👏 so others can find it too. Thanks 😊 (And you can always say hi at @cjlew23.)How to influence culture was originally published in Signal v. Noise on Medium, where people are continuing the conversation by highlighting and responding to this story.


Source: 37signals


My Life Now

My (professional) life in just a few pics:

I’m working with my brother on a few things and we pair program while video chatting and the new kiddo in my arms. I’m kind of experienced at this at this point in my life…

Life is pretty good. The evenings are super-tough and nothing can mentally prepare you for that difficult season.
But, my wife is a true hero. I told her the other day that women get the short-end-of-the-stick when it comes to early parenting. They have to move from carrying a human being in their belly for 9 months and then the painful process of breast-feeding and little sleep.
It’s such bullshit, to be honest. Men, on the other hand, just get a lot of the early upside…

So, I’m going to be a bit out of commission for a while:

The post My Life Now appeared first on John Saddington.
Source: https://john.do/


The Ultimate Guide to Facebook Remarketing

Facebook remarketing is often seen as a “love it or hate it” tool. The thing is, you need Facebook remarketing. Only 2% of first-time site visits are going to convert on your website. That means that 98% of your traffic won’t buy from you.   Even if they add your products to their cart, nearly 70% Read more
Source: https://adespresso.com/feed/


Make Like it Matters

(This is a sponsored post.)Our sponsor Media temple is holding a contest to give away a bunch of stuff, including a nice big monitor and gift cards. Entering is easy, you just drop them an image or URL to a project you're proud of. Do it quickly though, as entries end on Tuesday. Then the top 20 will be publicly voted on. US residents only.
Direct Link to Article — Permalink
Make Like it Matters is a post from CSS-Tricks
Source: CssTricks


Emulating CSS Timing Functions with JavaScript

CSS animations and transitions are great! However, while recently toying with an idea, I got really frustrated with the fact that gradients are only animatable in Edge (and IE 10+). Yes, we can do all sorts of tricks with background-position, background-size, background-blend-mode or even opacity and transform on a pseudo-element/ child, but sometimes these are just not enough. Not to mention that we run into similar problems when wanting to animate SVG attributes without a CSS correspondent.
Using a lot of examples, this article is going to explain how to smoothly go from one state to another in a similar fashion to that of common CSS timing functions using just a little bit of JavaScript, without having to rely on a library, so without including a lot of complicated and unnecessary code that may become a big burden in the future.

This is not how the CSS timing functions work. This is an approach that I find simpler and more intuitive than working with Bézier curves. I'm going to show how to experiment with different timing functions using JavaScript and dissect use cases. It is not a tutorial on how to do beautiful animation.
A few examples using a linear timing function
Let's start with a left to right linear-gradient() with a sharp transition where we want to animate the first stop. Here's a way to express that using CSS custom properties:
background: linear-gradient(90deg, #ff9800 var(--stop, 0%), #3c3c3c 0);
On click, we want the value of this stop to go from 0% to 100% (or the other way around, depending on the state it's already in) over the course of NF frames. If an animation is already running at the time of the click, we stop it, change its direction, then restart it.
We also need a few variables such as the request ID (this gets returned by requestAnimationFrame), the index of the current frame (an integer in the [0, NF] interval, starting at 0) and the direction our transition is going in (which is 1 when going towards 100% and -1 when going towards 0%).
While nothing is changing, the request ID is null. We also set the current frame index to 0 initially and the direction to -1, as if we've just arrived to 0% from 100%.
const NF = 80; // number of frames transition happens over

let rID = null, f = 0, dir = -1;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {};

addEventListener('click', e => {
if(rID) stopAni(); // if an animation is already running, stop it
dir *= -1; // change animation direction
update();
}, false);
Now all that's left is to populate the update() function. Within it, we update the current frame index f. Then we compute a progress variable k as the ratio between this current frame index f and the total number of frames NF. Given that f goes from 0 to NF (included), this means that our progress k goes from 0 to 1. Multiply this with 100% and we get the desired stop.
After this, we check whether we've reached one of the end states. If we have, we stop the animation and exit the update() function.
function update() {
f += dir; // update current frame index

let k = f/NF; // compute progress

document.body.style.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
The result can be seen in the Pen below (note that we go back on a second click):
See the Pen by thebabydino (@thebabydino) on CodePen.
The way the pseudo-element is made to contrast with the background below is explained in an older article.
The above demo may look like something we could easily achieve with an element and translating a pseudo-element that can fully cover it, but things get a lot more interesting if we give the background-size a value that's smaller than 100% along the x axis, let's say 5em:
See the Pen by thebabydino (@thebabydino) on CodePen.
This gives us a sort of a "vertical blinds" effect that cannot be replicated in a clean manner with just CSS if we don't want to use more than one element.
Another option would be not to alternate the direction and always sweep from left to right, except only odd sweeps would be orange. This requires tweaking the CSS a bit:
--c0: #ff9800;
--c1: #3c3c3c;
background: linear-gradient(90deg,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
In the JavaScript, we ditch the direction variable and add a type one (typ) that switches between 0 and 1 at the end of every transition. That's when we also update all custom properties:
const S = document.body.style;

let typ = 0;

function update() {
let k = ++f/NF;

S.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
f = 0;
S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);
S.setProperty('--stop', `0%`);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
This gives us the desired result (click at least twice to see how the effect differs from that in the first demo):
See the Pen by thebabydino (@thebabydino) on CodePen.
We could also change the gradient angle instead of the stop. In this case, the background rule becomes:
background: linear-gradient(var(--angle, 0deg),
#ff9800 50%, #3c3c3c 0);
In the JavaScript code, we tweak the update() function:
function update() {
f += dir;

let k = f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
We now have a gradient angle transition in between the two states (0deg and 180deg):
See the Pen by thebabydino (@thebabydino) on CodePen.
In this case, we might also want to keep going clockwise to get back to the 0deg state instead of changing the direction. So we just ditch the dir variable altogether, discard any clicks happening during the transition, and always increment the frame index f, resetting it to 0 when we've completed a full rotation around the circle:
function update() {
let k = ++f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
f = f%(2*NF);
stopAni();
return
}

rID = requestAnimationFrame(update)
};

addEventListener('click', e => {
if(!rID) update()
}, false);
The following Pen illustrates the result - our rotation is now always clockwise:
See the Pen by thebabydino (@thebabydino) on CodePen.
Something else we could do is use a radial-gradient() and animate the radial stop:
background: radial-gradient(circle,
#ff9800 var(--stop, 0%), #3c3c3c 0);
The JavaScript code is identical to that of the first demo and the result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
We may also not want to go back when clicking again, but instead make another blob grow and cover the entire viewport. In this case, we add a few more custom properties to the CSS:
--c0: #ff9800;
--c1: #3c3c3c;
background: radial-gradient(circle,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
The JavaScript is the same as in the case of the third linear-gradient() demo. This gives us the result we want:
See the Pen by thebabydino (@thebabydino) on CodePen.
A fun tweak to this would be to make our circle start growing from the point we clicked. To do so, we introduce two more custom properties, --x and --y:
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
When clicking, we set these to the coordinates of the point where the click happened:
addEventListener('click', e => {
if(!rID) {
S.setProperty('--x', `${e.clientX}px`);
S.setProperty('--y', `${e.clientY}px`);
update();
}
}, false);
This gives us the following result where we have a disc growing from the point where we clicked:
See the Pen by thebabydino (@thebabydino) on CodePen.
Another option would be using a conic-gradient() and animating the angular stop:
background: conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0%)
Note that in the case of conic-gradient(), we must use a unit for the zero value (whether that unit is % or an angular one like deg doesn't matter), otherwise our code won't work - writing conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0) means nothing gets displayed.
The JavaScript is the same as for animating the stop in the linear or radial case, but bear in mind that this currently only works in Chrome with Experimental Web Platform Features enabled in chrome://flags.
The Experimental Web Platform Features flag enabled in Chrome Canary (63.0.3210.0).
Just for the purpose of displaying conic gradients in the browser, there's a polyfill by Lea Verou and this works cross-browser but doesn't allow using CSS custom properties.
The recording below illustrates how our code works:
Recording of how our first conic-gradient() demo works in Chrome with the flag enabled (live demo).
This is another situation where we might not want to go back on a second click. This means we need to alter the CSS a bit, in the same way we did for the last radial-gradient() demo:
--c0: #ff9800;
--c1: #3c3c3c;
background: conic-gradient(
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0%)
The JavaScript code is exactly the same as in the corresponding linear-gradient() or radial-gradient() case and the result can be seen below:
Recording of how our second conic-gradient() demo works in Chrome with the flag enabled (live demo).
Before we move on to other timing functions, there's one more thing to cover: the case when we don't go from 0% to 100%, but in between any two values. We take the example of our first linear-gradient, but with a different default for --stop, let's say 85% and we also set a --stop-fin value - this is going to be the final value for --stop:
--stop-ini: 85%;
--stop-fin: 26%;
background: linear-gradient(90deg, #ff9800 var(--stop, var(--stop-ini)), #3c3c3c 0)
In the JavaScript, we read these two values - the initial (default) and the final one - and we compute a range as the difference between them:
const S = getComputedStyle(document.body),
INI = +S.getPropertyValue('--stop-ini').replace('%', ''),
FIN = +S.getPropertyValue('--stop-fin').replace('%', ''),
RANGE = FIN - INI;
Finally, in the update() function, we take into account the initial value and the range when setting the current value for --stop:
document.body.style.setProperty(
'--stop',
`${+(INI + k*RANGE).toFixed(2)}%`
);
With these changes we now have a transition in between 85% and 26% (and the other way on even clicks):
See the Pen by thebabydino (@thebabydino) on CodePen.
If we want to mix units for the stop value, things get hairier as we need to compute more things (box dimensions when mixing % and px, font sizes if we throw em or rem in the mix, viewport dimensions if we want to use viewport units, the length of the 0% to 100% segment on the gradient line for gradients that are not horizontal or vertical), but the basic idea remains the same.
Emulating ease-in/ ease-out
An ease-in kind of function means the change in value happens slow at first and then accelerates. ease-out is exactly the opposite - the change happens fast in the beginning, but then slows down towards the end.
The ease-in (left) and ease-out (right) timing functions (live).
The slope of the curves above gives us the rate of change. The steeper it is, the faster the change in value happens.
We can emulate these functions by tweaking the linear method described in the first section. Since k takes values in the [0, 1] interval, raising it to any positive power also gives us a number within the same interval. The interactive demo below shows the graph of a function f(k) = pow(k, p) (k raised to an exponent p) shown in purple and that of a function g(k) = 1 - pow(1 - k, p) shown in red on the [0, 1] interval versus the identity function id(k) = k (which corresponds to a linear timing function).
See the Pen by thebabydino (@thebabydino) on CodePen.
When the exponent p is equal to 1, the graphs of the f and g functions are identical to that of the identity function.
When exponent p is greater than 1, the graph of the f function is below the identity line - the rate of change increases as k increases. This is like an ease-in type of function. The graph of the g function is above the identity line - the rate of change decreases as k increases. This is like an ease-out type of function.
It seems an exponent p of about 2 gives us an f that's pretty similar to ease-in, while g is pretty similar to ease-out. With a bit more tweaking, it looks like the best approximation is for a p value of about 1.675:
See the Pen by thebabydino (@thebabydino) on CodePen.
In this interactive demo, we want the graphs of the f and g functions to be as close as possible to the dashed lines, which represent the ease-in timing function (below the identity line) and the ease-out timing function (above the identity line).
Emulating ease-in-out
The CSS ease-in-out timing function looks like in the illustration below:
The ease-in-out timing function (live).
So how can we get something like this?
Well, that's what harmonic functions are for! More exactly, the ease-in-out out shape is reminiscent the shape of the sin() function on the [-90°,90°] interval.
The sin(k) function on the [-90°,90°] interval (live).
However, we don't want a function whose input is in the [-90°,90°] interval and output is in the [-1,1] interval, so let's fix this!
This means we need to squish the hashed rectangle ([-90°,90°]x[-1,1]) in the illustration above into the unit one ([0,1]x[0,1]).
First, let's take the domain [-90°,90°]. If we change our function to be sin(k·180°) (or sin(k·π) in radians), then our domain becomes [-.5,.5] (we can check that -.5·180° = 90° and .5·180° = 90°):
The sin(k·π) function on the [-.5,.5] interval (live).
We can shift this domain to the right by .5 and get the desired [0,1] interval if we change our function to be sin((k - .5)·π) (we can check that 0 - .5 = -.5 and 1 - .5 = .5):
The sin((k - .5)·π) function on the [0,1] interval (live).
Now let's get the desired codomain. If we add 1 to our function making it sin((k - .5)·π) + 1 this shifts out codomain up into the [0, 2] interval:
The sin((k - .5)·π) + 1 function on the [0,1] interval (live).
Dividing everything by 2 gives us the (sin((k - .5)·π) + 1)/2 function and compacts the codomain into our desired [0,1] interval:
The (sin((k - .5)·π) + 1)/2 function on the [0,1] interval (live).
This turns out to be a good approximation of the ease-in-out timing function (represented with an orange dashed line in the illustration above).
Comparison of all these timing functions
Let's say we want to have a bunch of elements with a linear-gradient() (like in the third demo). On click, their --stop values go from 0% to 100%, but with a different timing function for each.
In the JavaScript, we create a timing functions object with the corresponding function for each type of easing:
tfn = {
'linear': function(k) {
return k;
},
'ease-in': function(k) {
return Math.pow(k, 1.675);
},
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675);
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1);
}
};
For each of these, we create an article element:
const _ART = [];

let frag = document.createDocumentFragment();

for(let p in tfn) {
let art = document.createElement('article'),
hd = document.createElement('h3');

hd.textContent = p;
art.appendChild(hd);
art.setAttribute('id', p);
_ART.push(art);
frag.appendChild(art);
}

n = _ART.length;
document.body.appendChild(frag);
The update function is pretty much the same, except we set the --stop custom property for every element as the value returned by the corresponding timing function when fed the current progress k. Also, when resetting the --stop to 0% at the end of the animation, we also need to do this for every element.
function update() {
let k = ++f/NF;

for(let i = 0; i < n; i++) {
_ART[i].style.setProperty(
'--stop',
`${+tfn[_ART[i].id](k).toFixed(5)*100}%`
);
}

if(!(f%NF)) {
f = 0;

S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);

for(let i = 0; i < n; i++)
_ART[i].style.setProperty('--stop', `0%`);

stopAni();
return;
}

rID = requestAnimationFrame(update)
};
This gives us a nice visual comparison of these timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
They all start and finish at the same time, but while the progress is constant for the linear one, the ease-in one starts slowly and then accelerates, the ease-out one starts fast and then slows down and, finally, the ease-in-out one starts slowly, accelerates and then slows down again at the end.
Timing functions for bouncing transitions
I first came across the concept years ago, in Lea Verou's CSS Secrets talk. These happen when the y (even) values in a cubic-bezier() function are outside the [0, 1] range and the effect they create is of the animated value going outside the interval between its initial and final value.
This bounce can happen right after the transition starts, right before it finishes or at both ends.
A bounce at the start means that, at first, we don't go towards the final state, but in the opposite direction. For example, if want to animate a stop from 43% to 57% and we have a bounce at the start, then, at first, out stop value doesn't increase towards 57%, but decreases below 43% before going back up to the final state. Similarly, if we go from an initial stop value of 57% to a final stop value of 43% and we have a bounce at the start, then, at first, the stop value increases above 57% before going down to the final value.
A bounce at the end means we overshoot our final state and only then go back to it. If want to animate a stop from 43% to 57% and we have a bounce at the end, then we start going normally from the initial state to the final one, but towards the end, we go above 57% before going back down to it. And if we go from an inital stop value of 57% to a final stop value of 43% and we have a bounce at the end, then, at first, we go down towards the final state, but, towards the end, we pass it and we briefly have stop values below 43% before our transition finishes there.
If what they do is still difficult to grasp, below there's a comparative example of all three of them in action.
The three cases.
These kinds of timing functions don't have their own keywords associated, but they look cool and they are what we want in a lot of situations.
Just like in the case of ease-in-out, the quickest way of getting them is by using harmonic functions. The difference lies in the fact that now we don't start from the [-90°,90°] domain anymore.
For a bounce at the beginning, we start with the [s, 0°] portion of the sin() function, where s (the start angle) is in the (-180°,-90°) interval. The closer it is to -180°, the bigger the bounce is and the faster it will go to the final state after it. So we don't want it to be really close to -180° because the result would look too unnatural. We also want it to be far enough from -90° that the bounce is noticeable.
In the interactive demo below, you can drag the slider to change the start angle and then click on the stripe at the bottom to see the effect in action:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,0]x[sin(s),0]) is the area we need move and scale into the [0,1]x[0,1] area in order to get our timing function. The part of the curve that's below its lower edge is where the bounce happens. You can adjust the start angle using the slider and then click on the bottom bar to see how the transition looks for different start angles.
Just like in the ease-in-out case, we first squish the domain into the [-1,0] interval by dividing the argument with the range (which is the maximum 0 minus the minimum s). Therefore, our function becomes sin(-k·s) (we can check that -(-1)·s = s and -0·s = 0):
The sin(-k·s) function on the [-1,0] interval (live).
Next, we shift this interval to the right (by 1, into [0,1]). This makes our function sin(-(k - 1)·s) = sin((1 - k)·s) (it checks that 0 - 1 = -1 and 1 - 1 = 0):
The sin(-(k - 1)·s) function on the [0,1] interval (live).
We then shift the codomain up by its value at 0 (sin((1 - 0)*s) = sin(s)). Our function is now sin((1 - k)·s) - sin(s) and our codomain [0,-sin(s)].
The sin(-(k - 1)·s) - sin(s) function on the [0,1] interval (live).
The last step is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is -sin(s)). This means our final easing function is 1 - sin((1 - k)·s)/sin(s)
The 1 - sin((1 - k)·s)/sin(s) function on the [0,1] interval (live).
For a bounce at the end, we start with the [0°, e] portion of the sin() function, where e (the end angle) is in the (90°,180°) interval. The closer it is to 180°, the bigger the bounce is and the faster it will move from the initial state to the final one before it overshoots it and the bounce happens. So we don't want it to be really close to 180° as the result would look too unnatural. We also want it to be far enough from 90° so that the bounce is noticeable.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([0,e]x[0,sin(e)]) is the area we need to squish and move into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's below its upper edge is where the bounce happens.
We start by squishing the domain into the [0,1] interval by dividing the argument with the range (which is the maximum e minus the minimum 0). Therefore, our function becomes sin(k·e) (we can check that 0·e = 0 and 1·e = e):
The sin(k·e) function on the [0,1] interval (live).
What's still left to do is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is sin(e)). This means our final easing function is sin(k·e)/sin(e).
The sin(k·e)/sin(e) function on the [0,1] interval (live).
If we want a bounce at each end, we start with the [s, e] portion of the sin() function, where s is in the (-180°,-90°) interval and e in the (90°,180°) interval. The larger s and e are in absolute values, the bigger the corresponding bounces are and the more of the total transition time is spent on them alone. On the other hand, the closer their absolute values get to 90°, the less noticeable their corresponding bounces are. So, just like in the previous two cases, it's all about finding the right balance.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,e]x[sin(s),sin(e)]) is the area we need to move and scale into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's beyond its horizontal edges is where the bounces happen.
We start by shifting the domain to the right into the [0,e - s] interval. This means our function becomes sin(k + s) (we can check that 0 + s = s and that e - s + s = e).
The sin(k + s) function on the [0,e - s] interval (live).
Then we shrink the domain to fit into the [0,1] interval, which gives us the function sin(k·(e - s) + s).
The sin(k·(e - s) + s) function on the [0,1] interval (live).
Moving on to the codomain, we first shift it up by its value at 0 (sin(0·(e - s) + s)), which means we now have sin(k·(e - s) + s) - sin(s). This gives us the new codomain [0,sin(e) - sin(s)].
The sin(k·(e - s) + s) - sin(s) function on the [0,1] interval (live).
Finally, we shrink the codomain to the [0,1] interval by dividing with the range (sin(e) - sin(s)), so our final function is (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)).
The (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)) function on the [0,1] interval (live).
So in order to do a similar comparative demo to that for the JS equivalents of the CSS linear, ease-in, ease-out, ease-in-out, our timing functions object becomes:
tfn = {
'bounce-ini': function(k) {
return 1 - Math.sin((1 - k)*s)/Math.sin(s);
},
'bounce-fin': function(k) {
return Math.sin(k*e)/Math.sin(e);
},
'bounce-ini-fin': function(k) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s));
}
};
The s and e variables are the values we get from the two range inputs that allow us to control the bounce amount.
The interactive demo below shows the visual comparison of these three types of timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alternating animations
In CSS, setting animation-direction to alternate also reverses the timing function. In order to better understand this, consider a .box element on which we animate its transform property such that we move to the right. This means our @keyframes look as follows:
@keyframes shift {
0%, 10% { transform: none }
90%, 100% { transform: translate(50vw) }
}
We use a custom timing function that allows us to have a bounce at the end and we make this animation alternate - that is, go from the final state (translate(50vw)) back to the initial state (no translation) for the even-numbered iterations (second, fourth and so on).
animation: shift 1s cubic-bezier(.5, 1, .75, 1.5) infinite alternate
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
One important thing to notice here is that, for the even-numbered iterations, our bounce doesn't happen at the end, but at the start - the timing function is reversed. Visually, this means it's reflected both horizontally and vertically with respect to the .5,.5 point.
The normal timing function (f, in red, with a bounce at the end) and the symmetrical reverse one (g, in purple, with a bounce at the start) (live)
In CSS, there is no way of having a different timing function other than the symmetrical one on going back if we are to use this set of keyframes and animation-direction: alternate. We can introduce the going back part into the keyframes and control the timing function for each stage of the animation, but that's outside the scope of this article.
When changing values with JavaScript in the fashion presented so far in this article, the same thing happens by default. Consider the case when we want to animate the stop of a linear-gradient() between an initial and a final position and we want to have a bounce at the end. This is pretty much the last example presented in the first section with timing function that lets us have a bounce at the end (one in the bounce-fin category described before) instead of a linear one.
The CSS is exactly the same and we only make a few minor changes to the JavaScript code. We set a limit angle E and we use a custom bounce-fin kind of timing function in place of the linear one:
const E = .75*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
};

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + timing(k)*RANGE).toFixed(2)}%`
);

/* same as before */
};

/* same as before */
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the initial state, the stop is at 85%. We animate it to 26% (which is the final state) using a timing function that gives us a bounce at the end. This means we go beyond our final stop position at 26% before going back up and stopping there. This is what happens during the odd iterations.
During the even iterations, this behaves just like in the CSS case, reversing the timing function, so that the bounce happens at the beginning, not at the end.
But what if we don't want the timing function to be reversed?
In this case, we need to use the symmetrical function. For any timing function f(k) defined on the [0,1] interval (this is the domain), whose values are in the [0,1] (codomain), the symmetrical function we want is 1 - f(1 - k). Note that functions whose shape is actually symmetrical with respect to the .5,.5 point, like linear or ease-in-out are identical to their symmetrical functions.
See the Pen by thebabydino (@thebabydino) on CodePen.
So what we do is use our timing function f(k) for the odd iterations and use 1 - f(1 - k) for the even ones. We can tell whether an iteration is odd or even from the direction (dir) variable. This is 1 for odd iterations and -1 for even ones.
This means we can combine our two timing functions into one: m + dir*f(m + dir*k).
Here, the multiplier m is 0 for the odd iterations (when dir is 1) and 1 for the even ones (when dir is -1), so we can compute it as .5*(1 - dir):
dir = +1 → m = .5*(1 - (+1)) = .5*(1 - 1) = .5*0 = 0
dir = -1 → m = .5*(1 - (-1)) = .5*(1 + 1) = .5*2 = 1
This way, our JavaScript becomes:
let m;

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + (m + dir*timing(m + dir*k))*RANGE).toFixed(2)}%`
);

/* same as before */
};

addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
The final result can be seen in this Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Even more examples
Gradient stops are not the only things that aren't animatable cross-browser with just CSS.
Gradient end going from orange to violet
For a first example of something different, let's say we want the orange in our gradient to animate to a kind of violet. We start with a CSS that looks something like this:
--c-ini: #ff9800;
--c-fin: #a048b9;
background: linear-gradient(90deg,
var(--c, var(--c-ini)), #3c3c3c)
In order to interpolate between the initial and final values, we need to know the format we get when reading them via JavaScript - is it going to be the same format we set them in? Is it going to be always rgb()/ rgba()?
Here is where things get a bit hairy. Consider the following test, where we have a gradient where we've used every format possible:
--c0: hsl(150, 100%, 50%); // springgreen
--c1: orange;
--c2: #8a2be2; // blueviolet
--c3: rgb(220, 20, 60); // crimson
--c4: rgba(255, 245, 238, 1); // seashell with alpha = 1
--c5: hsla(51, 100%, 50%, .5); // gold with alpha = .5
background: linear-gradient(90deg,
var(--c0), var(--c1),
var(--c2), var(--c3),
var(--c4), var(--c5))
We read the computed values of the gradient image and the individual custom properties --c0 through --c5 via JavaScript.
let s = getComputedStyle(document.body);

console.log(s.backgroundImage);
console.log(s.getPropertyValue('--c0'), 'springgreen');
console.log(s.getPropertyValue('--c1'), 'orange');
console.log(s.getPropertyValue('--c2'), 'blueviolet');
console.log(s.getPropertyValue('--c3'), 'crimson');
console.log(s.getPropertyValue('--c4'), 'seashell (alpha = 1)');
console.log(s.getPropertyValue('--c5'), 'gold (alpha = .5)');
The results seem a bit inconsistent.
Screenshots showing what gets logged in Chrome, Edge and Firefox (live).
Whatever we do, if we have an alpha of strictly less than 1, what we get via JavaScript seems to be always an rgba() value, regardless of whether we've set it with rgba() or hsla().
All browsers also agree when reading the custom properties directly, though, this time, what we get doesn't seem to make much sense: orange, crimson and seashell are returned as keywords regardless of how they were set, but we get hex values for springgreen and blueviolet. Except for orange, which was added in Level 2, all these values were added to CSS in Level 3, so why do we get some as keywords and others as hex values?
For the background-image, Firefox always returns the fully opaque values only as rgb(), while Chrome and Edge return them as either keywords or hex values, just like they do in the case when we read the custom properties directly.
Oh well, at least that lets us know we need to take into account different formats.
So the first thing we need to do is map the keywords to rgb() values. Not going to write all that manually, so a quick search finds this repo - perfect, it's exactly what we want! We can now set that as the value of a CMAP constant.
The next step here is to create a getRGBA(c) function that would take a string representing a keyword, a hex or an rgb()/ rgba() value and return an array containing the RGBA values ([red, green, blue, alpha]).
We start by building our regular expressions for the hex and rgb()/ rgba() values. These are a bit loose and would catch quite a few false positives if we were to have user input, but since we're only using them on CSS computed style values, we can afford to take the quick and dirty path here:
let re_hex = /^#([a-fd]{1,2})([a-fd]{1,2})([a-fd]{1,2})$/i,
re_rgb = /^rgba?((d{1,3},s){2}d{1,3}(,s((0|1)?.?d*))?)/;
Then we handle the three types of values we've seen we might get by reading the computed styles:
if(c in CMAP) return CMAP[c]; // keyword lookup, return rgb

if([4, 7].indexOf(c.length) !== -1 && re_hex.test(c)) {
c = c.match(re_hex).slice(1); // remove the '#'
if(c[0].length === 1) c = c.map(x => x + x);
// go from 3-digit form to 6-digit one
c.push(1); // add an alpha of 1

// return decimal valued RGBA array
return c.map(x => parseInt(x, 16))
}

if(re_rgb.test(c)) {
// extract values
c = c.replace(/rgba?(/, '').replace(')', '').split(',').map(x => +x.trim());
if(c.length === 3) c.push(1); // if no alpha specified, use 1

return c // return RGBA array
}
Now after adding the keyword to RGBA map (CMAP) and the getRGBA() function, our JavaScript code doesn't change much from the previous examples:
const INI = getRGBA(S.getPropertyValue('--c-ini').trim()),
FIN = getRGBA(S.getPropertyValue('--c-fin').trim()),
RANGE = [],
ALPHA = 1 - INI[3] || 1 - FIN[3];

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + k*RANGE[i])).join(',')})`
);

/* same as before */
};

(function init() {
if(!ALPHA) INI.pop(); // get rid of alpha if always 1
RANGE.splice(0, 0, ...INI.map((c, i) => FIN[i] - c));
})();

/* same as before */
This gives us a linear gradient animation:
See the Pen by thebabydino (@thebabydino) on CodePen.
We can also use a different, non-linear timing function, for example one that allows for a bounce at the end:
const E = .8*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
}

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + timing(k)*RANGE[i])).join(',')})`
);

/* same as before */
};

/* same as before */
This means we go all the way to a kind of blue before going back to our final violet:
See the Pen by thebabydino (@thebabydino) on CodePen.
Do note however that, in general, RGBA transitions are not the best place to illustrate bounces. That's because the RGB channels are strictly limited to the [0,255] range and the alpha channel is strictly limited to the [0,1] range. rgb(255, 0, 0) is as red as red gets, there's no redder red with a value of over 255 for the first channel. A value of 0 for the alpha channel means completely transparent, there's no greater transparency with a negative value.
By now, you're probably already bored with gradients, so let's switch to something else!
Smooth changing SVG attribute values
At this point, we cannot alter the geometry of SVG elements via CSS. We should be able to as per the SVG2 spec and Chrome does support some of this stuff, but what if we want to animate the geometry of SVG elements now, in a more cross-browser manner?
Well, you've probably guessed it, JavaScript to the rescue!
Growing a circle
Our first example is that of a circle whose radius goes from nothing (0) to a quarter of the minimum viewBox dimension. We keep the document structure simple, without any other aditional elements.
<svg viewBox='-100 -50 200 100'>
<circle/>
</svg>
For the JavaScript part, the only notable difference from the previous demos is that we read the SVG viewBox dimensions in order to get the maximum radius and we now set the r attribute within the update() function, not a CSS variable (it would be immensely useful if CSS variables were allowed as values for such attributes, but, sadly, we don't live in an ideal world):
const _G = document.querySelector('svg'),
_C = document.querySelector('circle'),
VB = _G.getAttribute('viewBox').split(' '),
RMAX = .25*Math.min(...VB.slice(2)),
E = .8*Math.PI;

/* same as before */

function update() {
/* same as before */

_C.setAttribute('r', (timing(k)*RMAX).toFixed(2));

/* same as before */
};

/* same as before */
Below, you can see the result when using a bounce-fin kind of timing function:
See the Pen by thebabydino (@thebabydino) on CodePen.
Pan and zoom map
Another SVG example is a smooth pan and zoom map demo. In this case, we take a map like those from amCharts, clean up the SVG and then create this effect by triggering a linear viewBox animation when pressing the +/ - keys (zoom) and the arrow keys (pan).
The first thing we do in the JavaScript is create a navigation map, where we take the key codes of interest and attach info about what we do when the corresponding keys are pressed (note that we need different key codes for + and - in Firefox for some reason).
const NAV_MAP = {
187: { dir: 1, act: 'zoom', name: 'in' } /* + */,
61: { dir: 1, act: 'zoom', name: 'in' } /* + Firefox ¯_(ツ)_/¯ */,
189: { dir: -1, act: 'zoom', name: 'out' } /* - */,
173: { dir: -1, act: 'zoom', name: 'out' } /* - Firefox ¯_(ツ)_/¯ */,
37: { dir: -1, act: 'move', name: 'left', axis: 0 } /* ⇦ */,
38: { dir: -1, act: 'move', name: 'up', axis: 1 } /* ⇧ */,
39: { dir: 1, act: 'move', name: 'right', axis: 0 } /* ⇨ */,
40: { dir: 1, act: 'move', name: 'down', axis: 1 } /* ⇩ */
}
When pressing the + key, what we want to do is zoom in. The action we perform is 'zoom' in the positive direction - we go 'in'. Similarly, when pressing the - key, the action is also 'zoom', but in the negative (-1) direction - we go 'out'.
When pressing the arrow left key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the negative (-1) direction - we go 'left'. When pressing the arrow up key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the negative (-1) direction - we go 'up'.
When pressing the arrow right key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the positive direction - we go 'right'. When pressing the arrow down key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the positive direction - we go 'down'.
We then get the SVG element, its initial viewBox, set the maximum zoom out level to these initial viewBox dimensions and set the smallest possible viewBox width to a much smaller value (let's say 8).
const _SVG = document.querySelector('svg'),
VB = _SVG.getAttribute('viewBox').split(' ').map(c => +c),
DMAX = VB.slice(2), WMIN = 8;
We also create an empty current navigation object to hold the current navigation action data and a target viewBox array to contain the final state we animate the viewBox to for the current animation.
let nav = {}, tg = Array(4);
On 'keyup', if we don't have any animation running already and the key that was pressed is one of interest, we get the current navigation object from the navigation map we created at the beginning. After this, we handle the two action cases ('zoom'/ 'move') and call the update() function:
addEventListener('keyup', e => {
if(!rID && e.keyCode in NAV_MAP) {
nav = NAV_MAP[e.keyCode];

if(nav.act === 'zoom') {
/* what we do if the action is 'zoom' */
}

else if(nav.act === 'move') {
/* what we do if the action is 'move' */
}

update()
}
}, false);
Now let's see what we do if we zoom. First off, and this is a very useful programming tactic in general, not just here in particular, we get the edge cases that make us exit the function out of the way.
So what are our edge cases here?
The first one is when we want to zoom out (a zoom in the negative direction) when our whole map is already in sight (the current viewBox dimensions are bigger or equal to the maximum ones). In our case, this should happen if we want to zoom out at the very beginning because we start with the whole map in sight.
The second edge case is when we hit the other limit - we want to zoom in, but we're at the maximum detail level (the current viewBox dimensions are smaller or equal to the minimum ones).
Putting the above into JavaScript code, we have:
if(nav.act === 'zoom') {
if((nav.dir === -1 && VB[2] >= DMAX[0]) ||
(nav.dir === 1 && VB[2] <= WMIN)) {
console.log(`cannot ${nav.act} ${nav.name} more`);
return
}

/* main case */
}
Now that we've handled the edge cases, let's move on to the main case. Here, we set the target viewBox values. We use a 2x zoom on each step, meaning that when we zoom in, the target viewBox dimensions are half the ones at the start of the current zoom action, and when we zoom out they're double. The target offsets are half the difference between the maximum viewBox dimensions and the target ones.
if(nav.act === 'zoom') {
/* edge cases */

for(let i = 0; i < 2; i++) {
tg[i + 2] = VB[i + 2]/Math.pow(2, nav.dir);
tg[i] = .5*(DMAX[i] - tg[i + 2]);
}
}
Next, let's see what we do if we want to move instead of zooming.
In a similar fashion, we get the edge cases that make us exit the function out of the way first. Here, these happen when we're at an edge of the map and we want to keep going in that direction (whatever the direction might be). Since originally the top left corner of our viewBox is at 0,0, this means we cannot go below 0 or above the maximum viewBox size minus the current one. Note that given we're initially fully zoomed out, this also means we cannot move in any direction until we zoom in.
else if(nav.act === 'move') {
if((nav.dir === -1 && VB[nav.axis] <= 0) ||
(nav.dir === 1 && VB[nav.axis] >= DMAX[nav.axis] - VB[2 + nav.axis])) {
console.log(`at the edge, cannot go ${nav.name}`);
return
}

/* main case */
For the main case, we move in the desired direction by half the viewBox size along that axis:
else if(nav.act === 'move') {
/* edge cases */

tg[nav.axis] = VB[nav.axis] + .5*nav.dir*VB[2 + nav.axis]
}
Now let's see what we need to do inside the update() function. This is going to be pretty similar to previous demos, except now we need to handle the 'move' and 'zoom' cases separately. We also create an array to store the current viewBox data in (cvb):
function update() {
let k = ++f/NF, j = 1 - k, cvb = VB.slice();

if(nav.act === 'zoom') {
/* what we do if the action is zoom */
}

if(nav.act === 'move') {
/* what we do if the action is move */
}

_SVG.setAttribute('viewBox', cvb.join(' '));

if(!(f%NF)) {
f = 0;
VB.splice(0, 4, ...cvb);
nav = {};
tg = Array(4);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
In the 'zoom' case, we need to recompute all viewBox values. We do this with linear interpolation between the values at the start of the animation and the target values we've previously computed:
if(nav.act === 'zoom') {
for(let i = 0; i < 4; i++)
cvb[i] = j*VB[i] + k*tg[i];
}
In the 'move' case, we only need to recompute one viewBox value - the offset for the axis we move along:
if(nav.act === 'move')
cvb[nav.axis] = j*VB[nav.axis] + k*tg[nav.axis];
And that's it! We now have a working pan and zoom demo with smooth linear transitions in between states:
See the Pen by thebabydino (@thebabydino) on CodePen.
From sad square to happy circle
Another example would be morphing a sad square SVG into a happy circle. We create an SVG with a square viewBox whose 0,0 point is right in the middle. Symmetrical with respect to the origin of the SVG system of coordinates we have a square (a rect element) covering 80% of the SVG. This is our face. We create the eyes with an ellipse and a copy of it, symmetrical with respect to the vertical axis. The mouth is a cubic Bézier curve created with a path element.
- var vb_d = 500, vb_o = -.5*vb_d;
- var fd = .8*vb_d, fr = .5*fd;

svg(viewBox=[vb_o, vb_o, vb_d, vb_d].join(' '))
rect(x=-fr y=-fr width=fd height=fd)
ellipse#eye(cx=.35*fr cy=-.25*fr
rx=.1*fr ry=.15*fr)
use(xlink:href='#eye'
transform='scale(-1 1)')
path(d=`M${-.35*fr} ${.35*fr}
C${-.21*fr} ${.13*fr}
${+.21*fr} ${.13*fr}
${+.35*fr} ${.35*fr}`)
In the JavaScript, we get the face and the mouth elements. We read the face width, which is equal to the height and we use it to compute the maximum corner rounding. This is the value for which we get a circle and is equal to half the square edge. We also get the mouth path data, from where we extract the initial y coordinate of the control points and compute the final y coordinate of the same control points.
const _FACE = document.querySelector('rect'),
_MOUTH = document.querySelector('path'),
RMAX = .5*_FACE.getAttribute('width'),
DATA = _MOUTH.getAttribute('d').slice(1)
.replace('C', '').split(/s+/)
.map(c => +c),
CPY_INI = DATA[3],
CPY_RANGE = 2*(DATA[1] - DATA[3]);
The rest is very similar to all other transition on click demos so far, with just a few minor differences (note that we use an ease-out kind of timing function):
/* same as before */

function timing(k) { return 1 - Math.pow(1 - k, 2) };

function update() {
f += dir;

let k = f/NF, cpy = CPY_INI + timing(k)*CPY_RANGE;

_FACE.setAttribute('rx', (timing(k)*RMAX).toFixed(2));
_MOUTH.setAttribute(
'd',
`M${DATA.slice(0,2)}
C${DATA[2]} ${cpy} ${DATA[4]} ${cpy} ${DATA.slice(-2)}`
);

/* same as before */
};

/* same as before */
And so we have our silly result:
See the Pen by thebabydino (@thebabydino) on CodePen.

Emulating CSS Timing Functions with JavaScript is a post from CSS-Tricks
Source: CssTricks


Emulating CSS Timing Functions with JavaScript

CSS animations and transitions are great! However, while recently toying with an idea, I got really frustrated with the fact that gradients are only animatable in Edge (and IE 10+). Yes, we can do all sorts of tricks with background-position, background-size, background-blend-mode or even opacity and transform on a pseudo-element/ child, but sometimes these are just not enough. Not to mention that we run into similar problems when wanting to animate SVG attributes without a CSS correspondent.
Using a lot of examples, this article is going to explain how to smoothly go from one state to another in a similar fashion to that of common CSS timing functions using just a little bit of JavaScript, without having to rely on a library, so without including a lot of complicated and unnecessary code that may become a big burden in the future.

This is not how the CSS timing functions work. This is an approach that I find simpler and more intuitive than working with Bézier curves. I'm going to show how to experiment with different timing functions using JavaScript and dissect use cases. It is not a tutorial on how to do beautiful animation.
A few examples using a linear timing function
Let's start with a left to right linear-gradient() with a sharp transition where we want to animate the first stop. Here's a way to express that using CSS custom properties:
background: linear-gradient(90deg, #ff9800 var(--stop, 0%), #3c3c3c 0);
On click, we want the value of this stop to go from 0% to 100% (or the other way around, depending on the state it's already in) over the course of NF frames. If an animation is already running at the time of the click, we stop it, change its direction, then restart it.
We also need a few variables such as the request ID (this gets returned by requestAnimationFrame), the index of the current frame (an integer in the [0, NF] interval, starting at 0) and the direction our transition is going in (which is 1 when going towards 100% and -1 when going towards 0%).
While nothing is changing, the request ID is null. We also set the current frame index to 0 initially and the direction to -1, as if we've just arrived to 0% from 100%.
const NF = 80; // number of frames transition happens over

let rID = null, f = 0, dir = -1;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {};

addEventListener('click', e => {
if(rID) stopAni(); // if an animation is already running, stop it
dir *= -1; // change animation direction
update();
}, false);
Now all that's left is to populate the update() function. Within it, we update the current frame index f. Then we compute a progress variable k as the ratio between this current frame index f and the total number of frames NF. Given that f goes from 0 to NF (included), this means that our progress k goes from 0 to 1. Multiply this with 100% and we get the desired stop.
After this, we check whether we've reached one of the end states. If we have, we stop the animation and exit the update() function.
function update() {
f += dir; // update current frame index

let k = f/NF; // compute progress

document.body.style.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
The result can be seen in the Pen below (note that we go back on a second click):
See the Pen by thebabydino (@thebabydino) on CodePen.
The way the pseudo-element is made to contrast with the background below is explained in an older article.
The above demo may look like something we could easily achieve with an element and translating a pseudo-element that can fully cover it, but things get a lot more interesting if we give the background-size a value that's smaller than 100% along the x axis, let's say 5em:
See the Pen by thebabydino (@thebabydino) on CodePen.
This gives us a sort of a "vertical blinds" effect that cannot be replicated in a clean manner with just CSS if we don't want to use more than one element.
Another option would be not to alternate the direction and always sweep from left to right, except only odd sweeps would be orange. This requires tweaking the CSS a bit:
--c0: #ff9800;
--c1: #3c3c3c;
background: linear-gradient(90deg,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
In the JavaScript, we ditch the direction variable and add a type one (typ) that switches between 0 and 1 at the end of every transition. That's when we also update all custom properties:
const S = document.body.style;

let typ = 0;

function update() {
let k = ++f/NF;

S.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
f = 0;
S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);
S.setProperty('--stop', `0%`);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
This gives us the desired result (click at least twice to see how the effect differs from that in the first demo):
See the Pen by thebabydino (@thebabydino) on CodePen.
We could also change the gradient angle instead of the stop. In this case, the background rule becomes:
background: linear-gradient(var(--angle, 0deg),
#ff9800 50%, #3c3c3c 0);
In the JavaScript code, we tweak the update() function:
function update() {
f += dir;

let k = f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
We now have a gradient angle transition in between the two states (0deg and 180deg):
See the Pen by thebabydino (@thebabydino) on CodePen.
In this case, we might also want to keep going clockwise to get back to the 0deg state instead of changing the direction. So we just ditch the dir variable altogether, discard any clicks happening during the transition, and always increment the frame index f, resetting it to 0 when we've completed a full rotation around the circle:
function update() {
let k = ++f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
f = f%(2*NF);
stopAni();
return
}

rID = requestAnimationFrame(update)
};

addEventListener('click', e => {
if(!rID) update()
}, false);
The following Pen illustrates the result - our rotation is now always clockwise:
See the Pen by thebabydino (@thebabydino) on CodePen.
Something else we could do is use a radial-gradient() and animate the radial stop:
background: radial-gradient(circle,
#ff9800 var(--stop, 0%), #3c3c3c 0);
The JavaScript code is identical to that of the first demo and the result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
We may also not want to go back when clicking again, but instead make another blob grow and cover the entire viewport. In this case, we add a few more custom properties to the CSS:
--c0: #ff9800;
--c1: #3c3c3c;
background: radial-gradient(circle,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
The JavaScript is the same as in the case of the third linear-gradient() demo. This gives us the result we want:
See the Pen by thebabydino (@thebabydino) on CodePen.
A fun tweak to this would be to make our circle start growing from the point we clicked. To do so, we introduce two more custom properties, --x and --y:
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
When clicking, we set these to the coordinates of the point where the click happened:
addEventListener('click', e => {
if(!rID) {
S.setProperty('--x', `${e.clientX}px`);
S.setProperty('--y', `${e.clientY}px`);
update();
}
}, false);
This gives us the following result where we have a disc growing from the point where we clicked:
See the Pen by thebabydino (@thebabydino) on CodePen.
Another option would be using a conic-gradient() and animating the angular stop:
background: conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0%)
Note that in the case of conic-gradient(), we must use a unit for the zero value (whether that unit is % or an angular one like deg doesn't matter), otherwise our code won't work - writing conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0) means nothing gets displayed.
The JavaScript is the same as for animating the stop in the linear or radial case, but bear in mind that this currently only works in Chrome with Experimental Web Platform Features enabled in chrome://flags.
The Experimental Web Platform Features flag enabled in Chrome Canary (63.0.3210.0).
Just for the purpose of displaying conic gradients in the browser, there's a polyfill by Lea Verou and this works cross-browser but doesn't allow using CSS custom properties.
The recording below illustrates how our code works:
Recording of how our first conic-gradient() demo works in Chrome with the flag enabled (live demo).
This is another situation where we might not want to go back on a second click. This means we need to alter the CSS a bit, in the same way we did for the last radial-gradient() demo:
--c0: #ff9800;
--c1: #3c3c3c;
background: conic-gradient(
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0%)
The JavaScript code is exactly the same as in the corresponding linear-gradient() or radial-gradient() case and the result can be seen below:
Recording of how our second conic-gradient() demo works in Chrome with the flag enabled (live demo).
Before we move on to other timing functions, there's one more thing to cover: the case when we don't go from 0% to 100%, but in between any two values. We take the example of our first linear-gradient, but with a different default for --stop, let's say 85% and we also set a --stop-fin value - this is going to be the final value for --stop:
--stop-ini: 85%;
--stop-fin: 26%;
background: linear-gradient(90deg, #ff9800 var(--stop, var(--stop-ini)), #3c3c3c 0)
In the JavaScript, we read these two values - the initial (default) and the final one - and we compute a range as the difference between them:
const S = getComputedStyle(document.body),
INI = +S.getPropertyValue('--stop-ini').replace('%', ''),
FIN = +S.getPropertyValue('--stop-fin').replace('%', ''),
RANGE = FIN - INI;
Finally, in the update() function, we take into account the initial value and the range when setting the current value for --stop:
document.body.style.setProperty(
'--stop',
`${+(INI + k*RANGE).toFixed(2)}%`
);
With these changes we now have a transition in between 85% and 26% (and the other way on even clicks):
See the Pen by thebabydino (@thebabydino) on CodePen.
If we want to mix units for the stop value, things get hairier as we need to compute more things (box dimensions when mixing % and px, font sizes if we throw em or rem in the mix, viewport dimensions if we want to use viewport units, the length of the 0% to 100% segment on the gradient line for gradients that are not horizontal or vertical), but the basic idea remains the same.
Emulating ease-in/ ease-out
An ease-in kind of function means the change in value happens slow at first and then accelerates. ease-out is exactly the opposite - the change happens fast in the beginning, but then slows down towards the end.
The ease-in (left) and ease-out (right) timing functions (live).
The slope of the curves above gives us the rate of change. The steeper it is, the faster the change in value happens.
We can emulate these functions by tweaking the linear method described in the first section. Since k takes values in the [0, 1] interval, raising it to any positive power also gives us a number within the same interval. The interactive demo below shows the graph of a function f(k) = pow(k, p) (k raised to an exponent p) shown in purple and that of a function g(k) = 1 - pow(1 - k, p) shown in red on the [0, 1] interval versus the identity function id(k) = k (which corresponds to a linear timing function).
See the Pen by thebabydino (@thebabydino) on CodePen.
When the exponent p is equal to 1, the graphs of the f and g functions are identical to that of the identity function.
When exponent p is greater than 1, the graph of the f function is below the identity line - the rate of change increases as k increases. This is like an ease-in type of function. The graph of the g function is above the identity line - the rate of change decreases as k increases. This is like an ease-out type of function.
It seems an exponent p of about 2 gives us an f that's pretty similar to ease-in, while g is pretty similar to ease-out. With a bit more tweaking, it looks like the best approximation is for a p value of about 1.675:
See the Pen by thebabydino (@thebabydino) on CodePen.
In this interactive demo, we want the graphs of the f and g functions to be as close as possible to the dashed lines, which represent the ease-in timing function (below the identity line) and the ease-out timing function (above the identity line).
Emulating ease-in-out
The CSS ease-in-out timing function looks like in the illustration below:
The ease-in-out timing function (live).
So how can we get something like this?
Well, that's what harmonic functions are for! More exactly, the ease-in-out out shape is reminiscent the shape of the sin() function on the [-90°,90°] interval.
The sin(k) function on the [-90°,90°] interval (live).
However, we don't want a function whose input is in the [-90°,90°] interval and output is in the [-1,1] interval, so let's fix this!
This means we need to squish the hashed rectangle ([-90°,90°]x[-1,1]) in the illustration above into the unit one ([0,1]x[0,1]).
First, let's take the domain [-90°,90°]. If we change our function to be sin(k·180°) (or sin(k·π) in radians), then our domain becomes [-.5,.5] (we can check that -.5·180° = 90° and .5·180° = 90°):
The sin(k·π) function on the [-.5,.5] interval (live).
We can shift this domain to the right by .5 and get the desired [0,1] interval if we change our function to be sin((k - .5)·π) (we can check that 0 - .5 = -.5 and 1 - .5 = .5):
The sin((k - .5)·π) function on the [0,1] interval (live).
Now let's get the desired codomain. If we add 1 to our function making it sin((k - .5)·π) + 1 this shifts out codomain up into the [0, 2] interval:
The sin((k - .5)·π) + 1 function on the [0,1] interval (live).
Dividing everything by 2 gives us the (sin((k - .5)·π) + 1)/2 function and compacts the codomain into our desired [0,1] interval:
The (sin((k - .5)·π) + 1)/2 function on the [0,1] interval (live).
This turns out to be a good approximation of the ease-in-out timing function (represented with an orange dashed line in the illustration above).
Comparison of all these timing functions
Let's say we want to have a bunch of elements with a linear-gradient() (like in the third demo). On click, their --stop values go from 0% to 100%, but with a different timing function for each.
In the JavaScript, we create a timing functions object with the corresponding function for each type of easing:
tfn = {
'linear': function(k) {
return k;
},
'ease-in': function(k) {
return Math.pow(k, 1.675);
},
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675);
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1);
}
};
For each of these, we create an article element:
const _ART = [];

let frag = document.createDocumentFragment();

for(let p in tfn) {
let art = document.createElement('article'),
hd = document.createElement('h3');

hd.textContent = p;
art.appendChild(hd);
art.setAttribute('id', p);
_ART.push(art);
frag.appendChild(art);
}

n = _ART.length;
document.body.appendChild(frag);
The update function is pretty much the same, except we set the --stop custom property for every element as the value returned by the corresponding timing function when fed the current progress k. Also, when resetting the --stop to 0% at the end of the animation, we also need to do this for every element.
function update() {
let k = ++f/NF;

for(let i = 0; i < n; i++) {
_ART[i].style.setProperty(
'--stop',
`${+tfn[_ART[i].id](k).toFixed(5)*100}%`
);
}

if(!(f%NF)) {
f = 0;

S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);

for(let i = 0; i < n; i++)
_ART[i].style.setProperty('--stop', `0%`);

stopAni();
return;
}

rID = requestAnimationFrame(update)
};
This gives us a nice visual comparison of these timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
They all start and finish at the same time, but while the progress is constant for the linear one, the ease-in one starts slowly and then accelerates, the ease-out one starts fast and then slows down and, finally, the ease-in-out one starts slowly, accelerates and then slows down again at the end.
Timing functions for bouncing transitions
I first came across the concept years ago, in Lea Verou's CSS Secrets talk. These happen when the y (even) values in a cubic-bezier() function are outside the [0, 1] range and the effect they create is of the animated value going outside the interval between its initial and final value.
This bounce can happen right after the transition starts, right before it finishes or at both ends.
A bounce at the start means that, at first, we don't go towards the final state, but in the opposite direction. For example, if want to animate a stop from 43% to 57% and we have a bounce at the start, then, at first, out stop value doesn't increase towards 57%, but decreases below 43% before going back up to the final state. Similarly, if we go from an initial stop value of 57% to a final stop value of 43% and we have a bounce at the start, then, at first, the stop value increases above 57% before going down to the final value.
A bounce at the end means we overshoot our final state and only then go back to it. If want to animate a stop from 43% to 57% and we have a bounce at the end, then we start going normally from the initial state to the final one, but towards the end, we go above 57% before going back down to it. And if we go from an inital stop value of 57% to a final stop value of 43% and we have a bounce at the end, then, at first, we go down towards the final state, but, towards the end, we pass it and we briefly have stop values below 43% before our transition finishes there.
If what they do is still difficult to grasp, below there's a comparative example of all three of them in action.
The three cases.
These kinds of timing functions don't have their own keywords associated, but they look cool and they are what we want in a lot of situations.
Just like in the case of ease-in-out, the quickest way of getting them is by using harmonic functions. The difference lies in the fact that now we don't start from the [-90°,90°] domain anymore.
For a bounce at the beginning, we start with the [s, 0°] portion of the sin() function, where s (the start angle) is in the (-180°,-90°) interval. The closer it is to -180°, the bigger the bounce is and the faster it will go to the final state after it. So we don't want it to be really close to -180° because the result would look too unnatural. We also want it to be far enough from -90° that the bounce is noticeable.
In the interactive demo below, you can drag the slider to change the start angle and then click on the stripe at the bottom to see the effect in action:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,0]x[sin(s),0]) is the area we need move and scale into the [0,1]x[0,1] area in order to get our timing function. The part of the curve that's below its lower edge is where the bounce happens. You can adjust the start angle using the slider and then click on the bottom bar to see how the transition looks for different start angles.
Just like in the ease-in-out case, we first squish the domain into the [-1,0] interval by dividing the argument with the range (which is the maximum 0 minus the minimum s). Therefore, our function becomes sin(-k·s) (we can check that -(-1)·s = s and -0·s = 0):
The sin(-k·s) function on the [-1,0] interval (live).
Next, we shift this interval to the right (by 1, into [0,1]). This makes our function sin(-(k - 1)·s) = sin((1 - k)·s) (it checks that 0 - 1 = -1 and 1 - 1 = 0):
The sin(-(k - 1)·s) function on the [0,1] interval (live).
We then shift the codomain up by its value at 0 (sin((1 - 0)*s) = sin(s)). Our function is now sin((1 - k)·s) - sin(s) and our codomain [0,-sin(s)].
The sin(-(k - 1)·s) - sin(s) function on the [0,1] interval (live).
The last step is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is -sin(s)). This means our final easing function is 1 - sin((1 - k)·s)/sin(s)
The 1 - sin((1 - k)·s)/sin(s) function on the [0,1] interval (live).
For a bounce at the end, we start with the [0°, e] portion of the sin() function, where e (the end angle) is in the (90°,180°) interval. The closer it is to 180°, the bigger the bounce is and the faster it will move from the initial state to the final one before it overshoots it and the bounce happens. So we don't want it to be really close to 180° as the result would look too unnatural. We also want it to be far enough from 90° so that the bounce is noticeable.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([0,e]x[0,sin(e)]) is the area we need to squish and move into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's below its upper edge is where the bounce happens.
We start by squishing the domain into the [0,1] interval by dividing the argument with the range (which is the maximum e minus the minimum 0). Therefore, our function becomes sin(k·e) (we can check that 0·e = 0 and 1·e = e):
The sin(k·e) function on the [0,1] interval (live).
What's still left to do is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is sin(e)). This means our final easing function is sin(k·e)/sin(e).
The sin(k·e)/sin(e) function on the [0,1] interval (live).
If we want a bounce at each end, we start with the [s, e] portion of the sin() function, where s is in the (-180°,-90°) interval and e in the (90°,180°) interval. The larger s and e are in absolute values, the bigger the corresponding bounces are and the more of the total transition time is spent on them alone. On the other hand, the closer their absolute values get to 90°, the less noticeable their corresponding bounces are. So, just like in the previous two cases, it's all about finding the right balance.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,e]x[sin(s),sin(e)]) is the area we need to move and scale into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's beyond its horizontal edges is where the bounces happen.
We start by shifting the domain to the right into the [0,e - s] interval. This means our function becomes sin(k + s) (we can check that 0 + s = s and that e - s + s = e).
The sin(k + s) function on the [0,e - s] interval (live).
Then we shrink the domain to fit into the [0,1] interval, which gives us the function sin(k·(e - s) + s).
The sin(k·(e - s) + s) function on the [0,1] interval (live).
Moving on to the codomain, we first shift it up by its value at 0 (sin(0·(e - s) + s)), which means we now have sin(k·(e - s) + s) - sin(s). This gives us the new codomain [0,sin(e) - sin(s)].
The sin(k·(e - s) + s) - sin(s) function on the [0,1] interval (live).
Finally, we shrink the codomain to the [0,1] interval by dividing with the range (sin(e) - sin(s)), so our final function is (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)).
The (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)) function on the [0,1] interval (live).
So in order to do a similar comparative demo to that for the JS equivalents of the CSS linear, ease-in, ease-out, ease-in-out, our timing functions object becomes:
tfn = {
'bounce-ini': function(k) {
return 1 - Math.sin((1 - k)*s)/Math.sin(s);
},
'bounce-fin': function(k) {
return Math.sin(k*e)/Math.sin(e);
},
'bounce-ini-fin': function(k) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s));
}
};
The s and e variables are the values we get from the two range inputs that allow us to control the bounce amount.
The interactive demo below shows the visual comparison of these three types of timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alternating animations
In CSS, setting animation-direction to alternate also reverses the timing function. In order to better understand this, consider a .box element on which we animate its transform property such that we move to the right. This means our @keyframes look as follows:
@keyframes shift {
0%, 10% { transform: none }
90%, 100% { transform: translate(50vw) }
}
We use a custom timing function that allows us to have a bounce at the end and we make this animation alternate - that is, go from the final state (translate(50vw)) back to the initial state (no translation) for the even-numbered iterations (second, fourth and so on).
animation: shift 1s cubic-bezier(.5, 1, .75, 1.5) infinite alternate
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
One important thing to notice here is that, for the even-numbered iterations, our bounce doesn't happen at the end, but at the start - the timing function is reversed. Visually, this means it's reflected both horizontally and vertically with respect to the .5,.5 point.
The normal timing function (f, in red, with a bounce at the end) and the symmetrical reverse one (g, in purple, with a bounce at the start) (live)
In CSS, there is no way of having a different timing function other than the symmetrical one on going back if we are to use this set of keyframes and animation-direction: alternate. We can introduce the going back part into the keyframes and control the timing function for each stage of the animation, but that's outside the scope of this article.
When changing values with JavaScript in the fashion presented so far in this article, the same thing happens by default. Consider the case when we want to animate the stop of a linear-gradient() between an initial and a final position and we want to have a bounce at the end. This is pretty much the last example presented in the first section with timing function that lets us have a bounce at the end (one in the bounce-fin category described before) instead of a linear one.
The CSS is exactly the same and we only make a few minor changes to the JavaScript code. We set a limit angle E and we use a custom bounce-fin kind of timing function in place of the linear one:
const E = .75*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
};

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + timing(k)*RANGE).toFixed(2)}%`
);

/* same as before */
};

/* same as before */
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the initial state, the stop is at 85%. We animate it to 26% (which is the final state) using a timing function that gives us a bounce at the end. This means we go beyond our final stop position at 26% before going back up and stopping there. This is what happens during the odd iterations.
During the even iterations, this behaves just like in the CSS case, reversing the timing function, so that the bounce happens at the beginning, not at the end.
But what if we don't want the timing function to be reversed?
In this case, we need to use the symmetrical function. For any timing function f(k) defined on the [0,1] interval (this is the domain), whose values are in the [0,1] (codomain), the symmetrical function we want is 1 - f(1 - k). Note that functions whose shape is actually symmetrical with respect to the .5,.5 point, like linear or ease-in-out are identical to their symmetrical functions.
See the Pen by thebabydino (@thebabydino) on CodePen.
So what we do is use our timing function f(k) for the odd iterations and use 1 - f(1 - k) for the even ones. We can tell whether an iteration is odd or even from the direction (dir) variable. This is 1 for odd iterations and -1 for even ones.
This means we can combine our two timing functions into one: m + dir*f(m + dir*k).
Here, the multiplier m is 0 for the odd iterations (when dir is 1) and 1 for the even ones (when dir is -1), so we can compute it as .5*(1 - dir):
dir = +1 → m = .5*(1 - (+1)) = .5*(1 - 1) = .5*0 = 0
dir = -1 → m = .5*(1 - (-1)) = .5*(1 + 1) = .5*2 = 1
This way, our JavaScript becomes:
let m;

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + (m + dir*timing(m + dir*k))*RANGE).toFixed(2)}%`
);

/* same as before */
};

addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
The final result can be seen in this Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Even more examples
Gradient stops are not the only things that aren't animatable cross-browser with just CSS.
Gradient end going from orange to violet
For a first example of something different, let's say we want the orange in our gradient to animate to a kind of violet. We start with a CSS that looks something like this:
--c-ini: #ff9800;
--c-fin: #a048b9;
background: linear-gradient(90deg,
var(--c, var(--c-ini)), #3c3c3c)
In order to interpolate between the initial and final values, we need to know the format we get when reading them via JavaScript - is it going to be the same format we set them in? Is it going to be always rgb()/ rgba()?
Here is where things get a bit hairy. Consider the following test, where we have a gradient where we've used every format possible:
--c0: hsl(150, 100%, 50%); // springgreen
--c1: orange;
--c2: #8a2be2; // blueviolet
--c3: rgb(220, 20, 60); // crimson
--c4: rgba(255, 245, 238, 1); // seashell with alpha = 1
--c5: hsla(51, 100%, 50%, .5); // gold with alpha = .5
background: linear-gradient(90deg,
var(--c0), var(--c1),
var(--c2), var(--c3),
var(--c4), var(--c5))
We read the computed values of the gradient image and the individual custom properties --c0 through --c5 via JavaScript.
let s = getComputedStyle(document.body);

console.log(s.backgroundImage);
console.log(s.getPropertyValue('--c0'), 'springgreen');
console.log(s.getPropertyValue('--c1'), 'orange');
console.log(s.getPropertyValue('--c2'), 'blueviolet');
console.log(s.getPropertyValue('--c3'), 'crimson');
console.log(s.getPropertyValue('--c4'), 'seashell (alpha = 1)');
console.log(s.getPropertyValue('--c5'), 'gold (alpha = .5)');
The results seem a bit inconsistent.
Screenshots showing what gets logged in Chrome, Edge and Firefox (live).
Whatever we do, if we have an alpha of strictly less than 1, what we get via JavaScript seems to be always an rgba() value, regardless of whether we've set it with rgba() or hsla().
All browsers also agree when reading the custom properties directly, though, this time, what we get doesn't seem to make much sense: orange, crimson and seashell are returned as keywords regardless of how they were set, but we get hex values for springgreen and blueviolet. Except for orange, which was added in Level 2, all these values were added to CSS in Level 3, so why do we get some as keywords and others as hex values?
For the background-image, Firefox always returns the fully opaque values only as rgb(), while Chrome and Edge return them as either keywords or hex values, just like they do in the case when we read the custom properties directly.
Oh well, at least that lets us know we need to take into account different formats.
So the first thing we need to do is map the keywords to rgb() values. Not going to write all that manually, so a quick search finds this repo - perfect, it's exactly what we want! We can now set that as the value of a CMAP constant.
The next step here is to create a getRGBA(c) function that would take a string representing a keyword, a hex or an rgb()/ rgba() value and return an array containing the RGBA values ([red, green, blue, alpha]).
We start by building our regular expressions for the hex and rgb()/ rgba() values. These are a bit loose and would catch quite a few false positives if we were to have user input, but since we're only using them on CSS computed style values, we can afford to take the quick and dirty path here:
let re_hex = /^#([a-fd]{1,2})([a-fd]{1,2})([a-fd]{1,2})$/i,
re_rgb = /^rgba?((d{1,3},s){2}d{1,3}(,s((0|1)?.?d*))?)/;
Then we handle the three types of values we've seen we might get by reading the computed styles:
if(c in CMAP) return CMAP[c]; // keyword lookup, return rgb

if([4, 7].indexOf(c.length) !== -1 && re_hex.test(c)) {
c = c.match(re_hex).slice(1); // remove the '#'
if(c[0].length === 1) c = c.map(x => x + x);
// go from 3-digit form to 6-digit one
c.push(1); // add an alpha of 1

// return decimal valued RGBA array
return c.map(x => parseInt(x, 16))
}

if(re_rgb.test(c)) {
// extract values
c = c.replace(/rgba?(/, '').replace(')', '').split(',').map(x => +x.trim());
if(c.length === 3) c.push(1); // if no alpha specified, use 1

return c // return RGBA array
}
Now after adding the keyword to RGBA map (CMAP) and the getRGBA() function, our JavaScript code doesn't change much from the previous examples:
const INI = getRGBA(S.getPropertyValue('--c-ini').trim()),
FIN = getRGBA(S.getPropertyValue('--c-fin').trim()),
RANGE = [],
ALPHA = 1 - INI[3] || 1 - FIN[3];

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + k*RANGE[i])).join(',')})`
);

/* same as before */
};

(function init() {
if(!ALPHA) INI.pop(); // get rid of alpha if always 1
RANGE.splice(0, 0, ...INI.map((c, i) => FIN[i] - c));
})();

/* same as before */
This gives us a linear gradient animation:
See the Pen by thebabydino (@thebabydino) on CodePen.
We can also use a different, non-linear timing function, for example one that allows for a bounce at the end:
const E = .8*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
}

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + timing(k)*RANGE[i])).join(',')})`
);

/* same as before */
};

/* same as before */
This means we go all the way to a kind of blue before going back to our final violet:
See the Pen by thebabydino (@thebabydino) on CodePen.
Do note however that, in general, RGBA transitions are not the best place to illustrate bounces. That's because the RGB channels are strictly limited to the [0,255] range and the alpha channel is strictly limited to the [0,1] range. rgb(255, 0, 0) is as red as red gets, there's no redder red with a value of over 255 for the first channel. A value of 0 for the alpha channel means completely transparent, there's no greater transparency with a negative value.
By now, you're probably already bored with gradients, so let's switch to something else!
Smooth changing SVG attribute values
At this point, we cannot alter the geometry of SVG elements via CSS. We should be able to as per the SVG2 spec and Chrome does support some of this stuff, but what if we want to animate the geometry of SVG elements now, in a more cross-browser manner?
Well, you've probably guessed it, JavaScript to the rescue!
Growing a circle
Our first example is that of a circle whose radius goes from nothing (0) to a quarter of the minimum viewBox dimension. We keep the document structure simple, without any other aditional elements.
<svg viewBox='-100 -50 200 100'>
<circle/>
</svg>
For the JavaScript part, the only notable difference from the previous demos is that we read the SVG viewBox dimensions in order to get the maximum radius and we now set the r attribute within the update() function, not a CSS variable (it would be immensely useful if CSS variables were allowed as values for such attributes, but, sadly, we don't live in an ideal world):
const _G = document.querySelector('svg'),
_C = document.querySelector('circle'),
VB = _G.getAttribute('viewBox').split(' '),
RMAX = .25*Math.min(...VB.slice(2)),
E = .8*Math.PI;

/* same as before */

function update() {
/* same as before */

_C.setAttribute('r', (timing(k)*RMAX).toFixed(2));

/* same as before */
};

/* same as before */
Below, you can see the result when using a bounce-fin kind of timing function:
See the Pen by thebabydino (@thebabydino) on CodePen.
Pan and zoom map
Another SVG example is a smooth pan and zoom map demo. In this case, we take a map like those from amCharts, clean up the SVG and then create this effect by triggering a linear viewBox animation when pressing the +/ - keys (zoom) and the arrow keys (pan).
The first thing we do in the JavaScript is create a navigation map, where we take the key codes of interest and attach info about what we do when the corresponding keys are pressed (note that we need different key codes for + and - in Firefox for some reason).
const NAV_MAP = {
187: { dir: 1, act: 'zoom', name: 'in' } /* + */,
61: { dir: 1, act: 'zoom', name: 'in' } /* + Firefox ¯_(ツ)_/¯ */,
189: { dir: -1, act: 'zoom', name: 'out' } /* - */,
173: { dir: -1, act: 'zoom', name: 'out' } /* - Firefox ¯_(ツ)_/¯ */,
37: { dir: -1, act: 'move', name: 'left', axis: 0 } /* ⇦ */,
38: { dir: -1, act: 'move', name: 'up', axis: 1 } /* ⇧ */,
39: { dir: 1, act: 'move', name: 'right', axis: 0 } /* ⇨ */,
40: { dir: 1, act: 'move', name: 'down', axis: 1 } /* ⇩ */
}
When pressing the + key, what we want to do is zoom in. The action we perform is 'zoom' in the positive direction - we go 'in'. Similarly, when pressing the - key, the action is also 'zoom', but in the negative (-1) direction - we go 'out'.
When pressing the arrow left key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the negative (-1) direction - we go 'left'. When pressing the arrow up key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the negative (-1) direction - we go 'up'.
When pressing the arrow right key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the positive direction - we go 'right'. When pressing the arrow down key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the positive direction - we go 'down'.
We then get the SVG element, its initial viewBox, set the maximum zoom out level to these initial viewBox dimensions and set the smallest possible viewBox width to a much smaller value (let's say 8).
const _SVG = document.querySelector('svg'),
VB = _SVG.getAttribute('viewBox').split(' ').map(c => +c),
DMAX = VB.slice(2), WMIN = 8;
We also create an empty current navigation object to hold the current navigation action data and a target viewBox array to contain the final state we animate the viewBox to for the current animation.
let nav = {}, tg = Array(4);
On 'keyup', if we don't have any animation running already and the key that was pressed is one of interest, we get the current navigation object from the navigation map we created at the beginning. After this, we handle the two action cases ('zoom'/ 'move') and call the update() function:
addEventListener('keyup', e => {
if(!rID && e.keyCode in NAV_MAP) {
nav = NAV_MAP[e.keyCode];

if(nav.act === 'zoom') {
/* what we do if the action is 'zoom' */
}

else if(nav.act === 'move') {
/* what we do if the action is 'move' */
}

update()
}
}, false);
Now let's see what we do if we zoom. First off, and this is a very useful programming tactic in general, not just here in particular, we get the edge cases that make us exit the function out of the way.
So what are our edge cases here?
The first one is when we want to zoom out (a zoom in the negative direction) when our whole map is already in sight (the current viewBox dimensions are bigger or equal to the maximum ones). In our case, this should happen if we want to zoom out at the very beginning because we start with the whole map in sight.
The second edge case is when we hit the other limit - we want to zoom in, but we're at the maximum detail level (the current viewBox dimensions are smaller or equal to the minimum ones).
Putting the above into JavaScript code, we have:
if(nav.act === 'zoom') {
if((nav.dir === -1 && VB[2] >= DMAX[0]) ||
(nav.dir === 1 && VB[2] <= WMIN)) {
console.log(`cannot ${nav.act} ${nav.name} more`);
return
}

/* main case */
}
Now that we've handled the edge cases, let's move on to the main case. Here, we set the target viewBox values. We use a 2x zoom on each step, meaning that when we zoom in, the target viewBox dimensions are half the ones at the start of the current zoom action, and when we zoom out they're double. The target offsets are half the difference between the maximum viewBox dimensions and the target ones.
if(nav.act === 'zoom') {
/* edge cases */

for(let i = 0; i < 2; i++) {
tg[i + 2] = VB[i + 2]/Math.pow(2, nav.dir);
tg[i] = .5*(DMAX[i] - tg[i + 2]);
}
}
Next, let's see what we do if we want to move instead of zooming.
In a similar fashion, we get the edge cases that make us exit the function out of the way first. Here, these happen when we're at an edge of the map and we want to keep going in that direction (whatever the direction might be). Since originally the top left corner of our viewBox is at 0,0, this means we cannot go below 0 or above the maximum viewBox size minus the current one. Note that given we're initially fully zoomed out, this also means we cannot move in any direction until we zoom in.
else if(nav.act === 'move') {
if((nav.dir === -1 && VB[nav.axis] <= 0) ||
(nav.dir === 1 && VB[nav.axis] >= DMAX[nav.axis] - VB[2 + nav.axis])) {
console.log(`at the edge, cannot go ${nav.name}`);
return
}

/* main case */
For the main case, we move in the desired direction by half the viewBox size along that axis:
else if(nav.act === 'move') {
/* edge cases */

tg[nav.axis] = VB[nav.axis] + .5*nav.dir*VB[2 + nav.axis]
}
Now let's see what we need to do inside the update() function. This is going to be pretty similar to previous demos, except now we need to handle the 'move' and 'zoom' cases separately. We also create an array to store the current viewBox data in (cvb):
function update() {
let k = ++f/NF, j = 1 - k, cvb = VB.slice();

if(nav.act === 'zoom') {
/* what we do if the action is zoom */
}

if(nav.act === 'move') {
/* what we do if the action is move */
}

_SVG.setAttribute('viewBox', cvb.join(' '));

if(!(f%NF)) {
f = 0;
VB.splice(0, 4, ...cvb);
nav = {};
tg = Array(4);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
In the 'zoom' case, we need to recompute all viewBox values. We do this with linear interpolation between the values at the start of the animation and the target values we've previously computed:
if(nav.act === 'zoom') {
for(let i = 0; i < 4; i++)
cvb[i] = j*VB[i] + k*tg[i];
}
In the 'move' case, we only need to recompute one viewBox value - the offset for the axis we move along:
if(nav.act === 'move')
cvb[nav.axis] = j*VB[nav.axis] + k*tg[nav.axis];
And that's it! We now have a working pan and zoom demo with smooth linear transitions in between states:
See the Pen by thebabydino (@thebabydino) on CodePen.
From sad square to happy circle
Another example would be morphing a sad square SVG into a happy circle. We create an SVG with a square viewBox whose 0,0 point is right in the middle. Symmetrical with respect to the origin of the SVG system of coordinates we have a square (a rect element) covering 80% of the SVG. This is our face. We create the eyes with an ellipse and a copy of it, symmetrical with respect to the vertical axis. The mouth is a cubic Bézier curve created with a path element.
- var vb_d = 500, vb_o = -.5*vb_d;
- var fd = .8*vb_d, fr = .5*fd;

svg(viewBox=[vb_o, vb_o, vb_d, vb_d].join(' '))
rect(x=-fr y=-fr width=fd height=fd)
ellipse#eye(cx=.35*fr cy=-.25*fr
rx=.1*fr ry=.15*fr)
use(xlink:href='#eye'
transform='scale(-1 1)')
path(d=`M${-.35*fr} ${.35*fr}
C${-.21*fr} ${.13*fr}
${+.21*fr} ${.13*fr}
${+.35*fr} ${.35*fr}`)
In the JavaScript, we get the face and the mouth elements. We read the face width, which is equal to the height and we use it to compute the maximum corner rounding. This is the value for which we get a circle and is equal to half the square edge. We also get the mouth path data, from where we extract the initial y coordinate of the control points and compute the final y coordinate of the same control points.
const _FACE = document.querySelector('rect'),
_MOUTH = document.querySelector('path'),
RMAX = .5*_FACE.getAttribute('width'),
DATA = _MOUTH.getAttribute('d').slice(1)
.replace('C', '').split(/s+/)
.map(c => +c),
CPY_INI = DATA[3],
CPY_RANGE = 2*(DATA[1] - DATA[3]);
The rest is very similar to all other transition on click demos so far, with just a few minor differences (note that we use an ease-out kind of timing function):
/* same as before */

function timing(k) { return 1 - Math.pow(1 - k, 2) };

function update() {
f += dir;

let k = f/NF, cpy = CPY_INI + timing(k)*CPY_RANGE;

_FACE.setAttribute('rx', (timing(k)*RMAX).toFixed(2));
_MOUTH.setAttribute(
'd',
`M${DATA.slice(0,2)}
C${DATA[2]} ${cpy} ${DATA[4]} ${cpy} ${DATA.slice(-2)}`
);

/* same as before */
};

/* same as before */
And so we have our silly result:
See the Pen by thebabydino (@thebabydino) on CodePen.

Emulating CSS Timing Functions with JavaScript is a post from CSS-Tricks
Source: CssTricks


Emulating CSS Timing Functions with JavaScript

CSS animations and transitions are great! However, while recently toying with an idea, I got really frustrated with the fact that gradients are only animatable in Edge (and IE 10+). Yes, we can do all sorts of tricks with background-position, background-size, background-blend-mode or even opacity and transform on a pseudo-element/ child, but sometimes these are just not enough. Not to mention that we run into similar problems when wanting to animate SVG attributes without a CSS correspondent.
Using a lot of examples, this article is going to explain how to smoothly go from one state to another in a similar fashion to that of common CSS timing functions using just a little bit of JavaScript, without having to rely on a library, so without including a lot of complicated and unnecessary code that may become a big burden in the future.

This is not how the CSS timing functions work. This is an approach that I find simpler and more intuitive than working with Bézier curves. I'm going to show how to experiment with different timing functions using JavaScript and dissect use cases. It is not a tutorial on how to do beautiful animation.
A few examples using a linear timing function
Let's start with a left to right linear-gradient() with a sharp transition where we want to animate the first stop. Here's a way to express that using CSS custom properties:
background: linear-gradient(90deg, #ff9800 var(--stop, 0%), #3c3c3c 0);
On click, we want the value of this stop to go from 0% to 100% (or the other way around, depending on the state it's already in) over the course of NF frames. If an animation is already running at the time of the click, we stop it, change its direction, then restart it.
We also need a few variables such as the request ID (this gets returned by requestAnimationFrame), the index of the current frame (an integer in the [0, NF] interval, starting at 0) and the direction our transition is going in (which is 1 when going towards 100% and -1 when going towards 0%).
While nothing is changing, the request ID is null. We also set the current frame index to 0 initially and the direction to -1, as if we've just arrived to 0% from 100%.
const NF = 80; // number of frames transition happens over

let rID = null, f = 0, dir = -1;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {};

addEventListener('click', e => {
if(rID) stopAni(); // if an animation is already running, stop it
dir *= -1; // change animation direction
update();
}, false);
Now all that's left is to populate the update() function. Within it, we update the current frame index f. Then we compute a progress variable k as the ratio between this current frame index f and the total number of frames NF. Given that f goes from 0 to NF (included), this means that our progress k goes from 0 to 1. Multiply this with 100% and we get the desired stop.
After this, we check whether we've reached one of the end states. If we have, we stop the animation and exit the update() function.
function update() {
f += dir; // update current frame index

let k = f/NF; // compute progress

document.body.style.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
The result can be seen in the Pen below (note that we go back on a second click):
See the Pen by thebabydino (@thebabydino) on CodePen.
The way the pseudo-element is made to contrast with the background below is explained in an older article.
The above demo may look like something we could easily achieve with an element and translating a pseudo-element that can fully cover it, but things get a lot more interesting if we give the background-size a value that's smaller than 100% along the x axis, let's say 5em:
See the Pen by thebabydino (@thebabydino) on CodePen.
This gives us a sort of a "vertical blinds" effect that cannot be replicated in a clean manner with just CSS if we don't want to use more than one element.
Another option would be not to alternate the direction and always sweep from left to right, except only odd sweeps would be orange. This requires tweaking the CSS a bit:
--c0: #ff9800;
--c1: #3c3c3c;
background: linear-gradient(90deg,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
In the JavaScript, we ditch the direction variable and add a type one (typ) that switches between 0 and 1 at the end of every transition. That's when we also update all custom properties:
const S = document.body.style;

let typ = 0;

function update() {
let k = ++f/NF;

S.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
f = 0;
S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);
S.setProperty('--stop', `0%`);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
This gives us the desired result (click at least twice to see how the effect differs from that in the first demo):
See the Pen by thebabydino (@thebabydino) on CodePen.
We could also change the gradient angle instead of the stop. In this case, the background rule becomes:
background: linear-gradient(var(--angle, 0deg),
#ff9800 50%, #3c3c3c 0);
In the JavaScript code, we tweak the update() function:
function update() {
f += dir;

let k = f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
We now have a gradient angle transition in between the two states (0deg and 180deg):
See the Pen by thebabydino (@thebabydino) on CodePen.
In this case, we might also want to keep going clockwise to get back to the 0deg state instead of changing the direction. So we just ditch the dir variable altogether, discard any clicks happening during the transition, and always increment the frame index f, resetting it to 0 when we've completed a full rotation around the circle:
function update() {
let k = ++f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
f = f%(2*NF);
stopAni();
return
}

rID = requestAnimationFrame(update)
};

addEventListener('click', e => {
if(!rID) update()
}, false);
The following Pen illustrates the result - our rotation is now always clockwise:
See the Pen by thebabydino (@thebabydino) on CodePen.
Something else we could do is use a radial-gradient() and animate the radial stop:
background: radial-gradient(circle,
#ff9800 var(--stop, 0%), #3c3c3c 0);
The JavaScript code is identical to that of the first demo and the result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
We may also not want to go back when clicking again, but instead make another blob grow and cover the entire viewport. In this case, we add a few more custom properties to the CSS:
--c0: #ff9800;
--c1: #3c3c3c;
background: radial-gradient(circle,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
The JavaScript is the same as in the case of the third linear-gradient() demo. This gives us the result we want:
See the Pen by thebabydino (@thebabydino) on CodePen.
A fun tweak to this would be to make our circle start growing from the point we clicked. To do so, we introduce two more custom properties, --x and --y:
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
When clicking, we set these to the coordinates of the point where the click happened:
addEventListener('click', e => {
if(!rID) {
S.setProperty('--x', `${e.clientX}px`);
S.setProperty('--y', `${e.clientY}px`);
update();
}
}, false);
This gives us the following result where we have a disc growing from the point where we clicked:
See the Pen by thebabydino (@thebabydino) on CodePen.
Another option would be using a conic-gradient() and animating the angular stop:
background: conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0%)
Note that in the case of conic-gradient(), we must use a unit for the zero value (whether that unit is % or an angular one like deg doesn't matter), otherwise our code won't work - writing conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0) means nothing gets displayed.
The JavaScript is the same as for animating the stop in the linear or radial case, but bear in mind that this currently only works in Chrome with Experimental Web Platform Features enabled in chrome://flags.
The Experimental Web Platform Features flag enabled in Chrome Canary (63.0.3210.0).
Just for the purpose of displaying conic gradients in the browser, there's a polyfill by Lea Verou and this works cross-browser but doesn't allow using CSS custom properties.
The recording below illustrates how our code works:
Recording of how our first conic-gradient() demo works in Chrome with the flag enabled (live demo).
This is another situation where we might not want to go back on a second click. This means we need to alter the CSS a bit, in the same way we did for the last radial-gradient() demo:
--c0: #ff9800;
--c1: #3c3c3c;
background: conic-gradient(
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0%)
The JavaScript code is exactly the same as in the corresponding linear-gradient() or radial-gradient() case and the result can be seen below:
Recording of how our second conic-gradient() demo works in Chrome with the flag enabled (live demo).
Before we move on to other timing functions, there's one more thing to cover: the case when we don't go from 0% to 100%, but in between any two values. We take the example of our first linear-gradient, but with a different default for --stop, let's say 85% and we also set a --stop-fin value - this is going to be the final value for --stop:
--stop-ini: 85%;
--stop-fin: 26%;
background: linear-gradient(90deg, #ff9800 var(--stop, var(--stop-ini)), #3c3c3c 0)
In the JavaScript, we read these two values - the initial (default) and the final one - and we compute a range as the difference between them:
const S = getComputedStyle(document.body),
INI = +S.getPropertyValue('--stop-ini').replace('%', ''),
FIN = +S.getPropertyValue('--stop-fin').replace('%', ''),
RANGE = FIN - INI;
Finally, in the update() function, we take into account the initial value and the range when setting the current value for --stop:
document.body.style.setProperty(
'--stop',
`${+(INI + k*RANGE).toFixed(2)}%`
);
With these changes we now have a transition in between 85% and 26% (and the other way on even clicks):
See the Pen by thebabydino (@thebabydino) on CodePen.
If we want to mix units for the stop value, things get hairier as we need to compute more things (box dimensions when mixing % and px, font sizes if we throw em or rem in the mix, viewport dimensions if we want to use viewport units, the length of the 0% to 100% segment on the gradient line for gradients that are not horizontal or vertical), but the basic idea remains the same.
Emulating ease-in/ ease-out
An ease-in kind of function means the change in value happens slow at first and then accelerates. ease-out is exactly the opposite - the change happens fast in the beginning, but then slows down towards the end.
The ease-in (left) and ease-out (right) timing functions (live).
The slope of the curves above gives us the rate of change. The steeper it is, the faster the change in value happens.
We can emulate these functions by tweaking the linear method described in the first section. Since k takes values in the [0, 1] interval, raising it to any positive power also gives us a number within the same interval. The interactive demo below shows the graph of a function f(k) = pow(k, p) (k raised to an exponent p) shown in purple and that of a function g(k) = 1 - pow(1 - k, p) shown in red on the [0, 1] interval versus the identity function id(k) = k (which corresponds to a linear timing function).
See the Pen by thebabydino (@thebabydino) on CodePen.
When the exponent p is equal to 1, the graphs of the f and g functions are identical to that of the identity function.
When exponent p is greater than 1, the graph of the f function is below the identity line - the rate of change increases as k increases. This is like an ease-in type of function. The graph of the g function is above the identity line - the rate of change decreases as k increases. This is like an ease-out type of function.
It seems an exponent p of about 2 gives us an f that's pretty similar to ease-in, while g is pretty similar to ease-out. With a bit more tweaking, it looks like the best approximation is for a p value of about 1.675:
See the Pen by thebabydino (@thebabydino) on CodePen.
In this interactive demo, we want the graphs of the f and g functions to be as close as possible to the dashed lines, which represent the ease-in timing function (below the identity line) and the ease-out timing function (above the identity line).
Emulating ease-in-out
The CSS ease-in-out timing function looks like in the illustration below:
The ease-in-out timing function (live).
So how can we get something like this?
Well, that's what harmonic functions are for! More exactly, the ease-in-out out shape is reminiscent the shape of the sin() function on the [-90°,90°] interval.
The sin(k) function on the [-90°,90°] interval (live).
However, we don't want a function whose input is in the [-90°,90°] interval and output is in the [-1,1] interval, so let's fix this!
This means we need to squish the hashed rectangle ([-90°,90°]x[-1,1]) in the illustration above into the unit one ([0,1]x[0,1]).
First, let's take the domain [-90°,90°]. If we change our function to be sin(k·180°) (or sin(k·π) in radians), then our domain becomes [-.5,.5] (we can check that -.5·180° = 90° and .5·180° = 90°):
The sin(k·π) function on the [-.5,.5] interval (live).
We can shift this domain to the right by .5 and get the desired [0,1] interval if we change our function to be sin((k - .5)·π) (we can check that 0 - .5 = -.5 and 1 - .5 = .5):
The sin((k - .5)·π) function on the [0,1] interval (live).
Now let's get the desired codomain. If we add 1 to our function making it sin((k - .5)·π) + 1 this shifts out codomain up into the [0, 2] interval:
The sin((k - .5)·π) + 1 function on the [0,1] interval (live).
Dividing everything by 2 gives us the (sin((k - .5)·π) + 1)/2 function and compacts the codomain into our desired [0,1] interval:
The (sin((k - .5)·π) + 1)/2 function on the [0,1] interval (live).
This turns out to be a good approximation of the ease-in-out timing function (represented with an orange dashed line in the illustration above).
Comparison of all these timing functions
Let's say we want to have a bunch of elements with a linear-gradient() (like in the third demo). On click, their --stop values go from 0% to 100%, but with a different timing function for each.
In the JavaScript, we create a timing functions object with the corresponding function for each type of easing:
tfn = {
'linear': function(k) {
return k;
},
'ease-in': function(k) {
return Math.pow(k, 1.675);
},
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675);
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1);
}
};
For each of these, we create an article element:
const _ART = [];

let frag = document.createDocumentFragment();

for(let p in tfn) {
let art = document.createElement('article'),
hd = document.createElement('h3');

hd.textContent = p;
art.appendChild(hd);
art.setAttribute('id', p);
_ART.push(art);
frag.appendChild(art);
}

n = _ART.length;
document.body.appendChild(frag);
The update function is pretty much the same, except we set the --stop custom property for every element as the value returned by the corresponding timing function when fed the current progress k. Also, when resetting the --stop to 0% at the end of the animation, we also need to do this for every element.
function update() {
let k = ++f/NF;

for(let i = 0; i < n; i++) {
_ART[i].style.setProperty(
'--stop',
`${+tfn[_ART[i].id](k).toFixed(5)*100}%`
);
}

if(!(f%NF)) {
f = 0;

S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);

for(let i = 0; i < n; i++)
_ART[i].style.setProperty('--stop', `0%`);

stopAni();
return;
}

rID = requestAnimationFrame(update)
};
This gives us a nice visual comparison of these timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
They all start and finish at the same time, but while the progress is constant for the linear one, the ease-in one starts slowly and then accelerates, the ease-out one starts fast and then slows down and, finally, the ease-in-out one starts slowly, accelerates and then slows down again at the end.
Timing functions for bouncing transitions
I first came across the concept years ago, in Lea Verou's CSS Secrets talk. These happen when the y (even) values in a cubic-bezier() function are outside the [0, 1] range and the effect they create is of the animated value going outside the interval between its initial and final value.
This bounce can happen right after the transition starts, right before it finishes or at both ends.
A bounce at the start means that, at first, we don't go towards the final state, but in the opposite direction. For example, if want to animate a stop from 43% to 57% and we have a bounce at the start, then, at first, out stop value doesn't increase towards 57%, but decreases below 43% before going back up to the final state. Similarly, if we go from an initial stop value of 57% to a final stop value of 43% and we have a bounce at the start, then, at first, the stop value increases above 57% before going down to the final value.
A bounce at the end means we overshoot our final state and only then go back to it. If want to animate a stop from 43% to 57% and we have a bounce at the end, then we start going normally from the initial state to the final one, but towards the end, we go above 57% before going back down to it. And if we go from an inital stop value of 57% to a final stop value of 43% and we have a bounce at the end, then, at first, we go down towards the final state, but, towards the end, we pass it and we briefly have stop values below 43% before our transition finishes there.
If what they do is still difficult to grasp, below there's a comparative example of all three of them in action.
The three cases.
These kinds of timing functions don't have their own keywords associated, but they look cool and they are what we want in a lot of situations.
Just like in the case of ease-in-out, the quickest way of getting them is by using harmonic functions. The difference lies in the fact that now we don't start from the [-90°,90°] domain anymore.
For a bounce at the beginning, we start with the [s, 0°] portion of the sin() function, where s (the start angle) is in the (-180°,-90°) interval. The closer it is to -180°, the bigger the bounce is and the faster it will go to the final state after it. So we don't want it to be really close to -180° because the result would look too unnatural. We also want it to be far enough from -90° that the bounce is noticeable.
In the interactive demo below, you can drag the slider to change the start angle and then click on the stripe at the bottom to see the effect in action:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,0]x[sin(s),0]) is the area we need move and scale into the [0,1]x[0,1] area in order to get our timing function. The part of the curve that's below its lower edge is where the bounce happens. You can adjust the start angle using the slider and then click on the bottom bar to see how the transition looks for different start angles.
Just like in the ease-in-out case, we first squish the domain into the [-1,0] interval by dividing the argument with the range (which is the maximum 0 minus the minimum s). Therefore, our function becomes sin(-k·s) (we can check that -(-1)·s = s and -0·s = 0):
The sin(-k·s) function on the [-1,0] interval (live).
Next, we shift this interval to the right (by 1, into [0,1]). This makes our function sin(-(k - 1)·s) = sin((1 - k)·s) (it checks that 0 - 1 = -1 and 1 - 1 = 0):
The sin(-(k - 1)·s) function on the [0,1] interval (live).
We then shift the codomain up by its value at 0 (sin((1 - 0)*s) = sin(s)). Our function is now sin((1 - k)·s) - sin(s) and our codomain [0,-sin(s)].
The sin(-(k - 1)·s) - sin(s) function on the [0,1] interval (live).
The last step is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is -sin(s)). This means our final easing function is 1 - sin((1 - k)·s)/sin(s)
The 1 - sin((1 - k)·s)/sin(s) function on the [0,1] interval (live).
For a bounce at the end, we start with the [0°, e] portion of the sin() function, where e (the end angle) is in the (90°,180°) interval. The closer it is to 180°, the bigger the bounce is and the faster it will move from the initial state to the final one before it overshoots it and the bounce happens. So we don't want it to be really close to 180° as the result would look too unnatural. We also want it to be far enough from 90° so that the bounce is noticeable.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([0,e]x[0,sin(e)]) is the area we need to squish and move into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's below its upper edge is where the bounce happens.
We start by squishing the domain into the [0,1] interval by dividing the argument with the range (which is the maximum e minus the minimum 0). Therefore, our function becomes sin(k·e) (we can check that 0·e = 0 and 1·e = e):
The sin(k·e) function on the [0,1] interval (live).
What's still left to do is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is sin(e)). This means our final easing function is sin(k·e)/sin(e).
The sin(k·e)/sin(e) function on the [0,1] interval (live).
If we want a bounce at each end, we start with the [s, e] portion of the sin() function, where s is in the (-180°,-90°) interval and e in the (90°,180°) interval. The larger s and e are in absolute values, the bigger the corresponding bounces are and the more of the total transition time is spent on them alone. On the other hand, the closer their absolute values get to 90°, the less noticeable their corresponding bounces are. So, just like in the previous two cases, it's all about finding the right balance.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,e]x[sin(s),sin(e)]) is the area we need to move and scale into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's beyond its horizontal edges is where the bounces happen.
We start by shifting the domain to the right into the [0,e - s] interval. This means our function becomes sin(k + s) (we can check that 0 + s = s and that e - s + s = e).
The sin(k + s) function on the [0,e - s] interval (live).
Then we shrink the domain to fit into the [0,1] interval, which gives us the function sin(k·(e - s) + s).
The sin(k·(e - s) + s) function on the [0,1] interval (live).
Moving on to the codomain, we first shift it up by its value at 0 (sin(0·(e - s) + s)), which means we now have sin(k·(e - s) + s) - sin(s). This gives us the new codomain [0,sin(e) - sin(s)].
The sin(k·(e - s) + s) - sin(s) function on the [0,1] interval (live).
Finally, we shrink the codomain to the [0,1] interval by dividing with the range (sin(e) - sin(s)), so our final function is (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)).
The (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)) function on the [0,1] interval (live).
So in order to do a similar comparative demo to that for the JS equivalents of the CSS linear, ease-in, ease-out, ease-in-out, our timing functions object becomes:
tfn = {
'bounce-ini': function(k) {
return 1 - Math.sin((1 - k)*s)/Math.sin(s);
},
'bounce-fin': function(k) {
return Math.sin(k*e)/Math.sin(e);
},
'bounce-ini-fin': function(k) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s));
}
};
The s and e variables are the values we get from the two range inputs that allow us to control the bounce amount.
The interactive demo below shows the visual comparison of these three types of timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alternating animations
In CSS, setting animation-direction to alternate also reverses the timing function. In order to better understand this, consider a .box element on which we animate its transform property such that we move to the right. This means our @keyframes look as follows:
@keyframes shift {
0%, 10% { transform: none }
90%, 100% { transform: translate(50vw) }
}
We use a custom timing function that allows us to have a bounce at the end and we make this animation alternate - that is, go from the final state (translate(50vw)) back to the initial state (no translation) for the even-numbered iterations (second, fourth and so on).
animation: shift 1s cubic-bezier(.5, 1, .75, 1.5) infinite alternate
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
One important thing to notice here is that, for the even-numbered iterations, our bounce doesn't happen at the end, but at the start - the timing function is reversed. Visually, this means it's reflected both horizontally and vertically with respect to the .5,.5 point.
The normal timing function (f, in red, with a bounce at the end) and the symmetrical reverse one (g, in purple, with a bounce at the start) (live)
In CSS, there is no way of having a different timing function other than the symmetrical one on going back if we are to use this set of keyframes and animation-direction: alternate. We can introduce the going back part into the keyframes and control the timing function for each stage of the animation, but that's outside the scope of this article.
When changing values with JavaScript in the fashion presented so far in this article, the same thing happens by default. Consider the case when we want to animate the stop of a linear-gradient() between an initial and a final position and we want to have a bounce at the end. This is pretty much the last example presented in the first section with timing function that lets us have a bounce at the end (one in the bounce-fin category described before) instead of a linear one.
The CSS is exactly the same and we only make a few minor changes to the JavaScript code. We set a limit angle E and we use a custom bounce-fin kind of timing function in place of the linear one:
const E = .75*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
};

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + timing(k)*RANGE).toFixed(2)}%`
);

/* same as before */
};

/* same as before */
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the initial state, the stop is at 85%. We animate it to 26% (which is the final state) using a timing function that gives us a bounce at the end. This means we go beyond our final stop position at 26% before going back up and stopping there. This is what happens during the odd iterations.
During the even iterations, this behaves just like in the CSS case, reversing the timing function, so that the bounce happens at the beginning, not at the end.
But what if we don't want the timing function to be reversed?
In this case, we need to use the symmetrical function. For any timing function f(k) defined on the [0,1] interval (this is the domain), whose values are in the [0,1] (codomain), the symmetrical function we want is 1 - f(1 - k). Note that functions whose shape is actually symmetrical with respect to the .5,.5 point, like linear or ease-in-out are identical to their symmetrical functions.
See the Pen by thebabydino (@thebabydino) on CodePen.
So what we do is use our timing function f(k) for the odd iterations and use 1 - f(1 - k) for the even ones. We can tell whether an iteration is odd or even from the direction (dir) variable. This is 1 for odd iterations and -1 for even ones.
This means we can combine our two timing functions into one: m + dir*f(m + dir*k).
Here, the multiplier m is 0 for the odd iterations (when dir is 1) and 1 for the even ones (when dir is -1), so we can compute it as .5*(1 - dir):
dir = +1 → m = .5*(1 - (+1)) = .5*(1 - 1) = .5*0 = 0
dir = -1 → m = .5*(1 - (-1)) = .5*(1 + 1) = .5*2 = 1
This way, our JavaScript becomes:
let m;

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + (m + dir*timing(m + dir*k))*RANGE).toFixed(2)}%`
);

/* same as before */
};

addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
The final result can be seen in this Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Even more examples
Gradient stops are not the only things that aren't animatable cross-browser with just CSS.
Gradient end going from orange to violet
For a first example of something different, let's say we want the orange in our gradient to animate to a kind of violet. We start with a CSS that looks something like this:
--c-ini: #ff9800;
--c-fin: #a048b9;
background: linear-gradient(90deg,
var(--c, var(--c-ini)), #3c3c3c)
In order to interpolate between the initial and final values, we need to know the format we get when reading them via JavaScript - is it going to be the same format we set them in? Is it going to be always rgb()/ rgba()?
Here is where things get a bit hairy. Consider the following test, where we have a gradient where we've used every format possible:
--c0: hsl(150, 100%, 50%); // springgreen
--c1: orange;
--c2: #8a2be2; // blueviolet
--c3: rgb(220, 20, 60); // crimson
--c4: rgba(255, 245, 238, 1); // seashell with alpha = 1
--c5: hsla(51, 100%, 50%, .5); // gold with alpha = .5
background: linear-gradient(90deg,
var(--c0), var(--c1),
var(--c2), var(--c3),
var(--c4), var(--c5))
We read the computed values of the gradient image and the individual custom properties --c0 through --c5 via JavaScript.
let s = getComputedStyle(document.body);

console.log(s.backgroundImage);
console.log(s.getPropertyValue('--c0'), 'springgreen');
console.log(s.getPropertyValue('--c1'), 'orange');
console.log(s.getPropertyValue('--c2'), 'blueviolet');
console.log(s.getPropertyValue('--c3'), 'crimson');
console.log(s.getPropertyValue('--c4'), 'seashell (alpha = 1)');
console.log(s.getPropertyValue('--c5'), 'gold (alpha = .5)');
The results seem a bit inconsistent.
Screenshots showing what gets logged in Chrome, Edge and Firefox (live).
Whatever we do, if we have an alpha of strictly less than 1, what we get via JavaScript seems to be always an rgba() value, regardless of whether we've set it with rgba() or hsla().
All browsers also agree when reading the custom properties directly, though, this time, what we get doesn't seem to make much sense: orange, crimson and seashell are returned as keywords regardless of how they were set, but we get hex values for springgreen and blueviolet. Except for orange, which was added in Level 2, all these values were added to CSS in Level 3, so why do we get some as keywords and others as hex values?
For the background-image, Firefox always returns the fully opaque values only as rgb(), while Chrome and Edge return them as either keywords or hex values, just like they do in the case when we read the custom properties directly.
Oh well, at least that lets us know we need to take into account different formats.
So the first thing we need to do is map the keywords to rgb() values. Not going to write all that manually, so a quick search finds this repo - perfect, it's exactly what we want! We can now set that as the value of a CMAP constant.
The next step here is to create a getRGBA(c) function that would take a string representing a keyword, a hex or an rgb()/ rgba() value and return an array containing the RGBA values ([red, green, blue, alpha]).
We start by building our regular expressions for the hex and rgb()/ rgba() values. These are a bit loose and would catch quite a few false positives if we were to have user input, but since we're only using them on CSS computed style values, we can afford to take the quick and dirty path here:
let re_hex = /^#([a-fd]{1,2})([a-fd]{1,2})([a-fd]{1,2})$/i,
re_rgb = /^rgba?((d{1,3},s){2}d{1,3}(,s((0|1)?.?d*))?)/;
Then we handle the three types of values we've seen we might get by reading the computed styles:
if(c in CMAP) return CMAP[c]; // keyword lookup, return rgb

if([4, 7].indexOf(c.length) !== -1 && re_hex.test(c)) {
c = c.match(re_hex).slice(1); // remove the '#'
if(c[0].length === 1) c = c.map(x => x + x);
// go from 3-digit form to 6-digit one
c.push(1); // add an alpha of 1

// return decimal valued RGBA array
return c.map(x => parseInt(x, 16))
}

if(re_rgb.test(c)) {
// extract values
c = c.replace(/rgba?(/, '').replace(')', '').split(',').map(x => +x.trim());
if(c.length === 3) c.push(1); // if no alpha specified, use 1

return c // return RGBA array
}
Now after adding the keyword to RGBA map (CMAP) and the getRGBA() function, our JavaScript code doesn't change much from the previous examples:
const INI = getRGBA(S.getPropertyValue('--c-ini').trim()),
FIN = getRGBA(S.getPropertyValue('--c-fin').trim()),
RANGE = [],
ALPHA = 1 - INI[3] || 1 - FIN[3];

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + k*RANGE[i])).join(',')})`
);

/* same as before */
};

(function init() {
if(!ALPHA) INI.pop(); // get rid of alpha if always 1
RANGE.splice(0, 0, ...INI.map((c, i) => FIN[i] - c));
})();

/* same as before */
This gives us a linear gradient animation:
See the Pen by thebabydino (@thebabydino) on CodePen.
We can also use a different, non-linear timing function, for example one that allows for a bounce at the end:
const E = .8*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
}

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + timing(k)*RANGE[i])).join(',')})`
);

/* same as before */
};

/* same as before */
This means we go all the way to a kind of blue before going back to our final violet:
See the Pen by thebabydino (@thebabydino) on CodePen.
Do note however that, in general, RGBA transitions are not the best place to illustrate bounces. That's because the RGB channels are strictly limited to the [0,255] range and the alpha channel is strictly limited to the [0,1] range. rgb(255, 0, 0) is as red as red gets, there's no redder red with a value of over 255 for the first channel. A value of 0 for the alpha channel means completely transparent, there's no greater transparency with a negative value.
By now, you're probably already bored with gradients, so let's switch to something else!
Smooth changing SVG attribute values
At this point, we cannot alter the geometry of SVG elements via CSS. We should be able to as per the SVG2 spec and Chrome does support some of this stuff, but what if we want to animate the geometry of SVG elements now, in a more cross-browser manner?
Well, you've probably guessed it, JavaScript to the rescue!
Growing a circle
Our first example is that of a circle whose radius goes from nothing (0) to a quarter of the minimum viewBox dimension. We keep the document structure simple, without any other aditional elements.
<svg viewBox='-100 -50 200 100'>
<circle/>
</svg>
For the JavaScript part, the only notable difference from the previous demos is that we read the SVG viewBox dimensions in order to get the maximum radius and we now set the r attribute within the update() function, not a CSS variable (it would be immensely useful if CSS variables were allowed as values for such attributes, but, sadly, we don't live in an ideal world):
const _G = document.querySelector('svg'),
_C = document.querySelector('circle'),
VB = _G.getAttribute('viewBox').split(' '),
RMAX = .25*Math.min(...VB.slice(2)),
E = .8*Math.PI;

/* same as before */

function update() {
/* same as before */

_C.setAttribute('r', (timing(k)*RMAX).toFixed(2));

/* same as before */
};

/* same as before */
Below, you can see the result when using a bounce-fin kind of timing function:
See the Pen by thebabydino (@thebabydino) on CodePen.
Pan and zoom map
Another SVG example is a smooth pan and zoom map demo. In this case, we take a map like those from amCharts, clean up the SVG and then create this effect by triggering a linear viewBox animation when pressing the +/ - keys (zoom) and the arrow keys (pan).
The first thing we do in the JavaScript is create a navigation map, where we take the key codes of interest and attach info about what we do when the corresponding keys are pressed (note that we need different key codes for + and - in Firefox for some reason).
const NAV_MAP = {
187: { dir: 1, act: 'zoom', name: 'in' } /* + */,
61: { dir: 1, act: 'zoom', name: 'in' } /* + Firefox ¯_(ツ)_/¯ */,
189: { dir: -1, act: 'zoom', name: 'out' } /* - */,
173: { dir: -1, act: 'zoom', name: 'out' } /* - Firefox ¯_(ツ)_/¯ */,
37: { dir: -1, act: 'move', name: 'left', axis: 0 } /* ⇦ */,
38: { dir: -1, act: 'move', name: 'up', axis: 1 } /* ⇧ */,
39: { dir: 1, act: 'move', name: 'right', axis: 0 } /* ⇨ */,
40: { dir: 1, act: 'move', name: 'down', axis: 1 } /* ⇩ */
}
When pressing the + key, what we want to do is zoom in. The action we perform is 'zoom' in the positive direction - we go 'in'. Similarly, when pressing the - key, the action is also 'zoom', but in the negative (-1) direction - we go 'out'.
When pressing the arrow left key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the negative (-1) direction - we go 'left'. When pressing the arrow up key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the negative (-1) direction - we go 'up'.
When pressing the arrow right key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the positive direction - we go 'right'. When pressing the arrow down key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the positive direction - we go 'down'.
We then get the SVG element, its initial viewBox, set the maximum zoom out level to these initial viewBox dimensions and set the smallest possible viewBox width to a much smaller value (let's say 8).
const _SVG = document.querySelector('svg'),
VB = _SVG.getAttribute('viewBox').split(' ').map(c => +c),
DMAX = VB.slice(2), WMIN = 8;
We also create an empty current navigation object to hold the current navigation action data and a target viewBox array to contain the final state we animate the viewBox to for the current animation.
let nav = {}, tg = Array(4);
On 'keyup', if we don't have any animation running already and the key that was pressed is one of interest, we get the current navigation object from the navigation map we created at the beginning. After this, we handle the two action cases ('zoom'/ 'move') and call the update() function:
addEventListener('keyup', e => {
if(!rID && e.keyCode in NAV_MAP) {
nav = NAV_MAP[e.keyCode];

if(nav.act === 'zoom') {
/* what we do if the action is 'zoom' */
}

else if(nav.act === 'move') {
/* what we do if the action is 'move' */
}

update()
}
}, false);
Now let's see what we do if we zoom. First off, and this is a very useful programming tactic in general, not just here in particular, we get the edge cases that make us exit the function out of the way.
So what are our edge cases here?
The first one is when we want to zoom out (a zoom in the negative direction) when our whole map is already in sight (the current viewBox dimensions are bigger or equal to the maximum ones). In our case, this should happen if we want to zoom out at the very beginning because we start with the whole map in sight.
The second edge case is when we hit the other limit - we want to zoom in, but we're at the maximum detail level (the current viewBox dimensions are smaller or equal to the minimum ones).
Putting the above into JavaScript code, we have:
if(nav.act === 'zoom') {
if((nav.dir === -1 && VB[2] >= DMAX[0]) ||
(nav.dir === 1 && VB[2] <= WMIN)) {
console.log(`cannot ${nav.act} ${nav.name} more`);
return
}

/* main case */
}
Now that we've handled the edge cases, let's move on to the main case. Here, we set the target viewBox values. We use a 2x zoom on each step, meaning that when we zoom in, the target viewBox dimensions are half the ones at the start of the current zoom action, and when we zoom out they're double. The target offsets are half the difference between the maximum viewBox dimensions and the target ones.
if(nav.act === 'zoom') {
/* edge cases */

for(let i = 0; i < 2; i++) {
tg[i + 2] = VB[i + 2]/Math.pow(2, nav.dir);
tg[i] = .5*(DMAX[i] - tg[i + 2]);
}
}
Next, let's see what we do if we want to move instead of zooming.
In a similar fashion, we get the edge cases that make us exit the function out of the way first. Here, these happen when we're at an edge of the map and we want to keep going in that direction (whatever the direction might be). Since originally the top left corner of our viewBox is at 0,0, this means we cannot go below 0 or above the maximum viewBox size minus the current one. Note that given we're initially fully zoomed out, this also means we cannot move in any direction until we zoom in.
else if(nav.act === 'move') {
if((nav.dir === -1 && VB[nav.axis] <= 0) ||
(nav.dir === 1 && VB[nav.axis] >= DMAX[nav.axis] - VB[2 + nav.axis])) {
console.log(`at the edge, cannot go ${nav.name}`);
return
}

/* main case */
For the main case, we move in the desired direction by half the viewBox size along that axis:
else if(nav.act === 'move') {
/* edge cases */

tg[nav.axis] = VB[nav.axis] + .5*nav.dir*VB[2 + nav.axis]
}
Now let's see what we need to do inside the update() function. This is going to be pretty similar to previous demos, except now we need to handle the 'move' and 'zoom' cases separately. We also create an array to store the current viewBox data in (cvb):
function update() {
let k = ++f/NF, j = 1 - k, cvb = VB.slice();

if(nav.act === 'zoom') {
/* what we do if the action is zoom */
}

if(nav.act === 'move') {
/* what we do if the action is move */
}

_SVG.setAttribute('viewBox', cvb.join(' '));

if(!(f%NF)) {
f = 0;
VB.splice(0, 4, ...cvb);
nav = {};
tg = Array(4);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
In the 'zoom' case, we need to recompute all viewBox values. We do this with linear interpolation between the values at the start of the animation and the target values we've previously computed:
if(nav.act === 'zoom') {
for(let i = 0; i < 4; i++)
cvb[i] = j*VB[i] + k*tg[i];
}
In the 'move' case, we only need to recompute one viewBox value - the offset for the axis we move along:
if(nav.act === 'move')
cvb[nav.axis] = j*VB[nav.axis] + k*tg[nav.axis];
And that's it! We now have a working pan and zoom demo with smooth linear transitions in between states:
See the Pen by thebabydino (@thebabydino) on CodePen.
From sad square to happy circle
Another example would be morphing a sad square SVG into a happy circle. We create an SVG with a square viewBox whose 0,0 point is right in the middle. Symmetrical with respect to the origin of the SVG system of coordinates we have a square (a rect element) covering 80% of the SVG. This is our face. We create the eyes with an ellipse and a copy of it, symmetrical with respect to the vertical axis. The mouth is a cubic Bézier curve created with a path element.
- var vb_d = 500, vb_o = -.5*vb_d;
- var fd = .8*vb_d, fr = .5*fd;

svg(viewBox=[vb_o, vb_o, vb_d, vb_d].join(' '))
rect(x=-fr y=-fr width=fd height=fd)
ellipse#eye(cx=.35*fr cy=-.25*fr
rx=.1*fr ry=.15*fr)
use(xlink:href='#eye'
transform='scale(-1 1)')
path(d=`M${-.35*fr} ${.35*fr}
C${-.21*fr} ${.13*fr}
${+.21*fr} ${.13*fr}
${+.35*fr} ${.35*fr}`)
In the JavaScript, we get the face and the mouth elements. We read the face width, which is equal to the height and we use it to compute the maximum corner rounding. This is the value for which we get a circle and is equal to half the square edge. We also get the mouth path data, from where we extract the initial y coordinate of the control points and compute the final y coordinate of the same control points.
const _FACE = document.querySelector('rect'),
_MOUTH = document.querySelector('path'),
RMAX = .5*_FACE.getAttribute('width'),
DATA = _MOUTH.getAttribute('d').slice(1)
.replace('C', '').split(/s+/)
.map(c => +c),
CPY_INI = DATA[3],
CPY_RANGE = 2*(DATA[1] - DATA[3]);
The rest is very similar to all other transition on click demos so far, with just a few minor differences (note that we use an ease-out kind of timing function):
/* same as before */

function timing(k) { return 1 - Math.pow(1 - k, 2) };

function update() {
f += dir;

let k = f/NF, cpy = CPY_INI + timing(k)*CPY_RANGE;

_FACE.setAttribute('rx', (timing(k)*RMAX).toFixed(2));
_MOUTH.setAttribute(
'd',
`M${DATA.slice(0,2)}
C${DATA[2]} ${cpy} ${DATA[4]} ${cpy} ${DATA.slice(-2)}`
);

/* same as before */
};

/* same as before */
And so we have our silly result:
See the Pen by thebabydino (@thebabydino) on CodePen.

Emulating CSS Timing Functions with JavaScript is a post from CSS-Tricks
Source: CssTricks


Emulating CSS Timing Functions with JavaScript

CSS animations and transitions are great! However, while recently toying with an idea, I got really frustrated with the fact that gradients are only animatable in Edge (and IE 10+). Yes, we can do all sorts of tricks with background-position, background-size, background-blend-mode or even opacity and transform on a pseudo-element/ child, but sometimes these are just not enough. Not to mention that we run into similar problems when wanting to animate SVG attributes without a CSS correspondent.
Using a lot of examples, this article is going to explain how to smoothly go from one state to another in a similar fashion to that of common CSS timing functions using just a little bit of JavaScript, without having to rely on a library, so without including a lot of complicated and unnecessary code that may become a big burden in the future.

This is not how the CSS timing functions work. This is an approach that I find simpler and more intuitive than working with Bézier curves. I'm going to show how to experiment with different timing functions using JavaScript and dissect use cases. It is not a tutorial on how to do beautiful animation.
A few examples using a linear timing function
Let's start with a left to right linear-gradient() with a sharp transition where we want to animate the first stop. Here's a way to express that using CSS custom properties:
background: linear-gradient(90deg, #ff9800 var(--stop, 0%), #3c3c3c 0);
On click, we want the value of this stop to go from 0% to 100% (or the other way around, depending on the state it's already in) over the course of NF frames. If an animation is already running at the time of the click, we stop it, change its direction, then restart it.
We also need a few variables such as the request ID (this gets returned by requestAnimationFrame), the index of the current frame (an integer in the [0, NF] interval, starting at 0) and the direction our transition is going in (which is 1 when going towards 100% and -1 when going towards 0%).
While nothing is changing, the request ID is null. We also set the current frame index to 0 initially and the direction to -1, as if we've just arrived to 0% from 100%.
const NF = 80; // number of frames transition happens over

let rID = null, f = 0, dir = -1;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {};

addEventListener('click', e => {
if(rID) stopAni(); // if an animation is already running, stop it
dir *= -1; // change animation direction
update();
}, false);
Now all that's left is to populate the update() function. Within it, we update the current frame index f. Then we compute a progress variable k as the ratio between this current frame index f and the total number of frames NF. Given that f goes from 0 to NF (included), this means that our progress k goes from 0 to 1. Multiply this with 100% and we get the desired stop.
After this, we check whether we've reached one of the end states. If we have, we stop the animation and exit the update() function.
function update() {
f += dir; // update current frame index

let k = f/NF; // compute progress

document.body.style.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
The result can be seen in the Pen below (note that we go back on a second click):
See the Pen by thebabydino (@thebabydino) on CodePen.
The way the pseudo-element is made to contrast with the background below is explained in an older article.
The above demo may look like something we could easily achieve with an element and translating a pseudo-element that can fully cover it, but things get a lot more interesting if we give the background-size a value that's smaller than 100% along the x axis, let's say 5em:
See the Pen by thebabydino (@thebabydino) on CodePen.
This gives us a sort of a "vertical blinds" effect that cannot be replicated in a clean manner with just CSS if we don't want to use more than one element.
Another option would be not to alternate the direction and always sweep from left to right, except only odd sweeps would be orange. This requires tweaking the CSS a bit:
--c0: #ff9800;
--c1: #3c3c3c;
background: linear-gradient(90deg,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
In the JavaScript, we ditch the direction variable and add a type one (typ) that switches between 0 and 1 at the end of every transition. That's when we also update all custom properties:
const S = document.body.style;

let typ = 0;

function update() {
let k = ++f/NF;

S.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
f = 0;
S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);
S.setProperty('--stop', `0%`);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
This gives us the desired result (click at least twice to see how the effect differs from that in the first demo):
See the Pen by thebabydino (@thebabydino) on CodePen.
We could also change the gradient angle instead of the stop. In this case, the background rule becomes:
background: linear-gradient(var(--angle, 0deg),
#ff9800 50%, #3c3c3c 0);
In the JavaScript code, we tweak the update() function:
function update() {
f += dir;

let k = f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
We now have a gradient angle transition in between the two states (0deg and 180deg):
See the Pen by thebabydino (@thebabydino) on CodePen.
In this case, we might also want to keep going clockwise to get back to the 0deg state instead of changing the direction. So we just ditch the dir variable altogether, discard any clicks happening during the transition, and always increment the frame index f, resetting it to 0 when we've completed a full rotation around the circle:
function update() {
let k = ++f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
f = f%(2*NF);
stopAni();
return
}

rID = requestAnimationFrame(update)
};

addEventListener('click', e => {
if(!rID) update()
}, false);
The following Pen illustrates the result - our rotation is now always clockwise:
See the Pen by thebabydino (@thebabydino) on CodePen.
Something else we could do is use a radial-gradient() and animate the radial stop:
background: radial-gradient(circle,
#ff9800 var(--stop, 0%), #3c3c3c 0);
The JavaScript code is identical to that of the first demo and the result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
We may also not want to go back when clicking again, but instead make another blob grow and cover the entire viewport. In this case, we add a few more custom properties to the CSS:
--c0: #ff9800;
--c1: #3c3c3c;
background: radial-gradient(circle,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
The JavaScript is the same as in the case of the third linear-gradient() demo. This gives us the result we want:
See the Pen by thebabydino (@thebabydino) on CodePen.
A fun tweak to this would be to make our circle start growing from the point we clicked. To do so, we introduce two more custom properties, --x and --y:
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
When clicking, we set these to the coordinates of the point where the click happened:
addEventListener('click', e => {
if(!rID) {
S.setProperty('--x', `${e.clientX}px`);
S.setProperty('--y', `${e.clientY}px`);
update();
}
}, false);
This gives us the following result where we have a disc growing from the point where we clicked:
See the Pen by thebabydino (@thebabydino) on CodePen.
Another option would be using a conic-gradient() and animating the angular stop:
background: conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0%)
Note that in the case of conic-gradient(), we must use a unit for the zero value (whether that unit is % or an angular one like deg doesn't matter), otherwise our code won't work - writing conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0) means nothing gets displayed.
The JavaScript is the same as for animating the stop in the linear or radial case, but bear in mind that this currently only works in Chrome with Experimental Web Platform Features enabled in chrome://flags.
The Experimental Web Platform Features flag enabled in Chrome Canary (63.0.3210.0).
Just for the purpose of displaying conic gradients in the browser, there's a polyfill by Lea Verou and this works cross-browser but doesn't allow using CSS custom properties.
The recording below illustrates how our code works:
Recording of how our first conic-gradient() demo works in Chrome with the flag enabled (live demo).
This is another situation where we might not want to go back on a second click. This means we need to alter the CSS a bit, in the same way we did for the last radial-gradient() demo:
--c0: #ff9800;
--c1: #3c3c3c;
background: conic-gradient(
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0%)
The JavaScript code is exactly the same as in the corresponding linear-gradient() or radial-gradient() case and the result can be seen below:
Recording of how our second conic-gradient() demo works in Chrome with the flag enabled (live demo).
Before we move on to other timing functions, there's one more thing to cover: the case when we don't go from 0% to 100%, but in between any two values. We take the example of our first linear-gradient, but with a different default for --stop, let's say 85% and we also set a --stop-fin value - this is going to be the final value for --stop:
--stop-ini: 85%;
--stop-fin: 26%;
background: linear-gradient(90deg, #ff9800 var(--stop, var(--stop-ini)), #3c3c3c 0)
In the JavaScript, we read these two values - the initial (default) and the final one - and we compute a range as the difference between them:
const S = getComputedStyle(document.body),
INI = +S.getPropertyValue('--stop-ini').replace('%', ''),
FIN = +S.getPropertyValue('--stop-fin').replace('%', ''),
RANGE = FIN - INI;
Finally, in the update() function, we take into account the initial value and the range when setting the current value for --stop:
document.body.style.setProperty(
'--stop',
`${+(INI + k*RANGE).toFixed(2)}%`
);
With these changes we now have a transition in between 85% and 26% (and the other way on even clicks):
See the Pen by thebabydino (@thebabydino) on CodePen.
If we want to mix units for the stop value, things get hairier as we need to compute more things (box dimensions when mixing % and px, font sizes if we throw em or rem in the mix, viewport dimensions if we want to use viewport units, the length of the 0% to 100% segment on the gradient line for gradients that are not horizontal or vertical), but the basic idea remains the same.
Emulating ease-in/ ease-out
An ease-in kind of function means the change in value happens slow at first and then accelerates. ease-out is exactly the opposite - the change happens fast in the beginning, but then slows down towards the end.
The ease-in (left) and ease-out (right) timing functions (live).
The slope of the curves above gives us the rate of change. The steeper it is, the faster the change in value happens.
We can emulate these functions by tweaking the linear method described in the first section. Since k takes values in the [0, 1] interval, raising it to any positive power also gives us a number within the same interval. The interactive demo below shows the graph of a function f(k) = pow(k, p) (k raised to an exponent p) shown in purple and that of a function g(k) = 1 - pow(1 - k, p) shown in red on the [0, 1] interval versus the identity function id(k) = k (which corresponds to a linear timing function).
See the Pen by thebabydino (@thebabydino) on CodePen.
When the exponent p is equal to 1, the graphs of the f and g functions are identical to that of the identity function.
When exponent p is greater than 1, the graph of the f function is below the identity line - the rate of change increases as k increases. This is like an ease-in type of function. The graph of the g function is above the identity line - the rate of change decreases as k increases. This is like an ease-out type of function.
It seems an exponent p of about 2 gives us an f that's pretty similar to ease-in, while g is pretty similar to ease-out. With a bit more tweaking, it looks like the best approximation is for a p value of about 1.675:
See the Pen by thebabydino (@thebabydino) on CodePen.
In this interactive demo, we want the graphs of the f and g functions to be as close as possible to the dashed lines, which represent the ease-in timing function (below the identity line) and the ease-out timing function (above the identity line).
Emulating ease-in-out
The CSS ease-in-out timing function looks like in the illustration below:
The ease-in-out timing function (live).
So how can we get something like this?
Well, that's what harmonic functions are for! More exactly, the ease-in-out out shape is reminiscent the shape of the sin() function on the [-90°,90°] interval.
The sin(k) function on the [-90°,90°] interval (live).
However, we don't want a function whose input is in the [-90°,90°] interval and output is in the [-1,1] interval, so let's fix this!
This means we need to squish the hashed rectangle ([-90°,90°]x[-1,1]) in the illustration above into the unit one ([0,1]x[0,1]).
First, let's take the domain [-90°,90°]. If we change our function to be sin(k·180°) (or sin(k·π) in radians), then our domain becomes [-.5,.5] (we can check that -.5·180° = 90° and .5·180° = 90°):
The sin(k·π) function on the [-.5,.5] interval (live).
We can shift this domain to the right by .5 and get the desired [0,1] interval if we change our function to be sin((k - .5)·π) (we can check that 0 - .5 = -.5 and 1 - .5 = .5):
The sin((k - .5)·π) function on the [0,1] interval (live).
Now let's get the desired codomain. If we add 1 to our function making it sin((k - .5)·π) + 1 this shifts out codomain up into the [0, 2] interval:
The sin((k - .5)·π) + 1 function on the [0,1] interval (live).
Dividing everything by 2 gives us the (sin((k - .5)·π) + 1)/2 function and compacts the codomain into our desired [0,1] interval:
The (sin((k - .5)·π) + 1)/2 function on the [0,1] interval (live).
This turns out to be a good approximation of the ease-in-out timing function (represented with an orange dashed line in the illustration above).
Comparison of all these timing functions
Let's say we want to have a bunch of elements with a linear-gradient() (like in the third demo). On click, their --stop values go from 0% to 100%, but with a different timing function for each.
In the JavaScript, we create a timing functions object with the corresponding function for each type of easing:
tfn = {
'linear': function(k) {
return k;
},
'ease-in': function(k) {
return Math.pow(k, 1.675);
},
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675);
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1);
}
};
For each of these, we create an article element:
const _ART = [];

let frag = document.createDocumentFragment();

for(let p in tfn) {
let art = document.createElement('article'),
hd = document.createElement('h3');

hd.textContent = p;
art.appendChild(hd);
art.setAttribute('id', p);
_ART.push(art);
frag.appendChild(art);
}

n = _ART.length;
document.body.appendChild(frag);
The update function is pretty much the same, except we set the --stop custom property for every element as the value returned by the corresponding timing function when fed the current progress k. Also, when resetting the --stop to 0% at the end of the animation, we also need to do this for every element.
function update() {
let k = ++f/NF;

for(let i = 0; i < n; i++) {
_ART[i].style.setProperty(
'--stop',
`${+tfn[_ART[i].id](k).toFixed(5)*100}%`
);
}

if(!(f%NF)) {
f = 0;

S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);

for(let i = 0; i < n; i++)
_ART[i].style.setProperty('--stop', `0%`);

stopAni();
return;
}

rID = requestAnimationFrame(update)
};
This gives us a nice visual comparison of these timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
They all start and finish at the same time, but while the progress is constant for the linear one, the ease-in one starts slowly and then accelerates, the ease-out one starts fast and then slows down and, finally, the ease-in-out one starts slowly, accelerates and then slows down again at the end.
Timing functions for bouncing transitions
I first came across the concept years ago, in Lea Verou's CSS Secrets talk. These happen when the y (even) values in a cubic-bezier() function are outside the [0, 1] range and the effect they create is of the animated value going outside the interval between its initial and final value.
This bounce can happen right after the transition starts, right before it finishes or at both ends.
A bounce at the start means that, at first, we don't go towards the final state, but in the opposite direction. For example, if want to animate a stop from 43% to 57% and we have a bounce at the start, then, at first, out stop value doesn't increase towards 57%, but decreases below 43% before going back up to the final state. Similarly, if we go from an initial stop value of 57% to a final stop value of 43% and we have a bounce at the start, then, at first, the stop value increases above 57% before going down to the final value.
A bounce at the end means we overshoot our final state and only then go back to it. If want to animate a stop from 43% to 57% and we have a bounce at the end, then we start going normally from the initial state to the final one, but towards the end, we go above 57% before going back down to it. And if we go from an inital stop value of 57% to a final stop value of 43% and we have a bounce at the end, then, at first, we go down towards the final state, but, towards the end, we pass it and we briefly have stop values below 43% before our transition finishes there.
If what they do is still difficult to grasp, below there's a comparative example of all three of them in action.
The three cases.
These kinds of timing functions don't have their own keywords associated, but they look cool and they are what we want in a lot of situations.
Just like in the case of ease-in-out, the quickest way of getting them is by using harmonic functions. The difference lies in the fact that now we don't start from the [-90°,90°] domain anymore.
For a bounce at the beginning, we start with the [s, 0°] portion of the sin() function, where s (the start angle) is in the (-180°,-90°) interval. The closer it is to -180°, the bigger the bounce is and the faster it will go to the final state after it. So we don't want it to be really close to -180° because the result would look too unnatural. We also want it to be far enough from -90° that the bounce is noticeable.
In the interactive demo below, you can drag the slider to change the start angle and then click on the stripe at the bottom to see the effect in action:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,0]x[sin(s),0]) is the area we need move and scale into the [0,1]x[0,1] area in order to get our timing function. The part of the curve that's below its lower edge is where the bounce happens. You can adjust the start angle using the slider and then click on the bottom bar to see how the transition looks for different start angles.
Just like in the ease-in-out case, we first squish the domain into the [-1,0] interval by dividing the argument with the range (which is the maximum 0 minus the minimum s). Therefore, our function becomes sin(-k·s) (we can check that -(-1)·s = s and -0·s = 0):
The sin(-k·s) function on the [-1,0] interval (live).
Next, we shift this interval to the right (by 1, into [0,1]). This makes our function sin(-(k - 1)·s) = sin((1 - k)·s) (it checks that 0 - 1 = -1 and 1 - 1 = 0):
The sin(-(k - 1)·s) function on the [0,1] interval (live).
We then shift the codomain up by its value at 0 (sin((1 - 0)*s) = sin(s)). Our function is now sin((1 - k)·s) - sin(s) and our codomain [0,-sin(s)].
The sin(-(k - 1)·s) - sin(s) function on the [0,1] interval (live).
The last step is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is -sin(s)). This means our final easing function is 1 - sin((1 - k)·s)/sin(s)
The 1 - sin((1 - k)·s)/sin(s) function on the [0,1] interval (live).
For a bounce at the end, we start with the [0°, e] portion of the sin() function, where e (the end angle) is in the (90°,180°) interval. The closer it is to 180°, the bigger the bounce is and the faster it will move from the initial state to the final one before it overshoots it and the bounce happens. So we don't want it to be really close to 180° as the result would look too unnatural. We also want it to be far enough from 90° so that the bounce is noticeable.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([0,e]x[0,sin(e)]) is the area we need to squish and move into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's below its upper edge is where the bounce happens.
We start by squishing the domain into the [0,1] interval by dividing the argument with the range (which is the maximum e minus the minimum 0). Therefore, our function becomes sin(k·e) (we can check that 0·e = 0 and 1·e = e):
The sin(k·e) function on the [0,1] interval (live).
What's still left to do is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is sin(e)). This means our final easing function is sin(k·e)/sin(e).
The sin(k·e)/sin(e) function on the [0,1] interval (live).
If we want a bounce at each end, we start with the [s, e] portion of the sin() function, where s is in the (-180°,-90°) interval and e in the (90°,180°) interval. The larger s and e are in absolute values, the bigger the corresponding bounces are and the more of the total transition time is spent on them alone. On the other hand, the closer their absolute values get to 90°, the less noticeable their corresponding bounces are. So, just like in the previous two cases, it's all about finding the right balance.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,e]x[sin(s),sin(e)]) is the area we need to move and scale into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's beyond its horizontal edges is where the bounces happen.
We start by shifting the domain to the right into the [0,e - s] interval. This means our function becomes sin(k + s) (we can check that 0 + s = s and that e - s + s = e).
The sin(k + s) function on the [0,e - s] interval (live).
Then we shrink the domain to fit into the [0,1] interval, which gives us the function sin(k·(e - s) + s).
The sin(k·(e - s) + s) function on the [0,1] interval (live).
Moving on to the codomain, we first shift it up by its value at 0 (sin(0·(e - s) + s)), which means we now have sin(k·(e - s) + s) - sin(s). This gives us the new codomain [0,sin(e) - sin(s)].
The sin(k·(e - s) + s) - sin(s) function on the [0,1] interval (live).
Finally, we shrink the codomain to the [0,1] interval by dividing with the range (sin(e) - sin(s)), so our final function is (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)).
The (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)) function on the [0,1] interval (live).
So in order to do a similar comparative demo to that for the JS equivalents of the CSS linear, ease-in, ease-out, ease-in-out, our timing functions object becomes:
tfn = {
'bounce-ini': function(k) {
return 1 - Math.sin((1 - k)*s)/Math.sin(s);
},
'bounce-fin': function(k) {
return Math.sin(k*e)/Math.sin(e);
},
'bounce-ini-fin': function(k) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s));
}
};
The s and e variables are the values we get from the two range inputs that allow us to control the bounce amount.
The interactive demo below shows the visual comparison of these three types of timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alternating animations
In CSS, setting animation-direction to alternate also reverses the timing function. In order to better understand this, consider a .box element on which we animate its transform property such that we move to the right. This means our @keyframes look as follows:
@keyframes shift {
0%, 10% { transform: none }
90%, 100% { transform: translate(50vw) }
}
We use a custom timing function that allows us to have a bounce at the end and we make this animation alternate - that is, go from the final state (translate(50vw)) back to the initial state (no translation) for the even-numbered iterations (second, fourth and so on).
animation: shift 1s cubic-bezier(.5, 1, .75, 1.5) infinite alternate
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
One important thing to notice here is that, for the even-numbered iterations, our bounce doesn't happen at the end, but at the start - the timing function is reversed. Visually, this means it's reflected both horizontally and vertically with respect to the .5,.5 point.
The normal timing function (f, in red, with a bounce at the end) and the symmetrical reverse one (g, in purple, with a bounce at the start) (live)
In CSS, there is no way of having a different timing function other than the symmetrical one on going back if we are to use this set of keyframes and animation-direction: alternate. We can introduce the going back part into the keyframes and control the timing function for each stage of the animation, but that's outside the scope of this article.
When changing values with JavaScript in the fashion presented so far in this article, the same thing happens by default. Consider the case when we want to animate the stop of a linear-gradient() between an initial and a final position and we want to have a bounce at the end. This is pretty much the last example presented in the first section with timing function that lets us have a bounce at the end (one in the bounce-fin category described before) instead of a linear one.
The CSS is exactly the same and we only make a few minor changes to the JavaScript code. We set a limit angle E and we use a custom bounce-fin kind of timing function in place of the linear one:
const E = .75*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
};

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + timing(k)*RANGE).toFixed(2)}%`
);

/* same as before */
};

/* same as before */
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the initial state, the stop is at 85%. We animate it to 26% (which is the final state) using a timing function that gives us a bounce at the end. This means we go beyond our final stop position at 26% before going back up and stopping there. This is what happens during the odd iterations.
During the even iterations, this behaves just like in the CSS case, reversing the timing function, so that the bounce happens at the beginning, not at the end.
But what if we don't want the timing function to be reversed?
In this case, we need to use the symmetrical function. For any timing function f(k) defined on the [0,1] interval (this is the domain), whose values are in the [0,1] (codomain), the symmetrical function we want is 1 - f(1 - k). Note that functions whose shape is actually symmetrical with respect to the .5,.5 point, like linear or ease-in-out are identical to their symmetrical functions.
See the Pen by thebabydino (@thebabydino) on CodePen.
So what we do is use our timing function f(k) for the odd iterations and use 1 - f(1 - k) for the even ones. We can tell whether an iteration is odd or even from the direction (dir) variable. This is 1 for odd iterations and -1 for even ones.
This means we can combine our two timing functions into one: m + dir*f(m + dir*k).
Here, the multiplier m is 0 for the odd iterations (when dir is 1) and 1 for the even ones (when dir is -1), so we can compute it as .5*(1 - dir):
dir = +1 → m = .5*(1 - (+1)) = .5*(1 - 1) = .5*0 = 0
dir = -1 → m = .5*(1 - (-1)) = .5*(1 + 1) = .5*2 = 1
This way, our JavaScript becomes:
let m;

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + (m + dir*timing(m + dir*k))*RANGE).toFixed(2)}%`
);

/* same as before */
};

addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
The final result can be seen in this Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Even more examples
Gradient stops are not the only things that aren't animatable cross-browser with just CSS.
Gradient end going from orange to violet
For a first example of something different, let's say we want the orange in our gradient to animate to a kind of violet. We start with a CSS that looks something like this:
--c-ini: #ff9800;
--c-fin: #a048b9;
background: linear-gradient(90deg,
var(--c, var(--c-ini)), #3c3c3c)
In order to interpolate between the initial and final values, we need to know the format we get when reading them via JavaScript - is it going to be the same format we set them in? Is it going to be always rgb()/ rgba()?
Here is where things get a bit hairy. Consider the following test, where we have a gradient where we've used every format possible:
--c0: hsl(150, 100%, 50%); // springgreen
--c1: orange;
--c2: #8a2be2; // blueviolet
--c3: rgb(220, 20, 60); // crimson
--c4: rgba(255, 245, 238, 1); // seashell with alpha = 1
--c5: hsla(51, 100%, 50%, .5); // gold with alpha = .5
background: linear-gradient(90deg,
var(--c0), var(--c1),
var(--c2), var(--c3),
var(--c4), var(--c5))
We read the computed values of the gradient image and the individual custom properties --c0 through --c5 via JavaScript.
let s = getComputedStyle(document.body);

console.log(s.backgroundImage);
console.log(s.getPropertyValue('--c0'), 'springgreen');
console.log(s.getPropertyValue('--c1'), 'orange');
console.log(s.getPropertyValue('--c2'), 'blueviolet');
console.log(s.getPropertyValue('--c3'), 'crimson');
console.log(s.getPropertyValue('--c4'), 'seashell (alpha = 1)');
console.log(s.getPropertyValue('--c5'), 'gold (alpha = .5)');
The results seem a bit inconsistent.
Screenshots showing what gets logged in Chrome, Edge and Firefox (live).
Whatever we do, if we have an alpha of strictly less than 1, what we get via JavaScript seems to be always an rgba() value, regardless of whether we've set it with rgba() or hsla().
All browsers also agree when reading the custom properties directly, though, this time, what we get doesn't seem to make much sense: orange, crimson and seashell are returned as keywords regardless of how they were set, but we get hex values for springgreen and blueviolet. Except for orange, which was added in Level 2, all these values were added to CSS in Level 3, so why do we get some as keywords and others as hex values?
For the background-image, Firefox always returns the fully opaque values only as rgb(), while Chrome and Edge return them as either keywords or hex values, just like they do in the case when we read the custom properties directly.
Oh well, at least that lets us know we need to take into account different formats.
So the first thing we need to do is map the keywords to rgb() values. Not going to write all that manually, so a quick search finds this repo - perfect, it's exactly what we want! We can now set that as the value of a CMAP constant.
The next step here is to create a getRGBA(c) function that would take a string representing a keyword, a hex or an rgb()/ rgba() value and return an array containing the RGBA values ([red, green, blue, alpha]).
We start by building our regular expressions for the hex and rgb()/ rgba() values. These are a bit loose and would catch quite a few false positives if we were to have user input, but since we're only using them on CSS computed style values, we can afford to take the quick and dirty path here:
let re_hex = /^#([a-fd]{1,2})([a-fd]{1,2})([a-fd]{1,2})$/i,
re_rgb = /^rgba?((d{1,3},s){2}d{1,3}(,s((0|1)?.?d*))?)/;
Then we handle the three types of values we've seen we might get by reading the computed styles:
if(c in CMAP) return CMAP[c]; // keyword lookup, return rgb

if([4, 7].indexOf(c.length) !== -1 && re_hex.test(c)) {
c = c.match(re_hex).slice(1); // remove the '#'
if(c[0].length === 1) c = c.map(x => x + x);
// go from 3-digit form to 6-digit one
c.push(1); // add an alpha of 1

// return decimal valued RGBA array
return c.map(x => parseInt(x, 16))
}

if(re_rgb.test(c)) {
// extract values
c = c.replace(/rgba?(/, '').replace(')', '').split(',').map(x => +x.trim());
if(c.length === 3) c.push(1); // if no alpha specified, use 1

return c // return RGBA array
}
Now after adding the keyword to RGBA map (CMAP) and the getRGBA() function, our JavaScript code doesn't change much from the previous examples:
const INI = getRGBA(S.getPropertyValue('--c-ini').trim()),
FIN = getRGBA(S.getPropertyValue('--c-fin').trim()),
RANGE = [],
ALPHA = 1 - INI[3] || 1 - FIN[3];

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + k*RANGE[i])).join(',')})`
);

/* same as before */
};

(function init() {
if(!ALPHA) INI.pop(); // get rid of alpha if always 1
RANGE.splice(0, 0, ...INI.map((c, i) => FIN[i] - c));
})();

/* same as before */
This gives us a linear gradient animation:
See the Pen by thebabydino (@thebabydino) on CodePen.
We can also use a different, non-linear timing function, for example one that allows for a bounce at the end:
const E = .8*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
}

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + timing(k)*RANGE[i])).join(',')})`
);

/* same as before */
};

/* same as before */
This means we go all the way to a kind of blue before going back to our final violet:
See the Pen by thebabydino (@thebabydino) on CodePen.
Do note however that, in general, RGBA transitions are not the best place to illustrate bounces. That's because the RGB channels are strictly limited to the [0,255] range and the alpha channel is strictly limited to the [0,1] range. rgb(255, 0, 0) is as red as red gets, there's no redder red with a value of over 255 for the first channel. A value of 0 for the alpha channel means completely transparent, there's no greater transparency with a negative value.
By now, you're probably already bored with gradients, so let's switch to something else!
Smooth changing SVG attribute values
At this point, we cannot alter the geometry of SVG elements via CSS. We should be able to as per the SVG2 spec and Chrome does support some of this stuff, but what if we want to animate the geometry of SVG elements now, in a more cross-browser manner?
Well, you've probably guessed it, JavaScript to the rescue!
Growing a circle
Our first example is that of a circle whose radius goes from nothing (0) to a quarter of the minimum viewBox dimension. We keep the document structure simple, without any other aditional elements.
<svg viewBox='-100 -50 200 100'>
<circle/>
</svg>
For the JavaScript part, the only notable difference from the previous demos is that we read the SVG viewBox dimensions in order to get the maximum radius and we now set the r attribute within the update() function, not a CSS variable (it would be immensely useful if CSS variables were allowed as values for such attributes, but, sadly, we don't live in an ideal world):
const _G = document.querySelector('svg'),
_C = document.querySelector('circle'),
VB = _G.getAttribute('viewBox').split(' '),
RMAX = .25*Math.min(...VB.slice(2)),
E = .8*Math.PI;

/* same as before */

function update() {
/* same as before */

_C.setAttribute('r', (timing(k)*RMAX).toFixed(2));

/* same as before */
};

/* same as before */
Below, you can see the result when using a bounce-fin kind of timing function:
See the Pen by thebabydino (@thebabydino) on CodePen.
Pan and zoom map
Another SVG example is a smooth pan and zoom map demo. In this case, we take a map like those from amCharts, clean up the SVG and then create this effect by triggering a linear viewBox animation when pressing the +/ - keys (zoom) and the arrow keys (pan).
The first thing we do in the JavaScript is create a navigation map, where we take the key codes of interest and attach info about what we do when the corresponding keys are pressed (note that we need different key codes for + and - in Firefox for some reason).
const NAV_MAP = {
187: { dir: 1, act: 'zoom', name: 'in' } /* + */,
61: { dir: 1, act: 'zoom', name: 'in' } /* + Firefox ¯_(ツ)_/¯ */,
189: { dir: -1, act: 'zoom', name: 'out' } /* - */,
173: { dir: -1, act: 'zoom', name: 'out' } /* - Firefox ¯_(ツ)_/¯ */,
37: { dir: -1, act: 'move', name: 'left', axis: 0 } /* ⇦ */,
38: { dir: -1, act: 'move', name: 'up', axis: 1 } /* ⇧ */,
39: { dir: 1, act: 'move', name: 'right', axis: 0 } /* ⇨ */,
40: { dir: 1, act: 'move', name: 'down', axis: 1 } /* ⇩ */
}
When pressing the + key, what we want to do is zoom in. The action we perform is 'zoom' in the positive direction - we go 'in'. Similarly, when pressing the - key, the action is also 'zoom', but in the negative (-1) direction - we go 'out'.
When pressing the arrow left key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the negative (-1) direction - we go 'left'. When pressing the arrow up key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the negative (-1) direction - we go 'up'.
When pressing the arrow right key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the positive direction - we go 'right'. When pressing the arrow down key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the positive direction - we go 'down'.
We then get the SVG element, its initial viewBox, set the maximum zoom out level to these initial viewBox dimensions and set the smallest possible viewBox width to a much smaller value (let's say 8).
const _SVG = document.querySelector('svg'),
VB = _SVG.getAttribute('viewBox').split(' ').map(c => +c),
DMAX = VB.slice(2), WMIN = 8;
We also create an empty current navigation object to hold the current navigation action data and a target viewBox array to contain the final state we animate the viewBox to for the current animation.
let nav = {}, tg = Array(4);
On 'keyup', if we don't have any animation running already and the key that was pressed is one of interest, we get the current navigation object from the navigation map we created at the beginning. After this, we handle the two action cases ('zoom'/ 'move') and call the update() function:
addEventListener('keyup', e => {
if(!rID && e.keyCode in NAV_MAP) {
nav = NAV_MAP[e.keyCode];

if(nav.act === 'zoom') {
/* what we do if the action is 'zoom' */
}

else if(nav.act === 'move') {
/* what we do if the action is 'move' */
}

update()
}
}, false);
Now let's see what we do if we zoom. First off, and this is a very useful programming tactic in general, not just here in particular, we get the edge cases that make us exit the function out of the way.
So what are our edge cases here?
The first one is when we want to zoom out (a zoom in the negative direction) when our whole map is already in sight (the current viewBox dimensions are bigger or equal to the maximum ones). In our case, this should happen if we want to zoom out at the very beginning because we start with the whole map in sight.
The second edge case is when we hit the other limit - we want to zoom in, but we're at the maximum detail level (the current viewBox dimensions are smaller or equal to the minimum ones).
Putting the above into JavaScript code, we have:
if(nav.act === 'zoom') {
if((nav.dir === -1 && VB[2] >= DMAX[0]) ||
(nav.dir === 1 && VB[2] <= WMIN)) {
console.log(`cannot ${nav.act} ${nav.name} more`);
return
}

/* main case */
}
Now that we've handled the edge cases, let's move on to the main case. Here, we set the target viewBox values. We use a 2x zoom on each step, meaning that when we zoom in, the target viewBox dimensions are half the ones at the start of the current zoom action, and when we zoom out they're double. The target offsets are half the difference between the maximum viewBox dimensions and the target ones.
if(nav.act === 'zoom') {
/* edge cases */

for(let i = 0; i < 2; i++) {
tg[i + 2] = VB[i + 2]/Math.pow(2, nav.dir);
tg[i] = .5*(DMAX[i] - tg[i + 2]);
}
}
Next, let's see what we do if we want to move instead of zooming.
In a similar fashion, we get the edge cases that make us exit the function out of the way first. Here, these happen when we're at an edge of the map and we want to keep going in that direction (whatever the direction might be). Since originally the top left corner of our viewBox is at 0,0, this means we cannot go below 0 or above the maximum viewBox size minus the current one. Note that given we're initially fully zoomed out, this also means we cannot move in any direction until we zoom in.
else if(nav.act === 'move') {
if((nav.dir === -1 && VB[nav.axis] <= 0) ||
(nav.dir === 1 && VB[nav.axis] >= DMAX[nav.axis] - VB[2 + nav.axis])) {
console.log(`at the edge, cannot go ${nav.name}`);
return
}

/* main case */
For the main case, we move in the desired direction by half the viewBox size along that axis:
else if(nav.act === 'move') {
/* edge cases */

tg[nav.axis] = VB[nav.axis] + .5*nav.dir*VB[2 + nav.axis]
}
Now let's see what we need to do inside the update() function. This is going to be pretty similar to previous demos, except now we need to handle the 'move' and 'zoom' cases separately. We also create an array to store the current viewBox data in (cvb):
function update() {
let k = ++f/NF, j = 1 - k, cvb = VB.slice();

if(nav.act === 'zoom') {
/* what we do if the action is zoom */
}

if(nav.act === 'move') {
/* what we do if the action is move */
}

_SVG.setAttribute('viewBox', cvb.join(' '));

if(!(f%NF)) {
f = 0;
VB.splice(0, 4, ...cvb);
nav = {};
tg = Array(4);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
In the 'zoom' case, we need to recompute all viewBox values. We do this with linear interpolation between the values at the start of the animation and the target values we've previously computed:
if(nav.act === 'zoom') {
for(let i = 0; i < 4; i++)
cvb[i] = j*VB[i] + k*tg[i];
}
In the 'move' case, we only need to recompute one viewBox value - the offset for the axis we move along:
if(nav.act === 'move')
cvb[nav.axis] = j*VB[nav.axis] + k*tg[nav.axis];
And that's it! We now have a working pan and zoom demo with smooth linear transitions in between states:
See the Pen by thebabydino (@thebabydino) on CodePen.
From sad square to happy circle
Another example would be morphing a sad square SVG into a happy circle. We create an SVG with a square viewBox whose 0,0 point is right in the middle. Symmetrical with respect to the origin of the SVG system of coordinates we have a square (a rect element) covering 80% of the SVG. This is our face. We create the eyes with an ellipse and a copy of it, symmetrical with respect to the vertical axis. The mouth is a cubic Bézier curve created with a path element.
- var vb_d = 500, vb_o = -.5*vb_d;
- var fd = .8*vb_d, fr = .5*fd;

svg(viewBox=[vb_o, vb_o, vb_d, vb_d].join(' '))
rect(x=-fr y=-fr width=fd height=fd)
ellipse#eye(cx=.35*fr cy=-.25*fr
rx=.1*fr ry=.15*fr)
use(xlink:href='#eye'
transform='scale(-1 1)')
path(d=`M${-.35*fr} ${.35*fr}
C${-.21*fr} ${.13*fr}
${+.21*fr} ${.13*fr}
${+.35*fr} ${.35*fr}`)
In the JavaScript, we get the face and the mouth elements. We read the face width, which is equal to the height and we use it to compute the maximum corner rounding. This is the value for which we get a circle and is equal to half the square edge. We also get the mouth path data, from where we extract the initial y coordinate of the control points and compute the final y coordinate of the same control points.
const _FACE = document.querySelector('rect'),
_MOUTH = document.querySelector('path'),
RMAX = .5*_FACE.getAttribute('width'),
DATA = _MOUTH.getAttribute('d').slice(1)
.replace('C', '').split(/s+/)
.map(c => +c),
CPY_INI = DATA[3],
CPY_RANGE = 2*(DATA[1] - DATA[3]);
The rest is very similar to all other transition on click demos so far, with just a few minor differences (note that we use an ease-out kind of timing function):
/* same as before */

function timing(k) { return 1 - Math.pow(1 - k, 2) };

function update() {
f += dir;

let k = f/NF, cpy = CPY_INI + timing(k)*CPY_RANGE;

_FACE.setAttribute('rx', (timing(k)*RMAX).toFixed(2));
_MOUTH.setAttribute(
'd',
`M${DATA.slice(0,2)}
C${DATA[2]} ${cpy} ${DATA[4]} ${cpy} ${DATA.slice(-2)}`
);

/* same as before */
};

/* same as before */
And so we have our silly result:
See the Pen by thebabydino (@thebabydino) on CodePen.

Emulating CSS Timing Functions with JavaScript is a post from CSS-Tricks
Source: CssTricks


Emulating CSS Timing Functions with JavaScript

CSS animations and transitions are great! However, while recently toying with an idea, I got really frustrated with the fact that gradients are only animatable in Edge (and IE 10+). Yes, we can do all sorts of tricks with background-position, background-size, background-blend-mode or even opacity and transform on a pseudo-element/ child, but sometimes these are just not enough. Not to mention that we run into similar problems when wanting to animate SVG attributes without a CSS correspondent.
Using a lot of examples, this article is going to explain how to smoothly go from one state to another in a similar fashion to that of common CSS timing functions using just a little bit of JavaScript, without having to rely on a library, so without including a lot of complicated and unnecessary code that may become a big burden in the future.

This is not how the CSS timing functions work. This is an approach that I find simpler and more intuitive than working with Bézier curves. I'm going to show how to experiment with different timing functions using JavaScript and dissect use cases. It is not a tutorial on how to do beautiful animation.
A few examples using a linear timing function
Let's start with a left to right linear-gradient() with a sharp transition where we want to animate the first stop. Here's a way to express that using CSS custom properties:
background: linear-gradient(90deg, #ff9800 var(--stop, 0%), #3c3c3c 0);
On click, we want the value of this stop to go from 0% to 100% (or the other way around, depending on the state it's already in) over the course of NF frames. If an animation is already running at the time of the click, we stop it, change its direction, then restart it.
We also need a few variables such as the request ID (this gets returned by requestAnimationFrame), the index of the current frame (an integer in the [0, NF] interval, starting at 0) and the direction our transition is going in (which is 1 when going towards 100% and -1 when going towards 0%).
While nothing is changing, the request ID is null. We also set the current frame index to 0 initially and the direction to -1, as if we've just arrived to 0% from 100%.
const NF = 80; // number of frames transition happens over

let rID = null, f = 0, dir = -1;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {};

addEventListener('click', e => {
if(rID) stopAni(); // if an animation is already running, stop it
dir *= -1; // change animation direction
update();
}, false);
Now all that's left is to populate the update() function. Within it, we update the current frame index f. Then we compute a progress variable k as the ratio between this current frame index f and the total number of frames NF. Given that f goes from 0 to NF (included), this means that our progress k goes from 0 to 1. Multiply this with 100% and we get the desired stop.
After this, we check whether we've reached one of the end states. If we have, we stop the animation and exit the update() function.
function update() {
f += dir; // update current frame index

let k = f/NF; // compute progress

document.body.style.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
The result can be seen in the Pen below (note that we go back on a second click):
See the Pen by thebabydino (@thebabydino) on CodePen.
The way the pseudo-element is made to contrast with the background below is explained in an older article.
The above demo may look like something we could easily achieve with an element and translating a pseudo-element that can fully cover it, but things get a lot more interesting if we give the background-size a value that's smaller than 100% along the x axis, let's say 5em:
See the Pen by thebabydino (@thebabydino) on CodePen.
This gives us a sort of a "vertical blinds" effect that cannot be replicated in a clean manner with just CSS if we don't want to use more than one element.
Another option would be not to alternate the direction and always sweep from left to right, except only odd sweeps would be orange. This requires tweaking the CSS a bit:
--c0: #ff9800;
--c1: #3c3c3c;
background: linear-gradient(90deg,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
In the JavaScript, we ditch the direction variable and add a type one (typ) that switches between 0 and 1 at the end of every transition. That's when we also update all custom properties:
const S = document.body.style;

let typ = 0;

function update() {
let k = ++f/NF;

S.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
f = 0;
S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);
S.setProperty('--stop', `0%`);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
This gives us the desired result (click at least twice to see how the effect differs from that in the first demo):
See the Pen by thebabydino (@thebabydino) on CodePen.
We could also change the gradient angle instead of the stop. In this case, the background rule becomes:
background: linear-gradient(var(--angle, 0deg),
#ff9800 50%, #3c3c3c 0);
In the JavaScript code, we tweak the update() function:
function update() {
f += dir;

let k = f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
We now have a gradient angle transition in between the two states (0deg and 180deg):
See the Pen by thebabydino (@thebabydino) on CodePen.
In this case, we might also want to keep going clockwise to get back to the 0deg state instead of changing the direction. So we just ditch the dir variable altogether, discard any clicks happening during the transition, and always increment the frame index f, resetting it to 0 when we've completed a full rotation around the circle:
function update() {
let k = ++f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
f = f%(2*NF);
stopAni();
return
}

rID = requestAnimationFrame(update)
};

addEventListener('click', e => {
if(!rID) update()
}, false);
The following Pen illustrates the result - our rotation is now always clockwise:
See the Pen by thebabydino (@thebabydino) on CodePen.
Something else we could do is use a radial-gradient() and animate the radial stop:
background: radial-gradient(circle,
#ff9800 var(--stop, 0%), #3c3c3c 0);
The JavaScript code is identical to that of the first demo and the result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
We may also not want to go back when clicking again, but instead make another blob grow and cover the entire viewport. In this case, we add a few more custom properties to the CSS:
--c0: #ff9800;
--c1: #3c3c3c;
background: radial-gradient(circle,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
The JavaScript is the same as in the case of the third linear-gradient() demo. This gives us the result we want:
See the Pen by thebabydino (@thebabydino) on CodePen.
A fun tweak to this would be to make our circle start growing from the point we clicked. To do so, we introduce two more custom properties, --x and --y:
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
When clicking, we set these to the coordinates of the point where the click happened:
addEventListener('click', e => {
if(!rID) {
S.setProperty('--x', `${e.clientX}px`);
S.setProperty('--y', `${e.clientY}px`);
update();
}
}, false);
This gives us the following result where we have a disc growing from the point where we clicked:
See the Pen by thebabydino (@thebabydino) on CodePen.
Another option would be using a conic-gradient() and animating the angular stop:
background: conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0%)
Note that in the case of conic-gradient(), we must use a unit for the zero value (whether that unit is % or an angular one like deg doesn't matter), otherwise our code won't work - writing conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0) means nothing gets displayed.
The JavaScript is the same as for animating the stop in the linear or radial case, but bear in mind that this currently only works in Chrome with Experimental Web Platform Features enabled in chrome://flags.
The Experimental Web Platform Features flag enabled in Chrome Canary (63.0.3210.0).
Just for the purpose of displaying conic gradients in the browser, there's a polyfill by Lea Verou and this works cross-browser but doesn't allow using CSS custom properties.
The recording below illustrates how our code works:
Recording of how our first conic-gradient() demo works in Chrome with the flag enabled (live demo).
This is another situation where we might not want to go back on a second click. This means we need to alter the CSS a bit, in the same way we did for the last radial-gradient() demo:
--c0: #ff9800;
--c1: #3c3c3c;
background: conic-gradient(
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0%)
The JavaScript code is exactly the same as in the corresponding linear-gradient() or radial-gradient() case and the result can be seen below:
Recording of how our second conic-gradient() demo works in Chrome with the flag enabled (live demo).
Before we move on to other timing functions, there's one more thing to cover: the case when we don't go from 0% to 100%, but in between any two values. We take the example of our first linear-gradient, but with a different default for --stop, let's say 85% and we also set a --stop-fin value - this is going to be the final value for --stop:
--stop-ini: 85%;
--stop-fin: 26%;
background: linear-gradient(90deg, #ff9800 var(--stop, var(--stop-ini)), #3c3c3c 0)
In the JavaScript, we read these two values - the initial (default) and the final one - and we compute a range as the difference between them:
const S = getComputedStyle(document.body),
INI = +S.getPropertyValue('--stop-ini').replace('%', ''),
FIN = +S.getPropertyValue('--stop-fin').replace('%', ''),
RANGE = FIN - INI;
Finally, in the update() function, we take into account the initial value and the range when setting the current value for --stop:
document.body.style.setProperty(
'--stop',
`${+(INI + k*RANGE).toFixed(2)}%`
);
With these changes we now have a transition in between 85% and 26% (and the other way on even clicks):
See the Pen by thebabydino (@thebabydino) on CodePen.
If we want to mix units for the stop value, things get hairier as we need to compute more things (box dimensions when mixing % and px, font sizes if we throw em or rem in the mix, viewport dimensions if we want to use viewport units, the length of the 0% to 100% segment on the gradient line for gradients that are not horizontal or vertical), but the basic idea remains the same.
Emulating ease-in/ ease-out
An ease-in kind of function means the change in value happens slow at first and then accelerates. ease-out is exactly the opposite - the change happens fast in the beginning, but then slows down towards the end.
The ease-in (left) and ease-out (right) timing functions (live).
The slope of the curves above gives us the rate of change. The steeper it is, the faster the change in value happens.
We can emulate these functions by tweaking the linear method described in the first section. Since k takes values in the [0, 1] interval, raising it to any positive power also gives us a number within the same interval. The interactive demo below shows the graph of a function f(k) = pow(k, p) (k raised to an exponent p) shown in purple and that of a function g(k) = 1 - pow(1 - k, p) shown in red on the [0, 1] interval versus the identity function id(k) = k (which corresponds to a linear timing function).
See the Pen by thebabydino (@thebabydino) on CodePen.
When the exponent p is equal to 1, the graphs of the f and g functions are identical to that of the identity function.
When exponent p is greater than 1, the graph of the f function is below the identity line - the rate of change increases as k increases. This is like an ease-in type of function. The graph of the g function is above the identity line - the rate of change decreases as k increases. This is like an ease-out type of function.
It seems an exponent p of about 2 gives us an f that's pretty similar to ease-in, while g is pretty similar to ease-out. With a bit more tweaking, it looks like the best approximation is for a p value of about 1.675:
See the Pen by thebabydino (@thebabydino) on CodePen.
In this interactive demo, we want the graphs of the f and g functions to be as close as possible to the dashed lines, which represent the ease-in timing function (below the identity line) and the ease-out timing function (above the identity line).
Emulating ease-in-out
The CSS ease-in-out timing function looks like in the illustration below:
The ease-in-out timing function (live).
So how can we get something like this?
Well, that's what harmonic functions are for! More exactly, the ease-in-out out shape is reminiscent the shape of the sin() function on the [-90°,90°] interval.
The sin(k) function on the [-90°,90°] interval (live).
However, we don't want a function whose input is in the [-90°,90°] interval and output is in the [-1,1] interval, so let's fix this!
This means we need to squish the hashed rectangle ([-90°,90°]x[-1,1]) in the illustration above into the unit one ([0,1]x[0,1]).
First, let's take the domain [-90°,90°]. If we change our function to be sin(k·180°) (or sin(k·π) in radians), then our domain becomes [-.5,.5] (we can check that -.5·180° = 90° and .5·180° = 90°):
The sin(k·π) function on the [-.5,.5] interval (live).
We can shift this domain to the right by .5 and get the desired [0,1] interval if we change our function to be sin((k - .5)·π) (we can check that 0 - .5 = -.5 and 1 - .5 = .5):
The sin((k - .5)·π) function on the [0,1] interval (live).
Now let's get the desired codomain. If we add 1 to our function making it sin((k - .5)·π) + 1 this shifts out codomain up into the [0, 2] interval:
The sin((k - .5)·π) + 1 function on the [0,1] interval (live).
Dividing everything by 2 gives us the (sin((k - .5)·π) + 1)/2 function and compacts the codomain into our desired [0,1] interval:
The (sin((k - .5)·π) + 1)/2 function on the [0,1] interval (live).
This turns out to be a good approximation of the ease-in-out timing function (represented with an orange dashed line in the illustration above).
Comparison of all these timing functions
Let's say we want to have a bunch of elements with a linear-gradient() (like in the third demo). On click, their --stop values go from 0% to 100%, but with a different timing function for each.
In the JavaScript, we create a timing functions object with the corresponding function for each type of easing:
tfn = {
'linear': function(k) {
return k;
},
'ease-in': function(k) {
return Math.pow(k, 1.675);
},
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675);
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1);
}
};
For each of these, we create an article element:
const _ART = [];

let frag = document.createDocumentFragment();

for(let p in tfn) {
let art = document.createElement('article'),
hd = document.createElement('h3');

hd.textContent = p;
art.appendChild(hd);
art.setAttribute('id', p);
_ART.push(art);
frag.appendChild(art);
}

n = _ART.length;
document.body.appendChild(frag);
The update function is pretty much the same, except we set the --stop custom property for every element as the value returned by the corresponding timing function when fed the current progress k. Also, when resetting the --stop to 0% at the end of the animation, we also need to do this for every element.
function update() {
let k = ++f/NF;

for(let i = 0; i < n; i++) {
_ART[i].style.setProperty(
'--stop',
`${+tfn[_ART[i].id](k).toFixed(5)*100}%`
);
}

if(!(f%NF)) {
f = 0;

S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);

for(let i = 0; i < n; i++)
_ART[i].style.setProperty('--stop', `0%`);

stopAni();
return;
}

rID = requestAnimationFrame(update)
};
This gives us a nice visual comparison of these timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
They all start and finish at the same time, but while the progress is constant for the linear one, the ease-in one starts slowly and then accelerates, the ease-out one starts fast and then slows down and, finally, the ease-in-out one starts slowly, accelerates and then slows down again at the end.
Timing functions for bouncing transitions
I first came across the concept years ago, in Lea Verou's CSS Secrets talk. These happen when the y (even) values in a cubic-bezier() function are outside the [0, 1] range and the effect they create is of the animated value going outside the interval between its initial and final value.
This bounce can happen right after the transition starts, right before it finishes or at both ends.
A bounce at the start means that, at first, we don't go towards the final state, but in the opposite direction. For example, if want to animate a stop from 43% to 57% and we have a bounce at the start, then, at first, out stop value doesn't increase towards 57%, but decreases below 43% before going back up to the final state. Similarly, if we go from an initial stop value of 57% to a final stop value of 43% and we have a bounce at the start, then, at first, the stop value increases above 57% before going down to the final value.
A bounce at the end means we overshoot our final state and only then go back to it. If want to animate a stop from 43% to 57% and we have a bounce at the end, then we start going normally from the initial state to the final one, but towards the end, we go above 57% before going back down to it. And if we go from an inital stop value of 57% to a final stop value of 43% and we have a bounce at the end, then, at first, we go down towards the final state, but, towards the end, we pass it and we briefly have stop values below 43% before our transition finishes there.
If what they do is still difficult to grasp, below there's a comparative example of all three of them in action.
The three cases.
These kinds of timing functions don't have their own keywords associated, but they look cool and they are what we want in a lot of situations.
Just like in the case of ease-in-out, the quickest way of getting them is by using harmonic functions. The difference lies in the fact that now we don't start from the [-90°,90°] domain anymore.
For a bounce at the beginning, we start with the [s, 0°] portion of the sin() function, where s (the start angle) is in the (-180°,-90°) interval. The closer it is to -180°, the bigger the bounce is and the faster it will go to the final state after it. So we don't want it to be really close to -180° because the result would look too unnatural. We also want it to be far enough from -90° that the bounce is noticeable.
In the interactive demo below, you can drag the slider to change the start angle and then click on the stripe at the bottom to see the effect in action:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,0]x[sin(s),0]) is the area we need move and scale into the [0,1]x[0,1] area in order to get our timing function. The part of the curve that's below its lower edge is where the bounce happens. You can adjust the start angle using the slider and then click on the bottom bar to see how the transition looks for different start angles.
Just like in the ease-in-out case, we first squish the domain into the [-1,0] interval by dividing the argument with the range (which is the maximum 0 minus the minimum s). Therefore, our function becomes sin(-k·s) (we can check that -(-1)·s = s and -0·s = 0):
The sin(-k·s) function on the [-1,0] interval (live).
Next, we shift this interval to the right (by 1, into [0,1]). This makes our function sin(-(k - 1)·s) = sin((1 - k)·s) (it checks that 0 - 1 = -1 and 1 - 1 = 0):
The sin(-(k - 1)·s) function on the [0,1] interval (live).
We then shift the codomain up by its value at 0 (sin((1 - 0)*s) = sin(s)). Our function is now sin((1 - k)·s) - sin(s) and our codomain [0,-sin(s)].
The sin(-(k - 1)·s) - sin(s) function on the [0,1] interval (live).
The last step is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is -sin(s)). This means our final easing function is 1 - sin((1 - k)·s)/sin(s)
The 1 - sin((1 - k)·s)/sin(s) function on the [0,1] interval (live).
For a bounce at the end, we start with the [0°, e] portion of the sin() function, where e (the end angle) is in the (90°,180°) interval. The closer it is to 180°, the bigger the bounce is and the faster it will move from the initial state to the final one before it overshoots it and the bounce happens. So we don't want it to be really close to 180° as the result would look too unnatural. We also want it to be far enough from 90° so that the bounce is noticeable.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([0,e]x[0,sin(e)]) is the area we need to squish and move into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's below its upper edge is where the bounce happens.
We start by squishing the domain into the [0,1] interval by dividing the argument with the range (which is the maximum e minus the minimum 0). Therefore, our function becomes sin(k·e) (we can check that 0·e = 0 and 1·e = e):
The sin(k·e) function on the [0,1] interval (live).
What's still left to do is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is sin(e)). This means our final easing function is sin(k·e)/sin(e).
The sin(k·e)/sin(e) function on the [0,1] interval (live).
If we want a bounce at each end, we start with the [s, e] portion of the sin() function, where s is in the (-180°,-90°) interval and e in the (90°,180°) interval. The larger s and e are in absolute values, the bigger the corresponding bounces are and the more of the total transition time is spent on them alone. On the other hand, the closer their absolute values get to 90°, the less noticeable their corresponding bounces are. So, just like in the previous two cases, it's all about finding the right balance.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,e]x[sin(s),sin(e)]) is the area we need to move and scale into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's beyond its horizontal edges is where the bounces happen.
We start by shifting the domain to the right into the [0,e - s] interval. This means our function becomes sin(k + s) (we can check that 0 + s = s and that e - s + s = e).
The sin(k + s) function on the [0,e - s] interval (live).
Then we shrink the domain to fit into the [0,1] interval, which gives us the function sin(k·(e - s) + s).
The sin(k·(e - s) + s) function on the [0,1] interval (live).
Moving on to the codomain, we first shift it up by its value at 0 (sin(0·(e - s) + s)), which means we now have sin(k·(e - s) + s) - sin(s). This gives us the new codomain [0,sin(e) - sin(s)].
The sin(k·(e - s) + s) - sin(s) function on the [0,1] interval (live).
Finally, we shrink the codomain to the [0,1] interval by dividing with the range (sin(e) - sin(s)), so our final function is (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)).
The (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)) function on the [0,1] interval (live).
So in order to do a similar comparative demo to that for the JS equivalents of the CSS linear, ease-in, ease-out, ease-in-out, our timing functions object becomes:
tfn = {
'bounce-ini': function(k) {
return 1 - Math.sin((1 - k)*s)/Math.sin(s);
},
'bounce-fin': function(k) {
return Math.sin(k*e)/Math.sin(e);
},
'bounce-ini-fin': function(k) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s));
}
};
The s and e variables are the values we get from the two range inputs that allow us to control the bounce amount.
The interactive demo below shows the visual comparison of these three types of timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alternating animations
In CSS, setting animation-direction to alternate also reverses the timing function. In order to better understand this, consider a .box element on which we animate its transform property such that we move to the right. This means our @keyframes look as follows:
@keyframes shift {
0%, 10% { transform: none }
90%, 100% { transform: translate(50vw) }
}
We use a custom timing function that allows us to have a bounce at the end and we make this animation alternate - that is, go from the final state (translate(50vw)) back to the initial state (no translation) for the even-numbered iterations (second, fourth and so on).
animation: shift 1s cubic-bezier(.5, 1, .75, 1.5) infinite alternate
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
One important thing to notice here is that, for the even-numbered iterations, our bounce doesn't happen at the end, but at the start - the timing function is reversed. Visually, this means it's reflected both horizontally and vertically with respect to the .5,.5 point.
The normal timing function (f, in red, with a bounce at the end) and the symmetrical reverse one (g, in purple, with a bounce at the start) (live)
In CSS, there is no way of having a different timing function other than the symmetrical one on going back if we are to use this set of keyframes and animation-direction: alternate. We can introduce the going back part into the keyframes and control the timing function for each stage of the animation, but that's outside the scope of this article.
When changing values with JavaScript in the fashion presented so far in this article, the same thing happens by default. Consider the case when we want to animate the stop of a linear-gradient() between an initial and a final position and we want to have a bounce at the end. This is pretty much the last example presented in the first section with timing function that lets us have a bounce at the end (one in the bounce-fin category described before) instead of a linear one.
The CSS is exactly the same and we only make a few minor changes to the JavaScript code. We set a limit angle E and we use a custom bounce-fin kind of timing function in place of the linear one:
const E = .75*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
};

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + timing(k)*RANGE).toFixed(2)}%`
);

/* same as before */
};

/* same as before */
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the initial state, the stop is at 85%. We animate it to 26% (which is the final state) using a timing function that gives us a bounce at the end. This means we go beyond our final stop position at 26% before going back up and stopping there. This is what happens during the odd iterations.
During the even iterations, this behaves just like in the CSS case, reversing the timing function, so that the bounce happens at the beginning, not at the end.
But what if we don't want the timing function to be reversed?
In this case, we need to use the symmetrical function. For any timing function f(k) defined on the [0,1] interval (this is the domain), whose values are in the [0,1] (codomain), the symmetrical function we want is 1 - f(1 - k). Note that functions whose shape is actually symmetrical with respect to the .5,.5 point, like linear or ease-in-out are identical to their symmetrical functions.
See the Pen by thebabydino (@thebabydino) on CodePen.
So what we do is use our timing function f(k) for the odd iterations and use 1 - f(1 - k) for the even ones. We can tell whether an iteration is odd or even from the direction (dir) variable. This is 1 for odd iterations and -1 for even ones.
This means we can combine our two timing functions into one: m + dir*f(m + dir*k).
Here, the multiplier m is 0 for the odd iterations (when dir is 1) and 1 for the even ones (when dir is -1), so we can compute it as .5*(1 - dir):
dir = +1 → m = .5*(1 - (+1)) = .5*(1 - 1) = .5*0 = 0
dir = -1 → m = .5*(1 - (-1)) = .5*(1 + 1) = .5*2 = 1
This way, our JavaScript becomes:
let m;

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + (m + dir*timing(m + dir*k))*RANGE).toFixed(2)}%`
);

/* same as before */
};

addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
The final result can be seen in this Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Even more examples
Gradient stops are not the only things that aren't animatable cross-browser with just CSS.
Gradient end going from orange to violet
For a first example of something different, let's say we want the orange in our gradient to animate to a kind of violet. We start with a CSS that looks something like this:
--c-ini: #ff9800;
--c-fin: #a048b9;
background: linear-gradient(90deg,
var(--c, var(--c-ini)), #3c3c3c)
In order to interpolate between the initial and final values, we need to know the format we get when reading them via JavaScript - is it going to be the same format we set them in? Is it going to be always rgb()/ rgba()?
Here is where things get a bit hairy. Consider the following test, where we have a gradient where we've used every format possible:
--c0: hsl(150, 100%, 50%); // springgreen
--c1: orange;
--c2: #8a2be2; // blueviolet
--c3: rgb(220, 20, 60); // crimson
--c4: rgba(255, 245, 238, 1); // seashell with alpha = 1
--c5: hsla(51, 100%, 50%, .5); // gold with alpha = .5
background: linear-gradient(90deg,
var(--c0), var(--c1),
var(--c2), var(--c3),
var(--c4), var(--c5))
We read the computed values of the gradient image and the individual custom properties --c0 through --c5 via JavaScript.
let s = getComputedStyle(document.body);

console.log(s.backgroundImage);
console.log(s.getPropertyValue('--c0'), 'springgreen');
console.log(s.getPropertyValue('--c1'), 'orange');
console.log(s.getPropertyValue('--c2'), 'blueviolet');
console.log(s.getPropertyValue('--c3'), 'crimson');
console.log(s.getPropertyValue('--c4'), 'seashell (alpha = 1)');
console.log(s.getPropertyValue('--c5'), 'gold (alpha = .5)');
The results seem a bit inconsistent.
Screenshots showing what gets logged in Chrome, Edge and Firefox (live).
Whatever we do, if we have an alpha of strictly less than 1, what we get via JavaScript seems to be always an rgba() value, regardless of whether we've set it with rgba() or hsla().
All browsers also agree when reading the custom properties directly, though, this time, what we get doesn't seem to make much sense: orange, crimson and seashell are returned as keywords regardless of how they were set, but we get hex values for springgreen and blueviolet. Except for orange, which was added in Level 2, all these values were added to CSS in Level 3, so why do we get some as keywords and others as hex values?
For the background-image, Firefox always returns the fully opaque values only as rgb(), while Chrome and Edge return them as either keywords or hex values, just like they do in the case when we read the custom properties directly.
Oh well, at least that lets us know we need to take into account different formats.
So the first thing we need to do is map the keywords to rgb() values. Not going to write all that manually, so a quick search finds this repo - perfect, it's exactly what we want! We can now set that as the value of a CMAP constant.
The next step here is to create a getRGBA(c) function that would take a string representing a keyword, a hex or an rgb()/ rgba() value and return an array containing the RGBA values ([red, green, blue, alpha]).
We start by building our regular expressions for the hex and rgb()/ rgba() values. These are a bit loose and would catch quite a few false positives if we were to have user input, but since we're only using them on CSS computed style values, we can afford to take the quick and dirty path here:
let re_hex = /^#([a-fd]{1,2})([a-fd]{1,2})([a-fd]{1,2})$/i,
re_rgb = /^rgba?((d{1,3},s){2}d{1,3}(,s((0|1)?.?d*))?)/;
Then we handle the three types of values we've seen we might get by reading the computed styles:
if(c in CMAP) return CMAP[c]; // keyword lookup, return rgb

if([4, 7].indexOf(c.length) !== -1 && re_hex.test(c)) {
c = c.match(re_hex).slice(1); // remove the '#'
if(c[0].length === 1) c = c.map(x => x + x);
// go from 3-digit form to 6-digit one
c.push(1); // add an alpha of 1

// return decimal valued RGBA array
return c.map(x => parseInt(x, 16))
}

if(re_rgb.test(c)) {
// extract values
c = c.replace(/rgba?(/, '').replace(')', '').split(',').map(x => +x.trim());
if(c.length === 3) c.push(1); // if no alpha specified, use 1

return c // return RGBA array
}
Now after adding the keyword to RGBA map (CMAP) and the getRGBA() function, our JavaScript code doesn't change much from the previous examples:
const INI = getRGBA(S.getPropertyValue('--c-ini').trim()),
FIN = getRGBA(S.getPropertyValue('--c-fin').trim()),
RANGE = [],
ALPHA = 1 - INI[3] || 1 - FIN[3];

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + k*RANGE[i])).join(',')})`
);

/* same as before */
};

(function init() {
if(!ALPHA) INI.pop(); // get rid of alpha if always 1
RANGE.splice(0, 0, ...INI.map((c, i) => FIN[i] - c));
})();

/* same as before */
This gives us a linear gradient animation:
See the Pen by thebabydino (@thebabydino) on CodePen.
We can also use a different, non-linear timing function, for example one that allows for a bounce at the end:
const E = .8*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
}

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + timing(k)*RANGE[i])).join(',')})`
);

/* same as before */
};

/* same as before */
This means we go all the way to a kind of blue before going back to our final violet:
See the Pen by thebabydino (@thebabydino) on CodePen.
Do note however that, in general, RGBA transitions are not the best place to illustrate bounces. That's because the RGB channels are stricly limited to the [0,255] range and the alpha channel is strictly limited to the [0,1] range. rgb(255, 0, 0) is as red as red gets, there's no redder red with a value of over 255 for the first channel. A value of 0 for the alpha channel means completely transparent, there's no greater transparency with a negative value.
By now, you're probably already bored with gradients, so let's switch to something else!
Smooth changing SVG attribute values
At this point, we cannot alter the geometry of SVG elements via CSS. We should be able to as per the SVG2 spec and Chrome does support some of this stuff, but what if we want to animate the geometry of SVG elements now, in a more cross-browser manner?
Well, you've probably guessed it, JavaScript to the rescue!
Growing a circle
Our first example is that of a circle whose radius goes from nothing (0) to a quarter of the minimum viewBox dimension. We keep the document structure simple, without any other aditional elements.
<svg viewBox='-100 -50 200 100'>
<circle/>
</svg>
For the JavaScript part, the only notable difference from the previous demos is that we read the SVG viewBox dimensions in order to get the maximum radius and we now set the r attribute within the update() function, not a CSS variable (it would be immensely useful if CSS variables were allowed as values for such attributes, but, sadly, we don't live in an ideal world):
const _G = document.querySelector('svg'),
_C = document.querySelector('circle'),
VB = _G.getAttribute('viewBox').split(' '),
RMAX = .25*Math.min(...VB.slice(2)),
E = .8*Math.PI;

/* same as before */

function update() {
/* same as before */

_C.setAttribute('r', (timing(k)*RMAX).toFixed(2));

/* same as before */
};

/* same as before */
Below, you can see the result when using a bounce-fin kind of timing function:
See the Pen by thebabydino (@thebabydino) on CodePen.
Pan and zoom map
Another SVG example is a smooth pan and zoom map demo. In this case, we take a map like those from amCharts, clean up the SVG and then create this effect by triggering a linear viewBox animation when pressing the +/ - keys (zoom) and the arrow keys (pan).
The first thing we do in the JavaScript is create a navigation map, where we take the key codes of interest and attach info about what we do when the corresponding keys are pressed (note that we need different key codes for + and - in Firefox for some reason).
const NAV_MAP = {
187: { dir: 1, act: 'zoom', name: 'in' } /* + */,
61: { dir: 1, act: 'zoom', name: 'in' } /* + Firefox ¯_(ツ)_/¯ */,
189: { dir: -1, act: 'zoom', name: 'out' } /* - */,
173: { dir: -1, act: 'zoom', name: 'out' } /* - Firefox ¯_(ツ)_/¯ */,
37: { dir: -1, act: 'move', name: 'left', axis: 0 } /* ⇦ */,
38: { dir: -1, act: 'move', name: 'up', axis: 1 } /* ⇧ */,
39: { dir: 1, act: 'move', name: 'right', axis: 0 } /* ⇨ */,
40: { dir: 1, act: 'move', name: 'down', axis: 1 } /* ⇩ */
}
When pressing the + key, what we want to do is zoom in. The action we perform is 'zoom' in the positive direction - we go 'in'. Similarly, when pressing the - key, the action is also 'zoom', but in the negative (-1) direction - we go 'out'.
When pressing the arrow left key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the negative (-1) direction - we go 'left'. When pressing the arrow up key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the negative (-1) direction - we go 'up'.
When pressing the arrow right key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the positive direction - we go 'right'. When pressing the arrow down key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the positive direction - we go 'down'.
We then get the SVG element, its initial viewBox, set the maximum zoom out level to these initial viewBox dimensions and set the smallest possible viewBox width to a much smaller value (let's say 8).
const _SVG = document.querySelector('svg'),
VB = _SVG.getAttribute('viewBox').split(' ').map(c => +c),
DMAX = VB.slice(2), WMIN = 8;
We also create an empty current navigation object to hold the current navigation action data and a target viewBox array to contain the final state we animate the viewBox to for the current animation.
let nav = {}, tg = Array(4);
On 'keyup', if we don't have any animation running already and the key that was pressed is one of interest, we get the current navigation object from the navigation map we created at the beginning. After this, we handle the two action cases ('zoom'/ 'move') and call the update() function:
addEventListener('keyup', e => {
if(!rID && e.keyCode in NAV_MAP) {
nav = NAV_MAP[e.keyCode];

if(nav.act === 'zoom') {
/* what we do if the action is 'zoom' */
}

else if(nav.act === 'move') {
/* what we do if the action is 'move' */
}

update()
}
}, false);
Now let's see what we do if we zoom. First off, and this is a very useful programming tactic in general, not just here in particular, we get the edge cases that make us exit the function out of the way.
So what are our edge cases here?
The first one is when we want to zoom out (a zoom in the negative direction) when our whole map is already in sight (the current viewBox dimensions are bigger or equal to the maximum ones). In our case, this should happen if we want to zoom out at the very beginning because we start with the whole map in sight.
The second edge case is when we hit the other limit - we want to zoom in, but we're at the maximum detail level (the current viewBox dimensions are smaller or equal to the minimum ones).
Putting the above into JavaScript code, we have:
if(nav.act === 'zoom') {
if((nav.dir === -1 && VB[2] >= DMAX[0]) ||
(nav.dir === 1 && VB[2] <= WMIN)) {
console.log(`cannot ${nav.act} ${nav.name} more`);
return
}

/* main case */
}
Now that we've handled the edge cases, let's move on to the main case. Here, we set the target viewBox values. We use a 2x zoom on each step, meaning that when we zoom in, the target viewBox dimensions are half the ones at the start of the current zoom action, and when we zoom out they're double. The target offsets are half the difference between the maximum viewBox dimensions and the target ones.
if(nav.act === 'zoom') {
/* edge cases */

for(let i = 0; i < 2; i++) {
tg[i + 2] = VB[i + 2]/Math.pow(2, nav.dir);
tg[i] = .5*(DMAX[i] - tg[i + 2]);
}
}
Next, let's see what we do if we want to move instead of zooming.
In a similar fashion, we get the edge cases that make us exit the function out of the way first. Here, these happen when we're at an edge of the map and we want to keep going in that direction (whatever the direction might be). Since originally the top left corner of our viewBox is at 0,0, this means we cannot go below 0 or above the maximum viewBox size minus the current one. Note that given we're initially fully zoomed out, this also means we cannot move in any direction until we zoom in.
else if(nav.act === 'move') {
if((nav.dir === -1 && VB[nav.axis] <= 0) ||
(nav.dir === 1 && VB[nav.axis] >= DMAX[nav.axis] - VB[2 + nav.axis])) {
console.log(`at the edge, cannot go ${nav.name}`);
return
}

/* main case */
For the main case, we move in the desired direction by half the viewBox size along that axis:
else if(nav.act === 'move') {
/* edge cases */

tg[nav.axis] = VB[nav.axis] + .5*nav.dir*VB[2 + nav.axis]
}
Now let's see what we need to do inside the update() function. This is going to be pretty similar to previous demos, except now we need to handle the 'move' and 'zoom' cases separately. We also create an array to store the current viewBox data in (cvb):
function update() {
let k = ++f/NF, j = 1 - k, cvb = VB.slice();

if(nav.act === 'zoom') {
/* what we do if the action is zoom */
}

if(nav.act === 'move') {
/* what we do if the action is move */
}

_SVG.setAttribute('viewBox', cvb.join(' '));

if(!(f%NF)) {
f = 0;
VB.splice(0, 4, ...cvb);
nav = {};
tg = Array(4);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
In the 'zoom' case, we need to recompute all viewBox values. We do this with linear interpolation between the values at the start of the animation and the target values we've previously computed:
if(nav.act === 'zoom') {
for(let i = 0; i < 4; i++)
cvb[i] = j*VB[i] + k*tg[i];
}
In the 'move' case, we only need to recompute one viewBox value - the offset for the axis we move along:
if(nav.act === 'move')
cvb[nav.axis] = j*VB[nav.axis] + k*tg[nav.axis];
And that's it! We now have a working pan and zoom demo with smooth linear transitions in between states:
See the Pen by thebabydino (@thebabydino) on CodePen.
From sad square to happy circle
Another example would be morphing a sad square SVG into a happy circle. We create an SVG with a square viewBox whose 0,0 point is right in the middle. Symmetrical with respect to the origin of the SVG system of coordinates we have a square (a rect element) covering 80% of the SVG. This is our face. We create the eyes with an ellipse and a copy of it, symmetrical with respect to the vertical axis. The mouth is a cubic Bézier curve created with a path element.
- var vb_d = 500, vb_o = -.5*vb_d;
- var fd = .8*vb_d, fr = .5*fd;

svg(viewBox=[vb_o, vb_o, vb_d, vb_d].join(' '))
rect(x=-fr y=-fr width=fd height=fd)
ellipse#eye(cx=.35*fr cy=-.25*fr
rx=.1*fr ry=.15*fr)
use(xlink:href='#eye'
transform='scale(-1 1)')
path(d=`M${-.35*fr} ${.35*fr}
C${-.21*fr} ${.13*fr}
${+.21*fr} ${.13*fr}
${+.35*fr} ${.35*fr}`)
In the JavaScript, we get the face and the mouth elements. We read the face width, which is equal to the height and we use it to compute the maximum corner rounding. This is the value for which we get a circle and is equal to half the square edge. We also get the mouth path data, from where we extract the initial y coordinate of the control points and compute the final y coordinate of the same control points.
const _FACE = document.querySelector('rect'),
_MOUTH = document.querySelector('path'),
RMAX = .5*_FACE.getAttribute('width'),
DATA = _MOUTH.getAttribute('d').slice(1)
.replace('C', '').split(/s+/)
.map(c => +c),
CPY_INI = DATA[3],
CPY_RANGE = 2*(DATA[1] - DATA[3]);
The rest is very similar to all other transition on click demos so far, with just a few minor differences (note that we use an ease-out kind of timing function):
/* same as before */

function timing(k) { return 1 - Math.pow(1 - k, 2) };

function update() {
f += dir;

let k = f/NF, cpy = CPY_INI + timing(k)*CPY_RANGE;

_FACE.setAttribute('rx', (timing(k)*RMAX).toFixed(2));
_MOUTH.setAttribute(
'd',
`M${DATA.slice(0,2)}
C${DATA[2]} ${cpy} ${DATA[4]} ${cpy} ${DATA.slice(-2)}`
);

/* same as before */
};

/* same as before */
And so we have our silly result:
See the Pen by thebabydino (@thebabydino) on CodePen.

Emulating CSS Timing Functions with JavaScript is a post from CSS-Tricks
Source: CssTricks


Emulating CSS Timing Functions with JavaScript

CSS animations and transitions are great! However, while recently toying with an idea, I got really frustrated with the fact that gradients are only animatable in Edge (and IE 10+). Yes, we can do all sorts of tricks with background-position, background-size, background-blend-mode or even opacity and transform on a pseudo-element/ child, but sometimes these are just not enough. Not to mention that we run into similar problems when wanting to animate SVG attributes without a CSS correspondent.
Using a lot of examples, this article is going to explain how to smoothly go from one state to another in a similar fashion to that of common CSS timing functions using just a little bit of JavaScript, without having to rely on a library, so without including a lot of complicated and unnecessary code that may become a big burden in the future.

This is not how the CSS timing functions work. This is an approach that I find simpler and more intuitive than working with Bézier curves. I'm going to show how to experiment with different timing functions using JavaScript and dissect use cases. It is not a tutorial on how to do beautiful animation.
A few examples using a linear timing function
Let's start with a left to right linear-gradient() with a sharp transition where we want to animate the first stop. Here's a way to express that using CSS custom properties:
background: linear-gradient(90deg, #ff9800 var(--stop, 0%), #3c3c3c 0);
On click, we want the value of this stop to go from 0% to 100% (or the other way around, depending on the state it's already in) over the course of NF frames. If an animation is already running at the time of the click, we stop it, change its direction, then restart it.
We also need a few variables such as the request ID (this gets returned by requestAnimationFrame), the index of the current frame (an integer in the [0, NF] interval, starting at 0) and the direction our transition is going in (which is 1 when going towards 100% and -1 when going towards 0%).
While nothing is changing, the request ID is null. We also set the current frame index to 0 initially and the direction to -1, as if we've just arrived to 0% from 100%.
const NF = 80; // number of frames transition happens over

let rID = null, f = 0, dir = -1;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {};

addEventListener('click', e => {
if(rID) stopAni(); // if an animation is already running, stop it
dir *= -1; // change animation direction
update();
}, false);
Now all that's left is to populate the update() function. Within it, we update the current frame index f. Then we compute a progress variable k as the ratio between this current frame index f and the total number of frames NF. Given that f goes from 0 to NF (included), this means that our progress k goes from 0 to 1. Multiply this with 100% and we get the desired stop.
After this, we check whether we've reached one of the end states. If we have, we stop the animation and exit the update() function.
function update() {
f += dir; // update current frame index

let k = f/NF; // compute progress

document.body.style.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
The result can be seen in the Pen below (note that we go back on a second click):
See the Pen by thebabydino (@thebabydino) on CodePen.
The way the pseudo-element is made to contrast with the background below is explained in an older article.
The above demo may look like something we could easily achieve with an element and translating a pseudo-element that can fully cover it, but things get a lot more interesting if we give the background-size a value that's smaller than 100% along the x axis, let's say 5em:
See the Pen by thebabydino (@thebabydino) on CodePen.
This gives us a sort of a "vertical blinds" effect that cannot be replicated in a clean manner with just CSS if we don't want to use more than one element.
Another option would be not to alternate the direction and always sweep from left to right, except only odd sweeps would be orange. This requires tweaking the CSS a bit:
--c0: #ff9800;
--c1: #3c3c3c;
background: linear-gradient(90deg,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
In the JavaScript, we ditch the direction variable and add a type one (typ) that switches between 0 and 1 at the end of every transition. That's when we also update all custom properties:
const S = document.body.style;

let typ = 0;

function update() {
let k = ++f/NF;

S.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
f = 0;
S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);
S.setProperty('--stop', `0%`);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
This gives us the desired result (click at least twice to see how the effect differs from that in the first demo):
See the Pen by thebabydino (@thebabydino) on CodePen.
We could also change the gradient angle instead of the stop. In this case, the background rule becomes:
background: linear-gradient(var(--angle, 0deg),
#ff9800 50%, #3c3c3c 0);
In the JavaScript code, we tweak the update() function:
function update() {
f += dir;

let k = f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
We now have a gradient angle transition in between the two states (0deg and 180deg):
See the Pen by thebabydino (@thebabydino) on CodePen.
In this case, we might also want to keep going clockwise to get back to the 0deg state instead of changing the direction. So we just ditch the dir variable altogether, discard any clicks happening during the transition, and always increment the frame index f, resetting it to 0 when we've completed a full rotation around the circle:
function update() {
let k = ++f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
f = f%(2*NF);
stopAni();
return
}

rID = requestAnimationFrame(update)
};

addEventListener('click', e => {
if(!rID) update()
}, false);
The following Pen illustrates the result - our rotation is now always clockwise:
See the Pen by thebabydino (@thebabydino) on CodePen.
Something else we could do is use a radial-gradient() and animate the radial stop:
background: radial-gradient(circle,
#ff9800 var(--stop, 0%), #3c3c3c 0);
The JavaScript code is identical to that of the first demo and the result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
We may also not want to go back when clicking again, but instead make another blob grow and cover the entire viewport. In this case, we add a few more custom properties to the CSS:
--c0: #ff9800;
--c1: #3c3c3c;
background: radial-gradient(circle,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
The JavaScript is the same as in the case of the third linear-gradient() demo. This gives us the result we want:
See the Pen by thebabydino (@thebabydino) on CodePen.
A fun tweak to this would be to make our circle start growing from the point we clicked. To do so, we introduce two more custom properties, --x and --y:
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
When clicking, we set these to the coordinates of the point where the click happened:
addEventListener('click', e => {
if(!rID) {
S.setProperty('--x', `${e.clientX}px`);
S.setProperty('--y', `${e.clientY}px`);
update();
}
}, false);
This gives us the following result where we have a disc growing from the point where we clicked:
See the Pen by thebabydino (@thebabydino) on CodePen.
Another option would be using a conic-gradient() and animating the angular stop:
background: conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0%)
Note that in the case of conic-gradient(), we must use a unit for the zero value (whether that unit is % or an angular one like deg doesn't matter), otherwise our code won't work - writing conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0) means nothing gets displayed.
The JavaScript is the same as for animating the stop in the linear or radial case, but bear in mind that this currently only works in Chrome with Experimental Web Platform Features enabled in chrome://flags.
The Experimental Web Platform Features flag enabled in Chrome Canary (63.0.3210.0).
Just for the purpose of displaying conic gradients in the browser, there's a polyfill by Lea Verou and this works cross-browser but doesn't allow using CSS custom properties.
The recording below illustrates how our code works:
Recording of how our first conic-gradient() demo works in Chrome with the flag enabled (live demo).
This is another situation where we might not want to go back on a second click. This means we need to alter the CSS a bit, in the same way we did for the last radial-gradient() demo:
--c0: #ff9800;
--c1: #3c3c3c;
background: conic-gradient(
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0%)
The JavaScript code is exactly the same as in the corresponding linear-gradient() or radial-gradient() case and the result can be seen below:
Recording of how our second conic-gradient() demo works in Chrome with the flag enabled (live demo).
Before we move on to other timing functions, there's one more thing to cover: the case when we don't go from 0% to 100%, but in between any two values. We take the example of our first linear-gradient, but with a different default for --stop, let's say 85% and we also set a --stop-fin value - this is going to be the final value for --stop:
--stop-ini: 85%;
--stop-fin: 26%;
background: linear-gradient(90deg, #ff9800 var(--stop, var(--stop-ini)), #3c3c3c 0)
In the JavaScript, we read these two values - the initial (default) and the final one - and we compute a range as the difference between them:
const S = getComputedStyle(document.body),
INI = +S.getPropertyValue('--stop-ini').replace('%', ''),
FIN = +S.getPropertyValue('--stop-fin').replace('%', ''),
RANGE = FIN - INI;
Finally, in the update() function, we take into account the initial value and the range when setting the current value for --stop:
document.body.style.setProperty(
'--stop',
`${+(INI + k*RANGE).toFixed(2)}%`
);
With these changes we now have a transition in between 85% and 26% (and the other way on even clicks):
See the Pen by thebabydino (@thebabydino) on CodePen.
If we want to mix units for the stop value, things get hairier as we need to compute more things (box dimensions when mixing % and px, font sizes if we throw em or rem in the mix, viewport dimensions if we want to use viewport units, the length of the 0% to 100% segment on the gradient line for gradients that are not horizontal or vertical), but the basic idea remains the same.
Emulating ease-in/ ease-out
An ease-in kind of function means the change in value happens slow at first and then accelerates. ease-out is exactly the opposite - the change happens fast in the beginning, but then slows down towards the end.
The ease-in (left) and ease-out (right) timing functions (live).
The slope of the curves above gives us the rate of change. The steeper it is, the faster the change in value happens.
We can emulate these functions by tweaking the linear method described in the first section. Since k takes values in the [0, 1] interval, raising it to any positive power also gives us a number within the same interval. The interactive demo below shows the graph of a function f(k) = pow(k, p) (k raised to an exponent p) shown in purple and that of a function g(k) = 1 - pow(1 - k, p) shown in red on the [0, 1] interval versus the identity function id(k) = k (which corresponds to a linear timing function).
See the Pen by thebabydino (@thebabydino) on CodePen.
When the exponent p is equal to 1, the graphs of the f and g functions are identical to that of the identity function.
When exponent p is greater than 1, the graph of the f function is below the identity line - the rate of change increases as k increases. This is like an ease-in type of function. The graph of the g function is above the identity line - the rate of change decreases as k increases. This is like an ease-out type of function.
It seems an exponent p of about 2 gives us an f that's pretty similar to ease-in, while g is pretty similar to ease-out. With a bit more tweaking, it looks like the best approximation is for a p value of about 1.675:
See the Pen by thebabydino (@thebabydino) on CodePen.
In this interactive demo, we want the graphs of the f and g functions to be as close as possible to the dashed lines, which represent the ease-in timing function (below the identity line) and the ease-out timing function (above the identity line).
Emulating ease-in-out
The CSS ease-in-out timing function looks like in the illustration below:
The ease-in-out timing function (live).
So how can we get something like this?
Well, that's what harmonic functions are for! More exactly, the ease-in-out out shape is reminiscent the shape of the sin() function on the [-90°,90°] interval.
The sin(k) function on the [-90°,90°] interval (live).
However, we don't want a function whose input is in the [-90°,90°] interval and output is in the [-1,1] interval, so let's fix this!
This means we need to squish the hashed rectangle ([-90°,90°]x[-1,1]) in the illustration above into the unit one ([0,1]x[0,1]).
First, let's take the domain [-90°,90°]. If we change our function to be sin(k·180°) (or sin(k·π) in radians), then our domain becomes [-.5,.5] (we can check that -.5·180° = 90° and .5·180° = 90°):
The sin(k·π) function on the [-.5,.5] interval (live).
We can shift this domain to the right by .5 and get the desired [0,1] interval if we change our function to be sin((k - .5)·π) (we can check that 0 - .5 = -.5 and 1 - .5 = .5):
The sin((k - .5)·π) function on the [0,1] interval (live).
Now let's get the desired codomain. If we add 1 to our function making it sin((k - .5)·π) + 1 this shifts out codomain up into the [0, 2] interval:
The sin((k - .5)·π) + 1 function on the [0,1] interval (live).
Dividing everything by 2 gives us the (sin((k - .5)·π) + 1)/2 function and compacts the codomain into our desired [0,1] interval:
The (sin((k - .5)·π) + 1)/2 function on the [0,1] interval (live).
This turns out to be a good approximation of the ease-in-out timing function (represented with an orange dashed line in the illustration above).
Comparison of all these timing functions
Let's say we want to have a bunch of elements with a linear-gradient() (like in the third demo). On click, their --stop values go from 0% to 100%, but with a different timing function for each.
In the JavaScript, we create a timing functions object with the corresponding function for each type of easing:
tfn = {
'linear': function(k) {
return k;
},
'ease-in': function(k) {
return Math.pow(k, 1.675);
},
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675);
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1);
}
};
For each of these, we create an article element:
const _ART = [];

let frag = document.createDocumentFragment();

for(let p in tfn) {
let art = document.createElement('article'),
hd = document.createElement('h3');

hd.textContent = p;
art.appendChild(hd);
art.setAttribute('id', p);
_ART.push(art);
frag.appendChild(art);
}

n = _ART.length;
document.body.appendChild(frag);
The update function is pretty much the same, except we set the --stop custom property for every element as the value returned by the corresponding timing function when fed the current progress k. Also, when resetting the --stop to 0% at the end of the animation, we also need to do this for every element.
function update() {
let k = ++f/NF;

for(let i = 0; i < n; i++) {
_ART[i].style.setProperty(
'--stop',
`${+tfn[_ART[i].id](k).toFixed(5)*100}%`
);
}

if(!(f%NF)) {
f = 0;

S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);

for(let i = 0; i < n; i++)
_ART[i].style.setProperty('--stop', `0%`);

stopAni();
return;
}

rID = requestAnimationFrame(update)
};
This gives us a nice visual comparison of these timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
They all start and finish at the same time, but while the progress is constant for the linear one, the ease-in one starts slowly and then accelerates, the ease-out one starts fast and then slows down and, finally, the ease-in-out one starts slowly, accelerates and then slows down again at the end.
Timing functions for bouncing transitions
I first came across the concept years ago, in Lea Verou's CSS Secrets talk. These happen when the y (even) values in a cubic-bezier() function are outside the [0, 1] range and the effect they create is of the animated value going outside the interval between its initial and final value.
This bounce can happen right after the transition starts, right before it finishes or at both ends.
A bounce at the start means that, at first, we don't go towards the final state, but in the opposite direction. For example, if want to animate a stop from 43% to 57% and we have a bounce at the start, then, at first, out stop value doesn't increase towards 57%, but decreases below 43% before going back up to the final state. Similarly, if we go from an initial stop value of 57% to a final stop value of 43% and we have a bounce at the start, then, at first, the stop value increases above 57% before going down to the final value.
A bounce at the end means we overshoot our final state and only then go back to it. If want to animate a stop from 43% to 57% and we have a bounce at the end, then we start going normally from the initial state to the final one, but towards the end, we go above 57% before going back down to it. And if we go from an inital stop value of 57% to a final stop value of 43% and we have a bounce at the end, then, at first, we go down towards the final state, but, towards the end, we pass it and we briefly have stop values below 43% before our transition finishes there.
If what they do is still difficult to grasp, below there's a comparative example of all three of them in action.
The three cases.
These kinds of timing functions don't have their own keywords associated, but they look cool and they are what we want in a lot of situations.
Just like in the case of ease-in-out, the quickest way of getting them is by using harmonic functions. The difference lies in the fact that now we don't start from the [-90°,90°] domain anymore.
For a bounce at the beginning, we start with the [s, 0°] portion of the sin() function, where s (the start angle) is in the (-180°,-90°) interval. The closer it is to -180°, the bigger the bounce is and the faster it will go to the final state after it. So we don't want it to be really close to -180° because the result would look too unnatural. We also want it to be far enough from -90° that the bounce is noticeable.
In the interactive demo below, you can drag the slider to change the start angle and then click on the stripe at the bottom to see the effect in action:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,0]x[sin(s),0]) is the area we need move and scale into the [0,1]x[0,1] area in order to get our timing function. The part of the curve that's below its lower edge is where the bounce happens. You can adjust the start angle using the slider and then click on the bottom bar to see how the transition looks for different start angles.
Just like in the ease-in-out case, we first squish the domain into the [-1,0] interval by dividing the argument with the range (which is the maximum 0 minus the minimum s). Therefore, our function becomes sin(-k·s) (we can check that -(-1)·s = s and -0·s = 0):
The sin(-k·s) function on the [-1,0] interval (live).
Next, we shift this interval to the right (by 1, into [0,1]). This makes our function sin(-(k - 1)·s) = sin((1 - k)·s) (it checks that 0 - 1 = -1 and 1 - 1 = 0):
The sin(-(k - 1)·s) function on the [0,1] interval (live).
We then shift the codomain up by its value at 0 (sin((1 - 0)*s) = sin(s)). Our function is now sin((1 - k)·s) - sin(s) and our codomain [0,-sin(s)].
The sin(-(k - 1)·s) - sin(s) function on the [0,1] interval (live).
The last step is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is -sin(s)). This means our final easing function is 1 - sin((1 - k)·s)/sin(s)
The 1 - sin((1 - k)·s)/sin(s) function on the [0,1] interval (live).
For a bounce at the end, we start with the [0°, e] portion of the sin() function, where e (the end angle) is in the (90°,180°) interval. The closer it is to 180°, the bigger the bounce is and the faster it will move from the initial state to the final one before it overshoots it and the bounce happens. So we don't want it to be really close to 180° as the result would look too unnatural. We also want it to be far enough from 90° so that the bounce is noticeable.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([0,e]x[0,sin(e)]) is the area we need to squish and move into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's below its upper edge is where the bounce happens.
We start by squishing the domain into the [0,1] interval by dividing the argument with the range (which is the maximum e minus the minimum 0). Therefore, our function becomes sin(k·e) (we can check that 0·e = 0 and 1·e = e):
The sin(k·e) function on the [0,1] interval (live).
What's still left to do is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is sin(e)). This means our final easing function is sin(k·e)/sin(e).
The sin(k·e)/sin(e) function on the [0,1] interval (live).
If we want a bounce at each end, we start with the [s, e] portion of the sin() function, where s is in the (-180°,-90°) interval and e in the (90°,180°) interval. The larger s and e are in absolute values, the bigger the corresponding bounces are and the more of the total transition time is spent on them alone. On the other hand, the closer their absolute values get to 90°, the less noticeable their corresponding bounces are. So, just like in the previous two cases, it's all about finding the right balance.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,e]x[sin(s),sin(e)]) is the area we need to move and scale into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's beyond its horizontal edges is where the bounces happen.
We start by shifting the domain to the right into the [0,e - s] interval. This means our function becomes sin(k + s) (we can check that 0 + s = s and that e - s + s = e).
The sin(k + s) function on the [0,e - s] interval (live).
Then we shrink the domain to fit into the [0,1] interval, which gives us the function sin(k·(e - s) + s).
The sin(k·(e - s) + s) function on the [0,1] interval (live).
Moving on to the codomain, we first shift it up by its value at 0 (sin(0·(e - s) + s)), which means we now have sin(k·(e - s) + s) - sin(s). This gives us the new codomain [0,sin(e) - sin(s)].
The sin(k·(e - s) + s) - sin(s) function on the [0,1] interval (live).
Finally, we shrink the codomain to the [0,1] interval by dividing with the range (sin(e) - sin(s)), so our final function is (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)).
The (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)) function on the [0,1] interval (live).
So in order to do a similar comparative demo to that for the JS equivalents of the CSS linear, ease-in, ease-out, ease-in-out, our timing functions object becomes:
tfn = {
'bounce-ini': function(k) {
return 1 - Math.sin((1 - k)*s)/Math.sin(s);
},
'bounce-fin': function(k) {
return Math.sin(k*e)/Math.sin(e);
},
'bounce-ini-fin': function(k) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s));
}
};
The s and e variables are the values we get from the two range inputs that allow us to control the bounce amount.
The interactive demo below shows the visual comparison of these three types of timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alternating animations
In CSS, setting animation-direction to alternate also reverses the timing function. In order to better understand this, consider a .box element on which we animate its transform property such that we move to the right. This means our @keyframes look as follows:
@keyframes shift {
0%, 10% { transform: none }
90%, 100% { transform: translate(50vw) }
}
We use a custom timing function that allows us to have a bounce at the end and we make this animation alternate - that is, go from the final state (translate(50vw)) back to the initial state (no translation) for the even-numbered iterations (second, fourth and so on).
animation: shift 1s cubic-bezier(.5, 1, .75, 1.5) infinite alternate
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
One important thing to notice here is that, for the even-numbered iterations, our bounce doesn't happen at the end, but at the start - the timing function is reversed. Visually, this means it's reflected both horizontally and vertically with respect to the .5,.5 point.
The normal timing function (f, in red, with a bounce at the end) and the symmetrical reverse one (g, in purple, with a bounce at the start) (live)
In CSS, there is no way of having a different timing function other than the symmetrical one on going back if we are to use this set of keyframes and animation-direction: alternate. We can introduce the going back part into the keyframes and control the timing function for each stage of the animation, but that's outside the scope of this article.
When changing values with JavaScript in the fashion presented so far in this article, the same thing happens by default. Consider the case when we want to animate the stop of a linear-gradient() between an initial and a final position and we want to have a bounce at the end. This is pretty much the last example presented in the first section with timing function that lets us have a bounce at the end (one in the bounce-fin category described before) instead of a linear one.
The CSS is exactly the same and we only make a few minor changes to the JavaScript code. We set a limit angle E and we use a custom bounce-fin kind of timing function in place of the linear one:
const E = .75*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
};

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + timing(k)*RANGE).toFixed(2)}%`
);

/* same as before */
};

/* same as before */
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the initial state, the stop is at 85%. We animate it to 26% (which is the final state) using a timing function that gives us a bounce at the end. This means we go beyond our final stop position at 26% before going back up and stopping there. This is what happens during the odd iterations.
During the even iterations, this behaves just like in the CSS case, reversing the timing function, so that the bounce happens at the beginning, not at the end.
But what if we don't want the timing function to be reversed?
In this case, we need to use the symmetrical function. For any timing function f(k) defined on the [0,1] interval (this is the domain), whose values are in the [0,1] (codomain), the symmetrical function we want is 1 - f(1 - k). Note that functions whose shape is actually symmetrical with respect to the .5,.5 point, like linear or ease-in-out are identical to their symmetrical functions.
See the Pen by thebabydino (@thebabydino) on CodePen.
So what we do is use our timing function f(k) for the odd iterations and use 1 - f(1 - k) for the even ones. We can tell whether an iteration is odd or even from the direction (dir) variable. This is 1 for odd iterations and -1 for even ones.
This means we can combine our two timing functions into one: m + dir*f(m + dir*k).
Here, the multiplier m is 0 for the odd iterations (when dir is 1) and 1 for the even ones (when dir is -1), so we can compute it as .5*(1 - dir):
dir = +1 → m = .5*(1 - (+1)) = .5*(1 - 1) = .5*0 = 0
dir = -1 → m = .5*(1 - (-1)) = .5*(1 + 1) = .5*2 = 1
This way, our JavaScript becomes:
let m;

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + (m + dir*timing(m + dir*k))*RANGE).toFixed(2)}%`
);

/* same as before */
};

addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
The final result can be seen in this Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Even more examples
Gradient stops are not the only things that aren't animatable cross-browser with just CSS.
Gradient end going from orange to violet
For a first example of something different, let's say we want the orange in our gradient to animate to a kind of violet. We start with a CSS that looks something like this:
--c-ini: #ff9800;
--c-fin: #a048b9;
background: linear-gradient(90deg,
var(--c, var(--c-ini)), #3c3c3c)
In order to interpolate between the initial and final values, we need to know the format we get when reading them via JavaScript - is it going to be the same format we set them in? Is it going to be always rgb()/ rgba()?
Here is where things get a bit hairy. Consider the following test, where we have a gradient where we've used every format possible:
--c0: hsl(150, 100%, 50%); // springgreen
--c1: orange;
--c2: #8a2be2; // blueviolet
--c3: rgb(220, 20, 60); // crimson
--c4: rgba(255, 245, 238, 1); // seashell with alpha = 1
--c5: hsla(51, 100%, 50%, .5); // gold with alpha = .5
background: linear-gradient(90deg,
var(--c0), var(--c1),
var(--c2), var(--c3),
var(--c4), var(--c5))
We read the computed values of the gradient image and the individual custom properties --c0 through --c5 via JavaScript.
let s = getComputedStyle(document.body);

console.log(s.backgroundImage);
console.log(s.getPropertyValue('--c0'), 'springgreen');
console.log(s.getPropertyValue('--c1'), 'orange');
console.log(s.getPropertyValue('--c2'), 'blueviolet');
console.log(s.getPropertyValue('--c3'), 'crimson');
console.log(s.getPropertyValue('--c4'), 'seashell (alpha = 1)');
console.log(s.getPropertyValue('--c5'), 'gold (alpha = .5)');
The results seem a bit inconsistent.
Screenshots showing what gets logged in Chrome, Edge and Firefox (live).
Whatever we do, if we have an alpha of strictly less than 1, what we get via JavaScript seems to be always an rgba() value, regardless of whether we've set it with rgba() or hsla().
All browsers also agree when reading the custom properties directly, though, this time, what we get doesn't seem to make much sense: orange, crimson and seashell are returned as keywords regardless of how they were set, but we get hex values for springgreen and blueviolet. Except for orange, which was added in Level 2, all these values were added to CSS in Level 3, so why do we get some as keywords and others as hex values?
For the background-image, Firefox always returns the fully opaque values only as rgb(), while Chrome and Edge return them as either keywords or hex values, just like they do in the case when we read the custom properties directly.
Oh well, at least that lets us know we need to take into account different formats.
So the first thing we need to do is map the keywords to rgb() values. Not going to write all that manually, so a quick search finds this repo - perfect, it's exactly what we want! We can now set that as the value of a CMAP constant.
The next step here is to create a getRGBA(c) function that would take a string representing a keyword, a hex or an rgb()/ rgba() value and return an array containing the RGBA values ([red, green, blue, alpha]).
We start by building our regular expressions for the hex and rgb()/ rgba() values. These are a bit loose and would catch quite a few false positives if we were to have user input, but since we're only using them on CSS computed style values, we can afford to take the quick and dirty path here:
let re_hex = /^#([a-fd]{1,2})([a-fd]{1,2})([a-fd]{1,2})$/i,
re_rgb = /^rgba?((d{1,3},s){2}d{1,3}(,s((0|1)?.?d*))?)/;
Then we handle the three types of values we've seen we might get by reading the computed styles:
if(c in CMAP) return CMAP[c]; // keyword lookup, return rgb

if([4, 7].indexOf(c.length) !== -1 && re_hex.test(c)) {
c = c.match(re_hex).slice(1); // remove the '#'
if(c[0].length === 1) c = c.map(x => x + x);
// go from 3-digit form to 6-digit one
c.push(1); // add an alpha of 1

// return decimal valued RGBA array
return c.map(x => parseInt(x, 16))
}

if(re_rgb.test(c)) {
// extract values
c = c.replace(/rgba?(/, '').replace(')', '').split(',').map(x => +x.trim());
if(c.length === 3) c.push(1); // if no alpha specified, use 1

return c // return RGBA array
}
Now after adding the keyword to RGBA map (CMAP) and the getRGBA() function, our JavaScript code doesn't change much from the previous examples:
const INI = getRGBA(S.getPropertyValue('--c-ini').trim()),
FIN = getRGBA(S.getPropertyValue('--c-fin').trim()),
RANGE = [],
ALPHA = 1 - INI[3] || 1 - FIN[3];

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + k*RANGE[i])).join(',')})`
);

/* same as before */
};

(function init() {
if(!ALPHA) INI.pop(); // get rid of alpha if always 1
RANGE.splice(0, 0, ...INI.map((c, i) => FIN[i] - c));
})();

/* same as before */
This gives us a linear gradient animation:
See the Pen by thebabydino (@thebabydino) on CodePen.
We can also use a different, non-linear timing function, for example one that allows for a bounce at the end:
const E = .8*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
}

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + timing(k)*RANGE[i])).join(',')})`
);

/* same as before */
};

/* same as before */
This means we go all the way to a kind of blue before going back to our final violet:
See the Pen by thebabydino (@thebabydino) on CodePen.
Do note however that, in general, RGBA transitions are not the best place to illustrate bounces. That's because the RGB channels are stricly limited to the [0,255] range and the alpha channel is strictly limited to the [0,1] range. rgb(255, 0, 0) is as red as red gets, there's no redder red with a value of over 255 for the first channel. A value of 0 for the alpha channel means completely transparent, there's no greater transparency with a negative value.
By now, you're probably already bored with gradients, so let's switch to something else!
Smooth changing SVG attribute values
At this point, we cannot alter the geometry of SVG elements via CSS. We should be able to as per the SVG2 spec and Chrome does support some of this stuff, but what if we want to animate the geometry of SVG elements now, in a more cross-browser manner?
Well, you've probably guessed it, JavaScript to the rescue!
Growing a circle
Our first example is that of a circle whose radius goes from nothing (0) to a quarter of the minimum viewBox dimension. We keep the document structure simple, without any other aditional elements.
<svg viewBox='-100 -50 200 100'>
<circle/>
</svg>
For the JavaScript part, the only notable difference from the previous demos is that we read the SVG viewBox dimensions in order to get the maximum radius and we now set the r attribute within the update() function, not a CSS variable (it would be immensely useful if CSS variables were allowed as values for such attributes, but, sadly, we don't live in an ideal world):
const _G = document.querySelector('svg'),
_C = document.querySelector('circle'),
VB = _G.getAttribute('viewBox').split(' '),
RMAX = .25*Math.min(...VB.slice(2)),
E = .8*Math.PI;

/* same as before */

function update() {
/* same as before */

_C.setAttribute('r', (timing(k)*RMAX).toFixed(2));

/* same as before */
};

/* same as before */
Below, you can see the result when using a bounce-fin kind of timing function:
See the Pen by thebabydino (@thebabydino) on CodePen.
Pan and zoom map
Another SVG example is a smooth pan and zoom map demo. In this case, we take a map like those from amCharts, clean up the SVG and then create this effect by triggering a linear viewBox animation when pressing the +/ - keys (zoom) and the arrow keys (pan).
The first thing we do in the JavaScript is create a navigation map, where we take the key codes of interest and attach info about what we do when the corresponding keys are pressed (note that we need different key codes for + and - in Firefox for some reason).
const NAV_MAP = {
187: { dir: 1, act: 'zoom', name: 'in' } /* + */,
61: { dir: 1, act: 'zoom', name: 'in' } /* + Firefox ¯_(ツ)_/¯ */,
189: { dir: -1, act: 'zoom', name: 'out' } /* - */,
173: { dir: -1, act: 'zoom', name: 'out' } /* - Firefox ¯_(ツ)_/¯ */,
37: { dir: -1, act: 'move', name: 'left', axis: 0 } /* ⇦ */,
38: { dir: -1, act: 'move', name: 'up', axis: 1 } /* ⇧ */,
39: { dir: 1, act: 'move', name: 'right', axis: 0 } /* ⇨ */,
40: { dir: 1, act: 'move', name: 'down', axis: 1 } /* ⇩ */
}
When pressing the + key, what we want to do is zoom in. The action we perform is 'zoom' in the positive direction - we go 'in'. Similarly, when pressing the - key, the action is also 'zoom', but in the negative (-1) direction - we go 'out'.
When pressing the arrow left key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the negative (-1) direction - we go 'left'. When pressing the arrow up key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the negative (-1) direction - we go 'up'.
When pressing the arrow right key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the positive direction - we go 'right'. When pressing the arrow down key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the positive direction - we go 'down'.
We then get the SVG element, its initial viewBox, set the maximum zoom out level to these initial viewBox dimensions and set the smallest possible viewBox width to a much smaller value (let's say 8).
const _SVG = document.querySelector('svg'),
VB = _SVG.getAttribute('viewBox').split(' ').map(c => +c),
DMAX = VB.slice(2), WMIN = 8;
We also create an empty current navigation object to hold the current navigation action data and a target viewBox array to contain the final state we animate the viewBox to for the current animation.
let nav = {}, tg = Array(4);
On 'keyup', if we don't have any animation running already and the key that was pressed is one of interest, we get the current navigation object from the navigation map we created at the beginning. After this, we handle the two action cases ('zoom'/ 'move') and call the update() function:
addEventListener('keyup', e => {
if(!rID && e.keyCode in NAV_MAP) {
nav = NAV_MAP[e.keyCode];

if(nav.act === 'zoom') {
/* what we do if the action is 'zoom' */
}

else if(nav.act === 'move') {
/* what we do if the action is 'move' */
}

update()
}
}, false);
Now let's see what we do if we zoom. First off, and this is a very useful programming tactic in general, not just here in particular, we get the edge cases that make us exit the function out of the way.
So what are our edge cases here?
The first one is when we want to zoom out (a zoom in the negative direction) when our whole map is already in sight (the current viewBox dimensions are bigger or equal to the maximum ones). In our case, this should happen if we want to zoom out at the very beginning because we start with the whole map in sight.
The second edge case is when we hit the other limit - we want to zoom in, but we're at the maximum detail level (the current viewBox dimensions are smaller or equal to the minimum ones).
Putting the above into JavaScript code, we have:
if(nav.act === 'zoom') {
if((nav.dir === -1 && VB[2] >= DMAX[0]) ||
(nav.dir === 1 && VB[2] <= WMIN)) {
console.log(`cannot ${nav.act} ${nav.name} more`);
return
}

/* main case */
}
Now that we've handled the edge cases, let's move on to the main case. Here, we set the target viewBox values. We use a 2x zoom on each step, meaning that when we zoom in, the target viewBox dimensions are half the ones at the start of the current zoom action, and when we zoom out they're double. The target offsets are half the difference between the maximum viewBox dimensions and the target ones.
if(nav.act === 'zoom') {
/* edge cases */

for(let i = 0; i < 2; i++) {
tg[i + 2] = VB[i + 2]/Math.pow(2, nav.dir);
tg[i] = .5*(DMAX[i] - tg[i + 2]);
}
}
Next, let's see what we do if we want to move instead of zooming.
In a similar fashion, we get the edge cases that make us exit the function out of the way first. Here, these happen when we're at an edge of the map and we want to keep going in that direction (whatever the direction might be). Since originally the top left corner of our viewBox is at 0,0, this means we cannot go below 0 or above the maximum viewBox size minus the current one. Note that given we're initially fully zoomed out, this also means we cannot move in any direction until we zoom in.
else if(nav.act === 'move') {
if((nav.dir === -1 && VB[nav.axis] <= 0) ||
(nav.dir === 1 && VB[nav.axis] >= DMAX[nav.axis] - VB[2 + nav.axis])) {
console.log(`at the edge, cannot go ${nav.name}`);
return
}

/* main case */
For the main case, we move in the desired direction by half the viewBox size along that axis:
else if(nav.act === 'move') {
/* edge cases */

tg[nav.axis] = VB[nav.axis] + .5*nav.dir*VB[2 + nav.axis]
}
Now let's see what we need to do inside the update() function. This is going to be pretty similar to previous demos, except now we need to handle the 'move' and 'zoom' cases separately. We also create an array to store the current viewBox data in (cvb):
function update() {
let k = ++f/NF, j = 1 - k, cvb = VB.slice();

if(nav.act === 'zoom') {
/* what we do if the action is zoom */
}

if(nav.act === 'move') {
/* what we do if the action is move */
}

_SVG.setAttribute('viewBox', cvb.join(' '));

if(!(f%NF)) {
f = 0;
VB.splice(0, 4, ...cvb);
nav = {};
tg = Array(4);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
In the 'zoom' case, we need to recompute all viewBox values. We do this with linear interpolation between the values at the start of the animation and the target values we've previously computed:
if(nav.act === 'zoom') {
for(let i = 0; i < 4; i++)
cvb[i] = j*VB[i] + k*tg[i];
}
In the 'move' case, we only need to recompute one viewBox value - the offset for the axis we move along:
if(nav.act === 'move')
cvb[nav.axis] = j*VB[nav.axis] + k*tg[nav.axis];
And that's it! We now have a working pan and zoom demo with smooth linear transitions in between states:
See the Pen by thebabydino (@thebabydino) on CodePen.
From sad square to happy circle
Another example would be morphing a sad square SVG into a happy circle. We create an SVG with a square viewBox whose 0,0 point is right in the middle. Symmetrical with respect to the origin of the SVG system of coordinates we have a square (a rect element) covering 80% of the SVG. This is our face. We create the eyes with an ellipse and a copy of it, symmetrical with respect to the vertical axis. The mouth is a cubic Bézier curve created with a path element.
- var vb_d = 500, vb_o = -.5*vb_d;
- var fd = .8*vb_d, fr = .5*fd;

svg(viewBox=[vb_o, vb_o, vb_d, vb_d].join(' '))
rect(x=-fr y=-fr width=fd height=fd)
ellipse#eye(cx=.35*fr cy=-.25*fr
rx=.1*fr ry=.15*fr)
use(xlink:href='#eye'
transform='scale(-1 1)')
path(d=`M${-.35*fr} ${.35*fr}
C${-.21*fr} ${.13*fr}
${+.21*fr} ${.13*fr}
${+.35*fr} ${.35*fr}`)
In the JavaScript, we get the face and the mouth elements. We read the face width, which is equal to the height and we use it to compute the maximum corner rounding. This is the value for which we get a circle and is equal to half the square edge. We also get the mouth path data, from where we extract the initial y coordinate of the control points and compute the final y coordinate of the same control points.
const _FACE = document.querySelector('rect'),
_MOUTH = document.querySelector('path'),
RMAX = .5*_FACE.getAttribute('width'),
DATA = _MOUTH.getAttribute('d').slice(1)
.replace('C', '').split(/s+/)
.map(c => +c),
CPY_INI = DATA[3],
CPY_RANGE = 2*(DATA[1] - DATA[3]);
The rest is very similar to all other transition on click demos so far, with just a few minor differences (note that we use an ease-out kind of timing function):
/* same as before */

function timing(k) { return 1 - Math.pow(1 - k, 2) };

function update() {
f += dir;

let k = f/NF, cpy = CPY_INI + timing(k)*CPY_RANGE;

_FACE.setAttribute('rx', (timing(k)*RMAX).toFixed(2));
_MOUTH.setAttribute(
'd',
`M${DATA.slice(0,2)}
C${DATA[2]} ${cpy} ${DATA[4]} ${cpy} ${DATA.slice(-2)}`
);

/* same as before */
};

/* same as before */
And so we have our silly result:
See the Pen by thebabydino (@thebabydino) on CodePen.

Emulating CSS Timing Functions with JavaScript is a post from CSS-Tricks
Source: CssTricks


Emulating CSS Timing Functions with JavaScript

CSS animations and transitions are great! However, while recently toying with an idea, I got really frustrated with the fact that gradients are only animatable in Edge (and IE 10+). Yes, we can do all sorts of tricks with background-position, background-size, background-blend-mode or even opacity and transform on a pseudo-element/ child, but sometimes these are just not enough. Not to mention that we run into similar problems when wanting to animate SVG attributes without a CSS correspondent.
Using a lot of examples, this article is going to explain how to smoothly go from one state to another in a similar fashion to that of common CSS timing functions using just a little bit of JavaScript, without having to rely on a library, so without including a lot of complicated and unnecessary code that may become a big burden in the future.

This is not how the CSS timing functions work. This is an approach that I find simpler and more intuitive than working with Bézier curves. I'm going to show how to experiment with different timing functions using JavaScript and dissect use cases. It is not a tutorial on how to do beautiful animation.
A few examples using a linear timing function
Let's start with a left to right linear-gradient() with a sharp transition where we want to animate the first stop. Here's a way to express that using CSS custom properties:
background: linear-gradient(90deg, #ff9800 var(--stop, 0%), #3c3c3c 0);
On click, we want the value of this stop to go from 0% to 100% (or the other way around, depending on the state it's already in) over the course of NF frames. If an animation is already running at the time of the click, we stop it, change its direction, then restart it.
We also need a few variables such as the request ID (this gets returned by requestAnimationFrame), the index of the current frame (an integer in the [0, NF] interval, starting at 0) and the direction our transition is going in (which is 1 when going towards 100% and -1 when going towards 0%).
While nothing is changing, the request ID is null. We also set the current frame index to 0 initially and the direction to -1, as if we've just arrived to 0% from 100%.
const NF = 80; // number of frames transition happens over

let rID = null, f = 0, dir = -1;

function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};

function update() {};

addEventListener('click', e => {
if(rID) stopAni(); // if an animation is already running, stop it
dir *= -1; // change animation direction
update();
}, false);
Now all that's left is to populate the update() function. Within it, we update the current frame index f. Then we compute a progress variable k as the ratio between this current frame index f and the total number of frames NF. Given that f goes from 0 to NF (included), this means that our progress k goes from 0 to 1. Multiply this with 100% and we get the desired stop.
After this, we check whether we've reached one of the end states. If we have, we stop the animation and exit the update() function.
function update() {
f += dir; // update current frame index

let k = f/NF; // compute progress

document.body.style.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
The result can be seen in the Pen below (note that we go back on a second click):
See the Pen by thebabydino (@thebabydino) on CodePen.
The way the pseudo-element is made to contrast with the background below is explained in an older article.
The above demo may look like something we could easily achieve with an element and translating a pseudo-element that can fully cover it, but things get a lot more interesting if we give the background-size a value that's smaller than 100% along the x axis, let's say 5em:
See the Pen by thebabydino (@thebabydino) on CodePen.
This gives us a sort of a "vertical blinds" effect that cannot be replicated in a clean manner with just CSS if we don't want to use more than one element.
Another option would be not to alternate the direction and always sweep from left to right, except only odd sweeps would be orange. This requires tweaking the CSS a bit:
--c0: #ff9800;
--c1: #3c3c3c;
background: linear-gradient(90deg,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
In the JavaScript, we ditch the direction variable and add a type one (typ) that switches between 0 and 1 at the end of every transition. That's when we also update all custom properties:
const S = document.body.style;

let typ = 0;

function update() {
let k = ++f/NF;

S.setProperty('--stop', `${+(k*100).toFixed(2)}%`);

if(!(f%NF)) {
f = 0;
S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);
S.setProperty('--stop', `0%`);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
This gives us the desired result (click at least twice to see how the effect differs from that in the first demo):
See the Pen by thebabydino (@thebabydino) on CodePen.
We could also change the gradient angle instead of the stop. In this case, the background rule becomes:
background: linear-gradient(var(--angle, 0deg),
#ff9800 50%, #3c3c3c 0);
In the JavaScript code, we tweak the update() function:
function update() {
f += dir;

let k = f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
stopAni();
return
}

rID = requestAnimationFrame(update)
};
We now have a gradient angle transition in between the two states (0deg and 180deg):
See the Pen by thebabydino (@thebabydino) on CodePen.
In this case, we might also want to keep going clockwise to get back to the 0deg state instead of changing the direction. So we just ditch the dir variable altogether, discard any clicks happening during the transition, and always increment the frame index f, resetting it to 0 when we've completed a full rotation around the circle:
function update() {
let k = ++f/NF;

document.body.style.setProperty(
'--angle',
`${+(k*180).toFixed(2)}deg`
);

if(!(f%NF)) {
f = f%(2*NF);
stopAni();
return
}

rID = requestAnimationFrame(update)
};

addEventListener('click', e => {
if(!rID) update()
}, false);
The following Pen illustrates the result - our rotation is now always clockwise:
See the Pen by thebabydino (@thebabydino) on CodePen.
Something else we could do is use a radial-gradient() and animate the radial stop:
background: radial-gradient(circle,
#ff9800 var(--stop, 0%), #3c3c3c 0);
The JavaScript code is identical to that of the first demo and the result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
We may also not want to go back when clicking again, but instead make another blob grow and cover the entire viewport. In this case, we add a few more custom properties to the CSS:
--c0: #ff9800;
--c1: #3c3c3c;
background: radial-gradient(circle,
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
The JavaScript is the same as in the case of the third linear-gradient() demo. This gives us the result we want:
See the Pen by thebabydino (@thebabydino) on CodePen.
A fun tweak to this would be to make our circle start growing from the point we clicked. To do so, we introduce two more custom properties, --x and --y:
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0)
When clicking, we set these to the coordinates of the point where the click happened:
addEventListener('click', e => {
if(!rID) {
S.setProperty('--x', `${e.clientX}px`);
S.setProperty('--y', `${e.clientY}px`);
update();
}
}, false);
This gives us the following result where we have a disc growing from the point where we clicked:
See the Pen by thebabydino (@thebabydino) on CodePen.
Another option would be using a conic-gradient() and animating the angular stop:
background: conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0%)
Note that in the case of conic-gradient(), we must use a unit for the zero value (whether that unit is % or an angular one like deg doesn't matter), otherwise our code won't work - writing conic-gradient(#ff9800 var(--stop, 0%), #3c3c3c 0) means nothing gets displayed.
The JavaScript is the same as for animating the stop in the linear or radial case, but bear in mind that this currently only works in Chrome with Experimental Web Platform Features enabled in chrome://flags.
The Experimental Web Platform Features flag enabled in Chrome Canary (63.0.3210.0).
Just for the purpose of displaying conic gradients in the browser, there's a polyfill by Lea Verou and this works cross-browser but doesn't allow using CSS custom properties.
The recording below illustrates how our code works:
Recording of how our first conic-gradient() demo works in Chrome with the flag enabled (live demo).
This is another situation where we might not want to go back on a second click. This means we need to alter the CSS a bit, in the same way we did for the last radial-gradient() demo:
--c0: #ff9800;
--c1: #3c3c3c;
background: conic-gradient(
var(--gc0, var(--c0)) var(--stop, 0%),
var(--gc1, var(--c1)) 0%)
The JavaScript code is exactly the same as in the corresponding linear-gradient() or radial-gradient() case and the result can be seen below:
Recording of how our second conic-gradient() demo works in Chrome with the flag enabled (live demo).
Before we move on to other timing functions, there's one more thing to cover: the case when we don't go from 0% to 100%, but in between any two values. We take the example of our first linear-gradient, but with a different default for --stop, let's say 85% and we also set a --stop-fin value - this is going to be the final value for --stop:
--stop-ini: 85%;
--stop-fin: 26%;
background: linear-gradient(90deg, #ff9800 var(--stop, var(--stop-ini)), #3c3c3c 0)
In the JavaScript, we read these two values - the initial (default) and the final one - and we compute a range as the difference between them:
const S = getComputedStyle(document.body),
INI = +S.getPropertyValue('--stop-ini').replace('%', ''),
FIN = +S.getPropertyValue('--stop-fin').replace('%', ''),
RANGE = FIN - INI;
Finally, in the update() function, we take into account the initial value and the range when setting the current value for --stop:
document.body.style.setProperty(
'--stop',
`${+(INI + k*RANGE).toFixed(2)}%`
);
With these changes we now have a transition in between 85% and 26% (and the other way on even clicks):
See the Pen by thebabydino (@thebabydino) on CodePen.
If we want to mix units for the stop value, things get hairier as we need to compute more things (box dimensions when mixing % and px, font sizes if we throw em or rem in the mix, viewport dimensions if we want to use viewport units, the length of the 0% to 100% segment on the gradient line for gradients that are not horizontal or vertical), but the basic idea remains the same.
Emulating ease-in/ ease-out
An ease-in kind of function means the change in value happens slow at first and then accelerates. ease-out is exactly the opposite - the change happens fast in the beginning, but then slows down towards the end.
The ease-in (left) and ease-out (right) timing functions (live).
The slope of the curves above gives us the rate of change. The steeper it is, the faster the change in value happens.
We can emulate these functions by tweaking the linear method described in the first section. Since k takes values in the [0, 1] interval, raising it to any positive power also gives us a number within the same interval. The interactive demo below shows the graph of a function f(k) = pow(k, p) (k raised to an exponent p) shown in purple and that of a function g(k) = 1 - pow(1 - k, p) shown in red on the [0, 1] interval versus the identity function id(k) = k (which corresponds to a linear timing function).
See the Pen by thebabydino (@thebabydino) on CodePen.
When the exponent p is equal to 1, the graphs of the f and g functions are identical to that of the identity function.
When exponent p is greater than 1, the graph of the f function is below the identity line - the rate of change increases as k increases. This is like an ease-in type of function. The graph of the g function is above the identity line - the rate of change decreases as k increases. This is like an ease-out type of function.
It seems an exponent p of about 2 gives us an f that's pretty similar to ease-in, while g is pretty similar to ease-out. With a bit more tweaking, it looks like the best approximation is for a p value of about 1.675:
See the Pen by thebabydino (@thebabydino) on CodePen.
In this interactive demo, we want the graphs of the f and g functions to be as close as possible to the dashed lines, which represent the ease-in timing function (below the identity line) and the ease-out timing function (above the identity line).
Emulating ease-in-out
The CSS ease-in-out timing function looks like in the illustration below:
The ease-in-out timing function (live).
So how can we get something like this?
Well, that's what harmonic functions are for! More exactly, the ease-in-out out shape is reminiscent the shape of the sin() function on the [-90°,90°] interval.
The sin(k) function on the [-90°,90°] interval (live).
However, we don't want a function whose input is in the [-90°,90°] interval and output is in the [-1,1] interval, so let's fix this!
This means we need to squish the hashed rectangle ([-90°,90°]x[-1,1]) in the illustration above into the unit one ([0,1]x[0,1]).
First, let's take the domain [-90°,90°]. If we change our function to be sin(k·180°) (or sin(k·π) in radians), then our domain becomes [-.5,.5] (we can check that -.5·180° = 90° and .5·180° = 90°):
The sin(k·π) function on the [-.5,.5] interval (live).
We can shift this domain to the right by .5 and get the desired [0,1] interval if we change our function to be sin((k - .5)·π) (we can check that 0 - .5 = -.5 and 1 - .5 = .5):
The sin((k - .5)·π) function on the [0,1] interval (live).
Now let's get the desired codomain. If we add 1 to our function making it sin((k - .5)·π) + 1 this shifts out codomain up into the [0, 2] interval:
The sin((k - .5)·π) + 1 function on the [0,1] interval (live).
Dividing everything by 2 gives us the (sin((k - .5)·π) + 1)/2 function and compacts the codomain into our desired [0,1] interval:
The (sin((k - .5)·π) + 1)/2 function on the [0,1] interval (live).
This turns out to be a good approximation of the ease-in-out timing function (represented with an orange dashed line in the illustration above).
Comparison of all these timing functions
Let's say we want to have a bunch of elements with a linear-gradient() (like in the third demo). On click, their --stop values go from 0% to 100%, but with a different timing function for each.
In the JavaScript, we create a timing functions object with the corresponding function for each type of easing:
tfn = {
'linear': function(k) {
return k;
},
'ease-in': function(k) {
return Math.pow(k, 1.675);
},
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675);
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1);
}
};
For each of these, we create an article element:
const _ART = [];

let frag = document.createDocumentFragment();

for(let p in tfn) {
let art = document.createElement('article'),
hd = document.createElement('h3');

hd.textContent = p;
art.appendChild(hd);
art.setAttribute('id', p);
_ART.push(art);
frag.appendChild(art);
}

n = _ART.length;
document.body.appendChild(frag);
The update function is pretty much the same, except we set the --stop custom property for every element as the value returned by the corresponding timing function when fed the current progress k. Also, when resetting the --stop to 0% at the end of the animation, we also need to do this for every element.
function update() {
let k = ++f/NF;

for(let i = 0; i < n; i++) {
_ART[i].style.setProperty(
'--stop',
`${+tfn[_ART[i].id](k).toFixed(5)*100}%`
);
}

if(!(f%NF)) {
f = 0;

S.setProperty('--gc1', `var(--c${typ})`);
typ = 1 - typ;
S.setProperty('--gc0', `var(--c${typ})`);

for(let i = 0; i < n; i++)
_ART[i].style.setProperty('--stop', `0%`);

stopAni();
return;
}

rID = requestAnimationFrame(update)
};
This gives us a nice visual comparison of these timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
They all start and finish at the same time, but while the progress is constant for the linear one, the ease-in one starts slowly and then accelerates, the ease-out one starts fast and then slows down and, finally, the ease-in-out one starts slowly, accelerates and then slows down again at the end.
Timing functions for bouncing transitions
I first came across the concept years ago, in Lea Verou's CSS Secrets talk. These happen when the y (even) values in a cubic-bezier() function are outside the [0, 1] range and the effect they create is of the animated value going outside the interval between its initial and final value.
This bounce can happen right after the transition starts, right before it finishes or at both ends.
A bounce at the start means that, at first, we don't go towards the final state, but in the opposite direction. For example, if want to animate a stop from 43% to 57% and we have a bounce at the start, then, at first, out stop value doesn't increase towards 57%, but decreases below 43% before going back up to the final state. Similarly, if we go from an initial stop value of 57% to a final stop value of 43% and we have a bounce at the start, then, at first, the stop value increases above 57% before going down to the final value.
A bounce at the end means we overshoot our final state and only then go back to it. If want to animate a stop from 43% to 57% and we have a bounce at the end, then we start going normally from the initial state to the final one, but towards the end, we go above 57% before going back down to it. And if we go from an inital stop value of 57% to a final stop value of 43% and we have a bounce at the end, then, at first, we go down towards the final state, but, towards the end, we pass it and we briefly have stop values below 43% before our transition finishes there.
If what they do is still difficult to grasp, below there's a comparative example of all three of them in action.
The three cases.
These kinds of timing functions don't have their own keywords associated, but they look cool and they are what we want in a lot of situations.
Just like in the case of ease-in-out, the quickest way of getting them is by using harmonic functions. The difference lies in the fact that now we don't start from the [-90°,90°] domain anymore.
For a bounce at the beginning, we start with the [s, 0°] portion of the sin() function, where s (the start angle) is in the (-180°,-90°) interval. The closer it is to -180°, the bigger the bounce is and the faster it will go to the final state after it. So we don't want it to be really close to -180° because the result would look too unnatural. We also want it to be far enough from -90° that the bounce is noticeable.
In the interactive demo below, you can drag the slider to change the start angle and then click on the stripe at the bottom to see the effect in action:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,0]x[sin(s),0]) is the area we need move and scale into the [0,1]x[0,1] area in order to get our timing function. The part of the curve that's below its lower edge is where the bounce happens. You can adjust the start angle using the slider and then click on the bottom bar to see how the transition looks for different start angles.
Just like in the ease-in-out case, we first squish the domain into the [-1,0] interval by dividing the argument with the range (which is the maximum 0 minus the minimum s). Therefore, our function becomes sin(-k·s) (we can check that -(-1)·s = s and -0·s = 0):
The sin(-k·s) function on the [-1,0] interval (live).
Next, we shift this interval to the right (by 1, into [0,1]). This makes our function sin(-(k - 1)·s) = sin((1 - k)·s) (it checks that 0 - 1 = -1 and 1 - 1 = 0):
The sin(-(k - 1)·s) function on the [0,1] interval (live).
We then shift the codomain up by its value at 0 (sin((1 - 0)*s) = sin(s)). Our function is now sin((1 - k)·s) - sin(s) and our codomain [0,-sin(s)].
The sin(-(k - 1)·s) - sin(s) function on the [0,1] interval (live).
The last step is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is -sin(s)). This means our final easing function is 1 - sin((1 - k)·s)/sin(s)
The 1 - sin((1 - k)·s)/sin(s) function on the [0,1] interval (live).
For a bounce at the end, we start with the [0°, e] portion of the sin() function, where e (the end angle) is in the (90°,180°) interval. The closer it is to 180°, the bigger the bounce is and the faster it will move from the initial state to the final one before it overshoots it and the bounce happens. So we don't want it to be really close to 180° as the result would look too unnatural. We also want it to be far enough from 90° so that the bounce is noticeable.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([0,e]x[0,sin(e)]) is the area we need to squish and move into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's below its upper edge is where the bounce happens.
We start by squishing the domain into the [0,1] interval by dividing the argument with the range (which is the maximum e minus the minimum 0). Therefore, our function becomes sin(k·e) (we can check that 0·e = 0 and 1·e = e):
The sin(k·e) function on the [0,1] interval (live).
What's still left to do is to expand the codomain into the [0,1] range. We do this by dividing by its upper limit (which is sin(e)). This means our final easing function is sin(k·e)/sin(e).
The sin(k·e)/sin(e) function on the [0,1] interval (live).
If we want a bounce at each end, we start with the [s, e] portion of the sin() function, where s is in the (-180°,-90°) interval and e in the (90°,180°) interval. The larger s and e are in absolute values, the bigger the corresponding bounces are and the more of the total transition time is spent on them alone. On the other hand, the closer their absolute values get to 90°, the less noticeable their corresponding bounces are. So, just like in the previous two cases, it's all about finding the right balance.
See the Pen by thebabydino (@thebabydino) on CodePen.
In the interactive demo above, the hashed area ([s,e]x[sin(s),sin(e)]) is the area we need to move and scale into the [0,1]x[0,1] square in order to get our timing function. The part of the curve that's beyond its horizontal edges is where the bounces happen.
We start by shifting the domain to the right into the [0,e - s] interval. This means our function becomes sin(k + s) (we can check that 0 + s = s and that e - s + s = e).
The sin(k + s) function on the [0,e - s] interval (live).
Then we shrink the domain to fit into the [0,1] interval, which gives us the function sin(k·(e - s) + s).
The sin(k·(e - s) + s) function on the [0,1] interval (live).
Moving on to the codomain, we first shift it up by its value at 0 (sin(0·(e - s) + s)), which means we now have sin(k·(e - s) + s) - sin(s). This gives us the new codomain [0,sin(e) - sin(s)].
The sin(k·(e - s) + s) - sin(s) function on the [0,1] interval (live).
Finally, we shrink the codomain to the [0,1] interval by dividing with the range (sin(e) - sin(s)), so our final function is (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)).
The (sin(k·(e - s) + s) - sin(s))/(sin(e - sin(s)) function on the [0,1] interval (live).
So in order to do a similar comparative demo to that for the JS equivalents of the CSS linear, ease-in, ease-out, ease-in-out, our timing functions object becomes:
tfn = {
'bounce-ini': function(k) {
return 1 - Math.sin((1 - k)*s)/Math.sin(s);
},
'bounce-fin': function(k) {
return Math.sin(k*e)/Math.sin(e);
},
'bounce-ini-fin': function(k) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s));
}
};
The s and e variables are the values we get from the two range inputs that allow us to control the bounce amount.
The interactive demo below shows the visual comparison of these three types of timing functions:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alternating animations
In CSS, setting animation-direction to alternate also reverses the timing function. In order to better understand this, consider a .box element on which we animate its transform property such that we move to the right. This means our @keyframes look as follows:
@keyframes shift {
0%, 10% { transform: none }
90%, 100% { transform: translate(50vw) }
}
We use a custom timing function that allows us to have a bounce at the end and we make this animation alternate - that is, go from the final state (translate(50vw)) back to the initial state (no translation) for the even-numbered iterations (second, fourth and so on).
animation: shift 1s cubic-bezier(.5, 1, .75, 1.5) infinite alternate
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
One important thing to notice here is that, for the even-numbered iterations, our bounce doesn't happen at the end, but at the start - the timing function is reversed. Visually, this means it's reflected both horizontally and vertically with respect to the .5,.5 point.
The normal timing function (f, in red, with a bounce at the end) and the symmetrical reverse one (g, in purple, with a bounce at the start) (live)
In CSS, there is no way of having a different timing function other than the symmetrical one on going back if we are to use this set of keyframes and animation-direction: alternate. We can introduce the going back part into the keyframes and control the timing function for each stage of the animation, but that's outside the scope of this article.
When changing values with JavaScript in the fashion presented so far in this article, the same thing happens by default. Consider the case when we want to animate the stop of a linear-gradient() between an initial and a final position and we want to have a bounce at the end. This is pretty much the last example presented in the first section with timing function that lets us have a bounce at the end (one in the bounce-fin category described before) instead of a linear one.
The CSS is exactly the same and we only make a few minor changes to the JavaScript code. We set a limit angle E and we use a custom bounce-fin kind of timing function in place of the linear one:
const E = .75*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
};

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + timing(k)*RANGE).toFixed(2)}%`
);

/* same as before */
};

/* same as before */
The result can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
In the initial state, the stop is at 85%. We animate it to 26% (which is the final state) using a timing function that gives us a bounce at the end. This means we go beyond our final stop position at 26% before going back up and stopping there. This is what happens during the odd iterations.
During the even iterations, this behaves just like in the CSS case, reversing the timing function, so that the bounce happens at the beginning, not at the end.
But what if we don't want the timing function to be reversed?
In this case, we need to use the symmetrical function. For any timing function f(k) defined on the [0,1] interval (this is the domain), whose values are in the [0,1] (codomain), the symmetrical function we want is 1 - f(1 - k). Note that functions whose shape is actually symmetrical with respect to the .5,.5 point, like linear or ease-in-out are identical to their symmetrical functions.
See the Pen by thebabydino (@thebabydino) on CodePen.
So what we do is use our timing function f(k) for the odd iterations and use 1 - f(1 - k) for the even ones. We can tell whether an iteration is odd or even from the direction (dir) variable. This is 1 for odd iterations and -1 for even ones.
This means we can combine our two timing functions into one: m + dir*f(m + dir*k).
Here, the multiplier m is 0 for the odd iterations (when dir is 1) and 1 for the even ones (when dir is -1), so we can compute it as .5*(1 - dir):
dir = +1 → m = .5*(1 - (+1)) = .5*(1 - 1) = .5*0 = 0
dir = -1 → m = .5*(1 - (-1)) = .5*(1 + 1) = .5*2 = 1
This way, our JavaScript becomes:
let m;

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--stop',
`${+(INI + (m + dir*timing(m + dir*k))*RANGE).toFixed(2)}%`
);

/* same as before */
};

addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
The final result can be seen in this Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
Even more examples
Gradient stops are not the only things that aren't animatable cross-browser with just CSS.
Gradient end going from orange to violet
For a first example of something different, let's say we want the orange in our gradient to animate to a kind of violet. We start with a CSS that looks something like this:
--c-ini: #ff9800;
--c-fin: #a048b9;
background: linear-gradient(90deg,
var(--c, var(--c-ini)), #3c3c3c)
In order to interpolate between the initial and final values, we need to know the format we get when reading them via JavaScript - is it going to be the same format we set them in? Is it going to be always rgb()/ rgba()?
Here is where things get a bit hairy. Consider the following test, where we have a gradient where we've used every format possible:
--c0: hsl(150, 100%, 50%); // springgreen
--c1: orange;
--c2: #8a2be2; // blueviolet
--c3: rgb(220, 20, 60); // crimson
--c4: rgba(255, 245, 238, 1); // seashell with alpha = 1
--c5: hsla(51, 100%, 50%, .5); // gold with alpha = .5
background: linear-gradient(90deg,
var(--c0), var(--c1),
var(--c2), var(--c3),
var(--c4), var(--c5))
We read the computed values of the gradient image and the individual custom properties --c0 through --c5 via JavaScript.
let s = getComputedStyle(document.body);

console.log(s.backgroundImage);
console.log(s.getPropertyValue('--c0'), 'springgreen');
console.log(s.getPropertyValue('--c1'), 'orange');
console.log(s.getPropertyValue('--c2'), 'blueviolet');
console.log(s.getPropertyValue('--c3'), 'crimson');
console.log(s.getPropertyValue('--c4'), 'seashell (alpha = 1)');
console.log(s.getPropertyValue('--c5'), 'gold (alpha = .5)');
The results seem a bit inconsistent.
Screenshots showing what gets logged in Chrome, Edge and Firefox (live).
Whatever we do, if we have an alpha of strictly less than 1, what we get via JavaScript seems to be always an rgba() value, regardless of whether we've set it with rgba() or hsla().
All browsers also agree when reading the custom properties directly, though, this time, what we get doesn't seem to make much sense: orange, crimson and seashell are returned as keywords regardless of how they were set, but we get hex values for springgreen and blueviolet. Except for orange, which was added in Level 2, all these values were added to CSS in Level 3, so why do we get some as keywords and others as hex values?
For the background-image, Firefox always returns the fully opaque values only as rgb(), while Chrome and Edge return them as either keywords or hex values, just like they do in the case when we read the custom properties directly.
Oh well, at least that lets us know we need to take into account different formats.
So the first thing we need to do is map the keywords to rgb() values. Not going to write all that manually, so a quick search finds this repo - perfect, it's exactly what we want! We can now set that as the value of a CMAP constant.
The next step here is to create a getRGBA(c) function that would take a string representing a keyword, a hex or an rgb()/ rgba() value and return an array containing the RGBA values ([red, green, blue, alpha]).
We start by building our regular expressions for the hex and rgb()/ rgba() values. These are a bit loose and would catch quite a few false positives if we were to have user input, but since we're only using them on CSS computed style values, we can afford to take the quick and dirty path here:
let re_hex = /^#([a-fd]{1,2})([a-fd]{1,2})([a-fd]{1,2})$/i,
re_rgb = /^rgba?((d{1,3},s){2}d{1,3}(,s((0|1)?.?d*))?)/;
Then we handle the three types of values we've seen we might get by reading the computed styles:
if(c in CMAP) return CMAP[c]; // keyword lookup, return rgb

if([4, 7].indexOf(c.length) !== -1 && re_hex.test(c)) {
c = c.match(re_hex).slice(1); // remove the '#'
if(c[0].length === 1) c = c.map(x => x + x);
// go from 3-digit form to 6-digit one
c.push(1); // add an alpha of 1

// return decimal valued RGBA array
return c.map(x => parseInt(x, 16))
}

if(re_rgb.test(c)) {
// extract values
c = c.replace(/rgba?(/, '').replace(')', '').split(',').map(x => +x.trim());
if(c.length === 3) c.push(1); // if no alpha specified, use 1

return c // return RGBA array
}
Now after adding the keyword to RGBA map (CMAP) and the getRGBA() function, our JavaScript code doesn't change much from the previous examples:
const INI = getRGBA(S.getPropertyValue('--c-ini').trim()),
FIN = getRGBA(S.getPropertyValue('--c-fin').trim()),
RANGE = [],
ALPHA = 1 - INI[3] || 1 - FIN[3];

/* same as before */

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + k*RANGE[i])).join(',')})`
);

/* same as before */
};

(function init() {
if(!ALPHA) INI.pop(); // get rid of alpha if always 1
RANGE.splice(0, 0, ...INI.map((c, i) => FIN[i] - c));
})();

/* same as before */
This gives us a linear gradient animation:
See the Pen by thebabydino (@thebabydino) on CodePen.
We can also use a different, non-linear timing function, for example one that allows for a bounce at the end:
const E = .8*Math.PI;

/* same as before */

function timing(k) {
return Math.sin(k*E)/Math.sin(E)
}

function update() {
/* same as before */

document.body.style.setProperty(
'--c',
`rgb${ALPHA ? 'a' : ''}(
${INI.map((c, i) => Math.round(c + timing(k)*RANGE[i])).join(',')})`
);

/* same as before */
};

/* same as before */
This means we go all the way to a kind of blue before going back to our final violet:
See the Pen by thebabydino (@thebabydino) on CodePen.
Do note however that, in general, RGBA transitions are not the best place to illustrate bounces. That's because the RGB channels are stricly limited to the [0,255] range and the alpha channel is strictly limited to the [0,1] range. rgb(255, 0, 0) is as red as red gets, there's no redder red with a value of over 255 for the first channel. A value of 0 for the alpha channel means completely transparent, there's no greater transparency with a negative value.
By now, you're probably already bored with gradients, so let's switch to something else!
Smooth changing SVG attribute values
At this point, we cannot alter the geometry of SVG elements via CSS. We should be able to as per the SVG2 spec and Chrome does support some of this stuff, but what if we want to animate the geometry of SVG elements now, in a more cross-browser manner?
Well, you've probably guessed it, JavaScript to the rescue!
Growing a circle
Our first example is that of a circle whose radius goes from nothing (0) to a quarter of the minimum viewBox dimension. We keep the document structure simple, without any other aditional elements.
<svg viewBox='-100 -50 200 100'>
<circle/>
</svg>
For the JavaScript part, the only notable difference from the previous demos is that we read the SVG viewBox dimensions in order to get the maximum radius and we now set the r attribute within the update() function, not a CSS variable (it would be immensely useful if CSS variables were allowed as values for such attributes, but, sadly, we don't live in an ideal world):
const _G = document.querySelector('svg'),
_C = document.querySelector('circle'),
VB = _G.getAttribute('viewBox').split(' '),
RMAX = .25*Math.min(...VB.slice(2)),
E = .8*Math.PI;

/* same as before */

function update() {
/* same as before */

_C.setAttribute('r', (timing(k)*RMAX).toFixed(2));

/* same as before */
};

/* same as before */
Below, you can see the result when using a bounce-fin kind of timing function:
See the Pen by thebabydino (@thebabydino) on CodePen.
Pan and zoom map
Another SVG example is a smooth pan and zoom map demo. In this case, we take a map like those from amCharts, clean up the SVG and then create this effect by triggering a linear viewBox animation when pressing the +/ - keys (zoom) and the arrow keys (pan).
The first thing we do in the JavaScript is create a navigation map, where we take the key codes of interest and attach info about what we do when the corresponding keys are pressed (note that we need different key codes for + and - in Firefox for some reason).
const NAV_MAP = {
187: { dir: 1, act: 'zoom', name: 'in' } /* + */,
61: { dir: 1, act: 'zoom', name: 'in' } /* + Firefox ¯_(ツ)_/¯ */,
189: { dir: -1, act: 'zoom', name: 'out' } /* - */,
173: { dir: -1, act: 'zoom', name: 'out' } /* - Firefox ¯_(ツ)_/¯ */,
37: { dir: -1, act: 'move', name: 'left', axis: 0 } /* ⇦ */,
38: { dir: -1, act: 'move', name: 'up', axis: 1 } /* ⇧ */,
39: { dir: 1, act: 'move', name: 'right', axis: 0 } /* ⇨ */,
40: { dir: 1, act: 'move', name: 'down', axis: 1 } /* ⇩ */
}
When pressing the + key, what we want to do is zoom in. The action we perform is 'zoom' in the positive direction - we go 'in'. Similarly, when pressing the - key, the action is also 'zoom', but in the negative (-1) direction - we go 'out'.
When pressing the arrow left key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the negative (-1) direction - we go 'left'. When pressing the arrow up key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the negative (-1) direction - we go 'up'.
When pressing the arrow right key, the action we perform is 'move' along the x axis (which is the first axis, at index 0) in the positive direction - we go 'right'. When pressing the arrow down key, the action we perform is 'move' along the y axis (which is the second axis, at index 1) in the positive direction - we go 'down'.
We then get the SVG element, its initial viewBox, set the maximum zoom out level to these initial viewBox dimensions and set the smallest possible viewBox width to a much smaller value (let's say 8).
const _SVG = document.querySelector('svg'),
VB = _SVG.getAttribute('viewBox').split(' ').map(c => +c),
DMAX = VB.slice(2), WMIN = 8;
We also create an empty current navigation object to hold the current navigation action data and a target viewBox array to contain the final state we animate the viewBox to for the current animation.
let nav = {}, tg = Array(4);
On 'keyup', if we don't have any animation running already and the key that was pressed is one of interest, we get the current navigation object from the navigation map we created at the beginning. After this, we handle the two action cases ('zoom'/ 'move') and call the update() function:
addEventListener('keyup', e => {
if(!rID && e.keyCode in NAV_MAP) {
nav = NAV_MAP[e.keyCode];

if(nav.act === 'zoom') {
/* what we do if the action is 'zoom' */
}

else if(nav.act === 'move') {
/* what we do if the action is 'move' */
}

update()
}
}, false);
Now let's see what we do if we zoom. First off, and this is a very useful programming tactic in general, not just here in particular, we get the edge cases that make us exit the function out of the way.
So what are our edge cases here?
The first one is when we want to zoom out (a zoom in the negative direction) when our whole map is already in sight (the current viewBox dimensions are bigger or equal to the maximum ones). In our case, this should happen if we want to zoom out at the very beginning because we start with the whole map in sight.
The second edge case is when we hit the other limit - we want to zoom in, but we're at the maximum detail level (the current viewBox dimensions are smaller or equal to the minimum ones).
Putting the above into JavaScript code, we have:
if(nav.act === 'zoom') {
if((nav.dir === -1 && VB[2] >= DMAX[0]) ||
(nav.dir === 1 && VB[2] <= WMIN)) {
console.log(`cannot ${nav.act} ${nav.name} more`);
return
}

/* main case */
}
Now that we've handled the edge cases, let's move on to the main case. Here, we set the target viewBox values. We use a 2x zoom on each step, meaning that when we zoom in, the target viewBox dimensions are half the ones at the start of the current zoom action, and when we zoom out they're double. The target offsets are half the difference between the maximum viewBox dimensions and the target ones.
if(nav.act === 'zoom') {
/* edge cases */

for(let i = 0; i < 2; i++) {
tg[i + 2] = VB[i + 2]/Math.pow(2, nav.dir);
tg[i] = .5*(DMAX[i] - tg[i + 2]);
}
}
Next, let's see what we do if we want to move instead of zooming.
In a similar fashion, we get the edge cases that make us exit the function out of the way first. Here, these happen when we're at an edge of the map and we want to keep going in that direction (whatever the direction might be). Since originally the top left corner of our viewBox is at 0,0, this means we cannot go below 0 or above the maximum viewBox size minus the current one. Note that given we're initially fully zoomed out, this also means we cannot move in any direction until we zoom in.
else if(nav.act === 'move') {
if((nav.dir === -1 && VB[nav.axis] <= 0) ||
(nav.dir === 1 && VB[nav.axis] >= DMAX[nav.axis] - VB[2 + nav.axis])) {
console.log(`at the edge, cannot go ${nav.name}`);
return
}

/* main case */
For the main case, we move in the desired direction by half the viewBox size along that axis:
else if(nav.act === 'move') {
/* edge cases */

tg[nav.axis] = VB[nav.axis] + .5*nav.dir*VB[2 + nav.axis]
}
Now let's see what we need to do inside the update() function. This is going to be pretty similar to previous demos, except now we need to handle the 'move' and 'zoom' cases separately. We also create an array to store the current viewBox data in (cvb):
function update() {
let k = ++f/NF, j = 1 - k, cvb = VB.slice();

if(nav.act === 'zoom') {
/* what we do if the action is zoom */
}

if(nav.act === 'move') {
/* what we do if the action is move */
}

_SVG.setAttribute('viewBox', cvb.join(' '));

if(!(f%NF)) {
f = 0;
VB.splice(0, 4, ...cvb);
nav = {};
tg = Array(4);
stopAni();
return
}

rID = requestAnimationFrame(update)
};
In the 'zoom' case, we need to recompute all viewBox values. We do this with linear interpolation between the values at the start of the animation and the target values we've previously computed:
if(nav.act === 'zoom') {
for(let i = 0; i < 4; i++)
cvb[i] = j*VB[i] + k*tg[i];
}
In the 'move' case, we only need to recompute one viewBox value - the offset for the axis we move along:
if(nav.act === 'move')
cvb[nav.axis] = j*VB[nav.axis] + k*tg[nav.axis];
And that's it! We now have a working pan and zoom demo with smooth linear transitions in between states:
See the Pen by thebabydino (@thebabydino) on CodePen.
From sad square to happy circle
Another example would be morphing a sad square SVG into a happy circle. We create an SVG with a square viewBox whose 0,0 point is right in the middle. Symmetrical with respect to the origin of the SVG system of coordinates we have a square (a rect element) covering 80% of the SVG. This is our face. We create the eyes with an ellipse and a copy of it, symmetrical with respect to the vertical axis. The mouth is a cubic Bézier curve created with a path element.
- var vb_d = 500, vb_o = -.5*vb_d;
- var fd = .8*vb_d, fr = .5*fd;

svg(viewBox=[vb_o, vb_o, vb_d, vb_d].join(' '))
rect(x=-fr y=-fr width=fd height=fd)
ellipse#eye(cx=.35*fr cy=-.25*fr
rx=.1*fr ry=.15*fr)
use(xlink:href='#eye'
transform='scale(-1 1)')
path(d=`M${-.35*fr} ${.35*fr}
C${-.21*fr} ${.13*fr}
${+.21*fr} ${.13*fr}
${+.35*fr} ${.35*fr}`)
In the JavaScript, we get the face and the mouth elements. We read the face width, which is equal to the height and we use it to compute the maximum corner rounding. This is the value for which we get a circle and is equal to half the square edge. We also get the mouth path data, from where we extract the initial y coordinate of the control points and compute the final y coordinate of the same control points.
const _FACE = document.querySelector('rect'),
_MOUTH = document.querySelector('path'),
RMAX = .5*_FACE.getAttribute('width'),
DATA = _MOUTH.getAttribute('d').slice(1)
.replace('C', '').split(/s+/)
.map(c => +c),
CPY_INI = DATA[3],
CPY_RANGE = 2*(DATA[1] - DATA[3]);
The rest is very similar to all other transition on click demos so far, with just a few minor differences (note that we use an ease-out kind of timing function):
/* same as before */

function timing(k) { return 1 - Math.pow(1 - k, 2) };

function update() {
f += dir;

let k = f/NF, cpy = CPY_INI + timing(k)*CPY_RANGE;

_FACE.setAttribute('rx', (timing(k)*RMAX).toFixed(2));
_MOUTH.setAttribute(
'd',
`M${DATA.slice(0,2)}
C${DATA[2]} ${cpy} ${DATA[4]} ${cpy} ${DATA.slice(-2)}`
);

/* same as before */
};

/* same as before */
And so we have our silly result:
See the Pen by thebabydino (@thebabydino) on CodePen.

Emulating CSS Timing Functions with JavaScript is a post from CSS-Tricks
Source: CssTricks


Variable Fonts from Adobe Originals

It's one thing to see a variable fonts demo (oooooo one font can change things like weight, width, and slant?) but it feels a lot more real when fonts you see and work with all the time go variable. Adobe made six of them available: Source Sans, Source Serif, Source Code, Myriad, Acumin, and Minion. You can't serve them on the web directly through TypeKit yet, but you can download them from GitHub to start playing.
Print designers have just as much reason to be excited, or perhaps more, as so long as you have software that supports variable fonts, you can use them right now:

Direct Link to Article — Permalink
Variable Fonts from Adobe Originals is a post from CSS-Tricks
Source: CssTricks