Below you will find a script I created for smooth scrolling when clicking local links. It is done via transform (no jQuery). As seen, I have implemented it using both inline CSS as well as external style sheets. I recommend the inline version, as it might be difficult to guess the index of the relevant style sheet.
The problem however, is that the actual movement of the scrollbar, happens after the transform is applied. Thus, if you click a link before scrolling transition is done, the code misbehaves.
Any thoughts on a solution to this?
EDIT:
I know there are jQuery solutions and third party polyfill libraries out there. My goal, however, was to recreate the jQuery functionality in plain vanilla JavaScript.
My Script:
// Get style rule's declaration block
function getStyleDeclaration(styleSheet, selectorText) {
const rules = styleSheet.cssRules;
return Array.from(rules).find(r => r.selectorText === selectorText).style;
// for (let i = 0; i < rules.length; i += 1) {
// if (rules[i].selectorText === selectorText) return rules[i].style;
// }
}
// Get specific style sheet, based on its title
// Many style sheets do not have a title however
// Which is main reason it is preferred to work with
// inline styles instead
function getStyleSheet(title) {
const styleSheets = document.styleSheets;
return Array.from(styleSheets).find(s => s.title === title);
// for (let i = 0; i < styleSheets.length; i += 1) {
// if (styleSheets[i].title === title) return styleSheets[i];
// }
}
function scrollToElement_ExternalStyleSheet(anchor, target) {
anchor.addEventListener("click", e => {
e.preventDefault();
const time = 1000;
// Distance from viewport to topof target
const distance = -target.getBoundingClientRect().top;
// Modify external style sheet
const transStyle = getStyleDeclaration(document.styleSheets[1], ".trans");
transStyle.transform = "translate(0, " + distance + "px)";
transStyle.transition = "transform " + time + "ms ease";
const root = document.documentElement; // <html> element
root.classList.add("trans");
window.setTimeout(() => {
root.classList.remove("trans");
root.scrollTo(0, -distance + window.pageYOffset);
}, time);
});
}
function scrollToElement_InlineStyle(anchor, target) {
const root = document.documentElement;
anchor.addEventListener('click', e => {
e.preventDefault();
const time = 900;
const distance = -target.getBoundingClientRect().top;
root.style.transform = 'translate(0, ' + distance + 'px)';
root.style.transition = 'transform ' + time + 'ms ease';
window.setTimeout(() => {
root.scrollTo(0, -distance + window.pageYOffset);
root.style.transform = null; // Revert to default
root.style.transition = null;
}, time);
});
}
function applySmoothScroll() {
const anchors = document.querySelectorAll("a");
const localAnchors = Array.from(anchors).filter(
a => a.getAttribute("href").indexOf("#") != -1
);
localAnchors.forEach(a => {
const targetString = a.getAttribute("href");
const target = document.querySelector(targetString);
// scrollToElement_ExternalStyleSheet(a, target);
scrollToElement_InlineStyle(a, target);
});
}
applySmoothScroll();
.box {
padding-bottom: 300px;
padding-top: 0.5rem;
background-color: orange;
text-align: center;
font-size: 200%;
}
.box:nth-child(even) {
background-color: lightblue;
color: white;
}
a {
color: black;
}
body {
margin: 0;
}
.trans {
transform: translate(0, -100px);
transition: transform 900ms ease;
}
<div id="s0" class="box">Click Me!</div>
<div id="s1" class="box">Click Me!</div>
<div id="s2" class="box">Click Me!</div>
Related
I have a 'theme toggle' which changes the look/feel of my UI. There is also a custom JS cursor that I don't need on one of the themes. I thought it would be a good idea to kill the script when the relevant class is on html as there's a lot of calculation going on with the pointer position. Rather than just use display: none and leave the script running.
There are 3 modes/themes:
Default (always first / onload)
Dark
Retro - which is the one where the script should be stopped.
I tried to achieve this using the following method. To test, I add the .retro class to html so it's the first theme, it does stop the script from running and clicking the toggle turns the script on again. But it only does it once.
Obviously removing the class onload doesn't work either but I just wanted to do the above to see if anything was happening. Can someone tell me where I'm going wrong. What's the best approach?
Essentially what I'm trying to achieve is: Run this script, unless the class .retro is present, if so stop it. If the class .retro is removed, run the script again.
/* Toggle */
const html = document.querySelector("html");
const button = document.querySelector(".contrast__link");
button.addEventListener("click", (e) => {
e.preventDefault();
if (html.classList.contains("dark-mode")) {
html.classList.remove("dark-mode");
html.classList.add("retro");
let slideIndex = swiper.activeIndex;
swiper.destroy(true, true);
swiper = initSwiper("slide", false, 999999999); // Retro: This should slide, no autoplay or loop
swiper.slideTo(slideIndex, 0);
} else if (html.classList.contains("retro")) {
html.classList.remove("retro");
let slideIndex = swiper.activeIndex;
swiper.destroy(true, true);
swiper = initSwiper("fade", true, 1200); // Default: This should fade, autoplay & loop
swiper.slideTo(slideIndex, 0);
} else {
html.classList.add("dark-mode");
}
});
/* Cursor */
function createHandler(callback) {
return function(event) {
if (!document.documentElement.classList.contains('retro')) {
callback.call(this, event)
}
}
}
window.addEventListener('mousemove', createHandler(function() {
var cursor = document.querySelector(".cursor");
var cursorTrail = document.querySelector(".cursor-trail");
var a = document.querySelectorAll("a");
var timeout;
window.addEventListener(
"mousemove",
function(e) {
var x = e.clientX;
var y = e.clientY;
cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
if (!timeout) {
timeout = setTimeout(function() {
timeout = null;
cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`;
}, 24);
}
},
false
);
/**
* Add/remove classes on click (anywhere).
*/
document.addEventListener("mousedown", function() {
cursor.classList.add("cursor--click");
});
document.addEventListener("mouseup", function() {
cursor.classList.remove("cursor--click");
});
/**
* Add/remove set classes on hover.
*
* 1. This used to start with `a.forEach((item) => {` but changed to `let` so
* that an additional (non-anchor) item could be targeted. `#hello` is for
* the image on the 404 page.
*/
// a.forEach((item) => {
let links = document.querySelectorAll('a, #hello'); /* [1] */
links.forEach((item) => { /* [1] */
item.addEventListener("mouseover", () => {
cursorTrail.classList.add("cursor-trail--hover");
});
item.addEventListener("mouseleave", () => {
cursorTrail.classList.remove("cursor-trail--hover");
});
});
/**
* Add custom classes on hover if the cursor needs to be manipulated in a
* unique way. If an element has a `data-interaction=""` value set. This will
* be added as a class to the cursor on hover. For example, this is used to
* style the prev/next arrows on the carousel.
*
* This could be set using a specific class but I've just left it targeting all
* `a` elements for now. Which will add a class of `undefined` if no dataset is
* specified.
*/
a.forEach((item) => {
const interaction = item.dataset.interaction;
item.addEventListener("mouseover", () => {
cursor.classList.add(interaction);
});
item.addEventListener("mouseleave", () => {
cursor.classList.remove(interaction);
});
});
}))
.contrast__link {
background: white;
}
.cursor {
background: red;
height: 20px;
width: 20px;
position: fixed;
}
.dark-mode {
background: black;
}
.retro {
background: blue;
}
Toggle Classes
<div class="cursor"><span></span></div>
<div class="cursor-trail"></div>
I would suggest to add and remove "mousemove" listener inside "click" listener, when you toggling the theme. So if user selects "retro", the event listener should be removed, and when it is another theme, the listener should be added.
As #Mykhailo Svyrydovych suggested, to avoid any overhead of the unneeded events call, you would need to have explictly named functions for each event handler, then you could use removeEventListener when you're toggling to the retro theme, removing the handler of each mousemove, mouseenter, mouseleave, mouseup and mousedown events that you don't want to run on that theme. Of course, you would need to re-bind all the events again using the proper addEventListener calls when toggling to the other two themes.
But I think that, sometimes, the easiest approach is good enough. If you measure the performance and then discover that the events keeping to fire isn't a big concern for your case, go for it. You can easily avoid the unwanted effects of the custom cursor, inside each event handler, just by checking a simple boolean:
const html = document.documentElement;
const button = document.querySelector(".contrast__link");
const cursor = document.querySelector(".cursor");
const cursorTrail = document.querySelector(".cursor-trail");
const a = document.querySelectorAll("a");
let timeout;
let customCursor = true;
button.addEventListener("click", (e) => {
e.preventDefault();
if (html.classList.contains("dark-mode")) {
html.classList.remove("dark-mode");
html.classList.add("retro");
customCursor = false;
cursor.style.display = "none";
} else if (html.classList.contains("retro")) {
html.classList.remove("retro");
customCursor = true;
cursor.style.display = "block";
} else {
html.classList.add("dark-mode");
customCursor = true;
cursor.style.display = "block";
}
});
window.addEventListener(
"mousemove",
function(e) {
if (!customCursor) return;
var x = e.clientX;
var y = e.clientY;
cursor.style.display = "block";
cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
if (!timeout) {
timeout = setTimeout(function() {
cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`;
}, 24);
}
},
false
);
document.addEventListener("mousedown", () => {
if (!customCursor) return;
cursor.classList.add("cursor--click");
});
document.addEventListener("mouseup", () => {
if (!customCursor) return;
cursor.classList.remove("cursor--click");
});
let links = document.querySelectorAll('a, #hello');
links.forEach((item) => {
item.addEventListener("mouseover", () => {
if (!customCursor) return;
cursorTrail.classList.add("cursor-trail--hover");
});
item.addEventListener("mouseleave", () => {
if (!customCursor) return;
cursorTrail.classList.remove("cursor-trail--hover");
});
});
a.forEach((item) => {
const interaction = item.dataset.interaction;
item.addEventListener("mouseover", () => {
if (!customCursor) return;
cursor.classList.add(interaction);
});
item.addEventListener("mouseleave", () => {
if (!customCursor) return;
cursor.classList.remove(interaction);
});
});
.contrast__link {
background: white;
}
.cursor {
background: red;
height: 20px;
width: 20px;
position: fixed;
display: none;
}
.dark-mode {
background: black;
}
.retro {
background: blue;
}
Toggle Classes
<div class="cursor"><span></span></div>
<div class="cursor-trail"></div>
P.S.: I removed the Swiper calls from my code sample because they appeared to be totally unrelated to the question being asked, and were causing console errors.
I have tried with simple CSS code, it's working
.retro .cursor{
display:none
}
but you want to stop the function also, so tried the following that also worked. When the theme is retro on mouse move the cursor changing function is not calling
/* Toggle */
const html = document.querySelector("html");
const cursor = document.querySelector(".cursor");
function addClass() {
if (html.classList.contains("dark-mode")) {
html.classList.remove("dark-mode");
html.classList.add("retro");
cursor.style.display = 'none'
} else if (html.classList.contains("retro")) {
html.classList.remove("retro");
} else {
html.classList.add("dark-mode");
}
}
document.addEventListener('mousemove', function(e) {
if (html.classList.contains("retro")) {
cursor.style.display = 'none'
} else {
cursor.style.display = 'block'
var x = e.clientX;
var y = e.clientY;
cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`;
}
});
.cursor {
background: red;
height: 20px;
width: 20px;
position: fixed;
}
.dark-mode {
background: black;
}
.retro {
background: blue;
}
<div class="cursor"></div>
<button onclick='addClass()'>Add Class</button>
You can use element insertion and window properties to load/unload code.
Look at code in snippet, there's simple system for theme changing.
When on retro, mouse clicks will be logged in console.
In my snippet, script will be working only on retro theme, but that doesn't changes concept.
const themeList = {
'default': {
enable() {
app.classList.add('default');
},
disable() {
app.classList.remove('default');
},
},
'dark-mode': {
enable() {
app.classList.add('dark-mode');
},
disable() {
app.classList.remove('dark-mode');
},
},
'retro': {
enable() {
app.classList.add('retro');
js = document.createElement("script");
js.id = 'retroThemeScript';
js.innerHTML = `
window.fnToBeRemoved = () => console.log('mouse down');
app.addEventListener("mousedown", window.fnToBeRemoved);
` // you can replace code with next line, so no need to store code in RAM
//js.src = '/scripts/retroScript.js';
js.type = 'text/javascript';
app.appendChild(js);
this.scriptElement = js;
},
disable() {
app.classList.remove('retro');
app.removeEventListener('mousedown', window.fnToBeRemoved);
delete window.fnToBeRemoved;
app.removeChild(this.scriptElement);
},
}
}
const app = document.querySelector('#app');
let currentTheme;
function setTheme(themeName) {
if (currentTheme)
themeList[currentTheme].disable();
themeList[themeName].enable();
currentTheme = themeName;
}
setTheme('default');
#app {
font-size: 24px;
width: 500px;
height: 500px;
background: silver;
}
#app.retro{
background: magenta;
}
#app.dark-mode {
background: gray;
}
<button onclick="setTheme('default')">Default</button>
<button onclick="setTheme('dark-mode')">Dark</button>
<button onclick="setTheme('retro')">Retro</button>
<div id="app">
Wanna cake?
</div>
I am trying to recreate the idea of this video by creating a typewriting logo of the site that contains emojis. However, it seems that rendering emoji on browser takes a little while, so rhombus with a question mark appears for a moment.
I believe that it's somehow connected with the nature of dec/hex rendering, but failed to find some detailed sources on the topic as I can't clearly state the problem. I would be very grateful for any ideas to resolve this problem.
My JS, CSS, and HTML respectively:
// ES6 Class
class TypeWriter {
constructor(txtElement, words, wait = 3000) {
this.txtElement = txtElement;
this.words = words;
this.txt = '';
this.wordIndex = 0;
this.wait = parseInt(wait, 10);
this.type();
this.isDeleting = false;
}
type() {
// Current index of word
const current = this.wordIndex % this.words.length;
// Get full text of current word
const fullTxt = this.words[current];
// Check if deleting
if(this.isDeleting) {
// Remove char
this.txt = fullTxt.substring(0, this.txt.length - 1);
} else {
// Add char
this.txt = fullTxt.substring(0, this.txt.length + 1);
}
// Insert txt into element
this.txtElement.innerHTML = `<span class="txt">${this.txt}</span>`;
// Initial Type Speed
let typeSpeed = 300;
if(this.isDeleting) {
typeSpeed /= 2;
}
// If word is complete
if(!this.isDeleting && this.txt === fullTxt) {
// Make pause at end
typeSpeed = this.wait;
// Set delete to true
this.isDeleting = true;
} else if(this.isDeleting && this.txt === '') {
this.isDeleting = false;
// Move to next word
this.wordIndex++;
// Pause before start typing
typeSpeed = 500;
}
setTimeout(() => this.type(), typeSpeed);
}
}
// Init On DOM Load
document.addEventListener('DOMContentLoaded', init);
// Init App
function init() {
const txtElement = document.querySelector('.txt-type');
const words = JSON.parse(txtElement.getAttribute('data-words'));
const wait = txtElement.getAttribute('data-wait');
// Init TypeWriter
new TypeWriter(txtElement, words, wait);
}
#import url('https://fonts.googleapis.com/css?family=Raleway:300,400');
/* https://css-tricks.com/snippets/css/typewriter-effect/ typewritting effect */
:root{
--header-height: 2rem;
--h2-font-size: 1.25rem;
--normal-font-size: .938rem;
font-family: Raleway;
color: rgb(42, 42, 42);
}
.nav__logo {
overflow: hidden;
position: relative;
}
a {
text-decoration: none;
color: rgb(42, 42, 42);
}
<div>
<b>#alexpoov </b>
<span class="nav__logo txt-type" data-wait="3000" data-words='["๐ป", "๐คน", "โ", "๐ป", "โ", "๐น", "๐ธ", "๐ค"]'>
</span>
</div>
If all you want is to change the emoji, the code you have overcomplicates it.
Attempting to change as little of your existing code as possible, the example below should show the emoji for 800ms and hide it for 500ms.
class TypeWriter {
constructor(txtElement, words) {
this.txtElement = txtElement;
this.words = words;
this.wordIndex = 0;
this.type(true); // show at the beginning
}
type(show) { // added 'show' param to determine whether to show or hide emoji
if (!show) {
this.txtElement.innerText = '';
setTimeout(() => this.type(true), 500); // wait 500ms and show
return;
}
this.txtElement.innerText = this.words[this.wordIndex];
this.wordIndex++;
this.wordIndex %= this.words.length;
setTimeout(() => this.type(false), 800); // wait 800ms and hide
}
}
If you want a pseudo-cursor object in place, you can add something like this after the emoji:
<span class="nav__logo cursor">|</span>
You can style it how you wish.
My goal I want to run loop that decrements a global variable stepwise in n ms (for Example: 200ms) time intervals.
Thanks in advance!
What i already tried
I tried to use ascy await. But in combination with css transition i run in an infinite loop (In codepen.io). But here in SO you will see that it starts not running smoothly if you keep pressing arrow up.
const procentage = document.querySelector(".procentage");
const green = engine.querySelector(".green");
let number = 0;
let decrementing = false;
window.addEventListener('keydown', (e) => {
e = e || window.event;
e.preventDefault();
if (e.keyCode == '38') {
console.log("accelerate");
actionHandler( number++ );
decrementing = false;
downLoop();
}
});
function actionHandler(num) {
procentage.innerHTML = num;
const str = num + "%"
green.style.width = str;
procentage.innerHTML = str;
}
window.addEventListener('keyup', (e) => {
e = e || window.event;
e.preventDefault();
if (e.keyCode == '38') {
console.log("decelerate");
decrementing = true;
downLoop();
}
});
async function downLoop() {
if (! decrementing) {
return false
};
const timer = ms => new Promise(res => setTimeout(res, ms));
while (number > 1) {
// how to decrement ever 200ms???
actionHandler( number-- );
await timer(200)
}
}
#engine {
background-color:black;
height: 50px;
position: relative;
}
p {
text-align: center;
}
.green {
background:green;
height: 50px;
width:0%;
transition: width 0.2s;
text-align:center;
}
.procentage {
position:absolute;
top: 50%;
left: 50%;
transform: translate(0%,-50%);
color: white;
fon-weight: bold;
font-size:28px;
}
<div id="engine">
<div><span class="procentage">0</span></div>
<div class="green"></div>
</div>
<p>press arrow Up</p>
Whenever you animate, you shouldn't rely on setInterval or setTimeout, because that means that you will update "somewhere after X milliseconds" which will often end up in the middle of a screen repaint, and will therefor cause janky animation.
Instead, you should use RequestAnimationFrame which does a calculation before every repaint. So if you got a monitor with a framerate of 60 Hz, that means that you will do 60 repaints every second. For each repaint, check if enough time have passed since the last update (shouldTriggerUpdate() below) and then check if you should update the number.
I also added the class KeyHandler to keep track of which keys that have been pressed.
I got sloppy at the end and just added a decrement as an "else" of the if statement. You will figure something out when you get there when you want to set up more keys to be pressed.
You shouldn't use KeyboardEvent.keyCode, but instead KeyboardEvent.code.
const procentage = document.querySelector(".procentage");
const green = engine.querySelector(".green");
let number = 0;
let speed = 200 // ms
let lastUpdated = 0; // ms
let animationId = 0; // use later on to pause the animation
class KeyHandler {
ArrowLeft = false
ArrowUp = false
ArrowRight = false
ArrowDown = false
#setKey(code, value) { // private method
if (typeof this[code] != undefined) {
this[code] = value;
}
}
set pressedKey(code) {
this.#setKey(code, true);
}
set releasedKey(code) {
this.#setKey(code, false);
}
}
let keyHandler = new KeyHandler();
window.addEventListener('keydown', (e) => {
e = e || window.event;
e.preventDefault();
keyHandler.pressedKey = e.code;
});
window.addEventListener('keyup', (e) => {
e.preventDefault();
keyHandler.releasedKey = e.code
});
function actionHandler(num) {
const str = num + "%"
green.style.width = str;
procentage.innerHTML = str;
}
function shouldTriggerUpdate(timeInMillis) {
let difference = timeInMillis - lastUpdated;
return difference >= speed;
}
function planeAnimation() {
let timeInMillis = new Date().getTime();
if (shouldTriggerUpdate(timeInMillis)) {
lastUpdated = timeInMillis;
if (keyHandler.ArrowUp) {
actionHandler(++number)
} else if (number > 0) {
actionHandler(--number)
}
}
animationId = requestAnimationFrame(planeAnimation)
}
animationId = requestAnimationFrame(planeAnimation);
#engine {
background-color: black;
height: 50px;
position: relative;
}
p {
text-align: center;
}
.green {
background: green;
height: 50px;
width: 0%;
transition: width 0.2s;
text-align: center;
}
.procentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(0%, -50%);
color: white;
fon-weight: bold;
font-size: 28px;
}
<div id="engine">
<div><span class="procentage">0</span></div>
<div class="green"></div>
</div>
<p>press arrow up</p>
From the above comments ...
"Instead of incrementing each time the number value push a new async timer function, set to 200 msec delay but not immediately triggered, into an array. Create an async generator from it and iterate over the latter via the for-await...of statement where one could decrement number again." โ Peter Seliger
"#PeterSeliger Hi Peter! Thank you for your comment. Can you make a small example please?" โ Maik Lowrey
And here the requested demonstration.
function createWait(delay) {
return async function wait () {
let settle;
const promise = new Promise((resolve) => { settle = resolve;});
setTimeout(settle, delay, { delay, state: 'ok' });
return promise;
};
}
async function* getWaitIterables(list) {
let wait;
while (wait = list.shift()) {
yield wait();
}
}
// demo for ...
// - creating an async `wait` function
// or a list of such kind.
// - creating an async generator from
// a list of async `wait` functions.
// - iterating an async generator of
// async `wait` functions.
const waitingList = [ // const waitingList = [];
2000, // waitingList.push(createWait(2000));
1000, // waitingList.push(createWait(1000));
3000, // waitingList.push(createWait(3000));
].map(createWait); // - The OP of cause needs to push into.
let number = 3; // - The incremented `number` value e.g. ... 3.
(async () => {
for await (const { delay, state } of getWaitIterables(waitingList)) {
--number;
console.log({ number, delay, state });
}
})();
console.log('... running ...', { number });
.as-console-wrapper { min-height: 100%!important; top: 0; }
I was trying to make an animation using the style.transform property. The intention was to loop the transform scale property with slight increase in x and y on each round, but it failed. How can I achieve this effect?
x = document.getElementById("btn");
x.onclick = function() {
for (let y = 0; y < 1; y += 0.1) {
x.style.transform = 'scale(1.' + y + ',' + '1.' + y + ')';
}
}
<button id='btn'>button</button>
You should use CSS transitions.
Style your button like so:
#btn {
transition: transform 0.1s
}
That code will make the button transition during 0.1 seconds whenever the transform property is changed, for example the scale.
Then, from your JavaScript code, you juste have to assign the transform style once, and CSS will transition automatically.
x = document.getElementById("btn");
x.onclick = function() {
x.style.transform = 'scale(2,2)'; // or any x and y value
}
x = document.getElementById("btn");
x.onclick = function() {
x.style.transform = 'scale(2,2)'; // or any x and y value
}
#btn {
transition: transform 0.1s
}
<button id="btn">button</button>
You could do it with a combination of setInterval, requestAnimationFrame and CSS transitions to control the speed of the animation, good performance and a smooth controlled transition.
const button = document.getElementById("btn");
function scaleButton(speed, size = 1) {
let end = size * 2;
let interval = setInterval(() => {
if (size === end) {
clearInterval(interval);
button.style.transitionDuration = '';
}
size += 0.1;
size = parseFloat(size.toFixed(1)); // Round to 1 decimal point.
requestAnimationFrame(() => {
button.style.transform = `scale(${size}, ${size})`;
});
}, speed);
button.style.transitionDuration = `${speed}ms`;
}
button.onclick = function() {
scaleButton(50);
};
html {
width: 100%;
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
#btn {
transition: transform linear;
}
<button id='btn'>button</button>
Here is a soultion using requestAnimationFrame;
x = document.getElementById("btn");
let y = 0;
function animate (y) {
if(y < 1) {
y += 0.1;
x.style.transform = `scale(${1 + y},${1 + y})`;
requestAnimationFrame( () => animate(y) );
}
}
x.onclick = function() {
animate(0);
}
<button id='btn'>button</button>
The browser has an, how to say, an painting cycle. Every 16ms (i am not sure if its exactly 16ms) the browser does an repaint.
The problem that you have, is that your loop is already done before the next repaint cycle.
Here is an solution with async / await:
You can create an function called readyToAnimate it returns an promise that resolves the callback function of the requestAnimationFrame.
The requestAnimationFrame callback gets executed before the next repaint.
Now in your loop you can use await readyToAnimate(). It will wait till the browser is ready for the next repaint.
x = document.getElementById("btn");
x.onclick = async function() {
for (let y = 0; y < 1; y += 0.1) {
await readyToAnimate();
x.style.transform = `translateX(${y * 200}px)`;
}
}
function readyToAnimate() {
return new Promise(res => requestAnimationFrame(res));
}
<button id='btn'>button</button>
Im trying to learn polymer and im following this tutorial here :
https://codelabs.developers.google.com/codelabs/polymer-2-carousel/index.html?index=..%2F..%2Findex#1
however the tutorial doesnt show how to associate text to the image in the carousel, i.e. i want to have text change when i click the buttons on the carousel
<!--
#license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<!-- Load the Polymer.Element base class -->
<link rel="import" href="bower_components/polymer/polymer-element.html">
<link rel="import" href="my-mixin.html">
<dom-module id="my-carousel">
<template>
<!-- Styles MUST be inside template -->
<style>
:host {
display: block;
position: relative;
overflow: hidden;
}
div > ::slotted(:not([selected])) {
display: none;
}
button {
position: absolute;
top: calc(50% - 20px);
padding: 0;
line-height: 40px;
border: none;
background: none;
color: #DDD;
font-size: 40px;
font-weight: bold;
opacity: 0.7;
}
button:hover,
button:focus {
opacity: 1;
}
#prevBtn {
left: 12px;
}
#nextBtn {
right: 12px;
}
button[disabled] {
opacity: 0.4;
}
</style>
<div>
<slot></slot>
</div>
<div id="buttons"> <button id="prevBtn" on-click="previous">โฎ</button>
<button id="nextBtn" on-click="next">โฏ</button></div>
</template>
<script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<script src="js/index.js"></script>
<script>
// Extend Polymer.Element with MyMixin
class MyCarousel extends MyMixin(Polymer.Element) {
static get is() { return 'my-carousel' }
_selectedChanged(selected, oldSelected) {
super._selectedChanged(selected, oldSelected);
if (selected) {
this.$.prevBtn.disabled = !selected.previousElementSibling;
this.$.nextBtn.disabled = !selected.nextElementSibling;
this._loadImage(selected);
this._loadImage(selected.previousElementSibling);
this._loadImage(selected.nextElementSibling);
} else {
this.$.prevBtn.disabled = true;
this.$.nextBtn.disabled = true;
}
}
previous() {
const elem = this.selected && this.selected.previousElementSibling;
if (elem && !this._touchDir) {
// Setup transition start state
const oldSelected = this.selected;
this._translateX(oldSelected, 0);
this._translateX(elem, -this.offsetWidth);
// Start the transition
this.selected = elem;
this._translateX(oldSelected, this.offsetWidth, true /* transition */);
this._translateX(elem, 0, true /* transition */);
}
}
next() {
const elem = this.selected && this.selected.nextElementSibling;
if (elem && !this._touchDir) {
// Setup transition start state
const oldSelected = this.selected;
this._translateX(oldSelected, 0);
this._translateX(elem, this.offsetWidth);
// Start the transition
this.selected = elem;
this._translateX(oldSelected, -this.offsetWidth, true /* transition */);
this._translateX(elem, 0, true /* transition */);
}
}
_loadImage(img) {
if (img && !img.src) {
img.src = img.getAttribute('data-src');
}
}
_translateX(elem, x, transition) {
elem.style.display = 'block';
elem.style.transition = transition ? 'transform 0.2s' : '';
elem.style.transform = 'translate3d(' + x + 'px, 0, 0)';
}
ready() {
super.ready();
requestAnimationFrame(this._installListeners.bind(this));
}
_installListeners() {
this.addEventListener('transitionend', this._resetChildrenStyles.bind(this));
this.addEventListener('touchstart', this._touchstart.bind(this));
this.addEventListener('touchmove', this._touchmove.bind(this));
this.addEventListener('touchend', this._touchend.bind(this));
}
_resetChildrenStyles() {
let elem = this.firstElementChild;
while (elem) {
elem.style.display = '';
elem.style.transition = '';
elem.style.transform = '';
elem = elem.nextElementSibling;
}
}
_touchstart(event) {
// No transition if less than two images
if (this.childElementCount < 2) {
return;
}
// Save start coordinates
if (!this._touchDir) {
this._startX = event.changedTouches[0].clientX;
this._startY = event.changedTouches[0].clientY;
}
}
_touchmove(event) {
// No transition if less than two images
if (this.childElementCount < 2) {
return;
}
// Is touchmove mostly horizontal or vertical?
if (!this._touchDir) {
const absX = Math.abs(event.changedTouches[0].clientX - this._startX);
const absY = Math.abs(event.changedTouches[0].clientY - this._startY);
this._touchDir = absX > absY ? 'x' : 'y';
}
if (this._touchDir === 'x') {
// Prevent vertically scrolling when swiping
event.preventDefault();
let dx = Math.round(event.changedTouches[0].clientX - this._startX);
const prevChild = this.selected.previousElementSibling;
const nextChild = this.selected.nextElementSibling;
// Don't translate past the current image if there's no adjacent image in that direction
if ((!prevChild && dx > 0) || (!nextChild && dx < 0)) {
dx = 0;
}
this._translateX(this.selected, dx);
if (prevChild) {
this._translateX(prevChild, dx - this.offsetWidth);
}
if (nextChild) {
this._translateX(nextChild, dx + this.offsetWidth);
}
}
}
_touchend(event) {
// No transition if less than two images
if (this.childElementCount < 2) {
return;
}
// Don't finish swiping if there are still active touches.
if (event.touches.length) {
return;
}
if (this._touchDir === 'x') {
let dx = Math.round(event.changedTouches[0].clientX - this._startX);
const prevChild = this.selected.previousElementSibling;
const nextChild = this.selected.nextElementSibling;
// Don't translate past the current image if there's no adjacent image in that direction
if ((!prevChild && dx > 0) || (!nextChild && dx < 0)) {
dx = 0;
}
if (dx > 0) {
if (dx > 100) {
if (dx === this.offsetWidth) {
// No transitionend will fire (since we're already in the final state),
// so reset children styles now
this._resetChildrenStyles();
} else {
this._translateX(prevChild, 0, true);
this._translateX(this.selected, this.offsetWidth, true);
}
this.selected = prevChild;
} else {
this._translateX(prevChild, -this.offsetWidth, true);
this._translateX(this.selected, 0, true);
}
} else if (dx < 0) {
if (dx < -100) {
if (dx === -this.offsetWidth) {
// No transitionend will fire (since we're already in the final state),
// so reset children styles now
this._resetChildrenStyles();
} else {
this._translateX(this.selected, -this.offsetWidth, true);
this._translateX(nextChild, 0, true);
}
this.selected = nextChild;
} else {
this._translateX(this.selected, 0, true);
this._translateX(nextChild, this.offsetWidth, true);
}
} else {
// No transitionend will fire (since we're already in the final state),
// so reset children styles now
this._resetChildrenStyles();
}
}
// Reset touch direction
this._touchDir = null;
}
}
// Register custom element definition using standard platform API
customElements.define(MyCarousel.is, MyCarousel);
</script>
</dom-module>
You cannot - not as it is done in the tutorial.
Your 'lorems' are hard coded inside index.html, so to achieve the behavior you are trying to get you would need to wrap them in another custom element, in a similar fashion my-carousel is structured, and use data binding to propagate change event between the two:
<my-carousel selected={{selected}}>
<img data-src="https://app-layout-assets.appspot.com/assets/bg1.jpg">
<img data-src="https://app-layout-assets.appspot.com/assets/bg2.jpg">
<img data-src="https://app-layout-assets.appspot.com/assets/bg3.jpg">
...
</my-carousel>
<my-text-selector selected={{selected}}>
<p>Lorem ipsum...</p>
<p>Lorem ipsum...</p>
<p>Lorem ipsum...</p>
...
<my-text-selector>
You will need to implement content switching based on changes to selected property. The above would also need to be wrapped in dom-bind as it's not inside a polymer managed element but in index.html.
Also look into Polymer Starter Kit for an example of using iron-pages element that basically manages content switching.