Introduction

EpiphanyShowcase

Welcome to Epiphany, a new learning and authoring experience with interactivity, version control and social features, aiming to become a platform for future interactive textbooks and research papers. With Epiphany, you can write equations easily:

\[|I_2|=\left| \int_{0}^T \psi(t) \left\{ u(a,t)- \int_{\gamma(t)}^a \frac{d\theta}{k(\theta,t)} \int_{a}^\theta c(\xi)u_t(\xi,t)\,d\xi \right\} dt \right|\]

or plot an interactive 3D surface:

\[f(x,y) = sin(1.5 x) cos(1.5 y)\]

await load(‘https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.min.js’)

let ui = createUi();

let hsv2rgb = function (H, S, V) { var R, G, B, C, Hi, X;

C = V * S;
Hi = Math.floor(H / 60);
X = C * (1 - Math.abs(((H / 60) % 2) - 1));

switch (Hi) {
    case 0: R = C; G = X; B = 0; break;
    case 1: R = X; G = C; B = 0; break;
    case 2: R = 0; G = C; B = X; break;
    case 3: R = 0; G = X; B = C; break;
    case 4: R = X; G = 0; B = C; break;
    case 5: R = C; G = 0; B = X; break;

    default: R = 0; G = 0; B = 0; break;
}

return new THREE.Vector3(R, G, B);

};

