leanrada.com notes
フィード

Cool URLs don’t change with snapshot testing
leanrada.com notes
I wrote automated tests to keep track of my site’s URLs and prevent links from breaking.When I reorganised my site, I changed a lot of URLs and broke some inbound links. Now that more and more sites have started linking to my site, I don’t want to inadvertently break links again.Cool URIs don't changeTim Berners-Lee (1998)This post is NOT about how to implement ‘cool URLs’. Too many ways to do that depending on the site’s setup and the site’s philosophy even. Rather, this post is about ensuring cool URLs — a contract to guarantee that published URLs continue to work. It doesn’t matter how your specific site implementation fulfil that contract.There already exists a contract: a social contract when you publish a webpage. Other people can link to your URL on their sites, and you can link to other people’s content on your site in return. And we expect these inbound and outbound links to work, otherwise the World Wide Web would be pretty boring to browse.A stronger contract is to treat URL
24日前

Implementing a Baybayin abugida input method on keyboard hardware
leanrada.com notes
For RSS readers: This article contains interactive content available on the original post on leanrada.com.I made an input method to write Baybayin, a syllable-based writing system, using Roman letters on a hardware keyboard. Banner photo lifted from a Reddit post showing Baybayin keycaps. This post is about a real Baybayin keyboard though.The writing systemBaybayin is an ancient writing system in which a consonant-vowel syllable is written as one letter — it is an abugida. By default, letters have an inherent vowel which is /a/. To indicate a different vowel, a dot is added above or below the letter.ᜃ (ka): The base letter on its own is the consonant plus the vowel ‘a’. ᜃᜒ (ki): A dot above makes it end with ‘i’ / ‘e’. ᜃᜓ (ku): A dot below makes it end with ‘u’ / ‘o’. ᜃ᜕ (k): A slash cancels the vowel (it’s called a virama).An example of a full word is ᜆtaᜆ᜕tᜎᜓlo(tatlo), which means the number three.HTML tip: I’m using the <ruby> element to annotate Baybayin pronunciation in this post,
1ヶ月前

Editing my website directly in the browser
leanrada.com notes
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 platf
6ヶ月前

Making my GitHub heatmap widget
leanrada.com notes
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 ContribsScraping the dataFirst, 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/contributionsLoading this endpoin...
6ヶ月前

Vibecoding Lima Gang, web-based map visualization
leanrada.com notes
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: WikimediaThe 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.VibecodingThis 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...
8ヶ月前

My own RSS reader
leanrada.com notes
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 modeYou 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
9ヶ月前

Simple live reload for developing static sites
leanrada.com notes
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
10ヶ月前

Language evolves, so we must too. On generative art
leanrada.com notes
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 HobbsExhibitionr/generative - Has the word 'generative' been hijacked by AI?I think it's time to move on to a different label. Words are forcommunication, and if a word results in failure of communication, weshould either agree on a common definition (we can’t) or just use betterword.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 themedium:Procedural art Algorithmic art Creative codingIt’s j
10ヶ月前

Minimal CSS-only blurry image placeholders
leanrada.com notes
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
1年前

Inline rendering with document.currentScript
leanrada.com notes
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().getFu
1年前

Rewriting my site in vanilla web
leanrada.com notes
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 READMEI 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
1年前

CSS sprite sheet animations
leanrada.com notes
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 cloverInteractive content: Visit the website to play with interactive content!Alternative text: spinning cloverInteractive content: Visit the website to play with interactive content!Alternative text: spinning cloverOn 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 blocksThe bandwidth benefit has been largely mitigated by HTTP/2 now, but sprite sheets have another purpose: animations! Displaying animations is one of the prima...
1年前

Centering a div in a div in 2020
leanrada.com notes
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.
1年前

I made an app to fix my motion sickness
leanrada.com notes
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 op...
1年前

Stop using ease-out in your UIs!
leanrada.com notes
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 overusedLet’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 e...
2年前

Creating a halftone effect with CSS
leanrada.com notes
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 demoToggle 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 basicsTo 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!Alte
2年前

AI art is not generative art
leanrada.com notes
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 unfortun
2年前

htmz story
leanrada.com notes
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 c...
2年前

Pure CSS single-page app routing
leanrada.com notes
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 tabExplanationThe :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 sectionNotice 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 tabExperiment: Nested routesOn...
2年前

getDuolingoStreak()
leanrada.com notes
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...
2年前