Sunday, December 31, 2023

signals and the danger of the new hotness

Like this video shows, Signals are fairly promising, fair cool technology from Preact, don't go plowing in - if you naively look to examples from the Preact page, you'll quickly find cases that (including the initial example with the counter!) that don't work the same in React - I think React is back to using useSignal vs just the Preact-y raw signal aspect...

Wednesday, December 27, 2023

the ux of social media platforms

A piece on Usenet's the September That Never Ended mentioned Google Groups will be dropping Usenet support, which makes me sad. I haven't really used Usenet for twenty years, but for the ten years before it was really important to me, and I liked seeing it was still going on and to be able to look up the odd half remembered post.

Usenet really had a good vibe; the idea of bring your own client and use it across a variety of topic rooms, each forming their own community was great - my favorites were,,, and comp.sys.palmtops.pilot. (A friend of mine has a conspiracy theory that Usenet was too distributed and uncontrollable and so was repressed by the Powers That Be in favor of more centralized forms of social media...)

It made me think about social media forums I've lived in over the years. Each tends to encourage a certain style / length of post, has different types of message continuity (threads, etc), makes it easier or harder to recognizing recurring authors, and has different styles of if you rely more on following people or sipping from the main firehose.

I had a weirdly geeky urge to categorize what I've most used over the years... (my current favorite in blue) These are all based on my judgements of how I or most people use it:
Forum Post Lengths Crowd Size Author Identity Follow or Commons text vs image
Usenet Long Many Medium Groups .Sig Commons (per Group) Text
Livejournal Very Long Friends List Avatar Follow Text
Blog Comments Short Private-ish Name Commons Text
Slashdot Short Large (Geeks) .Sig Commons Text
Atari Age Forums Medium Medium-Small (Gamers) Avatar + .Sig Commons (in Topics) Text
Facebook Medium-Short Real Life (+Algorithmy) Name + Avatar Follow (+ Algo) Mixed
Twitter Very Short Very Algorithmy Avatar Follow (+ Algo) Mixed
Reddit Short Many Medium Channels Username Commons (per Channel) Text
Tumblr Medium Medium ("Mutuals") Avatar Follow Images
WhatsApp Short Private Avatar Commons Text
Slack Medium-Short Private Avatar Commons (in Topics) Text
Discord Short Private Avatar Commons (in Topics) Text
Facebook connects me with a wider range of people from all parts of my life, and despite the privacy concerns and what not, I appreciate how easy its been to share as many photos as I'd ever want, and get them in front of folks, abeit in a haphazard way.

I love the community of tumblr and it's my favorite source of stuff to repost on my blog - but I haven't figured out how to get "followed", so it's mostly a read-only medium for me so far.

Slack closed-garden is my favorite community types now - if you find the right bunch of people (that balance of people who post a lot, and maybe some people who mostly lurk but chime in) it's fantastic. (On paper Discord has the same potential, and is a bit more hip, but somehow the UI for threading and private messaging is horribly confusing, and the whole things gives me Reddit-ish "I can't follow things" vibes.

Thursday, December 21, 2023

two flavors of hacks

Earlier this year I put up a website for people in my community street band to store group photos. I used the metaphor of a "shoebox", something you can throw stuff into without worrying about the hierarchy or structure. 

The first pass I put up this summer was super barebones, just a form to upload a batch of images and give the batch a date and attach some other metadata (event descriptions / people shown, if it's "best of", etc) Then the gallery was just all the raw photos on a giant page.

The band threw a holiday party, and at the last minute decided to have a projector with photos showing. I hadn't really set things up for a slideshow - but my buddy's Window laptop had a slideshow feature, so hack #1 one was that by saving the big page as "Webpage, Complete" we had a single big folder with all the images, and could then let the local viewer do its thing.

Seeing the photos from ten years of activist and community banding were emotionally resonant, so I was inspired to push a little further... I used Imagick to build a simple resizer for thumbails in PHP:

        $image = new Imagick($originalFile); 

        $d = $image->getImageGeometry(); 
        $w = $d['width']; 
        $h = $d['height']; 

        $max = 150;

        if($w > $max || $h > $max){
            if($w > $h) {
            } else {


Ran that against the existing images and put it into the upload flow. 

Once I had that, I made a new thumbnails gallery - much better download times...

But I thought I could do better with the slideshow aspect - so enter hack #2, I liked the idea of a 2 dimensional display of all the thumbnails (I took the squareroot of the image count to figure out how many to display across) that would pick an image at random, pan across the other images to it, and then zoom in. I made it as a p5 canvas webapp. (All the source is embedded in the page) It runs a little timer and just goes from image to image, or you can override that with the arrow keys. Plus it displays the metadata from its batch upload, so you have a sense of the date and event that the Windows slideshow lacked.

When things settle down, I might refactor and make a more reusable version of this. I think panning across a bunch of small thumbnails is really evocative.

Wednesday, December 20, 2023

the ux of security vs convenience vs human fallibility

TL;DR: iPhone owners should get the iOS 17.3 update when it's available and activate the "Stolen Device Protection" - and always be suspicious when you have to use your passcode among strangers!

Earlier this year there were WSJ reporting about an iPhone scam - it turns out if someone has the passcode to your phone when they steal it, they can easily reset your Apple account password - at which they can lock you out of all your stuff, as well as blocking the device from "Find My" and stopping you from wiping your data from the phone remotely. 

(So the scam at a bar might be to offer to take your picture with your phone, then quietly use the button presses that would require you to unlock the phone with passcode, not just Face ID, so the scammer would have the code when the snatched the device.)

One might question why Apple would allow the mere 6 or so digit passcode to authorize a general password reset, but it turns out that a LOT of people are very bad at remembering their account password (which doesn't get used all that much.) So Apple kept it in as a reasonable backdoor for restoring access, since the number of people losing their critical password dwarfs the number of snatched phones.

Daring Fireball reported on an upcoming fix from Apple and I'm impressed by the nuance of the workaround... with Stolen Device Protection enabled, if you're away from a familiar location (home or work) the device will require face or touch id and then wait for an hour before resetting the main password.

I guess a shoulder-surfed passcode and stolen phone still allows plenty of shenanigans, but it's nice that this more egregious form is being quashed.

Friday, December 15, 2023

Care with trailing slashes for POST to index.php'd urls

Because PHP is so tragically unhip (and it's nice to decouple your webapp's URLs from any particular technology) I use a few different strategies for routing across PHP projects. 

I've rolled my own router before with some .htaccess trickery, but the easiest way is to set up paths via folders, i.e. using "index.php" to allow the path to be, say "/action/login/" and not "/action/login.php".

One gotcha with that - if you POST to that kind of URL and fail to include the trailing slash, your server may redirect your POST to a GET of the URL with the slash - dropping the POSTed data

Thursday, December 14, 2023

history of webhacks

A History of Weird HTML Hacks. I always scratched my head about non-semnatic tables being SUCH a nono (especially when some of the CSS hacks for the "Holy Grail Layout" were just ridiculous)

 (Catching up some newsletters etc hence all the small updates)

webdev advent calendar

 On the devlishly good site HTMHell, a web dev advent calendar

(Reminds me of my recent efforts to look cool by reviving my old ed emberley animals advent - that is still some great looking stuff IMO.)

One gem already from day 3: you don't have to nest buttons (and other elements) within their form! This might come in very useful on my porchfest site, where I have a secondary form on the main page for handling uploaded band images...

(Looking into the "future" (from this blog entry point of view) Day 15: The Ghosts of Markup Past is a fascinating way the html world could have gone.)

the new hotness of css

CSS got some new cool stuff in 2023 - I really appreciate when functionality is added to browsers core, in a cross-platform standards based way. (I've always trusted the stability of browsers more than any given flavor-of-the-month web tool.)

I am glad to see "subgrid" solving a problem I've long wondered about with card  based layouts; how do you get card subsections to align with their siblings (or rather, cousins) 

Also the text-wrap stuff seems great for preventing orphans in layout and getting better line balance in general...

Wednesday, December 13, 2023

making a minimal multiplayer online game via Rune

I just found out about a cool game platform called Rune - it lets you play online multiplayer games with voice chat.

And in so many ways it's an indie game dev's dream come true: Rune handles the game state management and reconciliation and voice chat and social and provide a simple, framework agnostic js api.

Their Quick Start page has a nifty bootstrap:

npx rune-games-cli@latest create
npm run dev

Not only does that spit out a basic React template, but using VITE the terminal pops up a QR code that you can scan with your device (if it's on the network) and test on-device.

But, what if you're not coding in React? Their tic-tac-toe example uses barebones JS, but has a fair amount of tic-tac-toe-ness logic you'd have to remove to start from scratch. So here is my minimal quick start:

A Bare Bones Game

Lets make a simple game: the first player to click their button ten times wins! The UI will be a single button, and then your score and the other player's.

A minimal Rune game can consist of just 3 files: an index.html file to hold the structure of the page, a logic.js that syncs game logic across players, and a client.js that runs on each page to handle events and update the game display.

Here is the HTML for index.html

<!DOCTYPE html>
    <ul id="scoreboard"></ul>
    <button onclick="clickNow()">hit it</button>
    <script src=""></script>
    <script src="logic.js"></script>
    <script src="client.js"></script>   

That is just an unordered list for our score and the opponent's, a button to press, and then we include the Rune js and then our custom code.

Next step: logic.js. This can be as simple as a single call to Rune.initLogic(args)

Barebones arguments are minPlayers, maxPlayers, a setup() function that initializes game state, and then a plain object of actions the clients can call.

    minPlayers: 2,
    maxPlayers: 2,
    setup: (allPlayerIds) => {
      const game = { scores: {} }
      for (let playerId of allPlayerIds) {
        game.scores[playerId] = 0
      return game;
    actions: {
      incrementScore(payload, { game, playerId }) {        

Setup returns the game object. In our case, we just need a plain object for mapping playerid=>score. Our only action is "incrementScore()". We will actually ignore any orguments passed in (to that payload placeholder), and just increment the score for the playerId.

Finally we can write our client.js. The critical line here will be a call to Rune.initClient(args) - at minimum, args should have a value for "onChange", alerting the client that the game state has updated. We only need three things:

function clickNow (){

function onChange({ game, yourPlayerId, players }) {
  const myScore = game.scores[yourPlayerId];
  const otherPlayerId = Object.keys(game.scores).find(key => key !== yourPlayerId);
  const otherPlayerScore = game.scores[otherPlayerId];
document.getElementById("scoreboard").innerHTML = `
      <li>My score: ${myScore}</li>
      <li>Their score: ${otherPlayerScore}

Rune.initClient({ onChange })

So clickNow is our click event handler that calls the incrementScore function we set up in logic.js. We then create our onChange handler that has some useful information: the game state, this player's id, the list of players, etc. 

We can immediately look up our own score via the game.scores object, since we know our player ID. But for a two player game we have to do a little "find()" logic to get the score value that is NOT ours. Once we have that, we will fill in the guts of our scorebard with two list items.

Finally, we call initClient with our onChange handler.

We're ready to give it a whirl!  If you don't have a server handy, from the command line you can just naviagate to the folder with 3 files and run
npx serve@latest

And boom! Rune has a nice side-by-side mode that pretends to be two (or more) phones at once:

Congrats, you made a multiplayer game! You can click either player's "hit it" button and see the display advance for both players.

But wait... we want first to TEN, this just lets you play forever...

Winner Winner Chicken Dinner

So how do we tell Rune the game is over? We just call Rune.gameOver(); That takes a single object that can be a map of playerIDs to player score... conveniently we have that handy, since it's the same structure of game.scores - so we can just add this line to incrementScore , right after we ++ the relevant score:
      if(game.scores[playerId] >= 10) {

Now Rune automatically constructs a little results screen for us:

So there's a game over, but that's not much of an accolade, it's not instantly clear who won. But, if instead of numeric values, we can set each player ID key to "WON" or "LOST". and Rune will record those results. Lets just write some easy to follow code to make a new onject mapping players to the end is the entire incrementScore function :

    incrementScore(payload, { game, playerId, allPlayerIds }) {        
        if(game.scores[playerId] >= 10) {
          const results = {};          
          for (let playerId of allPlayerIds) {
            results[playerId] = game.scores[playerId] >= 10 ? "WON" : "LOST";

Now we get a result like 

That's much more clear. (In the real Rune UI it will be fancier.)

Two's Company, Three is More Fun

But, a two player game might be a little meh. Lets kick it up to 4 players!

We'll start by updating our scoreboard logic in client.js to show the player names, since pretty soon it won't be just "us vs them":

Lets just replace the line updating the innerHTML:
  document.getElementById("scoreboard").innerHTML =
      .sort((id1,id2)=> game.scores[id2] - game.scores[id1])
        return `<li>${players[id].displayName}: ${game.scores[id]}</li>`})

We'll use some of those nifty js array functions - first we get the keys of the players object, then we sort by looking up on each playerId current score. Then we build the list item for each player, and join it all together to make the guts of the scoreboard.

One final improvement: it might be nice to highlight the players name within the list, so they can see where they currently are in the standings. Lets just do a simple conditional color style inline. Here is our new onChange, removing some of the old unneeded Us vs Them logic:

function onChange({ game, yourPlayerId, players }) {
  document.getElementById("scoreboard").innerHTML =
      .sort((id1,id2)=> game.scores[id2] - game.scores[id1])
        const itsAMe = id===yourPlayerId;
        return `<li style='color:${itsAMe?'red':'black'}'>${players[id].displayName}: ${game.scores[id]}</li>`})

The only difference from before is setting itsAMe if it's the current player in this item, and using red to highlight it if so.

Ok, one FINAL final improvement to the client - lets add a little CSS to the index.html to get a sans-serif typeface - we're not barbarians:

    body {
        font-family: sans-serif;
    button {
        display: block;


So now all we need to do is tell our logic.js that maxPlayers is 4, and handle what happens when a player joins or leaves.

    maxPlayers: 4,
Is a simple enough change...and just like we passed an "actions" object to Rune.initLogic() we can pass an events object with handlers for players joining or leaving.. all we need to do is add and initalized their score as they join, and remove it if they go.

  events: {
    playerJoined: (playerId, { game }) => {
      game.scores[playerId] = 0
    playerLeft:(playerId, { game }) =>{
      delete game.scores[playerId];

Here's the result... I admit I enjoy the character names Rune uses:

So, tadah! You should now have a solid base to know how to wire up your game to Rune's great player management system no matter what JS framework you are using.

the power of qr for local dev

Looking to pick up some React Native - one trick I've seen two places now (The quick deploy environment Expo, and then a particular game hosting environment using Vite) is you start a local server, then it gives you a QR code in the terminal that lets you hop onto your server via your device. That's pretty dope!

I love the intersection of 80s ASCII Art and the ongoing  revival of early-2000s QR Codes  (Reminds me of an old joke that is now outdate: - "What's a pirate's favorite visual URL encoding?" "Q-Arrrrrrr?" "No, they don't scan them either")

Also, at other places in their example code they just have you run 

npx serve@latest

which fires up a baby webserver. I will add it to my old piece on got python? you got a webserver...

Tuesday, December 12, 2023

browsers are too big

Interesting piece from three years ago about the impossibility of creating a new web browser from scratch - there would just be too much they'd be expected to do.

It's a shame, because I still think browsers in general are one of the best basic platforms for "open" type work; providing great experiences in an egalitarian way. I don't think a world where every interaction with a company starts with "download our app" (or maybe worse, as a plugin to a social media platform) is one anyone wants...

Thursday, December 7, 2023

the year in re-vue

Big year for Javascript. Who knew Vue would seem like the most stable framework out there?


random firefox gripe - no X to clear on input="search"

I really try to stick with Firefox; with so many other browsers going down the webkit route I think it's healthy to support an alternative.

But chrome and safari have a convenience function that's great... if you have 

<input type="search">

You get a something like

It's great... it's only there on :hover or :focus and when there's content in the box and for a search or filter it's super convenient. 

Here's a codepen that does it for stuff including Firefox... no js needed, but a kind of annoying amount of non-semantic markup and CSS. I can't figure out if it's being seen as a bug or as a non-standard feature they won't enable by default...

It's frustrating though. It's such a nifty quality of life feature, there's not a lot of downside that I can see...