let updateGeometry = function(geometry, geometry2, plotoffsetx, plotoffsety, isUpdate) { var indices = [] var vertices2 = [] var vertices3 = [] var normalCal = [] var minZ = 10 var maxZ = -10

if (isUpdate)
{
    vertices2 = geometry.attributes.position.array
    vertices3 = geometry2.attributes.position.array
}
let h = 0
for (var y = 0; y < 21; y++)
    for (var x = 0; x < 21; x++) {
        let locY = y * (4.0 / 20.0) - 2.0
        let locX = x * (4.0 / 20.0) - 2.0
        let locZ = Math.sin((locX+plotoffsetx) * 1.5) * Math.cos((locY+plotoffsety) * 1.5)

        if (minZ > locZ)
            minZ = locZ;

        if (maxZ < locZ)
            maxZ = locZ

        if (isUpdate)
        {
            vertices2[h++] = locX
            vertices2[h++] = locY
            vertices2[h++] = locZ
        }
        else
        {
            vertices2.push(locX, locY, locZ)
        }
        normalCal.push(new THREE.Vector3(0, 0, 0))
    }

h = 0

for (var y = 0; y < 20; y++) {
    for (var x = 0; x < 20; ++x) {
        let baseIdx = y * 21 + x
        let ind1 = (y + 1) * 21 + x + 1
        let ind2 = y * 21 + x + 1
        let ind3 = (y + 1) * 21 + x

        if (!isUpdate){
            indices.push(baseIdx, ind2, ind1)
            indices.push(baseIdx, ind1, ind3)
        }
        let basev = new THREE.Vector3(vertices2[baseIdx * 3], vertices2[baseIdx * 3 + 1], vertices2[baseIdx * 3 + 2])
        let ind1v = new THREE.Vector3(vertices2[ind1 * 3], vertices2[ind1 * 3 + 1], vertices2[ind1 * 3 + 2])
        let ind2v = new THREE.Vector3(vertices2[ind2 * 3], vertices2[ind2 * 3 + 1], vertices2[ind2 * 3 + 2])
        let ind3v = new THREE.Vector3(vertices2[ind3 * 3], vertices2[ind3 * 3 + 1], vertices2[ind3 * 3 + 2])

        let v2 = new THREE.Vector3();
    v2.subVectors(ind2v, basev)
        let v1 = new THREE.Vector3();
    v1.subVectors(ind1v, basev)

        let n1 = new THREE.Vector3;
    n1.crossVectors(v2, v1)

        if (!isUpdate)
        {
            vertices3.push(vertices2[baseIdx * 3], vertices2[baseIdx * 3 + 1], vertices2[baseIdx * 3 + 2])
            vertices3.push(vertices2[ind3 * 3], vertices2[ind3 * 3 + 1], vertices2[ind3 * 3 + 2])
            vertices3.push(vertices2[baseIdx * 3], vertices2[baseIdx * 3 + 1], vertices2[baseIdx * 3 + 2])
            vertices3.push(vertices2[ind2 * 3], vertices2[ind2 * 3 + 1], vertices2[ind2 * 3 + 2])
        }
        else
        {
            vertices3[h++] = vertices2[baseIdx * 3]
            vertices3[h++] = vertices2[baseIdx * 3 + 1]
            vertices3[h++] = vertices2[baseIdx * 3 + 2]
            vertices3[h++] = vertices2[ind3 * 3]
            vertices3[h++] = vertices2[ind3 * 3 + 1]
            vertices3[h++] = vertices2[ind3 * 3 + 2]
            vertices3[h++] = vertices2[baseIdx * 3]
            vertices3[h++] = vertices2[baseIdx * 3 + 1]
            vertices3[h++] = vertices2[baseIdx * 3 + 2]
            vertices3[h++] = vertices2[ind2 * 3]
            vertices3[h++] = vertices2[ind2 * 3 + 1]
            vertices3[h++] = vertices2[ind2 * 3 + 2]
        }
        normalCal[baseIdx].add(n1);
        normalCal[ind1].add(n1);
        normalCal[ind2].add(n1);
        normalCal[ind3].add(n1);
    //  break;
    }
//  break;
}

let normals = []
let colors = []

if (isUpdate)
{
    normals = geometry.attributes.normal.array
    colors = geometry.attributes.color.array
}

let e = 0;
h = 0;

for (var i = 0; i < normalCal.length; ++i) {
    normalCal[i].normalize();

    if (!isUpdate)
    {
        normals.push(normalCal[i].x, normalCal[i].y, normalCal[i].z)
    }
    else
    {
        normals[e++] = normalCal[i].x
        normals[e++] = normalCal[i].y
        normals[e++] = normalCal[i].z
    }
    /*let cv = normalCal[i]
    cv.add(new THREE.Vector3(1,1,1))
    cv.normalize()
    colors.push(cv.x, cv.y, cv.z)*/
    let hue = (1 - (vertices2[i * 3 + 2] - minZ) / (maxZ - minZ)) * 240
    //console.log(hue)
    let color = hsv2rgb(hue, 1, 1);
    if (!isUpdate)
    {
        colors.push(color.x, color.y, color.z)
    }
    else
    {
        colors[h++] = color.x
        colors[h++] = color.y
        colors[h++] = color.z
    }
}

if (!isUpdate)
{
    // itemSize = 3 because there are 3 values (components) per vertex
    geometry.setIndex(indices)
    geometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices2, 3));
    geometry.addAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
    geometry.addAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
    geometry2.addAttribute('position', new THREE.Float32BufferAttribute(vertices3, 3));
}
else
{
    geometry.attributes.position.needsUpdate = true;
    geometry.attributes.normal.needsUpdate = true;
    geometry.attributes.color.needsUpdate = true;
    geometry.computeBoundingSphere();
    geometry2.attributes.position.needsUpdate = true;
    geometry2.computeBoundingSphere();
}

}

let vshader = ‘ void main() {\n\ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n\ gl_Position.z -= 0.01;\n\ }’

let fshader = ‘void main() {\n\ gl_FragColor = vec4(0.3,0.3,0.3,1.0);\n\ }’

let canvas = getCanvas() canvas.width = 640 canvas.height = 480

const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true });

renderer.autoClear = false;

var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(45, 640 / 480, 0.1, 1000); camera.position.z = 4; //var camera = new THREE.OrthographicCamera( 6.4 / 2, 6.4 / -2, 4.8 / 2, 4.8 / - 2, 1, 1000 );

renderer.setClearColor(“#FFFFFF”); renderer.setSize(640, 480);

let cameraDis = 7.0 let cameraFrom = new THREE.Vector3(12, 10, 6.2) cameraFrom.normalize() cameraFrom.multiplyScalar(cameraDis) let cameraTo = new THREE.Vector3(0, 0, 0) let cameraUp = new THREE.Vector3(0, 0, 1)

camera.position.set(cameraFrom.x, cameraFrom.y, cameraFrom.z) camera.up = cameraUp camera.up.normalize() camera.lookAt(cameraTo);

canvas.onresize = function (width, height) { camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); camera.position.z = 4; //camera = new THREE.OrthographicCamera( 6.4 / 2, 6.4 / -2, 4.8 / 2, 4.8 / - 2, 1, 1000 ); renderer.setSize(width, height);

camera.position.set(cameraFrom.x, cameraFrom.y, cameraFrom.z)
camera.up = cameraUp
camera.up.normalize()
camera.lookAt(cameraTo);

}

