diff --git a/iwsy/iwsy.js b/iwsy/iwsy.js
index 646a8d4..b5b81bc 100644
--- a/iwsy/iwsy.js
+++ b/iwsy/iwsy.js
@@ -300,6 +300,40 @@ const IWSY = (playerElement, scriptObject) => {
 		}
 	};
 
+	// Animate a fade
+	const animateFade = (timestamp, vfx) => {
+		if (script.stop) {
+			return;
+		}
+		if (vfx.start === undefined) {
+			vfx.start = timestamp;
+		}
+		const elapsed = timestamp - vfx.start;
+		if (elapsed < vfx.duration) {
+			const ratio =  0.5 - Math.cos(Math.PI * elapsed / vfx.duration) / 2;
+			for (const block of vfx.stepBlocks)
+			{
+				block.element.style.opacity = vfx.upDown ? ratio : 1.0 - ratio;
+			}
+			requestAnimationFrame(timestamp => {
+				animateFade(timestamp, vfx);
+			});
+		} else {
+			for (const block of vfx.stepBlocks)
+			{
+				block.element.style.opacity = vfx.upDown ? 1 : 0;
+				block.element.style.display = vfx.upDown ? `block` :`none`;
+			}
+			if (!vfx.continueFlag) {
+				if (script.runMode === `manual`) {
+					enterManualMode(step);
+				} else {
+					vfx.step.next();
+				}
+			}
+		}
+	};
+
 	// Fade up or down
 	const doFade = (step, upDown) => {
 		const stepBlocks = [];
@@ -327,48 +361,20 @@ const IWSY = (playerElement, scriptObject) => {
 				}
 			}
 		} else {
-			const animSteps = Math.round(step.duration * 25);
-			const continueFlag = step.continue === `yes`;
 			for (const block of stepBlocks)
 			{
 				block.element.style.display = `block`;
 			}
-			let animStep = 0;
-			const interval = setInterval(() => {
-				try {
-					if (animStep < animSteps) {
-						const ratio =  0.5 - Math.cos(Math.PI * animStep / animSteps) / 2;
-						for (const block of stepBlocks)
-						{
-							// if (block.element) {
-							block.element.style.opacity = upDown ? ratio : 1.0 - ratio;
-							// }
-						}
-						animStep++;
-					} else {
-						for (const block of stepBlocks)
-						{
-							// if (block.element) {
-							block.element.style.opacity = upDown ? 1 : 0;
-							block.element.style.display = upDown ? `block` :`none`;
-							// }
-						}
-						clearIntervalTimer(interval);
-						if (!continueFlag) {
-							if (script.runMode === `manual`) {
-								enterManualMode(step);
-							} else {
-								step.next();
-							}
-						}
-					}
-				} catch(err) {
-					clearIntervalTimer(interval);
-					throw Error(err);
-				}
-			}, 40);
-			addIntervalTimer(interval);
-			if (continueFlag) {
+			const vfx = {};
+			vfx.step = step;
+			vfx.stepBlocks = stepBlocks;
+			vfx.upDown = upDown;
+			vfx.duration = step.duration * 1000;
+			vfx.continueFlag = step.continue === `yes`;
+			requestAnimationFrame(timestamp => {
+				animateFade(timestamp, vfx);
+			});
+			if (vfx.continueFlag) {
 				step.next();
 			}
 		}
@@ -436,7 +442,11 @@ const IWSY = (playerElement, scriptObject) => {
 		image.style.top = `${yoff}px`;
 	};
 
-	const doPanzoom = (timestamp, vfx) => {
+	// Animate a pan-zoom
+	const animatePanzoom = (timestamp, vfx) => {
+		if (script.stop) {
+			return;
+		}
 		if (vfx.start === undefined) {
 			vfx.start = timestamp;
 		}
@@ -450,7 +460,7 @@ const IWSY = (playerElement, scriptObject) => {
 			image.style.left = `${vfx.xoff + (vfx.xoff2 - vfx.xoff) * ratio}px`;
 			image.style.top = `${vfx.yoff + (vfx.yoff2 - vfx.yoff) * ratio}px`;
 			requestAnimationFrame(timestamp => {
-				doPanzoom(timestamp, vfx);
+				animatePanzoom(timestamp, vfx);
 			});
 		} else {
 			image.style.width = `${vfx.w2}px`;
@@ -485,7 +495,7 @@ const IWSY = (playerElement, scriptObject) => {
 				};
 				delete(vfx.start);
 				requestAnimationFrame(timestamp => {
-					doPanzoom(timestamp, vfx);
+					animatePanzoom(timestamp, vfx);
 				});
 				break;
 			}
@@ -529,12 +539,46 @@ const IWSY = (playerElement, scriptObject) => {
 		image.addEventListener(`load`, () => {
 			initImage(spec);
 			requestAnimationFrame(timestamp => {
-				doPanzoom(timestamp, vfx);
+				animatePanzoom(timestamp, vfx);
 			});
 		});
 		player.appendChild(image);
 	};
 
+	// Animate a crossfade
+	const animateCrossfade = (timestamp, vfx) => {
+		if (script.stop) {
+			return;
+		}
+		if (vfx.start === undefined) {
+			vfx.start = timestamp;
+		}
+		const elapsed = timestamp - vfx.start;
+		if (elapsed < vfx.duration) {
+			const ratio =  0.5 - Math.cos(Math.PI * elapsed / vfx.duration) / 2;
+			vfx.block.element.style.opacity = 1.0 - ratio;
+			vfx.element.style.opacity = ratio;
+			requestAnimationFrame(timestamp => {
+				animateCrossfade(timestamp, vfx);
+			});
+		} else {
+			vfx.block.textPanel.innerHTML = vfx.newText;
+			if (vfx.content.url) {
+				vfx.block.element.style.background = `url("${vfx.content.url}")`;
+			}
+			vfx.block.element.style[`background-size`] = `cover`;
+			vfx.block.element.style.opacity = 1.0 ;
+			removeElement(vfx.element);
+			if (!vfx.continueFlag) {
+				if (script.runMode === `manual`) {
+					enterManualMode(step);
+				} else {
+					vfx.step.next();
+				}
+			}
+		}
+	};
+
 	// Handle a crossfade
 	const crossfade = step => {
 		for (const content of script.content) {
@@ -545,10 +589,14 @@ const IWSY = (playerElement, scriptObject) => {
 				const newText = converter.makeHtml(content.content.split(`%0a`).join(`\n`));
 				for (const block of script.blocks) {
 					if (block.defaults.name === step.block) {
+						if (block.element === undefined) {
+							throw Error(`Block '${block.defaults.name}' has no DOM element.`);
+						}
 						if (script.speed === `scan`) {
 							block.textPanel.innerHTML = newText;
 							step.next();
 						} else {
+							const continueFlag = step.continue === `yes`;
 							const element = document.createElement(`div`);
 							player.appendChild(element);
 							element.style.position = `absolute`;
@@ -575,35 +623,18 @@ const IWSY = (playerElement, scriptObject) => {
 							text.style[`text-align`] = block.textPanel.style[`text-align`];
 							text.style.color = block.textPanel.style.color;
 							text.innerHTML = newText;
+							const vfx = {};
+							vfx.step = step;
+							vfx.block = block;
+							vfx.element = element;
+							vfx.content = content;
+							vfx.newText = newText;
+							vfx.duration = step.duration * 1000;
+							vfx.continueFlag = continueFlag;
+							requestAnimationFrame(timestamp => {
+								animateCrossfade(timestamp, vfx);
+							});
                 
-							const animSteps = Math.round(step.duration * 25);
-							const continueFlag = step.continue === `yes`;
-							let animStep = 0;
-							const interval = setInterval(() => {
-								if (animStep < animSteps) {
-									const ratio =  0.5 - Math.cos(Math.PI * animStep / animSteps) / 2;
-									block.element.style.opacity = 1.0 - ratio;
-									element.style.opacity = ratio;
-									animStep++;
-								} else {
-									clearIntervalTimer(interval);
-									block.textPanel.innerHTML = newText;
-									if (content.url) {
-										block.element.style.background = `url("${content.url}")`;
-									}
-									block.element.style[`background-size`] = `cover`;
-									block.element.style.opacity = 1.0 ;
-									removeElement(element);
-									if (!continueFlag) {
-										if (script.runMode === `manual`) {
-											enterManualMode(step);
-										} else {
-											step.next();
-										}
-									}
-								}
-							}, 40);
-							addIntervalTimer(interval);
 							if (continueFlag) {
 								step.next();
 							}
@@ -721,6 +752,37 @@ const IWSY = (playerElement, scriptObject) => {
 		block.current.fontSize = target.defaults.fontSize;
 	};
 
+	// Animate a transition
+	const animateTransition = (timestamp, vfx) => {
+		if (script.stop) {
+			return;
+		}
+		if (vfx.start === undefined) {
+			vfx.start = timestamp;
+		}
+		const elapsed = timestamp - vfx.start;
+		if (elapsed < vfx.duration) {
+			const ratio =  0.5 - Math.cos(Math.PI * elapsed / vfx.duration) / 2;
+			try {
+				doTransitionStep(vfx.block, vfx.target, ratio);
+				requestAnimationFrame(timestamp => {
+					animateTransition(timestamp, vfx);
+				});
+			} catch (err) {
+				console.log(err);
+			}
+		} else {
+			setFinalState(vfx.block,vfx.target);
+			if (!vfx.continueFlag) {
+				if (script.runMode === `manual`) {
+					enterManualMode(step);
+				} else {
+					vfx.step.next();
+				}
+			}
+		}
+	};
+
 	// Handle a transition
 	const transition = step => {
 		let block = null;
@@ -744,31 +806,16 @@ const IWSY = (playerElement, scriptObject) => {
 			setFinalState(block,target);
 			step.next();
 		} else {
-			const animSteps = Math.round(step.duration * 25);
-			let animStep = 0;
 			const continueFlag = step.continue === `yes`;
-			const interval = setInterval(() => {
-				if (animStep < animSteps) {
-					const ratio =  0.5 - Math.cos(Math.PI * animStep / animSteps) / 2;
-					try {
-						doTransitionStep(block, target, ratio);
-					} catch (err) {
-						clearIntervalTimer(interval);
-					}
-					animStep++;
-				} else {
-					clearIntervalTimer(interval);
-					setFinalState(block,target);
-					if (!continueFlag) {
-						if (script.runMode === `manual`) {
-							enterManualMode(step);
-						} else {
-							step.next();
-						}
-					}
-				}
-			}, 40);
-			addIntervalTimer(interval);
+			const vfx = {};
+			vfx.step = step;
+			vfx.block = block;
+			vfx.target = target;
+			vfx.duration = step.duration * 1000;
+			vfx.continueFlag = continueFlag;
+			requestAnimationFrame(timestamp => {
+				animateTransition(timestamp, vfx);
+			});
 			if (continueFlag) {
 				step.next();
 			}
@@ -1084,7 +1131,7 @@ const IWSY = (playerElement, scriptObject) => {
 						if (script.runMode == `auto` || script.speed === `scan`) {
 							setTimeout(() => {
 								if (script.stop) {
-									script.stop = false;
+									// script.stop = false;
 									restoreCursor();
 								} else {
 									doStep(nextStep);
diff --git a/iwsy/resources/scripts/demo.json b/iwsy/resources/scripts/demo.json
index 2a237f0..a0beda6 100644
--- a/iwsy/resources/scripts/demo.json
+++ b/iwsy/resources/scripts/demo.json
@@ -37,12 +37,6 @@
       "duration": 2,
       "continue": false
     },
-    {
-      "title": "pause 2 seconds",
-      "action": "pause",
-      "label": "",
-      "duration": 2
-    },
     {
       "title": "set up title and subtitle",
       "action": "set content",