CSS transform: scale very shaky/choppy/jerky in Safari - javascript

I'm trying to implement a scroll based shrinking effect on on a div element. As the user scrolls, the div shrinks in height (using scale) until it finally disappears. I'm using React to render the HTML and styled-components for the CSS.
I'm having problems with the performance of this effect, specifically only with Safari, Version 13.1 (15609.1.20.111.8). In Safari, the transition effect is very janky and does not happen smoothly.
Chrome and Firefox work perfectly.
Here is the React component:
const MyComponent = React.forwardRef((_, ref) => {
const [scaleOffset, setScaleOffset] = React.useState(1)
const onScroll = React.useCallback(debounceWindowEventCallback(() => {
const paddingOffset = window.scrollY - ref.current.offsetTop
if (paddingOffset >= 0) setScaleOffset(1 - (paddingOffset / 100))
}), [ref]);
React.useEffect(() => {
if (scaleOffset <= 0) {
window.removeEventListener('scroll', onScroll)
console.log('Border has been fully shrunk')
}
}, [onScroll, scaleOffset])
React.useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [])
return (
<RootElement ref={ref}>
<ShrinkBorderTop scaleOffset={scaleOffset} />
</RootElement>
);
});
And the relevant styled components are:
const ShrinkBorderTop = styled.div`
position: absolute;
background-color: white;
transition: transform .175s ease-out;
width: 100%;
height: 200px;
transform: ${({ scaleOffset }) => `scale(1, ${scaleOffset})`};
`
What I've tried:
I initially thought its because my component was rendering too much (every scroll event) so I implemented the debounceWindowEventCallback function which uses window.requestAnimationFrame to ensure that there's only a single paint per frame. Unfortunately, that didn't help.
I then tried adding will-change: transition, transform, height, width; to ShrinkBorderTop, after googling the problem. This also didn't help.
Then I tried adding perspective: 1000; backface-visibility: hidden; to ShrinkBorderTop, this also didn't work.
I'm stumped as to what could be causing this problem, in my view its quite a standard animation and I've applied all the performance boosts I'm aware of, in addition to scale being hardware accelerated by default, but the problem still persists, only in safari.
What could be causing this?

Related

React styles update only after wheel event fulfilled

