Make slideshow/animation with multiple images - javascript

i know that there are question similar to this one, but my goal is different.
I have an array of Sceneobjects like this:
const scene = {
Id: Math.floor(Math.random() * 10000),
JsonObj: canvas.getJsonObj(),
image: this.editor?.export.toImage(),
duration: 1000,
};
And am using a library PikasoJS that alows me to export/import an image of a html div.
I put each image of div in scene object, and i want to display all these images in array scenes one by one automatically, and duration of scene is specified in duration field.
How can i implement something like this, i tried working with setInterval but it didn't work as i espected:
const playScene = () => {
const cpScenes = [...scenes];
let i = 0;
for(let i = 0;i < cpScenes.length;i++) {
setTimeout(() => {
let image = cpScenes[i].image;
console.log("am runnig");
this.editor.loadImage(image);
}, cpScenes[i].duration);
}
clearTimeout();
};
The way am thinking is that setTimeout should block execution of for loop and then executes it's code after specified time. But from what i noticed i was wrong.
This is my first time working with react and js.

Related

How to wait for x seconds before clicking the next button using Javascript

I have this code that clicks on the elements. The purpose of these buttons is to open accordions. It works but I need to modify it a little bit so as to reduce the load on the server. At the moment, all the buttons are clicked at once which causes some buttons on the same page not to work.
I was wondering if there is a way to wait for at least 3 seconds before clicking on the next button. All the buttons share the same class name.
const matchBtns = document.querySelectorAll('.classname')
matchBtns.forEach(matchbtn => matchbtn.click())
I have tried to wrap the forEach inside the setTimeout but I can't get it to work.
I tried
setTimeout(function() { matchBtns.forEach(matchbtn => matchbtn.click());}, 3000);
Thanks
You can change your implementation something like this
const matchBtns = document.querySelectorAll('.classname')
let nextClickIn = 0;
let delay = 3000;
matchbtns.forEach((matchbtn) => {
setTimeout(() => {matchbtn.click()}, nextClickIn)
nextClickIn += delay
})
Note: Provide the delay value in milliseconds
You can code this one as below:
const matchBtns = document.querySelectorAll('.classname')
let nextClick = 1;
let delay = 3000;
matchbtns.forEach((matchbtn) => {
setTimeout(() => {matchbtn.click()}, delay)
nextClick++;
delay *= nextClick
})

How to get total audio duration from multiple audio files

