Преглед изворни кода

separate parse from preload element creation

add latency to distinguish computed durations

add wrapper for elements to abstract delay/latency behavior

add gif duration computation
Casey DeLorme пре 2 година
родитељ
комит
7b045cc0de
3 измењених фајлова са 107 додато и 58 уклоњено
  1. 2 1
      demo.html
  2. 28 14
      readme.md
  3. 77 43
      show.js

+ 2 - 1
demo.html

@@ -24,7 +24,8 @@
                 'image.jpg',
                 {
                     file: 'animated.gif',
-                    delay: 960*5,
+                    delay: 5,
+                    latency: 960
                 },
                 {
                     file: 'substitute/%s.png',

+ 28 - 14
readme.md

@@ -20,34 +20,48 @@ A new instance of `Show` accepts three parameters:
 
 The parent element will have its children replaced.
 
-There are two formats for files.
+The files array has two accepted formats:
 
 - A string
 - An object
 
 The string is treated verbatim and assigned the default delay value.
 
-The only required value is `file`.
+When given an object it looks at four potential properties:
 
-The object format supports an optional `delay` as an override.
+- file (the name, which may include string substitution formatting)
+- delay (to override the parent, or used as a multiplier for videos or gifs)
+- range (used to iterate over a numeric range or hard-coded array of substitutions)
+- latency (used for gif and video with delay as an optional multiplier)
 
-If you are working with a group of files then the `range` array property is useful.
+The string format substitution works with two options:
 
-As an array it supports two numbers as minimum and maximum, or a set of string substitutes.
+- direct replacement of `%s`
+- left-padded (eg. `%03s` creating `001`, `002`, `003`, etc...)
 
-When `range` is present the `file` property will be treated as as "format string", which supports these formats:
+_A file name can contain multiple substitutions and multiple formats, all of them will be recursively processed._
 
-- a direct string replacement (eg. `%s`)
-- a left pad (eg. `%03s` creating `001`, `002`, `003`, etc)
+The `range` is an array with a start and end for a numeric loop, or raw values that can be numbers or strings.
 
-_Substitution only supports individual characters not whole patterns, but they do **not** need to be numbers._
+This system supports video files with the extensions of `mp4`, `webm`, and `ogg`.  _Due to browser limitations videos are treated as muted by default._
 
-**If the file is a video it will substitute the default `delay` for the video duration, otherwise it will treat the delay as a multiplier allowing the video to loop a specified number of times.**
+When the file is a video or ends in `.gif`, an attempt will be made to extract the duration of the video or animated gif, and an updated value will be set to `latency`.  _If `delay` was provided, it will be treated as a multiplier._
 
-This system supports video files with the extensions `mp4`, `webm`, and `ogg`.  _Due to browser behavior (namely Google Chrome), videos will be muted._
+The system attempts to preload up to 40% (or +-5) of the next and previous files which should prevent stutter when rendering.
 
-The system preloads all files and beings playing automatically.
+Simple operations are exposed to allow manipulation:
 
-It provides a `toggle()` function to change its state.
+- `toggle()` - change play state
+- `next()` - go to the next image
+- `previous()` - go to the previous image
 
-If the target window is not the active window the loop will be halted.
+**If the target window is not active, the rendering operation will be paused.**
+
+
+### gifDuration bypass
+
+Security on browsers may prevent `gifDuration` from being calculated when loading local files without a web server.
+
+To resolve this you can set `security.fileuri.strict_origin_policy` to `false` in firefox, or launch google chrome with the `--allow-file-access-from-files` flag.
+
+_Another option would be to set the `latency` property manually._

+ 77 - 43
show.js

@@ -1,5 +1,16 @@
 'use strict';
 (() => {
+
+	function ShowFile(file, delay, latency) {
+		this.file = file;
+		this.delay = delay;
+		this.latency = latency;
+	}
+
+	ShowFile.prototype.Delay = function() {
+		return this.latency ? this.latency * (this.delay ? this.delay : 1) : this.delay;
+	}
+
 	function Show(parent, files, delay) {
 		this.index = 0;
 		this.elapsed = 0;
@@ -10,7 +21,7 @@
 		this.delay = !isNaN(delay) ? delay : 3000;
 		this.parent = parent;
 		this.parent.innerHTML = '';
-		this.parent.appendChild(document.createElement('img'));
+		this.parent.appendChild(new Image());
 		(() => {
 			this.parse(files);
 			window.requestAnimationFrame(() => { this.render(); });
@@ -46,15 +57,68 @@
 		return f
 	};
 
+	Show.prototype.add = function(file, delay, latency) {
+		let o = new ShowFile(file, delay, latency);
+		// let o = {file: file, delay: delay, latency: latency};
+		this.list.push(o);
+	}
+
+	Show.prototype.parse = function(files) {
+		if (!(files instanceof Array)) return;
+		files.map((o) => {
+			if (typeof o === 'string') {
+				this.add(o);
+			} else if (o instanceof Object && !(o instanceof Array) && o !== null && typeof o.file !== 'undefined' && typeof o.file === 'string') {
+				if (typeof o.range !== 'undefined' && o.range instanceof Array) {
+					if (o.range.length === 2 && !(isNaN(o.range[2]) && isNaN(o.range[1])) && parseInt(o.range[0]) < parseInt(o.range[1])) {
+						for (let i = parseInt(o.range[0]); i <= parseInt(o.range[1]); i++) {
+							this.add(this.format(o.file, i), o.delay, o.latency);
+						}
+					} else {
+						for (let i of o.range) {
+							this.add(this.format(o.file, i), o.delay, o.latency);
+						}
+					}
+				} else {
+					this.add(o.file, o.delay, o.latency);
+				}
+			}
+		});
+	};
+
+	Show.prototype.gifDuration = async (el, o) => {
+		try {
+			let response = await fetch(o.file);
+			let data = await response.blob();
+			let f = new FileReader();
+			f.readAsArrayBuffer(data);
+			f.onload = (event) => {
+				let arr = new Uint8Array(f.result);
+				let d = 0;
+				for (var i = 0; i < arr.length; i++) {
+					if (arr[i] == 0x21
+					&& arr[i + 1] == 0xF9
+					&& arr[i + 2] == 0x04
+					&& arr[i + 7] == 0x00) {
+						const delay = (arr[i + 5] << 8) | (arr[i + 4] & 0xFF)
+						d += delay < 2 ? 10 : delay;
+					}
+				}
+				o.latency = d*10;
+			}
+		} catch(e) {}
+	}
+
 	Show.prototype.generate = function(idx) {
 		let o = this.list[idx];
-		let el;
+		let el = new Image();
 		if (o.file.endsWith('.mp4') || o.file.endsWith('.webm') || o.file.endsWith('.ogg')) {
 			el = document.createElement('video');
+			el.addEventListener('canplay', () => { o.latency = (isNaN(o.delay) ? 1 : o.delay) * Math.trunc(el.duration * 1000); }, {once: true});
 			el.muted = true;
 			el.loop = true;
-		} else {
-			el = document.createElement('img');
+		} else if (o.file.endsWith('.gif')) {
+			el.addEventListener('load', () => { this.gifDuration(el, o); }, {once: true});
 		}
 		el.src = o.file;
 		el.addEventListener('error', () => { this.list.splice(this.list.indexOf(o), 1); }, {once: true});
@@ -62,65 +126,35 @@
 	}
 
 	Show.prototype.preload = function() {
-		let partial = Math.max(Math.trunc(.1 * this.list.length), 1);
+		let partial = Math.min(.4 * this.list.length << 0, 5);
 		let position = partial > this.index ? this.list.length - Math.abs(partial - this.index) : this.index - partial;
 		for (let i = 0; i <= partial*2; i++) {
 			let l = (position+i)%this.list.length;
 			let o = this.list[l];
 			if (!this.preloaded.hasOwnProperty(o.file)) {
 				this.preloaded[o.file] = this.generate(l);
-				setTimeout(() => { delete this.preloaded[o.file]; }, 2*(o.delay ? o.delay : this.delay));
+				setTimeout(() => { delete this.preloaded[o.file]; }, partial*o.delay);
 			}
 		}
 	}
 
-	Show.prototype.add = function(file, delay) {
-		let o = {file: file, delay: delay};
-		this.list.push(o);
-		if (o.file.endsWith('.mp4') || o.file.endsWith('.webm') || o.file.endsWith('.ogg')) {
-			let el = document.createElement('video');
-			el.addEventListener('canplay', () => { o.delay = (isNaN(o.delay) ? 1 : o.delay) * Math.trunc(el.duration * 1000); }, {once: true});
-			el.src = o.file;
-		}
+	Show.prototype.resize = function() {
+		let el = this.parent.firstChild;
+		let vid = (el.tagName == 'VIDEO') ? 'video' : 'natural';
+		this.parent.firstChild.className = (el[vid+'Width'] / el[vid+'Height'] < this.parent.clientWidth / this.parent.clientHeight) ? 'fillheight' : 'fillwidth';
 	}
 
-	Show.prototype.parse = function(files) {
-		if (!(files instanceof Array)) return;
-		files.map((o) => {
-			if (typeof o === 'string') {
-				this.add(o);
-			} else if (o instanceof Object && !(o instanceof Array) && typeof o.file !== 'undefined' && typeof o.file === 'string') {
-				if (o.file.indexOf('%') > -1 && typeof o.range !== 'undefined' && o.range instanceof Array) {
-					if (o.range.length === 2 && !(isNaN(o.range[2]) && isNaN(o.range[1])) && parseInt(o.range[0]) < parseInt(o.range[1])) {
-						for (let i = parseInt(o.range[0]); i <= parseInt(o.range[1]); i++) {
-							this.add(this.format(o.file, i), o.delay);
-						}
-					} else {
-						for (let i of o.range) {
-							this.add(this.format(o.file, i), o.delay);
-						}
-					}
-				} else {
-					this.add(o.file, o.delay);
-				}
-			}
-		});
-	};
-
 	Show.prototype.render = function() {
 		if (!this.list.length) return;
+		this.resize();
 		let d = Date.now();
-		let el = this.parent.firstChild;
-		let vid = (el.tagName == 'VIDEO') ? 'video' : 'natural';
-		this.parent.firstChild.className = (el[vid+'Width'] / el[vid+'Height'] < this.parent.clientWidth / this.parent.clientHeight) ? 'fillheight' : 'fillwidth';
 		let o = this.list[this.index];
 		if (this.current != this.index) {
 			this.current = this.index;
 			this.preload();
 			let el = this.preloaded[o.file];
-			let vid = (el.tagName == 'VIDEO') ? 'video' : 'natural';
 			if (el.tagName !== this.parent.firstChild.tagName) this.parent.replaceChild(el.cloneNode(), this.parent.firstChild);
-			if (vid === 'video') {
+			if (el.tagName == 'VIDEO') {
 				if (this.parent.firstChild.src !== el.src) this.parent.firstChild.src = el.src;
 				this.parent.firstChild.currentTime = 0;
 				this.parent.firstChild.loop = true;
@@ -131,7 +165,7 @@
 			}
 		}
 		if (this.playing && document.hasFocus()) this.elapsed += (d - this.updated);
-		if (this.elapsed >= (o.delay | this.delay)) this.next();
+		if (this.elapsed >= (o.Delay() | this.delay)) this.next();
 		s.updated = d;
 		window.requestAnimationFrame(() => { this.render(); });
 	}