Skip to content

Instantly share code, notes, and snippets.

@bivald
Created January 14, 2013 10:26
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save bivald/4529135 to your computer and use it in GitHub Desktop.
Modified vs of http://www.retroaffect.com/blog/132/Photoshop_Animation_to_Sprite_Sheet/ to have 1 single row with correct height.
/*// =======================================================
The Retro Affect Animation Exporter
Created: January 6th, 2010
______________________________________________________________________________
Authors:
Peter Jones
David Carrigg
The function getFrame(IO) is by:
jugenjury from ps-scripts.com
http://ps-scripts.com/bb/viewtopic.php?p=13695
Organize the frames sections by:
Mark McCoy from torquepowered.com
http://www.torquepowered.com/community/blogs/view/11527
V1.4
Code Adjusted for personal Use by Will Canada DecipherOne Productions
(This script did not initally work for me. I re-wrote frame positioning and
added the prompt for the user to specify a frame numbers because the
frame numbers weren't being passed correctly from the document.
The code also now checks to make sure that the created sprite sheets diimensions
are in a power of 2 and limits sizes to a width and height of 4096.)
V1.5 by Will Canada
Adjusted logic error that was causing frames to, under certain conditions, be drawn outside of the sprite sheet.
In the previous version the script placed the last layer as the first frame, this was counter-intuitive to photoshop, script
now places first layer as first frame.
Fixed Issue where the number of rows wasn't properly scaling beyond a certain threshold.
Corrected spelling error in a user prompt.
Latest Modification January 28, 2011
______________________________________________________________________________
Summary
Converts frames of animation in Photoshop into a single sprite sheet.
Steps through each frame and creates a single image of whatever was
visible in that frame. Then using those new frames, places them side by
side in a sprite sheet. Also, using the height, width and number of frames,
it determines the best size for the sprite sheet at a power of two. The sprite
sheet is then "dehaloed"; a process that removes the thin white lines that
appear around 2D images when used in games. Finally, it saves it out as
a PNG onto your desktop using the same file name as its respective PSD.
______________________________________________________________________________
Options
- Set dehaloImage to 0 if your game does not blend pixels (doesn't use bilinear filtering), in other words if you're creating pixellated animations
- Set sheetGrid to 0 if you would like the frames to be placed on after another in a single row.
*/
var dehaloImage =0;
var sheetGrid = 0;
/*
______________________________________________________________________________
How to Use:
1) Place this .jsx file into
C:\Program Files\Adobe\Adobe Photoshop CS4\Presets\Scripts\
2) Restart Photoshop if it was already open
3) Run the script: File > Scripts > Animation Exporter
______________________________________________________________________________
Common Issues
If it's not working for your animation:
1) Make sure there are no locked layers
2) Make sure every frame have at least one visible layer
______________________________________________________________________________
Other Notes:
v1.1
Fixed an error that was causing layers in some PSDs to become invisible before saving the sprite sheet out.
- I use the framed animation toolset in Photoshop, not the timeline. If you do
use the timeline tools, I believe the script will make one frame in the sprite
sheet for every second of animation.
- You may or may not need the dehalo section depending on what you need it for.
- The chunks of code that are really cryptic are from the Script Listener.
______________________________________________________________________________
If you want to add to it:
The Photoshop Javascript Guide was invaluable:
http://www.adobe.com/devnet/photoshop/pdfs/photoshop_cs3_javascript_ref.pdf
Tested with CS4
*/// =======================================================
// Assign the active document to currentDoc
currentDoc = app.activeDocument;
// The save destination of the compelted sprite sheet
var pngDestination = "~/Desktop/";
// Saves the sprite sheet using the same name as the PSD
var fileName = currentDoc.name;
fileName = fileName.substr(0,fileName.length-4);
function main()
{
/*// =======================================================
Timeline Data
Gets the first, last and current frame of animation
*/// =======================================================
function getFrame(IO) {
if (IO=="in") var tIO = stringIDToTypeID("workInTime");
if (IO=="out") var tIO = stringIDToTypeID("workOutTime");
if (IO=="current") var tIO=stringIDToTypeID("currentFrame");
var fr = 0;
var secs = 0;
var mins = 0;
var hrs = 0;
var frRate = 30.0;
var rate = stringIDToTypeID("frameRate");
var frame = stringIDToTypeID("frame");
var seconds = stringIDToTypeID("seconds");
var minutes = stringIDToTypeID("minutes");
var hours = stringIDToTypeID("hours");
var tLine=stringIDToTypeID("timeline");
var get=charIDToTypeID('getd');
var nul=charIDToTypeID('null');
var prop=charIDToTypeID('Prpr');
var actionRef = new ActionReference();
actionRef.putProperty(prop,tIO);
actionRef.putClass(tLine);
var desc=new ActionDescriptor();
desc.putReference(nul, actionRef);
var TC=executeAction(get,desc,DialogModes.NO);
if (IO=="current") return TC.getInteger(tIO);
var TL=TC.getObjectValue(tIO);
try {fr=TL.getInteger(frame);} catch(e){}
try {secs=TL.getInteger(seconds);} catch(e){}
try {mins=TL.getInteger(minutes);} catch(e){}
try {hrs=TL.getInteger(hours);} catch(e){}
frRate = TL.getDouble(rate);
return hrs * 3600 * frRate + mins * 60 * frRate + secs * frRate + fr;
}
// Saves the current document in order to revert to the state prior to running the script
currentDoc.save();
// Selects all frames
var idanimationSelectAll = stringIDToTypeID( "animationSelectAll" );
var desc5 = new ActionDescriptor();
executeAction( idanimationSelectAll, desc5, DialogModes.NO );
// Sets frame time to .03 (1 frame per second)
var idsetd = charIDToTypeID( "setd" );
var desc6 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref3 = new ActionReference();
var idanimationFrameClass = stringIDToTypeID( "animationFrameClass" );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref3.putEnumerated( idanimationFrameClass, idOrdn, idTrgt );
desc6.putReference( idnull, ref3 );
var idT = charIDToTypeID( "T " );
var desc7 = new ActionDescriptor();
var idanimationFrameDelay = stringIDToTypeID( "animationFrameDelay" );
desc7.putDouble( idanimationFrameDelay, 0.030000 );
var idanimationFrameClass = stringIDToTypeID( "animationFrameClass" );
desc6.putObject( idT, idanimationFrameClass, desc7 );
executeAction( idsetd, desc6, DialogModes.NO );
// Converst frames to a timeline
var idconvertAnimation = stringIDToTypeID( "convertAnimation" );
var desc2 = new ActionDescriptor();
executeAction( idconvertAnimation, desc2, DialogModes.NO );
var numFrames = getFrame("out");
// Go to the first frame
var idsetd = charIDToTypeID( "setd" );
var desc7 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref3 = new ActionReference();
var idPrpr = charIDToTypeID( "Prpr" );
var idtime = stringIDToTypeID( "time" );
ref3.putProperty( idPrpr, idtime );
var idtimeline = stringIDToTypeID( "timeline" );
ref3.putClass( idtimeline );
desc7.putReference( idnull, ref3 );
var idT = charIDToTypeID( "T " );
var desc8 = new ActionDescriptor();
var idseconds = stringIDToTypeID( "seconds" );
desc8.putInteger( idseconds, 0 );
var idframe = stringIDToTypeID( "frame" );
desc8.putInteger( idframe, 0 );
var idframeRate = stringIDToTypeID( "frameRate" );
desc8.putDouble( idframeRate, 30.000000 );
var idtimecode = stringIDToTypeID( "timecode" );
desc7.putObject( idT, idtimecode, desc8 );
executeAction( idsetd, desc7, DialogModes.NO );
/*// =======================================================
Create each frame as a single layer
This cycles through each frame and creates a copy of all the visible layers
as a new layer name 'fr' + the frame number
*/// =======================================================
for(var i =0; i < numFrames; i++){
currentFrame = getFrame("current") + 1;
// Select all layers
var idselectAllLayers = stringIDToTypeID( "selectAllLayers" );
var desc4 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref1 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref1.putEnumerated( idLyr, idOrdn, idTrgt );
desc4.putReference( idnull, ref1 );
executeAction( idselectAllLayers, desc4, DialogModes.NO );
// Duplicate all layers
var idDplc = charIDToTypeID( "Dplc" );
var desc5 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref2 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref2.putEnumerated( idLyr, idOrdn, idTrgt );
desc5.putReference( idnull, ref2 );
var idVrsn = charIDToTypeID( "Vrsn" );
desc5.putInteger( idVrsn, 2 );
executeAction( idDplc, desc5, DialogModes.NO );
// Merge layers
var idMrgtwo = charIDToTypeID( "Mrg2" );
executeAction( idMrgtwo, undefined, DialogModes.NO );
// Rename the layer to "fr" + the frame number
var idsetd = charIDToTypeID( "setd" );
var desc11 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref7 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref7.putEnumerated( idLyr, idOrdn, idTrgt );
desc11.putReference( idnull, ref7 );
var idT = charIDToTypeID( "T " );
var desc12 = new ActionDescriptor();
var idNm = charIDToTypeID( "Nm " );
desc12.putString( idNm, "fr" + currentFrame );
var idLyr = charIDToTypeID( "Lyr " );
desc11.putObject( idT, idLyr, desc12 );
executeAction( idsetd, desc11, DialogModes.NO );
// Make the new layer invisble so it's not included in the next frame
var idHd = charIDToTypeID( "Hd " );
var desc2 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var list1 = new ActionList();
var ref1 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref1.putEnumerated( idLyr, idOrdn, idTrgt );
list1.putReference( ref1 );
desc2.putList( idnull, list1 );
executeAction( idHd, desc2, DialogModes.NO );
// Select the next frame
var idnextFrame = stringIDToTypeID( "nextFrame" );
var desc9 = new ActionDescriptor();
var idtoNextWholeSecond = stringIDToTypeID( "toNextWholeSecond" );
desc9.putBoolean( idtoNextWholeSecond, false );
executeAction( idnextFrame, desc9, DialogModes.NO );
}
// Delete old layers and leave only the new 'fr' layers
var startFrame = getFrame("in") + 1;
var endFrame = getFrame("out");
// Select all layers
var idselectAllLayers = stringIDToTypeID( "selectAllLayers" );
var desc6 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref4 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref4.putEnumerated( idLyr, idOrdn, idTrgt );
desc6.putReference( idnull, ref4 );
executeAction( idselectAllLayers, desc6, DialogModes.NO );
// Deselect all fr layers
for(i = startFrame; i <= endFrame; i++){
var idslct = charIDToTypeID( "slct" );
var desc13 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref11 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
ref11.putName( idLyr, "fr" + i );
desc13.putReference( idnull, ref11 );
var idselectionModifier = stringIDToTypeID( "selectionModifier" );
var idselectionModifierType = stringIDToTypeID( "selectionModifierType" );
var idremoveFromSelection = stringIDToTypeID( "removeFromSelection" );
desc13.putEnumerated( idselectionModifier, idselectionModifierType, idremoveFromSelection );
var idMkVs = charIDToTypeID( "MkVs" );
desc13.putBoolean( idMkVs, false );
executeAction( idslct, desc13, DialogModes.NO );
}
// Delete selected layers
var idDlt = charIDToTypeID( "Dlt " );
var desc8 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref6 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref6.putEnumerated( idLyr, idOrdn, idTrgt );
desc8.putReference( idnull, ref6 );
try{
executeAction( idDlt, desc8, DialogModes.NO );
}
catch(e){};
/*// =======================================================
Determine Canvas Size
Depending on the number and size of the frames, this determines the
best possible size of the sprite sheet in terms of pixel area.
/// =======================================================
Modified to take a specific width argument from the user.
Number of Frames wasn't being passed from getFrames so user is prompted instead.
Canvas sizes have to be in powers of 2 for this to work.
/*///=======================================================*/
var numFrames = prompt("Please Enter the Number of Frames for your Animation.",'8',"Animation Frames");
var numCols = numFrames;
if (numFrames == '' ||numFrames== null)
{
alert("Exiting Script - Sprite Sheet Creation Aborted!");
return 0;
}
var numRows = 1;
var idealCols = 0;
var idealRows = 0;
var idealWidth = 0;
var initial_idealWidth= 0;
var idealHeight = 0;
var sumRowsAndCols = 0;
var frameWidth = currentDoc.width;
var frameHeight = currentDoc.height;
if (sheetGrid == 1)
{
if((frameWidth&(frameWidth-1) ) == 0 && (frameHeight&(frameHeight-1) ) == 0)//make sure our canvas height and width are powers of 2
{
var MAXIMUM = prompt("Please enter a maximum texture width using a power of 2, in between 16 & 4096.",'1024',"Set Maximum Texture Width");
if (MAXIMUM == '' || MAXIMUM == null)
{
alert("Exiting Script - Sprite Sheet Creation Aborted!");
return 0;
}
while((MAXIMUM&(MAXIMUM-1))!=0)//If the number entered isn't a power of 2 reprompt the user.
{
var m_message=MAXIMUM ;
m_message += " is not a power of 2. Acceptable values are 16, 32, 64, 128, 256, 512, 1024, 2048, and 4096.";
MAXIMUM = prompt(m_message,'1024',"Set Maximum Texture Width");
};
if(MAXIMUM < 16)
{
MAXIMUM = 16;
alert("Texture sized changed to 16.");
}
else if(MAXIMUM > 4096)
{
MAXIMUM = 4096;
alert("Texture sized changed to 4096.");
}
idealCols = currentDoc.artLayers.length;
idealRows = 1;
//Begin calculating the ideal width
initial_idealWidth = numFrames * frameWidth;
var x=1;
if(initial_idealWidth< 16)
{
initial_idealWidth = 16;
}
else if(initial_idealWidth > 4096)
{
initial_idealWidth = 4096;
}
do
{
var ham = 16*x;
var ham2 = 32*x;
if(initial_idealWidth >ham && initial_idealWidth < ham2)
{
initial_idealWidth = ham2;
}
x*=2;
}
while((initial_idealWidth&(initial_idealWidth-1) ) != 0 );
//Sets idealWidth for checks
idealWidth = initial_idealWidth;
var done=false;
var x = 1;
//Sets the colums and rows based upon the desired texture size.
do
{
var ham = 16*x;
var ham2 = 32*x;
if(idealWidth==ham||idealWidth==ham2)
{
done=true;
}
else if(idealWidth >ham && idealWidth < ham2) //Our width is between 16 and 32 scale up to 32
{
idealWidth = ham2;
done = true;
}
else if(idealWidth > MAXIMUM)
{
do //Check and see if the ideal width is more then our maximum, if it is divide it up and increase the rows.
{
idealWidth = idealWidth / 2;
}
while(idealWidth > MAXIMUM);
done = true;
}
x = x*2; //Make x a 2 X multiplier
}
while(!done);
//Sets the colums and rows based upon the desired texture size.
var FramesPerRow= idealWidth/frameWidth;
idealRows = numFrames/FramesPerRow;
//Set the ideal Colums with our new data. This determines the number of frames per row.
idealCols=idealWidth/currentDoc.width;
idealHeight = idealRows * currentDoc.height;
}
else
{
alert("The canvas size must be in powers of 2. The script will now exit.");
return 0;
}
}
else {
idealCols = currentDoc.artLayers.length;
idealRows = 1;
idealWidth = idealCols * currentDoc.width;
idealHeight = idealRows * currentDoc.height;
}
/*// =======================================================
Organize the frames
*/// =======================================================
var activeLayer = currentDoc.activeLayer;
numLayers = currentDoc.artLayers.length;
app.preferences.rulerUnits = Units.PIXELS;
//make sure the ideal height is a power of 2
if(idealHeight> 4096)
{
MAXIMUM = 4096;
alert("Texture height sized changed to 4096. Animation may be too big for allocated height.");
}
if( sheetGrid ) {
var x = 1;
do
{
var ham = 16*x;
var ham2 = 32*x;
if(idealHeight >ham && idealHeight < ham2)
{
idealHeight = ham2;
break;
}
x*=2;
}
while((idealHeight&(idealHeight-1) ) != 0 );
}
currentDoc.resizeCanvas( idealWidth, idealHeight, AnchorPosition.TOPLEFT );
var rowTemp =0;
var colTemp = 0;
for (i=numLayers-1; i >= 0 ; i--)
{
currentDoc.artLayers[i].visible = 1;
currentDoc.artLayers[i].opacity = 100;
var x = frameWidth*colTemp;
var y = frameHeight*rowTemp;
currentDoc.artLayers[i].translate(x, y);
colTemp++;
if (colTemp == (idealCols))
{
rowTemp++;
colTemp = 0;
}
}
if (dehaloImage == 1) {
/*// =======================================================
Dehalo the image
A halo refers to the white line that appears around 2D images in games.
There are a few different ways to get rid of them, but the easiest way I found
was to place a copy of the flattened image behind everything, gaussien blur
it and set its Opacity to 1%. This gives color data to the edge of the image
so it doesn't default to white.
*/// =======================================================
// Select all layers
var idselectAllLayers = stringIDToTypeID( "selectAllLayers" );
var desc4 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref2 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref2.putEnumerated( idLyr, idOrdn, idTrgt );
desc4.putReference( idnull, ref2 );
executeAction( idselectAllLayers, desc4, DialogModes.NO );
// Merge all layers
var idMrgtwo = charIDToTypeID( "Mrg2" );
executeAction( idMrgtwo, undefined, DialogModes.NO );
// Duplicate layer
var idCpTL = charIDToTypeID( "CpTL" );
executeAction( idCpTL, undefined, DialogModes.NO );
// Select furthest layer back
var idmove = charIDToTypeID( "move" );
var desc7 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref5 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref5.putEnumerated( idLyr, idOrdn, idTrgt );
desc7.putReference( idnull, ref5 );
var idT = charIDToTypeID( "T " );
var ref6 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idPrvs = charIDToTypeID( "Prvs" );
ref6.putEnumerated( idLyr, idOrdn, idPrvs );
desc7.putReference( idT, ref6 );
executeAction( idmove, desc7, DialogModes.NO );
// Blur layer
var idGsnB = charIDToTypeID( "GsnB" );
var desc8 = new ActionDescriptor();
var idRds = charIDToTypeID( "Rds " );
var idPxl = charIDToTypeID( "#Pxl" );
desc8.putUnitDouble( idRds, idPxl, 1.000000 );
try{executeAction( idGsnB, desc8, DialogModes.NO );}
catch(e){};
// Set opacity to 1%
var idsetd = charIDToTypeID( "setd" );
var desc9 = new ActionDescriptor();
var idnull = charIDToTypeID( "null" );
var ref7 = new ActionReference();
var idLyr = charIDToTypeID( "Lyr " );
var idOrdn = charIDToTypeID( "Ordn" );
var idTrgt = charIDToTypeID( "Trgt" );
ref7.putEnumerated( idLyr, idOrdn, idTrgt );
desc9.putReference( idnull, ref7 );
var idT = charIDToTypeID( "T " );
var desc10 = new ActionDescriptor();
var idOpct = charIDToTypeID( "Opct" );
var idPrc = charIDToTypeID( "#Prc" );
desc10.putUnitDouble( idOpct, idPrc, 1.000000 );
var idLyr = charIDToTypeID( "Lyr " );
desc9.putObject( idT, idLyr, desc10 );
executeAction( idsetd, desc9, DialogModes.NO );
}
/*// =======================================================
Export Save for Web
This exports the final image as a PNG-24 to your desktop with the same
name as the PSD.
*/// =======================================================
var saveOptions = new ExportOptionsSaveForWeb();
saveOptions.format = SaveDocumentType.PNG;
saveOptions.PNG8 = false;
currentDoc.exportDocument( File(pngDestination + fileName + ".png"),ExportType.SAVEFORWEB,saveOptions);
// Revert to the original document
var idRvrt = charIDToTypeID( "Rvrt" );
executeAction( idRvrt, undefined, DialogModes.NO );
alert("Export Complete");
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment