Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created August 7, 2012 14:23
Show Gist options
  • Save bennadel/3285784 to your computer and use it in GitHub Desktop.
Save bennadel/3285784 to your computer and use it in GitHub Desktop.
Mapping CSS Sprite Image Coordinates With ColdFusion
<!--- Param the form values. --->
<cfparam name="form.submitted" type="boolean" default="false" />
<cfparam name="form.sprite" type="string" default="" />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Check to see if the form has been submitted. --->
<cfif (
form.submitted &&
len( form.sprite )
)>
<!--- Upload the file. --->
<cffile
result="upload"
action="upload"
filefield="sprite"
destination="#expandPath( './uploads/' )#"
nameconflict="makeunique"
/>
<!--- Map the spirte elements within the uploading file. --->
<cfset spriteMapper = new SpriteMapper(
imageNew( "#upload.serverDirectory#/#upload.serverFile#" )
) />
</cfif>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<cfcontent type="text/html; charset=utf-8" />
<cfoutput>
<!doctype html>
<html>
<head>
<title>CSS Sprite Coordinates</title>
</head>
<body>
<h1>
CSS Sprite Coordinates
</h1>
<form
method="post"
action="#cgi.script_name#"
enctype="multipart/form-data">
<input type="hidden" name="submitted" value="true" />
<p>
Upload your CSS sprite PNG:<br />
<input type="file" name="sprite" size="50" />
</p>
<p>
<input type="submit" value="Upload" />
</p>
</form>
<!---
Check to see if we have our sprite mapper. If so, then
we can access the shapes and output the coordinates.
--->
<cfif !isNull( spriteMapper )>
<h2>
Sprite Results:
</h2>
<!---
To make the output easier to read, we're going to
create classes rather than inline styles. As such,
we'll need an index counter to make the class
names unique.
--->
<cfset classIndex = 0 />
<!--- Output each shape. --->
<cfloop
index="shape"
array="#spriteMapper.getShapes()#">
<cfset classIndex++ />
<!--- Get the dimensions and offset of the sprite. --->
<cfset box = shape.getBoundingBox() />
<style type="text/css">
##sprite#classIndex# {
background-image: url( "./uploads/#upload.serverFile#" ) ;
background-position: -#box.minX#px -#box.minY#px ;
background-repeat: no-repeat ;
border: 5px solid ##D0D0D0 ;
height: #box.height#px ;
margin-bottom: 30px ;
width: #box.width#px ;
}
</style>
<div id="sprite#classIndex#">
<br />
</div>
</cfloop>
</cfif>
</body>
</html>
</cfoutput>
<cfcomponent output="false">
<cfscript>
// I map the incoming opaque pixels to shapes within the sprite
// image.
function init(){
// I am the collection of known, valid shapes. As shapes
// evolve in the map, some may be absorbed by others and will
// be removed from this collection.
variables.shapes = {};
// Each shape is keyed by its own unique ID.
variables.shapeIDIndex = 0;
// Return this object reference.
return( this );
};
// I add the given shape pixel to the map. When a pixel is added,
// it is either consumed by an existing shape, or is the means to
// start a completely new shape.
function addPixel( Numeric x, Numeric y ){
// Check the surrounding pixels for known shapes.
var surroundingShapes = this._getSurroundingShapes( x, y );
// Check to see if we have any surrounding shapes.
if (arrayLen( surroundingShapes )){
// Add the current pixel to the first known shape.
surroundingShapes[ 1 ].addPixel( x, y );
// If this new pixel has bridged the gap between two
// known shapes, when this pixel has united the two
// shapes into one shape. As such, one of the surrounding
// shapes must absorb the other ones.
this._mergeShapes( surroundingShapes );
// Nothing mroe to do with this pixel.
return;
}
// If we made it this far then we have a completely new shape.
// Create the shape with a new, unique ID.
var shape = new SpriteShape(
++variables.shapeIDIndex,
x,
y
);
// Store the new shape.
variables.shapes[ shape.getID() ] = shape;
}
// I get the shape that contains the given pixel.
function _getShapeContainingPixel( Numeric x, Numeric y ){
// Short-circuit some out-of-bounds cases.
if (
(x < 0) ||
(y < 0)
){
// These are not valid coordinates.
return;
}
// Loop over the known shapes to if any of them contain a
// pixel with the given coordinates.
for (var shapeID in variables.shapes){
var shape = variables.shapes[ shapeID ];
if (shape.containsPixel( x, y )){
return( shape );
}
}
// If we made it this far, no shape was found.
return;
}
// I return the unique set of shapes found in the sprite map.
function getShapes(){
var shapes = [];
// Convert the key-based collection into an indexed collection.
for (var shapeID in variables.shapes){
arrayAppend(
shapes,
variables.shapes[ shapeID ]
);
}
// Return the shapes.
return( shapes );
}
// I get the shapes in the pixels surrounding the given coordinate.
function _getSurroundingShapes( Numeric x, Numeric y ){
// Define the four potentionally known coorindates in relation
// to the given pixel. The other direcitons (east and south)
// have not yet been explored.
//
// NOTE: This direcitonal assumption is based on the logic
// contained within the scanning alogrithm of the SpriteMapper
// component. As such, this part of the code is strongly
// coupled to the other component.
var pixels = [
{
x = (x - 1),
y = y
},
{
x = (x - 1),
y = (y - 1)
},
{
x = x,
y = (y - 1)
},
{
x = (x + 1),
y = (y - 1)
}
];
// Define the shapes that surround the given pixel.
var shapes = [];
// Keep track of the IDs of the shapes we found, so that we
// keep the collection of returned shapes unique.
var shapeIDs = {};
// For a small optimization, we know that if we find a shape
// in the first pixel, we can skip the check for the middle
// two pixels and jump to the last pixel.
var skipMiddlePixels = false;
// Loop over connected pixels to gather regions.
for (var i = 1 ; i <= 4 ; i++){
if (skipMiddlePixels && (i < 4)){
continue;
}
var pixel = pixels[ i ];
// Get the parent shape (if there is one).
var shape = this._getShapeContainingPixel( pixel.x, pixel.y );
// If we found a shape at the given coordinates,
// add it to the collection.
if (
!isNull( shape ) &&
!structKeyExists( shapeIDs, shape.getID() )
){
// Add this shape to the collection.
arrayAppend( shapes, shape );
// Cache the ID.
shapeIDs[ shape.getID() ] = true;
// If this is the first pixel, then skip directly
// to the last pixel.
if (i == 1){
skipMiddlePixels = true;
}
}
}
// Return the surrounding shapes.
return( shapes );
}
// I merge the given, connected shapes.
function _mergeShapes( shapes ){
// Get the number of shapes to merge.
var shapeCount = arrayLen( shapes );
// If there is only one shape, there's nothing to merge.
if (shapeCount == 1){
return;
}
// If there is more than one shape, all the secondary shapes
// will get merge down into the first shape. Get a reference
// to the first shape.
var firstShape = shapes[ 1 ];
// Merge all the remaining shapes into the first shape.
for (var i = 2 ; i <= shapeCount ; i++){
var oldShape = shapes[ i ];
// Remove this shape from the cache.
structDelete( variables.shapes, oldShape.getID() );
// Merge this shape into the first one.
firstShape.absorbShape( oldShape );
}
}
</cfscript>
</cfcomponent>
<cfcomponent output="false">
<cfscript>
// I handle the reading of the image's underlying pixel data,
// determining which pixels are part of the canvas (ie. which
// are transparent) and which pixels are part of a shape (ie.
// which are opaque).
function init( Any image ){
// Get the underlying buffered image so that we can gain
// access to the pixel data.
variables.bufferedImage = imageGetBufferedImage( image );
// Store our actual map (this takes care of turning opaque
// pixels into shapes).
variables.spriteMap = new SpriteMap();
// Find all the pixels in the image that may be part of shapes.
this._findShapePixels();
// Return this object reference.
return( this );
}
// I traverse the image, looking for pixels that will be part of
// shapes (ie. for anything that is NOT transparent).
function _findShapePixels(){
// Get the bounding coordinates for our graphic.
var width = variables.bufferedImage.getWidth();
var height = variables.bufferedImage.getHeight();
// Move down the image.
for (var y = 0 ; y < height ; y++ ){
// For each row, move across, looking at each pixel.
for (var x = 0 ; x < width ; x++){
// Examine the given pixel to see if it should be
// used or ignored.
this._processPixel( x, y );
}
}
}
// I return the mapped shapes.
function getShapes(){
return( variables.spriteMap.getShapes() );
}
// I determine if the given pixel is transparent.
function _pixelIsTransparent( Numeric x, Numeric y ){
// Get the numeric representation of the pixel in the image.
var pixel = variables.bufferedImage.getRGB(
javaCast( "int", x ),
javaCast( "int", y )
);
// Get the left-most byte of the pixel (which holds the 8-bit
// alpha component of the pixel integer).
var alphaChannel = bitSHRN( pixel, 24 );
// Consider this pixel transparent if it has full transparency.
return( alphaChannel eq 0 );
}
// I look at a given pixel and determine if it is blank
// (transparent) or, if it should be consumed as part of a
// sprite shape.
function _processPixel( Numeric x, Numeric y ){
// Check to see if this pixel is transparent. If it is
// transparent, then it is not part of a shape.
if (this._pixelIsTransparent( x, y )){
// Nothing more to do with this pixel.
return;
}
// Add this pixel to the map.
variables.spriteMap.addPixel( x, y );
}
</cfscript>
</cfcomponent>
<cfcomponent output="false">
<cfscript>
// I manage the pixels for a single shape within the sprite. As
// pixels are added, they are mapped and the bouding box of the
// shape is updated.
function init( Numeric id, Numeric x, Numeric y ){
// Store the unique ID of the shape.
variables.id = id;
// Store the pixel map, including our first pixel.
variables.pixelMap = {
"#x#:#y#" = true
};
// Set up the initial bounding box defined by the first pixel.
variables.minX = x;
variables.maxX = (x + 1);
variables.minY = y;
variables.maxY = (y + 1);
// Return this object reference.
return( this );
}
// I absorb the dimentions of the given shape.
function absorbShape( Any shape ){
// Absorb all of the pixels in the underlying map.
structAppend(
variables.pixelMap,
shape.getPixelMap()
);
// Get the other shape's boudning box.
var boundingBox = shape.getBoundingBox();
// Grow the current bounding box to encompass the new
// bounding box of the absorbed shape.
variables.minX = min( variables.minX, boundingBox.minX );
variables.maxX = max( variables.maxX, boundingBox.maxX );
variables.minY = min( variables.minY, boundingBox.minY );
variables.maxY = max( variables.maxY, boundingBox.maxY );
}
// I add the given pixel to the shape.
function addPixel( Numeric x, Numeric y ){
// Add it the map.
variables.pixelMap[ "#x#:#y#" ] = true;
// Adjust the min/max box coordinates.
variables.minX = min( variables.minX, x );
variables.maxX = max( variables.maxX, (x + 1) );
variables.minY = min( variables.minY, y );
variables.maxY = max( variables.maxY, (y + 1) );
}
// I determine if the given pixel is within the known pixels of
// mapped shape.
function containsPixel( Numeric x, Numeric y ){
return(
structKeyExists( variables.pixelMap, "#x#:#y#" )
);
}
// I get the bounding box for the shape based on the min/max
// coordinates of the bounding box.
function getBoundingBox(){
var boundingBox = {
minX = variables.minX,
maxX = variables.maxX,
minY = variables.minY,
maxY = variables.maxY,
width = (variables.maxX - variables.minX),
height = (variables.maxY - variables.minY)
};
return( boundingBox );
}
// I return the ID property.
function getID(){
return( variables.id );
}
// I return the underlying pixel map.
function getPixelMap(){
return( variables.pixelMap );
}
</cfscript>
</cfcomponent>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment