diff --git a/default.profraw b/default.profraw
deleted file mode 100644
index 11a6b7d4ac..0000000000
Binary files a/default.profraw and /dev/null differ
diff --git a/examples/blocks/examples.js b/examples/blocks/examples.js
index cba1710237..474ace0d90 100644
--- a/examples/blocks/examples.js
+++ b/examples/blocks/examples.js
@@ -21,6 +21,7 @@ const LOCAL_EXAMPLES = [
"magic",
"streaming",
"covid",
+ "webcam",
"movies",
"superstore",
"citibike",
diff --git a/examples/blocks/src/raycasting/index.js b/examples/blocks/src/raycasting/index.js
index 8702afb1a2..b5039bcdd5 100644
--- a/examples/blocks/src/raycasting/index.js
+++ b/examples/blocks/src/raycasting/index.js
@@ -98,16 +98,13 @@ for (var j := 1; j <= radialSegments; j += 1) {
if (t >= 0) {
var t2 := 1 - u - v;
var d1[3] := v0 * t2 + v1 * u + v2 * v;
- var d2[3] := d1 - camera;
- var dist := norm3(d2);
+ var dist := norm3(d1 - camera);
if (dist < depth) {
depth := dist;
// Lighting
- var ww[3] := v0 - v1;
- var zz[3] := v2 - v1;
var n[3];
- cross_product3(ww, zz, n);
+ cross_product3(v0 - v1, v2 - v1, n);
color := acos(dot_product3(light, n) / (light_norm * norm3(n)))
}
}
diff --git a/examples/blocks/src/webcam/README.md b/examples/blocks/src/webcam/README.md
new file mode 100644
index 0000000000..bb9a184579
--- /dev/null
+++ b/examples/blocks/src/webcam/README.md
@@ -0,0 +1,2 @@
+
+A Perspective example which uses your computer's webcam as a data source.
\ No newline at end of file
diff --git a/examples/blocks/src/webcam/index.css b/examples/blocks/src/webcam/index.css
new file mode 100644
index 0000000000..323fa06b08
--- /dev/null
+++ b/examples/blocks/src/webcam/index.css
@@ -0,0 +1,89 @@
+/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+ * ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+ * ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+ * ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+ * ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+ * ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+ * ┃ Copyright (c) 2017, the Perspective Authors. ┃
+ * ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
+ * ┃ This file is part of the Perspective library, distributed under the terms ┃
+ * ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
+ * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ */
+
+body {
+ background: #242526;
+ color: white;
+ font-family: "Roboto Mono";
+ touch-action: none;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+#header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+#header a {
+ display: inline-flex;
+}
+
+perspective-viewer {
+ border-top: 1px solid #666;
+ flex: 1 1 auto;
+}
+
+label {
+ height: 32px;
+ font-size: 12px;
+ padding: 6px 0px;
+ margin-right: 4px;
+ margin-left: 14px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ border: 1px solid transparent;
+}
+
+img {
+ vertical-align: middle;
+ margin-left: 14px;
+}
+
+select, button {
+ font-family: "Roboto Mono";
+ font-size: 12px;
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid #666;
+ border-radius: 2px;
+ padding: 6px 10px;
+ color: #f4f5f6;
+ cursor: pointer;
+ margin-right: 4px;
+ margin-left: 4px;
+ outline: none;
+ user-select: none;
+ height: 32px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+}
+
+select:hover, button:hover {
+ color: #242526;
+ background-color: #f4f5f6;
+ border-color: #f4f5f6;
+}
\ No newline at end of file
diff --git a/examples/blocks/src/webcam/index.html b/examples/blocks/src/webcam/index.html
new file mode 100644
index 0000000000..0a86904b1b
--- /dev/null
+++ b/examples/blocks/src/webcam/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/blocks/src/webcam/layouts.json b/examples/blocks/src/webcam/layouts.json
new file mode 100644
index 0000000000..e1228f33ed
--- /dev/null
+++ b/examples/blocks/src/webcam/layouts.json
@@ -0,0 +1,153 @@
+[
+ {
+ "plugin": "Heatmap",
+ "title": "Heatmap Cam",
+ "group_by": ["x"],
+ "split_by": ["y"],
+ "columns": ["color"],
+ "expressions": {
+ "y": "-floor(\"index\" / 80)",
+ "x": "-\"index\" % 80"
+ },
+ "aggregates": {
+ "New Column 1": "any"
+ }
+ },
+ {
+ "plugin": "Heatmap",
+ "plugin_config": {},
+ "settings": true,
+ "theme": "Pro Light",
+ "title": "Downsampled Heatmap Cam",
+ "group_by": ["x"],
+ "split_by": ["y"],
+ "columns": ["color"],
+ "filter": [],
+ "sort": [],
+ "expressions": {
+ "y": "bucket(-floor(\"index\" / 80), 3)",
+ "x": "bucket(-\"index\" % 80, 3)"
+ },
+ "aggregates": {}
+ },
+ {
+ "plugin": "Datagrid",
+ "plugin_config": {
+ "columns": {
+ "color": {
+ "bg_gradient": 251.04,
+ "neg_bg_color": "#ffa38f",
+ "number_bg_mode": "gradient",
+ "number_fg_mode": "disabled",
+ "pos_bg_color": "#346ead"
+ }
+ },
+ "editable": false,
+ "scroll_lock": false
+ },
+ "title": "Spreadsheet Cam",
+ "group_by": ["y"],
+ "split_by": ["x"],
+ "columns": ["color"],
+ "filter": [],
+ "sort": [],
+ "expressions": {
+ "New Column 1": "bucket(\"color\", 5)",
+ "y": "floor(\"index\" / 80)",
+ "x": "-\"index\" % 80"
+ },
+ "aggregates": {}
+ },
+ {
+ "plugin": "Y Bar",
+ "plugin_config": {},
+ "title": "Luminosity Histogram",
+ "group_by": ["bucket(\"color\", 5)"],
+ "split_by": [],
+ "columns": ["color"],
+ "filter": [],
+ "sort": [],
+ "expressions": {
+ "bucket(\"color\", 5)": "bucket(\"color\", 5)",
+ "y": "-floor(\"index\" / 80)",
+ "x": "-\"index\" % 80"
+ },
+ "aggregates": {}
+ },
+ {
+ "plugin": "Datagrid",
+ "plugin_config": {
+ "columns": {
+ "color": {
+ "bg_gradient": 2463.68,
+ "neg_bg_color": "#ffa38f",
+ "number_bg_mode": "gradient",
+ "number_fg_mode": "disabled",
+ "pos_bg_color": "#307bb0"
+ }
+ },
+ "editable": false,
+ "scroll_lock": false
+ },
+ "title": "Small Spreadsheet Cam",
+ "group_by": ["bucket(y, 5)"],
+ "split_by": ["bucket(x, 5)"],
+ "columns": ["color"],
+ "filter": [["bucket(x, 5)", "<", 0.0]],
+ "sort": [],
+ "expressions": {
+ "bucket(y, 5)": "bucket(floor(\"index\" / 80), 2)",
+ "New Column 1": "bucket(\"color\", 5)",
+ "bucket(x, 5)": "bucket(-\"index\" % 80, 5)"
+ },
+ "aggregates": {}
+ },
+ {
+ "plugin": "Y Line",
+ "plugin_config": {},
+ "title": "Max Headroom",
+ "group_by": ["x"],
+ "split_by": ["y"],
+ "columns": ["New Column 2"],
+ "filter": [["x", "<", 0.0]],
+ "sort": [],
+ "expressions": {
+ "x": "-\"index\" % 80",
+ "y": "floor(\"index\" / 80)",
+ "New Column 2": "-floor(\"index\" / 80) * 20 - \"color\""
+ },
+ "aggregates": { "New Column 2": "avg" }
+ },
+ {
+ "plugin": "X/Y Scatter",
+ "plugin_config": {},
+ "title": "Scatter Cam",
+ "group_by": ["x", "y"],
+ "split_by": [],
+ "columns": ["x", "New Column 2", "color", null, null, null, null],
+ "filter": [["x", "<", 0.0]],
+ "sort": [],
+ "expressions": {
+ "New Column 2": "-floor(\"index\" / 80) * 50 - \"color\"",
+ "x": "-\"index\" % 80",
+ "y": "floor(\"index\" / 80)"
+ },
+ "aggregates": { "x": "avg", "New Column 2": "avg" }
+ },
+ {
+ "plugin": "Datagrid",
+ "plugin_config": {
+ "columns": {},
+ "editable": false,
+ "scroll_lock": false
+ },
+ "title": "Raw Stream",
+ "group_by": [],
+ "split_by": [],
+ "columns": ["index", "color"],
+ "filter": [],
+ "sort": [],
+ "expressions": {},
+ "aggregates": {}
+ }
+]
diff --git a/examples/blocks/src/webcam/preview.png b/examples/blocks/src/webcam/preview.png
new file mode 100644
index 0000000000..d4f9dbb0ce
Binary files /dev/null and b/examples/blocks/src/webcam/preview.png differ
diff --git a/examples/blocks/src/webcam/thumbnail.png b/examples/blocks/src/webcam/thumbnail.png
new file mode 100644
index 0000000000..6c06e1fa0b
Binary files /dev/null and b/examples/blocks/src/webcam/thumbnail.png differ
diff --git a/examples/blocks/src/webcam/webcam.js b/examples/blocks/src/webcam/webcam.js
new file mode 100644
index 0000000000..5c57cee2bd
--- /dev/null
+++ b/examples/blocks/src/webcam/webcam.js
@@ -0,0 +1,83 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ Copyright (c) 2017, the Perspective Authors. ┃
+// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
+// ┃ This file is part of the Perspective library, distributed under the terms ┃
+// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
+// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+import perspective from "/node_modules/@finos/perspective/dist/cdn/perspective.js";
+
+const canvas = document.getElementById("canvas");
+const context = canvas.getContext("2d", { willReadFrequently: true });
+const video = document.getElementById("video");
+const WIDTH = canvas.width;
+const HEIGHT = canvas.height;
+const WORKER = perspective.shared_worker();
+
+async function poll(table, tdata) {
+ context.drawImage(video, 0, 0, WIDTH, HEIGHT);
+ const data = context.getImageData(0, 0, WIDTH, HEIGHT);
+ for (let i = 0; i < data.data.byteLength / 4; i++) {
+ const r = data.data[i * 4];
+ const g = data.data[i * 4 + 1];
+ const b = data.data[i * 4 + 2];
+ const color = 255 - (0.21 * r + 0.72 * g + 0.07 * b);
+ tdata.color[i] = color;
+ }
+
+ await table.update(tdata);
+ setTimeout(() => poll(table, tdata), 50);
+}
+
+async function init_tables() {
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ });
+
+ video.srcObject = stream;
+ video.play();
+ }
+
+ const tdata = { index: [], color: [] };
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
+ tdata.index[i] = i;
+ tdata.color[i] = 0;
+ }
+
+ const table = await WORKER.table(tdata, { index: "index" });
+ poll(table, tdata);
+ return table;
+}
+
+async function init_layouts() {
+ const req = await fetch("layouts.json");
+ return await req.json();
+}
+
+const INIT_TASK = [init_tables(), init_layouts()];
+
+window.addEventListener("DOMContentLoaded", async function () {
+ const [table, layouts] = await Promise.all(INIT_TASK);
+ const settings = !/(iPad|iPhone|iPod)/g.test(navigator.userAgent);
+ const select = document.querySelector("select");
+ const viewer = document.querySelector("perspective-viewer");
+ viewer.load(table);
+ viewer.restore({ settings, ...layouts[0] });
+ for (const layout of layouts) {
+ const option = document.createElement("option");
+ option.value = layout.title;
+ option.textContent = layout.title;
+ select.appendChild(option);
+ }
+
+ select.addEventListener("change", async (event) => {
+ const layout = layouts.find((x) => x.title === event.target.value);
+ await viewer.restore(layout);
+ });
+});