var vertices4 = []

var geometry = new THREE.BufferGeometry(); var geometry2 = new THREE.BufferGeometry();

updateGeometry(geometry, geometry2, 0, 0, false)

var material = new THREE.MeshBasicMaterial({ vertexColors: THREE.VertexColors }); material.side = THREE.DoubleSide;

var mesh = new THREE.Mesh(geometry, material); // Add cube to Scene scene.add(mesh);

var material3 = new THREE.ShaderMaterial({ vertexShader: vshader, fragmentShader: fshader })

var frame = new THREE.LineSegments(geometry2, material3) scene.add(frame)

vertices4.push(-2.0, -2.0, -1.0) vertices4.push(2.0, -2.0, -1.0) vertices4.push(2.0, -2.0, -1.0) vertices4.push(2.0, 2.0, -1.0) vertices4.push(2.0, 2.0, -1.0) vertices4.push(-2.0, 2.0, -1.0) vertices4.push(-2.0, 2.0, -1.0) vertices4.push(-2.0, -2.0, -1.0)

vertices4.push(-2.0, -2.0, 1.0) vertices4.push(2.0, -2.0, 1.0) vertices4.push(2.0, -2.0, 1.0) vertices4.push(2.0, 2.0, 1.0) vertices4.push(2.0, 2.0, 1.0) vertices4.push(-2.0, 2.0, 1.0) vertices4.push(-2.0, 2.0, 1.0) vertices4.push(-2.0, -2.0, 1.0) vertices4.push(-2.0, -2.0, 1.0) vertices4.push(-2.0, -2.0, -1.0) vertices4.push(2.0, 2.0, 1.0) vertices4.push(2.0, 2.0, -1.0) vertices4.push(-2.0, 2.0, 1.0) vertices4.push(-2.0, 2.0, -1.0) vertices4.push(2.0, -2.0, 1.0) vertices4.push(2.0, -2.0, -1.0)

var geometry3 = new THREE.BufferGeometry(); geometry3.addAttribute(‘position’, new THREE.Float32BufferAttribute(vertices4, 3)); var coord = new THREE.LineSegments(geometry3, material3) scene.add(coord)

// Render Loop var render = function () { requestAnimationFrame(render); renderer.clear(); renderer.render(scene, camera); };

let onMouseDownPosition = { x: 0, y: 0 } let isMouseDown = false;

function onMouseDown(event) { onMouseDownPosition = { x: event.clientX, y: event.clientY } isMouseDown = true; }

function onTouchStart(event) { onMouseDownPosition = { x: event.touches[0].clientX, y: event.touches[0].clientY } isMouseDown = true; }

function onMouseUp(event) { isMouseDown = false; }

onMouseDownTheta = 0; onMouseDownPhi = 0; let radious = 5;

function resetCamera(offsetX, offsetY) { let vx = new THREE.Vector3 vx.crossVectors(cameraFrom, cameraUp).normalize()

let vy = new THREE.Vector3
vy.crossVectors(vx, cameraFrom).normalize()
vy.multiplyScalar(offsetY)
vx.multiplyScalar(offsetX)
cameraFrom.add(vx)
cameraFrom.add(vy)



cameraUp.crossVectors(new THREE.Vector3(0, 0, 1), cameraFrom)
cameraUp.crossVectors(cameraFrom, cameraUp)

cameraUp.normalize()

cameraFrom.normalize()
cameraFrom.multiplyScalar(cameraDis)
camera.position.set(cameraFrom.x, cameraFrom.y, cameraFrom.z)
camera.up = cameraUp
camera.lookAt(cameraTo);

}

function onMouseMove(event) {

event.preventDefault();

if (isMouseDown) {

    let offsetX = (event.clientX - onMouseDownPosition.x) / 10.0;
    let offsetY = (event.clientY - onMouseDownPosition.y) / 10.0;

    resetCamera(offsetX, offsetY)

    onMouseDownPosition.x = event.clientX
    onMouseDownPosition.y = event.clientY
}

}

