diff --git a/_posts/2025-01-17-colour-to-alpha.md b/_posts/2025-01-17-colour-to-alpha.md
index 228c695..bb46df0 100644
--- a/_posts/2025-01-17-colour-to-alpha.md
+++ b/_posts/2025-01-17-colour-to-alpha.md
@@ -2,7 +2,6 @@
 title: Replacing an image colour with transparency
 layout: post
 excerpt: What happens if you convert an RGB image to RGBA by pretending it was sitting on a white background?
-draft: true
 
 images: /assets/blog/alpha_test
 thumbnail: /assets/blog/alpha_test/thumbnail.png
diff --git a/_posts/2025-01-18-heic-depth.md b/_posts/2025-01-18-heic-depth.md
index 201ccc9..4c7a997 100644
--- a/_posts/2025-01-18-heic-depth.md
+++ b/_posts/2025-01-18-heic-depth.md
@@ -101,94 +101,75 @@ Click and drag to spin me around. It didn't really capture my nose very well, I
 
 <figure>
 <img class="no-wc" src="{{page.assets}}/rear_stereo/point_cloud_preview.png">
-<canvas style ="width: 100%;" id="canvas-id-1"></canvas>
+<point-cloud-viewer model="/assets/blog/heic_depth_map/rear_stereo/pointcloud.pcd" camera = '{"type":"perspective","position":[-3.598,-0.4154,1.971],"rotation":[0.2078,-1.06,0.1819],"zoom":1,"target":[0,0,0]}'>
+</point-cloud-viewer>
 <figcaption class="no-wc">If you have JS enabled this is interactive.</figcaption>
+<figcaption class="has-wc">An interactive point cloud view.</figcaption>
 </figure>
 
 
 <script type="module">
-import * as THREE from "three";
-import { OrbitControls } from "three/addons/controls/OrbitControls.js";
-import { DragControls } from "three/addons/controls/DragControls.js";
-import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
-import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+// import * as THREE from "three";
+// import { OrbitControls } from "three/addons/controls/OrbitControls.js";
+// import { DragControls } from "three/addons/controls/DragControls.js";
+// import { PCDLoader } from 'three/addons/loaders/PCDLoader.js';
+// import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 
 
-init('canvas-id-1', '{{page.assets}}/rear_stereo/pointcloud.pcd');
-init('canvas-id-2', '{{page.assets}}/front_facing/pointcloud.pcd');
+// init('canvas-id-1', '{{page.assets}}/rear_stereo/pointcloud.pcd');
+// init('canvas-id-2', '{{page.assets}}/front_facing/pointcloud.pcd');
 
-function init(canvas_id, url) {
-  let render, gui, orbitControls;
-  let canvas = document.getElementById(canvas_id);
-  const loader = new PCDLoader();
-  let scene = new THREE.Scene();
-  scene.add( new THREE.AxesHelper( 1 ) );
+// function init(canvas_id, url) {
+//   let render, gui, orbitControls;
+//   let canvas = document.getElementById(canvas_id);
+//   const loader = new PCDLoader();
+//   let scene = new THREE.Scene();
+//   scene.add( new THREE.AxesHelper( 1 ) );
 
-    loader.load(url, function ( points ) {
-        points.geometry.center();
-        points.geometry.rotateZ( -Math.PI/2 );
-        points.name = 'depth_map';
-        scene.add( points );
-        points.material.color = new THREE.Color(0x999999);
-        points.material.size = 0.001
-        render();
+//     loader.load(url, function ( points ) {
+//         points.geometry.center();
+//         points.geometry.rotateZ( -Math.PI/2 );
+//         points.name = 'depth_map';
+//         scene.add( points );
+//         points.material.color = new THREE.Color(0x999999);
+//         points.material.size = 0.001
+//         render();
 
-    } );
+//     } );
 
-  // --- Scene ---
-  const aspect = canvas.clientWidth / canvas.clientHeight;
-  let camera = new THREE.PerspectiveCamera( 30, aspect, 0.01, 40 );
-  camera.position.set( -2, 2, 3);
-  camera.lookAt(0, 0, 0);
+//   // --- Scene ---
+//   const aspect = canvas.clientWidth / canvas.clientHeight;
+//   let camera = new THREE.PerspectiveCamera( 30, aspect, 0.01, 40 );
+//   camera.position.set( -2, 2, 3);
+//   camera.lookAt(0, 0, 0);
 
-  // --- Renderer (use the existing canvas) ---
-  let renderer = new THREE.WebGLRenderer({ alpha: true, canvas: canvas, antialias: true });
-  renderer.setSize(canvas.clientWidth, canvas.clientHeight,);
+//   // --- Renderer (use the existing canvas) ---
+//   let renderer = new THREE.WebGLRenderer({ alpha: true, canvas: canvas, antialias: true });
+//   renderer.setSize(canvas.clientWidth, canvas.clientHeight,);
 
-  render = () => renderer.render(scene, camera);
+//   render = () => renderer.render(scene, camera);
 
-  // --- OrbitControls ---
-  orbitControls = new OrbitControls(camera, renderer.domElement);
-  orbitControls.addEventListener( 'change', render);
+//   // --- OrbitControls ---
+//   orbitControls = new OrbitControls(camera, renderer.domElement);
+//   orbitControls.addEventListener( 'change', render);
 
-  const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
-  scene.add(ambientLight);
+//   const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
+//   scene.add(ambientLight);
 
-  const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
-  dirLight.position.set(5, 5, 10);
-  scene.add(dirLight);
+//   const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
+//   dirLight.position.set(5, 5, 10);
+//   scene.add(dirLight);
 
-  window.addEventListener('resize', onWindowResize, false);
+//   window.addEventListener('resize', onWindowResize, false);
 
-  function onWindowResize() {
-    camera.aspect = canvas.clientWidth / canvas.clientHeight;
-    camera.updateProjectionMatrix();
-    renderer.setSize(canvas.clientWidth, canvas.clientHeight);
-  }
+//   function onWindowResize() {
+//     camera.aspect = canvas.clientWidth / canvas.clientHeight;
+//     camera.updateProjectionMatrix();
+//     renderer.setSize(canvas.clientWidth, canvas.clientHeight);
+//   }
 
-    // const elem = document.querySelector('#screenshot');
-    // elem.addEventListener('click', () => {
-    //   render();
-    //   canvas.toBlob((blob) => {
-    //     saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
-    //   });
-    // });
-     
-    // const saveBlob = (function() {
-    //   const a = document.createElement('a');
-    //   document.body.appendChild(a);
-    //   a.style.display = 'none';
-    //   return function saveData(blob, fileName) {
-    //      const url = window.URL.createObjectURL(blob);
-    //      console.log(url);
-    //      a.href = url;
-    //      a.download = fileName;
-    //      a.click();
-    //   };
-    // }());
-
-}
+// }
 </script>
 
 ## Update
@@ -207,6 +188,8 @@ The depth information, while lower resolution, is much better. My nose really po
 
 <figure>
 <img class="no-wc" src="{{page.assets}}/front_facing/point_cloud_preview.png">
-<canvas style ="width: 100%;" id="canvas-id-2"></canvas>
+<point-cloud-viewer model="/assets/blog/heic_depth_map/front_facing/pointcloud.pcd" camera = '{"type":"perspective","position":[-3.682,0.3606,1.82],"rotation":[-0.1955,-1.104,-0.1751],"zoom":1,"target":[0,0,0]}'>
+</point-cloud-viewer>
 <figcaption class="no-wc">If you have JS enabled this is interactive.</figcaption>
+<figcaption class="has-wc">An interactive point cloud view.</figcaption>
 </figure>
\ No newline at end of file
diff --git a/_sass/model_viewer.scss b/_sass/model_viewer.scss
index fbc1cb1..14be4d1 100644
--- a/_sass/model_viewer.scss
+++ b/_sass/model_viewer.scss
@@ -4,7 +4,8 @@
   height: 100%;
 }
 
