Learning 17: Images

External image files can be a useful resource when designing and engineering Perlenspiel games. This page describes the commands for loading, displaying, capturing and examining image files.

 

The imageData object

Perlenspiel's image and sprite functions employ a common object format to represent the structure and data of a 2-dimensional image. This imageData object contains six properties:

If the image was generated by a call to PS.imageLoad(), the .source property contains the filename string that was passed into that call. If the data was generated by PS.imageCapture(), .source contains the constant PS.GRID.

The .id property is set to a string that uniquely identifies the image object. If the data was generated by a call to PS.imageLoad(), this identifer will be identical to the string returned by that call.

The .width and .height properties indicate the width and height of the image in pixels.

The .pixelSize property indicates the format of the image's pixel data. Legal values of pixelSize are 1, 2, 3 or 4.

The .data property is a flat array containing (.width * .height * .pixelSize) integers. Each consecutive series of .pixelSize integers describes a single pixel. The pixel data is arranged in order of increasing rows, beginning with row 0.

Example

This example uses the file 3x3.bmp, a tiny 3x3 pixel image. The colors in this image (top to bottom and left to right) are pure red, green, blue, yellow, magenta, cyan, gray, white and black. It contains no alpha data, as the .bmp image format doesn't support it.

[3x3.bmp] 3x3.bmp, magnified to 120 x 120 pixels.

If this file was loaded into Perlenspiel using format 1, the resulting imageData object would look like this:

// 3.3.bmp in format 1

data = {
 source : "3x3.bmp", // source filename
 id : "image_0", // unique string identifier
 width : 3, // image width
 height : 3, // image height
 pixelSize : 1, // format = 1
 data : [
 0xFF0000, // red
 0x00FF00, // green
 0x0000FF, // blue
 0xFFFF00, // yellow
 0xFF00FF, // magenta
 0x00FFFF, // cyan
 0x808080, // gray
 0xFFFFFF, // white
 0x000000 // black
 ]
};

The .data array in this object consists of 9 integers (shown in hexadecimal for readability), one RGB triplet for each pixel.

Here's the same image in format 2:

// 3.3.bmp in format 2

data = {
 source : "3x3.bmp", // source filename
 id : "image_0", // unique string identifier
 width : 3, // image width
 height : 3, // image height
 pixelSize : 2, // format = 2
 data : [
 0xFF0000, 0xFF,
 0x00FF00, 0xFF,
 0x0000FF, 0xFF,
 0xFFFF00, 0xFF,
 0xFF00FF, 0xFF,
 0x00FFFF, 0xFF,
 0x808080, 0xFF,
 0xFFFFFF, 0xFF,
 0x000000, 0xFF
 ]
};

The .data array now contains 9 pairs of integers (18 total), one RGB triplet and one alpha value for each pixel. Because the image contains no alpha, all of the alpha values are 0xFF (255 = fully opaque).

In format 3, the imageData looks like this:

// 3.3.bmp in format 3

data = {
 source : "3x3.bmp", // source filename
 id : "image_0", // unique string identifier
 width : 3, // image width
 height : 3, // image height
 pixelSize : 3, // format = 3
 data : [
 0xFF, 0x00, 0x00,
 0x00, 0xF0, 0x00,
 0x00, 0x00, 0xFF,
 0xFF, 0xFF, 0x00,
 0xFF, 0x00, 0xFF,
 0x00, 0xFF, 0xFF,
 0x80, 0x80, 0x80,
 0xFF, 0xF0, 0xFF,
 0x00, 0x00, 0x00
 ]
};

The .data array now contains 9 triplets of integers (27 total), representing the red, green and blue components of each pixel.

The imageData for format 4:

// 3.3.bmp in format 4

data = {
 source : "3x3.bmp", // source filename
 id : "image_0", // unique string identifier
 width : 3, // image width
 height : 3, // image height
 pixelSize : 4, // format = 4
 data : [
 0xFF, 0x00, 0x00, 0xFF,
 0x00, 0xF0, 0x00, 0xFF,
 0x00, 0x00, 0xFF, 0xFF,
 0xFF, 0xFF, 0x00, 0xFF,
 0xFF, 0x00, 0xFF, 0xFF,
 0x00, 0xFF, 0xFF, 0xFF,
 0x80, 0x80, 0x80, 0xFF,
 0xFF, 0xF0, 0xFF, 0xFF,
 0x00, 0x00, 0x00, 0xFF
 ]
};

The .data array now contains 9 sets of four integers (36 total), representing the red, green, blue and alpha components of each pixel. Because the image contains no alpha, all of the alpha values are 0xFF (255 = fully opaque).

 

Image files and color accuracy

Perlenspiel is capable of reading three image file formats: .jpg/jpeg, .png and .bmp.

Two of these formats, .jpg/jpeg and .png, often contain embedded color profile data that can affect the way colors are interpreted when they are loaded into Perlenspiel.

If you load an image containing color profile data using PS.imageLoad(), the color values that appear in the .data property of the resulting imageData object probably won't match the values reported by your image editing software. This can be extremely annoying if you need the color data extracted from your images to exactly match specific values.

There are two ways around this problem:

  1. Don't use .jpg/jpeg or .png files. Use .bmp files instead, which do not contain embedded color profile data.
  2. Use one of the many tools available, such as pngcrush for Windows, that can strip embedded color profile data out of .jpg/jpeg and .png files.

Transparency

The .jpg/jpeg and .bmp image formats don't support alpha transparency. If you need alpha in your imported images, you must use .png files. Be sure to strip out any embedded color profile data if color accuracy is important.

 

PS.imageLoad ( filename, exec, format )

PS.imageLoad() attempts to asynchronously load the image specified by filename, which must be the name of a valid .bmp, .png or .jpg/jpeg image. The name can be specified relative to the directory in which the associated script file is stored, or it can be a fully qualified filename.

The required exec parameter must be a valid JavaScript function expecting at least one argument. When the image specified by filename is successfully loaded, this function is called with its first argument set to an imageData object containing the loaded image, or PS.ERROR if an error occurs.

The optional format parameter specifies the pixel format of the data that will be returned in the imageData object. Legal values for this parameter are 1, 2, 3, 4 or PS.DEFAULT. Refer to the imageData object section above for an explanation of this parameter.

If format is PS.DEFAULT or not supplied, the default data format (4) is used.

Usage notes

1. PS.imageLoad() returns PS.ERROR immediately if an obvious parameter error is detected. However, because file loads are asynchronous, any actual loading errors may occur after PS.imageLoad() returns. This means that PS.imageLoad() may return a file identifier even if the file subsequently fails to load. To detect loading errors, inspect the parameter of your exec function, which will be PS.ERROR if something went wrong during the load. A system error message will also appear in the event of a failed load.

Security issues

