2D skeletal animation tool
https://urraka.github.io/skel2d/
This is an experimental animation tool in which you define a skeleton and animations in a custom format while getting a live preview of the end result rendered with webgl.
sample: https://urraka.github.io/skel2d/#2327f320712b6f6e13be34c8a4e32747
- Setting up the skeleton
- Animation
- Skins
- Draw order
Skeleton with 2 child bones called bone1
and bone2
:
skeleton
bone1
bone2
Skeleton with a child bone called bone1
, which has a child bone called bone2
:
skeleton
bone1
bone1.bone2
Same as the previous one:
skeleton
bone1.bone2
As many levels as needed:
skeleton
grandpa.parent.child.grandchild
Notes:
- Indentation must be with tabs.
- Only alphanumeric characters plus
-
and_
are allowed for bone names. - A bone name can't start with a number.
- A bone name can't be
skeleton
.
l
: lengthr
: rotationx
: position-xy
: position-yi
: scale-xj
: scale-y- color (format is
#RGB
or#RRGGBB
optionally followed by an opacity value separated by a comma, i.e#F00,0.5
or#FF0000,0.5
)
flip-x
: flip on x axisflip-y
: flip on y axisno-rot
: don't inherit rotation from parentsno-scale
: don't inherit scale from parents
A red bone with length=100
, position=10,-10
, scale=1.5x
, flipped on x axis which doesn't inherit
rotation:
skeleton
parent.bone #F00 l100 r45 x10 y-10 i1.5 j1.5 flip-x no-rot
Notes:
- There can't be whitespace between a property and its value.
- The properties belong to the child-most bone defined in the line (in the example above
the properties apply to
bone
,parent
is unaffected).
The concept of slots and attachments is taken from Spine. Basically, each bone can have slots and each slot can have attachments (but only one active/visible attachment).
Currently, an attachment can be a sprite, a path or a shape.
This would define a bone with a slot named slot
which has two attachments named attachment1
and attachment2
. attachment1
will be the default active attachment for slot
because it's
the first one:
skeleton
bone
@slot[attachment1]
@slot[attachment2]
Note: having multiple attachments in one slot is only useful when doing animations, where you can switch a slot's active attachment.
If a bone has only one slot with one attachment it can be annoying to give them names, so by default a slot will take the owner bone name and attachments will take the owner slot name. Example of how this rule works:
skeleton
parent.bone
@ # slot named "bone" with attachment named "bone"
@[attachment] # slot named "bone" with attachment named "attachment"
@slot # slot named "slot" with attachment named "slot"
By default, an attachment will be of type none
which isn't very useful. This is a list of
available attachment type keywords:
:sprite
:rect
:circle
:ellipse
:path
This would define a slot with a rectangle attachment:
skeleton
bone
@ :rect
Slot properties must be placed before the attachment type keyword.
Currently, slots have only one property which is its color. Example:
skeleton
bone
@ #F00 :rect
The slot color will be multiplied with attachment colors. By default it's #FFF
so it won't affect
the attachment. What makes the slot color different from whatever color the attachment has is that it
can be animated.
Attachment properties must be placed after the attachment type keyword.
All attachment types share the same transform properties as bones for defining position, scale
and rotation (x
, y
, i
, j
, r
).
Shape and path attachments (that is, all of them except sprites) share some properties to define the stroke and fill styles:
t
: thickness for the stroke (default:1
)m
: miter limit (default:10
)f
: fill color (default:#000
)s
: stroke color (default:#000
)miter-join
orbevel-join
orround-join
: line join style (defaultmiter-join
)butt-cap
orsquare-cap
orround-cap
: line cap style (default:butt-cap
)
The following are properties specific to some attachment types:
w
: width (used by rects and ellipses, default:0
)h
: height (used by rects and ellipses, default:0
)d
: diameter for a circle or for rect corners (for making rounded rects, default:0
)
Example: a 100x80 rect with a line width of 5, filled with blue, rotated 90 degrees and round line joins:
skeleton
bone
@ :rect w100 h80 t5 f#00F r90 round-join
Sprites only have one additional property (apart from transform properties) which is a string with the name of the image:
skeleton
bone
@ :sprite "image-name.png"
Note: currently, strings are limited to no whitespace.
Paths are defined by the following commands:
M <point>
: move to (only supported as the first command, if ommited it will default to0,0
)L <point>
: line toQ <ctrl-point> <end-point>
: quadratic curve toB <ctrl-point1> <ctrl-point2> <end-point>
: bezier curve toC
: close path: <bone>
: switch default bone binding (doesn't count as a command and can be used beforeM
)
Each point given as a parameter to a command is bound to a bone to which it is relative to. By default, that bone is the one that owns the attachment, but it can be any bone in the skeleton. This allows paths to be changed by animating bones.
The format to define a point is x,y
or x,y:bone
to bind the point to a bone other than the default.
Simple path example, a red triangle with black outline:
skeleton
bone
@ :path f#F00
M 0,0 # M could be ommited here because it's the default 0,0
L 25,50
L 50,0
C
A bezier curve:
skeleton
bone
@ :path
B 50,100 100,-100 150,0
Same bezier curve but with points bound to different bones:
skeleton
bone
@ :path
B 0,0:cp1 0,0:cp2 0,0:end
bone.cp1 x50 y100
bone.cp2 x100 y-100
end x150 y0
Using the default bone binding switch (this is the same red triangle as above but the top vertex can
be controled by bone.child
):
skeleton
bone x150
@ :path f#F00
M 0,0
: child
L 0,0
: bone
L 50,0
C
bone.child x25 y50
Notes:
- When referencing a bone, it's enough to give the shortest unambiguous name for it. If the name
of a bone that is a direct child of
skeleton
happened to be ambiguous, theskeleton
keyword can be used as the parent bone to disambiguate. For example, if there are two bonesbone
andbone.bone
, the former can be referenced asskeleton.bone
and the latter asbone.bone
. - Currently, path rendering is quite limited and filling will only work with simple covex shapes.
Animations are defined with the anim
keyword. A name to identify it can be given with a string:
anim "name"
# ...
Animations consist of timelines. There are bone and slot timelines. Each timeline is used to animate a single animatable property from either a bone or a slot.
The following lists all the animatable bone and slot properties:
skeleton
bone
@slot
anim "name"
bone
r # rotation
x # position-x
y # position-y
i # scale-x
j # scale-y
s # flip-x
t # flip-y
@slot
@ # current attachment
r # color (red component)
g # color (green component)
b # color (blue component)
a # color (alpha component)
c # color
Notes:
- Slot names must have a leading
@
. - Bone and slot names can be the shortest unambiguous name that uniquely identifies them (same rule explained in path attachments notes).
- If a slot is given a color timeline (
c
), the individual color component timelines won't take effect.
An animation has the following properties:
fps
: frames per second (default:20
)frame
: defines the starting frame (default:0
)step
: number of frames to advance on each step (default:5
)easing
: function used for interpolation between frames (default:li
)
These properties have a specific syntax to define them. Here's an example that would define the default values:
anim "name" 20fps 0:5:li
# ...
As shown above, frame
, step
and easing
are defined together separated by a colon. It's not
necessary, however, to give a value to all of them. These are all valid ways of setting these
properties:
Syntax | Description |
---|---|
0: or 0:: |
sets frame |
:5: or :5 |
sets step |
::li or :li |
sets easing |
0:5: or 0:5 |
sets frame and step |
0::li |
sets frame and easing |
:5:li or 5:li |
sets step and easing |
0:5:li |
sets frame , step and easing |
These properties (frame
, step
and easing
) can be overriden on each animation item:
anim "name" 0:4
bone :2: # change step to 2
# ...
The purpose of timelines is to define a list of key frames for a given property. The syntax to define them can be thought of as a list of commands. For example:
anim "name"
bone
x 0 -> 50 --> 0
The above can be read as:
- Add key frame with value
0
- Advance one step (5 frames by default)
- Add key frame with value
50
- Advance two steps (10 frames)
- Add key frame with value
0
Most of the animatable properties take a numeric value, but there are some exceptions. Flip
timelines (s
and t
) take a boolean value (true
or false
). Attachment timelines (@
)
take an attachment name from the list of attachments available for the slot that is
being animated. Finally, color timelines (c
) take a color value with the same format described
in bone properties.
The following illustrates the syntax for the commands available:
-1.5 # add key frame with value -1.5 (numeric timelines)
+-1.5 # add key frame incrementing the previous key frame by -1.5 (numeric timelines)
*-1.5 # add key frame multiplying the previous key frame by -1.5 (numeric timelines)
true # add key frame with value "true" (flip timelines)
false # add key frame with value "false" (flip timelines)
name # add key frame with value "name" (attachment timelines)
> # advance 0 steps (noop)
-> # advance 1 step
----> # advance 4 steps (number of hyphens defines the number of steps to advance)
0:5:li> # set frame to 0, set step to 5, set easing to li and advance 0 steps
0:3:li-> # set frame to 0, set step to 3, set easing to li and advance 1 step (3 frames)
+1:> # advance 1 *frame* (and 0 steps)
+1::li> # advance 1 *frame*, set easing to li (and advance 0 steps)
{ # begin loop
}[2] # end loop (loop 2 times)
Notes:
- The syntax that sets
frame
,step
andeasing
is identical to the one described in animation properties, except that it must be followed by "advance zero or more steps" (>
preceded by zero or more-
) and that it can setframe
relatively by incrementing the current frame byx
(+x:>
). - Adding multiple key frames without advancing in time will result in only one key frame with the value of the last one.
- Setting
frame
to go back in time won't work. - Setting the
step
value will take immediate effect, i.e.:10:->
will advance10
frames. It also takes effect on all the following commands (until changed again). - Setting
easing
will change the function used to interpolate the previous key frame with the next one, and it will also change the default easing function for all the following frames (until changed again). - Whitespace is important. All the commands illustrated must be separated by whitespace and there
must not be any whitespace within a command (i.e.
{0 -> 1 -> 0}[2]
is invalid, must be{ 0 -> 1 -> 0 }[2]
). - Color component timelines (
r
,g
,b
anda
) take values between0
and1
. However, they are not restricted to it.
A more elaborate example:
skeleton
bone l50
@[red] :circle d30 t0 f#F00
@[blue] :circle d30 t0 f#00F
@line :path
L 0,0:handle
bone.handle
anim "test" 20fps
bone 40:sio
x 0 -> 200 -> 0
y :10:> 0 :so-> 20 :li--> 20 :si-> 0 :so-> -20 :li--> -20 :si-> 0
s false -> true -> false
i :10:> 0.1 -> 1 --> 1 -> 0.1 -> 1 --> 1 -> 0.1
handle :5:
x { -15 :si-> 0 :so-> 15 :si-> 0 :so-> -15 }[4]
y { 0 :so-> 15 :si-> 0 :so-> -15 :si-> 0 }[4]
@bone :40:
@ red -> blue -> red
See it in action: http://urraka.github.io/skel2d/#3a69c6d2c25e67f9950a
Easing functions change the way a key frame is interpolated with the next one for a smooth
transition. The default is a linear interpolation (li
). Currently there are only a few functions
available:
Identifier | Description | Function |
---|---|---|
li |
linear | y = x |
si |
sin-in | y = 1 - sin(pi/2 + x * pi/2); |
so |
sin-out | y = sin(x * pi/2) |
sio |
sin-in-out | y = 0.5 + sin(x * pi - pi/2) / 2 |
These functions are used to convert a value t
between 0
and 1
into a value t'
. The result
is always used in a linear interpolation. So given two key frame values a
and b
, the function
used to interpolate them will be a + (b - a) * t'
(or a + (b - c) * f(t)
where f
is the
easing function).
Skins are used to change the visuals of the skeleton by redefining its attachments.
Example:
skeleton
bone
@ :circle d10 f#F00
@slot :rect w10 h10
@other[small] :ellipse w20 h10
@other[big] :ellipse w50 h20
skin "skin-name"
@bone :circle d10 f#00F # change to blue fill color
@slot # remove (attachment of type none)
@other[small] :ellipse w10 h5 # make it smaller
Notes:
- The last attachment is left untouched so it will appear as defined in the skeleton.
- All properties must be redefined when overriding an attachment, even if they don't change.
The naming rules are the same as described in path attachments notes. The following would have the same effect as the previous example:
skeleton
bone
@ :circle d10 f#F00
@slot :rect w10 h10
@other[small] :ellipse w20 h10
@other[big] :ellipse w50 h20
skin "skin-name"
@skeleton.bone.bone[bone] :circle d10 f#00F
@skeleton.bone.slot[slot]
@skeleton.bone.other[small] :ellipse w10 h5
By default, skeleton attachments are drawn in the order in which their owner slots are defined. This can be changed by defining a draw order:
order
@slot1
@slot2
# ...
It's simply a list of slots from the skeleton. The ones on top will be drawn first, so they will appear in the back of following slots. Unlisted slots will be pushed at the end of the list in the order they were defined in the skeleton.