Demo of Perspective.
A classic fractal implemented entirely in ExprTK/Perspective.
license: apache-2.0 | |
height: 800 |
Demo of Perspective.
A classic fractal implemented entirely in ExprTK/Perspective.
perspective-viewer { | |
flex: 1; | |
margin: 24px; | |
overflow: visible; | |
} | |
perspective-viewer[theme="Material Light"], | |
perspective-viewer[theme="Material Dark"] { | |
--d3fc-positive--gradient: linear-gradient( | |
#94d0ff, | |
#8795e8, | |
#966bff, | |
#ad8cff, | |
#c774e8, | |
#c774a9, | |
#ff6ad5, | |
#ff6a8b, | |
#ff8b8b, | |
#ffa58b, | |
#ffde8b, | |
#cdde8b, | |
#8bde8b, | |
#20de8b | |
); | |
} | |
#app { | |
display: flex; | |
flex-direction: column; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: #f2f4f6; | |
} | |
#controls { | |
display: flex; | |
margin: 24px 24px 0px 40px; | |
} | |
.range { | |
position: relative; | |
display: inline-flex; | |
flex-direction: column; | |
margin-right: 24px; | |
} | |
span, | |
input, | |
button { | |
font-family: "Open Sans"; | |
font-size: 12px; | |
background: none; | |
margin: 0px; | |
border-color: #ccc; | |
color: #666; | |
padding: 6px 12px 6px 0px; | |
} | |
input { | |
height: 14px; | |
border-width: 0px; | |
border-style: solid; | |
border-bottom-width: 1px; | |
color: inherit; | |
outline: none; | |
} | |
input[type="range"] { | |
margin-top: 2px; | |
} | |
input[type="number"] { | |
font-family: "Roboto Mono"; | |
} | |
input:focus { | |
border-color: #1a7da1; | |
} | |
input::placeholder { | |
color: #ccc; | |
} | |
button { | |
border: 1px solid #ccc; | |
text-transform: uppercase; | |
text-align: center; | |
text-decoration: none; | |
display: inline-block; | |
padding-left: 12px; | |
height: 28px; | |
outline: none; | |
} | |
button:hover { | |
cursor: pointer; | |
} | |
#run { | |
justify-self: center; | |
margin-right: 24px; | |
height: 83px; | |
width: 80px; | |
} | |
#run:disabled { | |
opacity: 0.2; | |
} |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" /> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective@latest"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer@latest"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-datagrid@latest"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-d3fc@latest"></script> | |
<link rel="stylesheet" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@finos/perspective-viewer/dist/css/themes.css" /> | |
<link rel='stylesheet' href="index.css"> | |
</head> | |
<body> | |
<div id="app"> | |
<div id="controls"> | |
<button id="run" disabled>Run</button> | |
<div class="range"> | |
<span>Size</span> | |
<input id="width" min="25" max="700" type="number" placeholder="Width" value="200"></input> | |
<input id="height" min="25" max="500" type="number" placeholder="Height" value="200"></input> | |
</div> | |
<div class="range"> | |
<span id="xrange">X [-0.4 , -0.3]</span> | |
<input id="xmin" min="-2" max="1.0" step="0.1" value="-0.4" type="range"></input> | |
<input id="xmax" min="-2" max="1.0" step="0.1" value="-0.3" type="range"></input> | |
</div> | |
<div class="range"> | |
<span id="yrange">Y [-0.7 , -0.6]</span> | |
<input id="ymin" min="-1" max="1.0" step="0.1" value="-0.7" type="range"></input> | |
<input id="ymax" min="-1" max="1.0" step="0.1" value="-0.6" type="range"></input> | |
</div> | |
<div class="range"> | |
<span>Iterations</span> | |
<input id="iterations" min="1" max="1000" type="number" placeholder="Iterations" value="100"></input> | |
</div> | |
</div> | |
<perspective-viewer id="viewer"></perspective-viewer> | |
</div> | |
<script src="index.js"></script> | |
</body> | |
</html> |
function generate_mandelbrot(params) { | |
return ` | |
// color | |
var height := ${params.height}; | |
var width := ${params.width}; | |
var xmin := ${params.xmin}; | |
var xmax := ${params.xmax}; | |
var ymin := ${params.ymin}; | |
var ymax := ${params.ymax}; | |
var iterations := ${params.iterations}; | |
var x := floor("index" / height); | |
var y := "index" % height; | |
var c := iterations; | |
var cx := xmin + ((xmax - xmin) * x) / (width - 1); | |
var cy := ymin + ((ymax - ymin) * y) / (height - 1); | |
var vx := 0; | |
var vy := 0; | |
var vxx := 0; | |
var vyy := 0; | |
var vxy := 0; | |
for (var ii := 0; ii < iterations; ii += 1) { | |
if (vxx + vyy <= float(4)) { | |
vxy := vx * vy; | |
vxx := vx * vx; | |
vyy := vy * vy; | |
vx := vxx - vyy + cx; | |
vy := vxy + vxy + cy; | |
c -= 1; | |
} | |
}; | |
c`; | |
} | |
function generate_layout(params) { | |
return { | |
plugin: "Heatmap", | |
settings: true, | |
group_by: [`floor("index" / ${params.height})`], | |
split_by: [`"index" % ${params.height}`], | |
columns: ["color"], | |
expressions: [ | |
generate_mandelbrot(params).trim(), | |
`floor("index" / ${params.height})`, | |
`"index" % ${params.height}`, | |
], | |
}; | |
} | |
async function generate_data(table) { | |
const run = document.getElementById("run"); | |
let json = new Array(width * height); | |
for (let x = 0; x < width; ++x) { | |
for (let y = 0; y < height; ++y) { | |
const index = x * height + y; | |
json[index] = { | |
index, | |
}; | |
} | |
} | |
await table.replace(json); | |
run.innerHTML = `Run`; | |
} | |
// GUI | |
function get_gui_params() { | |
return [ | |
"xmin", | |
"xmax", | |
"ymin", | |
"ymax", | |
"width", | |
"height", | |
"iterations", | |
].reduce((acc, x) => { | |
acc[x] = window[x].valueAsNumber; | |
return acc; | |
}, {}); | |
} | |
function make_range(x, y, range, name) { | |
const title = () => | |
name + | |
" [" + | |
x.valueAsNumber.toFixed(1) + | |
", " + | |
y.valueAsNumber.toFixed(1) + | |
"]"; | |
x.addEventListener("input", () => { | |
window.run.disabled = false; | |
x.value = Math.min(x.valueAsNumber, y.valueAsNumber - 0.1); | |
range.innerHTML = title(); | |
}); | |
y.addEventListener("input", () => { | |
window.run.disabled = false; | |
y.value = Math.max(x.valueAsNumber + 0.1, y.valueAsNumber); | |
range.innerHTML = title(); | |
}); | |
} | |
const make_run_click_callback = (worker, state) => async () => { | |
if (window.run.innerHTML.trim() !== "Run") { | |
window.run.innerHTML = "Run"; | |
return; | |
} | |
window.run.disabled = true; | |
if (!state.table) { | |
state.table = await worker.table({ | |
index: "integer", | |
}); | |
window.viewer.load(Promise.resolve(state.table)); | |
} | |
const run = document.getElementById("run"); | |
const params = get_gui_params(); | |
const new_size = params.width * params.height; | |
if (!state.size || state.size !== new_size) { | |
let json = {index: new Array(new_size)}; | |
for (let x = 0; x < new_size; ++x) { | |
json.index[x] = x; | |
} | |
state.table.replace(json); | |
} | |
state.size = new_size; | |
run.innerHTML = `Run`; | |
window.viewer.restore(generate_layout(params)); | |
}; | |
function set_runnable() { | |
window.run.disabled = false; | |
} | |
window.addEventListener("DOMContentLoaded", async function () { | |
const heatmap_plugin = await window.viewer.getPlugin("Heatmap"); | |
heatmap_plugin.max_cells = 100000; | |
make_range(xmin, xmax, xrange, "X"); | |
make_range(ymin, ymax, yrange, "Y"); | |
window.width.addEventListener("input", set_runnable); | |
window.height.addEventListener("input", set_runnable); | |
window.iterations.addEventListener("input", set_runnable); | |
run.addEventListener( | |
"click", | |
make_run_click_callback(window.perspective.worker(), {}) | |
); | |
run.dispatchEvent(new Event("click")); | |
}); |