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")); | |
| }); |