<div class="codeview">
	<pre><code class='codeview-content language-javascript'></code></pre>
</div>
* {
	box-sizing: border-box;
}

body, html {
	margin: 0;
	padding: 0;
}

.codeview {
	width: 90vw;
	height: 90vh;
	margin: 5vh 5vw;
	background-color: black;
	padding: 1rem;
	overflow: scroll;
	animation: fadeIn 1s ease-in;
}
.codeview, .codeview * {
	font-family: "Fira Code", Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
	color: white;
	tab-size: 4;
}

.line {
	transition: all 1s;
	color: white;
}

@keyframes fadeIn {
	from {
		opacity: 0;
	}
	to {
		opacity: 1;
	}
}

.line.dim {
	filter: brightness(0.5);
}

.line.focus {
	filter: brightness(2);
	font-style: bold;
}

.line.removing {
	animation: fly-out cubic-bezier(1, 0, 1, 1) 1s forwards;
}

.line.adding {
	animation: fly-in cubic-bezier(0, 0, 0, 1) 1s forwards;
	transform: translateX(200vw);
	height: 0px;
}
/* TODO: come up with a better way */
.codeview-content .line.adding:nth-child(2n),
.codeview-content .line.removing:nth-child(2n){
	animation-delay: 100ms;
}

.codeview-content .line.adding:nth-child(3n),
.codeview-content .line.removing:nth-child(3n){
	animation-delay: 200ms;
}

.codeview-content .line.adding:nth-child(4n),
.codeview-content .line.removing:nth-child(4n){
	animation-delay: 300ms;
}

.codeview-content .line.adding:nth-child(5n),
.codeview-content .line.removing:nth-child(5n){
	animation-delay: 500ms;
}

.codeview-content .line.adding:nth-child(6n),
.codeview-content .line.removing:nth-child(6n){
	animation-delay: 600ms;
}

.codeview-content .line.adding:nth-child(7n),
.codeview-content .line.removing:nth-child(7n){
	animation-delay: 700ms;
}

.codeview-content .line.adding:nth-child(8n),
.codeview-content .line.removing:nth-child(8n){
	animation-delay: 800ms;
}

.codeview-content .line.adding:nth-child(9n),
.codeview-content .line.removing:nth-child(9n){
	animation-delay: 900ms;
}

.codeview-content .line.adding:nth-child(10n),
.codeview-content .line.removing:nth-child(10n){
	animation-delay: 1s;
}

@keyframes fly-out {
	from {
		transform: translateX(0);
	}
	to {
		transform: translateX(-200vw);
	}
}

@keyframes fly-in {
	from {
		transform: translateX(200vw);
		height: 0px;
	}
	to {
		transform: translateX(0);
		height: 100%;
	}
}
class CodeView {
	constructor(elem, code) {
		this.elem = document.querySelector(elem);
		this.code = code;
		this.render();
	}
	
	render() {
		this.elem.querySelector('.codeview-content').innerHTML = this._parse(this.code);
	}
	
	_parse(code, startIndex) {
		let manualCount = startIndex;
		return code
			.replace(/^\n/g, '  \n')
			.replace('\n\n', '\n  \n')
			.replace(/\n$/g, '\n  ')
			.split('\n')
			.map(item => item.replace(/&/g, "&amp;").replace(/>/g, "&gt;").replace(/</g, "&lt;"))
			.map((item, index) => `<div class='line line-${manualCount ? manualCount++ : index + 1}'>${item}</div>`).join('');
	}

	focusLine(num) {
		this._normalizeNums();
		
		const line = this.elem.querySelector(`.line-${num}`)
		
		line.classList.add('focus');
		line.classList.remove('dim');
		
		line.scrollIntoView({
			behavior: 'smooth',
			block: 'start'
		});
		
		this.elem.querySelectorAll(`.line:not(.line-${num})`).forEach(item => item.classList.add('dim'))
	}
	
	focus(...nums) {
		nums.forEach(num => this.focusLine(num));
	}
	
	replaceLine(lineNumber, code) {
		const line = this.elem.querySelector(`.line-${lineNumber}`);
		const newItems = createElementFromHTML(this._parse(code, lineNumber));

		line.classList.add('removing');

		setTimeout(() => {
			if (line.classList.contains('focus')) newItems.forEach(item => item.classList.add('focus'));
			if (line.classList.contains('dim')) newItems.forEach(item => item.classList.add('dim'));

			const next = line.nextSibling;

			newItems.forEach((item, index) => {
				item.classList.add('adding');
				line.parentNode.insertBefore(item, next);

				setTimeout(() => {
					item.scrollIntoView({
						behavior: "smooth",
						block: "start"
					});
					item.classList.remove('adding')
					if (index == newItems.length - 1) this._normalizeNums();
				}, 2000)
			});

			line.remove();
		}, 1000);
	}
	
	replace(code, from, to) {
		if (to) this.replaceMulti(from, to, code)
		else this.replaceLine(from, code)
	}
	
	replaceMulti(from, to, code) {	
		for (let i = from + 1; i <= to + 1; i++) {
			this.removeLine(i);
			this._normalizeNums();
		}
		
		this.replaceLine(from, code);
		this._normalizeNums();
	}
	
	insertAfter(lineNumber, code) {
		const line = this.elem.querySelector(`.line-${lineNumber}`);
		const newItems = createElementFromHTML(this._parse(code, lineNumber));
		const next = line.nextSibling;

		newItems.forEach((item, index) => {
			item.classList.add('adding');
			line.parentNode.insertBefore(item, next);
			setTimeout(() => {
				item.classList.remove('adding')
				if (index == newItems.length - 1) this._normalizeNums();
			}, 2000)
		});
	}
	insertBefore(line, code) {
		// TODO
	}
	
	removeLine(lineNumber) {
		const line = this.elem.querySelector(`.line-${lineNumber}`);
		line.classList.add('removing');
		setTimeout(() => {
			line.remove();
			this._normalizeNums();
		}, 1000)
	}
	
	_normalizeNums() {
		this.elem.querySelectorAll('.line').forEach((el, index) => {
			el.className = el.className.replace(/line-\d+/g, ` line-${index + 1} `)
		});
	}
	
	clearFocus(num) {
		this.elem.querySelectorAll(`.line`).forEach(item => item.classList.remove('dim', 'focus'))
	}
	
	scrollIntoView(num, block = 'end') {
		this.elem.querySelector(`.line-${num}`).scrollIntoView({
			behavior: 'smooth',
			block: block
		});
	}
	
	replaceText(code) {
		this.code = code;
		this.render();
	}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

const view = new CodeView('.codeview', ' ');

view.replace(`function h(tag, props, children) {

}`, 1)

setTimeout(() => {
	view.focus(2);
	view.replace(`	return {
		tag,
		props,
		children,
	}`, 2);
}, 5000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(7, `

function mount(vnode, container) {
 
}`)
}, 10000);

setTimeout(() => {
	view.clearFocus();
	view.focus(11);
	view.replace(`	const el = (vnode.el = document.createElement(vnode.tag))`, 11)
}, 15000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(11, `	for (const key in vnode.props) {
		el.setAttribute(key, vnode.props[key])
	}`);
	setTimeout(() => view.focus(12, 13, 14), 2000)
}, 20000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(14, `	// Children is a string/text
	if (typeof vnode.children === 'string') {
    	el.textContent = vnode.children
	}

	// Children are virtual nodes
	else {
	    vnode.children.forEach(child => {
        	mount(child, el) // Recursively mount the children
		})
	}`);
	setTimeout(() => view.focus(15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25), 2000)
}, 25000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(25, `	container.appendChild(el)`)
	setTimeout(() => view.focus(26), 2000)
}, 30000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(27, `
function unmount(vnode) {
 
}`)
	setTimeout(() => view.focus(29, 30, 31), 2000)
}, 35000);

setTimeout(() => {
	view.clearFocus();
	view.replace(`	vnode.el.parentNode.removeChild(vnode.el)`, 30)
	setTimeout(() => view.focus(30), 2000)
}, 40000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(31, ` 
function patch(n1, n2) {
 
}`)
	setTimeout(() => view.focus(32, 33, 34), 2000)
}, 45000);

setTimeout(() => {
	view.clearFocus();
	view.replace(`	const el = (n2.el = n1.el)`, 34)
	setTimeout(() => view.focus(34), 2000)
}, 50000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(34, `	if (n1.tag !== n2.tag) {
    	// Replace node
	    mount(n2, el.parentNode)
    	unmount(n1)
	} else {
    	// Nodes have different tags
	}`)
	setTimeout(() => view.focus(35, 36, 37, 38, 39, 40, 41), 2000)
}, 55000);

setTimeout(() => {
	view.clearFocus();
	view.replace(`		if (typeof n2.children === 'string') {
    	    el.textContent = n2.children
	    }`, 40)
	setTimeout(() => view.focus(40, 41, 42), 2000)
}, 60000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(42, ` 
		else {
			const c1 = n1.children
			const c2 = n2.children
			const commonLength = Math.min(c1.length, c2.length)
		}`)
	setTimeout(() => view.focus(44, 45, 46, 47,48), 2000)
}, 65000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(47, ` 
			for (let i = 0; i < commonLength; i++) {
		    	patch(c1[i], c2[i])
			}`)
	setTimeout(() => view.focus(49, 50, 51), 2000)
}, 70000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(51, ` 
			if (c1.length > c2.length) {
			    c1.slice(c2.length).forEach(child => {
			        unmount(child)
			    })
			}`)
	setTimeout(() => view.focus(53, 54, 55, 56, 57), 2000)
}, 75000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(57, ` 
			else if (c2.length > c1.length) {
			    c2.slice(c1.length).forEach(child => {
			        mount(child, el)
			    })
			}`)
	setTimeout(() => view.focus(59, 60, 61, 62, 63), 2000)
}, 80000);

setTimeout(() => {
	view.clearFocus();
	view.insertAfter(66, ` 
/*
Done :D
  
You just saw a demo of my project Codeview
This could be used as slides, or could be linked to scroll in a blog post for a cool effect!

All code is by Marc Backes. Code from this post: https://dev.to/themarcba/create-your-own-vue-js-from-scratch-part-2-building-the-vdom-3bp2
  
Like it? 👋
*/`)
	setTimeout(() => view.focus(68, 69, 70, 71, 72, 73, 74, 75, 76, 77), 2000)
}, 85000);

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

function createElementFromHTML(htmlString) {
	const div = document.createElement('div');
	div.innerHTML = htmlString;
	return Array.from(div.childNodes);
}

function sleep(ms) {
    const now = new Date().getTime();
    while(new Date().getTime() < now + ms){ /* do nothing */ } 
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.