|
| 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 |
0 commit comments