mirror of
https://github.com/TomHodson/tomhodson.github.com.git
synced 2025-06-26 10:01:18 +02:00
Compare commits
3 Commits
31d159a5b5
...
1a3d9d0bee
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1a3d9d0bee | ||
![]() |
bd1cdbb187 | ||
![]() |
655c777c2c |
@ -23,6 +23,9 @@
|
||||
<link rel="icon" type="image/png" href="/favicon/android-chrome-512x512.png" media="(prefers-color-scheme: dark)">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
|
||||
<!-- Webmentions provided webmention.io -->
|
||||
<link rel="webmention" href="https://webmention.io/thomashodson.com/webmention" />
|
||||
|
||||
<!-- Description tags -->
|
||||
{% if page.excerpt %}
|
||||
<!-- HTML Meta Tag Description -->
|
||||
@ -63,13 +66,16 @@
|
||||
<!-- Privacy Conscious Hit Counter -->
|
||||
{% unless jekyll.environment == "development" %}
|
||||
<script data-goatcounter="https://tomhodson.goatcounter.com/count"
|
||||
async src="https://gc.zgo.at/count.js"></script>
|
||||
async src="/assets/js/goat_count.js"></script>
|
||||
{% endunless %}
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
<script src="/assets/js/pre_page_load.js"></script>
|
||||
<script src="/assets/js/index.js" defer></script>
|
||||
|
||||
<!-- Add JS to expand images on click -->
|
||||
<script src="/assets/js/expand_img_tags.js" defer></script>
|
||||
|
||||
{% if page.mathjax %}
|
||||
<script>
|
||||
console.log("MathJax is enabled for this page. Enjoy the math!");
|
||||
|
@ -4,6 +4,7 @@ layout: project
|
||||
excerpt: An old ceiling mounted LED downlight becomes a chonky bike light.
|
||||
permalink: /projects/downlight_bikelight
|
||||
assets: /assets/projects/downlight_bikelight
|
||||
images: /assets/projects/downlight_bikelight/img
|
||||
date: 2025-05-11
|
||||
|
||||
img:
|
||||
@ -12,11 +13,30 @@ img:
|
||||
src: /assets/projects/downlight_bikelight/thumbnail.png
|
||||
|
||||
social_image: /assets/projects/downlight_bikelight/thumbnail.png
|
||||
model: /assets/projects//downlight_bikelight/models
|
||||
draft: True
|
||||
model: /assets/projects/downlight_bikelight/models
|
||||
---
|
||||
I pulled this dying LED downlight out of my kitchen ceiling.
|
||||
<figure>
|
||||
<img src="{{page.images}}/original_location.jpeg" alt="My hand pulling a circular LED downlight out of the ceiling.">
|
||||
</figure>
|
||||
|
||||
In doing so I realised that this downlight has an amusingly chonky heatsink and a nice lens.
|
||||
|
||||
<figure class = "two-wide">
|
||||
<img src="{{page.images}}/laid_out_front.jpeg" alt="All the parts of the downlight laid out on the carpet. There's a lens, LED and various spacers.">
|
||||
<img src="{{page.images}}/laid_out_top.jpeg" alt="All the parts of the downlight laid out on the carpet. There's a lens, LED and various spacers.">
|
||||
</figure>
|
||||
|
||||
So I made a front plate for it to turn it into a bicycle light. I think it suits the current cobbled together, solarpunky aesthetic of my bike.
|
||||
|
||||
<outline-model-viewer model = "{{page.model}}/fbx_export.glb" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[848.5,470.2,-294.9],"rotation":[-2.131,0.9915,2.214],"zoom":300,"target":[0,0,0]}'>
|
||||
<img class="outline-model-poster no-wc" src = "{{page.assets}}/thumbnail.svg">
|
||||
<p class="has-wc">Loading model...</p>
|
||||
</outline-model-viewer>
|
||||
|
||||
|
||||
Next job is to actually mount it!
|
||||
|
||||
<figure>
|
||||
<img src="{{page.images}}/cad.jpeg" alt="Me holding the printed out new front ring in front of the CAD model on my laptop.">
|
||||
</figure>
|
@ -12,7 +12,7 @@ img:
|
||||
src: /assets/projects/elegoo_mount/thumbnail.png
|
||||
|
||||
social_image: /assets/projects/elegoo_mount/thumbnail.png
|
||||
draft: True
|
||||
draft: false
|
||||
---
|
||||
|
||||
This is just a quick mount for a [BIQU H2V2](https://biqu.equipment/products/biqu-h2-v2-0-extruder) hotend on an Elegoo Neptune 2.
|
||||
|
@ -11,6 +11,7 @@ img:
|
||||
|
||||
social_image: /assets/projects/helmet_lights/thumbnail.png
|
||||
models: /assets/projects/helmet_lights/models
|
||||
date: 2023-01-01
|
||||
|
||||
---
|
||||
|
||||
|
@ -4,6 +4,8 @@ layout: project
|
||||
excerpt: 3D Printed Lamp Shades
|
||||
permalink: /projects/lamps
|
||||
assets: /assets/projects/lamps
|
||||
date: 2023-01-01
|
||||
draft: true
|
||||
|
||||
img:
|
||||
alt: A CAD model of a 3D printable mount for a common LED light onto a helmet.
|
||||
|
@ -125,10 +125,16 @@ hr.heading {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: 2em;
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
height: 0;
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
hr.byline {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.5em;
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
// Used for both blog and project summaries
|
||||
@mixin time-text {
|
||||
font-size: 0.75em;
|
||||
@ -163,10 +169,7 @@ section.byline-time {
|
||||
}
|
||||
}
|
||||
|
||||
hr.byline {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
section.byline {
|
||||
font-style: italic;
|
||||
|
72
assets/js/expand_img_tags.js
Normal file
72
assets/js/expand_img_tags.js
Normal file
@ -0,0 +1,72 @@
|
||||
// Add this style block once to your document (or in a CSS file)
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.fullscreen-overlay {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fullscreen-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Create and style fullscreen overlay container
|
||||
const fullscreenContainer = document.createElement("div");
|
||||
fullscreenContainer.className = "fullscreen-overlay";
|
||||
fullscreenContainer.style.position = "fixed";
|
||||
fullscreenContainer.style.top = 0;
|
||||
fullscreenContainer.style.left = 0;
|
||||
fullscreenContainer.style.width = "100vw";
|
||||
fullscreenContainer.style.height = "100vh";
|
||||
fullscreenContainer.style.background = "var(--theme-bg-color)";
|
||||
fullscreenContainer.style.display = "flex";
|
||||
fullscreenContainer.style.alignItems = "center";
|
||||
fullscreenContainer.style.justifyContent = "center";
|
||||
fullscreenContainer.style.zIndex = 9999;
|
||||
fullscreenContainer.style.cursor = "zoom-out";
|
||||
|
||||
const fullscreenImage = document.createElement("img");
|
||||
fullscreenImage.style.maxWidth = "90vw";
|
||||
fullscreenImage.style.maxHeight = "90vh";
|
||||
fullscreenImage.style.boxShadow = "0 0 20px rgba(0,0,0,0.8)";
|
||||
fullscreenContainer.appendChild(fullscreenImage);
|
||||
|
||||
document.body.appendChild(fullscreenContainer);
|
||||
|
||||
// Fullscreen API helpers
|
||||
function enterFullscreen(element) {
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function exitFullscreen() {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// Attach listeners
|
||||
document.querySelectorAll("img").forEach((img) => {
|
||||
img.style.cursor = "zoom-in";
|
||||
img.addEventListener("click", () => {
|
||||
console.log("Image clicked:", img.src);
|
||||
fullscreenImage.src = img.src;
|
||||
fullscreenContainer.classList.add("active");
|
||||
enterFullscreen(fullscreenContainer);
|
||||
});
|
||||
});
|
||||
|
||||
fullscreenContainer.addEventListener("click", () => {
|
||||
console.log("Exiting fullscreen");
|
||||
fullscreenContainer.classList.remove("active");
|
||||
exitFullscreen();
|
||||
});
|
323
assets/js/goat_count.js
Normal file
323
assets/js/goat_count.js
Normal file
@ -0,0 +1,323 @@
|
||||
// GoatCounter: https://www.goatcounter.com
|
||||
// This file is released under the ISC license: https://opensource.org/licenses/ISC
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
if (window.goatcounter && window.goatcounter.vars)
|
||||
// Compatibility with very old version; do not use.
|
||||
window.goatcounter = window.goatcounter.vars;
|
||||
else window.goatcounter = window.goatcounter || {};
|
||||
|
||||
// Load settings from data-goatcounter-settings.
|
||||
var s = document.querySelector("script[data-goatcounter]");
|
||||
if (s && s.dataset.goatcounterSettings) {
|
||||
try {
|
||||
var set = JSON.parse(s.dataset.goatcounterSettings);
|
||||
} catch (err) {
|
||||
console.error("invalid JSON in data-goatcounter-settings: " + err);
|
||||
}
|
||||
for (var k in set)
|
||||
if (
|
||||
[
|
||||
"no_onload",
|
||||
"no_events",
|
||||
"allow_local",
|
||||
"allow_frame",
|
||||
"path",
|
||||
"title",
|
||||
"referrer",
|
||||
"event",
|
||||
].indexOf(k) > -1
|
||||
)
|
||||
window.goatcounter[k] = set[k];
|
||||
}
|
||||
|
||||
var enc = encodeURIComponent;
|
||||
|
||||
// Get all data we're going to send off to the counter endpoint.
|
||||
var get_data = function (vars) {
|
||||
var data = {
|
||||
p: vars.path === undefined ? goatcounter.path : vars.path,
|
||||
r: vars.referrer === undefined ? goatcounter.referrer : vars.referrer,
|
||||
t: vars.title === undefined ? goatcounter.title : vars.title,
|
||||
e: !!(vars.event || goatcounter.event),
|
||||
s: [
|
||||
window.screen.width,
|
||||
window.screen.height,
|
||||
window.devicePixelRatio || 1,
|
||||
],
|
||||
b: is_bot(),
|
||||
q: location.search,
|
||||
};
|
||||
|
||||
var rcb, pcb, tcb; // Save callbacks to apply later.
|
||||
if (typeof data.r === "function") rcb = data.r;
|
||||
if (typeof data.t === "function") tcb = data.t;
|
||||
if (typeof data.p === "function") pcb = data.p;
|
||||
|
||||
if (is_empty(data.r)) data.r = document.referrer;
|
||||
if (is_empty(data.t)) data.t = document.title;
|
||||
if (is_empty(data.p)) data.p = get_path();
|
||||
|
||||
if (rcb) data.r = rcb(data.r);
|
||||
if (tcb) data.t = tcb(data.t);
|
||||
if (pcb) data.p = pcb(data.p);
|
||||
return data;
|
||||
};
|
||||
|
||||
// Check if a value is "empty" for the purpose of get_data().
|
||||
var is_empty = function (v) {
|
||||
return v === null || v === undefined || typeof v === "function";
|
||||
};
|
||||
|
||||
// See if this looks like a bot; there is some additional filtering on the
|
||||
// backend, but these properties can't be fetched from there.
|
||||
var is_bot = function () {
|
||||
// Headless browsers are probably a bot.
|
||||
var w = window,
|
||||
d = document;
|
||||
if (w.callPhantom || w._phantom || w.phantom) return 150;
|
||||
if (w.__nightmare) return 151;
|
||||
if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
|
||||
return 152;
|
||||
if (navigator.webdriver) return 153;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Object to urlencoded string, starting with a ?.
|
||||
var urlencode = function (obj) {
|
||||
var p = [];
|
||||
for (var k in obj)
|
||||
if (
|
||||
obj[k] !== "" &&
|
||||
obj[k] !== null &&
|
||||
obj[k] !== undefined &&
|
||||
obj[k] !== false
|
||||
)
|
||||
p.push(enc(k) + "=" + enc(obj[k]));
|
||||
return "?" + p.join("&");
|
||||
};
|
||||
|
||||
// Show a warning in the console.
|
||||
var warn = function (msg) {
|
||||
if (console && "warn" in console) console.warn("goatcounter: " + msg);
|
||||
};
|
||||
|
||||
// Get the endpoint to send requests to.
|
||||
var get_endpoint = function () {
|
||||
var s = document.querySelector("script[data-goatcounter]");
|
||||
if (s && s.dataset.goatcounter) return s.dataset.goatcounter;
|
||||
return goatcounter.endpoint || window.counter; // counter is for compat; don't use.
|
||||
};
|
||||
|
||||
// Get current path.
|
||||
var get_path = function () {
|
||||
var loc = location,
|
||||
c = document.querySelector('link[rel="canonical"][href]');
|
||||
if (c) {
|
||||
// May be relative or point to different domain.
|
||||
var a = document.createElement("a");
|
||||
a.href = c.href;
|
||||
if (
|
||||
a.hostname.replace(/^www\./, "") ===
|
||||
location.hostname.replace(/^www\./, "")
|
||||
)
|
||||
loc = a;
|
||||
}
|
||||
return loc.pathname + loc.search || "/";
|
||||
};
|
||||
|
||||
// Run function after DOM is loaded.
|
||||
var on_load = function (f) {
|
||||
if (document.body === null)
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
function () {
|
||||
f();
|
||||
},
|
||||
false
|
||||
);
|
||||
else f();
|
||||
};
|
||||
|
||||
// Filter some requests that we (probably) don't want to count.
|
||||
goatcounter.filter = function () {
|
||||
if (
|
||||
"visibilityState" in document &&
|
||||
document.visibilityState === "prerender"
|
||||
)
|
||||
return "visibilityState";
|
||||
if (!goatcounter.allow_frame && location !== parent.location)
|
||||
return "frame";
|
||||
if (
|
||||
!goatcounter.allow_local &&
|
||||
location.hostname.match(
|
||||
/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/
|
||||
)
|
||||
)
|
||||
return "localhost";
|
||||
if (!goatcounter.allow_local && location.protocol === "file:")
|
||||
return "localfile";
|
||||
if (localStorage && localStorage.getItem("skipgc") === "t")
|
||||
return "disabled with #toggle-goatcounter";
|
||||
return false;
|
||||
};
|
||||
|
||||
// Get URL to send to GoatCounter.
|
||||
window.goatcounter.url = function (vars) {
|
||||
var data = get_data(vars || {});
|
||||
if (data.p === null)
|
||||
// null from user callback.
|
||||
return;
|
||||
data.rnd = Math.random().toString(36).substr(2, 5); // Browsers don't always listen to Cache-Control.
|
||||
|
||||
var endpoint = get_endpoint();
|
||||
if (!endpoint) return warn("no endpoint found");
|
||||
|
||||
return endpoint + urlencode(data);
|
||||
};
|
||||
|
||||
// Count a hit.
|
||||
window.goatcounter.count = function (vars) {
|
||||
var f = goatcounter.filter();
|
||||
if (f) return warn("not counting because of: " + f);
|
||||
var url = goatcounter.url(vars);
|
||||
if (!url) return warn("not counting because path callback returned null");
|
||||
|
||||
if (!navigator.sendBeacon(url)) {
|
||||
// This mostly fails due to being blocked by CSP; try again with an
|
||||
// image-based fallback.
|
||||
var img = document.createElement("img");
|
||||
img.src = url;
|
||||
img.style.position = "absolute"; // Affect layout less.
|
||||
img.style.bottom = "0px";
|
||||
img.style.width = "1px";
|
||||
img.style.height = "1px";
|
||||
img.loading = "eager";
|
||||
img.setAttribute("alt", "");
|
||||
img.setAttribute("aria-hidden", "true");
|
||||
|
||||
var rm = function () {
|
||||
if (img && img.parentNode) img.parentNode.removeChild(img);
|
||||
};
|
||||
img.addEventListener("load", rm, false);
|
||||
document.body.appendChild(img);
|
||||
}
|
||||
};
|
||||
|
||||
// Get a query parameter.
|
||||
window.goatcounter.get_query = function (name) {
|
||||
var s = location.search.substr(1).split("&");
|
||||
for (var i = 0; i < s.length; i++)
|
||||
if (s[i].toLowerCase().indexOf(name.toLowerCase() + "=") === 0)
|
||||
return s[i].substr(name.length + 1);
|
||||
};
|
||||
|
||||
// Track click events.
|
||||
window.goatcounter.bind_events = function () {
|
||||
if (!document.querySelectorAll)
|
||||
// Just in case someone uses an ancient browser.
|
||||
return;
|
||||
|
||||
var send = function (elem) {
|
||||
return function () {
|
||||
goatcounter.count({
|
||||
event: true,
|
||||
path: elem.dataset.goatcounterClick || elem.name || elem.id || "",
|
||||
title:
|
||||
elem.dataset.goatcounterTitle ||
|
||||
elem.title ||
|
||||
(elem.innerHTML || "").substr(0, 200) ||
|
||||
"",
|
||||
referrer:
|
||||
elem.dataset.goatcounterReferrer ||
|
||||
elem.dataset.goatcounterReferral ||
|
||||
"",
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
Array.prototype.slice
|
||||
.call(document.querySelectorAll("*[data-goatcounter-click]"))
|
||||
.forEach(function (elem) {
|
||||
if (elem.dataset.goatcounterBound) return;
|
||||
var f = send(elem);
|
||||
elem.addEventListener("click", f, false);
|
||||
elem.addEventListener("auxclick", f, false); // Middle click.
|
||||
elem.dataset.goatcounterBound = "true";
|
||||
});
|
||||
};
|
||||
|
||||
// Add a "visitor counter" frame or image.
|
||||
window.goatcounter.visit_count = function (opt) {
|
||||
on_load(function () {
|
||||
opt = opt || {};
|
||||
opt.type = opt.type || "html";
|
||||
opt.append = opt.append || "body";
|
||||
opt.path = opt.path || get_path();
|
||||
opt.attr = opt.attr || {
|
||||
width: "200",
|
||||
height: opt.no_branding ? "60" : "80",
|
||||
};
|
||||
|
||||
opt.attr["src"] =
|
||||
get_endpoint() + "er/" + enc(opt.path) + "." + enc(opt.type) + "?";
|
||||
if (opt.no_branding) opt.attr["src"] += "&no_branding=1";
|
||||
if (opt.style) opt.attr["src"] += "&style=" + enc(opt.style);
|
||||
if (opt.start) opt.attr["src"] += "&start=" + enc(opt.start);
|
||||
if (opt.end) opt.attr["src"] += "&end=" + enc(opt.end);
|
||||
|
||||
var tag = { png: "img", svg: "img", html: "iframe" }[opt.type];
|
||||
if (!tag) return warn("visit_count: unknown type: " + opt.type);
|
||||
|
||||
if (opt.type === "html") {
|
||||
opt.attr["frameborder"] = "0";
|
||||
opt.attr["scrolling"] = "no";
|
||||
}
|
||||
|
||||
var d = document.createElement(tag);
|
||||
for (var k in opt.attr) d.setAttribute(k, opt.attr[k]);
|
||||
|
||||
var p = document.querySelector(opt.append);
|
||||
if (!p) return warn("visit_count: append not found: " + opt.append);
|
||||
p.appendChild(d);
|
||||
});
|
||||
};
|
||||
|
||||
// Make it easy to skip your own views.
|
||||
if (location.hash === "#toggle-goatcounter") {
|
||||
if (localStorage.getItem("skipgc") === "t") {
|
||||
localStorage.removeItem("skipgc", "t");
|
||||
alert("GoatCounter tracking is now ENABLED in this browser.");
|
||||
} else {
|
||||
localStorage.setItem("skipgc", "t");
|
||||
alert(
|
||||
"GoatCounter tracking is now DISABLED in this browser until " +
|
||||
location +
|
||||
" is loaded again."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!goatcounter.no_onload)
|
||||
on_load(function () {
|
||||
// 1. Page is visible, count request.
|
||||
// 2. Page is not yet visible; wait until it switches to 'visible' and count.
|
||||
// See #487
|
||||
if (
|
||||
!("visibilityState" in document) ||
|
||||
document.visibilityState === "visible"
|
||||
)
|
||||
goatcounter.count();
|
||||
else {
|
||||
var f = function (e) {
|
||||
if (document.visibilityState !== "visible") return;
|
||||
document.removeEventListener("visibilitychange", f);
|
||||
goatcounter.count();
|
||||
};
|
||||
document.addEventListener("visibilitychange", f);
|
||||
}
|
||||
|
||||
if (!goatcounter.no_events) goatcounter.bind_events();
|
||||
});
|
||||
})();
|
BIN
assets/projects/downlight_bikelight/img/cad.jpeg
Normal file
BIN
assets/projects/downlight_bikelight/img/cad.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
BIN
assets/projects/downlight_bikelight/img/laid_out_front.jpeg
Normal file
BIN
assets/projects/downlight_bikelight/img/laid_out_front.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 367 KiB |
BIN
assets/projects/downlight_bikelight/img/laid_out_top.jpeg
Normal file
BIN
assets/projects/downlight_bikelight/img/laid_out_top.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 530 KiB |
BIN
assets/projects/downlight_bikelight/img/original_location.jpeg
Normal file
BIN
assets/projects/downlight_bikelight/img/original_location.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 176 KiB |
BIN
assets/projects/downlight_bikelight/models/fbx_export.glb
Normal file
BIN
assets/projects/downlight_bikelight/models/fbx_export.glb
Normal file
Binary file not shown.
@ -45,7 +45,7 @@ Welcome to my little home on the web! Below you'll find recent blog posts, proje
|
||||
<span class="dt-label">Last Modified</span>
|
||||
</section>
|
||||
<hr class="heading">
|
||||
{% assign projects = site.projects | sort_natural: "last_modified_at"%}
|
||||
{% assign projects = site.projects | sort_natural: "date" | reverse %}
|
||||
{% for post in projects limit:5 %}
|
||||
{% if post.draft == false or jekyll.environment == "development" %}
|
||||
{% include project_summary.html %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user