About

Saturday, May 21, 2022

Verevolf

Zeldman:

You may not know his name, but he played a huge part in creating the web you take for granted today. And he’s back—kind of.

That would be Glenn Davis and the Verevolf site Zeldman’s talking about. The site is a growing archive of Davis’s personal (and unvarnished) recollections pioneering the early web, like the one where he recounts the origin story of his uber-famous Cool Site of the Day.

Credit: Web Design Museum

Or the how an email he wrote out of frustration snowballed into the Web Standards Project.

Personal retellings of history can be fraught with emotion and Davis’s posts are no exception. What’s super cool is how his experiences augment other retellings, including Chapter 7 of our own Web History series where Jay Hoffman documents the evolution of web standards.

To Shared LinkPermalink on CSS-Tricks


Verevolf originally published on CSS-Tricks. You should get the newsletter.



source https://css-tricks.com/verevolf/

Thursday, May 19, 2022

Inline Image Previews with Sharp, BlurHash, and Lambda Functions

Don’t you hate it when you load a website or web app, some content displays and then some images load — causing content to shift around? That’s called content reflow and can lead to an incredibly annoying user experience for visitors.

I’ve previously written about solving this with React’s Suspense, which prevents the UI from loading until the images come in. This solves the content reflow problem but at the expense of performance. The user is blocked from seeing any content until the images come in.

Wouldn’t it be nice if we could have the best of both worlds: prevent content reflow while also not making the user wait for the images? This post will walk through generating blurry image previews and displaying them immediately, with the real images rendering over the preview whenever they happen to come in.

So you mean progressive JPEGs?

You might be wondering if I’m about to talk about progressive JPEGs, which are an alternate encoding that causes images to initially render — full size and blurry — and then gradually refine as the data come in until everything renders correctly.

This seems like a great solution until you get into some of the details. Re-encoding your images as progressive JPEGs is reasonably straightforward; there are plugins for Sharp that will handle that for you. Unfortunately, you still need to wait for some of your images’ bytes to come over the wire until even a blurry preview of your image displays, at which point your content will reflow, adjusting to the size of the image’s preview.

You might look for some sort of event to indicate that an initial preview of the image has loaded, but none currently exists, and the workarounds are … not ideal.

Let’s look at two alternatives for this.

The libraries we’ll be using

Before we start, I’d like to call out the versions of the libraries I’ll be using for this post:

Making our own previews

Most of us are used to using <img /> tags by providing a src attribute that’s a URL to some place on the internet where our image exists. But we can also provide a Base64 encoding of an image and just set that inline. We wouldn’t usually want to do that since those Base64 strings can get huge for images and embedding them in our JavaScript bundles can cause some serious bloat.

But what if, when we’re processing our images (to resize, adjust the quality, etc.), we also make a low quality, blurry version of our image and take the Base64 encoding of that? The size of that Base64 image preview will be significantly smaller. We could save that preview string, put it in our JavaScript bundle, and display that inline until our real image is done loading. This will cause a blurry preview of our image to show immediately while the image loads. When the real image is done loading, we can hide the preview and show the real image.

Let’s see how.

Generating our preview

For now, let’s look at Jimp, which has no dependencies on things like node-gyp and can be installed and used in a Lambda.

Here’s a function (stripped of error handling and logging) that uses Jimp to process an image, resize it, and then creates a blurry preview of the image:

function resizeImage(src, maxWidth, quality) {
  return new Promise<ResizeImageResult>(res => {
    Jimp.read(src, async function (err, image) {
      if (image.bitmap.width > maxWidth) {
        image.resize(maxWidth, Jimp.AUTO);
      }
      image.quality(quality);

      const previewImage = image.clone();
      previewImage.quality(25).blur(8);
      const preview = await previewImage.getBase64Async(previewImage.getMIME());

      res({ STATUS: "success", image, preview });
    });
  });
}

For this post, I’ll be using this image provided by Flickr Commons:

Photo of the Big Boy statue holding a burger.

And here’s what the preview looks like:

Blurry version of the Big Boy statue.

If you’d like to take a closer look, here’s the same preview in a CodeSandbox.

Obviously, this preview encoding isn’t small, but then again, neither is our image; smaller images will produce smaller previews. Measure and profile for your own use case to see how viable this solution is.

Now we can send that image preview down from our data layer, along with the actual image URL, and any other related data. We can immediately display the image preview, and when the actual image loads, swap it out. Here’s some (simplified) React code to do that:

const Landmark = ({ url, preview = "" }) => {
    const [loaded, setLoaded] = useState(false);
    const imgRef = useRef<HTMLImageElement>(null);
  
    useEffect(() => {
      // make sure the image src is added after the onload handler
      if (imgRef.current) {
        imgRef.current.src = url;
      }
    }, [url, imgRef, preview]);
  
    return (
      <>
        <Preview loaded={loaded} preview={preview} />
        <img
          ref={imgRef}
          onLoad={() => setTimeout(() => setLoaded(true), 3000)}
          style=
        />
      </>
    );
  };
  
  const Preview: FunctionComponent<LandmarkPreviewProps> = ({ preview, loaded }) => {
    if (loaded) {
      return null;
    } else if (typeof preview === "string") {
      return <img key="landmark-preview" alt="Landmark preview" src={preview} style= />;
    } else {
      return <PreviewCanvas preview={preview} loaded={loaded} />;
    }
  };

Don’t worry about the PreviewCanvas component yet. And don’t worry about the fact that things like a changing URL aren’t accounted for.

Note that we set the image component’s src after the onLoad handler to ensure it fires. We show the preview, and when the real image loads, we swap it in.

Improving things with BlurHash

The image preview we saw before might not be small enough to send down with our JavaScript bundle. And these Base64 strings will not gzip well. Depending on how many of these images you have, this may or may not be good enough. But if you’d like to compress things even smaller and you’re willing to do a bit more work, there’s a wonderful library called BlurHash.

BlurHash generates incredibly small previews using Base83 encoding. Base83 encoding allows it to squeeze more information into fewer bytes, which is part of how it keeps the previews so small. 83 might seem like an arbitrary number, but the README sheds some light on this:

First, 83 seems to be about how many low-ASCII characters you can find that are safe for use in all of JSON, HTML and shells.

Secondly, 83 * 83 is very close to, and a little more than, 19 * 19 * 19, making it ideal for encoding three AC components in two characters.

The README also states how Signal and Mastodon use BlurHash.

Let’s see it in action.

Generating blurhash previews

For this, we’ll need to use the Sharp library.


Note

To generate your blurhash previews, you’ll likely want to run some sort of serverless function to process your images and generate the previews. I’ll be using AWS Lambda, but any alternative should work.

Just be careful about maximum size limitations. The binaries Sharp installs add about 9 MB to the serverless function’s size.

To run this code in an AWS Lambda, you’ll need to install the library like this:

"install-deps": "npm i && SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm i --arch=x64 --platform=linux sharp"

And make sure you’re not doing any sort of bundling to ensure all of the binaries are sent to your Lambda. This will affect the size of the Lambda deploy. Sharp alone will wind up being about 9 MB, which won’t be great for cold start times. The code you’ll see below is in a Lambda that just runs periodically (without any UI waiting on it), generating blurhash previews.


This code will look at the size of the image and create a blurhash preview:

import { encode, isBlurhashValid } from "blurhash";
const sharp = require("sharp");

export async function getBlurhashPreview(src) {
  const image = sharp(src);
  const dimensions = await image.metadata();

  return new Promise(res => {
    const { width, height } = dimensions;

    image
      .raw()
      .ensureAlpha()
      .toBuffer((err, buffer) => {
        const blurhash = encode(new Uint8ClampedArray(buffer), width, height, 4, 4);
        if (isBlurhashValid(blurhash)) {
          return res({ blurhash, w: width, h: height });
        } else {
          return res(null);
        }
      });
  });
}

Again, I’ve removed all error handling and logging for clarity. Worth noting is the call to ensureAlpha. This ensures that each pixel has 4 bytes, one each for RGB and Alpha.

Jimp lacks this method, which is why we’re using Sharp; if anyone knows otherwise, please drop a comment.

Also, note that we’re saving not only the preview string but also the dimensions of the image, which will make sense in a bit.

The real work happens here:

const blurhash = encode(new Uint8ClampedArray(buffer), width, height, 4, 4);

