Just over a week ago while scrolling YouTube shorts after work on a Friday, looking for something to want, this 35 second video stopped me and took 100% of my attention
After one and a half playthroughs, I paused it, tilted my head back and stared off into space as ideas rushed into my mind.
"I could make Chess in CSS with the CPU Hack."
"Could I connect it to that tablebase without JS?"
"The only way CSS can reach out to a server is with background ima---!!!
It can also use
url()
ascontent
on a pseudo element, which can resize the parent, theninset: 0px
an absolute positioncontainer
inside of it and I can measure that with tan(atan2()) aaand that's my GET request's response data...If the element resizes based on that content, it would be useful to have it off screen to avoid causing scrollbars - plus better DX for anyone using would if it didn't need a weird one-off setup that you had to build inside of... So how do I get the data out --
I've known how to lift data to
:root
since the CPU Hack since it already does that on a much smaller scale but never had a reason to try..."
Then I paused as my eyes darted around without any awareness of my physical surroundings, looking for edge cases among the components I've assembled - "What can't be done here?"
...
"I'd have to
@container style(--x: N)
wrap my urls so gamestate can choose what ones to use but I'd only have a hardcoded list of possible endpoints until we geturl()
param()...
AH WAIT! I could also generate the necessary request url and prompt the player to SMEAR it into the page, shrouded as the UI to confirm their move..."
I leaned back and splayed my fingers and palms, panning my eyes across the room slowly as excitement built inside me.
"16 bits on the width and 16 bits on the height is huge but well within possible..."
"I think that's it! I can do it!"
So I started building my chess board in CodePen, where all my ideas start their journey into realization. "I guess it's time to learn Chess so I know how to build the API request and to determine if ~32 bits was enough response data for it to communicate the CPU move..." I played Chess briefly back in my teenage IRC-loving, game hacking days and quit at the end of a losing streak with hysterical laughter when I devised a way to cheat every move of a match and still lost. (I made my friend go first then used the move he wrote in IRC against the "expert" computer player in the chess game I downloaded, then communicated to him what the CPU did as my move.) I was COMPLETELY oblivious to how much of Chess I knew absolutely nothing about. I searched for existing APIs I could just wrap, assuming I'll generate an image on my server use the height/width as the response data. I quickly found chess-api.com which can respond with a next move given "FEN" as input. I learned FEN, which lead to even more discovery of how much I didn't know about Chess. I knew I could generate the SMEAR Hack to build the gamestate in FEN format, and most importantly, I measured that I could indeed bitpack the important response data Now, I know with more certainty I can program Chess against a CPU Player in 100% CSS. I also knew at that moment, I should definitely rewatch The Queen's Gambit because I absolutely loved it and can't wait to pick up on new details.The rest of this plot exposition isn't directly relevant, but I'm enjoying the moment so I'll leave the writing here for anyone reading along
move: "b7b8q"
into 32 bits. (It's an 8x8 board and I know where everything is, not much fundamentally has to be provided in the response.)
The universe is talking to me and I'm putting the excitement into action to keep the loop flowing.
Time to test the CSS API URL GET ALL CAPS TRAIN idea and see if it works.
Demoing the basics
First, establish appropriate background noise for this endeavor, The Queen's Gambit.
Next, confirm measuring an image.
Easy enough! Just make sure all the content inside stays absolute so it doesn't change the size of the container holding the image.
Throw in a test for changing what URL we GET...
Proof of concept confirmed, next leg of this journey was lifting the response data to :root
so I can make all the setup invisible and at least 65535px off screen to the top and left.
I knew this was going to be heavy, esoteric CSS+HTML and I make things that help people make things, so right from the start, I built this with the intention of making it an easy-to-use library.
In version 1, Copy plop some specific (bulky) HTML in your page with a handful of options built in to make the requests and do the :root
lift for you.
Import the library and specify the API endpoint from your CSS,
@import url(https://unpkg.com/css-api-fetch@1/api-fetch.css);
@container style(--api-id: 1) {
.api-fetch { --api-fetch: url(https://css-api.propjockey.io/ip-address.php); }
}
And the data will be on :root
for the rest of your app once it's ready.
But it gets better, fast!
First Public Demonstration, Chrome Only, :root
GETting a user's IP Address with CSS.
I'm not going to go into super specific implementation details in this post of how exactly that CSS+HTML lifts the data to :root
in that original public demo because a couple of CSS loving friends pointed me in a much easier direction I hadn't learned about!
If the specific-element descendant knows it's The CPU Hack lets us accumulate the state as different elements are So if the data we want to lift is '512', we can sequence 'thousands', 'hundreds', 'tens', and 'ones', telling the child to present an element 0 to 9 representing the value it's holding in that position. Child knows what numeric position we're asking for because state cascades, The CPU Hack accumulates it into one value then tells the child to stop reporting after the ones position is saved. The HTML in the demo linked above reveals a bit of what setup is required for that. The CSS code for it is here in v1 of css-api-fetch.but I will put this quick summary here for anyone interested
:root:has(.specific-element:hover) { ..state... }
:hover
, it can change to put a different element on top of where it currently is which causes the next element to be hovered, then :root:has(...:hover)
changes its state based on that element too, in a loop.:hover
'd..api-transfer-value-0:hover
, ...5:hover
, 1
, 2
.:has()
lifts the value because the child presented the corresponding digit for us to :hover
and read at :root
.
Second Public Demonstration - Cross Browser, no :root
The only difference here is that I don't lift it to :root and instead leave it in the container, DX requires you to wrap all of your content that needs access to the data in an absolutely positioned element inside of the hack's context.
In my original exposition I mentioned that I wanted to avoid this scenario, but it's cross browser and it has its uses too. Plus, it's not like you can't build your whole app inside the specific html setup.
You can see in the HTML tab just how much less effort is involved in there.
Just 4 specific nested html elements are required in the cross-browser version, then you build anything you want using your response data inside of that.
Third Public Demonstration - Chrome Only, :root, trivial
Kizu mentioned it might be possible to simplify so I went digging, then after not quite putting the dots together, T. Afif showed a possible way in that direction, I learned a ton from that article but was unsatisfied with the division and precision loss so I dove into the articles and amazing tools Bramus made and came up with a variation that satisfied my use cases.
As you can see in the CodePen's HTML the setup is so simple it's literally one line of HTML to fetch and your response data is on :root
. In this case I used a second element to present the data too. AMAZING!
I packaged it up for a v3 release making all the handoff mindless, putting the measuring stuff off screen already set up since it's not important.
I posted about the release, then T. Afif linked a different article demonstrating a similar view-timeline configuration for measuring that I worked out for this - without the division! :)
I highly recommend reading all the work I've linked above, there's some exciting, talented CSS people sharing their ideas -- and knowing something is possible opens entire worlds of cool ideas!
What is css-api-fetch
doing?
Importing css-api-fetch and adding a single line of html, you can fairly easily do API requests in your CSS and not have to consider the complexity of view-timeline yourself. I made it so you don't have to think about it, but for the curious, here's the concept and setup the library uses internally:
- Two animations setting a --ints
from
the max values your API will respond withto
0 - host (scope) the animations and their timelines on
:root
- create a measuring element sized to the max values your API will respond with, with non-static position, overflow hidden, etc to make it suitable for reading view/scroll positions.
- Throw it absolute offscreen to the top left so it doesn't cause scrollbars on your app.
- load the image
url()
inside of a nestedsize
container
's pseudo element'scontent
and setview-timeline
s for bothinline
andblock
corresponding to the animations in step 1&2. - The animations now report the size of the image to the whole document
Here's a demonstration of all 6 of the steps above, step 4 is commented out so you can see it. We'll use Lorem Picsum to emulate specific height & width api responses easily.
That's the fundamentals but the library sets up 4 separate API request/responses that you can use simultaneously without knowing how it works.
What's css-bin-bits
?
All 3 of the public releases have demonstrations that use a library I made ages ago, css-bin-bits that makes it super easy to do bitwise operations on 16 bit integers in 100% CSS and you don't usually have to write any calc() yourself - the library takes care of it all. For the 32 bit IP Address, using css-bin-bits
I was able to limit the size of the image to exactly 16 bits on a single dimension.
Without bitpacking the 32 bit IP Address into two 16 bit values, T. Afif demonstrates you could use a 255255px by 255255px
measuring container and split the response data out with decimal math - skipping css-bin-bits
. css-api-fetch
supports you setting any maximum, shipping with 99999
as the default maximum, much more than 16 bits (65535), but not quite 255255
lol.
How do you set up an API to respond with data in the image?
The lightest and most trivial way to pack data into the width and height of an image is to make the server respond with a bare-minimum SVG.
Here's how the IP address is packed and served in PHP, for example:
<?php
header('Content-type: image/svg+xml');
$adr = $_SERVER['REMOTE_ADDR'] ?: '255.255.255.255';
$hex = str_pad(implode(array_map('dechex', explode('.', preg_replace('/[^\d\.]/', '', $adr), 4))), 8, '0', STR_PAD_LEFT);
/* rx returns first 4 hex chars, removing up to 3 leading 0's */
$width = hexdec(preg_replace('/(^0{0,3})|(.{4}$)/', '', $hex) ?: '0');
/* rx returns the last 4 hex chars, removing up to 3 leading 0's */
$height = hexdec(preg_replace('/^.{4}0{0,3}/', '', $hex) ?: '0');
echo '<svg xmlns="http://www.w3.org/2000/svg" width="' . $width . 'px" height="' . $height . 'px"></svg>';
?>
The API requests you set up could easily accomplish a handful of low hanging fruit that CSS can't otherwise do without JavaScript
- RNG
- Current Time
- TimeZone
- Country Code
- Operating System
- or connection to any other api if you wrap it - such as chess-api
What comes next?
Chess?!
Not yet! Plus, Lilian has already made Chess in CSS!
I'm not sure how it works or if there's a publicly available demo, but I'm sure she'd answer any questions! :)
For now, my excitement is focused elsewhere......
I want more than 32 bits per request.
I want 512 bits per request.
No JavaScript.
Open Contact 👽
Please do reach out if you need help with any of this, have feature requests, want to share what you've created, or wish to learn more.
Top comments (1)
Banger