I am trying to get the total duration of audio from an array of audio paths.
Here is what the array would look like:
var sound_paths = ["1.mp3","2.mp3",...]
I have looked at this post, which helped:
how to get audio.duration value by a function
However, I do not know how to implement this over an array. The idea is that I want to loop over each audio file, get its duration, and add it to a "sum_duration" variable.
I cannot seem to figure out a way to do this over a for loop. I have tried Promises (which I am admittedly new at): (Note the functions come from a class)
getDuration(src,cb){
// takes a source audio file and a callback function
var audio = new Audio();
audio.addEventListener("loadedmetadata",()=>{
cb(audio.duration);
});
audio.src = src;
}
getAudioArrayDuration(aud_path_arr){
// takes in an array of audio paths, finds the total audio duration
// in seconds
return new Promise((resolve)=>{
var duration = 0;
for(const aud_path in aud_path_arr){
var audio = new Audio();
audio.src = aud_path;
audio.onloadedmetadata = ()=>{
console.log(audio.duration);
duration += audio.duration;
}
resolve(duration);
}
});
}
However, this obviously does not work, and will just return the duration value of 0.
How can I loop over audio files and return the total audio duration of each file summed?
I think, in this case, working with promises is the right approach, time to get used to them ;) Try to remember, a promise will fullfil your wish in the near future. What makes your problem harder is that you have an array of files to check, each will need to separately be covered by it's own Promise, just so that your program can know when all of them have been fullfilled.
I always call my 'promised' getters 'fetch...', that way I know it'll return a promise instead of a direct value.
function fetchDuration(path) {
return new Promise((resolve) => {
const audio = new Audio();
audio.src = path;
audio.addEventListener(
'loadedmetadata',
() => {
// To keep a promise maintainable, only do 1
// asynchronous activity for each promise you make
resolve(audio.duration)
},
);
})
}
function fetchTotalDuration(paths) {
// Create an array of promises and wait until all have completed
return Promise.all(paths.map((path) => fetchDuration(path)))
// Reduce the results back to a single value
.then((durations) => durations.reduce(
(acc, duration) => acc + duration,
0,
))
;
}
At some point, your code is going to have to deal with this asynchronous stuff, I honestly believe that Promises are the easiest way to do this. It takes a little getting used to, but it'll be worth it in the end. The above could be used in your code something along the lines of:
window.addEventListener('DOMContentLoaded', () => {
fetchTotalDuration(["1.mp3","2.mp3",...])
.then((totalDuration) => {
document.querySelector('.player__total-duration').innerHTML = totalDuration;
})
;
});
I hacked this together real quick, so you'll have to adapt it to your function structure, but it's a working code snippet that should send you in the right direction.
Simply keep track of which audio files have been loaded, and if that matches the number of audio files queried, you call the callback with the total duration.
You should also take failing requests into account, so that if the loadedmetadata event never fires, you can react accordingly (by either falling back to 0 duration for that file, or throwing an Exception, etc.).
const cb = function(duration) {
console.log(`Total duration: ${duration}`);
};
let sound_paths = ["https://rawcdn.githack.com/anars/blank-audio/92f06aaa1f1f4cae365af4a256b04cf9014de564/5-seconds-of-silence.mp3","https://rawcdn.githack.com/anars/blank-audio/92f06aaa1f1f4cae365af4a256b04cf9014de564/2-seconds-of-silence.mp3"];
let totalDuration = 0;
let loadedSounds = [];
sound_paths.map(src => {
const audio = new Audio();
audio.addEventListener("loadedmetadata", ()=>{
totalDuration += audio.duration;
loadedSounds.push(audio);
if ( loadedSounds.length === sound_paths.length ) {
cb( totalDuration );
}
});
audio.src = src;
});

Offscreen-Canvas not rendering some of the time

I am trying to implement a in browser raster drawing plugin for the leaflet library that that extends the leaflets GridLayer api. Essentially for every tile there is function createTile that returns a canvas with some drawing on it. and leaflet shows the tile in correct position.
initialize: function(raster_data){
this.raster_data = raster_data;
},
createTile: function (tile_coords) {
let _tile = document.createElement('canvas');
let _tile_ctx = _tile.getContext('2d');
// do some drawing here with values from this.raster_data
return _tile;
}
This implementation is so far working fine. Than I thought of offloading drawing with offscreen-canvas in a webworker. so I restructured the code like this
initialize: function(raster_data){
this.raster_data = raster_data;
this.tile_worker = new Worker('tile_renderer.js')
},
createTile: function (tile_coords) {
let _tile = document.createElement('canvas').transferControlToOffscreen();
this.tile_worker.postMessage({
_tile: _tile,
_raster_data: this.raster_data
},[_tile])
return _tile;
}
This works but every now and then i see a canvas that is just blank. That thing is quite random I don't know start from where and how should I debug this. can this be a problem that I am using a single worker for rendering every tile? any help is appreciated. Here is an example of a blank canvas.
This a known bug: https://crbug.com/1202481
The issue appears when too many OffscreenCanvases are sent to the Worker serially.
The workaround is then to batch send all these OffscreenCanvases in a single call to postMessage().
In your code you could achieve this by storing all the objects to be sent and use a simple debouncing strategy using a 0 timeout to send them all at once:
createTile: function (tile_coords) {
let _tile = document.createElement('canvas');
_tile.setAttribute('width', 512);
_tile.setAttribute('height', 512);
let _off_tile = _tile.transferControlToOffscreen();
this.tiles_to_add.push( _off_tile ); // store for later
clearTimeout( this.batch_timeout_id ); // so that the callback is called only once
this.batch_timeout_id = setTimeout( () => {
// send them all at once
this.tileWorker.postMessage( { tiles: this.tiles_to_add }, this.tiles_to_add );
this.tiles_to_add.length = 0;
});
return _tile;
}
Live example: https://artistic-quill-tote.glitch.me/

Browser requests the same files declared in a setInterval

