Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Functions For The Scripting Feature + Bugs #1858

Open
haloflooder opened this issue Aug 30, 2018 · 8 comments
Open

New Functions For The Scripting Feature + Bugs #1858

haloflooder opened this issue Aug 30, 2018 · 8 comments
Assignees
Labels
Milestone

Comments

@haloflooder
Copy link

@haloflooder haloflooder commented Aug 30, 2018

I've been playing around with Aseprite's scripting feature in 1.2.9 and I feel like there should be more functions so scripts can be more flexible. I also found some bugs as well.

Once scripts are stable and have more flexibility. People will be able to create scripts to add features that Aseprite doesn't have.

Features

Functions For Data Input

Scripts would be extremely useful if we are capable of inputting data via dialog window. Not sure what you would think about a "GUI Builder" type of system. I think it would be more flexible and less annoying compared to a bunch of windows popping up one after another asking a series of questions such as "WHAT COLOR DO YOU WANT" "HOW MANY FINGERS AM I HOLDING?" "WHAT'S YOUR MOTHER's BIRTHDAY?"

I created a mock up image of what it can potentially look like using the example code below.

Show Dialog Window Functions, Code, and Mock Up Image

Each element for the dialog window api uses a user defined variable that they set before creating the dialog window. When the dialog window goes away, it will update the user defined variables with the new values from the input from the element.

Dialog.create(string)
string: Window title
description: This function will be the first thing to do when creating a dialog window
returns: dialog window object

Dialog.show(obj)
obj: The dialog window object
description: This function will make a dialog window pop up on the screen.
returns: True is function executed successfully

obj.addHeader(string)
string: Title for section header
description: This makes a section header to help organize the dialog window's content.
returns: True if element is created successfully

obj.addColorPicker(string, user defined var)
string: The label for the element
user define var: This is where a variable would go. The color picker will also use it as the default value
description: This element creates a color picker that is similar to the color picker in the "Insert Text" tool
returns: True if element is created successfully

obj.addStringInput(string, user defined var, [string])
string: The label for the element
user defined var: This is where the user defined variable would go. The text field will also use it as the default value
[string]: An optional string that adds a gray suffix to the text field. e.g. 128px (the px is the suffix that is grayed out)
description: This element creates a standard text field
returns: True if element is created successfully

obj.addDropdown(string, table, user defined var)
string: The label for the element
table: Contains values to fill in the drop down menu
user defined var: This is where the user defined variable would go. The drop down menu will use it as the default value.
description: This element creates a drop down menu. The user defined value would be updated with the value in the drop down menu when the dialog window goes away.
returns: True if element is created successfully

obj.addNumberInput(string, user defined var, [string])
string: The label for the element
user defined var: This is where the user defined variable would go. The number field will also use it as the default value
[string]: An optional string that adds a gray suffix to the number field. e.g. 128px (the px is the suffix that is grayed out)
description: This element creates a standard number field
returns: True if element is created successfully

obj.addNumberSlider(string, min, max, user defined var)
string: The label for the element
min: Minimum number for the slider
max: Maximum number for the slider
user defined var: This is where the user defined variable would go. The number slider will also use it as the default value
description: This element creates a number slider that is similar to the opacity slider
returns: True if element is created successfully

obj.addStaticString(string, string)
string: The label for the element
string: An uneditable string in the text field
description: This element creates a static text that can't be edited
returns: True if element is created successfully

obj.addCheckbox(string, user defined var, string, user defined var, ... )
string: The label for the checkbox
user defined var: This is where the user defined variable would go. The variable is a boolean so the checkbox will be ticked depending on the state of the variable
description: This element creates a series of checkboxes. There can be an unlimited amount of checkboxes in the arguments.
returns: True if element is created successfully

obj.addRadio(table, user defined var)
table: The table should contain multiple string values to define the labels for the radio buttons
user defined var: This is where the user defined variable would go. The variable is an integer that also sets the default selected radio button.
description: This element creates a series of radio buttons. There can be an unlimited amount of radio buttons but only one can be selected.
returns: True if element is created successfully

obj.addButtons(string, user defined function, string, user defined function, ... )
string: The label for the button
user defined function: This is where the user defined function would go. If the button is pressed. It would close the window, update the user defined variables with the new values, and execute the user defined function.
returns: True if element is created successfully

Example Code

local pc = app.PixelColor

local color1 = pc.rgba(255,255,255,255)
local color2 = pc.rgba(0,0,0,255)
local str = "Default Text"
local dropdown = "Test 1"
local dropdownOptions = {"Test 1", "Test 2", "Test 3"}
local num1 = "80085"
local num2 = "255"
local check1 = true
local check2 = false
local radio = 0;

local okFunc = function()
    print("Color picked: " .. pc.rgbaR(color1) .. "," .. pc.rgbaG(color1) .. "," ..
            pc.rgbaB(color1) .. "," .. pc.rgbaA(color1))
    print("String entered: " .. str)
    print("Dropdown selection: " .. dropdown)
    print("Number entered: " .. tostring(num1))
    print("Slider input: " .. tostring(num2))
end

local cancelFunc = function()
    print("You cancelled me!")
end

local window= Dialog.create("Example Window")

local radioText = {"Radio 1", "Radio 2"}

window.addHeader("I am a title of a section!")
window.addColorPicker("Color",color1)
window.addStringInput("Text",str)
window.addDropdown("Dropdown", dropdownOptions, dropdown);
window.addNumberInput("Number",num1)
window.addNumberSlider("Slider",0,255,num2)
window.addStaticString("Hello", "World")
window.addCheckbox("Checkbox 1",check1,"Checkbox 2",check2)
window.addRadio(radioText, radio);
window.addHeader(""); -- Just shows a dotted line
window.addButtons("OK",okFunc,"Cancel",cancelFunc, ... )

Dialog.show(window)

image

Adding To The Selection API

The selection API needs a function where you can check to see if a coordinate in the image is currently selected or not. This will be extremely helpful if you only want to manipulate part of an image that the user selected with a wand/polygon/etc tool. Something like isSelected(x,y) would be awesome.

There also should be a way to add more selection instead of just having 1 selection (similar to holding the shift key to add more selections)

Show Functions
Selection.isSelected(x,y)
Description: Checks to see if the coordinate is part of a selection
Returns: True if coord is part of a selection

Selection.addSelection(Rectangle)
Description: Adds a selection to the existing selection
Returns: True if the task completed successfully

Selection.deselect([Rectangle]) // Modified version of the deselect api
Description: Removes a selection from the existing selection with the provided Rectangle object. If the is no Rectangle provided, deselect everything so it's backward compatible with existing scripts
Returns: True if the task completed successfully

Selection.getSelections()
Description: Returns a table filled with Rectangle objects if there are more than 1 rectangle in the selection
Returns: Table filled with rectangles.
-- Creates a checkerboard pattern in a selection
local pc = app.pixelColor
local img = app.activeImage

local sel = spr.selection
local rect = sel.bounds

local dotCol = col.rgba(0,0,0,255);

for y=rect.y, rect.y+rect.height, 1 do
    for x=rect.x, rect.x+rect.height, 1 do
        if (sel.isSelected(x,y)) then -- Useful in this case
            if ((x+(y%2))%2 == 0) then
                img.putPixel(x,y, dotCol);
            else
                img.putPixel(x,y, img.getPixel(x,y));
            end
        end
    end
end

Functions For The Timeline

It would be nice to be able to change layers in a script or even change the frame. It could be useful for creating a custom script that outputs a sprite sheet for a specific game developing software or automatically creating a ghosting effect for animations.

The layer's index could be used as the layer's id's or there could be unique id's assigned to each layers.

Show Layer Functions This is a list of functions that would be useful with layers.
newLayer([int])
int: Optional argument that allows you to create multiple new layers
Description: Creates a single new layer on top of the selected layer or multiple new layers if an integer is provided. 
Returns: A table filled with id's of the newly created layers

deleteLayer(int)
int: Layer's id
Description: Deletes a layer with the provided layer id
Returns: True if the task completed successfully

clearLayer(int)
int: Layer's id
Description: Clears the entire layer so it's empty.
Returns: True if the task completed successfully

flattenLayer(int,[int])
int: layer's id
int: An optional argument that is the layer's id
Description: Flattens the layer to the layer below if there is no second argument. If there is a second argument, it will flatten the first layer id to the second layer id.
Returns: The layer's id it was flattened to

moveLayer(int,int)
int: Layer's id (From)
int: Layer's id (To)
Description: This function moves a layer to a lower or higher depth. It shouldn't replace an existing layer
Returns: True if the task completed successfully

getActiveLayer()
Description: Gets the layer that's currently selected
Returns: The layer's id

setActiveLayer(int)
int: Layer's id
Description: Selects a layer in Aseprite with the provided id
Returns: True if the layer exists

getLayers()
Description: Gets all the layers in the sprite document
Returns: A table with layer id's

groupCreate([string])
string: Optional string that set the group's name
Description: Creats a new empty group
Returns: Group layer's id

groupDelete(int)
int: Layer's id
Description: Deletes the group along with the layers within the group
Returns: True if task is completed successfully

groupLayers(array,[string])
table: Contains multiple values of layer id's
string: Optional string that set the group's name
Description: Creates a new group with the provided layer id's in the table.
Returns: Group layer's id

groupGetLayers(int)
int: layer's id
Description:  Gets all the layers within the group layer
Returns: Table of layers within the group

groupAddLayers(table)
table: A table that contains layer id's
Description: Adds the layers from the table to the group
Returns: True if task is completed successfully or table of layer id's that were added to the group

groupRemoveLayers(table)
table: A table that contains layer id's
Description: Removes the layers from the table and puts the removed layer underneath the group
Returns: True if task is completed successfully or table of layer id's that were removed from the group

groupMoveLayer(int)
int: layer's id
Description: The moves the layer within the group.
Returns: True if task is completed successfully

getLayerType(int)
int: Layer's id
Description: Gets the layer type which will either be a layer or a group
Returns: Layer's type (0, 1, etc)

getLayerName(int)
int: Layer's id
Description: Gets the layer's name
Returns: A string

setLayerName(int,string)
int: Layer's id
string: A string to name the layer
Description: This function gives the layer a new name
Returns: True if task is completed successfully

getLayerMode(int)
int: Layer's id
Description: This function gets the layer's mode which can just be integers
Returns: An integer that represents the layer's mode

setLayerMode(int.int)
int: Layer's id
int: The Mode
Description: Sets the layer's mode with the provided integer
Returns: True if the task completed successfully

getLayerOpacity(int)
int: Layer's id
Description: Gets the layer's opacity
Returns: The layer's opacity

setLayerOpacity(int,int)
int: Layer's id
int: The new opacity
Description: Sets the layer's opacity
Returns: True if the task completed successfully

getLayerParent(int)
int: Layer's id
Description: Gets the layer's parent if it has one
Returns: -1 if there is no parent or the parent's layer id


Show Frame/Cel Functions

Frame Functions

newFrame(int,[bool])
int: The amount of frames it should create
bool: Whether or not it should copy the images of the layers from the active frame
Description: Creates new frames in front of the active frame. If the second argument is true, then it will copy the contents from the active frame.
Returns: True if the task successfully completed

getActiveFrame()
int:
Description: Gets the frame that's currently selected
Returns:

setActiveFrame(int)
int: A frame number
Description: Selects a frame in Aseprite with the provided number
Returns: True if the task successfully completed

getFrames()
Description: Gets a total amount of frames the sprite document has
Returns: The total amount of frames from the document.

setFrameDuration(int,int)
int: The frame number
Description: Sets the frame's duration
Returns: True if the task completed successfully

getFrameDuration(int)
int: The frame number
Description: Gets the frame's duration
Returns: The frame's duration

addFrameTag(string, int, int, PixelColor, int)
string: Tag name
int: From frame
int: To frame
PixelColor: Tag's color
int: Animation direction (0,1,2)
Description: Creates a frame tag
return: True if task successfully completed

getFrameTag(int)
int: The frame number
Description: Gets the frame's tag information
Returns: A tag object or a table filled with property values

getFrameTags()
Description: Returns a table of tables with the starting/ending of frames of tags (e.g. { {0,4}, {5,10}, ... } )
Returns: A table of tables with frame numbers or a table of tag objects

setFrameTag(int, string, int, int, PixelColor, int)
int: Frame number that is in a tag
string: Tag name
int: From frame
int: To frame
PixelColor: Tag's color
int: Animation direction (0,1,2)
Description: Updates an existing frame tag with new values

Cel Functions

To-Do. Don't know too much about duplicating cels

linkCels(int,int)
int: Frame number
int: Frame number
Description: Links the cells on the active layer within the frame numbers
Returns: True if the task completed successfully.

Functions For The Image/Sprite API

There should also be a way to get a merged image from the sprite document since the active image only return an image from the selected layer. We also need to get better support for Indexed mode as well. It would be nice to have access to the indexed colors. Manipulating the palette would be great as well.

It would also be nice to have a virtual image for caching images/pixels on. Could technically use tables to store pixel data but it would be easier to manage things with a virtual image.

Being able to close/change tabs would be useful for certain tasks.

Show Functions
sprite.getPalette()
Description: Retrieves the palette that the sprite is using
Returns: A table of PixelColors in the correct indexed order.

sprite.setPalette(table)
Table: A table filled with PixelColors
Description: Replaces the current palette with the one provided in the table
Returns: True if the task completed successfully

sprite.getFlattenedImage()
Description: Gets a flattened version of the entire sprite document
Returns: An image object

image.x
Description: Returns the x coord of the image (see bugs for more info)
Return: X Coord

image.y
Description: Returns the y coord of the image (see bugs for more info)
Return: Y Coord

image.rotate(int)
int: The rotational degree
Description: Rotates the image with the provided number
Return: True if the task completed successfully

app.virtualImage(w,h)
Description: Creates a virtual image with the provided width and height. Virtual image deletes itself when the script is done with the execution.
Returns: Image object

app.listTabs()
Description: Gets all the tabs that are open in Aseprite
Returns: Table filled with tab names

app.setActiveTab(string)
string: Tab name
Description: Change the active tab to the provided tab name
Returns: True if the task completed successfully

app.closeTab(string)
string: Tab name
Description: Closes the tab that's open in Aseprite. Asks the user to save the document if it wasn't recently saved.
Returns: True if the task completed successfully

Misc Functions

I think it would be neat if we had access to the clipboard so we can manipulate it (text, image, etc). Networking would be interesting to have as well so we can communicate with other applications. I know LUA have networking but I don't know if you'll be implementing that feature. Also, I wonder if it would be possible to add scripts that runs in the background of Aseprite.

Bugs

Weird Things About The Image API

Currently, I don't know if this is a bug or a feature or if you fixed the issue recently. The active image is a bit weird when it comes to the width and height. If you create a new empty document that is 128x128. The image size will return 128x128 until you start drawing on it (see image below).
image
If you draw outside the bounds of an image, Aseprite will crash. I feel like Aseprite should either ignore or give a warning with putPixel(x,y,col) if the programmer/scripter accidentally draw outside of the image. This issue could also be fixed if the image had an X and Y value in the property.

Speed Issue With Image API

Speed is another weird issue. The example script that was provided with Aseprite runs relatively fast (36ms with a 128x128 sprite). I made a script that creates a checkerboard pattern by just drawing a pixel every other pixel and it took 230ms to execute on a 128x128 sprite. I documented everything I did HERE that eventually lead to a script that takes 16ms to render a checkerboard pattern on a 128x128 sprite.

Minor Bug With Selection

When you make a new selection with a script. The new selection won't render on Aseprite but the selection is still made.

Aseprite and System version

  • Aseprite version: v1.2.9 x64 (steam)
  • System: Windows 10 Pro x64
@dacap dacap added the scripting label Aug 30, 2018
@haloflooder
Copy link
Author

@haloflooder haloflooder commented Sep 3, 2018

Just got some additional ideas while I was making random stuff with scripts

Getting Access to the User's Selected Colors

I think this would be a simple thing to add to scripting. Being able to get the color from the Foreground and Background color the user selected.

Show Functions
sprite.activeForegroundColor
sprite.activeBackgroundColor
Returns: The color from the user's selected foreground/background color

Extensions Support