We’re calling blurhash‘s encode method, passing it our image and the image’s dimensions. The last two arguments are componentX and componentY, which from my understanding of the documentation, seem to control how many passes blurhash does on our image, adding more and more detail. The acceptable values are 1 to 9 (inclusive). From my own testing, 4 is a sweet spot that produces the best results.

Let’s see what this produces for that same image:

{
  "blurhash" : "UAA]{ox^0eRiO_bJjdn~9#M_=|oLIUnzxtNG",
  "w" : 276,
  "h" : 400
}

That’s incredibly small! The tradeoff is that using this preview is a bit more involved.

Basically, we need to call blurhash‘s decode method and render our image preview in a canvas tag. This is what the PreviewCanvas component was doing before and why we were rendering it if the type of our preview was not a string: our blurhash previews use an entire object — containing not only the preview string but also the image dimensions.

Let’s look at our PreviewCanvas component:

const PreviewCanvas: FunctionComponent<CanvasPreviewProps> = ({ preview }) => {
    const canvasRef = useRef<HTMLCanvasElement>(null);
  
    useLayoutEffect(() => {
      const pixels = decode(preview.blurhash, preview.w, preview.h);
      const ctx = canvasRef.current.getContext("2d");
      const imageData = ctx.createImageData(preview.w, preview.h);
      imageData.data.set(pixels);
      ctx.putImageData(imageData, 0, 0);
    }, [preview]);
  
    return <canvas ref={canvasRef} width={preview.w} height={preview.h} />;
  };

Not too terribly much going on here. We’re decoding our preview and then calling some fairly specific Canvas APIs.

Let’s see what the image previews look like:

In a sense, it’s less detailed than our previous previews. But I’ve also found them to be a bit smoother and less pixelated. And they take up a tiny fraction of the size.

Test and use what works best for you.

Wrapping up

There are many ways to prevent content reflow as your images load on the web. One approach is to prevent your UI from rendering until the images come in. The downside is that your user winds up waiting longer for content.

A good middle-ground is to immediately show a preview of the image and swap the real thing in when it’s loaded. This post walked you through two ways of accomplishing that: generating degraded, blurry versions of an image using a tool like Sharp and using BlurHash to generate an extremely small, Base83 encoded preview.

Happy coding!


Inline Image Previews with Sharp, BlurHash, and Lambda Functions originally published on CSS-Tricks. You should get the newsletter.



source https://css-tricks.com/inline-image-previews-with-sharp-blurhash-and-lambda-functions/

The Many Different Ways to Fetch Data in jOOQ

The jOOQ API is all about convenience, and as such, an important operation (the most important one?) like fetch() must come with convenience, too. The default way to fetch data is this: It fetches the entire result set into memory and closes the underlying JDBC resources eagerly. But what other options do we have? Iterable … Continue reading The Many Different Ways to Fetch Data in jOOQ

from Java, SQL and jOOQ. https://ift.tt/4TQXl1c
via IFTTT

Wednesday, May 18, 2022

An Interactive Starry Backdrop for Content

I was fortunate last year to get approached by Shawn Wang (swyx) about doing some work for Temporal. The idea was to cast my creative eye over what was on the site and come up with some ideas that would give the site a little “something” extra. This was quite a neat challenge as I consider myself more of a developer than a designer. But I love learning and leveling up the design side of my game.

One of the ideas I came up with was this interactive starry backdrop. You can see it working in this shared demo:

The neat thing about this design is that it’s built as a drop-in React component. And it’s super configurable in the sense that once you’ve put together the foundations for it, you can make it completely your own. Don’t want stars? Put something else in place. Don’t want randomly positioned particles? Place them in a constructed way. You have total control of what to bend it to your will.

So, let’s look at how we can create this drop-in component for your site! Today’s weapons of choice? React, GreenSock and HTML <canvas>. The React part is totally optional, of course, but, having this interactive backdrop as a drop-in component makes it something you can employ on other projects.

Let’s start by scaffolding a basic app

import React from 'https://cdn.skypack.dev/react'
import ReactDOM from 'https://cdn.skypack.dev/react-dom'
import gsap from 'https://cdn.skypack.dev/gsap'

