diff --git a/Data/Scripts/902_Anim GameData/001_Animation.rb b/Data/Scripts/902_Anim GameData/001_Animation.rb index c29f8790a..eb4a28b83 100644 --- a/Data/Scripts/902_Anim GameData/001_Animation.rb +++ b/Data/Scripts/902_Anim GameData/001_Animation.rb @@ -41,8 +41,8 @@ module GameData "None" => :none, "Linear" => :linear, "EaseIn" => :ease_in, - "EaseOut" => :ease_out, - "EaseBoth" => :ease_both + "EaseBoth" => :ease_both, + "EaseOut" => :ease_out } USER_AND_TARGET_SEPARATION = [200, -200, -100] # x, y, z (from user to target) @@ -66,7 +66,7 @@ module GameData # change during the animation. # TODO: If more "SetXYZ"/"MoveXYZ" properties are added, ensure the "SetXYZ" # ones are given a duration of 0 in def validate_compiled_animation. - # Also add display names to def property_display_name. + # Also add display names to def self.property_display_name. SUB_SCHEMA = { # These properties cannot be changed partway through the animation. # NOTE: "Name" isn't a property here, because the particle's name comes @@ -162,6 +162,30 @@ module GameData :target_cry => nil } + def self.property_display_name(property) + return { + :frame => _INTL("Frame"), + :blending => _INTL("Blending"), + :flip => _INTL("Flip"), + :x => _INTL("X"), + :y => _INTL("Y"), + :z => _INTL("Priority"), + :zoom_x => _INTL("Zoom X"), + :zoom_y => _INTL("Zoom Y"), + :angle => _INTL("Angle"), + :visible => _INTL("Visible"), + :opacity => _INTL("Opacity") + }[property] || property.capitalize + end + + def self.property_can_interpolate?(property) + return false if !property + SUB_SCHEMA.each_value do |prop| + return true if prop[0] == property && prop[5] && prop[5] == INTERPOLATION_TYPES + end + return false + end + @@cmd_to_pbs_name = nil # Used for writing animation PBS files extend ClassMethodsIDNumbers diff --git a/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb b/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb index f75bb3f92..8300a4ffb 100644 --- a/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb +++ b/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb @@ -296,6 +296,9 @@ class AnimationEditor # Move particle up/down the list? end + # TODO: :keyframe_pane is currently inaccessible (intentionally). If it will + # have its own commands and should be accessible again, change def + # on_mouse_release in ParticleList. def set_keyframe_pane_contents keyframe_pane = @components[:keyframe_pane] keyframe_pane.add_header_label(:header, _INTL("Edit keyframe")) @@ -810,6 +813,24 @@ class AnimationEditor @components[:particle_list].set_particles(@anim[:particles]) @components[:particle_list].particle_index = idx2 refresh + when :cycle_interpolation + # value is [particle index, property, keyframe] + # Get current interpolation type + interp_type = nil + @anim[:particles][value[0]][value[1]].each do |cmd| + next if cmd[0] != value[2] + interp_type = cmd[3] if !interp_type + end + interp_type ||= :none + # Get the interpolation type to change to + interps = GameData::Animation::INTERPOLATION_TYPES.values + idx = (interps.index(interp_type) + 1) % interps.length + interp_type = interps[idx] + # Set the new interpolation type + AnimationEditor::ParticleDataHelper.set_interpolation(@anim[:particles][value[0]], value[1], value[2], interp_type) + @components[:particle_list].change_particle_commands(value[0]) + refresh_component(:commands_pane) + refresh_component(:canvas) end when :animation_properties # TODO: Will changes here need to refresh any other components (e.g. side diff --git a/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb b/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb index df84037e1..b782057ea 100644 --- a/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb +++ b/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb @@ -16,7 +16,7 @@ module AnimationEditor::ParticleDataHelper return ret end - def get_keyframe_particle_value(particle, frame, property) + def get_keyframe_particle_value(particle, property, frame) if !GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.include?(property) raise _INTL("Couldn't get default value for property {1} for particle {2}.", property, particle[:name]) @@ -81,7 +81,7 @@ module AnimationEditor::ParticleDataHelper def get_all_keyframe_particle_values(particle, frame) ret = {} GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.each_pair do |prop, default| - ret[prop] = get_keyframe_particle_value(particle, frame, prop) + ret[prop] = get_keyframe_particle_value(particle, prop, frame) end return ret end @@ -151,7 +151,7 @@ module AnimationEditor::ParticleDataHelper # 0 - SetXYZ # [+/- duration, interpolation type] --- MoveXYZ (duration's sign is whether # it makes the value higher or lower) - def get_particle_property_commands_timeline(particle, commands, property) + def get_particle_property_commands_timeline(particle, property, commands) return nil if !commands || commands.length == 0 if particle[:name] == "SE" ret = [] @@ -188,6 +188,93 @@ module AnimationEditor::ParticleDataHelper end def add_command(particle, property, frame, value) + # Return a new set of commands if there isn't one + if !particle[property] || particle[property].empty? + return [[frame, 0, value]] + end + # Find all relevant commands + set_now = nil + move_ending_now = nil + move_overlapping_now = nil + particle[property].each do |cmd| + if cmd[1] == 0 + set_now = cmd if cmd[0] == frame + else + move_ending_now = cmd if cmd[0] + cmd[1] == frame + move_overlapping_now = cmd if cmd[0] < frame && cmd[0] + cmd[1] > frame + end + end + new_command_needed = true + # Replace existing command at frame if it has a duration of 0 + if set_now + set_now[2] = value + new_command_needed = false + end + # If a command has a duration >0 and ends at frame, replace its value + if move_ending_now + move_ending_now[2] = value + new_command_needed = false + end + return particle[property] if !new_command_needed + # Add a new command + new_cmd = [frame, 0, value] + particle[property].push(new_cmd) + # If the new command interrupts an interpolation, split that interpolation + if move_overlapping_now + end_frame = move_overlapping_now[0] + move_overlapping_now[1] + new_cmd[1] = end_frame - frame # Duration + new_cmd[2] = move_overlapping_now[2] # Value + new_cmd[3] = move_overlapping_now[3] # Interpolation type + move_overlapping_now[1] = frame - move_overlapping_now[0] # Duration + move_overlapping_now[2] = value # Value + end + # Sort and return the commands + particle[property].sort! { |a, b| a[0] == b[0] ? a[1] == b[1] ? 0 : a[1] <=> b[1] : a[0] <=> b[0] } + return particle[property] + end + + # Cases: + # * SetXYZ - delete it + # * MoveXYZ start - turn into a SetXYZ at the end point + # * MoveXYZ end - delete it (this may happen to remove the start diamond too) + # * MoveXYZ end and start - merge both together (use first's type) + # * SetXYZ and MoveXYZ start - delete SetXYZ (leave MoveXYZ alone) + # * SetXYZ and MoveXYZ end - (unlikely) delete both + # * SetXYZ and MoveXYZ start and end - (unlikely) delete SetXYZ, merge Moves together + def delete_command(particle, property, frame) + # Find all relevant commands + set_now = nil + move_ending_now = nil + move_starting_now = nil + particle[property].each do |cmd| + if cmd[1] == 0 + set_now = cmd if cmd[0] == frame + else + move_starting_now = cmd if cmd[0] == frame + move_ending_now = cmd if cmd[0] + cmd[1] == frame + end + end + # Delete SetXYZ if it is at frame + particle[property].delete(set_now) if set_now + # Edit/delete MoveXYZ commands starting/ending at frame + if move_ending_now && move_starting_now # Merge both MoveXYZ commands + move_ending_now[1] += move_starting_now[1] # Duration + move_ending_now[2] = move_starting_now[2] # Value + particle[property].delete(move_starting_now) + elsif move_ending_now # Delete MoveXYZ ending now + particle[property].delete(move_ending_now) + elsif move_starting_now && !set_now # Turn into SetXYZ at its end point + move_starting_now[0] += move_starting_now[1] + move_starting_now[1] = 0 + move_starting_now[3] = nil + move_starting_now.compact! + end + return (particle[property].empty?) ? nil : particle[property] + end + + # Removes commands for the particle's given property if they don't make a + # difference. Returns the resulting set of commands. + def optimize_commands(particle, property) # Split particle[property] into values and interpolation arrays set_points = [] # All SetXYZ commands (the values thereof) end_points = [] # End points of MoveXYZ commands (the values thereof) @@ -202,14 +289,6 @@ module AnimationEditor::ParticleDataHelper end end end - # Add new command to points (may replace an existing command) - interp = :none - (frame + 1).times do |i| - interp = :none if set_points[i] || end_points[i] - interp = interps[i] if interps[i] - end - interps[frame] = interp if interp != :none - set_points[frame] = value # For visibility only, set the keyframe with the first command (of any kind) # to be visible, unless the command being added overwrites it. Also figure # out the first keyframe that has a command, and the first keyframe that has @@ -264,42 +343,60 @@ module AnimationEditor::ParticleDataHelper return (ret.empty?) ? nil : ret end - # Cases: - # * SetXYZ - delete it - # * MoveXYZ start - turn into a SetXYZ at the end point - # * MoveXYZ end - delete it (this may happen to remove the start diamond too) - # * MoveXYZ end and start - merge both together (use first's type) - # * SetXYZ and MoveXYZ start - delete SetXYZ (leave MoveXYZ alone) - # * SetXYZ and MoveXYZ end - (unlikely) delete both - # * SetXYZ and MoveXYZ start and end - (unlikely) delete SetXYZ, merge Moves together - def delete_command(particle, property, frame) - # Find all relevant commands + # SetXYZ at frame + # - none: Do nothing. + # - interp: Add MoveXYZ (calc duration/value at end). + # MoveXYZ at frame + # - none: Turn into two SetXYZ (MoveXYZ's value for end point, calc value + # for start point). + # - interp: Change type. + # SetXYZ and MoveXYZ at frame + # - none: Turn MoveXYZ into SetXYZ at the end point. + # - interp: Change MoveXYZ's type. + # End of earlier MoveXYZ (or nothing) at frame + # - none: Do nothing. + # - interp: Add MoveXYZ (calc duration/value at end). + def set_interpolation(particle, property, frame, type) + # Find relevant command set_now = nil - move_ending_now = nil move_starting_now = nil particle[property].each do |cmd| - if cmd[1] == 0 - set_now = cmd if cmd[0] == frame + next if cmd[0] != frame + set_now = cmd if cmd[1] == 0 + move_starting_now = cmd if cmd[1] != 0 + end + if move_starting_now + # If a MoveXYZ command exists at frame, amend it + if type == :none + old_end_point = move_starting_now[0] + move_starting_now[1] + old_value = move_starting_now[2] + # Turn the MoveXYZ command into a SetXYZ (or just delete it if a SetXYZ + # already exists at frame) + if set_now + particle[property].delete(move_starting_now) + else + move_starting_now[1] = 0 + move_starting_now[2] = get_keyframe_particle_value(particle, property, frame)[0] + move_starting_now[3] = nil + move_starting_now.compact! + end + # Add a new SetXYZ at the end of the (former) interpolation + add_command(particle, property, old_end_point, old_value) else - move_starting_now = cmd if cmd[0] == frame - move_ending_now = cmd if cmd[0] + cmd[1] == frame + # Simply change the type + move_starting_now[3] = type + end + elsif type != :none + # If no MoveXYZ command exists at frame, make one (if type isn't :none) + particle[property].each do |cmd| # Assumes commands are sorted by keyframe + next if cmd[0] <= frame + val_at_end = get_keyframe_particle_value(particle, property, cmd[0])[0] + particle[property].push([frame, cmd[0] - frame, val_at_end, type]) + particle[property].sort! { |a, b| a[0] == b[0] ? a[1] == b[1] ? 0 : a[1] <=> b[1] : a[0] <=> b[0] } + break end end - # Delete SetXYZ if it is at frame - particle[property].delete(set_now) if set_now - # Edit/delete MoveXYZ commands starting/ending at frame - if move_ending_now && move_starting_now # Merge both MoveXYZ commands - move_ending_now[1] += move_starting_now[1] - particle[property].delete(move_starting_now) - elsif move_ending_now # Delete MoveXYZ ending now - particle[property].delete(move_ending_now) - elsif move_starting_now && !set_now # Turn into SetXYZ at its end point - move_starting_now[0] += move_starting_now[1] - move_starting_now[1] = 0 - move_starting_now[3] = nil - move_starting_now.compact! - end - return (particle[property].empty?) ? nil : particle[property] + return particle[property] end #----------------------------------------------------------------------------- diff --git a/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb b/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb index f8aebb6e8..280dcb4a1 100644 --- a/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb +++ b/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb @@ -8,10 +8,15 @@ class AnimationEditor::ParticleList < UIControls::BaseControl LIST_X = 0 LIST_Y = TIMELINE_HEIGHT + VIEWPORT_SPACING LIST_WIDTH = 180 - VIEWPORT_SPACING + EXPAND_BUTTON_X = VIEWPORT_SPACING + EXPAND_BUTTON_WIDTH = 19 + LIST_BOX_X = EXPAND_BUTTON_X + EXPAND_BUTTON_WIDTH + VIEWPORT_SPACING + LIST_INDENT = 8 COMMANDS_X = LIST_WIDTH + VIEWPORT_SPACING COMMANDS_Y = LIST_Y ROW_HEIGHT = 24 + ROW_SPACING = 1 # Gap at top of each row DIAMOND_SIZE = 3 TIMELINE_LEFT_BUFFER = DIAMOND_SIZE + 1 # Allows diamonds at keyframe 0 to be drawn fully TIMELINE_TEXT_SIZE = 16 @@ -19,6 +24,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl INTERP_LINE_HEIGHT = KEYFRAME_SPACING - ((DIAMOND_SIZE * 2) + 3) INTERP_LINE_Y = (ROW_HEIGHT / 2) - (INTERP_LINE_HEIGHT / 2) DURATION_BUFFER = 20 # Extra keyframes shown after the animation's end + PROPERTY_BG_COLOR = Color.new(224, 224, 224) CONTROL_BG_COLORS = { :foreground => Color.new(128, 160, 248), # Blue :midground => Color.new(128, 160, 248), # Blue @@ -32,7 +38,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl :target_side_background => Color.new(128, 248, 248) # Cyan } SE_CONTROL_BG_COLOR = Color.gray - TIME_AFTER_ANIMATION_COLOR = Color.new(224, 224, 224) + TIME_AFTER_ANIMATION_COLOR = Color.new(160, 160, 160) attr_reader :keyframe # The selected keyframe attr_reader :values @@ -312,10 +318,14 @@ class AnimationEditor::ParticleList < UIControls::BaseControl end def busy? - return true if @controls.any? { |c| c[1].busy? } + return true if controls_busy? return super end + def controls_busy? + return @controls.any? { |c| c[1].busy? } + end + def changed? return @changed end @@ -366,7 +376,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl overall_commands = [] @particles[index].each_pair do |property, value| next if !value.is_a?(Array) - cmds = AnimationEditor::ParticleDataHelper.get_particle_property_commands_timeline(@particles[index], value, property) + cmds = AnimationEditor::ParticleDataHelper.get_particle_property_commands_timeline(@particles[index], property, value) @commands[[index, property]] = cmds cmds.each_with_index do |cmd, i| next if !cmd @@ -468,22 +478,6 @@ class AnimationEditor::ParticleList < UIControls::BaseControl #----------------------------------------------------------------------------- - def property_display_name(property) - return { - :frame => _INTL("Frame"), - :blending => _INTL("Blending"), - :flip => _INTL("Flip"), - :x => _INTL("X"), - :y => _INTL("Y"), - :z => _INTL("Priority"), - :zoom_x => _INTL("Zoom X"), - :zoom_y => _INTL("Zoom Y"), - :angle => _INTL("Angle"), - :visible => _INTL("Visible"), - :opacity => _INTL("Opacity") - }[property] || property.capitalize - end - def repaint @list_scrollbar.repaint if @list_scrollbar.invalid? @time_scrollbar.repaint if @time_scrollbar.invalid? @@ -503,21 +497,23 @@ class AnimationEditor::ParticleList < UIControls::BaseControl @time_bg_sprite.bitmap.fill_rect(draw_x, TIMELINE_HEIGHT, greyed_width, VIEWPORT_SPACING, Color.black) end # Draw hover highlight - hover_color = nil - if @captured_keyframe && !@captured_row - if @hover_keyframe && @hover_keyframe == @captured_keyframe && !@hover_row + if !controls_busy? + hover_color = nil + if @captured_keyframe && !@captured_row + if @hover_keyframe && @hover_keyframe == @captured_keyframe && !@hover_row + hover_color = HOVER_COLOR + else + hover_color = CAPTURE_COLOR + end + draw_x = TIMELINE_LEFT_BUFFER + (@captured_keyframe * KEYFRAME_SPACING) - @left_pos + @timeline_sprite.bitmap.fill_rect(draw_x - (KEYFRAME_SPACING / 2), 0, + KEYFRAME_SPACING, TIMELINE_HEIGHT - 1, hover_color) + elsif !@captured_keyframe && !@captured_row && @hover_keyframe && !@hover_row hover_color = HOVER_COLOR - else - hover_color = CAPTURE_COLOR + draw_x = TIMELINE_LEFT_BUFFER + (@hover_keyframe * KEYFRAME_SPACING) - @left_pos + @timeline_sprite.bitmap.fill_rect(draw_x - (KEYFRAME_SPACING / 2), 0, + KEYFRAME_SPACING, TIMELINE_HEIGHT - 1, hover_color) end - draw_x = TIMELINE_LEFT_BUFFER + (@captured_keyframe * KEYFRAME_SPACING) - @left_pos - @timeline_sprite.bitmap.fill_rect(draw_x - (KEYFRAME_SPACING / 2), 0, - KEYFRAME_SPACING, TIMELINE_HEIGHT - 1, hover_color) - elsif !@captured_keyframe && !@captured_row && @hover_keyframe && !@hover_row - hover_color = HOVER_COLOR - draw_x = TIMELINE_LEFT_BUFFER + (@hover_keyframe * KEYFRAME_SPACING) - @left_pos - @timeline_sprite.bitmap.fill_rect(draw_x - (KEYFRAME_SPACING / 2), 0, - KEYFRAME_SPACING, TIMELINE_HEIGHT - 1, hover_color) end # Draw timeline markings each_visible_keyframe(true) do |i| @@ -551,37 +547,81 @@ class AnimationEditor::ParticleList < UIControls::BaseControl spr = @list_sprites[index] return if !spr spr.bitmap.clear - box_x = (@particle_list[index].is_a?(Array)) ? 16 : 0 - # Get the background color - p_index = (@particle_list[index].is_a?(Array)) ? @particle_list[index][0] : @particle_list[index] + # Get useful information + is_property = @particle_list[index].is_a?(Array) + p_index = (is_property) ? @particle_list[index][0] : @particle_list[index] particle_data = @particles[p_index] + box_x = LIST_BOX_X + box_x += LIST_INDENT if is_property + # Get the background color if particle_data[:name] == "SE" bg_color = SE_CONTROL_BG_COLOR + elsif is_property + bg_color = PROPERTY_BG_COLOR else bg_color = CONTROL_BG_COLORS[@particles[p_index][:focus]] || Color.magenta end # Draw hover highlight - hover_color = nil - if @captured_row && !@captured_keyframe - if @captured_row == index - if @hover_row && @hover_row == index && !@hover_keyframe - hover_color = HOVER_COLOR - else - hover_color = CAPTURE_COLOR + if !controls_busy? && !@captured_keyframe + hover_color = nil + if @captured_row + if @captured_row == index + if !@hover_keyframe && @hover_row && @hover_row == index && + @captured_row_button && @hover_row_button == @captured_row_button + hover_color = HOVER_COLOR + else + hover_color = CAPTURE_COLOR + end + end + elsif @hover_row && @hover_row == index && !@hover_keyframe + hover_color = HOVER_COLOR + end + if hover_color + case @captured_row_button || @hover_row_button + when :expand + spr.bitmap.fill_rect(EXPAND_BUTTON_X, (ROW_HEIGHT - EXPAND_BUTTON_WIDTH + 1) / 2, + EXPAND_BUTTON_WIDTH, EXPAND_BUTTON_WIDTH, hover_color) + when :row + spr.bitmap.fill_rect(box_x, ROW_SPACING, spr.width - box_x, spr.height - ROW_SPACING, hover_color) end end - elsif !@captured_row && !@captured_keyframe && @hover_row && @hover_row == index && !@hover_keyframe - hover_color = HOVER_COLOR end - spr.bitmap.fill_rect(box_x, 1, spr.width - box_x, spr.height - 1, hover_color) if hover_color # Draw outline - spr.bitmap.outline_rect(box_x, 1, spr.width - box_x, spr.height - 1, bg_color, 2) + spr.bitmap.outline_rect(box_x, ROW_SPACING, spr.width - box_x, spr.height - ROW_SPACING, bg_color, 2) # Draw text - if @particle_list[index].is_a?(Array) - draw_text(spr.bitmap, box_x + 4, 0, "→") # ► - draw_text(spr.bitmap, box_x + 4 + 17, 3, property_display_name(@particle_list[index][1])) + if is_property + draw_text(spr.bitmap, box_x + 4, 3, GameData::Animation.property_display_name(@particle_list[index][1]) + ":") else - draw_text(spr.bitmap, 4, 3, @particles[p_index][:name] || "Unnamed") + draw_text(spr.bitmap, box_x + 4, 3, @particles[p_index][:name] || "Unnamed") + end + # Draw expand/collapse arrow or dotted lines + if is_property + 6.times do |j| + spr.bitmap.fill_rect(10, j * 2, 1, 1, Color.black) + end + 9.times do |i| + spr.bitmap.fill_rect(10 + (i * 2), 12, 1, 1, Color.black) + end + elsif @expanded_particles.include?(p_index) + 11.times do |i| + j = (i == 0 || i == 10) ? 1 : 0 + h = [2, 4, 5, 6, 7, 8, 7, 6, 5, 4, 2][i] + h = ((i > 5) ? 10 - i : i) + 3 - j + spr.bitmap.fill_rect(5 + i, 9 + j, 1, h, Color.black) + end + elsif particle_data[:name] != "SE" + 11.times do |j| + i = (j == 0 || j == 10) ? 1 : 0 + w = [2, 4, 5, 6, 7, 8, 7, 6, 5, 4, 2][j] + w = ((j > 5) ? 10 - j : j) + 3 - i + spr.bitmap.fill_rect(7 + i, 7 + j, w, 1, Color.black) + end + end + # Draw dotted line leading to the next property line + if @particle_list[index + 1]&.is_a?(Array) + 5.times do |j| + spr.bitmap.fill_rect(10, 14 + (j * 2), 1, 1, Color.black) + end end end @@ -589,11 +629,14 @@ class AnimationEditor::ParticleList < UIControls::BaseControl bg_spr = @commands_bg_sprites[index] return if !bg_spr bg_spr.bitmap.clear - p_index = (@particle_list[index].is_a?(Array)) ? @particle_list[index][0] : @particle_list[index] + is_property = @particle_list[index].is_a?(Array) + p_index = (is_property) ? @particle_list[index][0] : @particle_list[index] particle_data = @particles[p_index] # Get the background color if particle_data[:name] == "SE" bg_color = SE_CONTROL_BG_COLOR + elsif is_property + bg_color = PROPERTY_BG_COLOR else bg_color = CONTROL_BG_COLORS[@particles[p_index][:focus]] || Color.magenta end @@ -604,33 +647,53 @@ class AnimationEditor::ParticleList < UIControls::BaseControl draw_x = TIMELINE_LEFT_BUFFER + (i * KEYFRAME_SPACING) - @left_pos # Draw bg if i < @duration - DURATION_BUFFER && visible_cmds[i] - bg_spr.bitmap.fill_rect(draw_x, 1, KEYFRAME_SPACING, ROW_HEIGHT - 2, bg_color) + bg_spr.bitmap.fill_rect(draw_x, ROW_SPACING, KEYFRAME_SPACING, ROW_HEIGHT - ROW_SPACING, bg_color) end # Draw hover highlight hover_color = nil - if @captured_row && @captured_keyframe - if @captured_row == index && @captured_keyframe == i - if @hover_row && @hover_row == index && @hover_keyframe && @hover_keyframe == i - hover_color = HOVER_COLOR - else - hover_color = CAPTURE_COLOR - end + if !controls_busy? + earlier_captured_keyframe = @captured_keyframe + later_captured_keyframe = (earlier_captured_keyframe || -1) + 1 + earlier_hovered_keyframe = @hover_keyframe + later_hovered_keyframe = (earlier_hovered_keyframe || -1) + 1 + if is_property + later_captured_keyframe = @captured_row_button || 0 + later_hovered_keyframe = @hover_row_button || 0 + end + if @captured_row && @captured_keyframe + if @captured_row == index && i >= earlier_captured_keyframe && i < later_captured_keyframe + if @hover_row && @hover_row == index && @hover_keyframe && i >= earlier_hovered_keyframe && i < later_hovered_keyframe + hover_color = HOVER_COLOR + else + hover_color = CAPTURE_COLOR + end + end + elsif !@captured_row && !@captured_keyframe && @hover_row && @hover_keyframe && + @hover_row == index && i >= earlier_hovered_keyframe && i < later_hovered_keyframe + hover_color = HOVER_COLOR + end + end + if hover_color + if is_property + bg_spr.bitmap.fill_rect(draw_x, 2, KEYFRAME_SPACING, ROW_HEIGHT - 3, hover_color) + else + bg_spr.bitmap.fill_rect(draw_x - (KEYFRAME_SPACING / 2), 2, KEYFRAME_SPACING, ROW_HEIGHT - 3, hover_color) end - elsif !@captured_row && !@captured_keyframe && - @hover_row && @hover_row == index && @hover_keyframe && @hover_keyframe == i - hover_color = HOVER_COLOR end - bg_spr.bitmap.fill_rect(draw_x - (KEYFRAME_SPACING / 2), 2, KEYFRAME_SPACING, ROW_HEIGHT - 3, hover_color) if hover_color next if i >= @duration - DURATION_BUFFER next if !visible_cmds[i] # Draw outline - bg_spr.bitmap.fill_rect(draw_x, 1, KEYFRAME_SPACING, 1, Color.black) # Top - bg_spr.bitmap.fill_rect(draw_x, ROW_HEIGHT - 1, KEYFRAME_SPACING, 1, Color.black) # Bottom + outline_color = Color.black + if is_property + outline_color = CONTROL_BG_COLORS[@particles[p_index][:focus]] || Color.magenta + end + bg_spr.bitmap.fill_rect(draw_x, ROW_SPACING, KEYFRAME_SPACING, 1, outline_color) # Top + bg_spr.bitmap.fill_rect(draw_x, ROW_HEIGHT - 1, KEYFRAME_SPACING, 1, outline_color) # Bottom if i <= 0 || !visible_cmds[i - 1] - bg_spr.bitmap.fill_rect(draw_x, 1, 1, ROW_HEIGHT - 1, Color.black) # Left + bg_spr.bitmap.fill_rect(draw_x, ROW_SPACING, 1, ROW_HEIGHT - ROW_SPACING, outline_color) # Left end if i == @duration - DURATION_BUFFER - 1 || (i < @duration - 1 && !visible_cmds[i + 1]) - bg_spr.bitmap.fill_rect(draw_x + KEYFRAME_SPACING, 1, 1, ROW_HEIGHT - 1, Color.black) # Right + bg_spr.bitmap.fill_rect(draw_x + KEYFRAME_SPACING, ROW_SPACING, 1, ROW_HEIGHT - ROW_SPACING, outline_color) # Right end end end @@ -714,17 +777,47 @@ class AnimationEditor::ParticleList < UIControls::BaseControl listed_element = @particle_list[new_hover_row] p_index = listed_element.is_a?(Array) ? listed_element[0] : listed_element break if @particles[p_index][:name] == "SE" - ret = [area, nil, new_hover_row] + mouse_y_in_row = mouse_y + @top_pos - (new_hover_row * ROW_HEIGHT) - rect.y + case mouse_x + when EXPAND_BUTTON_X...(EXPAND_BUTTON_X + EXPAND_BUTTON_WIDTH) + next if listed_element.is_a?(Array) + next if mouse_y_in_row < (ROW_HEIGHT - EXPAND_BUTTON_WIDTH + 1) / 2 + next if mouse_y_in_row >= ((ROW_HEIGHT - EXPAND_BUTTON_WIDTH + 1) / 2) + EXPAND_BUTTON_WIDTH + ret = [area, nil, new_hover_row, :expand] + when LIST_BOX_X...(@list_viewport.rect.width) + next if listed_element.is_a?(Array) + next if mouse_y_in_row < ROW_SPACING + ret = [area, nil, new_hover_row, :row] + end when :timeline new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER + (KEYFRAME_SPACING / 2) - 1) / KEYFRAME_SPACING break if new_hover_keyframe < 0 || new_hover_keyframe >= @duration ret = [area, new_hover_keyframe, nil] when :commands new_hover_row = (mouse_y + @top_pos - rect.y) / ROW_HEIGHT - new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER + (KEYFRAME_SPACING / 2) - 1) / KEYFRAME_SPACING break if new_hover_row >= @particle_list.length + listed_element = @particle_list[new_hover_row] + if listed_element.is_a?(Array) + new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER - 1) / KEYFRAME_SPACING + else + new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER + (KEYFRAME_SPACING / 2) - 1) / KEYFRAME_SPACING + end break if new_hover_keyframe < 0 || new_hover_keyframe >= @duration - ret = [area, new_hover_keyframe, new_hover_row] + if listed_element.is_a?(Array) + break if !GameData::Animation.property_can_interpolate?(listed_element[1]) + cmds = @commands[listed_element] + break if !cmds + earlier_keyframe = nil + later_keyframe = nil + cmds.each_with_index do |cmd, i| + earlier_keyframe = i if cmd && i <= new_hover_keyframe + later_keyframe = i if cmd && !later_keyframe && i > new_hover_keyframe + end + break if !earlier_keyframe || !later_keyframe + ret = [area, earlier_keyframe, new_hover_row, later_keyframe] + else + ret = [area, new_hover_keyframe, new_hover_row] + end end break end @@ -738,6 +831,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl @captured_area = hover_element[0] @captured_keyframe = hover_element[1] @captured_row = hover_element[2] + @captured_row_button = hover_element[3] end end @@ -748,19 +842,38 @@ class AnimationEditor::ParticleList < UIControls::BaseControl if hover_element.is_a?(Array) if @captured_area == hover_element[0] && @captured_keyframe == hover_element[1] && - @captured_row == hover_element[2] - if @captured_row && @particle_list[@captured_row].is_a?(Array) - # TODO: If I want to be able to select individual property rows and/or - # diamonds, I shouldn't have this line. - @captured_row = @particle_list.index(@particle_list[@captured_row][0]) + @captured_row == hover_element[2] && + @captured_row_button == hover_element[3] + if @captured_area == :commands && @captured_row && @particle_list[@captured_row].is_a?(Array) + set_changed + @values[:cycle_interpolation] = [*@particle_list[@captured_row], @captured_keyframe] + else + case @captured_row_button + when :expand + particle_index = @particle_list[@captured_row] + particle_index = particle_index[0] if particle_index.is_a?(Array) + if @expanded_particles.include?(particle_index) # Contract + @expanded_particles.delete(particle_index) + else # Expand + @expanded_particles.push(particle_index) + end + set_particles(@particles) + else # :row button or somewhere in the commands area or timeline, just change selection + if @captured_row && @particle_list[@captured_row].is_a?(Array) + @captured_row = @particle_list.index(@particle_list[@captured_row][0]) + end + set_changed if @keyframe != @captured_keyframe || @row_index != @captured_row + @keyframe = @captured_keyframe || -1 + # TODO: If :keyframe_pane should be accessible by clicking on the + # timeline, change the below line to = @captured_row || -1. + @row_index = @captured_row if @captured_row + end end - set_changed if @keyframe != @captured_keyframe || @row_index != @captured_row - @keyframe = @captured_keyframe || -1 - @row_index = @captured_row || -1 end end @captured_keyframe = nil @captured_row = nil + @captured_row_button = nil super # Make this control not busy again end @@ -778,6 +891,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl @hover_area = nil @hover_keyframe = nil @hover_row = nil + @hover_row_button = nil return end # Check each interactive area for whether the mouse is hovering over it, and @@ -787,7 +901,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl invalidate if @hover_area != hover_element[0] # Moved to a different region case hover_element[0] when :list - invalidate_rows if @hover_row != hover_element[2] + invalidate_rows if @hover_row != hover_element[2] || @hover_row_button != hover_element[3] when :timeline invalidate_time if @hover_keyframe != hover_element[1] when :commands @@ -797,6 +911,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl @hover_area = hover_element[0] @hover_keyframe = hover_element[1] @hover_row = hover_element[2] + @hover_row_button = hover_element[3] elsif hover_element if @hover_area == hover_element case @hover_area @@ -813,11 +928,13 @@ class AnimationEditor::ParticleList < UIControls::BaseControl @hover_area = hover_element @hover_keyframe = nil @hover_row = nil + @hover_row_button = nil else invalidate if @hover_area @hover_area = nil @hover_keyframe = nil @hover_row = nil + @hover_row_button = nil end end @@ -825,7 +942,9 @@ class AnimationEditor::ParticleList < UIControls::BaseControl return if !self.visible @list_scrollbar.update @time_scrollbar.update - @controls.each { |c| c[1].update } + if !@captured_area + @controls.each { |c| c[1].update } + end super # Refresh sprites if a scrollbar has been moved self.top_pos = @list_scrollbar.position