function onTouchMove(event) { event.preventDefault();

if (isMouseDown) {

    let offsetX = (event.touches[0].clientX - onMouseDownPosition.x) / 10.0;
    let offsetY = (event.touches[0].clientY - onMouseDownPosition.y) / 10.0;

    resetCamera(offsetX, offsetY)

    onMouseDownPosition.x = event.touches[0].clientX
    onMouseDownPosition.y = event.touches[0].clientY
}

}

canvas.onmousemove = onMouseMove; canvas.onmouseup = onMouseUp; canvas.onmousedown = onMouseDown; canvas.ontouchstart = onTouchStart; canvas.ontouchmove = onTouchMove; canvas.ontouchend = onMouseUp;

var UIComponents = function () { this.offsetX = 0.0; this.offsetY = 0.0; this.displayOutline = false; };

var uimodel = new UIComponents(); let controllerX = ui.add(uimodel, ‘offsetX’, -1, 1); let controllerY = ui.add(uimodel, ‘offsetY’, -1, 1); ui.add(uimodel, ‘displayOutline’);

globals.set(‘aa’, 2)

var blah = ‘test’ controllerX.onChange(function(value){ updateGeometry(mesh.geometry, frame.geometry, uimodel.offsetX,uimodel.offsetY,true) });

controllerY.onChange(function(value){ updateGeometry(mesh.geometry, frame.geometry, uimodel.offsetX,uimodel.offsetY,true) });

render()

Or you could simply write a program to generate text outputs, for example, a bubble sort. Epiphany will be language agnostic. Currently, Javascript, Python (Thanks to pyodide!) and ClosureScript are supported. More languages will come in the future.

function printArray(a) { let result = ‘ ’ for (var i = 0; i < a.length; ++i) { result += a[i] + ‘ ’ } log(‘[’ + result + ‘]’) } function bubbleSort(a) { for (var i = 0; i < a.length; ++i) { for (var e = 0; e < a.length - i - 1; ++e) { if (a[e + 1] < a[e]) { let t = a[e + 1] a[e + 1] = a[e] a[e] = t } } } } let run = function () { clr(); log(“Bubble Sort:”) let array = [] for (var i = 0; i < 10; ++i) { array.push(Math.floor(Math.random() * Math.floor(10))) } printArray(array) log(‘Result:’) bubbleSort(array); printArray(array) } let ui = createUi() var ControlUI = function () { this.run = function () { run(); }; }; var text = new ControlUI(); ui.add(text, ‘run’); run();

You can also load existing Javascript packages, such as chart.js, and create  interactive visualizations:

await load(‘https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js’)

function getRandomInt(max) { return Math.floor(Math.random() * Math.floor(max)); }

chartColors = { red: ‘rgb(255, 99, 132)’, orange: ‘rgb(255, 159, 64)’, yellow: ‘rgb(255, 205, 86)’, green: ‘rgb(75, 192, 192)’, blue: ‘rgb(54, 162, 235)’, purple: ‘rgb(153, 102, 255)’, grey: ‘rgb(201, 203, 207)’ };

var config = { type: ‘line’, data: { labels: [‘January’, ‘February’, ‘March’, ‘April’, ‘May’, ‘June’, ‘July’], datasets: [{ label: ‘dataset - big points’, data: [ getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10) ], backgroundColor: chartColors.red, borderColor: chartColors.red, fill: false, borderDash: [5, 5], pointRadius: 15, pointHoverRadius: 10, }, { label: ‘dataset - individual point sizes’, data: [ getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10) ], backgroundColor: chartColors.blue, borderColor: chartColors.blue, fill: false, borderDash: [5, 5], pointRadius: [2, 4, 6, 18, 0, 12, 20], }, { label: ‘dataset - large pointHoverRadius’, data: [ getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10) ], backgroundColor: chartColors.green, borderColor: chartColors.green, fill: false, pointHoverRadius: 30, }, { label: ‘dataset - large pointHitRadius’, data: [ getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10), getRandomInt(10) ], backgroundColor: chartColors.yellow, borderColor: chartColors.yellow, fill: false, pointHitRadius: 20, }] }, options: { responsive: false, maintainAspectRatio: false, legend: { display: true, position: ‘top’, }, hover: { mode: ‘index’ }, scales: { xAxes: [{ display: true, scaleLabel: { display: true, labelString: ‘Month’ } }], yAxes: [{ display: true, scaleLabel: { display: true, labelString: ‘Value’ } }] }, title: { display: true, text: ‘Chart.js Line Chart - Different point sizes’ } } };