const ROOT_NODE = document.querySelector('#app')

const Starscape = () => <h1>Cool Thingzzz!</h1>

const App = () => <Starscape/>

ReactDOM.render(<App/>, ROOT_NODE)

First thing we need to do is render a <canvas> element and grab a reference to it that we can use within React’s useEffect. For those not using React, store a reference to the <canvas> in a variable instead.

const Starscape = () => {
  const canvasRef = React.useRef(null)
  return <canvas ref={canvasRef} />
}

Our <canvas> is going to need some styles, too. For starters, we can make it so the canvas takes up the full viewport size and sits behind the content:

canvas {
  position: fixed;
  inset: 0;
  background: #262626;
  z-index: -1;
  height: 100vh;
  width: 100vw;
}

Cool! But not much to see yet.

We need stars in our sky

We’re going to “cheat” a little here. We aren’t going to draw the “classic” pointy star shape. We’re going to use circles of differing opacities and sizes.

Draw a circle on a <canvas> is a case of grabbing a context from the <canvas> and using the arc function. Let’s render a circle, err star, in the middle. We can do this within a React useEffect:

const Starscape = () => {
  const canvasRef = React.useRef(null)
  const contextRef = React.useRef(null)
  React.useEffect(() => {
    canvasRef.current.width = window.innerWidth
    canvasRef.current.height = window.innerHeight
    contextRef.current = canvasRef.current.getContext('2d')
    contextRef.current.fillStyle = 'yellow'
    contextRef.current.beginPath()
    contextRef.current.arc(
      window.innerWidth / 2, // X
      window.innerHeight / 2, // Y
      100, // Radius
      0, // Start Angle (Radians)
      Math.PI * 2 // End Angle (Radians)
    )
    contextRef.current.fill()
  }, [])
  return <canvas ref={canvasRef} />
}

So what we have is a big yellow circle:

This is a good start! The rest of our code will take place within this useEffect function. That’s why the React part is kinda optional. You can extract this code out and use it in whichever form you like.

We need to think about how we’re going to generate a bunch of “stars” and render them. Let’s create a LOAD function. This function is going to handle generating our stars as well as the general <canvas> setup. We can also move the sizing logic of the <canvas> sizing logic into this function:

const LOAD = () => {
  const VMIN = Math.min(window.innerHeight, window.innerWidth)
  const STAR_COUNT = Math.floor(VMIN * densityRatio)
  canvasRef.current.width = window.innerWidth
  canvasRef.current.height = window.innerHeight
  starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
    x: gsap.utils.random(0, window.innerWidth, 1),
    y: gsap.utils.random(0, window.innerHeight, 1),
    size: gsap.utils.random(1, sizeLimit, 1),
    scale: 1,
    alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
  }))
}

Our stars are now an array of objects. And each star has properties that define their characteristics, including:

  • x: The star’s position on the x-axis
  • y: The star’s position on the y-axis
  • size: The star’s size, in pixels
  • scale: The star’s scale, which will come into play when we interact with the component
  • alpha: The star’s alpha value, or opacity, which will also come into play during interactions

We can use GreenSock’s random() method to generate some of these values. You may also be wondering where sizeLimit, defaultAlpha, and densityRatio came from. These are now props we can pass to the Starscape component. We’ve provided some default values for them:

const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {

A randomly generated star Object might look like this:

{
  "x": 1252,
  "y": 29,
  "size": 4,
  "scale": 1,
  "alpha": 0.5
}

But, we need to see these stars and we do that by rendering them. Let’s create a RENDER function. This function will loop over our stars and render each of them onto the <canvas> using the arc function:

const RENDER = () => {
  contextRef.current.clearRect(
    0,
    0,
    canvasRef.current.width,
    canvasRef.current.height
  )
  starsRef.current.forEach(star => {
    contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
    contextRef.current.beginPath()
    contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)
    contextRef.current.fill()
  })
}

Now, we don’t need that clearRect function for our current implementation as we are only rendering once onto a blank <canvas>. But clearing the <canvas> before rendering anything isn’t a bad habit to get into, And it’s one we’ll need as we make our canvas interactive.

Consider this demo that shows the effect of not clearing between frames.

Our Starscape component is starting to take shape.