I use JavaScript to change the background-image of a div dynamically every 4 seconds. my code looks like this:
const images = [
'img/landing-min.jpg',
'img/gallery-1.jpg',
'img/side-img.jpg',
];
window.onload = () => {
const landing = document.getElementById('landing');
landing.dataset.i = 0;
setInterval(() => {
landing.style.backgroundImage = `url(${images[landing.dataset.i]})`;
landing.dataset.i = (landing.dataset.i+1) % images.length;
}, 4000)
}
The problem is that the browser makes a request to get the file each time the interval changes the background image, which makes a lot of duplicate requests. I want all files to be requested only once.

JavaScript - Loop through an array of image directories and apply them to an image

I have an array containing a few values, each one of them is an image location on a website.
["img/image1.png", "img/image2.png", "img/image3.png", "img/image4.png"]
I need to loop through them and apply them as a src attribute to an image sequentially every second, image1 then image2, image3 and etc...
I Really want to use a for loop but I have compleately no idea where to start...
I don't think a for loop applies here because of the asynchronous nature of what you want.
Let's think about the behaviour:
# psuedocode
choose the next image
set image src
when the image has loaded
wait one second
loop from line #1
Implementing that logic
const images = ["https://via.placeholder.com/150", "https://via.placeholder.com/150x50", "https://via.placeholder.com/150x100", "https://via.placeholder.com/150x25"]
const image = document.querySelector('#image')
let currentImage = 0
function updateImage() {
// choose the next image
const newImageSrc = images[currentImage++ % images.length]
// set image src
image.src = newImageSrc
// when the image has loaded
image.onload = function () {
// wait one second (1000 milliseconds) and "loop"
setTimeout(updateImage, 1000)
}
}
// start with an image immediately without waiting
updateImage()
<img id="image">
Other answers here have used setInterval. It's my opinion that you should avoid setInterval as much as possible, and use a setTimeout with a self-referencing loop.
consider
function doSomethingIntesive() {
...
}
setInterval(doSomethingIntesive, 1000)
you want to call doSomethingIntensive every second. What happens if it takes more than 1 second to execute doSomethingIntensive? In your case, doSomethingIntensive would be "download the image and show it". Over slow connections, it's definitely possible for it to take more than 1 second.
now consider
function doSomethingIntensive() {
...
setTimeout(doSomethingIntensive, 1000)
}
setTimeout(doSomethingIntensive, 1000) // or just doSomethingIntensive() if you don't want the initial wait
This code with do the intensive operation in its entirety then queue a new invocation of the intensive function. You end up with much more consistent behaviour.
a nifty trick is to use a "virtual" image to download the image before applying it to the real dom element - taking advantage of browser caching.
const images = ["https://via.placeholder.com/150", "https://via.placeholder.com/150x50", "https://via.placeholder.com/150x100", "https://via.placeholder.com/150x25"]
const image = document.querySelector('#image')
let currentImage = 0
const vImage = new Image()
function updateImage() {
// choose the next image
const newImageSrc = images[currentImage++ % images.length]
vImage.onload = function() {
image.src = newImageSrc
/* this should be instant but it's still good
* practice to always use the onload event handler */
image.onload = function () {
setTimeout(updateImage, 1000)
}
}
vImage.src = newImageSrc
}
updateImage()
<img id="image">
open up your browser's dev tools and enable connection throttling. The first image might take a while to show up, but you'll never have an empty (no) image showing. ...although that might be beyond what you need for this.
You would want to use setTimeout, this will run a function in x milliseconds.
const images = ["https://via.placeholder.com/150", "https://via.placeholder.com/150x50", "https://via.placeholder.com/150x100", "https://via.placeholder.com/150x25"]
// Get the image reference
const img = document.querySelector('#my-image')
// Add an event to listen for when it loads and trigger the next change
img.onload = () => setTimeout(changeImage, 1000)
// A counter to count the times we change images
let counter = 0
function changeImage() {
// Set the image src
// We will get the modulus value
// Using this the number will never be larger than the length
img.src = images[counter++ % images.length]
}
changeImage()
<img id="my-image">
(added by geuis) Alternate setInterval example since the questioner may not have a high level understanding of js:
let counter = 0
setInterval(() => {
img.src = images[counter % images.length];
counter++;
}, 1000);

Categories

Resources