-outline-model-viewer {
+outline-model-viewer,
+point-cloud-viewer {
   width: 100%;
   min-height: 300px;
   display: flex;
diff --git a/assets/js/outline-model-viewer/PointCloudViewer.js b/assets/js/outline-model-viewer/PointCloudViewer.js
new file mode 100644
index 0000000..50a9ee8
--- /dev/null
+++ b/assets/js/outline-model-viewer/PointCloudViewer.js
@@ -0,0 +1,85 @@
+import * as THREE from "three";
+import { OrbitControls } from "three/addons/controls/OrbitControls.js";
+import { PCDLoader } from "three/addons/loaders/PCDLoader.js";
+import { Timer } from "three/addons/Addons.js";
+
+import {
+  componentHTML,
+  setupThreeJS,
+  deserialiseCamera,
+  deserialiseControls,
+} from "./helpers.js";
+
+export class PointCloudViewer extends HTMLElement {
+  constructor() {
+    super();
+    this.isVisible = true;
+    this.shadow = this.attachShadow({ mode: "open" });
+  }
+
+  connectedCallback() {
+    const { container, canvas, scene, renderer, gui } = setupThreeJS(this);
+
+    const loader = new PCDLoader();
+    scene.add(new THREE.AxesHelper(1));
+
+    const model = this.getAttribute("model");
+    const render = () => renderer.render(scene, this.camera);
+    this.render = render;
+
+    loader.load(model, function (points) {
+      points.material.size = 0.05;
+      gui
+        .add(points.material, "size", 0.01, 0.2)
+        .name("Point Size")
+        .onChange(render);
+
+      points.geometry.center();
+      points.geometry.rotateZ(-Math.PI / 2);
+      points.name = "depth_map";
+      scene.add(points);
+      points.material.color = new THREE.Color(0x999999);
+      render();
+      console.log("Model Loaded.");
+    });
+
+    // --- OrbitControls ---
+    this.controls = new OrbitControls(this.camera, renderer.domElement);
+    this.controls.addEventListener("change", render);
+    this.controls.enableDamping = true;
+    this.controls.dampingFactor = 0.25;
+    this.controls.autoRotate = true;
+    deserialiseControls(this);
+
+    canvas.addEventListener("click", () => {
+      this.controls.autoRotate = false;
+    });
+
+    const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
+    scene.add(ambientLight);
+
+    const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
+    dirLight.position.set(5, 5, 10);
+    scene.add(dirLight);
+
+    window.addEventListener("resize", onWindowResize, false);
+
+    function onWindowResize() {
+      this.camera.aspect = canvas.clientWidth / canvas.clientHeight;
+      this.camera.updateProjectionMatrix();
+      renderer.setSize(canvas.clientWidth, canvas.clientHeight);
+    }
+    const timer = new Timer();
+
+    const update = () => {
+      if (this.isVisible) {
+        timer.update();
+        const delta = timer.getDelta();
+        requestAnimationFrame(update);
+        this.controls.update(delta);
+        this.render();
+      }
+    };
+    update();
+  }
+}
diff --git a/assets/js/outline-model-viewer/helpers.js b/assets/js/outline-model-viewer/helpers.js
new file mode 100644
index 0000000..333916b
--- /dev/null
+++ b/assets/js/outline-model-viewer/helpers.js
@@ -0,0 +1,210 @@
+import * as THREE from "three";
+import GUI from "lil-gui";
+
+export function serialiseCamera(component) {
+  const { camera, controls } = component;
+  const position = Object.values(camera.position);
+  const extractXYZ = ({ _x, _y, _z }) => [_x, _y, _z];
+  const rotation = extractXYZ(camera.rotation);
+  const fixed = (l) => l.map((x) => parseFloat(x.toPrecision(4)));
+  return JSON.stringify({
+    type: "perspective",
+    fov: camera.fov,
+    near: camera.near,
+    far: camera.far,
+    position: fixed(position),
+    rotation: fixed(rotation),
+    zoom: camera.zoom,
+    target: fixed(Object.values(controls.target)),
+  });
+}
+
+// Todo alllow isometric camera
+export function deserialiseCamera(component) {
+  const { canvas, initial_camera_state } = component;
+  const aspect = canvas.clientWidth / canvas.clientHeight;
+
+  const camera = new THREE.PerspectiveCamera(30, aspect, 0.01, 40);
+
+  if (!initial_camera_state) return;
+  if (initial_camera_state.type !== "perspective") return;
+  if (initial_camera_state.fov) camera.fov = initial_camera_state.fov;
+  if (initial_camera_state.near) camera.near = initial_camera_state.near;
+  if (initial_camera_state.far) camera.far = initial_camera_state.far;
+  if (initial_camera_state.zoom) camera.zoom = initial_camera_state.zoom;
+  if (initial_camera_state.position)
+    camera.position.set(...initial_camera_state.position);
+  if (initial_camera_state.rotation)
+    camera.rotation.set(...initial_camera_state.rotation);
+
+  camera.updateProjectionMatrix();
+
+  return camera;
+}
+
+export function deserialiseControls(component) {
+  const { controls, initial_camera_state } = component;
+  if (initial_camera_state.target && controls.target)
+    controls.target.set(...initial_camera_state.target);
+}
+
+const saveBlob = (function () {
+  const a = document.createElement("a");
+  document.body.appendChild(a);
+  a.style.display = "none";
+  return function saveData(blob, fileName) {
+    const url = window.URL.createObjectURL(blob);
+    console.log(url);
+    a.href = url;
+    a.download = fileName;
+    a.click();
+  };
+})();
+
+function takeScreenshot(component) {
+  const { canvas, render } = component;
+  render();
+  canvas.toBlob((blob) => {
+    saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`);
+  });
+}
+
+function componentHTML(component_rect) {
+  const { height } = component_rect;
+  console.log("Height:", height);
+  return `
+      <div id="container">
+      <span id = "clicked-item"></span>
+      <!-- <button id="fullscreen-btn">⛶</button> --!>
+      <canvas class = "object-viewer"></canvas>
+      </div>
+      <link rel="stylesheet" href="/node_modules/lil-gui/dist/lil-gui.min.css">
+      <style>
+
+        #container {
+          position: relative;
+          width: 100%;
+          display: flex;
+          flex-direction: column;
+          border-radius: inherit;
+        }
+
+        #clicked-item {
+            position: absolute;
+            top: 10px;
+            left: 10px;
+            z-index: 10;
+            font-size: 0.7em;
+            background: none;
+            border: none;
+            color: var(--theme-text-color);
+            opacity: 50%;
+        }
+
+        #fullscreen-btn {
+          position: absolute;
+          top: 10px;
+          right: 10px;
+          z-index: 10;
+          font-size: 24px;
+          background: none;
+          border: none;
+          cursor: pointer;
+          color: var(--theme-text-color);
+        }
+
+        #fullscreen-btn:hover {
+          color: var(--theme-subtle-outline);
+        }
+
+        .lil-gui .title {height: 2em;}
+        .lil-gui.root {
+          margin-top: calc(${height}px - 2em);
+          width: 100%;
+          z-index: 1;
+          --background-color: none;
+          --text-color: var(--theme-text-color);
+          --title-background-color: none;
+          --title-text-color: var(--theme-text-color);
+          --widget-color: var(--theme-subtle-outline);
+          --hover-color: lightgrey;
+          --focus-color: lightgrey;
+          --number-color: #2cc9ff;
+          --string-color: #a2db3c;
+      }
+
+      .lil-gui button {
+        border: var(--theme-subtle-outline) 1px solid;
+      }
+
+      .lil-gui .controller.string input {
+        background-color: var(--theme-subtle-outline);
+        color: var(--theme-text-color);
+      }
+
+        canvas {
+          position: absolute;
+          width: 100%;
+          height: ${height}px;
+          border-radius: inherit;
+        }
+      </style>
+    `;
+}
+
+// Usage const { container, canvas, scene, gui } = setupThreeJS(this);
+function setupThreeJS(component) {
+  const component_rect = component.getBoundingClientRect();
+
+  //  Create the component HTML
+  component.shadow.innerHTML = componentHTML(component_rect);
+  component.container = component.shadow.querySelector("div#container");
+  component.canvas = component.shadow.querySelector("canvas");
+  const canvas_rect = component.canvas.getBoundingClientRect();
+
+  if (component.getAttribute("camera")) {
+    component.initial_camera_state = JSON.parse(
+      component.getAttribute("camera")
+    );
+    component.removeAttribute("camera");
+  }
+
+  component.camera = deserialiseCamera(component);
+
+  component.scene = new THREE.Scene();
+
+  component.renderer = new THREE.WebGLRenderer({
+    canvas: component.canvas,
+    alpha: true,
+  });
+
+  component.renderer.setPixelRatio(window.devicePixelRatio);
+  component.renderer.setSize(canvas_rect.width, canvas_rect.height, false);
+
+  component.gui = new GUI({
+    title: "Settings",
+    container: component.container,
+    injectStyles: true,
+    closeFolders: true,
+  });
+
+  if ((component.getAttribute("debug") || "closed") !== "open")
+    component.gui.close();
+
+  const params = {
+    printCamera: () => console.log(serialiseCamera(component)),
+    screenshot: () => takeScreenshot(component),
+    resetCamera: () => {
+      deserialiseCamera(component);
+      component.render();
+    },
+  };
+
+  component.gui.add(params, "printCamera").name("Print Viewport State");
+  component.gui.add(params, "screenshot").name("Take Screenshot");
+  component.gui.add(params, "resetCamera").name("Reset Viewport");
+
+  return component;
+}
+
+export { componentHTML, setupThreeJS };
diff --git a/assets/js/outline-model-viewer/index.js b/assets/js/outline-model-viewer/index.js
index a27b3be..a4e51d7 100644
--- a/assets/js/outline-model-viewer/index.js
+++ b/assets/js/outline-model-viewer/index.js
@@ -12,9 +12,14 @@ import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
 import { Timer } from "three/addons/Addons.js";
 
 import GUI from "lil-gui";
+import { componentHTML, setupThreeJS, serialiseCamera } from "./helpers.js";
 import { CustomOutlinePass } from "./CustomOutlinePass.js";
 import FindSurfaces from "./FindSurfaces.js";
 
+import { PointCloudViewer } from "./PointCloudViewer.js";
+
+customElements.define("point-cloud-viewer", PointCloudViewer);
+
 // Todo:
 // Swap in the version of this code that has a debug GUI behind a flag
 // Consider support for transparent objects by rendering them as a wireframe in the color and excluding them from the edge pass.
@@ -59,26 +64,11 @@ function printGLTFScene(scene, maxDepth = 3, depth = 0, indent = 0) {
   });
 }
 
-const serialiseCamera = (camera, controls) => {
-  const position = Object.values(camera.position);
-  const extractXYZ = ({ _x, _y, _z }) => [_x, _y, _z];
-  const rotation = extractXYZ(camera.rotation);
-  const fixed = (l) => l.map((x) => parseFloat(x.toPrecision(4)));
-  return JSON.stringify({
-    position: fixed(position),
-    rotation: fixed(rotation),
-    zoom: camera.zoom,
-    target: fixed(Object.values(controls.target)),
-  });
-};
-
 export class OutlineModelViewer extends HTMLElement {
   constructor() {
     super();
-    this.isVisible = true; // Track visibility
+    this.isVisible = true;
     this.shadow = this.attachShadow({ mode: "open" });
-
-    // Mouse and raycaster
     this.raycaster = new THREE.Raycaster();
     this.mouse = new THREE.Vector2();
     this.intersectedObject = null; // Store currently intersected object
@@ -95,8 +85,7 @@ export class OutlineModelViewer extends HTMLElement {
     const mul = 2;
 
     let component_rect = this.getBoundingClientRect();
-
-    this.render(component_rect.height);
+    this.shadow.innerHTML = componentHTML(component_rect);
 
     const model_path = this.getAttribute("model");
     const spin = (this.getAttribute("spin") || "true") === "true";
@@ -492,87 +481,6 @@ export class OutlineModelViewer extends HTMLElement {
     }
     document.addEventListener("fullscreenchange", onFullScreenChange);
   }
-
-  render(height) {
-    this.shadow.innerHTML = `
-      <div id="container">
-      <span id = "clicked-item"></span>
-      <!-- <button id="fullscreen-btn">⛶</button> --!>
-      <canvas class = "object-viewer"></canvas>
-      </div>
-      <link rel="stylesheet" href="/node_modules/lil-gui/dist/lil-gui.min.css">
-      <style>
-
-        #container {
-          position: relative;
-          width: 100%;
-          display: flex;
-          flex-direction: column;
-          border-radius: inherit;
-        }
-
-        #clicked-item {
-            position: absolute;
-            top: 10px;
-            left: 10px;
-            z-index: 10;
-            font-size: 0.7em;
-            background: none;
-            border: none;
-            color: var(--theme-text-color);
-            opacity: 50%;
-        }
-
-        #fullscreen-btn {
-          position: absolute;
-          top: 10px;
-          right: 10px;
-          z-index: 10;
-          font-size: 24px;
-          background: none;
-          border: none;
-          cursor: pointer;
-          color: var(--theme-text-color);
-        }
-
-        #fullscreen-btn:hover {
-          color: var(--theme-subtle-outline);
-        }
-
-        .lil-gui .title {height: 2em;}
-        .lil-gui.root {
-          margin-top: calc(${height}px - 2em);
-          width: 100%;
-          z-index: 1;
-          --background-color: none;
-          --text-color: var(--theme-text-color);
-          --title-background-color: none;
-          --title-text-color: var(--theme-text-color);
-          --widget-color: var(--theme-subtle-outline);
-          --hover-color: lightgrey;
-          --focus-color: lightgrey;
-          --number-color: #2cc9ff;
-          --string-color: #a2db3c;
-      }
-
-      .lil-gui button {
-        border: var(--theme-subtle-outline) 1px solid;
-      }
-
-      .lil-gui .controller.string input {
-        background-color: var(--theme-subtle-outline);
-        color: var(--theme-text-color);
-      }
-
-        canvas {
-          position: absolute;
-          width: 100%;
-          height: ${height}px;
-          border-radius: inherit;
-        }
-      </style>
-    `;
-  }
 }
 
 customElements.define("outline-model-viewer", OutlineModelViewer);