See the code
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {
  const canvasRef = React.useRef(null)
  const contextRef = React.useRef(null)
  const starsRef = React.useRef(null)
  React.useEffect(() => {
    contextRef.current = canvasRef.current.getContext('2d')
    const LOAD = () => {
      const VMIN = Math.min(window.innerHeight, window.innerWidth)
      const STAR_COUNT = Math.floor(VMIN * densityRatio)
      canvasRef.current.width = window.innerWidth
      canvasRef.current.height = window.innerHeight
      starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
        x: gsap.utils.random(0, window.innerWidth, 1),
        y: gsap.utils.random(0, window.innerHeight, 1),
        size: gsap.utils.random(1, sizeLimit, 1),
        scale: 1,
        alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
      }))
    }
    const RENDER = () => {
      contextRef.current.clearRect(
        0,
        0,
        canvasRef.current.width,
        canvasRef.current.height
      )
      starsRef.current.forEach(star => {
        contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
        contextRef.current.beginPath()
        contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)
        contextRef.current.fill()
      })
    }
    LOAD()
    RENDER()
  }, [])
  return <canvas ref={canvasRef} />
}

Have a play around with the props in this demo to see how they affect the the way stars are rendered.

Before we go further, you may have noticed a quirk in the demo where resizing the viewport distorts the <canvas>. As a quick win, we can rerun our LOAD and RENDER functions on resize. In most cases, we’ll want to debounce this, too. We can add the following code into our useEffect call. Note how we also remove the event listener in the teardown.

// Naming things is hard...
const RUN = () => {
  LOAD()
  RENDER()
}

RUN()

// Set up event handling
window.addEventListener('resize', RUN)
return () => {
  window.removeEventListener('resize', RUN)
}

Cool. Now when we resize the viewport, we get a new generated starry.

Interacting with the starry backdrop

Now for the fun part! Let’s make this thing interactive.

The idea is that as we move our pointer around the screen, we detect the proximity of the stars to the mouse cursor. Depending on that proximity, the stars both brighten and scale up.

We’re going to need to add another event listener to pull this off. Let’s call this UPDATE. This will work out the distance between the pointer and each star, then tween each star’s scale and alpha values. To make sure those tweeted values are correct, we can use GreenSock’s mapRange() utility. In fact, inside our LOAD function, we can create references to some mapping functions as well as a size unit then share these between the functions if we need to.

Here’s our new LOAD function. Note the new props for scaleLimit and proximityRatio. They are used to limit the range of how big or small a star can get, plus the proximity at which to base that on.

const Starscape = ({
  densityRatio = 0.5,
  sizeLimit = 5,
  defaultAlpha = 0.5,
  scaleLimit = 2,
  proximityRatio = 0.1
}) => {
  const canvasRef = React.useRef(null)
  const contextRef = React.useRef(null)
  const starsRef = React.useRef(null)
  const vminRef = React.useRef(null)
  const scaleMapperRef = React.useRef(null)
  const alphaMapperRef = React.useRef(null)
  
  React.useEffect(() => {
    contextRef.current = canvasRef.current.getContext('2d')
    const LOAD = () => {
      vminRef.current = Math.min(window.innerHeight, window.innerWidth)
      const STAR_COUNT = Math.floor(vminRef.current * densityRatio)
      scaleMapperRef.current = gsap.utils.mapRange(
        0,
        vminRef.current * proximityRatio,
        scaleLimit,
        1
      );
      alphaMapperRef.current = gsap.utils.mapRange(
        0,
        vminRef.current * proximityRatio,
        1,
        defaultAlpha
      );
    canvasRef.current.width = window.innerWidth
    canvasRef.current.height = window.innerHeight
    starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
      x: gsap.utils.random(0, window.innerWidth, 1),
      y: gsap.utils.random(0, window.innerHeight, 1),
      size: gsap.utils.random(1, sizeLimit, 1),
      scale: 1,
      alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
    }))
  }
}

And here’s our UPDATE function. It calculates the distance and generates an appropriate scale and alpha for a star:

const UPDATE = ({ x, y }) => {
  starsRef.current.forEach(STAR => {
    const DISTANCE = Math.sqrt(Math.pow(STAR.x - x, 2) + Math.pow(STAR.y - y, 2));
    gsap.to(STAR, {
      scale: scaleMapperRef.current(
        Math.min(DISTANCE, vminRef.current * proximityRatio)
      ),
      alpha: alphaMapperRef.current(
        Math.min(DISTANCE, vminRef.current * proximityRatio)
      )
    });
  })
};

But wait… it doesn’t do anything?

Well, it does. But, we haven’t set our component up to show updates. We need to render new frames as we interact. We can reach for requestAnimationFrame often. But, because we’re using GreenSock, we can make use of gsap.ticker. This is often referred to as “the heartbeat of the GSAP engine” and it’s is a good substitute for requestAnimationFrame.

To use it, we add the RENDER function to the ticker and make sure we remove it in the teardown. One of the neat things about using the ticker is that we can dictate the number of frames per second (fps). I like to go with a “cinematic” 24fps:

// Remove RUN
LOAD()
gsap.ticker.add(RENDER)
gsap.ticker.fps(24)

window.addEventListener('resize', LOAD)
document.addEventListener('pointermove', UPDATE)
return () => {
  window.removeEventListener('resize', LOAD)
  document.removeEventListener('pointermove', UPDATE)
  gsap.ticker.remove(RENDER)
}

Note how we’re now also running LOAD on resize. We also need to make sure our scale is being picked up in that RENDER function when using arc:

const RENDER = () => {
  contextRef.current.clearRect(
    0,
    0,
    canvasRef.current.width,
    canvasRef.current.height
  )
  starsRef.current.forEach(star => {
    contextRef.current.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
    contextRef.current.beginPath()
    contextRef.current.arc(
      star.x,
      star.y,
      (star.size / 2) * star.scale,
      0,
      Math.PI * 2
    )
    contextRef.current.fill()
  })
}

It works! 🙌

It’s a very subtle effect. But, that’s intentional because, while it’s is super neat, we don’t want this sort of thing to distract from the actual content. I’d recommend playing with the props for the component to see different effects. It makes sense to set all the stars to low alpha by default too.

The following demo allows you to play with the different props. I’ve gone for some pretty standout defaults here for the sake of demonstration! But remember, this article is more about showing you the techniques so you can go off and make your own cool backdrops — while being mindful of how it interacts with content.

Refinements

There is one issue with our interactive starry backdrop. If the mouse cursor leaves the <canvas>, the stars stay bright and upscaled but we want them to return to their original state. To fix this, we can add an extra handler for pointerleave. When the pointer leaves, this tweens all of the stars down to scale 1 and the original alpha value set by defaultAlpha.

const EXIT = () => {
  gsap.to(starsRef.current, {
    scale: 1,
    alpha: defaultAlpha,
  })
}

// Set up event handling
window.addEventListener('resize', LOAD)
document.addEventListener('pointermove', UPDATE)
document.addEventListener('pointerleave', EXIT)
return () => {
  window.removeEventListener('resize', LOAD)
  document.removeEventListener('pointermove', UPDATE)
  document.removeEventListener('pointerleave', EXIT)
  gsap.ticker.remove(RENDER)
}

Neat! Now our stars scale back down and return to their previous alpha when the mouse cursor leaves the scene.

Bonus: Adding an Easter egg

Before we wrap up, let’s add a little Easter egg surprise to our interactive starry backdrop. Ever heard of the Konami Code? It’s a famous cheat code and a cool way to add an Easter egg to our component.

We can practically do anything with the backdrop once the code runs. Like, we could make all the stars pulse in a random way for example. Or they could come to life with additional colors? It’s an opportunity to get creative with things!

We’re going listen for keyboard events and detect whether the code gets entered. Let’s start by creating a variable for the code:

const KONAMI_CODE =
  'arrowup,arrowup,arrowdown,arrowdown,arrowleft,arrowright,arrowleft,arrowright,keyb,keya';

Then we create a second effect within our the starry backdrop. This is a good way to maintain a separation of concerns in that one effect handles all the rendering, and the other handles the Easter egg. Specifically, we’re listening for keyup events and check whether our input matches the code.

