Skip to content

Instantly share code, notes, and snippets.

@texodus
Last active July 22, 2019 19:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save texodus/1ce655d6bc0cc0d9db852d562af3e487 to your computer and use it in GitHub Desktop.
Save texodus/1ce655d6bc0cc0d9db852d562af3e487 to your computer and use it in GitHub Desktop.
Perspective Mandelbrot Visualizer
perspective-viewer {
flex: 1;
margin: 24px;
overflow: visible;
--plugin--box-shadow: 0px 4px 3px rgba(0, 0, 0, 0.1);
--plugin--border: 1px solid #ddd;
--hypergrid-row-hover--background: rgba(0, 0, 0, 0.05);
--hypergrid-cell-hover--background: rgba(0, 0, 0, 0.1);
--notional--hypergrid-positive--background: #55a755;
--notional--hypergrid-positive--color: #ffffff;
--notional--hypergrid-negative--background: #c25a5a;
--notional--hypergrid-negative--color: #ffffff;
--d3fc-gradient-positive: linear-gradient(
#94D0FF,
#8795E8,
#966bff,
#AD8CFF,
#C774E8,
#c774a9,
#FF6AD5,
#ff6a8b,
#ff8b8b,
#ffa58b,
#ffde8b,
#cdde8b,
#8bde8b,
#20de8b
);
}
perspective-viewer.macsoft {
--d3fc-gradient-positive: linear-gradient(
#1b4247,
#09979b,
#75d8d5,
#ffc0cb,
#fe7f9d,
#65323e
);
}
#app {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #eee
}
#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;
}
<!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://unpkg.com/@finos/perspective-viewer"></script>
<script src="https://unpkg.com/@finos/perspective-viewer-hypergrid"></script>
<script src="https://unpkg.com/@finos/perspective-viewer-d3fc"></script>
<script src="https://unpkg.com/@finos/perspective"></script>
<link rel='stylesheet' href="https://unpkg.com/@finos/perspective-viewer/dist/umd/material.css" is="custom-style">
<link rel='stylesheet' href="index.css" is="custom-style">
</head>
<body>
<div id="app">
<div id="controls">
<button id="run">Run</button>
<div class="range">
<span>Size</span>
<input id="width" min="25" max="300" type="number" placeholder="Width" value="100"></input>
<input id="height" min="25" max="300" type="number" placeholder="Height" value="50"></input>
</div>
<div class="range">
<span id="xrange">X [-0.4 , -0.1]</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.1" 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="60"></input>
<input id="batch" min="1" max="1000" type="number" placeholder="Batch Size" value="1"></input>
</div>
<div class="range">
<span>Theme</span>
<button id="theme">Macsoft</button>
</div>
</div>
<perspective-viewer
id="viewer"
class="macsoft"
plugin="d3_heatmap"
row-pivots='["x"]'
column-pivots='["y"]'
columns='["c"]'>
</perspective-viewer>
</div>
<script src="index.js"></script>
</body>
</html>
// Mandelbrot calculation
function set_defaults(iterations, args = {}) {
args.vx = args.vx || 0.0;
args.vy = args.vy || 0.0;
args.vxx = args.vxx || 0;
args.vyy = args.vyy || 0;
args.vxy = args.vxy || 0;
args.c = args.c || iterations;
}
function iter_args(cx, cy, iterations, args = {}) {
if (args.vxx + args.vyy <= 4) {
args.vxy = args.vx * args.vy;
args.vxx = args.vx * args.vx;
args.vyy = args.vy * args.vy;
args.vx = args.vxx - args.vyy + cx;
args.vy = args.vxy + args.vxy + cy;
args.c--;
}
return args;
}
function mandel_step(json, ix, iy, xmin, xmax, ymin, ymax, width, height, iterations) {
const index = ix * height + iy;
const args = json[index];
const x = xmin + ((xmax - xmin) * ix) / (width - 1);
const y = ymin + ((ymax - ymin) * iy) / (height - 1);
set_defaults(iterations, args);
const new_args = iter_args(x, y, iterations, args);
new_args.x = ix;
new_args.y = iy;
json[index] = new_args;
}
function *mandel_iter(width, height, batch) {
for (let bi = 0; bi < batch; ++bi) {
for (let ix = 0; ix < width; ++ix) {
for (let iy = 0; iy < height; ++iy) {
yield [ix, iy];
}
}
}
}
async function mandelbrot(table, view, xmin, xmax, ymin, ymax, width, height, iterations, batch) {
const run = document.getElementById("run");
let json = [];
try {
for (let ii = 0; ii < iterations / batch; ++ii) {
for (const [ix, iy] of mandel_iter(width, height, batch)) {
mandel_step(json, ix, iy, xmin, xmax, ymin, ymax, width, height, iterations);
}
table.replace(json);
json = await view.to_json();
run.innerHTML = `Stop (${ii * batch}/${iterations})`;
}
} finally {
run.innerHTML = `Run`;
}
view.delete();
}
// GUI
const SCHEMA = {
x: "integer",
y: "integer",
vx: "float",
vy: "float",
vxx: "float",
vyy: "float",
vxy: "float",
c: "float"
};
function make_range(x, y, range, name) {
const title = () =>
name + " [" +
x.valueAsNumber.toFixed(1) + ", " +
y.valueAsNumber.toFixed(1) + "]";
x.addEventListener("input", () => {
x.value = Math.min(x.valueAsNumber, y.valueAsNumber - 0.1);
range.innerHTML = title();
});
y.addEventListener("input", () => {
y.value = Math.max(x.valueAsNumber + 0.1, y.valueAsNumber);
range.innerHTML = title();
});
}
const make_theme_click_callback = (theme, viewer) => () => {
if (theme.innerHTML === "Vaporwave") {
theme.innerHTML = "Macsoft";
viewer.classList.add("macsoft");
} else {
theme.innerHTML = "Vaporwave";
viewer.classList.remove("macsoft");
}
}
const make_run_click_callback = (worker, state) => async () => {
if (window.run.innerHTML.trim() !== "Run") {
state.view.delete();
window.run.innerHTML = "Run";
return;
}
window.run.innerHTML = `Stop (0/${window.iterations.valueAsNumber})`;
const old_table = state.table;
if (state.view) {
state.view.delete();
}
state.table = worker.table(SCHEMA);
state.view = state.table.view();
mandelbrot(state.table, state.view, ...[
"xmin",
"xmax",
"ymin",
"ymax",
"width",
"height",
"iterations",
"batch"
].map(x => window[x].valueAsNumber));
window.viewer.load(state.table);
if (old_table) {
old_table.delete();
}
}
// Main
window.addEventListener("WebComponentsReady", async function() {
make_range(xmin, xmax, xrange, "X");
make_range(ymin, ymax, yrange, "Y");
theme.addEventListener("click", make_theme_click_callback(theme, viewer));
run.addEventListener("click", make_run_click_callback(window.perspective.worker(), {}));
run.dispatchEvent(new Event("click"));
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment