Skip to content

Commit c43148b

Browse files
committed
Animation Editor
1 parent 574aa9c commit c43148b

5 files changed

Lines changed: 958 additions & 1 deletion

File tree

browserassets/src/tgui/tgui.bundle.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

code/modules/admin/admin_verbs.dm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ var/list/admin_verbs = list(
388388
/client/proc/cmd_caviewer,
389389
/client/proc/cmd_paraviewer,
390390
/client/proc/cmd_ambient_viewer,
391+
/client/proc/cmd_animviewer,
392+
391393
/client/proc/cmd_custom_spawn_event,
392394
/client/proc/cmd_special_shuttle,
393395
/client/proc/toggle_all_artifacts,
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// A lightweight datum to create and manage animation steps (inspired by parallax_viewer.dm).
2+
// Provides a simple API for creating, editing, reordering, serializing and previewing animation steps.
3+
4+
/client/proc/cmd_animviewer()
5+
SET_ADMIN_CAT(ADMIN_CAT_FUN)
6+
set name = "Animation Editor"
7+
set desc = "Animation Editor"
8+
ADMIN_ONLY
9+
SHOW_VERB_DESC
10+
11+
if(holder)
12+
var/datum/animation_editor/E = new /datum/animation_editor(src.mob)
13+
E.ui_interact(mob)
14+
15+
/datum/animation_editor
16+
var/list/steps = list() // list of /datum/animation_step
17+
var/atom/target = null
18+
var/list/valid_keys = list(
19+
"alpha",
20+
"color",
21+
// "glide_size",
22+
"infra_luminosity",
23+
"layer",
24+
"maptext_width",
25+
"maptext_height",
26+
"maptext_x",
27+
"maptext_y",
28+
"luminosity",
29+
"pixel_x",
30+
"pixel_y",
31+
"pixel_w",
32+
"pixel_z",
33+
"transform",
34+
"dir",
35+
"icon",
36+
"icon_state",
37+
"invisibility",
38+
"maptext",
39+
"suffix",
40+
)
41+
42+
/datum/animation_editor/New()
43+
. = ..()
44+
45+
/datum/animation_editor/ui_state(mob/user)
46+
return tgui_admin_state
47+
48+
/datum/animation_editor/ui_static_data(mob/user)
49+
. = list()
50+
.["valid_keys"] = src.valid_keys
51+
.["easing_options"] = list(
52+
"LINEAR_EASING" = LINEAR_EASING,
53+
"CIRCULAR_EASING" = CIRCULAR_EASING,
54+
"SINE_EASING" = SINE_EASING,
55+
"QUAD_EASING" = QUAD_EASING,
56+
"CUBIC_EASING" = CUBIC_EASING,
57+
"BOUNCE_EASING" = BOUNCE_EASING,
58+
"ELASTIC_EASING" = ELASTIC_EASING,
59+
"JUMP_EASING" = JUMP_EASING
60+
)
61+
.["easing_flags"] = list(
62+
"EASE_IN" = EASE_IN,
63+
"EASE_OUT" = EASE_OUT
64+
)
65+
66+
.["flags"] = list(
67+
"ANIMATION_END_NOW" = ANIMATION_END_NOW,
68+
"ANIMATION_LINEAR_TRANSFORM" = ANIMATION_LINEAR_TRANSFORM,
69+
"ANIMATION_PARALLEL" = ANIMATION_PARALLEL,
70+
"ANIMATION_RELATIVE" = ANIMATION_RELATIVE,
71+
"ANIMATION_CONTINUE" = ANIMATION_CONTINUE,
72+
"ANIMATION_SLICE" = ANIMATION_SLICE,
73+
"ANIMATION_END_LOOP" = ANIMATION_END_LOOP
74+
)
75+
76+
/datum/animation_editor/ui_data()
77+
. = list()
78+
.["steps"] = src.steps
79+
.["target"] = isatom(src.target) ? src.target.name : null
80+
81+
/datum/animation_editor/ui_act(action, list/params, datum/tgui/ui)
82+
. = ..()
83+
USR_ADMIN_ONLY
84+
if(.)
85+
return
86+
87+
var/step_index
88+
if( !isnum(params["index"]) || params["index"] > length(src.steps) )
89+
step_index = null
90+
else
91+
step_index = params["index"]+1 // adjust for 1-based indexing
92+
93+
. = TRUE
94+
switch(action)
95+
96+
if("update_step")
97+
if(!step_index)
98+
return
99+
var/step = src.steps[step_index]
100+
if(!step)
101+
return
102+
103+
switch(params["field"])
104+
if("name")
105+
step["name"] = params["value"]
106+
if("time", "loop", "easing", "flags")
107+
if(isnum(params["value"]))
108+
step[params["field"]] = params["value"]
109+
110+
if("import_steps")
111+
if(!params["data"])
112+
return
113+
var/data = json_decode(params["data"])
114+
// validate the crap out of it
115+
if(!islist(data))
116+
return
117+
for(var/i = 1; i <= length(data); i++)
118+
var/step = data[i]
119+
if(!islist(step))
120+
return
121+
if(!("var_list" in step) || !("time" in step))
122+
return
123+
for(var/key in step["var_list"])
124+
if(!(key in src.valid_keys))
125+
return
126+
// if we made it here, it's probably fine
127+
src.steps = data
128+
129+
if("update_step_var")
130+
if(!step_index)
131+
return
132+
var/step = src.steps[step_index]
133+
if(!step)
134+
return
135+
var/key = params["key"]
136+
if(!(key in src.valid_keys))
137+
return
138+
// Update var value
139+
step["var_list"][key] = params["value"]
140+
141+
if("delete_step_var")
142+
if(!step_index)
143+
return
144+
var/step = src.steps[step_index]
145+
if(!step)
146+
return
147+
var/key = params["key"]
148+
if(!(key in step["var_list"]))
149+
return
150+
// Delete var
151+
step["var_list"] -= key
152+
153+
if("move_step")
154+
if(!step_index)
155+
return
156+
var/step = src.steps[step_index]
157+
if(!step)
158+
return
159+
src.steps.Swap(step_index, params["new_index"]+1)
160+
161+
if("add_step_var")
162+
if(!step_index)
163+
return
164+
var/step = src.steps[step_index]
165+
if(!step)
166+
return
167+
var/key = params["key"]
168+
if(!(key in src.valid_keys))
169+
return
170+
// Add var with default value
171+
step["var_list"][key] = 0
172+
173+
if("modify_ref_value")
174+
var/atom/target = pick_ref(usr)
175+
if(!isatom(target))
176+
return
177+
src.target = target
178+
179+
if("add_step")
180+
src.add_animation(steps.len + 1)
181+
182+
if("delete_step")
183+
if(!step_index)
184+
return
185+
src.steps.Cut(step_index, step_index+1)
186+
187+
if("play_animation")
188+
src.play()
189+
190+
191+
/datum/animation_editor/proc/add_animation(step_index)
192+
var/new_animation = list(
193+
"name"="New Step",
194+
"var_list"=list("pixel_x"=0, "pixel_y"=0),
195+
"time"=1.0,
196+
"loop"=0,
197+
"easing"=0,
198+
"flags"=0
199+
)
200+
steps += list(new_animation)
201+
202+
203+
/datum/animation_editor/ui_interact(mob/user, datum/tgui/ui)
204+
ui = tgui_process.try_update_ui(user, src, ui)
205+
if(!ui)
206+
ui = new(user, src, "AnimationEditor")
207+
ui.open()
208+
209+
// Simple JSON-ish serialization for storage (returns a string)
210+
/datum/animation_editor/proc/serialize()
211+
. = null
212+
// var/out = "[\n"
213+
// var/i = 1
214+
// while(steps && i <= steps.len)
215+
// var/datum/animation_step/s = steps[i]
216+
// if(!s) { i++; continue }
217+
// out += "\t{"
218+
// out += "\"name\":\"" + escape_json(s.name) + "\","
219+
// out += "\"sprite\":\"" + escape_json(s.sprite) + "\","
220+
// out += "\"duration\":" + s.duration
221+
// out += ",\"offset\":" + s.offset
222+
// out += ",\"rotation\":" + s.rotation
223+
// out += ",\"scale\":" + s.scale
224+
// out += ",\"blend\":\"" + escape_json(s.blend) + "\""
225+
// out += "}"
226+
// if(i < steps.len) out += ","
227+
// out += "\n"
228+
// i++
229+
// out += "]"
230+
// return out
231+
232+
// Very small parser for the above format (expects exact keys) - returns TRUE on success
233+
/datum/animation_editor/proc/deserialize(text)
234+
if(!text) return FALSE
235+
// crude: look for objects between { }
236+
// var/list/newsteps = list()
237+
// var/pos = 1
238+
// while(TRUE)
239+
// var/a = text[pos..].find("{")
240+
// if(a == 0) break
241+
// pos += a
242+
// var/b = text[pos..].find("}")
243+
// if(b == 0) break
244+
// var/block = text[pos+1 .. pos + b - 1]
245+
// pos += b
246+
// // parse basic keys
247+
// var/name = parse_kv_string(block, "name")
248+
// var/sprite = parse_kv_string(block, "sprite")
249+
// var/duration = tofloat(parse_kv_number(block, "duration")) ? 1.0
250+
// var/offset = toint(parse_kv_number(block, "offset")) ? 0
251+
// var/rotation = toint(parse_kv_number(block, "rotation")) ? 0
252+
// var/scale = tofloat(parse_kv_number(block, "scale")) ? 1.0
253+
// var/blend = parse_kv_string(block, "blend") ? "normal"
254+
// newsteps[newsteps.len + 1] = new /datum/animation_step(name, sprite, duration, offset, rotation, scale, blend)
255+
// steps = newsteps
256+
// return TRUE
257+
258+
// Playback: calls on_play_frame(step_index, step) for each step
259+
/datum/animation_editor/proc/play()
260+
if(istype(target))
261+
var/mob/M = target
262+
M.name = M.name
263+
264+
var/is_first = TRUE
265+
for(var/step in src.steps)
266+
if(is_first)
267+
is_first = FALSE
268+
animate(src.target, time=step["time"], step["var_list"], loop=step["loop"], easing=step["easing"], flags=step["flags"])
269+
else
270+
animate(time=step["time"], step["var_list"], loop=step["loop"], easing=step["easing"], flags=step["flags"])
271+
272+
273+
274+
// Helpers
275+
/datum/animation_editor/proc/escape_json(str)
276+
if(!str) return ""
277+
// var/new = str
278+
// new = new.replace("\"", "\\\"")
279+
// new = new.replace("\n", "\\n")
280+
// return new
281+
282+
283+
/datum/animation_editor/proc/unescape_json(str)
284+
if(!str) return ""
285+
// var/out = str.replace("\\\"", "\"")
286+
// out = out.replace("\\n", "\n")
287+
// return out

goonstation.dme

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,7 @@
805805
#include "code\modules\ambient_lighting\ambient_lighting_controller.dm"
806806
#include "code\modules\ambient_lighting\ambient_lighting_screen_parent.dm"
807807
#include "code\modules\ambient_lighting\ambient_lighting_viewer.dm"
808+
#include "code\modules\animation\animation_editor.dm"
808809
#include "code\modules\animation\AnimationLibrary.dm"
809810
#include "code\modules\antagonists\__antagonist.dm"
810811
#include "code\modules\antagonists\__generic_antagonist.dm"

0 commit comments

Comments
 (0)