Introduction

Epiphany

Welcome to Epiphany, a new blogging experience allowing interactive content, version control and social publication. With Epiphany, you can write equations:

\[|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 using three.js:

\[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() //var context = canvas.getContext( ‘webgl2’ ); canvas.style = { width: 0, height: 0 }

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 write a program to generate text outputs, for example, a bubble sort. You may choose to hide the source code or display it along with the execution result. Currently, both Javascript and Python (Thanks to pyodide!) 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 an interactive visualization:

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() }

Epiphany is our response to the recent trend in content consuming, where people spend less time on traditional media, such as books, while spending more time consuming information on-line. This trend can attribute to a few unique benefits of the on-line contents. For example, on-line contents are often bite sized tailored for people’s increasingly fragmented time. On-line contents are more timely also. After all, before you could buy any book about blockchain, on-line contents were the only learning sources. In recent years, we also witnessed the trend of interactive contents in the form of embedded program that is not available with traditional media.

Many have noticed the same trend. Bill Gates in his 2019 annual letter says textbooks are becoming obsolete and the future books should be personalized with interactive contents to engage the readers. Similarly, this Atlantic article also asks the question: What would you get if you designed the scientific paper from scratch today? And we can’t agree more with the following:

It was a shame that in mathematics it’s been a tradition for hundreds of years to make papers as formal and austere as possible, often suppressing the very visual aids that mathematicians use to make their discoveries.

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 and the textbook of the future. These interactive contents really showcase the expressiveness of programs. A Program can strengthen understanding by exposing the internals of an idea, providing a rigorous definition of a procedure and dismissing any ambiguity left in the text. A program also gives readers a chance to approve or disapprove any hypothesis they might have during their study by allowing adjustments. After all, a reader can debug, step through and substitute inputs if a live program is available. If text and equations are only the blueprints of a machine. Having a runnable program amounts to providing a real machine for dissembling.

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 Markdown with math equation support.
  2. Allowing embedding programs. Javascript and Python are currently supported, more to come.
  3. A language agnostic API for plotting, creating an UI and interacting with an Epiphany post.
  4. Github like version control and collaboration.
  5. Socialized publication.

On the technical side, Epiphany relies purely on web technologies. With the growing power of WebAssembly, web can carry out 90% what off-line compute 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 similar 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.