Webkit-based browsers, such as Chrome and Safari, are picky about security. By default, they will not load resources (including image and audio files) that (1) do not reside on an actual HTTP server, and (2) do not share the root filepath of any script that tries to access them. This means that you have to install all of your scripts, images and audio files on a live server, in the same root filepath, before you can test or deploy an application using these browsers. (It's possible to reconfigure the browsers to avoid this necessity, but this is not recommended.)

A typical Perlenspiel application directory might look like this:

game.html (the game's main web page)
*.js (your game scripts)
cover.html (cover image page, if any)
cover.png (cover image graphics, if any)
/images/ (a folder for your image files)
/audio/ (a folder for custom audio files, if any)

Firefox is currently more lenient about security. Resource files don't need to be on a server, or share the same root filepath. This may change in the future.

Demonstration

This demo creates a 3x3 grid, loads the image file "images/3x3.bmp", reports its properties in the debugger, and finally assigns the colors in the image to the corresponding beads in the grid.

[Run Demo]

PS.init = function( system, options ) {
 var myLoader;

 // Image loading function
 // Called when image loads successfully
 // [data] parameter will contain imageData

 myLoader = function ( imageData ) {
 var x, y, ptr, color;

 // Report imageData in debugger

 PS.debug( "Loaded " + imageData.source +
 ":\nid = " + imageData.id +
 "\nwidth = " + imageData.width +
 "\nheight = " + imageData.height +
 "\nformat = " + imageData.pixelSize + "\n" );

 // Extract colors from imageData and
 // assign them to the beads

 ptr = 0; // init pointer into data array
 for ( y = 0; y < 3; y += 1 ) {
 for ( x = 0; x < 3; x += 1 ) {
 color = imageData.data[ ptr ]; // get color
 PS.color( x, y, color ); // assign to bead
 ptr += 1; // point to next value
 }
 }
 };

 PS.gridSize( 3, 3 ); // set initial size
 PS.border( PS.ALL, PS.ALL, 0 ); // no borders

 // Load image in format 1

 PS.imageLoad( "images/3x3.bmp", myLoader, 1 );

 PS.statusText( "PS.imageLoad() Demo" );
};

Return value

PS.imageLoad() returns a string that uniquely identifies the loaded image file. This string also appears in the .id property of the imageData object created by the call.

A new identifier is generated by each call to PS.imageLoad()). If you load the same image file more than once for some reason, each load instance will be assigned a different identifier.

PS.ERROR is returned if an error occurs.

 

PS.imageBlit ( image, x, y, region )

The above demonstration of PS.imageLoad() showed how to manually read the colors in an imageData object and copy them to the grid.

The PS.imageBlit() command performs this common task automatically, rapidly copying or "blitting" all or part of an imageData object to the current grid plane at the specified coordinates.

The required image parameter must be a reference to an imageData object, such as those passed into the loader function of a successful PS.imageLoad() call, or the object returned by a call to PS.imageCapture() (see below).

The required xpos and ypos parameters specify the position on the grid where the top left corner of the image will be located. They can be any positive or negative integers. Non-integral values are floored.

The optional region parameter specifies a zero-based rectangular region inside the image that will be used for the blit. Refer to the API documentation for PS.imageBlit() for complete details.

If region is PS.DEFAULT or not supplied, the entire image is blitted to the grid.

Usage notes

1. The xpos and ypos parameters can be negative, or larger than the current grid. If this happens, some or all of the blitted image may not be visible.

2. Existing bead borders and glyphs are not affected when drawing with PS.imageBlit().

Additional notes are available in the API documentation.

Demonstration 1

This demo creates a 3x3 grid, loads the image file "images/3x3.bmp", reports its properties in the debugger, and then blits the image to the grid.

[Run Demo]

PS.init = function( system, options ) {
 var myLoader;

 // Image loading function
 // Called when image loads successfully
 // [data] parameter will contain imageData

 myLoader = function ( imageData ) {
 var x, y, ptr, color;

 // Report imageData in debugger

 PS.debug( "Loaded " + imageData.source +
 ":\nid = " + imageData.id +
 "\nwidth = " + imageData.width +
 "\nheight = " + imageData.height +
 "\nformat = " + imageData.pixelSize + "\n" );

 // Blit the image to the grid at 0, 0

 PS.imageBlit( imageData, 0, 0 );
 };

 PS.gridSize( 3, 3 ); // set initial size
 PS.border( PS.ALL, PS.ALL, 0 ); // no borders

 // Load image in default format (4)

 PS.imageLoad( "images/3x3.bmp", myLoader );

 PS.statusText( "PS.imageBlit() Demo 1" );
};

Demonstration 2

This demo uses an image larger than the grid to show how PS.imageBlit() can display partial images.

[33x33 image] 33x33.bmp, magnified to 132 x 132 pixels.

This demo creates a 15x15 grid, loads the image file "images/33x33.bmp", reports its properties in the debugger, and then blits the center of the image to the grid. Use the arrow/WASD keys to scroll the image around the grid, making different portions of it visible.

[Run Demo]

var G; // establish global namespace

( function () {
 // Private variables and functions

 var xpos = -9; // x-pos of image on grid
 var ypos = -10; // y-pos of image on grid
 var xmin; // minimum allowable x-pos
 var ymin; // minimum allowable y-pos
 var data; // holds the imageData

 // Image loading function
 // Called when image loads successfully
 // [data] parameter will contain imageData

 var loader = function ( imageData ) {
 data = imageData; // save data for later

 PS.debug( "Loaded " + data.source +
 ":\nid = " + data.id +
 "\nwidth = " + data.width +
 "\nheight = " + data.height +
 "\nformat = " + data.pixelSize + "\n" );

 // Calculate minimum x/y positions that will
 // keep image on the grid

 xmin = 0 - ( data.width - G.width );
 ymin = 0 - ( data.height - G.height );

 // Blit image to grid at center

 PS.imageBlit( data, xpos, ypos );
 };

 // This is where G is declared as an object,
 // and its properties initialized

 G = {
 width : 16, // width of grid
 height : 16, // height of grid

 // Load image in format 1

 load : function () {
 PS.imageLoad( "images/33x33.bmp", loader, 1 );
 },

 // Move image by x, y offset

 move : function ( x, y ) {
 var nx, ny;

 // Calculate proposed new position

 nx = xpos + x;
 ny = ypos + y;

 // Will new pos move image off grid?
 // If so, return without moving

 if ( ( nx < xmin ) || ( nx > 0 ) ||
 ( ny < ymin ) || ( ny > 0 ) ) {
 return;
 }

 // Reposition image and update x/y

 PS.imageBlit( data, nx, ny );
 xpos = nx;
 ypos = ny;
 }
 };
}() )

PS.init = function( system, options ) {
 PS.gridSize( G.width, G.height ); // set initial size
 PS.border( PS.ALL, PS.ALL, 0 ); // no borders
 G.load(); // load the image
 PS.statusText( "PS.imageBlit() Demo 2" );
};

PS.keyDown = function( key, shift, ctrl, options ) {
 switch ( key ) {
 case PS.KEY_ARROW_UP:
 case 119: // lower-case w
 case 87: // upper-case W
 {
 G.move( 0, 1 );
 break;
 }
 case PS.KEY_ARROW_DOWN:
 case 115: // lower-case s
 case 83: // upper-case S
 {
 G.move( 0, -1 );
 break;
 }
 case PS.KEY_ARROW_LEFT:
 case 97: // lower-case a
 case 65: // upper-case A
 {
 G.move( 1, 0 );
 break;
 }
 case PS.KEY_ARROW_RIGHT:
 case 100: // lower-case d
 case 68: // upper-case D
 {
 G.move( -1, 0 );
 break;
 }
 }
};

Return value

PS.imageBlit() returns true if any part of image was actually drawn to the grid, or false if nothing was drawn.

PS.ERROR is returned if an error occurs.

 

PS.imageCapture ( format, region )

The PS.imageCapture() command creates an imageData object based on the current bead colors in the grid.

The optional format parameter specifies the pixel format of the data that will be returned in the imageData object. Legal values for this parameter are 1, 2, 3, 4 or PS.DEFAULT. Refer to the imageData object documentation for an explanation of this parameter.

If format is PS.DEFAULT or not supplied, the default format (3) is used.

The optional region parameter specifies a zero-based rectangular region inside the image that will be used for the dump. Refer to the API documentation for PS.imageCapture() for complete details.

If region is PS.DEFAULT or not supplied, the entire grid is used.

Usage notes

1. The value returned by PS.imageCapture() can be passed to any Perlenspiel function expecting an imageData object.

2. The returned image represents the visible state of the grid, taking into effect all image planes, alpha transparency, sprites, etc.

Demonstration

Refer to the documentation for PS.imageDump() below for a demonstration of how to use PS.imageCapture().

Return value

PS.imageCapture() returns an imageData object in the specified format, representing the current bead colors of the grid.

PS.ERROR is returned if an error occurs.

 

PS.imageDump ( image, format, length, region, hex )

The PS.imageDump() command outputs a formatted text representation of an imageData object into the debugger, where it can be examined or cut/pasted into your code.

The required image parameter must be a reference to a valid imageData object, such as those returned by calls to PS.imageLoad() and PS.imageCapture().

The optional format parameter specifies the pixel format of the data that will be returned in the imageData object. Legal values for this parameter are 1, 2, 3, 4 or PS.DEFAULT. Refer to the imageData object documentation for an explanation of this parameter.

If format is PS.DEFAULT or not supplied, image.pixelSize is used.

The optional length parameter specifies the number of pixels that will be included in each line of debugger output. It should be a positive integer between one (1) and (image.width * image.height), inclusive. Values outside this range are clamped. Non-integral values are floored.

If length is PS.DEFAULT or not supplied, the value image.width is used.

The optional region parameter specifies a zero-based rectangular region inside the image that will be used for the dump. Refer to the API documentation for PS.imageDump() for complete details.

If region is PS.DEFAULT or not supplied, the entire image is dumped.

The optional hex parameter specifies the numeric base of the pixel data values. If hex is true, any non-zero number, PS.DEFAULT or not supplied, the data is output in hexadecimal (base 16). If hex is false or zero (0), the data is output in decimal (base 10).

Usage notes

1. The text dumped into the debugger by PS.imageDump() is formatted as a valid JavaScript representation of the specified imageData object. You can cut and paste this text into your source code, assign it to a variable or property, and thereafter use that variable or property as a parameter to any Perlenspiel function expecting an imageData object. This technique effectively “hardwires” the image into your code, eliminating the need to load an image file during a game.

Demonstration

This demo creates a 4x4 grid. Touch any bead to change it to a random primary color. Press the ESC key to dump the current grid to the debugger as a JavaScript representation of a imageData object.

[Run Demo]

PS.init = function( system, options ) {
 PS.gridSize( 4, 4 );
 PS.statusText( "PS.imageCapture/Dump() Demo" );
};

PS.touch = function( x, y, data, options ) {
 var colors, hue;

 colors = [
 PS.COLOR_BLACK, PS.COLOR_GRAY,
 PS.COLOR_RED, PS.COLOR_ORANGE,
 PS.COLOR_YELLOW, PS.COLOR_GREEN,
 PS.COLOR_BLUE, PS.COLOR_INDIGO,
 PS.COLOR_VIOLET, PS.COLOR_CYAN,
 PS.COLOR_MAGENTA
 ];

 // Pick a random color from list

 hue = colors[ PS.random( colors.length ) - 1 ];
 PS.color( x, y, hue ); // set bead color
};

PS.keyDown = function( key, shift, ctrl, options ) {
 var data;

 if ( key === PS.KEY_ESCAPE ) {
 // Capture current grid in format 1

 data = PS.imageCapture( 1 );

 // Dump the captured image to debugger

 PS.imageDump( data );
 }
};

Return value

PS.imageDump() returns PS.DONE if the call is successful, else PS.ERROR.

 

Terms to know

Next: Grid planes and advanced animation