Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Creating Pixel-Art Of Ruth Bader Ginsburg Using CSS Box-Shadow In Lucee CFML 5.3.6.61

By Ben Nadel on

Like many of you, I'm completely gutted by the loss of Ruth Bader Ginsburg; and am still trying to wrap my head around what this loss means for our future and our rights given the malignant forces in this world. And, as I often do, I turn to programming as a "happy place" to find comfort - a place where form follows function, up is up, down is down, and things generally make logical sense.

And so, this morning, I wanted to try something that was discussed on the Shadows episode of the CSS podcast: the ability to create pixel art using nothing but a single CSS box-shadow property. This moment of meditation seems especially fitting given that fact that I often confuse "RGB" (Red, Green, Blue) and RBG (Ruth, Bader, Ginsburg).

Making pixel art with the CSS box-shadow property is possible because the box-shadow property can take a comma-delimited list shadows that each get applied, in turn, to the host element:

box-shadow: shadow 1 [ , shadow 2 [ , ... N ] ] ;

Using this behavior, we can think of each shadow as representing a single pixel that is offset on the X/Y access relative to the host element. The dimensions of each "pixel block" will match the dimensions of the host element. So, if the host element is 5px by 5px, then each shadow will be 5px by 5px (inset and blur-radius not withstanding).

The hard part becomes translating an image's pixel-values into a series of HEX-based shadows. Luckily, the Lucee CFML Image functionality has everything we need built right into it. We can load an image, access its underlying raster data, and then using a little bit-math to translate RGB color channels into a standard 6-digit hexadecimal color.

Photo Source: The following demo using a photo from an Elle Magazine article about Justice Ginsburg.

Here's the ColdFusion and CSS code that I came up with:

<cfscript>

	colorData = getColorData( "./rbg-elle.jpg", 7 );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I extract a two-dimensional array of colors ( rows x columns ) from the given
	* image, using the given block size (ie, how pixelated is are the colors). A struct
	* of relevant data is returned.
	* 
	* @filePath I am the image being inspected.
	* @blockSize I am the length of the single pixel-block.
	*/
	public struct function getColorData(
		required string filePath,
		required numeric blockSize
		) {

		var image = imageNew( filePath );
		var info = image.info();
		var width = info.width;
		var height = image.height;

		// The act of resizing the image down so that (one pixel == one block) will
		// automatically create pixelated source data.
		image.resize( ceiling( width / blockSize ), "", "highQuality" );

		// Convert the canvas of RGB channel values into HEX colors.
		var colors = getColorsFromBufferedImage( image.getBufferedImage() );

		return({
			width: width,
			height: height,
			blockSize: blockSize,
			colors: colors,
			// This is a string that can can be used with the CSS "box-shadow" property.
			boxShadows: generateBoxShadows( colors, blockSize )
		});

	}


	/**
	* I return a two-dimensional array of HEX colors that represent the RGB channels in
	* the given buffered image.
	* 
	* @bufferedImage I am the Java BufferedImage being translated.
	*/
	public array function getColorsFromBufferedImage( required any bufferedImage ) {

		var width = bufferedImage.getWidth();
		var height = bufferedImage.getHeight();
		var colors = [].resize( height );

		for ( var y = 0 ; y < height ; y++ ) {

			var row = colors[ y + 1 ] = [].resize( width );

			for ( var x = 0 ; x < width ; x++ ) {

				var rgb = bufferedImage.getRGB( x, y );
				var red = rgb.bitShrn( 16 ).bitAnd( 255 );
				var green = rgb.bitShrn( 8 ).bitAnd( 255 );
				var blue = rgb.bitShrn( 0 ).bitAnd( 255 );

				// Convert the RGB color channels into 6-digit HEX format.
				row[ x + 1 ] = (
					( "0" & formatBaseN( red, 16 ) ).right( 2 ) &
					( "0" & formatBaseN( green, 16 ) ).right( 2 ) &
					( "0" & formatBaseN( blue, 16 ) ).right( 2 )
				);

			}

		}

		return( colors );

	}


	/**
	* I generate a compound CSS box-shadow string from the given colors.
	* 
	* @colors I am the two-dimensional array of HEX colors.
	* @blockSize I am the size of each pixel block / shadow.
	*/
	public string function generateBoxShadows(
		required array colors,
		required numeric blockSize
		)
		localmode = "modern"
		{

		var shadows = [];

		loop index = "y" item = "row" array = colors {
			loop index = "x" item = "color" array = row {

				shadows.append( "#( x * blockSize )#px #( y * blockSize )#px ###color#" );

			}
		}

		return( shadows.toList( "," ) );

	}

