DEV Community

Cover image for Curious Geckos 🦎🦎🦎: The Most Precise CSS-only Position-Aware Mini-Game!
Philip Warkentien II
Philip Warkentien II

Posted on • Edited on

Curious Geckos 🦎🦎🦎: The Most Precise CSS-only Position-Aware Mini-Game!

A CSS-only, no JS, no checkbox, position-aware (hover/touch aware) interactive gecko-luring pastime!

Β 

TL;DR I revisited an old CSS-only hack with new CSS features and some ingenuity to bypass rendering limitations and achieve as much as a 25.6x precision gain over the best previous solution – getting much closer to native position awareness! Try out the mini-game, play with just the β€œdebounced CSS algorithm,” and if you are in a rush, start reading from Section 2.


Intro

We're gathered here today to gain awareness about an ingenious CSS trick I call CSS-only Position Awareness. We could also call it Position-Aware CSS, but it fails to unambiguously state that no JavaScript was used.

It's based on a known trick, but, as you'll soon find out, CSS :has enabled us to push it further. And so we did. Besides, AI still has no clue about it and maybe neither do you. Here's what ChatGPT had to say this morning – March 27th, 2024:

Screenshot of ChatGPT, see caption below


transcription:

πŸ‘¨ β€” Is it possible to achieve position-awareness with only CSS and HTML?"
πŸ€– β€” "No, it's not possible to achieve position-awareness using only CSS and HTML. JavaScript is necessary to interact with the mouse events and update CSS properties or HTML content based on the mouse's position."

This article will be interactive and hopefully inspiring. So, stick around! (Or at least scroll to see the cute photos)


Table of Contents

  1. The Idea
  2. Position Awareness
  3. Building a Position-Aware Game
  4. Cast and Credits
  5. Previous Art
  6. FAQ

1. The idea

Back in September 2023, I was custom styling a <table> component and ran into a fun use case for the :has() pseudo-class. Without resorting to a "CSS-grid table," but using the actual – correct and accessible – <table> markup, I could now highlight not only the row but also the column being hovered!

See it in action:

πŸ’‘: [ Fun fact ] Sten Hougaard discovered this same trick a month later and got featured on CodePen's Instagram. πŸ‘

What stood out was how little CSS I needed to write. All it took was 37 lines of pure CSS to highlight a 7-column table by any given number of rows. What if I needed more columns? Could I refactor this CSS algorithm to support any number of columns? While digging for this answer I stumbled upon something else, a better algorithm for CSS-only Position Awareness. And that's where the game comes in.

Before we get all scientific, let's play the mini-game!

Β 

Click to play Curious Geckos

Β 

πŸ“™: Supported on all devices and modern browsers. For the best mouse-tracking UX try Chrome Desktop (or Edge Desktop)

This was all achieved without a single line of JavaScript! You're welcome to double-check and disable JavaScript on your browser.


2. Position Awareness

A code is position-aware when we can pinpoint the --x and --y coordinates of a cursor on the screen and make these values available for any element on the page. To achieve this without JavaScript requires a position grid to detect which subsection of the screen is being hovered. Ideally, we would have a position cell per pixel, but we aren't there yet. We might, however, be just a Moore's Law cycle away.

Previous iterations of the Position Grid were modest, only tracking 4 quadrants and calling it Direction Awareness. It evolved into 10x10 grids (100 cells) called Mouse Tracking, some as large as 16x16. However, Mouse Tracking was not the best name for something that should include track pads and touch devices. Now it evolves once more, with the addition of :has and dynamic rendering. I present to you the Position Awareness grid, with 81x81, 6561 cells!

There's a catch to Position Awareness: we change the expected behavior of hovering on a page. Instead of tracking :hover interactions with DOM elements, we are tracking interactions with subsections of the screen.

Here's a β€œdebounced” Position-Aware CSS algorithm in action:

πŸ–±οΈπŸ’» : Intended for mouse and trackpad devices.

πŸ“™: 81x81 grid for Desktop, 27x27 grid mobile

Β 

2.a.: Precision Gains

Previous approach was achieved by placing non-nested cells before a target element, and transferring placement coordinates through the β€œany sibling ahead” selector .cell:hover ~ .position-aware-element. This overloads the wrapper component with a huge number of child nodes and can impact performance.

Here's a brute-force image generated by painting each pixel with CSS to illustrate what happens when we style too many elements. Hit play at your peril 😱.
Brute force image generated with CSS, painting a pixel grid with CSS

Current approach takes advantage of the :has() pseudo-class which supports nested position cells. By nesting a single level deep we reduce the max number N of children cells to the square root of N. E.g.: a previous shallow position grid with 100 children cells can now be represented with 10 children, each with 10 children. This way, every single cell has only at most 10 children. Imagine if we nest even deeper! This two-level approach hits a performance cap at around 33x33 (1089 points of awareness). This is the resolution I'm using for Curious Geckos. Beyond that limit I would need to create dynamically rendered grid subsections, like I showcased on the blue Position-Aware CSS example above. This is similar to how 3D game engines dynamically increase the level of detail on elements near the camera.

πŸ“™: More on this in a future article


3. Building a Position-Aware Game

Now that we have a Position-Aware Grid filled with (x, y) coordinates, how do we go from --x and --y values to a working mini-game?

Here's the trimmed version of the steps:

3.1.: Feed --x and --y coordinates into the πŸͺ°(fly) and 🦎(gecko) wrappers