const codeRef = React.useRef([])
React.useEffect(() => {
  const handleCode = e => {
    codeRef.current = [...codeRef.current, e.code]
      .slice(
        codeRef.current.length > 9 ? codeRef.current.length - 9 : 0
      )
    if (codeRef.current.join(',').toLowerCase() === KONAMI_CODE) {
      // Party in here!!!
    }
  }
  window.addEventListener('keyup', handleCode)
  return () => {
    window.removeEventListener('keyup', handleCode)
  }
}, [])

We store the user input in an Array that we store inside a ref. Once we hit the party code, we can clear the Array and do whatever we want. For example, we may create a gsap.timeline that does something to our stars for a given amount of time. If this is the case, we don’t want to allow Konami code to input while the timeline is active. Instead, we can store the timeline in a ref and make another check before running the party code.

const partyRef = React.useRef(null)
const isPartying = () =>
  partyRef.current &&
  partyRef.current.progress() !== 0 &&
  partyRef.current.progress() !== 1;

For this example, I’ve created a little timeline that colors each star and moves it to a new position. This requires updating our LOAD and RENDER functions.

First, we need each star to now have its own hue, saturation and lightness:

// Generating stars! ⭐️
starsRef.current = new Array(STAR_COUNT).fill().map(() => ({
  hue: 0,
  saturation: 0,
  lightness: 100,
  x: gsap.utils.random(0, window.innerWidth, 1),
  y: gsap.utils.random(0, window.innerHeight, 1),
  size: gsap.utils.random(1, sizeLimit, 1),
  scale: 1,
  alpha: defaultAlpha
}));

Second, we need to take those new values into consideration when rendering takes place:

starsRef.current.forEach((star) => {
  contextRef.current.fillStyle = `hsla(
    ${star.hue},
    ${star.saturation}%,
    ${star.lightness}%,
    ${star.alpha}
  )`;
  contextRef.current.beginPath();
  contextRef.current.arc(
    star.x,
    star.y,
    (star.size / 2) * star.scale,
    0,
    Math.PI * 2
  );
  contextRef.current.fill();
});

And here’s the fun bit of code that moves all the stars around:

partyRef.current = gsap.timeline().to(starsRef.current, {
  scale: 1,
  alpha: defaultAlpha
});

const STAGGER = 0.01;

for (let s = 0; s < starsRef.current.length; s++) {
  partyRef.current
    .to(
    starsRef.current[s],
    {
      onStart: () => {
        gsap.set(starsRef.current[s], {
          hue: gsap.utils.random(0, 360),
          saturation: 80,
          lightness: 60,
          alpha: 1,
        })
      },
      onComplete: () => {
        gsap.set(starsRef.current[s], {
          saturation: 0,
          lightness: 100,
          alpha: defaultAlpha,
        })
      },
      x: gsap.utils.random(0, window.innerWidth),
      y: gsap.utils.random(0, window.innerHeight),
      duration: 0.3
    },
    s * STAGGER
  );
}

From there, we generate a new timeline and tween the values of each star. These new values get picked up by RENDER. We’re adding a stagger by positioning each tween in the timeline using GSAP’s position parameter.

That’s it!

That’s one way to make an interactive starry backdrop for your site. We combined GSAP and an HTML <canvas>, and even sprinkled in some React that makes it more configurable and reusable. We even dropped an Easter egg in there!

Where can you take this component from here? How might you use it on a site? The combination of GreenSock and <canvas> is a lot of fun and I’m looking forward to seeing what you make! Here are a couple more ideas to get your creative juices flowing…


An Interactive Starry Backdrop for Content originally published on CSS-Tricks. You should get the newsletter.



source https://css-tricks.com/an-interactive-starry-backdrop-for-content/

Tuesday, May 17, 2022

Improving Icons for UI Elements with Typographic Alignment and Scale

Utilizing icons in user interface elements is helpful. In addition to element labeling, icons can help reinforce a user element’s intention to users. But I have to say, I notice a bit of icon misalignment while browsing the web. Even if the icon’s alignment is correct, icons often do not respond well when typographic styles for the element change.

I took note of a couple real-world examples and I’d like to share my thoughts on how I improved them. It’s my hope these techniques can help others build user interface elements that better accommodate typographic changes and while upholding the original goals of the design.

Example 1 — Site messaging

