-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Ricky Reusser
committed
Mar 5, 2020
1 parent
3305a57
commit ed6fd18
Showing
21 changed files
with
685 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<!DOCTYPE html> | ||
<html lang="en" dir="ltr"> | ||
<head> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"> | ||
<title>Idyll</title> | ||
<meta property="og:title" content="Idyll Project"> | ||
<meta charset="utf-8"> | ||
<meta property="og:type" content="article"> | ||
|
||
<link rel="stylesheet" href="../styles.css"> | ||
</head> | ||
<body> | ||
<div id="idyll-mount"><div data-reactroot=""><div class="idyll-root"><nav class="menu"><button class="menu__thumb"><span class="menu__hamburger"></span><span class="menu__hamburger"></span><span class="menu__hamburger"></span></button><div class="menu__content"><div class="menu__heading"><a href="/">rreusser.github.io</a></div><div class="menu__items"><a class="menu__item" href="/sketches/">Sketches</a><a class="menu__item" href="/writing/">Writing</a><a class="menu__item" href="https://github.com/rreusser">github.com/rreusser</a><a class="menu__item" href="https://twitter.com/rickyreusser">@rickyreusser</a></div></div></nav><div class="article-header"><div class="article-header__content"><h1 class="hed">ObservableHQ Test</h1><div class="byline"><a href="https://github.com/rreusser">Ricky Reusser</a></div><div class="published-at">March 4, 2020</div></div></div><div class=" idyll-text-container"><div></div><div></div><div></div></div></div></div></div> | ||
<script src="index.js"></script> | ||
</body> | ||
</html> |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
// https://observablehq.com/@rreusser/instanced-webgl-circles@213 | ||
export default function define(runtime, observer) { | ||
const main = runtime.module(); | ||
main.variable(observer()).define(["md"], function(md){return( | ||
md`# Instanced WebGL Circles | ||
A question came up of how to draw lots of circles efficiently. The most efficient method I know of uses WebGL with the [\`ANGLE_instanced_arrays\`](https://developer.mozilla.org/en-US/docs/Web/API/ANGLE_instanced_arrays) extension to place and scale many identical circle instances. | ||
It seemed like a fun exercise and a good way to incrementally iterate on my Observable skills, so here we are. I'm going to use the [regl](https://github.com/regl-project/regl) WebGL wrapper because it adds tons of convenience with very few of its own abstractions. Still, this method should generalize to WebGL and most other WebGL libraries without too much translation. | ||
We start by importing a simple context creation helper from [@rreusser/regl-tools](https://observablehq.com/@rreusser/regl-tools). Among other things, it exposes \`createReglCanvas\` with an API equivalent to that of [@observable/stdlib's \`DOM.context2d\` method](https://github.com/observablehq/stdlib/tree/6ef1ed21955cd3c849a40b10140ad120a3b9820f#DOM_context2d).` | ||
)}); | ||
main.variable(observer()).define(["md"], function(md){return( | ||
md`Next, we create and view a regl context. We configure a couple options while setting it up: | ||
- Disable antialiasing. Very expensive; not that helpful here. | ||
- Limit the pixel ratio to 1.5. More pixels make for a lot more fragments but doesn't actually look that much better. | ||
- Use the [\`ANGLE_instanced_arrays\`](https://developer.mozilla.org/en-US/docs/Web/API/ANGLE_instanced_arrays) extension to draw lots of circles with a single WebGL draw call.` | ||
)}); | ||
main.variable(observer("viewof regl")).define("viewof regl", ["reglCanvas","width"], function(reglCanvas,width){return( | ||
reglCanvas( | ||
width, | ||
Math.max(400, width * 0.6), // height | ||
Math.min(devicePixelRatio, 1.5), // pixel ratio | ||
{ | ||
extensions: ['ANGLE_instanced_arrays'], | ||
attributes: { antialias: false, depth: false} | ||
} | ||
) | ||
)}); | ||
main.variable(observer("regl")).define("regl", ["Generators", "viewof regl"], (G, _) => G.input(_)); | ||
main.variable(observer("viewof numCircleInstances")).define("viewof numCircleInstances", ["html"], function(html) | ||
{ | ||
const form = html`<form> | ||
<input name=i type=range min=20 max=4000 step=1 value=2000 style="width:180px;min-width:30%;"> | ||
<output style="font-size:smaller;font-style:oblique;" name=o></output> | ||
</form>`; | ||
form.i.oninput = () => form.o.value = `circle instance count = ${(form.value = form.i.valueAsNumber).toFixed(0)}`; | ||
form.i.oninput(); | ||
return form; | ||
} | ||
); | ||
main.variable(observer("numCircleInstances")).define("numCircleInstances", ["Generators", "viewof numCircleInstances"], (G, _) => G.input(_)); | ||
main.variable(observer("viewof numCircleDivisions")).define("viewof numCircleDivisions", ["html"], function(html) | ||
{ | ||
const form = html`<form> | ||
<input name=i type=range min=3 max=200 step=1 value=160 style="width:180px;min-width:30%;"> | ||
<output style="font-size:smaller;font-style:oblique;" name=o></output> | ||
</form>`; | ||
form.i.oninput = () => form.o.value = `circle divisions = ${(form.value = form.i.valueAsNumber).toFixed(0)}`; | ||
form.i.oninput(); | ||
return form; | ||
} | ||
); | ||
main.variable(observer("numCircleDivisions")).define("numCircleDivisions", ["Generators", "viewof numCircleDivisions"], (G, _) => G.input(_)); | ||
main.variable(observer()).define(["md","numCircleInstances","numCircleDivisions"], function(md,numCircleInstances,numCircleDivisions){return( | ||
md`Below is our main iteration loop. We simply clear the screen and execute a single draw command to draw ${numCircleInstances} circles each with ${numCircleDivisions} divisions, totaling ${numCircleInstances * (numCircleDivisions + 1)} vertices.` | ||
)}); | ||
main.variable(observer("loop")).define("loop", ["regl","draw"], function*(regl,draw) | ||
{ | ||
while (true) { | ||
regl.poll(); | ||
regl.clear({ color: [0.05, 0.05, 0.05, 1] }); | ||
draw(); | ||
yield; | ||
} | ||
} | ||
); | ||
main.variable(observer()).define(["md","tex"], function(md,tex){return( | ||
md`We now define what a single circle looks like. We can get away with a regular JavaScript \`Array\` of ${tex`(x, y)`} pairs. [\`regl\` is smart enough](https://github.com/regl-project/regl/blob/master/API.md#buffers) to do some basic flattening into a typed array so that we don't have to.` | ||
)}); | ||
main.variable(observer("circleInstanceGeometry")).define("circleInstanceGeometry", ["numCircleDivisions"], function(numCircleDivisions){return( | ||
Array.from(Array(numCircleDivisions + 1).keys()).map(i => { | ||
var theta = Math.PI * 2 * i / numCircleDivisions; | ||
return [Math.cos(theta), Math.sin(theta)]; | ||
}) | ||
)}); | ||
main.variable(observer()).define(["md","tex"], function(md,tex){return( | ||
md`Next, we define a list of ${tex`\theta`} values we'll use in the vertex shader to place each instance.` | ||
)}); | ||
main.variable(observer("instanceTheta")).define("instanceTheta", ["numCircleInstances"], function(numCircleInstances){return( | ||
Array.from(Array(numCircleInstances).keys()).map(i => | ||
i / numCircleInstances * 2 * Math.PI | ||
) | ||
)}); | ||
main.variable(observer()).define(["md"], function(md){return( | ||
md`Finally we define the actual draw command. One subtle thing to note here is that due to Observable data flow, this command is recreated each time the parameters above are changed. The proper way to avoid this would be to create buffers (\`circleInstanceGeometryBuffer = regl.buffer(circleInstanceGeometry)\` and the same for \`instanceTheta\`), then pass the buffers as a regl property to the draw command *when the command is invoked*. | ||
This small change would decouple the command definition from the variables above so that Observable would not recreate the command. That said, I've not done this here for two reasons. The addition adds some complexity to the code, and recreating commands many times doesn't seem to cause problems—though I suspect there probably is an upper limit to how many commands you can allocate before things just stop working.` | ||
)}); | ||
main.variable(observer("draw")).define("draw", ["regl","circleInstanceGeometry","instanceTheta","numCircleInstances","numCircleDivisions"], function(regl,circleInstanceGeometry,instanceTheta,numCircleInstances,numCircleDivisions){return( | ||
regl({ | ||
vert: ` | ||
precision highp float; | ||
attribute float theta; | ||
attribute vec2 circlePoint; | ||
varying vec3 vColor; | ||
uniform vec2 aspectRatio; | ||
uniform float time; | ||
const float PI = 3.1415926535; | ||
void main () { | ||
// Use lots of sines and cosines to place the circles | ||
vec2 circleCenter = vec2(cos(theta), sin(theta)) | ||
* (0.6 + 0.2 * cos(theta * 6.0 + cos(theta * 8.0 + time))); | ||
// Modulate the circle sizes around the circle and in time | ||
float circleSize = 0.2 + 0.12 * cos(theta * 9.0 - time * 2.0); | ||
vec2 xy = circleCenter + circlePoint * circleSize; | ||
// Define some pretty colors | ||
float th = 8.0 * theta + time * 2.0; | ||
vColor = 0.6 + 0.4 * vec3( | ||
cos(th), | ||
cos(th - PI / 3.0), | ||
cos(th - PI * 2.0 / 3.0) | ||
); | ||
gl_Position = vec4(xy / aspectRatio, 0, 1); | ||
}`, | ||
frag: ` | ||
precision highp float; | ||
varying vec3 vColor; | ||
uniform float alpha; | ||
void main () { | ||
gl_FragColor = vec4(vColor, alpha); | ||
}`, | ||
attributes: { | ||
// This attribute defines what we draw; we fundamentally draw circle vertices | ||
circlePoint: circleInstanceGeometry, | ||
|
||
// This attribute allows us to compute where we draw each circle. the divisor | ||
// means we step through one value *per circle*. | ||
theta: {buffer: instanceTheta, divisor: 1}, | ||
}, | ||
uniforms: { | ||
// Scale so that it fits in the view whether it's portrait or landscape: | ||
aspectRatio: ctx => ctx.framebufferWidth > ctx.framebufferHeight ? | ||
[ctx.framebufferWidth / ctx.framebufferHeight, 1] : | ||
[1, ctx.framebufferHeight / ctx.framebufferWidth], | ||
|
||
time: regl.context('time'), | ||
|
||
// Decrease opacity when there are more circles | ||
alpha: Math.max(0, Math.min(1, 0.15 * 2000 / numCircleInstances)), | ||
}, | ||
blend: { | ||
// Additive blending | ||
enable: true, | ||
func: {srcRGB: 'src alpha', srcAlpha: 1, dstRGB: 1, dstAlpha: 1}, | ||
equation: {rgb: 'add', alpha: 'add'} | ||
}, | ||
// GL_LINES are in general *pretty bad*, but they're good for some things | ||
primitive: 'line strip', | ||
depth: {enable: false}, | ||
count: numCircleDivisions + 1, | ||
instances: numCircleInstances, | ||
}) | ||
)}); | ||
main.variable(observer("createREGL")).define("createREGL", ["require"], function(require){return( | ||
require('regl') | ||
)}); | ||
main.variable(observer("reglCanvas")).define("reglCanvas", ["createREGL"], function(createREGL){return( | ||
function reglCanvas (width, height, dpi, reglOptions) { | ||
dpi = dpi === undefined ? devicePixelRatio : dpi; | ||
reglOptions = reglOptions || {} | ||
var canvas = document.createElement("canvas"); | ||
canvas.width = dpi * width; | ||
canvas.height = dpi * height; | ||
canvas.style.width = width + "px"; | ||
const regl = createREGL(Object.assign({}, reglOptions, {pixelRatio: dpi, canvas})); | ||
canvas.value = regl; | ||
canvas.__reglConfig = {dpi, reglOptions} | ||
return canvas; | ||
} | ||
)}); | ||
return main; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Instanced WebGL Circles | ||
|
||
https://observablehq.com/@rreusser/instanced-webgl-circles@213 | ||
|
||
View this notebook in your browser by running a web server in this folder. For | ||
example: | ||
|
||
~~~sh | ||
python -m SimpleHTTPServer | ||
~~~ | ||
|
||
Or, use the [Observable Runtime](https://github.com/observablehq/runtime) to | ||
import this module directly into your application. To npm install: | ||
|
||
~~~sh | ||
npm install @observablehq/runtime@4 | ||
npm install https://api.observablehq.com/d/5f7ba4d775a49df0.tgz?v=3 | ||
~~~ | ||
|
||
Then, import your notebook and the runtime as: | ||
|
||
~~~js | ||
import {Runtime, Inspector} from "@observablehq/runtime"; | ||
import define from "@rreusser/instanced-webgl-circles"; | ||
~~~ | ||
|
||
To log the value of the cell named “foo”: | ||
|
||
~~~js | ||
const runtime = new Runtime(); | ||
const main = runtime.module(define); | ||
main.value("foo").then(value => console.log(value)); | ||
~~~ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<!DOCTYPE html> | ||
<meta charset="utf-8"> | ||
<title>Instanced WebGL Circles</title> | ||
<link rel="stylesheet" type="text/css" href="./inspector.css"> | ||
<body> | ||
<script type="module"> | ||
|
||
import define from "./index.js"; | ||
import {Runtime, Library, Inspector} from "./runtime.js"; | ||
|
||
const runtime = new Runtime(); | ||
const main = runtime.module(define, Inspector.into(document.body)); | ||
|
||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export {default} from "./[email protected]"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import define from "./index.js"; | ||
import {Runtime, Library, Inspector} from "./runtime.js"; | ||
|
||
const notebookId = '5f7ba4d775a49df0' | ||
const notebookVersion = '213' | ||
const notebookIdentifier = `${notebookId}@${notebookVersion}` | ||
|
||
window.observableNotebooks = window.observableNotebooks || {}; | ||
var notebook = window.observableNotebooks[notebookIdentifier] = window.observableNotebooks[notebookIdentifier] || {}; | ||
|
||
const runtime = new Runtime(Object.assign(new Library, { | ||
width: 640 | ||
})); | ||
|
||
Object.assign(notebook, { | ||
runtime: runtime, | ||
define: define, | ||
Inspector: Inspector, | ||
Runtime: Runtime, | ||
Library: Library | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"name": "@rreusser/instanced-webgl-circles", | ||
"main": "[email protected]", | ||
"version": "213.0.0", | ||
"homepage": "https://observablehq.com/@rreusser/instanced-webgl-circles", | ||
"author": { | ||
"name": "Ricky Reusser", | ||
"url": "https://observablehq.com/@rreusser" | ||
}, | ||
"type": "module", | ||
"peerDependencies": { | ||
"@observablehq/runtime": "4" | ||
} | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
const React = require('react'); | ||
|
||
function scriptWithCode (source) { | ||
var s = document.createElement('script'); | ||
s.type = 'module'; | ||
s.src = `data:text/javascript;base64,${btoa(source)}`; | ||
return s; | ||
} | ||
|
||
function createNotebook (identifier) { | ||
const script = document.createElement('script'); | ||
script.src = '/static/loader.js'; | ||
script.type = 'module'; | ||
|
||
document.body.appendChild(script); | ||
|
||
var initialized = false; | ||
function initialize () { | ||
if (initialized) return; | ||
|
||
var o = window.observableNotebooks[identifier]; | ||
o.main = o.runtime.module(o.define, name => { | ||
var cell = state.pendingCells[name]; | ||
if (!cell) return; | ||
var element = cell.visible ? cell.element : document.createElement('div'); | ||
return new o.Inspector(element); | ||
}); | ||
initialized = true; | ||
return state; | ||
} | ||
|
||
var state = { | ||
pendingCells: {}, | ||
loaded: new Promise((resolve, reject) => script.onload = () => resolve(state)), | ||
initialize: initialize, | ||
}; | ||
return state; | ||
} | ||
|
||
var states = {}; | ||
function getNotebook (notebookId, version) { | ||
var identifier = `${notebookId}@${version}` | ||
if (!states[identifier]) { | ||
states[identifier] = createNotebook(identifier); | ||
} | ||
return states[identifier]; | ||
} | ||
|
||
class ObservableNotebookCell extends React.Component { | ||
getRef (component) { | ||
console.log(this.props); | ||
this.ref = component; | ||
|
||
var notebook = getNotebook(this.props.notebook, this.props.version); | ||
|
||
notebook.pendingCells[this.props.cell] = {element: this.ref, visible: this.props.visible}; | ||
|
||
notebook.loaded.then(() => notebook.initialize()); | ||
} | ||
|
||
render () { | ||
return <div ref={this.getRef.bind(this)}/>; | ||
} | ||
} | ||
|
||
ObservableNotebookCell.defaultProps = { | ||
visible: true | ||
}; | ||
|
||
module.exports = ObservableNotebookCell; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
[menu fullWidth:true/] | ||
|
||
[Header | ||
title: "ObservableHQ Test" | ||
author: "Ricky Reusser" | ||
authorLink: "https://github.com/rreusser" date: "March 4, 2020" | ||
fullWidth:true | ||
/] | ||
|
||
[Cell notebook:"5f7ba4d775a49df0" version:"213" cell:"viewof regl"/] | ||
[Cell notebook:"5f7ba4d775a49df0" version:"213" cell:"loop" visible:`false`/] | ||
[Cell notebook:"5f7ba4d775a49df0" version:"213" cell:"viewof numCircleInstances"/] |
Oops, something went wrong.