@for $i from 1 through 33 {
   .position-aware-container:has(tr:nth-child(#{$i}):hover) {
      :is(.fly__wrapper, .gecko__placement) {
      --y: #{($i - 1) * $axis-step};
      }
   }

   .position-aware-container:has(td:nth-child(#{$i}):hover) {
      :is(.fly__wrapper, .gecko__placement) {
      --x: #{($i - 1) * $axis-step};
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

3.2.: Use CSS calc to rotate the πŸͺ°(fly) by a centrifugal angle
angles

3.3.: Create SVG puppets for the geckos:
SVG Puppet

Β· 3.3.a.: A Puppet, also known as a Rig Model, is a technique used for animation. You might have seen it before on my portfolio
Tarantula

πŸ“™: More on this in a future article

Β· 3.3.b.: Group each body part in a hierarchical structure. E.g.: If I rotate the shoulder, the entire arm rotates.

Β· 3.3.c.: Anchor geckos by their thorax to facilitate head angle calculation:
Trigonometry diagram showcasing how the Gecko's head rotation angle is being calculated

--head-angle: max(
   var(--min-head-angle),
   // limit CCW head rotation
   min(
      var(--max-head-angle),
      // limit CW head rotation
      calc(
         (atan2(var(--dy), var(--dx)) * var(--head-angle-intensity, 1)) - calc(
               (
                  var(--head-initial-rotation, 0) * var(
                     --head-angle-intensity,
                     1
                     ) * 1deg
               ) + var(--head-angle-correction, 0deg)
            )
         )
   )
);
Enter fullscreen mode Exit fullscreen mode

Β· 3.3.d.: Split the head rotation in three for a more natural motion:

.gecko__neck__body {
   --rotate-member: calc(var(--head-angle) * 1 / 6);
}
.gecko__neck__head {
   --rotate-member: calc(var(--head-angle) * 1 / 3);
}
.gecko__head {
   --rotate-member: calc(var(--head-angle) * 1 / 2);
}
Enter fullscreen mode Exit fullscreen mode

3.4.: Set attack zones per gecko and per @media query.
Attack zones: position grid cells that trigger the gecko's attack animation when hovered

3.5.: Detect attack zone :hover and trigger attack state

// ----- Gecko 1 attacks ----- //
:is(
   .gecko-trap-1_base,
   .gecko-trap-1_min200,
   .gecko-trap-1_min250,
   .gecko-trap-1_min300,
   .gecko-trap-1_min350,
   .gecko-trap-1_min450,
   .gecko-trap-1_max166,
   .gecko-trap-1_max150,
   .gecko-trap-1_max125,
   .gecko-trap-1_max100,
   .gecko-trap-1_max75,
   .gecko-trap-1_max66,
   .gecko-trap-1_max50
) {
body:has(& span:hover) {
Enter fullscreen mode Exit fullscreen mode
  • Expand the hovered cell to full-screen. This locks hover to the last hovered cell. Locking the attack state.
  • Attack state freezes fly movement and animates the desired gecko.

3.6.: Slide in a pulsating call-to-action with a higher z-index, this will trigger the restart state.

3.7.: Add some flourishes:

  • tail animations
  • fine-tuned gecko placements to best fit the aspect ratio
  • Black and White gecko's resting arm changes position based on the @media query for best composition

3.8.: You're done!


4. Cast and Credits

cast




Super Hypo Tangerine Leopard Gecko - by Tamara Locke
Oreo Whiteout African Fat-Tailed Gecko β€” by unknown
Mandarin Tangerine Leopard Gecko β€” by CB Reptile


5. Previous art (Post-project research)

Although many developers have tackled some form of Position Awareness, I would like to highlight a few:

  1. Fabrice Weinberg, June 2013: 2x2 (first ever?) Direction-Aware Grid (Code)
  2. Gabrielle Wee, Jan 2017: 4 cells. Beautiful direction-aware effect. (Code)
  3. Gabrielle Wee, April 2018: 10x10 grid with 100 individual <a> tags and 100 individual style definitions. (Code) > "It would be smoother with more links but also would take longer to load, so I only used 100 instead of something like 1000." > – Gabrielle Wee's notes on this Pen
  4. Christopher Joshua, Nov 2019: 16x16 grid with 256 individual <i> tags and 768 lines of CSS just for tracking. (Code)
  5. Honorable Mention (1) Jane Ori's amazing etch-a-sketch with an impressive 75x50 grid! (Code) Genius state tracking with the space toggle hack + animations. Β Β  ______________________________________ Β Β  (1): Honorable mention since it achieves Position Tracking but not Position Awareness. Β· 5.a.: Position information is dynamically set, not extracted. Β· 5.b.: Cell can't be hovered twice. Β· 5.c.: Regardless, it's impressive and deserves to be shared!

6. FAQ

  1. Is this really a game? Well, it's as much of a game as playing catch is a game. ⚾ It's the reason I called it a mini-game ⚾ or a pastime.
  2. Why make this? To Flex... and to Grid.
  3. What do you mean by β€œno checkbox”?
    • Most CSS-only games use checkbox for state management, but not this one.
    • This game runs completely on :hover events, even to track victory/restart.
  4. UX is jittery, can I improve it? Troubleshooting:
    • Try Chrome Desktop or Edge Desktop for the best UX.
      • If you're a MacBook user, you can emulate Chrome inside Safari: Safari > Develop (tab) > Open Page With > Google Chrome
    • Use a JS disabler (extension).
    • If it's still slow, use Firefox or Safari Desktop for a debounced hover tracking experience.

Β 
Β 

Thank you for reading!

You're welcome to ask questions and speak your mind.

Follow me on X, @warkentien2

Top comments (4)

Collapse
 
tiptronic profile image
Andy Fuchs

Very, very cool!
Great work!!!

Collapse
 
1link profile image
1Link.Fun

Cool~

Collapse
 
ellen_web profile image
Ellen

Adorable! *-*

Collapse
 
netsi1964 profile image
netsi1964 πŸ™πŸ»

Great work and interesting post! Thanks for mentioning of my pen at CodePen :-)