Hugo: Designing with dynamic image colors

Hugo: Designing with dynamic image colors

April 13, 2024

In a Nutshell

  1. I analyze image colors returned by Hugo’s .Colors method.
  2. Then I use the lightest and darkest colors to set complementary border colors.
  3. The result is enhanced visual harmony between images and their surrounding content.

The Results

Here are several of the Forkful images as they appear using Hextra theme’s dark and light mode. The image borders are set to the darkest and lightest colors returned by RESOURCE.Colors. I added test strips showing the full output:

Example 1

Example 2

Example 3

The test strips look so nice, I think I’m going to add them to pages as design elements.

How I do it

I’ll describe it from the top-down.

1. Displaying an image

Here’s the kind of code I write in the templates to display an image. It’s already been saved in the variable $image from an earlier call to resources.Get:

list.html
{{ $colors := partial "func/colors/LightestAndDarkest" $image.Colors }}
{{ $dark   := index $colors "darkest"  }}
{{ $light  := index $colors "lightest" }}

<img data-dark  = "{{ $dark }}" 
     data-light = "{{ $light }}" 
     style      = "border: 2px solid {{ $dark }}" 
     src        = "{{ $image.Permalink }}">

You can see, I call a “function” (a partial ending with a return statement) to get the lightest and darkest colors. Then I use the colors to create the border. I also save them as data- attributes for later use in JavaScript:

head-end.html
 const setDarkTheme = () => {
   // ...
   document.querySelectorAll("img.prog-lang").forEach(function(img) {
     img.style.border = "2px solid " + img.dataset.dark;
   });
 }
 const setLightTheme = () => {
   // ...
   document.querySelectorAll("img.prog-lang").forEach(function(img) {
     img.style.border = "2px solid " + img.dataset.light;
   });
 }

This could really use some refactoring.

2. Calculating the lightest and darkest colors

I wrote two partials for this. Here’s the file layout:

          • Luminance.html
          • LightestAndDarkest.html
  • My current style is to create folders named after the type of input each partial expects. So, func/color is for functions that take a color string as input.

    LightestAndDarkest.html

    Now, this first one: what can I say, it’s not the most beautiful code. I’m new to Hugo template programming. And I wasn’t able to find a way to sort a list of colors by their luminance in Hugo’s templating language. I.e., in Ruby I’d do something like this:

    hypothetical-ruby-code.rb
    sorted_colors  = colors.sort_by { |c| luminance(c) }
    light_and_dark = { darkest: sorted_colors.first, lightest: sorted_colors.last }

    But for the Hugo template I had to write this procedural code in a loop:

    LightestAndDarkest.html
    {{/*
    	Given a list of colors, return the lightest and darkest.
    */}}
    
    {{ $darkest_color  := "" }}
    {{ $darkest_lum    := 1 }}
    {{ $lightest_color := "" }}
    {{ $lightest_lum   := 0 }}
    
    {{ with . }}
    	{{ range . }}
    		{{ $luminance := partial "func/color/Luminance" . }}
    		{{ if lt $luminance $darkest_lum }}
    			{{ $darkest_color = . }}
    			{{ $darkest_lum = $luminance }}
    		{{ end }}
    		{{ if gt $luminance $lightest_lum }}
    			{{ $lightest_color = . }}
    			{{ $lightest_lum = $luminance }}
    		{{ end }}
    	{{ end }}
    {{ end }}
    
    {{ return dict "lightest" $lightest_color "darkest" $darkest_color }}

    Maybe there’s a cleaner way?

    Luminance.html

    This was a fun one to write. I found the formula for luminance on Stack Overflow that pointed to the original excellent article by Darel Rex Finley. It’s a weighted sum of the RGB components. Here’s my translation into Hugo template syntax:

    Luminance.html
    {{/* 
    	Input is expected to be a CSS-style hex color string (e.g. #RRGGBB).
    	Output is the luminance from 0 to 1.
    
    	Luminance = sqrt( 0.299*R^2 + 0.587*G^2 + 0.114*B^2 )
    
    	See: https://alienryderflex.com/hsp.html
    	     https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color
    		 https://gohugo.io/methods/resource/colors/
    */}}
    
    {{ $r := (printf "0x%s" (substr . 1 2)) | int }}
    {{ $g := (printf "0x%s" (substr . 3 2)) | int }}
    {{ $b := (printf "0x%s" (substr . 5 2)) | int }}
    
    {{ $r_norm := div $r 255.0 }}
    {{ $g_norm := div $g 255.0 }}
    {{ $b_norm := div $b 255.0 }}
    
    {{ return (math.Sqrt (add (pow $r_norm 2 | mul 0.299) (pow $g_norm 2 | mul 0.587) (pow $b_norm 2 | mul 0.114))) }}

    And that’s it. Really just three small pieces of code. if you don’t have light/dark switching, it’s even simpler. I hope this helps you in your Hugo projects. I’m still learning, so if you have any suggestions, please let me know. Thanks for reading!