← gentropic.org

Previous

older work, mostly geology in a browser

Most of this is older than the GCU's name. It starts at my master's, with desktop tools I wrote in Python, and moves into the browser once I picked up JavaScript around 2017 — mostly to get geology into a tab (stereonets, a phone compass, drillhole tools), partly because the browser APIs were fun to poke at.

The desktop tools are shown as screenshots, since they don't run in a browser; everything after is collected still running, in its original state, rather than as screenshots of dead links. A fair amount of what the GCU does now started as one of these.

circa 2016 · before the browser

Desktop roots — the master's tools

Before endarthur.github.io existed there were desktop tools, written in Python. The main one is OpenStereo — a GUI for directional-data and structural-geology analysis (load orientation data, plot poles and great circles on a stereonet, contour their density, run the statistics). It isn't mine originally: it was created by Carlos Grohmann and Ginaldo Campanha at USP around 2010. I took it over for the 2.0 rewrite as my master's work and still maintain it — and I built its numerical engine, auttitude, as a separate library, which is exactly why I could later port a slice of it to JavaScript for the browser compass and everything that came after.

The other line is mesh analysis. Capivaras takes a photogrammetry model of an outcrop and lets you pick surfaces straight off it on the GPU — extracting attitudes and roughness, and tracing structures semi-automatically with Dijkstra's shortest path over the mesh treated as a graph. It's the self-contained successor to ply2atti and scanline. The AR-desurvey and photogrammetry experiments later in the browser are the lightweight cousins of this.

OpenStereo 2.0b — directional-data analysis (Python) ● the master's project
OpenStereo — desktop stereonet analysis GUI with poles and density contours
Poles and density contours on an equal-area net (the Tocher dataset, 200 poles). The desktop home of auttitude — and where the stereonet thread that runs through this whole page begins.
Capivaras — GPU-picking outcrop-mesh analysis (Python) ● the mesh line
Capivaras — plane sets picked off a photogrammetry outcrop mesh
Plane sets picked straight off a photogrammetry outcrop mesh; traces by shortest-path over the mesh-as-graph. The self-contained successor to ply2atti + scanline.

credit OpenStereo was created by C.H. Grohmann & G.A.C. Campanha (USP, AGU 2010); I did the 2.0 rewrite, maintain it, and built its auttitude engine. Capivaras is part of Camila Duelis Viana's PhD; its predecessors ply2atti + scanline are from Viana (2016).

2017 · the first page

Stereonets on HTML

My first personal page. The heading — Computatro et Malleo, roughly "by computer and by hammer" — stuck, and it's still the motto. I'd just picked up JavaScript and wanted to see what the browser could do with structural data.

On a phone it works as a rough geological compass: device orientation drives an equal-area stereonet, with a trigger to lock a reading. It's experimental — I wouldn't measure anything real with it — but it's where the habit of putting geology in a browser started.

endarthur.github.io ● 2017 · runs live
The actual page, embedded and running. On a desktop it's the static stereonet and the motto; on a phone with a compass it becomes the instrument. The sibling os.html is a dedicated plotter.

credit Device-orientation handled with gyronorm.js (Doruk Eker); the stereonet maths is my own auttitude.

2017–2018 · DIGItal KOMpass

Digikom — the phone becomes the needle

DIGItal KOMpass — and despite my presenting it at TRANSFORM 2020, it's really a 2017 thing (the copyright header gives it away), the front page's very next step. The page-as-compass was fine, but the screen sits on the same device you're pressing against the rock. So I split it in two: the phone is the sensor, the desktop is the display, and they talk straight to each other over WebRTC. You pair them by scanning a QR the desktop shows — no server, no app, no accounts.

It's also where those toolbar icons live that you spotted first — the save-floppy and the dip-pen, drawn by hand on millimetre paper and typed out as SVG paths, coordinate by coordinate. I'd lost the source years ago; this copy was pulled back from the deployed page, so it's preserved now, hand-drawn icons and all.