I know that there is already a plan to add support to import scripts through extensions but @jmswrnr had good idea with scripts that is added through extensions.

What if there was an ability to create custom tools through extensions that executes a script? Could also import a custom icon for the custom tool with an image that is included in the zip file. Also, being able to add to the parameters on the top bar will be, in jmswrnr's words: "icing on the top".

Kinda like how you can choose the brush size, tolerance, the contiguous checkbox, etc with the other tools. Being able to create parameters with the extension and getting access to those parameters with a script will be amazing.

@dacap
Copy link
Member

@dacap dacap commented Sep 6, 2018

I'll be replying by parts from time to time:

sprite.activeForegroundColor
sprite.activeBackgroundColor

Will be app.fgColor and app.bgColor

Selection.isSelected(x,y)

Will be Selection:contains(Point)

Selection.addSelection(Rectangle)

Will be Selection:add(Rectangle | Selection)

Selection.deselect([Rectangle])

Will be Selection:subtract(Rectangle | Selection)

Selection.getSelections()

Will not be implemented yet. I wand to implement a pixel iteration
using something like this:

local img = Image(32, 32)
local sel = Selection()
sel:select(2, 2, 8, 8)
sel:add(4, 4, 8, 8)
for it in img:pixels(sel) do
  local c = it() -- get pixel color
  ...
  it(c)          -- set pixel color
end
@dante8300
Copy link

@dante8300 dante8300 commented Sep 12, 2018

Wow thank you for this information!!!

@dacap dacap self-assigned this Oct 9, 2018
@dacap dacap added this to the v1.2 milestone Oct 9, 2018
@dacap
Copy link
Member

@dacap dacap commented Oct 10, 2018

Already some progress with dialogs:

screen shot 2018-10-10 at 11 33 50

local dlg = Dialog()
local function ok()
  data = dlg.data
  app.alert(string.format("text='%s' height=%d", data.text, data.height))
  dlg:close()
end
dlg
  :separator("I am Title of a Section!")
  :color{id="color",label="Color:",color=Color(255,0,0,255)}
  :button{text="Semi-White",
          onclick=
            function()
              local data = dlg.data
              data.color = Color(255,255,255,128)
              dlg.data = data
            end}
  :number{id="height",label="Height:",text="0"}
  :button{text="Inc",
          onclick=
            function()
              local data = dlg.data
              data.height = data.height+1
              dlg.data = data
            end}
  :entry{id="text",text="Default Text",label="Text:"}
  :label{label="Hello:",text="World"}
  :check{id="check1",text="Checkbox 1"}
  :check{id="check2",text="Checkbox 2"}
  :radio{id="radio1",text="Radio 1",selected=true}
  :radio{id="radio2",text="Radio 2",selected=false}
  :radio{id="radio21",text="Radio 1",label="Group2"}
  :radio{id="radio22",text="Radio 2"}
  :radio{id="radio31",text="Radio 1",label="Group3"}
  :radio{id="radio32",text="Radio 2"}
  :separator()
  :button{text="&OK",id="ok",onclick=ok}
  :button{text="&Cancel",id="cancel"}
  :show()
@dacap
Copy link
Member

@dacap dacap commented Oct 11, 2018

The Dialog() will be released in v1.2.10, commit: bccd3f1 docs: https://github.com/aseprite/api/blob/master/api/dialog.md#dialog

@dacap dacap modified the milestones: v1.2, v1.x-bugs Dec 11, 2018
@Jengamon
Copy link

@Jengamon Jengamon commented Jun 10, 2019

Something that would be cool would be the ability to send an Image to the clipboard. I'm building a script that takes an image as input, cuts, and rearranges the image into a new Image, but it is only one out of many, so it makes no sense to do anything but send it to the clipboard directly for pasting into another larger image.

@PureAsbestos
Copy link

@PureAsbestos PureAsbestos commented Oct 10, 2019

@dacap radio buttons behave very strangely. If there were just a separate parameter for a group ID, everything would be so much easier

@PureAsbestos
Copy link

@PureAsbestos PureAsbestos commented Oct 10, 2019

I can give more specifics if you want. Not sure if this should be a separate issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants
You can’t perform that action at this time.