enum KEY_TYPE { normal, adder } enum CURVE_TYPE { linear, bezier, cut } enum DRIVER_TYPE { none, linear, wiggle, sine } function valueKey(_time, _value, _anim = noone, _in = 0, _ot = 0) constructor { #region ---- main ---- time = _time; ratio = time / (TOTAL_FRAMES - 1); value = _value; anim = _anim; ease_y_lock = true; ease_in = is_array(_in)? _in : [_in, 1]; ease_out = is_array(_ot)? _ot : [_ot, 0]; var _int = anim? anim.prop.key_inter : CURVE_TYPE.linear; ease_in_type = _int; ease_out_type = _int; dopesheet_x = 0; drivers = { seed : irandom_range(100000, 999999), type : DRIVER_TYPE.none, speed : 1, octave : 2, frequency : 4, amplitude : 1, axis_sync : false, phase : 0, }; #endregion static setTime = function(time) { self.time = time; ratio = time / (TOTAL_FRAMES - 1); } static clone = function(target = noone) { var key = new valueKey(time, value, target); key.ease_in = ease_in; key.ease_out = ease_out; key.ease_in_type = ease_in_type; key.ease_out_type = ease_out_type; return key; } static cloneAnimator = function(shift = 0, anim = noone, removeDup = true) { if(anim != noone) { //check value compat between animator if(value_bit(self.anim.prop.type) & value_bit(anim.prop.type) == 0) { noti_warning("Type incompatible"); return noone; } if(typeArray(self.anim.prop) != typeArray(anim.prop)) { noti_warning("Type incompatible"); return noone; } } if(anim == noone) anim = self.anim; var key = new valueKey(time + shift, value, anim); key.ease_in = ease_in; key.ease_out = ease_out; key.ease_in_type = ease_in_type; key.ease_out_type = ease_out_type; array_push(anim.values, key); anim.setKeyTime(key, time + shift, removeDup); return key; } static getDrawIndex = function() { if(anim.prop.type == VALUE_TYPE.trigger) return 1; if(drivers.type) return 2; if(ease_in_type == CURVE_TYPE.cut) return 1; return 0; } static toString = function() { return $"[Keyframe] {time}: {value}"; } } function valueAnimator(_val, _prop, _sep_axis = false) constructor { #region ---- main ---- suffix = ""; values = []; //staticValue = 0; length = 1; sep_axis = _sep_axis; index = 0; prop = _prop; y = 0; key_map = array_create(TOTAL_FRAMES); key_map_mode = KEYFRAME_END.hold; animate_frames = []; if(_prop.type != VALUE_TYPE.trigger) array_push(values, new valueKey(0, _val, self)); #endregion static refreshAnimation = function() { animate_frames = array_verify(animate_frames, TOTAL_FRAMES); var _anim = false; var _fr = noone; for( var i = 0, n = array_length(values); i < n; i++ ) { var _key = values[i]; if(_fr == noone) array_fill(animate_frames, 0, _key.time, 0); else { var frInd = array_equals(_fr.ease_out, [0, 0]) && array_equals(_fr.ease_in, [0, 1]) && isEqual(_fr.value, _key.value); array_fill(animate_frames, _fr.time, _key.time, !frInd); // if(frInd) array_fill(animate_frames, _fr.time, _key.time, 0); // else array_fill(animate_frames, _fr.time, _key.time, 1); } _fr = _key; } if(_fr) array_fill(animate_frames, _fr.time, TOTAL_FRAMES, 0); } static updateKeyMap = function() { length = array_length(values); if(!prop.is_anim && !LOADING && !APPENDING) return; if(array_empty(values)) { array_resize(key_map, TOTAL_FRAMES); return; } var _len = max(TOTAL_FRAMES, values[array_length(values) - 1].time); key_map_mode = prop.on_end; if(array_length(key_map) != _len) array_resize(key_map, _len); if(prop.type == VALUE_TYPE.trigger) { array_fill(key_map, 0, _len, 0); for( var i = 0, n = array_length(values); i < n; i++ ) key_map[values[i].time] = true; return; } if(array_length(values) < 2) { array_fill(key_map, 0, _len, 0); return; } var _firstKey = values[0].time; array_fill(key_map, 0, _firstKey, -1); var _keyIndex = _firstKey; for( var i = 1, n = array_length(values); i < n; i++ ) { var _k1 = values[i].time; array_fill(key_map, _keyIndex, _k1, i - 1); _keyIndex = _k1; } array_fill(key_map, _keyIndex, _len, 999_999); } static interpolate = function(from, to, rat) { if(prop.type == VALUE_TYPE.boolean) return 0; if(to.ease_in_type == CURVE_TYPE.linear && from.ease_out_type == CURVE_TYPE.linear) return rat; if(to.ease_in_type == CURVE_TYPE.cut) return 0; if(from.ease_out_type == CURVE_TYPE.cut) return 1; if(rat == 0 || rat == 1) return rat; var eox = clamp(from.ease_out[0], 0, 0.9); var eix = clamp(to.ease_in[0], 0, 0.9); var eoy = from.ease_out[1]; var eiy = to.ease_in[1]; var bz = [0, eox, eoy, 1. - eix, eiy, 1]; return eval_curve_segment_x(bz, rat); } static lerpValue = function(from, to, _lrp) { var _f = from.value; var _t = to.value; if(is_struct(_f)) { if(!struct_has(_f, "lerpTo")) return _f; return _f.lerpTo(_t, _lrp); } if(prop.display_type == VALUE_DISPLAY.d3quarternion && prop.display_data.angle_display == QUARTERNION_DISPLAY.quarterion) return quarternionArraySlerp(_f, _t, _lrp); if(prop.type == VALUE_TYPE.color) { if(is_array(_f) && is_array(_t)) { var _len = ceil(lerp(array_length(_f), array_length(_t), _lrp)); var res = array_create(_len); for( var i = 0; i < _len; i++ ) { var rat = i / (_len - 1); var rf = rat * (array_length(_f) - 1); var rt = rat * (array_length(_t) - 1); var cf = array_get_decimal(_f, rf, true); var ct = array_get_decimal(_t, rt, true); res[i] = merge_color(cf, ct, _lrp); } return res; } return processType(merge_color(_f, _t, _lrp)); } if(is_array(_f) || is_array(_t)) { var _len = max(array_safe_length(_f), array_safe_length(_t)); var _vec = array_create(_len); for(var i = 0; i < _len; i++) _vec[i] = processType( lerp( is_array(_f)? array_safe_get_fast(_f, i, 0) : _f, is_array(_t)? array_safe_get_fast(_t, i, 0) : _t, _lrp) ); return _vec; } if(prop.type == VALUE_TYPE.text) return processType(_f); return processType(lerp(_f, _t, _lrp)); } static getName = function() { return prop.name + suffix; } static getValue = function(_time = CURRENT_FRAME) { //if(!prop.is_anim) return staticValue; length = array_length(values); ///////////////////////////////////////////////////////////// TRIGGER TYPE ///////////////////////////////////////////////////////////// if(prop.type == VALUE_TYPE.trigger) { if(length == 0 || !prop.is_anim) return false; if(array_length(key_map) != TOTAL_FRAMES) updateKeyMap(); return key_map[_time]; } ///////////////////////////////////////////////////////////// OPTIMIZATION ///////////////////////////////////////////////////////////// if(length == 0) return processTypeDefault(); if(length == 1) { var _key = values[0]; if(_key.drivers.type && _time >= _key.time) return processType(processDriver(_time, _key)); return processType(_key.value); } if(prop.type == VALUE_TYPE.path) return processType(values[0].value); if(!prop.is_anim) return processType(values[0].value); var _len = max(TOTAL_FRAMES, values[length - 1].time); if(array_length(key_map) != _len) updateKeyMap(); var _time_first = prop.loop_range == -1? values[0].time : values[length - 1 - prop.loop_range].time; var _time_last = values[length - 1].time; var _time_dura = _time_last - _time_first; ////////////////////////////////////////////////////////////// LOOP TIME /////////////////////////////////////////////////////////////// if(_time > _time_last) { switch(prop.on_end) { case KEYFRAME_END.loop : _time = _time_first + safe_mod(_time - _time_last, _time_dura + 1); break; case KEYFRAME_END.ping : var time_in_loop = safe_mod(_time - _time_first, _time_dura * 2); if(time_in_loop < _time_dura) _time = _time_first + time_in_loop; else _time = _time_first + _time_dura * 2 - time_in_loop; break; } } var _keyIndex; if(_time >= _len) _keyIndex = 999_999; else if(_time <= 0) _keyIndex = -1; else _keyIndex = array_safe_get_fast(key_map, _time); //////////////////////////////////////////////////////////// BEFORE FIRST ////////////////////////////////////////////////////////////// if(_keyIndex == -1) { if(prop.on_end == KEYFRAME_END.wrap) { var from = values[length - 1]; var to = values[0]; var fTime = from.time; var tTime = to.time; var prog = TOTAL_FRAMES - fTime + _time; var totl = TOTAL_FRAMES - fTime + tTime; var rat = prog / totl; var _lrp = interpolate(from, to, rat); return lerpValue(from, to, _lrp); } return processType(values[0].value); //First frame } ///////////////////////////////////////////////////////////// AFTER LAST /////////////////////////////////////////////////////////////// if(_keyIndex == 999_999) { var _lstKey = values[length - 1]; if(_lstKey.drivers.type) return processType(processDriver(_time, _lstKey)); if(prop.on_end == KEYFRAME_END.wrap) { var from = _lstKey; var to = values[0]; var prog = _time - from.time; var totl = TOTAL_FRAMES - from.time + to.time; var rat = prog / totl; var _lrp = interpolate(from, to, rat); return lerpValue(from, to, _lrp); } return processType(_lstKey.value); //Last frame } ///////////////////////////////////////////////////////////// INBETWEEN //////////////////////////////////////////////////////////////// var from = values[_keyIndex]; var to = values[_keyIndex + 1]; var rat = (_time - from.time) / (to.time - from.time); var _lrp = interpolate(from, to, rat); if(from.drivers.type) return processDriver(_time, from, lerpValue(from, to, _lrp), rat); return lerpValue(from, to, _lrp); } static processTypeDefault = function() { if(!sep_axis && typeArray(prop)) return []; return 0; } static processDriver = function(_time, _key, _val = undefined, _intp = 0) { static _processDriver = function(val, drivers, _t, _index = 0, _intp = 0) { switch(drivers.type) { case DRIVER_TYPE.linear : return val + _t * drivers.speed; case DRIVER_TYPE.wiggle : var w = perlin1D(_t, drivers.seed + _index, drivers.frequency / 10, drivers.octave, -1, 1) * drivers.amplitude; return val + w; case DRIVER_TYPE.sine : var w = sin((drivers.phase * (_index + 1) + _t * drivers.frequency / TOTAL_FRAMES) * pi * 2) * drivers.amplitude; return val + w; } return 0; } var _dt = _time - _key.time; _val = _val == undefined? _key.value : _val; var _res = _val; if(prop.type == VALUE_TYPE.integer || prop.type == VALUE_TYPE.float) { if(is_array(_val)) { _res = array_create(array_length(_val)); for( var i = 0, n = array_length(_val); i < n; i++ ) _res[i] = is_numeric(_val[i])? _processDriver(_val[i], _key.drivers, _dt, _key.drivers.axis_sync? 0 : i, _intp) : _val[i]; } else _res = _processDriver(_val, _key.drivers, _dt, 0, _intp); } return _res; } static processType = function(_val) { INLINE if(PROJECT.attributes.strict) return processValue(_val); var _res = _val; if(!sep_axis && typeArray(prop) && is_array(_val)) { for(var i = 0; i < array_length(_val); i++) _res[i] = processValue(_val[i]); } else _res = processValue(_val); return _res; } static processValue = function(_val) { INLINE if(is_array(_val)) return _val; if(is_struct(_val)) return _val; switch(prop.type) { case VALUE_TYPE.integer : return prop.unit.mode == VALUE_UNIT.constant? round(_val) : _val; case VALUE_TYPE.float : return _val; case VALUE_TYPE.text : return is_string(_val)? _val : string_real(_val); case VALUE_TYPE.color : return is_real(_val)? cola(_val) : _val; case VALUE_TYPE.surface : return is_string(_val)? get_asset(_val) : _val; } return _val; } static insertKey = function(_key, _index) { array_push(values, _index, _key); } function onUndo() { updateKeyMap(); prop.triggerSetFrom(); } static setKeyTime = function(_key, _time, _replace = true, record = false) { if(!array_exists(values, _key)) return 0; if(_key.time == _time && !_replace) return 0; if(!LOADING) PROJECT.modified = true; var _prevTime = _key.time; _time = max(_time, 0); _key.setTime(_time); array_remove(values, _key); if(_replace) for( var i = 0; i < array_length(values); i++ ) { if(values[i].time != _time) continue; if(record) { var act = new Action(ACTION_TYPE.custom, function(data) { if(data.undo) insertKey(data.overKey, data.index); updateKeyMap(); data.undo = !data.undo; }, { overKey : values[i], index : i, undo : true, tooltip : $"Set {prop.name} value" }); mergeAction(act); } values[i] = _key; updateKeyMap(); return 2; } for( var i = 0; i < array_length(values); i++ ) { //insert key before the last key if(values[i].time < _time) continue; if(record) recordAction(ACTION_TYPE.custom, function(data) { var _prevTime = data.key.time; setKeyTime(data.key, data.time, false); data.time = _prevTime; }, { key : _key, time : _prevTime, tooltip : $"Set {prop.name} value" }, onUndo); array_insert(values, i, _key); if(_replace) updateKeyMap(); return 1; } if(record) recordAction(ACTION_TYPE.custom, function(data) { // insert key after the last key var _prevTime = data.key.time; setKeyTime(data.key, data.time, false); data.time = _prevTime; }, { key : _key, time : _prevTime, tooltip : $"Set {prop.name} value" }, onUndo); array_push(values, _key); if(_replace) updateKeyMap(); return 1; } static setValue = function(_val = 0, _record = true, _time = CURRENT_FRAME, ease_in = 0, ease_out = 0) { //staticValue = _val; if(prop.type == VALUE_TYPE.trigger) { if(!prop.is_anim) { values[0] = new valueKey(0, _val, self); updateKeyMap(); return true; } for(var i = 0; i < array_length(values); i++) { //Find trigger var _key = values[i]; if(_key.time == _time) { if(!global.FLAG.keyframe_override) return false; _key.value = _val; return false; } else if(_key.time > _time) { array_insert(values, i, new valueKey(_time, _val, self)); updateKeyMap(); return true; } } //print($"{_time}: {_val} | Insert last"); array_push(values, new valueKey(_time, _val, self)); updateKeyMap(); return true; } if(!prop.is_anim) { if(isEqual(values[0].value, _val)) return false; if(_record) recordAction_variable_change(values[0], "value", values[0].value, prop.name, onUndo); values[0].value = _val; return true; } if(array_length(values) == 0) { // Should not be called normally var k = new valueKey(_time, _val, self, ease_in, ease_out); array_push(values, k); if(_record) recordAction(ACTION_TYPE.array_insert, values, [ k, array_length(values) - 1, $"add {prop.name} keyframe" ], onUndo); return true; } for(var i = 0; i < array_length(values); i++) { var _key = values[i]; if(_key.time == _time) { if(!global.FLAG.keyframe_override) return false; if(_key.value != _val) { if(_record) recordAction_variable_change(_key, "value", _key.value, prop.name, onUndo); _key.value = _val; return true; } return false; } else if(_key.time > _time) { var k = new valueKey(_time, _val, self, ease_in, ease_out); array_insert(values, i, k); if(_record) recordAction(ACTION_TYPE.array_insert, values, [k, i, $"add {prop.name} keyframe" ], onUndo); updateKeyMap(); return true; } } var k = new valueKey(_time, _val, self, ease_in, ease_out); if(_record) recordAction(ACTION_TYPE.array_insert, values, [ k, array_length(values), $"add {prop.name} keyframe" ], onUndo); array_push(values, k); updateKeyMap(); return true; } static removeKey = function(key) { if(array_length(values) > 1) array_remove(values, key); else prop.is_anim = false; updateKeyMap(); } static serialize = function(scale = false) { var _data = []; for(var i = 0; i < array_length(values); i++) { var _value_list = []; _value_list[0] = scale? values[i].time / (TOTAL_FRAMES - 1) : values[i].time; var val = values[i].value; if(prop.type == VALUE_TYPE.struct) { val = json_stringify(val); } else if(is_struct(val) && struct_has(val, "serialize")) { val = val.serialize(); } else if(!sep_axis && typeArray(prop) && is_array(val)) { var __v = []; for(var j = 0; j < array_length(val); j++) { if(is_struct(val[j]) && struct_has(val[j], "serialize")) array_push(__v, val[j].serialize()); else array_push(__v, val[j]); } val = __v; } _value_list[1] = val; _value_list[2] = values[i].ease_in; _value_list[3] = values[i].ease_out; _value_list[4] = values[i].ease_in_type; _value_list[5] = values[i].ease_out_type; _value_list[6] = values[i].ease_y_lock; _value_list[7] = values[i].drivers; array_push(_data, _value_list); } return _data; } static deserialize = function(_data, scale = false) { values = []; if(prop.type == VALUE_TYPE.gradient && LOADING_VERSION < 1340 && !CLONING) { //backward compat: Gradient var _val = []; var value = _data[0][1]; if(is_array(value)) for(var i = 0; i < array_length(value); i++) { var _keyframe = value[i]; var _t = struct_try_get(_keyframe, "time"); var _v = struct_try_get(_keyframe, "value"); array_push(_val, new gradientKey(_t, _v)); } var grad = new gradientObject(); grad.keys = _val; array_push(values, new valueKey(0, grad, self)); updateKeyMap(); return; } var base = prop.def_val; for(var i = 0; i < array_length(_data); i++) { var _keyframe = _data[i]; var _time = array_safe_get_fast(_keyframe, 0); if(scale && _time <= 1) _time = round(_time * (TOTAL_FRAMES - 1)); var value = array_safe_get_fast(_keyframe, 1); var ease_in = array_safe_get_fast(_keyframe, 2); var ease_out = array_safe_get_fast(_keyframe, 3); var ease_in_type = array_safe_get_fast(_keyframe, 4); var ease_out_type = array_safe_get_fast(_keyframe, 5); var ease_y_lock = array_safe_get_fast(_keyframe, 6, true); var driver = array_safe_get_fast(_keyframe, 7, {}); var _val = value; if(prop.type == VALUE_TYPE.struct) _val = json_try_parse(value); else if(prop.type == VALUE_TYPE.path && prop.display_type == VALUE_DISPLAY.path_array) { for(var j = 0; j < array_length(value); j++) _val[j] = value[j]; } else if(prop.type == VALUE_TYPE.gradient) { var grad = new gradientObject(); _val = grad.deserialize(value); } else if(prop.type == VALUE_TYPE.d3Material) { var mat = new __d3dMaterial(); _val = mat.deserialize(value); } else if(prop.type == VALUE_TYPE.color) { if(is_array(_val)) { for( var i = 0, n = array_length(_val); i < n; i++ ) _val[i] = LOADING_VERSION < 11640 && !is_int64(_val[i])? cola(_val[i]) : int64(_val[i]); } else _val = LOADING_VERSION < 11640 && !is_int64(_val)? cola(_val) : int64(_val); } else if(!sep_axis && typeArray(prop)) { _val = []; if(is_array(value)) { for(var j = 0; j < array_length(value); j++) _val[j] = processValue(value[j]); } else if(is_array(base)) { for(var j = 0; j < array_length(base); j++) _val[j] = processValue(value); } if(prop.type == VALUE_TYPE.curve && array_length(value) % 6 == 0) { array_insert(_val, 0, 0); array_insert(_val, 1, 1); } } //print($"Deserialize {prop.node.name}:{prop.name} = {_val} "); var vk = new valueKey(_time, _val, self, ease_in, ease_out); vk.ease_in_type = ease_in_type; vk.ease_out_type = ease_out_type; vk.ease_y_lock = ease_y_lock; struct_override(vk.drivers, driver); array_push(values, vk); } updateKeyMap(); } static cleanUp = function() {} }