</cfscript>

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<title>
		Creating Pixel-Art Of Ruth Bader Ginsburg Using CSS Box-Shadow In Lucee CFML 5.3.6.61
	</title>
</head>
<body>

	<div class="rbg"></div>

	<cfoutput>
		<style type="text/css">

			.rbg {
				height: #colorData.height#px ;
				overflow: hidden ;
				position: relative ;
				width: #colorData.width#px ;
			}

			.rbg:before {
				/*
					All of the "pixels" will be drawn as a series of box-shadows off of
					the content block. Each shadow will be the same dimensions as the
					content block.
				*/
				box-shadow: #colorData.boxShadows# ;
				content: "" ;
				height: #colorData.blockSize#px ;
				left: -#colorData.blockSize#px ;
				position: absolute ;
				top: -#colorData.blockSize#px ;
				width: #colorData.blockSize#px ;
			}

		</style>
	</cfoutput>

</body>
</html>

As you can see the, the HTML for this page has but a single Div element with no explicit content. The only styling is a pseudo-element, :after, which is being decorated with a large, compound box-shadow property. And, when we run this code, we get the following browser output:

A pixelated image of Ruth Bader Ginsburg using CSS box-shadow and Lucee CFML.

As you can see, the photo of Justice Ginsburg has been pixelated using a 7-pixel block-size. And, if we examine the source of the page, we see the following:

The box-shadow source of a pixel-art image.

As you can see, the source of the page contains a large, compound box-shadow CSS property in which each embedded shadow represents a single 7x7 pixel block within the overall photo.

Forgive me if you found the timing of this crude. But, as I said above, programming is my happy place; and, I think we all need to find our happy place today.



Reader Comments

Wow. Ben, you have truly excelled yourself!

This is one of the coolest things, I have ever seen. I never knew you could extract color data from an image. And, then recreating it, using a single DIV with a box-shadow property, is quite frankly mind blowing. 😊

I was wondering whether, it is possible to create colour palettes from an image, using just Coldfusion? I remember you did something like this with ImageMagick and then I created an electron app, using this methodology.
By the way, I managed to get the app to work on my local machine, fully packaged. But, when I installed it, on my second laptop, it did''t work. I never managed to resolve this.

It would be great to do the same thing but without the ImageMagick part?
The only issue, doing this in Coldfusion, is that you couldn't do this offline, which was why my electron app was so cool. But, I would still like to give it a go, because it is actually quite useful, having a colour palette to refer to, for UI design purposes.

Reply to this Comment

@Charles,

Ha ha, thank you - it was fun to try this out. It's just fun to see what CSS can do. Keep in mind that I have no "practical" use for this kind of thing; but, sometimes, the impractical stuff is the most interesting.

Re: ImageMagick vs. ColdFusion, I think the biggest hurdle would be that ImageMagick has the histogram functionality, which is what reduces the image to a set of popular colors. I don't believe anything like that is at the ColdFusion level - though, maybe something like that already exists at the Java level?

As you say, though, once you go ColdFusion, then you need to have a ColdFusion server running somewhere, which limits your app idea.

Reply to this Comment

Hey Ben! Thanks for this information.

I was wondering whether it is possible to spin up a CommandBox Lucee Server? But there might be a permissions issue, although that could be overcome during the Electron installation procedure?

I might talk to Brad Wood about this?

It would be great to be able to have a portable Coldfusion Server, similar to the portable NodeJS Server that Electron apps, use by default.

Reply to this Comment

Hiya -- that is one of the coolest CFM apps I've seen. That and some of your other blog posts (which I've been following for a little while) have inspired me to download and install Lucee to try out this pixel-art app. I've been doing ColdFuison programming since late 1990's but I didn't have a compelling reason to try open-source CFML competitors until now. Though I'm still exploring Lucee, I like what I see so far.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.