Published
Edited
Jun 6, 2021
Importers
5 stars
Insert cell
Insert cell
md`## Rendering Pipeline`
Insert cell
Insert cell
perspectiveController = html`${viewof perspectiveParameters}`
Insert cell
md`Static Renderer`
Insert cell
svg`<svg width="${width}px" height="512">${anaglyphRenderer(scenePostPerspective)}</svg>`
Insert cell
worldController = html`${viewof worldParameters}`
Insert cell
viewof rightColor = {
// near, zRange, eyeGap, scale
const controller = view`<table>
<tr><td>Red: ${["red", Inputs.range([0, 255], { value: 0, step: 1 })]}</td></tr>
<tr><td>Green: ${["green", Inputs.range([0, 255], { value: 255, step: 1 })]}</td></tr>
<tr><td>Blue: ${["blue", Inputs.range([0, 255], { value: 235, step: 1 })]}</td></tr>
</table>`
return controller
}
Insert cell
scene = {
let scene = []
scene = scene.concat(axis(3))
scene = scene.concat(pyramidGrid)
return scene
}
Insert cell
processSceneGivenParameters = (s, params) => {
const wt = worldTransformsGivenParams(params)
return s.map(wt)
}
Insert cell
scenePostWorld = processSceneGivenParameters(scene, worldParameters)
Insert cell
projectSceneGivenParameters = (s, params) => {
const processedParams = {
...params,
far: params.near + params.zRange
}
const pt = anaglyphPerspectiveTransformsGivenParams(processedParams)
return s.map(pt)
}
Insert cell
scenePostPerspective = projectSceneGivenParameters(scenePostWorld, perspectiveParameters)
Insert cell
Insert cell
md`### Part 1: World Transforms`
Insert cell
worldTransformsGivenParams = ({ rX, rY, rZ, mx, my, mz, cx = 0, cy = 0, cz = 0 }) => {
const rotationX = [
[ 1, 0, 0, 0],
[ 0, Math.cos(rX), -Math.sin(rX), 0],
[ 0, Math.sin(rX), Math.cos(rX), 0],
[ 0, 0, 0, 1]
]
const rotationY = [
[ Math.cos(rY), 0, Math.sin(rY), 0 ],
[ 0, 1, 0, 0 ],
[ -Math.sin(rY), 0, Math.cos(rY), 0 ],
[ 0, 0, 0, 1 ]
]
const rotationZ = [
[ Math.cos(rZ), -Math.sin(rZ), 0, 0 ],
[ Math.sin(rZ), Math.cos(rZ), 0, 0 ],
[ 0, 0, 1, 0 ],
[ 0, 0, 0, 1 ]
]
const cameraTransform =
[
[ 1, 0, 0, 0 ],
[ 0, 1, 0, 0 ],
[ 0, 0, 1, 0 ],
[ -cx, -cy, -cz, 1 ]
];
return (entity) => {
const coordinatesList = entity.map((coordinate) => {
return mathjs.multiply(
mathjs.add(coordinate, [mx, my, mz, 0]),
rotationZ,
rotationY,
rotationX,
cameraTransform
)
})
Object.keys(entity).forEach((key) => {
if (!Number.isInteger(parseInt(key))) {
coordinatesList[key] = entity[key]
}
})
return coordinatesList
}
}
Insert cell
md`### Part 2: Perspective Transform`
Insert cell
perspectiveTransformGivenParams = ({ side, eyeGap, near, far, scale, horizontalShift, verticalShift }) => {
// https://jsantell.com/3d-projection/
const sign = (side) ? ((side === 'left') ? 1 : -1) : 0
const hahaha = eyeGap * 0.01
const r = 1 + hahaha * sign + horizontalShift
const l = -1 + hahaha * sign + horizontalShift
const t = 1 + verticalShift
const b = -1 + verticalShift
const perspectiveMatrix = [
[2 * near / (r - l), 0, 0, 0],
[0, 2 * near / (t - b), 0, 0],
[(r + l) / (r - l), (t + b) / (t - b), -(far + near)/(far - near), -2 * far * near / (far - near)],
[0, 0, -1, 0]
]
return (coordinate) => {
const projected = mathjs.multiply(coordinate, perspectiveMatrix)

const divided = [
projected[0] / projected[3] * scale,
projected[1] / projected[3] * scale,
projected[2]
]

return divided
}
}
Insert cell
anaglyphPerspectiveTransformsGivenParams = ({
near,
far,
cameraOffset,
eyeGap,
scale,
xprime,
yprime,
horizontalShift,
verticalShift
}) => {
return (entity) => {
const left = []
const right = []
const shift = eyeGap / 2
const params = {
near, far, eyeGap, scale, horizontalShift, verticalShift
}
const leftPerspectiveTransform = perspectiveTransformGivenParams({ ...params, side: 'left' })
const rightPerspectiveTransform = perspectiveTransformGivenParams({ ...params, side: 'right' })

entity.forEach((coordinate) => {
// 1. World Space Transforms
let outputLeft = coordinate
let outputRight = coordinate
// 2. Position the cameras
outputLeft = mathjs.add(outputLeft, [0, 0, -cameraOffset, 0])
outputRight = mathjs.add(outputRight, [0, 0, -cameraOffset, 0])
outputLeft = mathjs.add(outputLeft, [-shift, 0, 0, 0])
outputRight = mathjs.add(outputRight, [shift, 0, 0, 0])

// 3. Apply the Perspective Transform
outputLeft = leftPerspectiveTransform(outputLeft)
outputRight = rightPerspectiveTransform(outputRight)
// 4. Pan the outputs
outputLeft = mathjs.add(outputLeft, [xprime, yprime, 0])
outputRight = mathjs.add(outputRight, [xprime, yprime, 0])
left.push(outputLeft)
right.push(outputRight)
})
// Output. left and right are an array of { x, y } points on the projection plane
return {
...entity,
left,
right
}
}
}
Insert cell
md`### Part 3: Renderer`
Insert cell
behindProjectionPlane = (e) => {
return e.left.map(d => d[2]).find((d => d <= 0)) === undefined &&
e.right.map(d => d[2]).find((d => d <= 0)) === undefined
}
Insert cell
colors = Object.assign({
left: "red",
right: `rgb(${rightColor.red},${rightColor.green},${rightColor.blue})`
})
Insert cell
anaglyphRenderer = (listOfStuff) => {
const leftLayer = []
const rightLayer = []
const svgStuff = listOfStuff
.filter(behindProjectionPlane)
.forEach((item) => {
if (item.left.length > 1) {
const leftPath = "M " + item.left.map(p => `${p[0]} ${p[1]}`).join(' L ')
const rightPath = "M " + item.right.map(p => `${p[0]} ${p[1]}`).join(' L ')
leftLayer.push(svg`<path d="${leftPath}" fill="none" stroke="${colors.left}" stroke-width="1" style="mix-blend-mode: multiply;" />`)
rightLayer.push(svg`<path d="${rightPath}" fill="none" stroke="${colors.right}" stroke-width="1" style="mix-blend-mode: multiply;" />`)
} else {
if (item.text) {
leftLayer.push(svg`<g transform="translate(${item.left[0][0]},${item.left[0][1]})">${letterPlotter(item.text, { color: colors.left })}</g>`)
rightLayer.push(svg`<g transform="translate(${item.right[0][0]},${item.right[0][1]})">${letterPlotter(item.text, { color: colors.right })}</g>`)
} else {
leftLayer.push(svg`<circle cx="${item.left[0][0]}" cy="${item.left[0][1]}" r="1" fill="${colors.left}" />`)
rightLayer.push(svg`<circle cx="${item.right[0][0]}" cy="${item.right[0][1]}" r="1" fill="${colors.right}" />`)
}
}
})
return svg`<g>
<g transform="translate(${width / 2},${height / 2})">
${leftLayer}
</g>
<g transform="translate(${width / 2},${height / 2})">
${rightLayer}
</g>
</g>`
}
Insert cell
Insert cell
md`## Parameters`
Insert cell
perspectiveParameters
Insert cell
viewof perspectiveParameters = {
// near, zRange, eyeGap, scale
const controller = view`<table>
<tr><td>Camera Offset: ${["cameraOffset", Inputs.range([10, 1000], { value: 40, step: 10 })]}</td></tr>
<tr><td>Near (i.e. distance in front of the carema where we start projecting, aka. the projection plane): ${["near", Inputs.range([10, 40], { value: 20, step: 1 })]}</td></tr>
<tr><td>zRange (Far - Near) - i.e. how far past the projection plane to include: ${["zRange", Inputs.range([1, 40], { value: 20, step: 1 })]}</td></tr>
<tr><td>Eye Gap: ${["eyeGap", Inputs.range([0, 4], { value: 0.8, step: 0.01 })]}</td></tr>
<tr><td>Scale: ${["scale", Inputs.range([1, 20001], { value: 3800, step: 10 })]}</td></tr>
<tr><td>Horizontal Shift Window: ${["horizontalShift", Inputs.range([-10, 10], { value: 0, step: 0.1 })]}</td></tr>
<tr><td>Vertical Shift Window: ${["verticalShift", Inputs.range([-10, 10], { value: 0, step: 0.1 })]}</td></tr>
<tr><td>Shift in X': ${["xprime", Inputs.range([-500, 500], { value: 0, step: 1 })]}</td></tr>
<tr><td>Shift in Y': ${["yprime", Inputs.range([-500, 500], { value: 0, step: 1 })]}</td></tr>
</table>`
return controller
}
Insert cell
viewof worldParameters = {
// rX, rY, rZ
const controller = view`<table>
<tr>
<td>Rotation in X:
${["rX", Inputs.range([-Math.PI / 2, Math.PI / 2], { value: -Math.PI / 4, step: Math.PI / 32 })]}
</td>
</tr>
<tr>
<td>Rotation in Y:
${["rY", Inputs.range([-Math.PI / 2, Math.PI / 2], { value: 0, step: Math.PI / 32 })]}
</td>
</tr>
<tr>
<td>Rotation in Z:
${["rZ", Inputs.range([-Math.PI, Math.PI], { value: Math.PI / 8, step: Math.PI / 32 })]}
</td>
</tr>
<tr><td>Shift in X: ${["mx", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Shift in Y: ${["my", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Shift in Z: ${["mz", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Move Camera in X: ${["cx", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Move Camera in Y: ${["cy", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Move Camera in Z: ${["cz", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
</table>`
return controller
}
Insert cell
md`## Objects`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
md`## Exports`
Insert cell
intergratedRenderer(scene, {
world: { rX: -1, rY: 0, rZ: -0.85, mx: 0, my: 0, mz: 0 }
},
{
update: (layerX, layerY, worldParams, perspectiveParams) => {
const newRX = -Math.PI / 2 + layerY / height * Math.PI / 2

return {
worldParameters: {
...worldParams,
rX: newRX
},
perspectiveParameters: {
...perspectiveParams
}
}
}
})
Insert cell
{
const thing = {
a: 'hahaahah'
}
return thing?.a || 'nah'
}
Insert cell
intergratedRenderer = (rawScene, params, interactions) => {
const world = params?.world || worldParameters
const perspective = params?.perspective || perspectiveParameters
const sceneInWorld = processSceneGivenParameters(rawScene, world)
const sceneProjected = projectSceneGivenParameters(sceneInWorld, perspective)
const el = svg`<svg width="${width}px" height="512">${anaglyphRenderer(sceneProjected)}</svg>`
let isMouseDown = false
const defaultUpdateFn = (layerX, layerY, worldParams, perspectiveParams) => {
const newRZ = -Math.PI + layerX / width * 2 * Math.PI
const newRX = -Math.PI / 2 + layerY / height * Math.PI / 2

return {
worldParameters: {
...worldParams,
rZ: newRZ,
rX: newRX
},
perspectiveParameters: {
...perspectiveParams
}
}
}
const updateFn = interactions?.update || defaultUpdateFn
const interactionStart = interactions?.start || (event => {
event.preventDefault();
isMouseDown = true
})
const interactionMove = interactions?.move || (event => {
if (isMouseDown === false) return;
event.preventDefault();

const update = updateFn(event.layerX, event.layerY, world, perspective)
const newWorld = processSceneGivenParameters(rawScene, update.worldParameters)
const newProjectedScene = projectSceneGivenParameters(newWorld, update.perspectiveParameters)

el.querySelectorAll('*').forEach(n => n.remove());
el.append(anaglyphRenderer(newProjectedScene))
})
const interactionEnd = interactions?.end || (event => {
isMouseDown = false
})
el.addEventListener('touchstart', interactionStart)
el.addEventListener('pointerdown', interactionStart)
el.addEventListener('pointermove', interactionMove)
el.addEventListener('pointerup', interactionEnd)
return Object.assign(el, {
onmousedown: interactionStart,
onmousemove: interactionMove,
onmouseup: interactionEnd
})
}
Insert cell
Insert cell
import {letterPlotter} from '@tonyhschu/experiment-with-plottable-letters'
Insert cell
import {view} from '@tomlarkworthy/view'
Insert cell
mathjs = require("https://unpkg.com/mathjs@4.0.0/dist/math.min.js")
Insert cell
d3 = require('d3@6')
Insert cell
height = 512
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more