previous/digikom/ — recovered from digikom.surge.sh ● 2017 · preserved
The recovered app, running from a local copy so it outlives surge.sh. On a desktop: the stereonet, the connect-phone QR, and that hand-drawn toolbar; pair a phone and its orientation drives the plot.
the hand-drawn toolbar — SVG typed off graph paper ● 2017 · source
<g id="saveFloppy" transform="translate(82) scale(.5)">
  <path d="M 0 0 H 24 L 25 1 V 26 H 0 Z" />
  <path d="M 2.2 26 V 11 H 22 V 26" />
  <path d="M 2.2 0 V 8 A 1 1 0 0 0 3.2 9 H 18 A 1 1 0 0 0 19 8 V 0" />
  <path d="M 6 0 V 8 A 1 1 0 0 0 7 9 H 18 A 1 1 0 0 0 19 8 V 0" />
  <path d="M 13 1 H 17 V 8 H 13 Z" />
  <path d="M 23 23 H 24 V 24 H 23 Z" />
  <path d="M 0.6 23 H 1.6 V 24 H 0.6 Z" />
</g>
<g id="toggleMarker" transform="translate(15) rotate(45) scale(.5)">
  <path d="M 0 0 V 20 H 7 V 0" />
  <path d="M 1.5 20 V 25 H 5.5 V 20" />
  <path d="M 3 25 V 30 H 4 V 25" />
  <path d="M 3 30 V 31 H 4 V 30 Z" fill-opacity="1" />
</g>
The save-floppy (with its clipped corner and the little shutter arcs) and the dip-pen — every coordinate a whole or half integer, read straight off the grid. A third icon, a "clear" page, sits commented-out in the original, never finished.

credit Peer-to-peer with PeerJS; device orientation via gyronorm.js (Doruk Eker); QR codes by qrcodejs (David Shim); stereonet maths my own auttitude.

2019 · june

Drillhole desurvey, with the maths shown

This is the one I kept coming back to. Desurveying is the unglamorous step that turns a drillhole's downhole survey — depth, azimuth, dip at intervals — into an actual path in 3D, and it's fiddlier than it looks. So I wrote out the derivation (tangent and minimum-curvature), and then, because it's easier to see than read, built a small three.js drillhole editor next to it.

Paste a survey, set lithology intervals, watch the hole draw in 3D, export a GLB — and that GLB feeds the AR and WebXR viewers, so you can stand a drillhole on your desk through a phone. Maths, tool, and pipeline in one page.

endarthur.github.io/writing/desurvey.html ● 2019 · runs live
The page itself — the derivation up top, the interactive drillhole editor below. Written in 2019, tweaked through 2020: the maths didn't change, the browser APIs did.

credit Method after the drillhole-desurvey write-ups by the Leapfrog blog and Steve Henley; 3D + GLTF export with three.js (its official exporter example).

2019 · june 28

The stereonet calculator

Made the day after desurvey, and about as bare as it looks: an equal-area stereonet and two number boxes. Type a plane's attitude — dip direction and dip — and its pole and great circle move on the net as you type. That's the whole interface, and honestly that's all I wanted: the smallest possible "see the geometry while you enter it."

The real work was underneath. I was porting auttitude — my structural-geology library — to JavaScript properly, as ES modules this time. Half the source is commented-out experiments: arcs between points, small circles, plane intersections. It's more workbench than page, and I kept it that way.

endarthur.github.io/writing/attitude.html ● 2019 · runs live
Live — type an attitude in the two boxes and the plane moves, pole and great circle. Sparse on purpose; the work was porting auttitude to JS underneath.
2019 · july

VR on a path

