L

leanrada.com notes

leanrada.com notes

Editing my website directly in the browser

This website is editable! If you add ?edit to any URL on this site, it activates edit mode. In this mode, you can edit text and apply basic formatting to any element. Of course, you won’t be able to save changes without having write access to my files, but I can. Using the Web File System API, it can update my local copy of the website’s source code. In short, I can edit my site via my site. In fact, this article was written this way! It’s still rough in some cases, but the solution is usable. But why? As usual, I made this mainly for fun. But having a WYSIWYG (what-you-see-is-what-you-get) editor for my posts has been a long time goal ever since I rewrote my site in vanilla HTML. After I moved away from a static site generator, the need for an extra transformational build step disappeared, allowing the raw HTML source to be directly served in the client. This unlocked a path to source editing in the client! As well as other basic things for free. The HTML file can now act as the editing platform, in addition to being the coding and authoring medium, and of course, the publishing medium. We're sticking with standards with this one. I’ve noticed that this has been the tendency of my personal use of Web tech. Instead of… I use CMS the file system frontmatter read data already in the markup, <meta>, <title>, <time>, <tag-chip>s WYSIWYG editor editable HTML page Advanced editor (?) text editor (edit HTML code) Widgets (??) JavaScript Anyway, these things don’t really impact visitors and readers of this site. It’s more about my writing and authoring experience. Thoughts can now go directly into the page the way it’s gonna be read. I already felt a bit of that improved flow when I removed the build step and added a live reload script, minimising the development feedback loop. With direct editing, feedback loop is instant — no more switching between the text editor and the browser! It’s been extremely valuable during the proofreading and editing phase. Content, editable Browsers support editing HTML natively via the contenteditable attribute. Set this attribute on any element and it becomes editable. It’s a barebones way to get a WYSIWYG editor in HTML. This is what I’m using as of time of writing. contenteditable formatting options are limited though, and browser implementations vary. Not a problem for now. Whether contenteditable was good or terrible was a minor detail in the bigger picture. The real problem was the Document Object Model… DOM vs source When I said no build step I kinda lied. You see, there's still an inherent compilation step that happens inside the browser. This step is the parsing of HTML into the Document Object Model, or the DOM. The “DOM” essentially are the elements that you actually interact with on a webpage. Technically, the DOM is the interface to the elements themselves. For general purposes, the interface is the abstraction. The contenteditable editor edits the DOM, not the HTML source code itself. I needed to somehow save the corresponding DOM edits into the source. It was not very straightforward. HTML to DOM conversion is one way. DOM to source HTML would be another thing. And no, innerHTML/outerHTML is not the exact inverse operation. For example, simply saving html.outerHTML to a file won’t work, because outerHTML (or any DOM introspection method, for that matter) takes a snapshot of the current state of the DOM, including dynamically rendered elements. This would break interactive elements and custom elements in the source, as the rendered elements would incorrectly overwrite the original markup. Saving the generated outerHTML directly would also mess up any manual formatting that I make in the source code, collapsing whitespaces and forcing HTML entities. (I don’t use Prettier; it produces invalid HTML) Solution: source patching So I made an algorithm to patch the source according to changes made in the DOM. What it does is listen for changes in the DOM tree, and tries to identify the ‘smallest’ textual patch needed to update the source. It starts with the MutationObserver API. MutationObserver lets the code respond to changes in the DOM. For each change, the API provides the relevant node(s), as well as the two surrounding unchanged siblings. Btw, the MutationObserver API has been somewhat unwieldy. It only provides coarse details about each change, not like a detailed diff. Also, a single user interaction (say, pressing the Enter key) could cause multiple mutations to fire (the Enter key causes a <p> to be inserted, then a <br> inside the p). Some mutations also end up cancelling each other in the same callback or across multiple callbacks (e.g. span that is added and removed immediately). The exact order and amount of effects also vary by browser. My workaround was to collapse multiple mutations into one logical change per resulting subtree. The idea is to update the relevant part of the source without changing the rest. To do this, it finds two anchor points around the mutation. These two points must remain stable throughout the mutation in both DOM and source. Only the content between them are replaced. So for each change, the steps are: Find the unchanged element before the change. Find the unchanged element after the change. Use their positions in source as boundaries. Splice the source between the two boundaries with the updated HTML. Then there’s the subproblem of mapping the position of the boundary elements in the source code. Extracting the position of any given DOM element in the source code is a bit more involved: Get the element’s ‘address’, or its path in the DOM (unique steps from the root to the element). Scan the source HTML, tag by tag, keeping track of your current ‘address’ by identifying opening and closing tags. Compare the current tag’s path-in-source against the element’s path-in-DOM. If paths match, then the current tag corresponds to the element! getPositionInSource pseudocode /** Gets the position of the given Element in the source code */ getPositionInSource(element: Element, source: string) { // Get the DOM path of the target domPath: Element = []; while (element) { domPath.unshift(element); element = element.parentElement; } let startIndex, endIndex; sourcePath = []; // match all HTML tags for (tag of source.matchAll(/<\/?[a-z0-9-]+/gi)) { // Note: does not handle void elements and optional end tags if (isOpenTag(tag)) { sourcePath.push(tag); if (matchPath(domPath, sourcePath)) { // Source path and DOM path matches! // This is the target element's opening tag startIndex = tag.index; } } else { // is a closing tag if (matchPath(domPath, sourcePath)) { // Source path and DOM path matches! // This is the target element's closing tag endIndex = source.indexOf(">", tag.index); return { startIndex, endIndex }; } sourcePath.pop(); } } } Handling void elements (e.g. <input>) and optional end tags (e.g. <li>s with no end tags) require a bit more logic to keep the current path accurate, but the main idea remains the same. The main algorithm can be described as basically a contextual find-and-replace. Instead of searching for the text to be replaced, it finds the surrounding text and replaces the content in the middle. This preserves the source code’s original formatting and avoids overwriting dynamic elements! Yay! There’s still a lot of room for improvement. My current issue is that it’s converting nearby HTML entities to rendered versions (e.g. '&hellip;' to '…'). I think I’d need to implement a more fine-grained editing within an individual text node to solve that. There are probably a bunch more edges cases that I haven’t discovered, but hey it works well enough to write the text you’re reading! You can check out the code for the edit mode for more details! Note: This script was made specifically for myself and my site. Thoughts The algorithm doesn’t have to be perfect; I can always fall back to editing the source if certain things don’t work. A good thing about this is that I can switch to either method any time because in the end I'm working on the same HTML file. When I first imagined this solution, I thought I would only be using it to do the final edits just before publishing a post, but here I am actually writing a whole post. I can now imagine a workflow where I write the main text in the browser, then go into the HTML to insert media, mark up code blocks, custom elements, and other formatting. And of course, I’d use a code editor to code the interactive sections that I enjoy adding to my posts. In the future, think I’d like to add more formatting options and UI for inserting code blocks, media, lists, headings, etc. Maybe use Quill or something? The top thing I would love to have is inserting hyperlinks. During final passes on a post, I like to sprinkle hyperlinks on interesting terms, even if just to link the Wikipedia entry. I’ll post about any improvements I make along the way. This could be a series! I mean, it already kind of is: Rewriting my site in vanilla web Simple live reload for developing static sites 📍(this post) [future formatting feature?] [edit CSS? JS?]

2025/9/1
articleCard.readMore

Making my GitHub heatmap widget

For RSS readers: This article contains interactive content available on the original post on leanrada.com. This post is about how I made the GitHub heatmap widget on my site. Here’s the raw, live WebComponent <gh-contribs> by the way: Interactive content: Visit the post to interact with this content. Alternative name: Gh Contribs Scraping the data First, I had to scrape the heatmap data. I don’t know if there’s a proper API, but I found an endpoint that renders an HTML partial of what I wanted. As far as I know, the GitHub website today works using partial HTMLs to update its UI without reloading the whole page. I think this endpoint populates the contribution graph section of the profile page. The endpoint that returns a user’s contribution graph is https://github.com/users/{username}/contributions. I presume the user has to have contribution stats public. This undocumented API could also break at any time. 😬 The response HTML for my github.com/users/Kalabasa/contributions Loading this endpoint gives you an unstyled piece of HTML containing an HTML table of contribution data and other UI. The table cells are invisible because of the lack of styles! When embedded in the profile page, it inherits the appropriate styling in context. The first column is the weekday label, and the rest of the cells seem to represent a single day each. The data is encoded in the HTML that presents the data! This reminds me of Hypermedia as the Engine of Application State. html = data. <tbody> <tr style="height: 10px"> <td class="ContributionCalendar-label" style="position: relative"> <span class="sr-only">Monday</span> <span aria-hidden="true" style="clip-path: None; position: absolute; bottom: -3px"> Mon </span> </td> <td tabindex="0" data-ix="0" aria-selected="false" aria-describedby="contribution-graph-legend-level-2" style="width: 10px" data-date="2024-08-05" id="contribution-day-component-1-0" data-level="2" role="gridcell" data-view-component="true" class="ContributionCalendar-day"> </td> <td tabindex="0" data-ix="1" aria-selected="false" aria-describedby="contribution-graph-legend-level-1" style="width: 10px" data-date="2024-08-12" id="contribution-day-component-1-1" data-level="1" role="gridcell" data-view-component="true" class="ContributionCalendar-day"> </td> <td tabindex="0" data-ix="2" aria-selected="false" aria-describedby="contribution-graph-legend-level-2" style="width: 10px" data-date="2024-08-19" id="contribution-day-component-1-2" data-level="2" role="gridcell" data-view-component="true" class="ContributionCalendar-day"> </td> What I was looking for here was the data-level attribute on each cell. It contains a coarse integer value that indicates the activity level for the day. Coupled with the data-date attribute, it became rather easy to scrape this data! Instead of keeping track of columns and rows, I just go through each data-date and data-level as a (date,level) data point. Here’s my parse function using cheerio, a jQuery clone for Node.js. const res = await fetch( "https://github.com/users/Kalabasa/contributions"); let data = parseContribs(await res.text()); /** * Parses a GitHub contribution calendar HTML string and extracts contribution data. * * @param {string} html - The HTML string containing the GitHub contribution calendar. * @returns {{ date: Date, level: number }[]} Array of contribution objects with date and activity level. * @throws {Error} If the contribution calendar table cannot be found in the HTML. */ function parseContribs(html) { const ch = cheerio.load(html); const chTable = ch("table.ContributionCalendar-grid"); if (!chTable.length) throw new Error("Can't find table."); const chDays = chTable.find("[data-date]"); const data = chDays .map((_, el) => { const chDay = ch(el); const date = new Date(chDay.attr("data-date")); const level = parseInt(chDay.attr("data-level"), 10); return { date, level }; }) .get(); return data; } // gh-contribs.json [ [1,2,1,2,2,1,1], [0,1,0,2,0,1,0], [1,1,1,1,1,1,0], [0,1,0,0,1,1,0], [0,0,1,0,0,0] ] Rendering the data The reason why the data is reformatted into a grid like that is to make the rendering logic straightforward. The data is structured so that it can be directly converted into HTML without thinking in dates and weeks that are in the original data. Here are the current JSON and WebComponent side by side. Each row in the data gets directly rendered as a column in the component. Interactive content: Visit the post to interact with this content. Alternative name: Inline script Interactive content: Visit the post to interact with this content. Alternative name: Gh Contribs As such, <gh-contribs>’s initialisation logic is really simple: const contribs = await fetch( "path/to/gh-contribs.json" ).then((res) => res.json()); let htmlString = ""; for (const col of contribs) { for (const level of col) { htmlString += html`<div data-level="${level}">${level}</div>`; } } this.innerHTML = htmlString; gh-contribs { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, auto); gap: 12px; div { position: relative; width: 18px; height: 18px; background: #222c2c; color: transparent; &::after { content: ""; position: absolute; inset: 0; background: #54f8c1; } &[data-level="0"]::after { opacity: 0; } &[data-level="1"]::after { opacity: 0.3; } &[data-level="2"]::after { opacity: 0.6; } &[data-level="3"]::after { opacity: 1; } } } Why not use the original HTML? Why not just embed the contributions HTML from GitHub? Slice the relevant <tr>s and <td>s…? Why parse the original HTML table, convert it to JSON, then render it as HTML again? The main reason to do [HTML → JSON → HTML] is to remain flexible. As you know, that endpoint is undocumented. Also, depending on the HTML structure of the original is risky. Risk of breakage, risk of unwanted content, etc. This way, I can change how I get the data without refactoring the WebComponent. I could go [GitHub API → JSON → HTML] or [local git script → JSON → HTML] or whatever. It also works the other end. I actually rewrote this widget recently (from statically-generated HTML into a WebComponent) without having to change the scraper script or the JSON data structure. Final touches The WebComponent renders just the grid itself for flexibility. This let me use it in different ways, like with an icon and heading as in the home page. heatmap Interactive content: Visit the post to interact with this content. Alternative name: Gh Contribs Here’s the source code for this WebComponent if you’re interested.

2025/8/4
articleCard.readMore

Vibecoding Lima Gang, web-based map visualization

I made Lima Gang, a map-based visualization, over a weekend using ‘vibecoding’. Lima what? lima means five in several languages particularly in island nations in Southeast Asia and the Pacific. These are part of the Austronesian language family that spans several seas from Madagascar to Indonesia, to Taiwan, to Hawaii. Extent of Austronesian peoples. They must’ve been good with boats. Source: Wikimedia The Lima Gang is a meme based on the fact that the word lima seemed to have survived intact, echoed in various tongues, and now somehow uniting people across vast oceans. This is more than a meme, Unicode even gave recognition to the Lima Gang by including its gang sign as an official emoji: 🖐🏽 Jokes aside, I’m posting this to share a vibecoding experience. Vibecoding This small one-off app that I knew almost exactly how to make is a perfect case for some vibecoding practice. My initial thoughts were, it was going to be mostly boilerplate HTML code, some standard map UI code, and some standard visualization code. There are hundreds of map visualization web pages just like this, nothing too novel here. I used ChatGPT to generate about 80-90% of the code. First, I had it generate the basic UI. It gave me a basic Leaflet-based map. Basic Leaflet map. It’s good to start with the basics. From my brief experience, it’s better to implement small chunks at a time than bigger integrated pieces. I didn’t like the look of OpenStreetMap, and I know Carto has some nice-looking maps, so I asked ChatGPT to switch to Carto. I remember having to manually tweak the tile layer API URL to get the specific style that I wanted: {s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png. This was the first time I looked into any documentation for the components that I was using. Skipping the friction of going through documentation to get started with an API or a library was great for building up momentum at the start of a project, and I think LLMs are good with that initial scaffolding, but looking up documentaton is still necessary at some point. Getting the data The main source of data is this Wiktionary entry on ‘lima’. I asked ChatGPT to create a script that scrapes this page. It should look for all entries where ‘lima’ means ‘five’, and list the corresponding languages in JSON format. A couple of iterations later, it gave me a Python script that uses the Wiktionary REST API. # ... url = "https://en.wiktionary.org/api/rest_v1/page/definition/lima" response = requests.get(url) data = response.json() language_map = {} for section, entries in data.items(): for entry in entries: language = entry["language"] entry_definition_htmls = entry.get("definitions", []) entry_definitions = {extract_definition(d.get("definition", "")) for d in entry_definition_htmls} entry_definitions.discard(None) if language not in language_map: language_map[language] = {"name": language, "definitions": set()} language_map[language]["definitions"].update(entry_definitions) # ... Again, not having to look these APIs up was a great time-saver. And I don’t usually code in Python, so it would’ve taken me time to review sets and list comprehensions otherwise. I did have to go into the REST API and the Python code because of increasingly complex requirements, like getting secondary meanings, and massaging the data. ChatGPT wasn’t really able to dive in and iterate on these more complex requirements. In the end I got this JSON file, the list of languages where ‘lima’ means 5: [ { "name": "Agutaynen" }, { "name": "Bariai" }, { "name": "Cia-Cia", "otherMeanings": ["hand"] }, { "name": "Dibabawon Manobo" }, { "name": "East Futuna" }, { "name": "Gela" }, { "name": "Hiligaynon", "otherMeanings": ["hand", "handle", "lime"] }, // ... ] I still needed to map this data geographically, so I asked ChatGPT to assign geographic IDs for the purpose of rendering these on the map. It’s basically a fill-in-the-blank exercise for ChatGPT. I think even spreadsheet apps have this kind of AI autocomplete now. Language Geo code Agutaynen PHL [Philippines] Bariai PNG [Papua New Guinea] Cia-Cia IDN [Indonesia] Dibabawon Manobo ? East Futuna Gela Hiligaynon This is where I leaned a bit into the vibecoding vibes, because I have no scalable way to verify this data. I can verify code and logic, but I’m no linguist. I relied on ChatGPT’s knowledge and/or search capabilities to map the languages to where they are spoken. To instill some confidence, I created a new thread and asked it to ‘fact-check’ the data it generated in a previous instance. Of course, it proceeded to ignore my second instruction in its very first bullet point, but it actually gave me a good number of issues to double-check. The Lusi issue was hallucinated, but the rest seemed reasonable. The Hawaiian and Tokelauan cases were actually good points to raise, and I had to add special handling for those later. I think a safer way to go about it is to use the LLM to map the language name to language code, then use a database to lookup country codes from language codes. This way the LLM would just be doing a syntactic transformation, which it should be good at, leaving the accuracy-sensitive job of identifying the appropriate country to a deterministic database lookup. Maybe there’d be no need for an LLM at all if I got the language code from the initial Wiktionary scrape in the first place. 🤔 I also asked ChatGPT to fetch GeoJSON data needed to render these geographic areas on the map. It hallucinated some GeoJSON datasets like raw.githubusercontent.com/david/world/b1b8704/data/country.geojson. In the end, I had to search for some of the GeoJSON data myself. Map with GeoJSON layer highlighting the identified areas. There were a few special cases regarding geographic areas as hinted at earlier. These are the state of Hawaii and some territories of New Zealand. These aren’t countries themselves, and it didn’t make sense to highlight the whole USA to represent the Hawaiian language, or all of New Zealand for the Tokelauan language. ChatGPT didn’t like that I was using both countries and states and territories in the same dataset. I had to implement these special cases mostly manually, only giving really small isolated instructions to ChatGPT. Interactivity I asked it to generate popups on hover per geographic area to show the data in more detail. It added code using Leaflet tooltips. Another cool API I didn’t know beforehand. Overall pretty straightforward. Map tooltips on hover. That’s most of it. I did the CSS polishing bits and mobile responsiveness. I don’t feel like ChatGPT would have an eye for this as a language model. Maybe it’s multimodal now, but I still don’t trust it for visual things. In the end, what happened wasn’t technically pure vibecoding as in the original definition. It started out that way, but I saw some pretty wrong code and and took the wheel at some point. Unfortunately I don’t possess the blissful gift of ignorance. Opinions From this experience, I can say that ChatGPT (or LLMs in general?) can get you started on tiny apps really quickly. That initial phase of writing boilerplate, finding the right libraries, and setting it all up can be automated away. But, it’s still not quite reliable enough to go bigger or deeper without proper direction. I don’t think this is a groundbreaking observation, many others have noted similar strengths and weaknesses of LLMs for coding. To be fair, I didn’t use Cursor or any kind of coding agent. I have some experience with Cursor at work, and while it is significanly more capable than the general chat interface, still I doubt it could create the whole app in a single ask. It likely would require the same directed iteration in chunks as with a general chat interface, just maybe faster and in slightly bigger chunks. I think for successful AI-assisted coding, you must have some kind of an implementation plan in mind, a breakdown of components and subcomponents to implement and integrate (just like in real life). In my case, I didn’t really have an explicit plan, but I had a sense of which bite-sized chunks to implement and iterate on. I’ve seen some practices where the agent generates that plan itself, and iterates on it in small chunks. Like making a todo list for itself then completing that. I think that would actually go further, and with less supervision. But you still have to review and know what it’s doing at any point so you can course-correct in case it goes off track, which happens often. In conclusion: I guess you have to know what you’re doing.

2025/6/13
articleCard.readMore

My own RSS reader

I started making my own RSS reader webapp (a what reader?). It can be accessed at /tools/rss/. Currently it’s hardcoded to just load my own site feed leanrada.com/rss.xml and that’s totally fine. I made this primarily to test how my own posts get rendered in ‘reader mode’. Reader mode You see, I like to incorporate interactive elements in my posts. However, custom elements and JS are scrubbed by RSS reader apps, ultimately resulting in a clean but confusing and broken experience. Even custom CSS which I use to format some graphs and illustrations are lost. The same could be said for the Reading mode feature offered by most full-fledged web browsers today, if not their add-ons. It’s a feature that strips ‘distracting’ elements away. My own RSS reader actually uses the same library used by Mozilla Firefox’s Reader View. My barebones RSS reader showing a reader mode transformation of posts. But many of my posts are not meant to be just for reading. I want my posts to be interactive and playable. A reading mode transformation is inherently incompatible. Still, I wanted to provide an RSS feed. I had to compromise. An easy way out would be to omit the post body in the feed, maybe put in a summary instead, and just link to the original URL like what other feeds do. The BBC news feed does this, for example. But that’s boring. An alt text kind of way I went with an alt text kind of way. That is, all interactive content are replaced by alternative plain text in the feed body. Original content Converted content Right now it’s pretty crude — my HTML to RSS converter scans custom tags in the markup, and replaces them with the tag name in sentence case. So <some-demo></some-demo> would turn into Some demo. It also tries to look for alt and aria-label attributes if present. It’s not really a complete solution since it doesn’t cover other stuff like plain DOM-based JS stuff (non-WebComponents). I don’t think RSS readers will start running third-party JS or WebComponents anytime soon, so this is not going to be a temporary solution either. The idea for the RSS reader is that it will help me continuously test these conversions, and remind myself to polish the HTML to RSS solution. Prelude prepended to the post body when there is interactive content in the post. Beyond just a testing tool, this RSS reader is, well, an RSS reader! It is a feed reader! Now that I’ve made an actual RSS reader, it’d be trivial to add other feeds and use this as my main reader app! I’ve always wanted to make my own feed reader. I’m currently using an online service InoReader to subscribe to and read around 123 feeds in total. I could potentially switch to my own reader…? I should probably start with OPML import functionality. It would be a nice project, at least. Actually, more than a feed reader, I’ve always wanted to make my own personal front page containing not just RSS feeds, but also social media posts and other stuff I follow all in one place. So many project ideas… 🤔

2025/5/15
articleCard.readMore

Simple live reload for developing static sites

When developing my website, I’m using a simple client-side script to automatically reload the page whenever I make a change to the source files. Since it’s not coupled to any particular backend, I could continue using python3 -m http.server -d ./site/ or whatever local web server I wanted and it would still work. I could clone this repo on a new machine and get going with only preinstalled programs: a text editor, a browser, and a Python (or whatever) HTTP server. And live reload should* just work. Here’s the code (39 lines): let watching = new Set(); watch(location.href); new PerformanceObserver((list) => { for (const entry of list.getEntries()) { watch(entry.name); } }).observe({ type: "resource", buffered: true }); function watch(urlString) { if (!urlString) return; const url = new URL(urlString); if (url.origin !== location.origin) return; if (watching.has(url.pathname)) return; watching.add(url.pathname); console.log("watching", url.pathname); let lastModified, etag; async function check() { const res = await fetch(url, { method: "head" }); const newLastModified = res.headers.get("Last-Modified"); const newETag = res.headers.get("ETag"); if ( (lastModified !== undefined || etag !== undefined) && (lastModified !== newLastModified || etag !== newETag) ) { location.reload(); } lastModified = newLastModified; etag = newETag; } setInterval(check, 1000); } Chuck it into your HTML <script src="https://kalabasa.github.io/simple-live-reload/script.js"></script> It should just work! ✨ *Check the README for more details. How it works, in a nutshell Start PerformanceObserver to watch loaded URLs Poll HEAD metadata Check Last-Modified and ETag PerformanceObserver — This class was intended to measure performance of things like network requests. But in this case it was repurposed to record requested URLs so we can watch them for changes. This includes lazy-loaded resources, and resources not in the markup (e.g. imported JS modules)! HEAD — Upon recording a requested URL, start polling the URL with HEAD HTTP requests to get resource metadata. Frequent polling should be fine if you’re using a local server which is what I would expect for development. The response returned by HEAD contains metadata useful for determining when to refresh. Last-Modified and ETag — These are the headers used to indicate when the underlying resource has changed. The script triggers a location.reload() when any of these change. Story time I was directly inspired by livejs (2004), which polls headers as well. However, it has not been updated for modern browsers. Instead of watching network requests, it scans the markup for <script> and <link> (CSS) resources. In fact, I’ve been using livejs for a long while. I’ve never been fond of the other solutions which require integration with your local filesystem, via extra programs that you install and run. They always seem to run slow or take up lots of resources, and sometimes choke if there are errors. I’m planning to make a fine-grained version of this module. Simple live reloading is fine, but a more advanced hot reloading that doesn't always refresh the whole page would be great. For the modern web, the advanced version must be able to hot reload WebComponents in place, and do other fun stuff. I wonder if it’s even possible. 🤔 GitHub repo: Kalabasa/simple-live-reload

2025/4/18
articleCard.readMore

Language evolves, so we must too. On generative art

A year ago, I stood by the statement that AI Art is not Generative Art, arguing that the term ‘generative art’ is an art movement in itself that is separate from AI art. Safe to say, it has been a losing battle. Even the converse, “generative art is not necessarily AI art”, would be met by confusion from most people. ‘Generative’ has been equated to AI. Fidenza, a generative artwork system, by Tyler Hobbs Exhibition r/generative - Has the word 'generative' been hijacked by AI? I think it's time to move on to a different label. Words are for communication, and if a word results in failure of communication, we should either agree on a common definition (we can’t) or just use better word. So, what are the other words for this art? If you haven’t read the mentioned prequel post, you may not understand what “art” I’m taliking about. There’s one example above, but here’s more. Fortunately, there are already well-established terms to describe the medium: Procedural art Algorithmic art Creative coding It’s just a matter of choosing one and leaving the now-loaded ‘generative’ term behind. By the process of elimination, I think ‘procedural art’ is the best term to use. Let me explain each of the other options. Why not Algorithmic ‘Algorithmic art’ is a top candidate, for sure, and many others have opted for it in the exodus from ‘generative’. Algorithmic art and procedural art are pretty much synonymous, so why or why not? Pros: Unlike generative art, algorithmic art doesn’t have the unfortunate connotation from sharing a word with ‘generative AI’. Cons: While ‘algorithmic art’ doesn’t immediately invoke visions of generative AI, the term ‘algortihm’ under the broad definition of a ‘set of instructions’ would still include machine learning models. In fact, the word ‘algorithm’ has also entered commonplace usage via ‘social media algorithms’, which are yet another application of machine learning and statistics. In this sense, it is used interchangeably with the recommender system used to construct personalised content feeds. “The algorithm” is a black box, and the word suggests a system beyond understanding. This connotation is not in the spirit of procedural art, where the procedures are authored knowingly. Another distinction is that while an algorithm is usually defined as a process that takes some input and processes it into an output, most procedural art programs don’t need inputs at all. They create things from scratch, from maths, from chaos. OK, technically random seeds would be considerend input. But on a conceptual level, no inputs! Why not Creative Coding First of all, it’s such a vague phrase. Why not ‘dynamic programming’? 😉 Anyway, that’s not the main point. Procedural art has grown beyond just coding. I would even say most procedural artists don't use code, but nodes and graphs. The majority of these procedural art would be used in video games and movie VFX. Instead of coding, one would use node-based programming connecting nodes that represent operations, building up a graph to produce a complex image or even a 3D model. Shahriar Shahrabi - Procedural Chinese Landscape Painting in Geometry Nodes And it’s not just for industrial VFX purposes. It’s also used mainly for multimedia, interactive, and installation art. Audiovisual projection by Obscura Digital using TouchDesigner, a node-based software. (Picture from YouTube Symphony Orchestra) So... yeah this genre of art, is not coding. OK, then why Procedural Art Because the ‘procedure’, the ‘how’ is the core feature of procedural art. In procedural art, we describe precisely how the artwork is made. And it’s far from describing to a chatbot in plain English. The how is well-defined and understood. Every shape, stroke, their positions, colours, ranges, constraints, rules of interaction, etc., are all described in some precise way. Sometimes we go down to individual pixel level as in a shader. Sometimes at a more geometric level, like in turtle or vector graphics. Sometimes on a mathematical level, like fractals. Sometimes it’s even a simulation with emergent properties! Procedural art is more about the process, watching things unfold and emerge, rather than the final output that algorithms are obsessed about. Procedural art is about the interaction of rules in interesting ways, regardless of whether the rules were written in code, or connected as a graph, or wired in redstone dust. Finally, procedural art is not AI art. [Insert same exact argument as AI Art is not Generative Art but with a different name, here]. For funsies, here’s a quick guide: In AI art, you describe what the artwork should be. In procedural art, you describe how the artwork could be. What now Language evolves because of general usage. See: AI, crypto, cyber, generative. As a fellow user of language, I must evolve my languaging as well — starting with this website. I have updated the following pages to either disambiguate or even completely replace mentions of “generative art”: /art/ /wares/dimensions/ For the record, I’m not rejecting the other perfectly acceptable terms like algorithmic art and creative coding. My art is still algorithmic, and it’s the result of creative coding. I’m just moving away from ‘generative’. I’m sure we’ll settle on a universal term soon. Language evolves, after all. Appendix Searches for “generative art” over time A visitor has left me this note, which made me rethink

2025/4/3
articleCard.readMore

Minimal CSS-only blurry image placeholders

Here’s a CSS technique that produces blurry image placeholders (LQIPs) without cluttering up your markup — Only a single custom property needed! <img src="…" style="--lqip:192900"> The custom property above gives you this image: Try changing the property’s value (WARNING: FLASHING) { const card = document.currentScript.parentElement; const input = card.querySelector("input"); const code = card.querySelector("code"); const preview = card.querySelector("div"); let currentValueStr = "192900"; let targetCode = null input.addEventListener("input", event => { if (!targetCode) { targetCode = Array.from(code.querySelectorAll("span")).filter(el => el.textContent.includes(currentValueStr)).slice(-1)[0] ?? code; } const lqip = Number(event.currentTarget.value); // use this page's lqip to avoid breakage if I ever update the scheme preview.style.setProperty("--my-lqip", lqip); targetCode.innerHTML = targetCode.innerHTML.replace(currentValueStr, lqip); currentValueStr = String(lqip); }); } Granted, it’s a very blurry placeholder especially in contrast to other leading solutions. But the point is that it’s minimal and non-invasive! No need for wrapper elements or attributes with long strings of data, or JavaScript at all. Note for RSS readers / ‘Reader’ mode clients: This post makes heavy use of CSS-based images. Your client may not support it. Example images Check out the LQIP gallery for examples! Survey of LQIP approaches There have been many different techniques to implement LQIPs (low quality image placeholders), such as a very low resolution WebP or JPEG (beheaded JPEGs even), optimised SVG shape placements (SQIP), and directly applying a discrete cosine transform (BlurHash). Don’t forget good old progressive JPEGs and interlaced PNGs! Canva and Pinterest use solid colour placeholders. At the other end of the spectrum, we have low tech solutions such as a simple solid fill of the image’s average colour. Pure inline CSS solutions have the advantage rendering immediately — even a background-image: url(…a data URL) would be fine! Gradify generates linear-gradients that very roughly approximate the full image. The big disadvantage of pure CSS approaches is that you typically litter your markup with lengthy inline styles or obnoxious data URLs. My handcoded site with no build step would be extra incompatible with this approach! <!-- typical gradify css --> <img width="200" height="150" style=" background: linear-gradient(45deg, #f4a261, transparent), linear-gradient(-45deg, #e76f51, transparent), linear-gradient(90deg, #8ab17d, transparent), linear-gradient(0deg, #d62828, #023047); "> BlurHash is a solution that minimises markup by compressing image data into a short base-83 string, but decoding and rendering that data requires additional JS… <!-- a blurhash markup --> <img width="200" height="150" src="…" data-blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"> BlurHash example Is it possible to decode a blur hash in CSS instead? Decoding in pure CSS Unlike BlurHash, we can’t use a string encoding because there are very few if any string manipulation functions in CSS (2025), so strings are out. In the end, I came up with my own hash / encoding, and the integer type was the best vessel for it. The usual way to encode stuff in a single integer is by bit packing, where you pack multiple numbers in an integer as bits. Amazingly, we can unpack them in pure CSS! To unpack bits, all you need is bit shifting and bit masking. Bit shifting can be done by division and floor operations — calc(x / y) and round(down,n) — and bit masking via the modulo function mod(a,b). * { /* Example packed int: */ /* 0b11_00_001_101 */ --packed-int: 781; --bits-9-10: mod(round(down, calc(var(--packed-int) / 256)), 4); /* 3 */ --bits-7-8: mod(round(down, calc(var(--packed-int) / 64)), 4); /* 0 */ --bits-4-6: mod(round(down, calc(var(--packed-int) / 8)), 8); /* 1 */ --bits-0-3: mod(var(--packed-int), 8); /* 5 */ } Of course, we could also use pow(2,n) instead of hardcoded powers of two. So, a single CSS integer value was going to be the encoding of the “hash” of my CSS-only blobhash (that’s what I’m calling it now). But how much information can we pack in a single CSS int? Side quest: Limits of CSS values The spec doesn’t say anything about the allowed range for int values, leaving the fate of my shenanigans to browser vendors. From my experiments, apparently you can only use integers from -999,999 up to 999,999 in custom properties before you lose precision. Just beyond that limit, we start getting values rounded to tens — 1,234,567 becomes 1,234,560. Which is weird (precision is counted in decimal places!?), but I bet it’s due to historical, Internet Explorer-esque reasons. Anyway, within the range of [-999999, 999999] there are 1,999,999 values. This meant that with a single integer hash, almost two million LQIP configurations could be described. To make calculation easier, I reduced it to the nearest power of two down which is 220. 220 = 1,048,576 < 1,999,999 < 2,097,152 = 221 In short, I had 20 bits of information to encode the CSS-based LQIP hash. Why is it called a “hash”? Because it’s a mapping from an any-size data to a fixed-size value. In this case, there are an infinite number of images of arbitrary sizes, but only 1,999,999 possible hash values. The Scheme With only 20 bits, the LQIP image must be a very simplified version of the full image. I ended up with this scheme: a single base colour + 6 brightness components, to be overlaid on top of the base colour in a 3×2 grid. A rather extreme version of chroma subsampling. This totals 9 numbers to pack into the 20-bit integer: The base colour is encoded in the lower 8 bits in the Oklab colour space. 2 bits for luminance, and 3 bits for each of the a and b coordinates. I’ve found Oklab to give subjectively balanced results, but RGB should work just as well. The 6 greyscale components are encoded in the higher 12 bits — 2 bits each. An offline script was created to compress any given image into this integer format. The script was quite simple: Get the average or dominant colour — there are a lot of libraries that can do that — then resize the image down to 3×2 pixels and get the greyscale values. Here’s my script. I even tried a genetic algorithm to optimise the LQIP bits, but the fitness function was hard to establish. Ultimately, I would’ve needed an offline CSS renderer for this to work accurately. Maybe a future iteration could use some headless Chrome solution to automatically compare real renderings of the LQIP against the source image. Once encoded, it’s set as the value of --lqip via the style attribute in the target element. It could then be decoded in CSS. Here’s the actual code I used for decoding: [style*="--lqip:"] { --lqip-ca: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 18))), 4); --lqip-cb: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 16))), 4); --lqip-cc: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 14))), 4); --lqip-cd: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 12))), 4); --lqip-ce: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 10))), 4); --lqip-cf: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 8))), 4); --lqip-ll: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 6))), 4); --lqip-aaa: mod(round(down, calc((var(--lqip) + pow(2, 19)) / pow(2, 3))), 8); --lqip-bbb: mod(calc(var(--lqip) + pow(2, 19)), 8); Before rendering the decoded values, the raw number data values need to be converted to CSS colours. It’s fairly straightforward, just a bunch linear interpolations into colour constructor functions. /* continued */ --lqip-ca-clr: hsl(0 0% calc(var(--lqip-ca) / 3 * 100%)); --lqip-cb-clr: hsl(0 0% calc(var(--lqip-cb) / 3 * 100%)); --lqip-cc-clr: hsl(0 0% calc(var(--lqip-cc) / 3 * 100%)); --lqip-cd-clr: hsl(0 0% calc(var(--lqip-cd) / 3 * 100%)); --lqip-ce-clr: hsl(0 0% calc(var(--lqip-ce) / 3 * 100%)); --lqip-cf-clr: hsl(0 0% calc(var(--lqip-cf) / 3 * 100%)); --lqip-base-clr: oklab( calc(var(--lqip-ll) / 3 * 0.6 + 0.2) calc(var(--lqip-aaa) / 8 * 0.7 - 0.35) calc((var(--lqip-bbb) + 1) / 8 * 0.7 - 0.35) ); } Time for another demo! Try different values of --lqip to decode { const lqip = Number(event.currentTarget.value); render(lqip); currentValueStr = String(lqip); }); function render(lqip) { preview.style.setProperty("--my-lqip", lqip); }; } You can see here how each component variable maps to the LQIP image. E.g. the cb value corresponds to the relative brightness of the top middle area. Fun fact: The above preview content is implemented in pure CSS! Rendering it all Finally, rendering the LQIP. I used multiple radial gradients to render the greyscale components, and a flat base colour at the bottom. [style*="--lqip:"] { background-image: radial-gradient(50% 75% at 16.67% 25%, var(--lqip-ca-clr), transparent), radial-gradient(50% 75% at 50% 25%, var(--lqip-cb-clr), transparent), radial-gradient(50% 75% at 83.33% 25%, var(--lqip-cc-clr), transparent), radial-gradient(50% 75% at 16.67% 75%, var(--lqip-cd-clr), transparent), radial-gradient(50% 75% at 50% 75%, var(--lqip-ce-clr), transparent), radial-gradient(50% 75% at 83.33% 75%, var(--lqip-cf-clr), transparent), linear-gradient(0deg, var(--lqip-base-clr), var(--lqip-base-clr)); } The above is a simplified version of the full renderer for illustrative purposes. The real one has doubled layers, smooth gradient falloffs, and blend modes. As you might expect, the radial gradients are arranged in a 3×2 grid. You can see it in this interactive deconstructor view! LQIP deconstructor! Reveal the individual layers using this slider! Change the --lqip value, { const card = document.currentScript.parentElement; const [revealInput, lqipInput] = card.querySelectorAll("input"); const preview = card.querySelector(".lqip-reveal"); let currentValueStr = "-747540"; render(Number(currentValueStr)); lqipInput.addEventListener("input", event => { const lqip = Number(event.currentTarget.value); render(lqip); currentValueStr = String(lqip); }); revealInput.addEventListener("input", event => { preview.style.setProperty("--reveal", event.currentTarget.value / 100); }); function render(lqip) { preview.style.setProperty("--my-lqip", lqip); }; } These radial gradients are the core of the CSS-based LQIP. The position and radius of the gradients are an important detail that would determine how well these can approximate real images. Besides that, another requirement is that these individual radial gradients must be seamless when combined together. I implemented smooth gradient falloffs to make the final result look seamless. It took special care to make the gradients extra smooth, so let’s dive into it… Bilinear interpolation approximation with radial gradients Radial gradients use linear interpolation by default. Interpolation refers to how it maps the in-between colours from the start colour to the end colour. And linear interpolation, the most basic interpolation, well… CSS radial-gradients with linear interpolation It doesn’t look good. It gives us these hard edges (highlighted above). You could almost see the elliptical edges of each radial gradient and their centers. In real raster images, we’d use bilinear interpolation at the very least when scaling up low resolution images. Bicubic interpolation is even better. One way to simulate the smoothness of bilinear interpolation in an array of CSS radial-gradients is to use ‘quadratic easing’ to control the gradation of opacity. This means the opacity falloff of the gradient would be smoother around the center and the edges. Each gradient would get feathered edges, smoothening the overall composite image. CSS radial-gradients: Quadratic interpolation (touch to see edges) CSS radial-gradients: Linear interpolation (touch to see edges) Image: Bilinear interpolation Image: Bicubic interpolation Image: Your browser’s native interpolation Image: No interpolation However, CSS gradients don’t support nonlinear interpolation of opacity yet as of writing (not to be confused with colour space interpolation, which browsers do support!). The solution for now is to add more points in the gradient to get a smooth opacity curve based on the quadratic formula. radial-gradient( <position>, rgb(82 190 240 / 100%) 0%, rgb(82 190 204 / 98%) 10%, rgb(82 190 204 / 92%) 20%, rgb(82 190 204 / 82%) 30%, rgb(82 190 204 / 68%) 40%, rgb(82 190 204 / 32%) 60%, rgb(82 190 204 / 18%) 70%, rgb(82 190 204 / 8%) 80%, rgb(82 190 204 / 2%) 90%, transparent 100% ) The quadratic interpolation is based on two quadratic curves (parabolas), one for each half of the gradient — one upward and another downward. The quadratic easing blends adjacent radial gradients together, mimicking the smooth bilinear (or even bicubic) interpolation. It’s almost like a fake blur filter, thus achieving the ‘blur’ part of this BlurHash alternative. Check out the gallery for a direct comparison to BlurHash. Toggle images Appendix: Alternatives considered Four colours instead of monochromatic preview Four 5-bit colours, where each R is 2 bits, G is 2 bits, and B is just a zero or one. The four colours would map to the four corners of the image box, rendered as radial gradients This was my first attempt, and I fiddled with this for a while, but mixing four colours properly require proper bilinear interpolation and probably a shader. Just layering gradients resulted in muddiness (just like mixing too many watercolour pigments), and there was no CSS blend mode that could fix it. So I abandoned it, and moved on to a monochromatic approach. Single solid colour This was what I used on this website before. It’s simple and effective. A clean-markup approach could still use the custom --lqip variable: <img src="…" style="--lqip:#9bc28e"> <style> /* we save some bytes by ‘aliasing’ this property */ * { background-color: var(--lqip) } </style> HTML attribute instead of CSS custom property We can use HTML attributes to control CSS soon! Here’s what the LQIP markup would look like in the future: <img src="…" lqip="192900"> Waiting for attr() Level 5 for this one. It’s nicer and shorter, fewer weird punctuations in markup (who came up with the double dash for CSS vars anyway?). The value can then be referenced in CSS with attr(lqip type(<number>)) instead of var(--lqip). For extra safety, a data- prefix could be added to the attribute name. Can’t wait for this to get widespread adoption. I also want it for my TAC components.

2025/3/30
articleCard.readMore

Inline rendering with document​.currentScript

For quick and dirty rendering of simple dynamic content, you may not need the complexity of a templating language like Handlebars or a PHP backend. Let’s use the example phrase, “Come on, it’s <currentYear>”. It should result in “Come on, it’s 2025$(new Date().getFullYear())” when rendered today. You can write this directly in HTML—without IDs, classes, or querySelectors in your JS! Thanks to the document.currentScript property, we can refer to the currently running <script> element directly and go from there. So the dynamic phrase “Come on, it’s 2025$(new Date().getFullYear())” would now be written as: Come on, it’s <script> document.currentScript.replaceWith(new Date().getFullYear()) </script> The script simply replaces itself with its computed value on the spot. The code’s a bit wordy though, but we can alias it to a constant like $ via $=(...n)=>document.currentScript.replaceWith(...n). Then we’d have something reminiscent of template literals in JS. Come on, it’s <script>$(new Date().getFullYear())</script> The code is pretty readable at a glance without context (after you get past the bit of indirection that is the $ alias). Click to see the WebComponent version! Disclaimer: This is a joke. <script>$(RenderJS.toString())</script> This is the RenderJS custom element. All it does is replace itself with the result of its contents treated as JavaScript code. To use it, we must first name the tag. The beloved dollar sign $ is not allowed, so we use the next best thing—the emoji 💲. Sadly, that’s not enough: Tag names must start with a letter of the alphabet and include a hyphen. As such, we are forced to name it: j-💲 representing JavaScript. <script>customElements.define('j-💲', RenderJS);</script> Example usage I overslept by about <j-💲>Math.PI</j-💲> hours this morning. I overslept by about Math.PI hours this morning. A noscript fallback would be nice for those that don’t run JS. Come on, it’s <noscript>current year</noscript> <script>$(new Date().getFullYear())</script> The resulting script-noscript juxtaposition in the markup looks almost like Angular/Vue’s if-else markup, neat. IIRC, I learned about this technique from the Surreal library or something similar. But I’m sure this is not a new discovery. These APIs have been standardised for a long time. Real world examples If you thought this technique is only useful for rendering the current year, you’re mostly right! As an example, I’ve used inline scripts to refer to relative dates in my living pages. Because “8$(new Date().getFullYear()-2017) years ago” sounds more natural than “2017”. It was planted <noscript>in 2017</noscript> <script>$(new Date().getFullYear()-2017, ' years ago')</script> but now it has grown tall and strong. in 2017 $(new Date().getFullYear()-2017, ' years ago') but now it has grown tall and strong. Random greeting Another example is in my front page, which says a randomly selected greeting on each visit. <span class="intro-line"> <noscript>Hello!</noscript> <script> $([ "Hello!", "Hey~~~", "What’s up ↑", "Hi there →", "Hey there →", ][Date.now() % 5]) </script> </span> The randomiser is just a Date.now() with modulo because it was more concise than Math.floor(Math.random() * 5). Splitting text for animation Split text into characters using inline scripts to generate markup per character which can then be controlled or animated! In my case, I used it to render circular text, but a more common use case is text animation like the following. document.currentScript.outerHTML= "supercalifragilisticexpialidocious".replace(/./g, c => `${c}`) week. It has been a <script>document.currentScript.outerHTML= "supercalifragilisticexpialidocious" .replace(/./g, c => `<strong>${c}</strong>`) </script> week. <style> strong { display: inline-block; animation: wave 0.3s infinite alternate ease-in-out; &:nth-child(4n + 1) { animation-delay: -0.6s; } &:nth-child(4n + 2) { animation-delay: -0.45s; } &:nth-child(4n + 3) { animation-delay: -0.3s; } &:nth-child(4n + 4) { animation-delay: -0.15s; } } @keyframes wave { to { transform: translateY(-2px); } } </style> Rendering metadata to reduce duplication This post’s heading is dynamically derived from the page title metadata. I only have to define this post’s title once, in the <title> tag. <blog-header> <h1><script>$(document.title)</script></h1> <img src="hero.jpg" alt="" loading="lazy"></img> </blog-header> Auto-updating footer How about an auto-updating copyright notice in the footer which may or may not have legal implications? (IANAL) <footer> © <script>$(new Date().getFullYear())</script> Mycorp, Ltd. </footer> For comparison, here’s the StackOverflow/ChatGPT answer: document .getElementById("copyright-year") .textContent = new Date().getFullYear(); <footer> © <span id="copyright-year"></span> Mycorp, Ltd. </footer> Why pollute the global ID namespace and separate coupled code if we can avoid it? Date().split` `[3] is also a short (but very hacky) way to get the year. At this point you may be asking, is this technique only really useful for rendering the current year? Maybe, but we can do more than just rendering text. Come on, it’s 2025$(new Date().toLocaleDateString()), the web is rich and interactive! The ubiquitous counter app example This is not a simple templating example anymore, but shows the power of currentScript in hydrating self-contained bits of interactive HTML. 0 Increment Decrement const [span, increment, decrement] = document.currentScript.parentElement.children; let count = 0; increment.onclick = () => span.replaceChildren(++count); decrement.onclick = () => span.replaceChildren(--count); <div> Count: <span>0</span> <button>Increment</button> <button>Decrement</button> <script> const [span, increment, decrement] = document.currentScript.parentElement.children; let count = 0; increment.onclick = () => span.replaceChildren(++count); decrement.onclick = () => span.replaceChildren(--count); </script> </div> I used this ‘local script’ pattern all the time in a previous version of this blog. It’s useful when making interactive illustrations in the middle of a long post. You wouldn’t want to put that logic at the very end or start of the file, away from the relevant section! Same goes for styles, now made even better with the @scope rule. It’s a good way to manage islands of interactivity. The catch There are reasons people gravitate to libraries and frameworks. They’re just too convenient. If you want to use the alias definition above—the $ shorthand—then you'd first have to define it at the top of the HTML, synchronously. Say you want to use this in multiple pages, so you put this in a common script file and load it via <script src="$.js"</script>. This could result in a parser- and render-blocking fetch: potentially very bad for performance! For my own cases, I don’t use the alias. I straight up just use document.currentScript​.replaceWith. Wish it was shorter… Also, you cannot do data-driven templating this way, in the sense that you have templates to apply data onto. Metadata such as date published, post title, and tags that are stored in some posts table or in frontmatter somewhere, cannot be used for inline scripting purposes. You can’t fetch data from inline scripts. Therefore, unless data is somehow pre-injected in the global scope, no luck in pure vanilla JS templating. This is where templating languages like Liquid really shine. For my site, I don’t need a templating system—I use direct patching to data-drive my HTML (in which the source and the output files are the same). Even if you use a templating framework or a static site generator, inline scripts remain useful! Further reading Surreal - a library that offers this technique, plus other jQuery-style helpers. Note the synchronous script tag loading issue though! currentScript discussion in SvelteKit - this as an alternative to global IDs for self-hydrating things. In the end they went with generated IDs. :| Appendix document.write() used to (?) work like this for inline rendering, but it’s now deprecated because it had inconsistent behaviour. Come on, it’s <script>document.write(new Date().getFullYear())</script>, stop using <code>document.write</code>! 2025document.write(new Date().getFullYear()), stop using document.write!">

2025/3/7
articleCard.readMore

Rewriting my site in vanilla web

I rewrote this website in vanilla HTML/CSS/JS. Here’s the story. But why? Over the years, I’ve used a bunch of libraries & frameworks to build this website, before finally making my own static site generator that I called compose-html. As the name suggests, it composes HTML files together, very much like Astro. compose-html’s README I like systems that have a small set of tight abstractions that are easy to understand. I’m not a fan of rigid, non-atomic concepts like “pages” and “layouts”, “themes”, “frontmatters” — I mean, these are just ‘components’ and ‘data’! I dislike those that dictate your project directory structure and coding style. If your documentation has a ‘Project structure’ section, I’m out! So I built my own simple site builder and that was nice BUT it didn’t end up making life easier. The real world is messy, and HTML more so. Simply composing pieces of HTML together isn’t that straightforward and the abstraction leaked. My compose-html framework eventually turned into a 2k LoC that was more liability than freedom. Though it served me very well, it was a dead end. Maybe nothing can solve my problem… As in, literally nothing. No framework. No build steps. What if HTML wasn’t a render target, but was both the authoring and publishing medium? What if I rewrote my site in vanilla HTML/CSS/JS? A crazy idea infiltrated my conciousness. Is it feasible? A common reason for adding complexity is to avoid repetitive work like copying headers & footers to every page. So we have PHP, Handlebars, Next.JS. Modern HTML/JS now has Web Components, custom elements which could be used to encapsulate repetitive sections! This was already possible without Web Components, but it makes it nicer. One could go write HTML like this: <!doctype html> <site-header></site-header> <main> My page's content </main> <site-footer></site-footer> What about the repetitive <html>, <head>, and <body> tags? Fortunately, web browsers and the HTML spec itself are lenient. These tags are actually optional! One would still need to manually copy and paste some common tags like the <script> to load the custom elements, and maybe a common.css file and a few meta tags. But I’d say it’s a similar level of boilerplate as some other frameworks, if not a bit un-DRY. What about people who disable JS? No problem. They would still see the main content itself, just not the navigational headers & footers. I presume these people would be savvy enough to navigate by URL. Another reason to use a generator is to generate data-driven content, especially for blog sites which usually have a blog index page with autogenerated collections of posts. A chronological list of posts. I don’t want to hand-code lists of posts. Especially since a slice of the latest posts is mirrored in the homepage. As I said, the real world is messy, and there is not one absolute dogma that can solve it all. A bit of automation is perfectly fine whenever needed! Just there’s no need to build-systemify the entire site. With these concerns out of the way, the rewrite was looking more feasible. My approach To make sense of the rewrite and keep the site maintainable going forward, I decided to follow these principles: Semantic HTML TAC CSS methodology 1. Semantic HTML Basically means using semantic tags instead of generic divs and spans One example is the time tag that I used to indicate post date. <time datetime="2025-02-26">26 Feb 2025</time> Along the usual benefits of semantic HTML, the variety of tags will come in handy in this very rewrite, which will become obvious in the next point. 2. TAC methodology TAC methodology is a modern CSS approach takes advantage of the modern web. The main takeaway is that we should make up new tags instead of divs-with-classes to represent conceptual components. For example: <blog-post-info hidden> <time datetime="2025-02-26">26 Feb 2025</time> · 1 min read </blog-post-info> BEM methodology: <div class="blog-post-info blog-post-info_hidden"> <time class="blog-post-info__date" datetime="2025-02-26"> 26 Feb 2025 </time> · 1 min read </div> blog-post-info, the styling of these elements could easily use tag and attribute selectors (the T and A of TAC!) without the need for classes! The markup is leaner, and the CSS even looks modular when taking advantage of modern CSS nesting: blog-post-info { display: block; /* note: made-up tags default to `inline` */ color: #fff; &[hidden] { display: none; } /* semantic HTML helps narrow the element to select */ > time { color: #ccc; font-weight: bold } } While TAC was called a CSS methodology by the authors, it influences Web Component philosophy as well, into the next point. 3. Web Components with light DOM I’ve always found the Web Component abstraction to be a bit heavy. You have the Shadow DOM, encapsulation modes (?), slots, templates, and many more related concepts. Now, some of those are pretty useful like slots and templates (which aren’t exclusive to Web Components). But overall, Web Components feel a bit clunky. The ‘light DOM’ approach does away with all of that. Like the example above: <blog-post-info hidden> <time datetime="2025-02-26">26 Feb 2025</time> · 1 min read </blog-post-info> If implemented with shadow DOM, it could’ve look like this: <blog-post-info datetime="2025-02-26" minread="1"></blog-post-info> <!-- or maybe --> <blog-post-info datetime="2025-02-26"> 1 min read </blog-post-info> The light DOM aligns with the TAC methodology, so it’s a good match. I admit scoped styles and slots are neat, but there aren’t essential (see TAC) and there are workarounds to slots. I’m not making a modular design system after all. Using the light DOM also provides a smoother transition from plain JS style to Web Components. Relevant, as I was converting some old JS code. Imagine the following common pattern: for (const blogPostInfo of document.querySelectorAll(".blog-post-info")) { const time = blogPostInfo.querySelector("time"); // ... initialisation code } customElements.define( "blog-post-info", class BlogPostInfo extends HTMLElement { connectedCallback() { const time = this.querySelector("time"); // ... initialisation code } } ); The mapping was straightforward enough that I was able to partially automate the conversion via LLM. While I’m not really making the most out of Web Components technology, I don’t actually need the extra features. I have a confession — I set this.innerHTML directly within a Web Component, and it’s so much simpler than setting up templates. I do try to sanitize. Details aside, these principles made the whole rewrite easier because it reduced the amount of actual refactoring. I wasn’t able to particularly follow them to the letter, especially for nasty old code. But for future code, I hope to keep using these techniques. A brief premature retrospective Pros: Instant feedback loop. Zero build time. No bugs out of my control. It’s what turned me off Eleventy. No limitations imposed by framework or paradigm. Cons: Big common files, common.js and common.css, ‘cause no bundler. Verbose. No shortcuts, e.g. anchor tag — compare markdown links. Frequent copy pasting. Harder to redesign the site now. I’m fine with a little bit of verbosity. For contrast, I wrote the htmz page manually in plain HTML, including the syntax-highlighted code snippets! Have you ever tried manual syntax highlighting? But not this time, I added the Prism.js library to automate syntax highlighting. Tips & tricks AI — I used LLMs to help me convert a bunch of pages into the new style. What I did was give it an example of an old page and the converted version (manually converted by me), and then it gave me a converter script that I tweaked and ran through most of the pages. I did the same to convert components and it was a breeze. The converted script was iteratively improved upon and made more robust by me and the LLM via examples of incorrect conversions and corrected versions. I guess the trick was to give it more examples instead of more elaborate instructions. // this snippet from the AI-assisted converter script // converts <blog-post-info> elements input("blog-post-info").each((i, el) => { const tag = input(el); const hidden = tag.attr("hidden") != null; const date = tag.attr("date"); const readMins = tag.attr("read-mins"); let out = `<blog-post-info${hidden ? " hidden" : ""}>\n`; const dateDate = new Date(date); const yyyy = dateDate.getFullYear(); const mm = (dateDate.getMonth() + 1).toString().padStart(2, "0"); const dd = dateDate.getDate().toString().padStart(2, "0"); out += ` <time datetime="${yyyy}-${mm}-${dd}">${date}</time>\n`; out += ` · ${readMins} min read\n`; out += `</blog-post-info>`; tag.remove(); main.before(out + "\n\n"); }); Autoload — I added client-side JS that searched for custom tags and loaded the appropriate script files when those tags enter the viewport. In short, lazy loading components. I did have to impose a rigid file structure, because whenever it encounters a tag it would try to import(`/components/${tagName}.js`) — all my autoloaded components had to be in that flat directory. Am I a hypocrite? No, I can change that rule anytime. // autoloads components in the viewport new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { if (components.has(entry.target.tagName)) { import(`/components/${entry.target.tagName}.js`); components.delete(entry.target.tagName); } intersectionObserver.unobserve(entry.target); } } }); This is not an exercise in purity — This is a real website, a personal one at that. This is not a pure HTML proof-of-concept. Not a TAC role model. Not a Web Component masterpiece. I would add inline JS whenever it’s more convenient, break encapsulation if necessary, use classes instead of pure tag selectors. Don’t let the ideal pure plain static TAC+WebComponent vanilla HTML+CSS get in the way of finishing the project. In other words, pragmatism over principles. Homepage redesign I couldn’t resist the temptation to not just port, but redesign the site (at least, the homepage). The homepage sections are now more dense, more desktop-equal (not mobile-first), and the bento section has been revamped! See also, autoupdating note count, project count, GitHub stats, and hit counter. Sprinkles of automation, no build system required! I’ll probably add a live Spotify card in there somewhere. That’s about it! The whole site rewrite went smoother and quicker than expected! And I’m quite liking the raw authoring experience. Now, how long will this new paradigm hold up? 😏

2025/3/1
articleCard.readMore

CSS sprite sheet animations

For RSS readers: This article contains interactive content available on the original post on leanrada.com. Check out this demo first (Click it!): Yes, it’s the Twitter heart button. This heart animation was done using an old technique called sprite sheets🡵. Interactive content: Visit the website to play with interactive content! Alternative text: spinning clover Interactive content: Visit the website to play with interactive content! Alternative text: spinning clover Interactive content: Visit the website to play with interactive content! Alternative text: spinning clover On the web sprite sheets are used mainly to reduce the amount of HTTP requests by bundling multiple images together into a single image file. Displaying a sub-image involves clipping the sheet in the appropriate coordinates. Sprite sheet / texture atlas of Minecraft blocks The bandwidth benefit has been largely mitigated by HTTP/2 now, but sprite sheets have another purpose: animations! Displaying animations is one of the primary uses of sprite sheets, besides loading performance. GrafxKid"> Characters w/ animations, sprite sheet by GrafxKid It’s neat for small raster-based animations such as loading spinners, characters, icons, and micro-interactions. Interactive content: Visit the website to play with interactive content! Alternative text: walking animation of a red monster character Interactive content: Visit the website to play with interactive content! Alternative text: walking animation of a blue monster character How Assumming you already have a sprite sheet image and coordinates in hand, all you need is a way to clip that image for display. There are a few ways to clip an image in CSS. method coordinates via background-image background-position overflow: hidden with nested <img> left, top on the nested element clip-path clip-path, left, top The left and top rules can be substituted for transform: translate(…). The background-image way is the most convenient since you only need one element. .element { background-image: url('heart.png'); /* size of one frame */ width: 100px; height: 100px; /* size of the whole sheet */ background-size: 2900px 100px; /* coordinates of the desired frame (negated) */ background-position: -500px 0px; } This is the sprite sheet for the heart animation from Twitter: Using this image, the code above produces a still image of the frame at (500,0) — the sixth frame. Removing the clipping method reveals that it’s just a part of the whole sheet (this view will be fun when it’s actually animating): If the sprite sheet wasn’t made to be animated, that is, if it was just a collection of multiple unrelated sub-images like the Minecraft example earlier, then the CSS rules above are all we need to know. That’s it. Since this sprite sheet was made to be animated, that is, it contains animation frames, more needs to be done. To animate this, we animate the background-position over each frame in the sequence, flashing each frame in quick succession. .element { background-image: url('heart.png'); /* size of one frame */ width: 100px; height: 100px; /* size of the whole sheet */ background-size: 2900px 100px; - /* coordinates of the desired frame (negated) */ - background-position: -500px 0px; + /* animate the coordinates */ + animation: heartAnimation 2s steps(29, jump-none) infinite; +} + +@keyframes heartAnimation { + from { + /* first frame */ + background-position: 0px 0px; + } + to { + /* last frame */ + background-position: -2800px 0px; + } +} Important: Note the steps()🡵 timing function in the animation rule above! This is required for the transition to land exactly on the frames. Voilà. And the view without clipping: zoetrope"> It’s like a zoetrope The exact parameters for the steps() function are a bit fiddly and it depends on whether you loop it or reverse it, but here’s what worked for the heart animation with 29 total frames. animation-timing-function: steps(29, jump-none); Using any other timing function results in a weird smooth in-betweening movement like this: Remember, steps()🡵 is crucial! Why not APNG? For autoplaying stuff like loading spinners, you might want plain old GIFs or APNGs🡵 instead. But we don’t have tight control over the playback with these formats. With sprite sheets, we can pause, reverse, play on hover, change the frame rate… …make it scroll-driven, Interactive content: Visit the website to play with interactive content! Alternative text: spinning globe Interactive content: Visit the website to play with interactive content! Alternative text: spinning moon Interactive content: Visit the website to play with interactive content! Alternative text: spinning globe Note: Scroll-driven animations are experimental. No Firefox support atm. … or make it interactive! Interactivity The nice thing about this being in CSS is that we can make it interactive via selectors. Continuing with the heart example, we can turn it into a stylised toggle control via HTML & CSS: .element { background-image: url('heart.png'); /* size of one frame */ width: 100px; height: 100px; /* size of the whole sheet */ background-size: 2900px 100px; + } + +.input:checked ~ .element { /* animate the coordinates */ - animation: heartAnimation 2s steps(29, jump-none) infinite; + animation: heartAnimation 2s steps(29, jump-none) forwards; } @keyframes heartAnimation { from { /* first frame */ background-position: 0px 0px; } to { /* last frame */ background-position: -2800px 0px; } } Or use the new :has(:checked). Additionally, CSS doesn’t block the main thread. In modern browsers, the big difference between CSS animations and JS-driven animations (i.e. requestAnimationFrame loops) is that the JS one runs on the main thread along with event handlers and DOM operations, so if you have some heavy JS (like React rerendering the DOM), JS animations would suffer along with it. Of course, JS could still be used, if only to trigger these CSS sprite animations by adding or removing CSS classes. Why not animated SVGs? If you have a vector format, then an animated SVG🡵 is a decent option! This format is kinda hard to author and integrate though — one would need both animation skills and coding skills to implement it. Some paid tools apparently exist to make it easier? And Lottie? That 300-kilobyte library? Uh, sure, if you really need it. Limitations of sprite sheets The sheet could end up as a very large image file if you’re not very careful. It’s only effective for the narrow case of small frame-by-frame raster animations. Beyond that, better options may exist, such animated SVGs, the <video> tag, the <canvas> tag, etc. How do you support higher pixel densities? Media queries on background-image? <img> with srcset could work, but the coordinates are another matter. But it could be solved generally with CSS custom properties and calc. Gallery Interactive content: Visit the website to play with interactive content! Alternative text: star animation from Twitter ‘Favorite’ icon animation Interactive content: Visit the website to play with interactive content! Alternative text: walking animation of a snail Interactive content: Visit the website to play with interactive content! Alternative text: idle hiding animation of a snail Interactive content: Visit the website to play with interactive content! Alternative text: climb animation of a snail Interactive content: Visit the website to play with interactive content! Alternative text: attack animation of a snail Interactive content: Visit the website to play with interactive content! Alternative text: death animation of a snail Snail enemy sprites from my game MiniForts Video: | Source: /notes/css-sprite-sheets/work.mp4 I actually had to implement these hover animations via sprite sheets at work. Video: | Source: /notes/css-sprite-sheets/work2.mp4 Behind the scenes See the Pen GSAP Draggable 360° sprite slider by Jamie Jefferson (@jamiejefferson) on CodePen. See the Pen Steps Animation by simurai (@simurai) on CodePen.

2024/11/3
articleCard.readMore

Centering a div in a div in 2020

tl;dr: use place-content: center on a grid container. .container { display: grid; place-content: center; } Here’s how that looks like: That’s it. Two CSS rules. Yes, four years late according to caniuse. But this is apparently still not well-known today. Based on recent developments🡵, looks like we just need a few more years before we can finally get rid of the extra display rule so we can have the one CSS rule to center them all.

2024/10/17
articleCard.readMore

I made an app to fix my motion sickness

Last May, Apple announced a new feature called Vehicle Motion Cues for their iOS devices. It’s an overlay that can help reduce motion sickness while riding a vehicle. I have really bad motion sickness, and riding cars, buses, and trains makes me nauseous. This feature would have been a nice relief for me, but as it stands, I use Android. Instead of buying a Malus fruit device, I took the matter into my own programmer hands. I created an alternative app for Android. To be sure, I checked the patents. Apple does have one regarding a certain motion sickness solution, but it’s specifically for head-mounted displays, not handheld devices. I figured it’s because there is prior art for handheld devices, such as KineStop for Android by Urbandroid🡵. My app is called EasyQueasy. What it does is display onscreen vestibular signals that try to help prevent motion sickness. This functions as an overlay that is displayed on top of whatever you’re doing, browsing the web or watching videos. The app is open source. I might try to get it on F-Droid someday. Probably not Google Play since Google suspended my developer account due to “inactivity”. I made this for myself so I haven’t really sorted out distribution. GitHub repo is here anyway🡵. Side note: Apologies to the RSS readers of this blog and segfault. I accidentally published this post in the RSS feed, but not on the site, which lead to an incomplete draft post and a 404 error. How does it work? There are two facets to this inquiry: How does it help with motion sickness? How does the app work behind the scenes? Let’s go over them sequentially. How to unsick motion Motion sickness🡵 happens when your brain receives conflicting motion cues from your senses. Motion cues come from the vestibular system (“sense of balance”, inner ears) and the visual system (eyes). The system diagram When you’re in a moving car, your eyes mostly see the stationary interiors of the car but your inner ears feel the movement. The signal of being stationary and the signal of being in motion does not make sense in the central processing system somewhere in the brain (which decides that the solution to this particular situation is to vomit). There’s a bug in the central processing module that triggers an unexpected response to conflicting signals. Side note: The inverse happens in virtual reality when you move or rotate your character — your eyes recognise motion but your inner ears (your physical body) feel no motion, causing sickness. As I mentioned, I have really bad motion sickness, and both car movement and VR movement are almost unbearable to me. To reduce motion sickness, we need to match the visual input with the vestibular input. Unfortunately, there’s no hardware that can override the inner ears’ sense of motion just yet. But we can semi-hijack the visual system via age-old hardware called screens. By displaying movement on-screen that matches the actual movement of the body, motion sickness can be thus reduced! The workaround is to hijack one of the sources to produce a ‘correct’ signal. In this case, we add an override to the visual input. Behind the screens Almost every smartphone has an accelerometer in them. It’s a hardware module that measures acceleration in real time. Coincidentally, the inner ears also sense the same particular aspect of motion, acceleration. That’s why it can detect up or down (the planet’s gravity is an ever-present acceleration towards “down”), functioning as a “sense of balance”, and detect relative motion at the same time. So, smartphones have “inner ear” hardware. By reading off of this sensor, we can generate the correct visuals. The visual layer is just an adapter to same acceleration data used by the other source. By converting motion data to visual data, we can be sure that the signals match! This is the final solution. The other Android app, KineStop, appear to use the phone’s magnetic sensor (compass) in addition to the accelerometer. So, it also knows about your absolute orientation in the world. It’s more effective in turning motions, but not so much for bumpy rides. There are definitely multiple ways to solve the problem, but I find that the purely acceleration-based solution is sufficient for me. Dead reckoning The accelerometer provides acceleration data, but what we actually need to draw dots on the screen is position. Recall in calculus or physics that the integral of acceleration is the velocity and the integral of velocity is position. There are two layers of integration before we can draw dots on the screen. acceleration → velocity → position What the app does is numerical integration🡵. It keeps track of an estimated velocity and an estimated position in memory. Then, on every tick, it adds the current acceleration vector to the velocity vector, and add the velocity to the position. Note: all vectors here are in 3D. The calculated position vector provides an estimate of the phone’s current position! Finally, we can draw dots! To provide the correct sensation of movement, the dots must appear stable relative to the origin, that is, the earth. Negating the calculated position vector achieves this effect. // pseudocode onEveryFrame() { velocity += accelerometer.getAcceleration() position += velocity drawDots(offset = -position) } Error correction The accelerometer signal is very noisy. While that is workable for many applications, when we’re doing position integration the errors really add up, and simple dead reckoning is way off. Standard signal processing techniques can be applied to deal with noise, like a low-pass filter. // Example: With a low-pass filter onEveryFrame() { // low-pass filter smoothAcceleration += (accelerometer.getAcceleration() - smoothAcceleration) * 0.60 // position integration velocity += smoothAcceleration position += velocity drawDots(offset = -position) } But a simpler solution like dampening the velocity over time is plenty sufficient. // Example: With velocity dampening onEveryFrame() { // position integration velocity += accelerometer.getAcceleration() position += velocity // dampen velocity *= 0.80 drawDots(offset = -position) } And that is basically how EasyQueasy works. And presumably iOS does it similarly as well. EasyQueasy features EasyQueasy’s features and advantages over the current iOS solution are: Configurable speed and other parameters. It’s 3D! Because movement in the real-world is three-dimensional! Option to activate the overlay from any screen via quick gesture shortcuts. Gallery

2024/9/2
articleCard.readMore

Stop using ease-out in your UIs!

For RSS readers: This article contains interactive content available on the original post on leanrada.com. Before anything, let me present you with a set of controls with no context. Press me Press me Stop using ease-out, or ease-in-out, or whatever-out, in every UI animation! There is a lot of propaganda on the internet against ease-in, saying that it's “unnatural”, “unusual”, or that it's “of the devil”. Some say that it's both “sluggish” and “abrupt”. Many pointing to ease-out as the safe, smooth, satisfying messiah of animation (including its safer kin, ease-in-out). There are even published ‘best practices’ which can be summed up to “just use ease-out”🡵. This post is here to set things straight — in a nonlinear fashion. So, why not ease-out? And what to use instead? Reason #1. It’s overused Let’s get the weakest point out of the way. Ease out is boring because it’s everywhere. Because it’s part of the browser default ease function (which is a combination of a fast ease-in with a slow ease-out). It’s like how corners are getting rounder and rounder on the web simply because it's easy and built-in. Source: Figma Is the public missing out on better web animations (and better corners) because of the ubiquity of these practices? Yes. Reason #2. It’s unrealistic How about a study in skeuomorphism? Imagine a mechanical toggle switch. Something like the following video: Now here’s an interactive physics simulation of the same spring-based toggle switch: Interactive content: Visit the website to play with interactive content! Alternative text: interactive simulation of a mechanical switch Drag your pointer horizontally and vertically across the simulation window to control the appendage that toggles the switch. This is a physics-based simulation with simulated forces, torques, and constraints. Let’s slow the simulation down and show some force lines for a better look. Interactive content: Visit the website to play with interactive content! Alternative text: interactive simulation of a mechanical switch Play around with the simulation! Here’s what a typical motion looks like: Typical output graph of the above simulation Notice the position curve (red). We'll get to the force curve later. Does the position curve look anything familiar? Compare that with some standard easing functions: Common easing functions. Source: MDN The toggle switch’s position curve follows the ease-in curve! A slow start, gradually building up momentum, then finally stopping to a satisfying ‘click’! Contrary to “best practice”, the natural motion of a toggle switch does not follow an ease-out nor an ease-in-out curve! I’ll go further and say that ease-out is unnatural for any kind of UI control that represents a tactile interaction. Like buttons. In the real world, buttons (the ones that are nice to press anyway) have some kind of buckling mechanism when pressed. Similar to the toggle switch example above, there’s a buildup, a fall, and a final ‘click’ into place. A button. In the real world. Best practice says that the sharp stop is unnatural and should be avoided. But that well-defined resolution is part of what makes switches and buttons feel good. Just imagine a button that dampens the motion the more you press it. It’ll feel squishy and mushy. You know what else slows down the further you go? Ease-out! And yet, thousands of UIs still use ease out for UI controls! Ease out everywhere. One of these is a skeuomorphic rocker switch, ironically. Has abstract UI design gone too far? Counterexamples Of course, not all UI animations need ease in, such as macro interactions, card movement, scrolling, expand/collapse, or any object that ‘animate themselves’ as opposed to raw manipulation. Source: Material Design Unlike switches and buttons, these things don’t have a frame or housing that can immediately stop them when they ‘bottom out’. So they have to decelerate naturally. In the end, it depends. But as a general rule? Ease-in for tactile things. Reason #3. Ease-in is more satisfying The graph again. This time focus on the force curve. If you’re a mechanical keyboard enthusiast, you might’ve recognised the general landmarks in the force curve (blue) above. That ‘tactile bump’ and subsequent drop in force is a big part of what provides the satisfying feedback that mechanical keys are known for (and coincidentally, produces the ease-in motion). Force curve of a tactile keyboard switch. Source: deskthority.net But why is it satisfying? I present my sub-thesis: The sequence of tension and release is intrinsically satisfying. me I will now present supporting evidence with xkcd-style graphs. A. Popping bubble wraps Disclaimer: We are getting into subjective and pseudoscience territory. Estimated force curve of popping a bubble in a bubble wrap. Popping bubble wraps is a satisfying sensation. Popping a bubble in a bubble wrap follows the familiar buildup, release, and resolution pattern that is associated with ease-in. Relevant xkcd.🡵 B. Scratching an itch Estimated discomfort experienced during an itch’s lifetime. While the act of scratching by itself is mildly pleasurable, when paired with an itchy skin, it becomes a satisfying experience. C. Music theory 🚧 This section is WIP, something about dissonance and consonance 🚧 D. Arousal jag theory The abrupt fall from elevated levels of arousal to a lower, more appropriate level is thought to produce a pleasurable response. APA Dictionary of Psychology, ‘arousal jag’🡵 This theory was introduced by a psychologist named Daniel E. Berlyne in 1970. The idea is that an increase in tension followed by a sharp decrease produces a satisfying feeling. This framework works well for ease-in’s case in the context of the force curve, or more directly, with ease-in’s initial slow buildup followed by its abrupt resolution. If you’re looking for the psychology behind microinteractions, well, that’s one of them. E. The Three-Act Structure The three-act structure with a tension graph. In storytelling or filmmaking, the three-act structure🡵 is a model for analysing or creating good stories. It mirrors the tension-resolution sequence in a grander scale. And with bigger scope comes the potential for a higher level of satisfaction — catharsis. Great stories that use the three-act structure always leave you in a state of catharsis. Is it possible to have micro-catharses in UI animations? All of these patterns of satisfaction reflect ease-in’s slow buildup and sudden resolution. While there are other kinds of satisfying phenomenon, like sand slicing🡵 which has nothing to do with any of this; tension and release is a way to induce the positive effect. There must be a balance to the proportion of tension and release, else the negative effects of the tension may overcome the positive effects of release, or the tension too light that the release is too shallow. A bubble wrap that is really hard to pop would be quite annoying, and a grand story that ends prematurely would be disappointing. The easing curve must be manually finetuned depending on the purpose. For buttons and toggles, keep it shorter than a bubble wrap pop. 🫰 Snap! Try snapping your fingers. Did you do it? If so, you just made an ease-in motion. Don’t believe me?🡵 When people say they want snappy animations, what do they really want? Tips Use a custom curve appropriate to the size of the element. Don’t use the default ease-in because it’s likely to feel too slow for most use cases. In CSS, there’s cubic-bezier()🡵 for example. Use a duration appropriate to the curve, the size of the element, and the scale of the movement. Partially start the animation on pointer press, not on release. For mouse users, there’s already an initial ‘actuation force’ required to trigger the mouse button. In these cases, the initial velocity shouldn’t be zero (the easing curve shouldn’t start at a horizontal slope). For touchscreens, there’s no such actuation force. Conclusion Don’t use ease-out in everything! Try ease-in! Or try a combination of both with varying weights! Just try anything at all. Tweak your curves as often as you tweak your paddings.

2024/7/15
articleCard.readMore

Creating a halftone effect with CSS

For RSS readers: This article contains interactive content available on the original post on leanrada.com. Here’s a quick halftone effect (i.e. a retro printed look) using CSS with only one div at the minimum. First of all, here’s a live demo: Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Toggle the filter class using the checkbox above. To further illustrate the halftone effect, the following demo can vary the size of the dots and the degree to which they ‘bleed’: Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Bleed There are several ways to do this in CSS. The above is a bit more advanced with 2-3 extra divs. I’ll try to show a simple method, first. Halftone basics To keep it simple, let’s start with a black-and-white image. It should be easy to layer in additional colors with the same principle. Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Actually, let’s start with a simpler image. A gradient, to illustrate how halftone works in the first place. Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Bleed A halftone🡵 pattern is an array of ink dots simulating the appearance of smooth gradiation of tones using just two pure tones (pure black ‘ink’ and pure white background in this case). By varying the size of the dots, the average ink coverage in a given area determines how light or dark the tone is in that area. Dots large enough would bleed into each other, creating the effect of negative dots. Screen and threshold Dot size and bleed can be emulated in one go using two simple image processing operations, screen and threshold. The first step is to screen the source image (in this case, the gradient) with a blurry dot matrix pattern. Screen🡵 is an operation that mixes the pixels of the source image and the overlay image using some kind of an inverted multiplication formula. Essentially, it lightens lighter areas multiplicatively. Because the dots are blurry (i.e. having feathered edges), the screen operation gives us smaller-looking dots in lighter areas on the original image and denser dots in darker areas — exactly what we want in halftone. This operation is done via CSS mix-blend-mode: screen. The blurry dot pattern is generated using a radial-gradient as a repeated background-image, like this: background-image: radial-gradient(14px at 50% 50%, black, white); background-size: 20px 20px; The next step is to threshold🡵 the resulting image. That is, convert the image into pure black & pure white pixels. Dark pixels become fully black, and light pixels become white — according to some defined threshold between light vs dark. This creates the signature black-ink-matrix-on-white-paper look. In CSS, there is no threshold filter, but it can be simulated by applying an extremely high contrast filter, pushing pixel values to the extremes of pure white and pure black. Effectively the same result as thresholding. In code, that’s simply a filter: contrast(999). Another thing we can add is a blur filter, just before the thresholding operation. This emulates surface tension of the ink, or something. Let’s take a moment to look at the basic black-and-white solution so far: <div class="halftone"> <img src=...> </div> <style> .halftone { position: relative; /* brightness controls the threshold point */ filter: brightness(0.8) blur(3px) contrast(999); } .halftone::after { position: absolute; inset: 0; background: radial-gradient(10px at center, black, white); background-size: 20px 20px; mix-blend-mode: screen; } </style> Colours may yeet knowingly When you get the black ink dots going, adding the rest of the colours is easy. Just add a set of dots for each of CMY — cyan, magenta, and yellow, the “primary colours” of ink — to complete the CMYK🡵! Make sure to stagger the dots so they are distributed evenly. How to stagger them well is left as an exercise to the dear reader, you (see halftone angles🡵, moiré patterns🡵, etc). background: radial-gradient(10px at center, #000, white), radial-gradient(10px at ..., #0ff, white), radial-gradient(10px at ..., #f0f, white), radial-gradient(10px at ..., #ff0, white); These additional layers will work just as well as black because the contrast filter operates on each RGB channel independently. The colours of cyan (#0ff), magenta (#f0f), and yellow (#ff0) are at their own extremes in each RGB channel, just like black (#000) and white (#fff). Thus, the contrast filter produces a similar thresholding effect on each colour in CMYK independently and simultaneously! Note: This is not a very accurate representation of halftone, mainly due to the operations being in RGB, not CMY. An accurate simulation would be to apply thresholding to each channel in some CMY space via JS or maybe WebGL. But this shallow emulation may look good enough in many cases. Here’s the result…? Only magenta is showing, because the magenta layer is the top layer in that background-image list! The other layers are hidden beneath the magenta layer. We need to combine these layers to see all the colours. In order to mix the four layers of ‘ink’ correctly, you must use the multipy blend mode to simulate how inks mix together (i.e. subtractive colour mixing🡵). Since we’re mixing background-images together, we use this property: background-blend-mode: multiply. Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Aaand that’s it! A simple Halftone effect with a single div wrapper! This simple filter is not very robust, so you may want to tailor the brightness and saturation levels of the particular source image. <div class="halftone"> <img src=...> </div> <style> .halftone { position: relative; filter: brightness(0.8) blur(3px) contrast(999); } .halftone::after { position: absolute; inset: 0; background: radial-gradient(10px at center, black, white), radial-gradient(10px at 5px 5px, cyan, white), radial-gradient(10px at 15px 5px, magenta, white), radial-gradient(10px at 10px 15px, yellow, white); background-size: 20px 20px; background-blend-mode: multiply; mix-blend-mode: screen; } </style> A minor point, but the demo above actually uses two separate overlay divs instead of a single div. This is to achieve better dot staggering. Variations Notice anything wrong with the last image above? There’s an unexpected pattern on the magenta in that flower petal. It should be a neat grid matrix, not this weird smiley face pattern or whatever it is. Even worse, the amount of ink is not correctly in proportion to the original image’s colour — There are more magenta dots than expected! Apparently, the black dots were turning into the coloured ones. I think the problem was that: coloured source image ⊕ black dot pattern = coloured dots, where the symbol ⊕ represents the screen-threshold operation. In other words, colour is contagious! What I did to fix this was separate the K layer (black) from CMY, and have it use its own greyscale copy of the source image. greyscale source image ⊕ black dot pattern = black dots. Here’s a vivid example where you can toggle the ‘separate-K’ version for comparison purposes: Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Separate K layer Size Bleed Rotation There are more ways to go about this with different qualities and levels of realism and complexity. Like dithering. I think the initial single-div solution is actually fine as long as you tweak the source image to be more readable under the filter. Gallery To finish with, here are a more demos! Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Separate K layer Size Bleed Rotation Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Separate K layer Size Bleed Rotation Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Separate K layer Size Bleed Rotation Interactive content: Visit the website to play with interactive content! Alternative text: CSS halftone demo Separate K layer Size Bleed Rotation P.S. Please don’t look at the demos’ source code. It’s terrible.

2024/5/5
articleCard.readMore

AI art is not generative art

AI art is not generative art (clickbait title). While the technical definition says that one is a subset of the other, I think it’s useful to distinguish between these two categories. Why I’m writing this in the first place — Starting 2022, the term “generative art” had been progressively becoming synonymous with art produced by AI text-to-image systems. As a consumer and producer of (traditional) generative art, it was becoming a bit annoying to browse generative art content on the internet. Whether through tags like #generativeart or communities like r/generative, spaces are being flooded with AI-generated images which I and many others are not interested in. End rant. In 2024, things are a bit different. AI art is now commonly referred to as ‘AI art’. I shouldn’t have procrastinated writing this post for so long. There are also cases where generative artists are pressured to relabel their art, so as to not be mistaken for being AI (and avoid things associated with it). It’s an unfortunate niche pressured to be nichey-er. It’s kinda like animals vs humans. While technically & scientifically, humans are (a subset of) animals; in daily life “animals” usually mean non-human animals, as in “animal lover” and “animal rights”. I digress. Section overview This article looks at the similarities and differences between (traditional) generative art and text-to-image AI art in different angles. Skip to different sections if you want. History Craft Process History Tracing the history of each practice may offer insights and nuance. Don’t worry, there are pictures! And interactive things! (too lazy to implement interactive things for now) This is not a comprehensive history of either field. Btw, this timeline layout is better viewed on a large screen. artificial neural networks were proposed; abstract machines modeled after biological neurons (brain cells). 1950s The Perceptron Frank Rosenblatt built a relatively simple neural network that can be trained, called the perceptron. The perceptron was designed for image recognition. The perceptron looks at some input (an image) and decides from a set of pre-learned classes of images, which kind of image it is. Conceptual art Artist Sol LeWitt started doing something called wall drawings, which are sets of instructions that produce abstract drawings. These can be thought of as precursors to generative art. Example Wall Drawing: “Wall Drawing #797. The first drafter has a black marker and makes an irregular horizontal line near the top of the wall. Then the second drafter tries to copy it (without touching it) using a red marker. The third drafter does the same, using a yellow marker. The fourth drafter does the same using a blue marker. Then the second drafter followed by the third and fourth copies the last line drawn until the bottom of the wall is reached.” Product of Wall Drawing #797, executed from instructions written by LeWitt. "The idea becomes a machine that makes the art." (Paragraphs on Conceptual Art, 1967) It’s not quite ‘generative art’ but it’s close. Besides, LeWitt called it something else: ‘conceptual art’. Code art Vera Molnár brought us closer to modern generative art by actually using computers and writing code to generate artworks. Vera Molnár1970s AI winter ⛄ The limitations of relatively primitive AI and weak computing power back then resulted in massive disappointment about AI. Budgets were cut. R&D slowed down. Hype was lost. The Perceptron did not achieve self-awareness as promised. Artists began meeting in generative art conferences and the artistic community started converging on a shared definition of generative art. Generative art — work that has been produced by the execution, usually by a computer system, of a set of rules or procedures determined by the artist. 2010s Deep learning The intersection of advancements in computing power, improved machine learning algorithms, and huge ever-growing datasets of digital things resulted in bigger and better neural networks. The great potential of AI was reignited. It’s the start of the deep learning revolution. Image recognition, picture captioning, automated language translation, text-to-speech, speech-to-text, etc. got seriously buffed. Google’s DeepDream exploits image recognition feeding into digital pareidolia. It can be considered early AI art, but it’s quite different from modern AI art. Generative networks Due to deep learning, we had advancements in generative networks, which are networks that can generate new data on their own instead of simply being a complex input-output mapping. This appears to be the start of the image synthesis wave. Here is a fake person generated by a generative adversarial network (GAN) trained specifically on portraits. Tap to regenerate a new one from thispersondoesnotexist.com. Text to image The first modern text-to-image model was introduced. It was called alignDRAW. It can generate images from natural language descriptions, making these machines massively accessible to the general public. Anyone could potentially create any image! alignDRAW’s output for “A toilet seat sits open in the grass field.”2021 DALL·E was revealed by OpenAI. Another text-to-image model, and it became famous. (Or, rather it was made famous by “DALL-E mini” which was viral.) DALL·E’s output for “A toilet seat sits open in the grass field.”2022 Meanwhile, 2022 saw a burst of NFTs, most of which have been associated with generative art. Honestly, some of them are on the borderline shallow end of generative art, Mad Libs-style images generated from shufflings of pre-drawn hairstyles, sunglasses, and clothes. Automated dress-up. They are more akin to the musical dice game. Typical images associated with NFTs2022 Text-to-image models were approaching the quality of real images. Generated artworks were approaching the quality of human art. AI-generated painting2020s Today, we see different techniques and tools to make generative art, from straight up canvas coding, to frameworks like p5.js, to full-blown authoring software like TouchDesigner and Houdini. An output of the QQL algorithm which uses the p5.js library2020s Tools to make AI art and images have been heavily commercialised, with online image generation services provided by OpenAI/Microsoft, Google, and Midjourney. There are also local models for advanced users and tinkerers. Microsoft’s image creator which at the time used the DALL·E 3 model So. AI art is a relatively new movement borne out of general image synthesizers. This was made possible since visual artworks are images too. Meanwhile, there have been decades of development and refinement in (traditional) generative art, techniques, and algorithms. The terminology may overlap (generative networks ~ generative art), but these are just labels. You may call them whatever you want, but they’re still separate art movements that cannot be historically reconciled. By the way, I don’t care if you want to call them ‘art’ or not. That’s not the point of this post. If it bothers you, please imagine that all instances of the word ‘art’ were replaced with ‘image’. Same for the word ‘artist’. The craft OK so, whatever sure, history is in the past. Why don’t we look at it in terms of what artists are doing today. What do they do? What do they practise to improve? Unique skills Both practices involve some skills that are not in common with the other. They each have their own defining skills. Generative artists AI artists Generative art involves programming and uses math (usually geometry or trigonometry) to create graphics. A good undertanding of functions and their domains and ranges and how they compose together is essential, especially when utilising random number generators. Typical generative art math Beyond these core skills, there are a lot of areas generative artists could expand into, like graph theory, simulations, fractals, image processing, and other algorithms. Scattered Pathways by Nate Nolting. Generative art using a graph pathfinding algorithm AI art, on the other hand, is heavy on prompt engineering. It could be as simple as writing English descriptions, or as precise as inputting parameterised labels if the tool allows for it. Prompt: Dog, (Artist Name, Artist Name), Masterpiece, Studio Quality, 6k, glowing, axe, mecha, science_fiction, solo, weapon, jungle, green_background, nature, outdoors, solo, tree, weapon, mask, dynamic lighting, detailed shading, digital texture painting Negative prompt: un-detailed skin, semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, ugly eyes, (out of frame:1.3), worst quality, low quality, jpeg artifacts, cgi, sketch, cartoon, drawing, (out of frame:1.1) Parameters: Steps: 50, Sampler: Euler a, CFG scale: 7.0, Seed: 1579799523, Face restoration, Size: 512x512 Example prompt with advanced parameters (e.g. for StableDiffusion) Beyond writing prompts, there are several tools that allow more control like influencing certain compositional elements. AI artists could also train or fine-tune models by curating image-label datasets as extra training data. Images generated with ControlNet, controlling the general shape of the subject. There seems to be little to no transferable skills between these practices. Sure, you can program some scripts to automate parts of an AI art workflow, or add an AI filter step to an otherwise procedurally-generated image. But there is no inherent overlap. At the highest level, it is the difference between computer science vs data science. At the lowest, it is the difference between coding and writing English. That’s why you can’t just ask an AI artist to please create an interactive audio-reactive visualisation for your music video, or a public art installation that reacts to the weather. Likewise, you can’t expect a generative artist to easily generate photorealistic art in seconds. Skills in common One big thing that’s common between these practices is the need to curate. Since there is an element of chaos and randomness in both processes, a curation step is almost always required before finishing a work. Curation requires artistic vision, which could be considered a skill in itself. Generative artists AI artists What’s curation? As one finishes a piece, they usually generate multiple outputs by running the process with different seeds or starting parameters. These produce different variations, from which the best one(s) are selected for publishing, discarding the rest. That’s curation. Alternatively, one might refine and iterate their prompt or program until the outputs satisfy a certain artistic vision. Uncurated set of outputs from a generative art algorithm AI image generators usually generate multiple outputs at a time for you to curate. It could be argued that having artistic vision and doing iterations are pretty common to all arts, and so those alone don’t make generative art and AI art any more similar to each other than any other art. Overall, the significant differences in skillsets mean that generative artists and AI artists are not interchangeable, and the same should go for their respective arts. The process Finally, let’s attack the subject from the POV of processes and systems. Setting aside the question of whether Art is Product or Process, let us indulge in these animations of art in the process of being products. AI art being formed via denoising Video: | Source: /notes/ai-art-not-generative-art/flowers.mp4 Generative art being formed via procedural strokes Most modern AI image generators are based on diffusion models — they form images via ‘denoising’. The animation above (left) shows the sequence of steps to progressively denoise a pure noise image into something coherent based on a prompt. The prompt for the above generation specifically was ‘cherry blossom branches against a clear blue sky’. The denoising process is a black box, an inscrutable network of billions of artificial neurons trained to generate coherent arrays of pixels. Generative art algorithms, on the other hand, are relatively more hand-crafted, less magical. A generative art algorithm consists of instructions to explicitly draw every element that will be in the piece. This is apparent in the above animation (right) where each drawn stroke can be seen. This particular animation was from The Soul of Flowers by Che-Yu Wu. There is large contrast in the transparency of these processes. One is a black box. The other is literally the instructions to create the art. System diagrams Beyond the processes themselves, art interacts with the context of its creation. And there is a lot to unpack here, especially for AI art. I’ll use diagrams to illustrate the context of the creation process. In the generative art process, I see 3 components at the minimum: the artist, the program, and the rendered output. The generative artist writes the program that renders the piece of art They may use libraries or frameworks as part of the program, but the artist ultimately writes, at a certain level of abstraction, the specific steps or rules which the program would execute. The medium is code. In AI art, there are 4 components to the process: the artist, the model, the rendered output, and the dataset. The AI artist prompts the model trained from the dataset to produce the piece of art There is an immediate difference. Unlike AI art, generative art don’t need datasets. And it’s also a controversial thing, the dataset. The data used to train a text-to-image model can consist of billions of images, usually scraped from the internet and includes other artworks by other artists. If you consider that the model has been influenced by a lot of other images on the web, then the question of how important the AI artist’s influence over the final piece is raised. Was that chiaroscuro an artistic choice, or an emergent byproduct of the model? Were those bold paint strokes an explicit expression of the artist, or just some typical style chosen by the model? In AI art, there is confusion over which elements were expressed by the artist and which by the machine. Leaving the contentious topic of datasets aside, there is another point, but not as essential, of who authored the main program. Who created the most crucial piece of the system? Generative art programs are usually created by the artists themselves, while AI art programs are usually created by AI researchers, only to be used as a product by AI artists. One makes the machine that makes the art, the other uses a machine to make the art. Of course, AI artists could train (not fine-tune!) their own models from scratch, if they have a big(!) enough dataset of their own and some machine learning know-how. But I don't think that’s a common occurrence. In summary, one is a direct expression from the artist where the medium is code, and the other is a complicated web of datasets including other artworks, machine learning, and just-add-water kind of products, and I don’t even know what the medium is. Conclusion The point is that these are separate art mediums. Or art movements, if you will. The histories, the crafts, and the processes point to the same conclusion — they are no more similar than oil painting is to photography, or a film director to an animator. So let’s not confuse one with the other. :) Some aspects of the post have probably been outdated by the time I publish this post. I’ll still include them in the post because why not. P.S. a similar thing: cryptoisnotcryptocurrency.com, Wikipedia article. Now, I should go back to regular programming for this blog.

2024/3/25
articleCard.readMore

htmz story

This post is not the usual programming post. It’s been an interesting week, I guess. I just finished my mini side-project htmz🡵, a snippet / library / microframework / whatever for HTML whose main feature was that it only weighed a total of 181 bytes. It almost fits inside a single ‘tweet’ (wait, the limit is now 280 not 140?). Here is the entire framework in its final form: <iframe hidden name=htmz onload="setTimeout(()=> document.querySelector(contentWindow.location.hash||null) ?.replaceWith(...contentDocument.body.childNodes))"></iframe> See the project documentation🡵 for more info on what it does. I posted it on Hacker News🡵, went to sleep because it was 3am at that point. Then the next morning it was at the top of HN! I didn’t expect this at all. But naturally I rushed to the comments section which quickly grew too numerous for me to read. They were generally positive, and acknowledged the project’s hackyness and elegance (these adjectives usually mean opposite things). It was pretty cool! The top comment in the thread sums it up. A bunch of common themes were raised. I guess I’ll write about them in this post. Why htmz? htmz was initially inspired by htmx🡵, another web library/framework. One of htmx’s main features was that it provides the capability for any element, not just the entire window, to be the target for update by an HTTP request (e.g. by clicking a link you load a partial HTML update to the page). I wondered for a bit then remembered that we already have iframes, a native HTML way for links to update not just the entire window, but a ‘part’ of the page. Granted, iframes are limited and not as dynamic as some of htmx’s operations like appends and deletions. So I set out to make an iframe-only solution to this class of problems. htmz initially stood for ‘htmx zero’ or something like that, because you need exactly zero JavaScript to be able to use iframes, while htmx stood at around 16 kB compressed (decompresses to a total of 48 kB of JavaScript!). Principles The initial premise was that browsers already offer much of the required functionality out of the box. Unfortunately, iframes have lots of limitations and a bit of JS is needed after all. As the solution evolved, I needed to keep the amount of JS in check, otherwise I’d just reinvent existing libraries. The main principle became “lean on existing browser functionality”. This meant, among other things: No click handlers. Browsers can handle clicks by themselves. No need for JS to intercept and babysit every interaction. No AJAX / fetch. Browsers can fetch HTML resources by themselves. In fact, this is a primary function of a browser! No DOM parsing. Browsers can load HTML by themselves. In fact, this is a primary function… No extra attributes. Scanning for and parsing extra attributes is a lot of JS. I ended up with minimal JS that transplants content from the iframe into the main document. That was the only thing browsers don’t do (yet). The only exception to this rule was the setTimeout wrapper which is to prevent browsers from automatically scrolling to the updated content on click. (It’s optional!) Since the solution was not zero JS anymore, htmz was backronymised as Html with Targeted Manipulation Zones. Was it a joke? (short answer: maybe) Starting with the name itself, htmz sounded like a parody of the more popular htmx. I thought it was obvious that this project was just a fun hack to share. But some people took it too seriously. I know, this is HN, where serious startups and stuff are posted daily. Or rather, this is the Internet. I was reminded of Poe’s law🡵. On the other hand, the initial documentation for the project did give mixed vibes (it was a half-joke half-solution project after all). And the marketing bits were too good for the project’s own good. This project has a favicon while htmx.org has none. Marketing overload! But really, I just enjoy polishing the little details in projects. Orrr maybe I’m the only one who finds humour in a section called ‘Installing’ that, instead of telling you to install a package, tells you to simply copy a snippet. Orrr what about when ‘Extensions’ are not plugins but code you actually have to write yourself… Ha! Get it? 😂😂😂 Subversion of expectations, anyone? No? OK, fine… In any case, I had my fun writing these aspects of the project! I made an npm package for good measure: Code golfing Some commenters pointed out insights which led to significant reductions in the size of the snippet. Some even made pull requests, that I reviewed and merged (Am I an ‘open-source maintainer’ yet?). My initial release started with 181 bytes: <iframe hidden name=htmz onload="setTimeout(()=> document.querySelector(this.contentWindow.location.hash||':not(*)') ?.replaceWith(...this.contentDocument.body.childNodes))"></iframe> Turns out ':not(*)' is not necessary as a selector fallback. querySelector can accept null! Down to 176 bytes: <iframe hidden name=htmz onload="setTimeout(()=> document.querySelector(this.contentWindow.location.hash||null) ?.replaceWith(...this.contentDocument.body.childNodes))"></iframe> Apparently, this is also unnecessary within inline attribute scripts when referring to the element it’s attached to. Now down to 166 bytes (its final form as of today): <iframe hidden name=htmz onload="setTimeout(()=> document.querySelector(contentWindow.location.hash||null) ?.replaceWith(...contentDocument.body.childNodes))"></iframe> That’s about 90% of the original! I’m pretty sure this could be reduced further (setTimeout isn’t strictly required) but I left it as it was. This code golfing diversion has been fun and I learned about HTML spec stuff that I wouldn’t normally discover in my day job as a React framework user. Knowledge sharing Some commenters shared similar approaches and techniques. Some tips / workarounds on how to best utilise the snippet were thrown around. There were even discussions in the project’s Issue tracker on GitHub. For one, the new Sec-Fetch-Dest🡵 header that someone mentioned is pretty cool. It lets the server know if the request is for an iframe or for a new tab, etc. Pretty handy for hypertext servers! I learned lots and, not wanting to get these learnings lost into the archives, incorporated these tips into the main documentation, into the examples/demos, and some even became real-ish extensions🡵: It’s a multi-way exchange. htmz and its iframe shenanigans have apparently inspired the creation of new HTML frameworks, such as morphlex🡵 and htmf🡵. I also saw people taking the idea into different directions, like using iframes to lazy-load HTML partials🡵. I feel happy about having inspired people! There are even some who said so via the guestbook, which was especially nice! Future I think the htmz story is done and I’m already looking forward to my next project (the 7DRL challenge). It’s still getting some mentions in blogs, podcasts, here and there, but it’s not like I’m gonna turn this into some serious full-time open-source project with versions and releases and (lack of) funds and all that jazz. GitHub activity (stars, issues, PRs) have slowed down as well. There’s not much you can work with 166 characters, after all. But the idea lives on; may the snippet proliferate! :D

2024/3/1
articleCard.readMore

Pure CSS single-page app routing

You’re probably a busy person, so here’s the CSS: section:not(:target) { display: none; } Demo: Open in a new tab My AppOpen in a new tab Explanation The :target🡵 CSS selector selects the element that is targeted by the URL fragment. Combined with :not, we can hide sections that are not referenced by the URL fragment. Just as JS routers use the fragment to hide/show sections in the DOM, this “CSS router” uses the same fragment to hide/show sections in the DOM. Experiment: Default section Notice that the example above doesn’t start with the Home section. The content is blank initially. This is because on initial page load we don’t have a URL fragment to begin with. We need to make an exception for the Home section. Let’s start by not hiding the #home section by default. Only hide #home if there’s a specific :target section. - section:not(:target) { + section:not(#home, :target), + :root:has(:target) #home { display: none; } Demo v2: Open in a new tab My AppOpen in a new tab Experiment: Nested routes One thing that makes most client-side routers modular is the ability to nest routes. We can do the same with CSS. - section:not(:target) { + section:not(:target, :has(:target)) { display: none; } Demo v3: This demo is best when you view it in a separate tab My AppOpen in a new tab Parameterised routes? The ultimate feature for client-side routers is to dynamically catch routes with parameters like for example /post/:id. Since HTML is static, there’s no real way to do this with CSS. ☹ Unless… you could render all possible :id values in the markup and use it like you would nested routes. <!-- ... --> <section id="post/128"><!-- ... --></section> <section id="post/129"><!-- ... --></section> <section id="post/130"><!-- ... --></section> <!-- ... --> But that’d be like putting the entire database in HTML. And if you had multiple parameters in the route, it would be combinatorial explosion. So, nope. 👋

2024/2/27
articleCard.readMore

getDuolingoStreak()

How to fetch your Duolingo streak using an unconfirmed API on duolingo.com: function getDuolingoStreak(username) { const res = await fetch( `https://www.duolingo.com/2017-06-30/users?username=${username}&fields=streak,streakData%7BcurrentStreak,previousStreak%7D%7D` ); const data = await res.json(); const userData = data.users[0]; // I didn't know which of these fields matter, so I just get the max of them. const streak = Math.max( userData?.streak ?? 0, userData?.streakData?.currentStreak?.length ?? 0, userData?.streakData?.previousStreak?.length ?? 0 ); return streak; } That’s my current max streak. I can then render this data into a card like that. I put one of these cards in the /misc/ section. Let’s look at the API itself. www.duolingo.com/2017-06-30 seems to be the API prefix, which is a bit weird. What is 2017-06-30? What happened on that date?🡵 Maybe the Duolingo team used a date-based versioning🡵 at the time? In any case, big thanks to the Duolingo team for keeping this apparently-6-year-old public API alive and accessible. The query parameters for the /users endpoint are interesting. It’s similar to GraphQL🡵 where you can specify exactly which fields you want in the response. You can even specify specific sub-fields within objects, using some kind of a DSL in the fields parameter. The fields parameter in the above query, when decoded, is fields=streak,streakDate{currentStreak,previousStreak}}. Fields are comma-delimited and objects are enclosed in braces. This could actually be GraphQL! There is an extra closing brace at the end which seems necessary for the request to be successful.

2024/2/7
articleCard.readMore

Writing a single-file website with Rust

At the company I work for, there is something called a “professional education leave”, which lets me take a day off to learn something new or attend conferences or whatever professional education means. I took the opportunity to learn the Rust🡵 programming language. I didn’t want to follow tutorials, so I thought of a project to learn with, which ended up being “make a single-file website web server”. It was inspired by the article my website is one binary by j3s🡵. Rust first impressions fn main() { println!("Hello World!"); } My first impression was that it is a mature programming language that is crazy about compile-time stuff. For example, the above println! call is a macro, which expands to something more verbose… Apparently, with macros you can also make full-blown DSL🡵s that are directly embedded in Rust code. The rustc compiler’s error messages were very good. I think I haven’t really dived deep enough to discover the standout features that I keep hearing about like memory management safety. So unfortunately I didn’t get the hype at this time. Building a web server The standard Rust library provides a TCP module, but no HTTP one. So it’s a level lower than NodeJS, which is understandable. I didn’t want to implement the HTTP protocol, so I searched for an HTTP library. I found rouille🡵, a web micro-framework. I installed it via cargo which is like npm but for Rust. It was really easy to get it set up and running. For starters, I made two pages: / and /about. // main.rs use rouille::Response; use rouille::router; mod pages { pub mod index; pub mod about; } fn main() { rouille::start_server("0.0.0.0:8080", move |request| { let response = router!(request, (GET) (/) => { pages::index::page(request) }, (GET) (/about) => { pages::about::page(request) }, _ => Response::empty_404() ); return response; }); } Notice the router! macro! Apparently it allows me to write GET and the routes / and /about plainly into code. Those aren’t strings! Nor are they Rust keywords! It’s macro magic. It’s even more magical than Kotlin’s lambdas. I think this is a powerful but dangerous tool which could make learning Rust codebases harder. I like it though. Instead of parsing these routes at runtime, errors can be caught at compile time. Rust modules One little snag was that Rust’s module system was a little confusing, coming from ES6 where everything is explicit. In Rust, modules map to files. Module naming and hierarchy follows the filesystem structure. I didn’t fully understand modules properly but it seems that I can use the mod keyword to import modules. And that was enough knowledge to proceed with the project. In the above code I separated the code that renders the pages into separate files in pages/index.rs and pages/about.rs. To import them in the main file, I had this declaration at the top: mod pages { pub mod index; pub mod about; } As I defined a top-level page() function in each of these files, calling them from the main file would be something like this: pages::index::page(request). Rendering HTML The top-level page functions simply return static HTML Responses for now. Here’s one that handles the root / route: // pages/index.rs use rouille::Request; use rouille::Response; pub fn page(_request: &Request) -> Response { return Response::html(r#"<!doctype html> <title>Tiny site</title> <h1>Hello, world!</h1> <p>Welcome to my tiny site!</p> <a href="/about">About</a> "#); } Which renders this page: Tiny site Hello, world! Welcome to my tiny site! About "> To avoid repetitively escaping double quotes in HTML, I used the raw string literal which looks r#"something like this"# to write the HTML response in Rust. HTML templates / components I haven’t done this, but I imagine components or “includes” can be implemented by simply calling component functions normally and concatenating the resulting HTML fragments together. Nothing fancy required, for a tiny website. I wonder if there are templating systems that take advantage of Rust’s compile-time macros. A quick search revealed a lot of them. In fact, that seems to be the “Rust way” of doing HTML templates. One example is Maud🡵: html! { article data-index="12345" { h1 { "My blog" } tag-cloud { "pinkie pie pony cute" } } } That looks a lot like Jetpack Compose in Kotlin. Seems like fun way to write HTML. I fear I might be taken by a strange mood and convert my entire website into Rust. I guess paid hosting for non-static sites would be a good deterrent. Conclusion That was a nice dip into Rust. I’m still interested in the hyped-up features like memory management but I guess an HTML web server wouldn’t need much of those things after all. Maybe the next Rust learning exercise would be something like a small video game. Now that’s something that would require memory management and algorithms. Here’s the repo for my tiny web server in Rust: github.com/Kalabasa/tala🡵.

2023/12/30
articleCard.readMore

2023 in review

What happened in 2023? Stuff happened. 2023 website redesign 🎨 Feb 2023, I redesigned my site, with a goal of making it more personal instead of just being a mere portfolio. It came with a blog section, which made me start writing a blog. Started a blog 🖊 “I’m starting a blog! I’ve done some blogging in the past (for gamedev), and I already do write-ups for my projects, so I think it’d be good to officially keep a blog! I have some ideas to populate the first few posts, then we’ll see how it goes from there. Watch this space!” First blog post on the new site (now unpublished), 24 Feb 2023 Well, it went well! In 2023 I had: The hottest posts were: 🔥 Sort, sweep, and prune: Collision detection algorithms 🔥 Dynamic patrol behaviour in stealth games with Markov chains It is a nice new hobby and something I shall continue. 🙌 Side projects 💻 This year I launched two webapps, portabl.ink and GuhitKudlit! Portablink was just a fun experiment without much utility. I don’t have analytics on it so I don’t know if anyone visits it. We’ll see if I renew the domain. GuhitKudlit did great and is getting regular visitors coming from Google. From the Google Search Console report, it looks like there is demand for baybayin translation and generation or tattoos or whatever. I might rewrite this site properly if it sees more use. Personal / Misc ✨ 💼 Got a new job. Still a software engineer. 🎮 Finally finished Tears of the Kingdom🡵. 🎮 Replayed Splinter Cell: Chaos Theory. First was around 15 years ago. 🎓 Began an attempt to learn the Japanese language. Steam 🫧 Here’s a bit of my Steam Year in Review: MOBA, Platformer, and Stealth… I guess I can conclude that I played a diverse range of games this year? This graph is missing the roguelike category though. Most played games on Steam in order Apparently, I played a lot of Dota. I don’t even remember playing it this year. You might have noticed the bias for the roguelike genre🡵. Shoutout to Noita🡵, FTL🡵, and Monster Train🡵. Great games. Spotify 🎵 Here’s a nonsensical thing from my Spotify Wrapped — My Top Artists in 2023: Tycho🡵 is chillwave ambient IDM downtempo post-rock instrumental music. Nice listening. Vanilla🡵 is instrumental hip hop soul-sampling electronic music. Nice grooves. The third artist is me, so it doesn’t count. I don’t actually know who the fourth artist is. I’ll explain below. Lamp🡵 is a Japanese band making indie pop jazz pop bossa nova J-pop music. I like the chord progressions. I think I use Spotify a bit differently than most people. I don’t primarily listen to artists. I just use the playlists, especially the dynamic Discover Weekly playlist. As such, “top artists” don’t really make sense. These top 5 artists don’t make up a majority of my listens. Accidentally listening to any artist’s song twice skyrockets them to my “top artists” list. Nonetheless, there are some exceptional artists that I discover and do replay from time to time; some were mentioned above. Conclusion 💡 The year two thousand and twenty-three was definitely one of the years of all time.

2023/12/25
articleCard.readMore