What I want to achieve is smoothly scaled div container while scrolling (using mouse wheel to be strict) so user can zoom in and out.
However, my styles are "applied" by the browser only either when I scroll really slow or scroll normally and then wait about 0.2 seconds (after that time the changes are "bunched up"). I would like for the changes to be visible even during "fast" scrolling, not at the end.
The element with listener:
<div onWheel={(event) => {
console.log("wheeling"); // this console log fires frequently,
// and I want to update styles at the same rate
changeZoom(event);
}}
>
<div ref={scaledItem}> // content div that will be scaled according to event.deltaY
... // contents
</div>
</div>
My React code:
const changeZoom = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
if (!scaledItem.current) return;
const newZoom = parseFloat(scaledItem.current.style.scale) + event.deltaY * 0.001;
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.scale = currentZoom.toString();
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.scale = "1";
}, [])
What I have tried first was to omit all the React states, and edit scaledItem.current.style.scale directly from useCallback, but the changes took place in a bunch, after the wheeling events stopped coming. Then I moved zoom amount to currentZoom useState hook, but rerenders don't help either.
Edit:
I have also tried adding EventListener inside useEffect directly to the DOM Node:
useEffect(() => {
if (!scaledItemWrapper.current) return; // ref for wrapper of my scaled content
const container = scaledItemWrapper.current;
container.addEventListener("wheel", changeZoom);
return () => {
container.removeEventListener("wheel", changeZoom);
};
}, [changeZoom]);
Instead of setting up multiple states and observing can you try using a single state below is a working example. Try this if this works
https://codesandbox.io/s/wonderful-cerf-69doe?file=/src/App.js:0-727
export default () => {
const [pos, setPos] = useState({ x: 0, y: 0, scale: 1 });
const changeZoom = (e) => {
e.preventDefault();
const delta = e.deltaY * -0.01;
const newScale = pos.scale + delta;
const ratio = 1 - newScale / pos.scale;
setPos({
scale: newScale,
x: pos.x + (e.clientX - pos.x) * ratio,
y: pos.y + (e.clientY - pos.y) * ratio
});
};
return (
<div onWheelCapture={changeZoom}>
<img
src="https://source.unsplash.com/random/300x300?sky"
style={{
transformOrigin: "0 0",
transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`
}}
/>
</div>
);
};
Use a CSS transition
What I want to achieve is smoothly scaled div container while scrolling
The question's JavaScript
I didn't have to make changes to the posted JavaScript in my answer's code snippets, besides replacing scale with transform: scale() because it currently has incomplete browser support. Perhaps this can be written better but it does the job here, and is not the cause of the choppy behavior you observe.
Creating fluid motion from choppy input
Scroll events are by nature "bunched" because they arrive as the wheel is being turned "a notch". While that's not as true for all scrollable devices, it is for most people's mouse, so we have to deal with it for the foreseeable future. The browser also does additional bunching, in case of fast motion, but even without that the problem is already there.
So it's best to write code in a way that choppy input still results in a fluid motion, regardless of the step size. Once you have that, it automatically accounts for additional bunching by the browser.
You can add a CSS transition on the transform property to smooth out the scaling movement. It seems to work well with a value of 0.2 seconds, which I assume makes sense as it spreads the motion over the 0.2 seconds the browser is bunching up the changes in.
transition: transform 0.2s ease-out;
Performance implications
As a bonus, your app can keep rendering just 5 times a second.
Conversely, a solution that causes React to capture the maximum amount of fine grained scroll events will likely cause performance issues. A CSS transform is a lot cheaper then achieving the same effect through repeated renders.
Demonstration
You can observe the difference in the following 2 snippets.
It only runs properly if you open it full page. Otherwise it works but it will scroll the whole page too. I didn't want to make the code overly complex just to prevent that on SO.
Without transition (choppy)
const {useCallback, useEffect, useState, useRef} = React;
const minZoom = .01;
function App() {
const [currentZoom, setCurrentZoom] = useState("1");
const scaledItem = useRef();
const changeZoom = useCallback((event) => {
if (!scaledItem.current) return;
const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
const newZoom = Math.max(minZoom, parseFloat(scaleNumber) + event.deltaY * 0.001);
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.transform = `scale(${currentZoom.toString()})`;
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.transform = "scale(1)";
}, [])
return <div onWheel={(event) => {
console.log("wheeling");
changeZoom(event);
}}
>
<div class="scaled" ref={scaledItem}>
<p>Scale me up and down! (Use "Full page" link of snippet)</p>
</div>
</div>
}
ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
border: 2px solid lightgreen;
transform: scale(1);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<div id="root2"></div>
With transition (smooth)
const {useCallback, useEffect, useState, useRef} = React;
const minZoom = .01;
function App() {
const [currentZoom, setCurrentZoom] = useState("1");
const scaledItem = useRef();
const changeZoom = useCallback((event) => {
if (!scaledItem.current) return;
const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
const newZoom = Math.max(minZoom, parseFloat(scaleNumber) + event.deltaY * 0.001);
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.transform = `scale(${currentZoom.toString()})`;
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.transform = "scale(1)";
}, [])
return <div onWheel={(event) => {
console.log("wheeling");
changeZoom(event);
}}
>
<div class="scaled" ref={scaledItem}>
<p>Scale me up and down! (Use "Full page" link of snippet)</p>
</div>
</div>
}
ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
border: 2px solid lightgreen;
transform: scale(1);
transition: transform .2s ease-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

sveltekit adds new page on top of old one

I just got a really unexpected bug in my sveltekit application and I can't find anything online talking about it
I have a normal sveltekit application but instead of hydrating the new code when navigating to a new page, it just adds the new code on top of the old one, when i refresh the page it removes the old code (from the previous page)
Edit: after a little bit more exploring I realized it only happens on one page, what part of my code could make this happen?
I had the same issue when having a page loading indicator on SvelteKit for internal navigating. Any page DOM modification, to display the loading indicator during the page navigating, caused the paged appearing twice error. The workaround was to not modify the page during the navigating and use only CSS to display, animate and hide the loading indicator.
Here is my loading indicator code and Here is my documentation regarding the issue. I am not sure if this an internal bug in SvelteKit, so I did not file any bug reports, as I do not have a clean repeatable example. You can also see the fixed page loading indicator on the action on this page if you click any of the blockchains.
<script>
/**
* Svelte does not give a load indication if you hit a link that leads to a page with slow load() function.
* Svelte uses internal router, not server-side loading.
* Thus, we need to manually give some indication in the user interface if the loading takes more than a blink of an eye.
*
* The component is originally made for https://tradingstrategy.ai
*
* Based on the original implementation https://github.com/shajidhasan/sveltekit-page-progress-demo by Shajid Hasan.
*
* As this component is absolutely position, you can put it at any part of your __layout.svelte.
*/
import { onDestroy, onMount } from 'svelte';
import { writable } from 'svelte/store';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const navigationState = writable();
const progress = tweened(0, {
duration: 3500,
easing: cubicOut
});
const unsubscribe = navigationState.subscribe((state) => {
// You will always get state=undefined
// event on the server-side rendering, so
// safely ignore it
//console.log("The loading state is", state);
if (state === 'loading-with-progress-bar') {
progress.set(0, { duration: 0 });
progress.set(0.8, { duration: 5000 });
} else if (state === 'loaded') {
progress.set(1, { duration: 1000 });
}
});
onMount(() => {
// progress.set(0.7);
});
onDestroy(() => {
unsubscribe();
});
</script>
<!-- See the (little) documentation of special SvelteKit events here https://kit.svelte.dev/docs#events -->
<svelte:window
on:sveltekit:navigation-start={() => {
// If the page loads fast enough in the preloading state,
// never display the progress bar
$navigationState = 'preloading';
// Delay the progress bar to become visible an eyeblink... only show if the page load takes too long
setTimeout(function() {
// After 250ms switch preloading to loading-with-progress-bar
if($navigationState === 'preloading') {
$navigationState = 'loading-with-progress-bar';
}
}, 500);
}}
on:sveltekit:navigation-end={() => {
$navigationState = 'loaded';
}}
/>
<!--
Make sure the container component is always in the DOM structure.
If we make changes to the page structure during the navigation, we get a page double render error:
https://stackoverflow.com/questions/70051025/sveltekit-adds-new-page-on-top-of-old-one
Not sure if this is a bug or a feature.
Thus, make sure any progress animation is done using CSS only.
-->
<div class="page-progress-bar" class:loaded={$navigationState === 'loaded'} class:preloading={$navigationState === 'preloading'} class:loading={$navigationState === 'loading-with-progress-bar'}>
<div class="progress-sliver" style={`--width: ${$progress * 100}%`} />
</div>
<style>
/* Always stay fixed at the top, but stay transparent if no activity is going on */
.page-progress-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 0.5rem;
background: transparent;
z-index: 100;
opacity: 0;
transition: opacity 0.5s;
}
/* After transitioning from preloading to loading state, make the progress bar visible with CSS transition on opacity */
.page-progress-bar.loading {
opacity: 1;
transition: opacity 0.5s;
}
.progress-sliver {
width: var(--width);
background-color: var(--price-up-green);
height: 100%;
}
</style>
I also saw this issue with the transition directive. The new page loads while the exit animation is playing. I used local transitions to solve this.
https://svelte.dev/tutorial/local-transitions

How does really work rendering in browser (event loop)

I've created simple demos, let's get started...
Should to say what we have to use chrome and firefox for comparison
Demo 1:
block.addEventListener("click", () => {
block.style.transform = "translateX(500px)";
block.style.transition = "";
block.style.transition = "transform 1s ease-in-out";
block.style.transform = "translateX(100px)";
});
.main {
width: 100px;
height: 100px;
background: orange;
}
<div id="block" class="main"></div>
In both browsers, we'll not see any changes
Demo 2:
block.addEventListener("click", () => {
block.style.transform = "translateX(500px)";
block.style.transition = "";
requestAnimationFrame(() => {
block.style.transition = "transform 1s ease-in-out";
block.style.transform = "translateX(100px)";
});
});
.main {
width: 100px;
height: 100px;
background: orange;
}
<div id="block" class="main"></div>
In chrome we'll see animation, in firefox we'll see another thing. Need to mention that firefox conforms actions from video of Jake Archibald in the Loop. But not in case with chrome. Seems that firefox conforms to spec, but not chrome
Demo 2 (alternate):
block.addEventListener("mouseover", () => {
block.style.transform = "translateX(500px)";
block.style.transition = "";
requestAnimationFrame(() => {
block.style.transition = "transform 1s ease-in-out";
block.style.transform = "translateX(100px)";
});
});
.main {
width: 100px;
height: 100px;
background: orange;
}
<div id="block" class="main"></div>
Now we see that chrome works correctly, but firefox does same thing as chrome was doing on Demo 2. They has changed by theirs places
I've also tested events: mouseenter, mouseout, mouseover, mouseleave, mouseup, mousedown. The most interesting thing that the last two ones work same in chrome and firefox and they are both incorrect, I think.
In conclusion: It seems what these two UAs differently treats events. But how do they do?
Demo 3:
block.addEventListener("click", () => {
block.style.transform = "translateX(500px)";
block.style.transition = "";
requestAnimationFrame(() => {
requestAnimationFrame(() => {
block.style.transition = "transform 1s ease-in-out";
block.style.transform = "translateX(100px)";
});
});
});
.main {
width: 100px;
height: 100px;
background: orange;
}
<div id="block" class="main"></div>
Here we see that firefox works as well as chrome, how this expects, by the Archibald's words. But do you remember demo 2, both versions, why their behaviour is so different?
TL;DR; if you want your code to work the same everywhere force a reflow yourself after you set the values you want as initial ones.
block.addEventListener("click", () => {
block.style.transform = "translateX(500px)";
block.style.transition = "";
requestAnimationFrame(() => {
// if you want your transition to start from 0
block.style.transform = "translateX(0px)";
// force reflow
document.body.offsetWidth;
block.style.transition = "transform 1s ease-in-out";
block.style.transform = "translateX(100px)";
});
});
.main {
width: 100px;
height: 100px;
background: orange;
}
<div id="block" class="main"></div>
What you are stumbling upon here is called the reflow. I already wrote about it in other answers, but basically this reflow is the calculations of all the boxes in the page needed to determine how to paint every elements.
This reflow (a.k.a layout or recalc) can be an expensive operation, so browsers will wait as much as they can before doing this.
However when this happens is not part of the event loop's specifications. The only constraint is that when the ResizeObserver's notifications are to be fired this recalc has been done.
Though implementations can very well do it before if they wish (Safari for instance will do it as soon as it has a small idle time), or we can even force it by accessing some properties that do require an updated layout.
So, before this layout has been recalculated, the CSSOM won't even see the new values you passed to the element style, and it will treat it just like if you did change these values synchronously, i.e it will ignore all the previous values.
Given both Firefox and Chrome do wait until the last moment (before firing the ResizeObserver's notifications) to trigger the reflow, we can indeed expect that in these browsers your transition would start from the initial position (translate(0)) and that the intermediate values would get ignored. But once again, this is not true for Safari.
So what happens here in Chrome? I'm currently on my phone and can't make extensive tests, but I can already see that the culprit is the line setting the transition, setting it first will "fix" the issue.
block.addEventListener("click", () => {
block.style.transform = "translateX(500px)";
block.style.transition = "transform 1s ease-in-out";
requestAnimationFrame(() => {
block.style.transform = "translateX(100px)";
});
});
.main {
width: 100px;
height: 100px;
background: orange;
}
<div id="block" class="main"></div>
Since I have to make a guess I'd say setting the transition probably makes the element switch its rendering path (e.g from CPU rendered to GPU rendered) and that they will force a reflow doing so. But this is still just a guess without proper testings. The best would be to open an issue to https://crbug.com since this is probably not the intended behavior.
As to why you have different behavior from different events it's probably because these events are firing at different moments, for instance at least mousemove will get throttled to painting frames, I'd have to double check for mousedown and mouseup though.

Trigger and reverse CSS transition with Javascript

I am currently exploring the possibilities of the FLIP technique which reduces all CSS transitions only to transform and opacity (because of GPU-acceleration). It involves manipulating styles directly with Javascript. Although it is not very hard to trigger such a transition, I've found myself unable to reverse it. Normally a transition defined within CSS on e.g. hover is reversed automatically when you stop hovering. But CSS is not enough to trigger (and reverse) transition on clicks. I want to be able to do the following:
a) Click on an item (and trigger size change by e.g. CSS class toggling)
b) Calculate the difference between its initial and new size and trigger a transform with transition
c) Click on it again while it is changing its size
d) Reverse the transition from the position it was in when clicked on
My problem is with the d) step. For some reason the element abruptly changes its size to neither the old nor the new size but to a completely different size. I have an example of what I'm trying to do here:
https://codesandbox.io/s/flamboyant-snowflake-47t59
Click on a square and then click on it again while it's enlarging.
Is there any reliable way to properly do what I'm trying to do? Are there good alternatives?
I've managed to find a working solution (it imports rematrix library to make calculations with transformation matrices easier). Both this solution and the simpler one by Richard are capable of 60fps animation - I guess the difference is the amount of cases each approach can deal with.
https://codesandbox.io/s/working-transform-reverse-urc16
I believe this is what you're looking for. Also, it seems to me that you don't quite understand requestAnimationFrame fully. It's made to replace a technique that involves recursive calls of setInterval to change the CSS property of an element every few milliseconds (read more here). So I suggest reading more about requestAnimationFrame first.
I'd like to note that this animation below can be far more easily made simply using CSS transition. Furthermore, MDN mentions that both have more or less similar performance anyway (read here). Still, here's a minimal working example of what you want:
let clicked = false;
let square = document.querySelector(".small-sq");
let expandID
let retractID
square.style.transformOrigin = 'top left'
square.style.transform = 'scale(1)'
square.addEventListener('click', e => {
let timeStart = new Date()
let currentScale = parseFloat(square.style.transform.slice(6, -1))
if (!clicked) {
cancelAnimationFrame(retractID)
expandID = requestAnimationFrame(() => repeat(timeStart, currentScale, clicked))
}
else {
cancelAnimationFrame(expandID)
retractID = requestAnimationFrame(() => repeat(timeStart, currentScale, clicked))
}
clicked = !clicked
})
function repeat(timeStart, currentScale, status) {
let timeDifference = new Date() - timeStart
if (status) {
let scaleValue = currentScale + (1 * timeDifference / 5000) > 2 ? 2 : currentScale + (1 * timeDifference / 5000)
square.style.transform = `scale(${scaleValue})`
expandID = requestAnimationFrame(() => repeat(timeStart, scaleValue, status))
if (scaleValue >= 2)
cancelAnimationFrame(expandID)
}
else {
let scaleValue = currentScale - (1 * timeDifference / 5000) < 1 ? 1 : currentScale - (1 * timeDifference / 5000)
square.style.transform = `scale(${scaleValue})`
retractID = requestAnimationFrame(() => repeat(timeStart, scaleValue, status))
if (scaleValue <= 1)
cancelAnimationFrame(retractID)
}
}
.small-sq {
height: 100px;
width: 100px;
background-color: rgb(131, 42, 131);
}
.big-sq {
height: 500px;
width: 500px;
background-color: rgb(131, 42, 131);
}
<div id="app">
<div class="small-sq"></div>
</div>
If you use CSS transition, then it can be converted to the following:
let square = document.querySelector('.small-sq')
square.addEventListener('click', e => {
square.classList.toggle('toggled')
})
.small-sq {
height: 100px;
width: 100px;
background-color: rgb(131, 42, 131);
transform-origin: left top;
transition: transform 1s linear;
}
.small-sq.toggled {
transform: scale(2)
}
<div id="app">
<div class="small-sq"></div>
</div>

How to restart updated animation after dynamically changing keyframes?

I'm trying to create a marquee (yes, I've done LOTS of searching on that topic first) using animated text-indent. I prefer this solution over others I've tried, like using translation 100%, which causes text to leak out beyond the boundaries of my marquee.
I've been trying to follow this example here: https://www.jonathan-petitcolas.com/2013/05/06/simulate-marquee-tag-in-css-and-javascript.html
...which I've updated a bit, doing it in TypeScript, using API updates (appendRule instead of insertRule) and dropping concerns about old browser support.
The problem is that the animation restarts using the old keyframe rules -- the step described by the comment "re-assign the animation (to make it run)" doesn't work.
I've looked at what's going on in a debugger, and the rules are definitely being changed -- old rules deleted, new rules added. But it's as if the old rules are cached somewhere, and they aren't being cleared out.
Here's my current CSS:
#marquee {
position: fixed;
left: 0;
right: 170px;
bottom: 0;
background-color: midnightblue;
font-size: 14px;
padding: 2px 1em;
overflow: hidden;
white-space: nowrap;
animation: none;
}
#marquee:hover {
animation-play-state: paused;
}
#keyframes marquee-0 {
0% {
text-indent: 450px;
}
100% {
text-indent: -500px;
}
}
And the relevant section of my TypeScript:
function updateMarqueeAnimation() {
const marqueeRule = getKeyframesRule('marquee-0');
if (!marqueeRule)
return;
marquee.css('animation', 'unset');
const element = marquee[0];
const textWidth = getTextWidth(marquee.text(), element);
const padding = Number(window.getComputedStyle(element).getPropertyValue('padding-left').replace('px', '')) +
Number(window.getComputedStyle(element).getPropertyValue('padding-right').replace('px', ''));
const offsetWidth = element.offsetWidth;
if (textWidth + padding <= offsetWidth)
return;
marqueeRule.deleteRule('0%');
marqueeRule.deleteRule('100%');
marqueeRule.appendRule('0% { text-indent: ' + offsetWidth + 'px; }');
marqueeRule.appendRule('100% { text-indent: -' + textWidth + 'px; }');
setTimeout(() => marquee.css('animation', 'marquee-0 15s linear infinite'));
}
I've tried a number of tricks so far to get around this problem, including things like cloning the marquee element and replacing it with its own clone, and none of that has helped -- the animation continues to run as if the original stylesheet values are in effect, so the scrolling of the marquee doesn't adapt to different widths of text.
The next thing I'll probably try is dynamically creating new keyframes objects instead of editing the rules inside of an existing keyframes object, but that's a messy solution I'd rather avoid if anyone has a better solution.
I found a way to get my marquee working, and it did involved dynamically adding and removing keyframes rules from a stylesheet, but that wasn't as painful or ugly as I thought it might be.
let animationStyleSheet: CSSStyleSheet;
let keyframesIndex = 0;
let lastMarqueeText = '';
function updateMarqueeAnimation(event?: Event) {
const newText = marquee.text();
if (event === null && lastMarqueeText === newText)
return;
lastMarqueeText = newText;
marquee.css('animation', 'none');
const element = marquee[0];
const textWidth = getTextWidth(newText, element);
const padding = Number(window.getComputedStyle(element).getPropertyValue('padding-left').replace('px', '')) +
Number(window.getComputedStyle(element).getPropertyValue('padding-right').replace('px', ''));
const offsetWidth = element.offsetWidth;
if (textWidth + padding <= offsetWidth)
return;
if (!animationStyleSheet) {
$('head').append('<style id="marquee-animations" type="text/css"></style>');
animationStyleSheet = ($('#marquee-animations').get(0) as HTMLStyleElement).sheet as CSSStyleSheet;
}
if (animationStyleSheet.cssRules.length > 0)
animationStyleSheet.deleteRule(0);
const keyframesName = 'marquee-' + keyframesIndex++;
const keyframesRule = `#keyframes ${keyframesName} { 0% { text-indent: ${offsetWidth}px } 100% { text-indent: -${textWidth}px; } }`;
const seconds = (textWidth + offsetWidth) / 100;
animationStyleSheet.insertRule(keyframesRule, 0);
marquee.css('animation', `${keyframesName} ${seconds}s linear infinite`);
}
There's other stuff going on here not needed for a general solution. One thing is that this method is called for two reasons: The window is being resized, or an update to the marquee text has been made. I always want to update when the window is resized, but otherwise I don't want to update the animation if the text hasn't changed, otherwise it could unnecessarily reset when someone is trying to read it.
The other thing is that I don't want text to scroll at all if it happens to fit nicely without scrolling.

Categories