Thursday, May 21, 2026

scripting a storyboard template tool in photoshop javascript

this tool allows placing an image in a grid layout in a new photoshop document.  it has some examples for creating a simple ui in photoshop using javascript. it also has some examples of writing methods/creating variables in photoshop javascript. it also is an example of storing a photoshop ui file in separate file than the core of the tool.  there may be bugs or better ways to implement so please modify/use at your own risk.

here is a video of the tool in action:



//ui.jsx
//a simple tool to create story panels from an input image. example from a blank panel
//tested in Photoshop 10.0.1
//please modify/use at your own risk

#include "core.jsx"

/*check if input path is a valid image path
Args:
    path (str): path to check ex: Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg
Returns:
    (bool): true if path is a valid image.
*/
function isPathValidImage(path)
{
    var fileObj = new File(path);
    var validExtensions = ["jpg", "jpeg", "png", "tif"];

    if(!fileObj.exists)
    {
        return false;
    }

    var pathExt = path.split('.').pop().toLowerCase();
    //check if path's extension is in valid extensions
    for(var i=0; i<validExtensions.length; i++)
    {
        if(pathExt == validExtensions[i])
        {
            //path is valid extension. dont need to check any more extensions
            return true;
        }
    }

    return false;
}


/*
Returns:
    (bool): true if there is a document open false otherwise
*/
function isDocumentOpen()
{
    return app.documents.length > 0
}

function showUI(){

    if(!isDocumentOpen())
    {
        alert("requires a document open to use tool.")
        return
    }

    var dlg = new Window("dialog", "Panel Importer UI");
    dlg.orientation = "column";
    dlg.preferredSize = [600, 200];
    dlg.alignChildren = "left"; //align all widgets in window to the left

    //stores options for generating panels
    var parameterGroupLabel = dlg.add("statictext", undefined, "1. choose parameters for final panel layout")
    var parameterGroup = dlg.add("group");
    parameterGroup.orientation = "row";
    var numRowsLabel = parameterGroup.add("statictext", undefined, "rows:");
    var numRowsIntField = parameterGroup.add("edittext", undefined, "2");
    numRowsIntField.characters = 4; //set width

    var numColsLabel = parameterGroup.add("statictext", undefined, "cols:");
    var numColsIntField = parameterGroup.add("edittext", undefined, "2");
    numColsIntField.characters = 4; //set width

    var borderWidthPixelsLabel = parameterGroup.add("statictext", undefined, "border width (pixels):");
    var borderWidthPixelsIntField = parameterGroup.add("edittext", undefined, "20");
    borderWidthPixelsIntField.characters = 4; //set width    
    //

    //limit int fields to numbers only
    var intFields = [numRowsIntField, numColsIntField, borderWidthPixelsIntField];    
    //limit int fields to numbers
    var sharedOnChange = function()
    {
        //alert("field changed:" + this.text);
        //remove non numeric character
        this.text = this.text.replace(/[^\d]/g, '');
    }
    //assign shared onChange function
    for(var j=0; j<intFields.length; j++)
    {
        intFields[j].onChanging = sharedOnChange;
    }
    //


    //button
    var addPathBtn = dlg.add("button", undefined, "2. add image path to use for panel")
    //stores image path
    var imagePathText = dlg.add("statictext", undefined, "No Image Selected");
    imagePathText.preferredSize = [600, 50];

    var createPanelBtn = dlg.add("button", undefined, "3. create panels from image");
    var closeBtn = dlg.add("button", undefined, "close");

    //add function to button click
    addPathBtn.onClick = function()
    {
        var fileObj = File.openDialog("Select Image");
        if(fileObj)
        {
            selectedPath = fileObj.fsName;
            if(!isPathValidImage(selectedPath))
            {
                alert("selected path not a valid image. exiting");
                return;
            }
            imagePathText.text = selectedPath;
        }

    }

    createPanelBtn.onClick = function()
    {
        var imagePath = imagePathText.text;

        //get parameters from ui
        var numRows = parseInt(numRowsIntField.text);
        var numCols = parseInt(numColsIntField.text);
        var numPanels = numRows*numCols;
        var borderWidth = parseInt(borderWidthPixelsIntField.text);

        var panelImporter = new PanelImporter(imagePath=imagePath, 
                                            numPanels=numPanels, 
                                            numRows=numRows, 
                                            numCols=numCols, 
                                            borderWidth=borderWidth);
        panelImporter.doIt();
        alert("yaay creating storyboard panels done!");
        dlg.close(); //close ui when done so active document refreshed with story panels
    }

    closeBtn.onClick = function()
    {
        dlg.close();
    }

    dlg.show();

}


showUI();

//core.jsx
//old style class with ability to import an image multiple times to form a panel layout
/*
Args:
    imagePath(str): path to image to use for each panel
    numPanels(int): number of panels to create
    numRows(int): number of rows for storyboard
    numCols(int): number of cols for storyboard
    borderWidth(int): how many pixels to separate panels
*/
function PanelImporter(imagePath, numPanels, numRows, numCols, borderWidth)//(imagePath, numPanels, numRows, numCols, borderWidth)
{

    this.doc = app.activeDocument; //assumes in a created document. todo make a document if there is none
    this._imagePath = imagePath;
    this._numPanels = numPanels;
    this._numRows = numRows;
    this._numCols = numCols;
    this._borderWidth = borderWidth;

    /*do any additional initializations for this class
    apparently cant run a method of this class in its init. when using old style function for class
    */
    this._init = function()
    {
        this._images = this._getImages(this._imagePath, this._numPanels);//will hold image paths to use for each panel
    }
    
    /*
    Args:
        imagePath(str): path to image to use for each panel ex: /Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg
    */
    this.doIt = function()
    {
        this._init();
        this._createPanels();   
    }

    /*
    Args:
        imagePath(str): path to image to use for each panel ex: /Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg
        numPanels(int): number of times to repeat imagePath
    Returns:
        list(str): the imagePath repeated numPanels many times
    */
    this._getImages = function(imagePath, numPanels)
    {
        var images = [];
        //var imagePath = "/Users/Nathaniel/Desktop/photoshopTools/storyPanels/test_scene.jpg";
        for(var i=0; i<numPanels; i++)
        {
            images.push(imagePath);
        }

        return images;
    }

    /*creates storyboard panels using a single image for each panel
    */
    this._createPanels = function()
    {
        //var doc = app.activeDocument;
        var targetDoc = this.doc;//app.activeDocument;
        var images = this._images;
        var numRows = this._numRows;
        var numCols = this._numCols;
        var borderWidth = this._borderWidth;


        var imageIndex = 0; //will be used to query images
        var numImages = images.length;
        if(numImages > numRows*numCols)
        {
            alert("too many images. make sure number of images is smaller than number of rows multiplied by number of columns");
            return;
        }


        for(var i=0; i<numRows; i++)
        {
            for(var j=0; j<numCols; j++)
            {
                if(imageIndex >= numImages)
                {
                    break;
                }

                var pathToImage = images[imageIndex];
                var fileObj = new File(pathToImage);


                if(!fileObj.exists){
                    alert("image file doesnt exist");
                    return;
                }

                //open image file
                var imgDoc = app.open(fileObj);

                //select all and copy
                imgDoc.selection.selectAll();
                imgDoc.selection.copy();

                

                //close the opened image file
                imgDoc.close(SaveOptions.DONOTSAVECHANGES);

                //paste image into active document
                var newLayer = targetDoc.paste();

                var originalRulerUnits = app.preferences.rulerUnits;
                app.preferences.rulerUnits = Units.PIXELS;

                //set goal x y pixels
                //width = right - left
                //bounds [left, top, right, bottom]
                var widthImage = newLayer.bounds[2].value - newLayer.bounds[0].value;
                var heightImage = newLayer.bounds[3].value - newLayer.bounds[1].value;

                var goalX = j*widthImage + (j+1)*borderWidth; //the columns are used for x coordinate
                var goalY = i*heightImage + (i+1)*borderWidth;//the rows are used for the y coordinate

                var curX = newLayer.bounds[0].value;
                var curY = newLayer.bounds[1].value;

                var dx = goalX - curX;
                var dy = goalY - curY;

                //move imported image to goal x, y pixels
                newLayer.translate(dx, dy);

                //increment image index
                imageIndex += 1;
            }

        }
        //restore original untis
        app.preferences.rulerUnits = originalRulerUnits;
    }


}

Thanks for looking

Tuesday, April 7, 2026

Sketches













Thanks for looking

Thursday, March 5, 2026

Face sketch

 


Thanks for looking