Exactly what it says, and yes — it's a bit nauseating. A-Frame had just made WebVR genuinely easy, so I made a small generator: mark a path over a 3D model in MeshLab point by point, hand it the model and the picked-points file, set a camera height and a duration, and it spits out a standalone HTML page that flies you along that path in VR — phone, headset, whatever. The fly-along-the-path motion itself is a component by josepedrodias (via an answer from ngokevin) — that did most of the real work; I wrote the part that assembles the page. Mostly I was flying through photogrammetry models of outcrops.

There's no collision detection, so the camera will happily hug and clip through the ground if you forget to lift it, and a quick path is a straight line to motion sickness — the page literally suggests shrinking the model and slowing down to cope. Janky, but I'm still fond of it.

endarthur.github.io/writing/aframe_path.html ● 2019 · runs live
The generator itself — pick a path in MeshLab, give it the model and a duration, and it downloads a standalone VR page. The flythrough (and the nausea) is the generated output, not this form.

credit The along-path component is josepedrodias's (via ngokevin); built on A-Frame + aframe-extras (Don McCurdy). Example model: "Quarry Slope" by DronWork (Sketchfab).

2019

Stereo panoramas, by slit-scan

This one I'm fond of, even though it barely works — and the idea isn't even mine. It's Paul Bourke's (the same Paul Bourke whose file-format encyclopedia half the internet has landed on at some point): as a camera rotates, extract a narrow vertical slit from each frame and stitch the slits into a panorama; take two slits, slightly offset, and the parallax between them gives a stereo pair you can cross your eyes at. He traces it to the analog rotating slit-cameras — the Roundshots and their kin.

I just wired it into a phone browser. As you turn, every time the compass ticks to a new bearing it grabs a one-pixel strip — 720 of them for a full circle — sampling a third and two-thirds of the way across the frame for the two eyes. getUserMedia, device orientation, a canvas; about sixty lines, no libraries.

The catch, and you feel it instantly: it leans entirely on the phone's compass to know where each strip belongs, and the compass is jittery and drifts. So unless you've got a very stable rotating base, the columns smear and the panorama tears. Honestly more a lovely idea than a working tool.

stereopanoramics.html — the slit-scan core ● 2019 · source
const constraints = { video: { facingMode: "environment" } };

const azimuth_resolution = 720;
var disparity = 60;
var h;
var li, ri;

var plunge = new Float32Array(azimuth_resolution);

function handleSuccess(stream) {
  video.srcObject = stream;
  h = stream.getVideoTracks()[0].getSettings().height;
  right.height = h;
  left.height = h;
  right.width = azimuth_resolution;
  left.width = azimuth_resolution;
  right_ctx = right.getContext("2d");
  left_ctx = left.getContext("2d");

  ri = Math.floor((h * 2) / 3);
  li = Math.floor(h / 3);
  window.addEventListener("deviceorientation", handleOrientation);
}

function handleOrientation(e) {
  let az = e.alpha;
  const i = Math.floor(
    (((az + 360.0) % 360.0) * azimuth_resolution) / 360.0
  );
  if (plunge[i] === 0.0) {
    plunge[i] = 1;
    right_ctx.drawImage(video, ri, 0, 1, h, i, 0, 1, h);
    left_ctx.drawImage(video, li, 0, 1, h, i, 0, 1, h);
  }
}
The whole trick, give or take the camera plumbing: as the compass bearing (e.alpha) reaches a new value, map it to a column 0–720 and, if that column is still empty, copy a one-pixel strip of the camera frame into it — two strips, a third and two-thirds across, for the stereo pair. That's the panorama.

credit Concept and method: Paul Bourke (Synthetic Stereoscopic Panoramic Images, 2006).

2019 · november

Photogrammetry, calibrated with a door

Another tiny, half-finished one — the page's <title> still says "Practical Desurvey" from whatever I copied it off. The useful idea: before you can do photogrammetry with a phone you need the camera's field of view, and you can get it from any object of known size and distance. I used a door — a trick I picked up from a write-up by Forrest Johnson — and in Brazil they're usually 210 cm, so you walk back until it just fills the frame top to bottom, measure the distance, and FoV = 2·atan(h/2d). The calculator works it out and caches it in your browser.

The rest is the stereo-panorama trick again — slit-scan as you turn — but wrapped onto an A-Frame cylinder, sized from that calibrated FoV, so you can look around inside the panorama in VR. Same compass-drift problem as before; I just liked tying the calibration to the capture.

endarthur.github.io/writing/camera.html ● 2019 · runs live
The calibration half runs fine here — the door formula and the FoV calculator (210 cm, walk back, done). The camera capture and the VR cylinder below need a phone.

credit The door-as-reference trick from a write-up by Forrest Johnson; photogrammetry definition per the ASPRS (1942).

2020 · spring

Serverless tools — the link is the channel

Spring 2020 — which is to say, lockdown — so I made a run of little tools for connecting people, all under one rule: no server. A voice call (dial), a call with screen-share (com), plain screen sharing (screen), live location on a map (location). Each works the same way: you send someone a link, and the link itself carries everything needed to connect, peer-to-peer over WebRTC. No accounts, nothing to sign up for.

The trick that makes it serverless is this little page, store: open it with some data packed into the URL fragment and it drops that data into the browser's storage, or broadcasts it to your other tabs, and closes itself. The link is the channel — that's the keystone the rest stand on, in about twenty lines. (I came back to the same idiom in 2023 for a deadman's switch: a tab that pings a webhook if it ever goes quiet.)

tools/store.html — the serverless keystone ● 2020 · source
if (window.location.hash) {
  const encodedData = window.location.hash.substring(1);
  const decodedData = decodeURIComponent(encodedData);
  const parsedData = JSON.parse(decodedData);
  switch (parsedData.target) {
    case "local":
      window.localStorage[parsedData.key] = parsedData.data;
      break;
    case "session":
      window.sessionStorage[parsedData.key] = parsedData.data;
      break;
    case "broadcast":
      const bc = new BroadcastChannel(parsedData.channel);
      bc.postMessage(parsedData.data);
    default:
      break;
  }
  if (parsedData.close) {
    window.close();
  }
}
The entire page, give or take the boilerplate. Pass it state through the URL, it stores or broadcasts it — no server in the loop. Everything else just adds WebRTC on top.
endarthur.github.io/tools/location.html ● 2020 · runs live
Live location on a Leaflet map, shared peer-to-peer through a link — no server ever holds your position. Like the rest, it wants a peer on the far end to do its thing.

credit Peer connections with PeerJS and simple-peer (Feross Aboukhadijeh); the map is Leaflet.

circa 2020 · the peak

ECMAltine — geology as a little language

This is the one I find genuinely audacious. ECMAltine is a tiny language for building geological models — the browser port of Malt, a forward-modelling library I'd been writing in Python. You write planes, faults, folds and lithologies as composable functions — a plane is a normal vector, a surface is a signed-distance field, a fault is a function that offsets space on one side of itself, a fold is a sinusoidal warp of the coordinate — and Model([...]) threads a point through all of them in order, returning the rock code at that location. Geological history as a pipeline of space-deformations, which is how structural geology actually composes.

Press run and it evaluates that model across a 128×128×128 grid — two million points — and bakes the result into a 3D texture: a voxel block model, the same object BMA chews on today. A hand-patched three.js shader then samples that texture along cutting planes you can drag through the scene, so you slice live cross-sections of your geology. A REPL for structural modelling, in a browser tab, around 2020.

And it didn't stop here. A couple of years on I rebuilt it as jsgem, running the implicit model through a marching-cubes worker into actual 3D meshes instead of sampled sections — GemPy-in-a-browser, properly. For years I thought that one was gone (never committed, surviving only in a conference recording) — but it wasn't. It's the next entry, recovered and running. The line doesn't end on a loss after all.

endarthur.github.io/tools/ECMAltine.html ● circa 2020 · runs live
Live — the DSL editor on the left, the model's cross-sections on the right. Edit the geology, press run, drag the section planes through the result. (Open it full-screen for the real thing; the controls are listed across the top.)
the malt engine — geology as functions ● circa 2020 · source
function PlanarFault(plane, point, displacement, footwall_movement = false) {
  const faultSurface = PlanarSurface(plane, point, footwall_movement ? -1.0 : 1.0);
  return (X, v) => {
    if (faultSurface(X) > 0) {
      X.add(displacement);
    }
    return [X, v];
  };
}

function SinusoidalFold(hinge, axial_plane_normal, point, amplitude, wavelength) {
  const axial_trace = new THREE.Vector3()
    .crossVectors(axial_plane_normal, hinge)
    .normalize();
  const axialPlane = PlanarSurface(axial_plane_normal, point);
  return (X, v) => {
    X.addScaledVector(axial_trace, amplitude * Math.sin(axialPlane(X) / wavelength));
    return [X, v];
  };
}

function Lith(surface, code) {
  return (X, v) => [X, surface(X) > 0 ? code : v];
}

function Model(events) {
  return (X) => {
    let code = 0;
    for (let i = 0; i < events.length; i++) {
      [X, code] = events[i](X, code);
      if (code !== 0) break;
    }
    return code;
  };
}
Every event is a function (X, v) => [X, v]: a fault displaces space on one side of itself, a fold warps the coordinate by a sine, a lithology stamps a rock code, and Model threads a point through them all. Geology as a fold of functions — the same insight GemPy encodes, hand-built here.
Malt — the Python parent (forward-modelling library) ● the original
Malt — the desktop Python forward-geological-modelling library, with its 3D block
Where it came from: Malt, the desktop Python original — the same algebra (ma.PlanarFault, ma.SinusoidalFold, ma.Model), a Maya-style panel, and the model rendered as a solid 3D block. ECMAltine is its browser port; jsgem (next) became its meshing successor. (Screenshot from a 2021 portfolio.)

credit 3D with three.js; the live editor is CodeMirror + tern + acorn (Marijn Haverbeke), linted by JSHint.

2022 · recovered 2026

jsgem — the one we got back

This is where ECMAltine was headed: jsgem (or js-gem) runs the same Malt kernel, but instead of slicing cross-sections it threads the model through a marching-cubes worker and builds an actual 3D mesh — GemPy-in-a-browser, properly. A live editor, a terminal, and a meshed geological body you can orbit. I made it around 2022.

And then I lost it. It was never committed; for years it survived only in the TRANSFORM conference recording. Until it didn't — in June 2026 it surfaced as a single uncommitted file on an old hard drive, was recovered, and ran again, meshing geology in the browser for the first time in four years. The image below is that run.

jsgem — recovered & running ● 2022 · alive again
jsgem running — an implicit geological model marching-cubed into a 3D mesh
112,764 triangles for the first lithology, building the second — the implicit model in the editor (left), marching-cubed into a solid body (right). Recovered from one uncommitted copy and run again, four years on.
endarthur.github.io/tools/jsgem.html ● run it yourself
The recovered suite, live (it builds the default model on load — give the marching cubes a moment). Goes live once the recovery commit is deployed.

credit Meshing via gridhopping (marching cubes, by Nenad Markuš); 3D with three.js; editor CodeMirror; terminal jQuery Terminal; plus math.js, DanfoJS, PapaParse, WinBox. The ECMAltine.js Malt kernel is my own.

still out there

That's the timeline — desktop roots through 2022, and the lost capstone brought home. One thing's still missing: an Alexa skill from around 2019 that did stereonet maths by voice, on auttitude. Its code didn't surface on the old drives; it may yet be recoverable from an Amazon developer account. The hunt continues. Named, not silenced — and, this time, mostly found.