Chart.defaults.global.animation.duration = 0

let canvas = getCanvas()

canvas.width = 640 canvas.height = 480 canvas.style = { width: ‘640px’, height: ‘480px’ }

let ctx = canvas.getContext(‘2d’);

let lines = new Chart(ctx, config);

canvas.onresize = function (width, height) { lines.update() }

If you are interested in more examples:

Wave function collapse algorithm, is an interesting way game developers use to generate semi-random game maps and texture maps.

Buffon’s needle, is a geometry probability problem. Ants use the same solution to estimate the size of a potential nest.

A toy video encoder, illustrates the secrete behinds magical video compression algorithms.

Inverse Kinematics Explained Visually is a notebook on how to maneuver a robotic arm to reach a target.

Anime4k is an interesting algorithm that can upscale animations to 4k in real-time.

We built Epiphany for a few reasons. First, scientific papers and textbooks are painful to read. Their use of equations and austere languages suppresses the intuition and the visuals behind ideas, depriving the joy of learning. Learning can be fun, only if the way of telling is right. Human nature prefers learning from visuals, examples and hands-on experiences, which Epiphany provides by embedded programs. Students will have a chance to step through and debug an idea, or substitute parameters to conduct any experiments right on the textbook to enhance understanding.

Second, new scientific studies require extensive computation, data and visualizations. However they are not presentable with printed materials. Ten years ago, we wouldn’t imagine computational biology and computational chemistry. But today, even psychology study and social science needs statistics and data. We therefore need to define a new medium to carry new knowledge.

Furthermore, traditional books are too slow to keep up with technology. As Bill Gates says, textbooks are obsolete and the future books should be software. In fact, most people learned deep learning and blockchain through blogs, due to the lack of books. New generation of writers, such as Andrew Ng, even chose to release their work on a chapter by chapter basis online to gather feedback from readers, just like software development. In the future, writing a textbook will be as easy as writing blogs. A book will not be authored by one or a few writers, but by a group of professionals organized on the internet. The writing of the book will never end, just like software. It will update frequently with new content and improvements based on user feedback.

Finally, students' spending on course materials/textbooks is still high at $500 per year, $64 per textbook/unit. And only less than 10% of that money go to authors. An online textbook platform can cut the inefficiency out of traditional publishing, reducing students' burden and rewarding creators fairly.

The idea of Epiphany came after a series of inspirations. Other than the two most obvious ones, Medium and Jupyter Notebook, popular interactive contents also helped spark this idea, such as distill.pub, the Explorable Multiverse Analyses, the immersive math, the textbook of the future and the animated biology book. These interactive contents really showcase the expressiveness of software. If text and equations are only the blueprints of machines. Having embedded runnable programs amounts to providing real machines for students to dissemble. It’s obvious that the later is a more efficient way of learning. What lacks though is a platform for these interactive contents and great tooling for their creation.

Therefore, we made Epiphany, the result of us pondering over what a future interactive content platform should look like. Epiphany supports the following features:

  1. Writing in realtime Markdown with math equation support.

  2. Allowing embedding programs. Javascript, Python and ClojureScript are currently supported, more to come.

  3. A language agnostic API for plotting, creating UI and interacting with an Epiphany post.

  4. Github like version control and collaboration.

  5. Socialized publishing.

On the technical side, Epiphany relies purely on web technologies. With the growing power of WebAssembly and WebGPU, web can carry out 90% what off-line computation can accomplish. Putting everything in the browser creates a more integrated experience and enables us to take the advantages of the hundreds and thousands of Javascript packages available already. Two of the recent related projects, observable and iodide also use web technologies and had influenced our choice.

We hope Epiphany could grow into a community for thinkers. The codex platform Da Vinci would use if he were a modern man.