I found this messaging example on a popular media website. The icon’s position doesn’t look so bad. But when changing some of the element’s style properties like font-size and line-height, it begins to unravel.

Identified issues

  • the icon is absolutely positioned from the left edge using a relative unit (rem)
  • because the icon is taken out of the flow, the parent is given a larger padding-left value to help with overall spacing – ideally, our padding-x is uniform, and everything looks good whether or not an icon is present
  • the icon (it’s an SVG) is also sized in rems – this doesn’t allow for respective resizing if its parent’s font-size changes

Recommendations

Screenshot of the site messaging element. It is overlayed with a red-dashed line indicating the icon's top edge and a blue-dashed line indicating the text's topmost point. The red-dashed line is slightly higher than the blue-dashed line.
Indicating the issues with aligning the icon and typography.

We want our icon’s top edge to be at the blue dashed line, but we often find our icon’s top edge at the red dashed line.

Have you ever inserted an icon next to some text and it just won’t align to the top of the text? You may move the icon into place with something like position: relative; top: 0.2em. This works well enough, but if typographic styles change in the future, your icon could look misaligned.

We can position our icon more reliably. Let’s use the element’s baseline distance (the distance from one line’s baseline to the next line’s baseline) to help solve this.

Screenshot of the site messaging element. It is overlayed with arrows indicating the baseline distance from the baseline of one line to the next line's baseline.
Calculating the baseline distance.

Baseline distance is font-size * line-height.

We’ll store that in a CSS custom property:

--baselineDistance: calc(var(--fontSize) * var(--lineHeight));

We can then move our icon down using the result of (baseline distance – font size) / 2.

--iconOffset: calc((var(--baselineDistance) - var(--fontSize)) / 2);

With a font-size of 1rem (16px) and line-height of 1.5, our icon will be moved 4 pixels.

  • baseline distance = 16px * 1.5 = 24px
  • icon offset = (24px16px) / 2 = 4px

Demo: before and after

Example 2 – unordered lists

The second example I found is an unordered list. It uses a web font (Font Awesome) for its icon via a ::before pseudo-element. There have been plenty of great articles on styling both ordered and unordered lists, so I won’t go into details about the relatively new ::marker pseudo-element and such. Web fonts can generally work pretty well with icon alignment depending on the icon used.

Identified issues

  • no absolute positioning used – when using pseudo-elements, we don’t often use flexbox like our first example and absolute positioning shines here
  • the list item uses a combination of padding and negative text-indent to help with layout – I am never able to get this to work well when accounting for multi-line text and icon scalability

Recommendations

Because we’ll also use a pseudo-element in our solution, we’ll leverage absolute positioning. This example’s icon size was a bit larger than its adjacent copy (about 2x). Because of this, we will alter how we calculate the icon’s top position. The center of our icon should align vertically with the center of the first line.

Start with the baseline distance calculation:

--baselineDistance: calc(var(--fontSize) * var(--lineHeight));

Move the icon down using the result of (baseline distance – icon size) / 2.

--iconOffset: calc((var(--baselineDistance) - var(--iconSize)) / 2);

So with a font-size of 1rem (16px), a line-height of 1.6, and an icon sized 2x the copy (32px), our icon will get get a top value of -3.2 pixels.

  • baseline distance = 16px * 1.6 = 25.6px
  • icon offset = (25.6px32px) / 2 = -3.2px

With a larger font-size of 2rem (32px), line-height of 1.2, and 64px icon, our icon will get get a top value of -12.8 pixels.

  • baseline distance = 32px * 1.2 = 38.4px
  • icon offset = (38.4px64px) / 2 = -12.8px

Demo: before and after

Conclusion

For user interface icons, we have a lot of options and techniques. We have SVGs, web fonts, static images, ::marker, and list-style-type. One could even use background-colors and clip-paths to achieve some interesting icon results. Performing some simple calculations can help align and scale icons in a more graceful manner, resulting in implementations that are a bit more bulletproof.

See also: Previous discussion on aligning icon to text.


Improving Icons for UI Elements with Typographic Alignment and Scale originally published on CSS-Tricks. You should get the newsletter.



source https://css-tricks.com/improving-icons-for-ui-elements-with-typographic-alignment-and-scale/