#include "timelinewidget.h" #include "panels/panels.h" #include "io/config.h" #include "project/sequence.h" #include "project/clip.h" #include "panels/project.h" #include "panels/timeline.h" #include "io/media.h" #include "ui/sourcetable.h" #include "panels/effectcontrols.h" #include "effects/effects.h" #include #include #include #include #include #include #include TimelineWidget::TimelineWidget(QWidget *parent) : QWidget(parent) { bottom_align = false; setMouseTracking(true); track_height = 40; clip_pixmap = NULL; setAcceptDrops(true); } bool same_sign(int a, int b) { return (a < 0) == (b < 0); } void TimelineWidget::dragEnterEvent(QDragEnterEvent *event) { if (static_cast(event->source()) == panel_project->source_table) { QPoint pos = event->pos(); event->accept(); QList items = panel_project->source_table->selectedItems(); long entry_point = getFrameFromScreenPoint(pos.x(), false); panel_timeline->drag_frame_start = entry_point + getFrameFromScreenPoint(50, false); panel_timeline->drag_track_start = (bottom_align) ? -1 : 0; for (int i=0;i(items.at(i)->data(0, Qt::UserRole + 1).value()); long duration = m->get_length_in_frames(panel_timeline->sequence->frame_rate); Ghost g = {NULL, entry_point, entry_point + duration}; g.media = m; g.clip_in = 0; for (int j=0;jaudio_tracks.size();j++) { g.track = j; g.media_stream = &m->audio_tracks[j]; ignore_infinite_length = true; panel_timeline->ghosts.append(g); } for (int j=0;jvideo_tracks.size();j++) { g.track = -1-j; g.media_stream = &m->video_tracks[j]; if (m->video_tracks[j].infinite_length && !ignore_infinite_length) g.out = g.in + 100; panel_timeline->ghosts.append(g); } entry_point += duration; } init_ghosts(); panel_timeline->importing = true; } } void TimelineWidget::dragMoveEvent(QDragMoveEvent *event) { if (panel_timeline->importing) { QPoint pos = event->pos(); update_ghosts(pos); panel_timeline->repaint_timeline(); } } void TimelineWidget::dragLeaveEvent(QDragLeaveEvent *event) { if (panel_timeline->importing) { panel_timeline->ghosts.clear(); panel_timeline->importing = false; panel_timeline->repaint_timeline(); } } void TimelineWidget::dropEvent(QDropEvent* event) { if (panel_timeline->importing) { event->accept(); for (int i=0;ighosts.size();i++) { const Ghost& g = panel_timeline->ghosts.at(i); panel_timeline->sequence->delete_area(g.in, g.out, g.track); Clip& c = panel_timeline->sequence->new_clip(); c.media = g.media; c.media_stream = g.media_stream; c.timeline_in = g.in; c.timeline_out = g.out; c.clip_in = g.clip_in; c.color_r = 128; c.color_g = 128; c.color_b = 192; c.sequence = panel_timeline->sequence; c.track = g.track; c.name = c.media->name; if (c.track < 0) { // add default video effects c.effects.append(new TransformEffect(&c)); } else { // add default audio effects c.effects.append(new VolumeEffect(&c)); c.effects.append(new PanEffect(&c)); } } panel_timeline->ghosts.clear(); panel_timeline->importing = false; panel_timeline->redraw_all_clips(); } } void TimelineWidget::mousePressEvent(QMouseEvent *event) { QPoint pos = event->pos(); panel_timeline->drag_frame_start = getFrameFromScreenPoint(pos.x(), false); panel_timeline->drag_track_start = getTrackFromScreenPoint(pos.y()); int clip_index = panel_timeline->trim_target; if (clip_index == -1) clip_index = getClipIndexFromCoords(panel_timeline->drag_frame_start, panel_timeline->drag_track_start); switch (panel_timeline->tool) { case TIMELINE_TOOL_POINTER: case TIMELINE_TOOL_RIPPLE: { if (clip_index >= 0) { if (panel_timeline->is_clip_selected(clip_index)) { // TODO if shift is down, deselect it } else { // if "shift" is not down if (!(event->modifiers() & Qt::ShiftModifier)) { panel_timeline->selections.clear(); } Clip& clip = panel_timeline->sequence->get_clip(clip_index); panel_timeline->selections.append({clip.timeline_in, clip.timeline_out, clip.track}); } panel_timeline->moving_init = true; } else { panel_timeline->selections.clear(); } panel_timeline->repaint_timeline(); } break; case TIMELINE_TOOL_EDIT: panel_timeline->seek(panel_timeline->drag_frame_start); panel_timeline->selecting = true; panel_timeline->repaint_timeline(); break; case TIMELINE_TOOL_RAZOR: { if (clip_index >= 0) { panel_timeline->sequence->split_clip(clip_index, panel_timeline->drag_frame_start); } panel_timeline->splitting = true; panel_timeline->redraw_all_clips(); } break; } } void TimelineWidget::mouseReleaseEvent(QMouseEvent *event) { if (panel_timeline->moving_proc) { if (event->modifiers() & Qt::AltModifier) { // if holding alt, duplicate rather than move // duplicate clips for (int i=0;ighosts.size();i++) { Ghost& g = panel_timeline->ghosts[i]; Clip& c = panel_timeline->sequence->insert_clip(*g.clip); c.timeline_in = g.in; c.timeline_out = g.out; c.track = g.track; c.undeletable = true; // step 2 - delete anything that exists in area that clip is moving to panel_timeline->sequence->delete_area(g.in, g.out, g.track); c.undeletable = false; } } else { // move clips // TODO can we do this better than 3 consecutive for loops? for (int i=0;ighosts.size();i++) { // step 1 - set clips that are moving to "undeletable" (to avoid step 2 deleting any part of them) panel_timeline->ghosts[i].clip->undeletable = true; } for (int i=0;ighosts.size();i++) { Ghost& g = panel_timeline->ghosts[i]; if (panel_timeline->tool == TIMELINE_TOOL_POINTER) { // step 2 - delete anything that exists in area that clip is moving to // note: ripples are non-destructive so this is pointer-tool exclusive panel_timeline->sequence->delete_area(g.in, g.out, g.track); } // step 3 - move clips g.clip->timeline_in = g.in; g.clip->timeline_out = g.out; g.clip->track = g.track; g.clip->clip_in = g.clip_in; } for (int i=0;ighosts.size();i++) { // step 4 - set clips back to deletable panel_timeline->ghosts[i].clip->undeletable = false; } } // ripple ops if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { long ripple_length, ripple_point; if (panel_timeline->trim_in) { ripple_length = panel_timeline->ghosts.at(0).old_in - panel_timeline->ghosts.at(0).in; ripple_point = (ripple_length < 0) ? panel_timeline->ghosts.at(0).old_in : panel_timeline->ghosts.at(0).in; } else { ripple_length = panel_timeline->ghosts.at(0).old_out - panel_timeline->ghosts.at(0).out; ripple_point = (ripple_length < 0) ? panel_timeline->ghosts.at(0).old_out : panel_timeline->ghosts.at(0).out; } for (int i=0;ighosts.size();i++) { long comp_point; if (panel_timeline->trim_in) { comp_point = (ripple_length < 0) ? panel_timeline->ghosts.at(i).old_in : panel_timeline->ghosts.at(i).in; } else { comp_point = (ripple_length < 0) ? panel_timeline->ghosts.at(i).old_out : panel_timeline->ghosts.at(i).out; } if (ripple_point > comp_point) ripple_point = comp_point; } if (panel_timeline->trim_in) { panel_timeline->ripple(ripple_point, ripple_length); } else { panel_timeline->ripple(ripple_point, -ripple_length); } } panel_timeline->redraw_all_clips(); } // destroy all ghosts panel_timeline->ghosts.clear(); panel_timeline->selecting = false; panel_timeline->moving_proc = false; panel_timeline->moving_init = false; panel_timeline->splitting = false; panel_timeline->snapped = false; pre_clips.clear(); post_clips.clear(); // find out how many clips are selected bool single_select = false; int selected_clip = 0; for (int i=0;isequence->clip_count();i++) { if (panel_timeline->is_clip_selected(i)) { if (!single_select) { // found ONE selected clip selected_clip = i; single_select = true; } else { // more than one clip is selected single_select = false; break; } } } if (single_select) { panel_effect_controls->set_clip(&panel_timeline->sequence->get_clip(selected_clip)); } else { panel_effect_controls->set_clip(NULL); } } void TimelineWidget::init_ghosts() { for (int i=0;ighosts.size();i++) { Ghost& g = panel_timeline->ghosts[i]; g.old_in = g.in; g.old_out = g.out; g.old_track = g.track; g.old_clip_in = g.clip_in; if (panel_timeline->trim_target > -1) { // used for trim ops g.ghost_length = g.old_out - g.old_in; g.media_length = g.clip->media->get_length_in_frames(panel_timeline->sequence->frame_rate); } } for (int i=0;iselections.size();i++) { Selection& s = panel_timeline->selections[i]; s.old_in = s.in; s.old_out = s.out; s.old_track = s.track; } } void TimelineWidget::update_ghosts(QPoint& mouse_pos) { int mouse_track = getTrackFromScreenPoint(mouse_pos.y()); long frame_diff = getFrameFromScreenPoint(mouse_pos.x(), false) - panel_timeline->drag_frame_start; int track_diff = mouse_track - panel_timeline->drag_track_start; int snap_range = 15; long validator; if (panel_timeline->trim_target > -1) { // trim ops // validate ghosts for (int i=0;ighosts.size();i++) { Ghost& g = panel_timeline->ghosts[i]; if (panel_timeline->trim_in) { // prevent clip length from being less than 1 frame long validator = g.ghost_length - frame_diff; if (validator < 1) frame_diff -= (1 - validator); // prevent timeline in from going below 0 validator = g.old_in + frame_diff; if (validator < 0) frame_diff -= validator; if (!g.clip->media_stream->infinite_length) { // prevent clip_in from going below 0 validator = g.old_clip_in + frame_diff; if (validator < 0) frame_diff -= validator; } // ripple ops if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { for (int j=0;jtimeline_in - frame_diff; if (validator < 0) frame_diff += validator; // prevent any post-clips colliding with pre-clips for (int k=0;ktrack == post->track) { validator = post->timeline_in - frame_diff - pre->timeline_out; if (validator < 0) frame_diff += validator; } } } } } else { // prevent clip length from being less than 1 frame long validator = g.ghost_length + frame_diff; if (validator < 1) frame_diff += (1 - validator); if (!g.clip->media_stream->infinite_length) { // prevent clip length exceeding media length validator = g.ghost_length + frame_diff; if (validator > g.media_length) frame_diff -= validator - g.media_length; } // ripple ops if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { for (int j=0;jtrack == post->track) { validator = post->timeline_in + frame_diff - pre->timeline_out; if (validator < 0) frame_diff -= validator; } } } } } } // resize ghosts for (int i=0;ighosts.size();i++) { Ghost& g = panel_timeline->ghosts[i]; if (panel_timeline->trim_in) { g.in = g.old_in + frame_diff; g.clip_in = g.old_clip_in + frame_diff; } else { g.out = g.old_out + frame_diff; } } // resize selections for (int i=0;iselections.size();i++) { Selection& s = panel_timeline->selections[i]; if (panel_timeline->trim_in) { s.in = s.old_in + frame_diff; } else { s.out = s.old_out + frame_diff; } } } else if (panel_timeline->tool == TIMELINE_TOOL_POINTER || panel_timeline->importing) { // only move clips on pointer (not ripple or rolling) // validate ghosts for (int i=0;ighosts.size();i++) { Ghost& g = panel_timeline->ghosts[i]; // prevent clips from moving below 0 on the timeline validator = g.old_in + frame_diff; if (validator < 0) frame_diff -= validator; // prevent clips from crossing tracks if (same_sign(g.old_track, panel_timeline->drag_track_start)) { while (!same_sign(g.old_track, g.old_track + track_diff)) { if (g.old_track < 0) { track_diff--; } else { track_diff++; } } } // shitty hacky code but works wonders panel_timeline->snapped = false; if (panel_timeline->snapping) { for (int j=0;jsequence->clip_count();j++) { Clip& c = panel_timeline->sequence->get_clip(j); panel_timeline->snapped = false; for (int k=0;k<4;k++) { switch (k) { case 0: // snap in to timeline_in validator = g.old_in + frame_diff - c.timeline_in; panel_timeline->snap_point = c.timeline_in; break; case 1: // snap in to timeline out validator = g.old_in + frame_diff - c.timeline_out; panel_timeline->snap_point = c.timeline_out; break; case 2: // snap out to timeline_in validator = g.old_out + frame_diff - c.timeline_in; panel_timeline->snap_point = c.timeline_in; break; case 3: // snap out to timeline_out validator = g.old_out + frame_diff - c.timeline_out; panel_timeline->snap_point = c.timeline_out; break; } if (validator > -snap_range && validator < snap_range) { frame_diff -= validator; panel_timeline->snapped = true; break; } } if (panel_timeline->snapped) break; } } } // move ghosts for (int i=0;ighosts.size();i++) { Ghost& g = panel_timeline->ghosts[i]; g.in = g.old_in + frame_diff; g.out = g.old_out + frame_diff; g.track = g.old_track; if (panel_timeline->importing) { int abs_track_diff = abs(track_diff); if (g.old_track < 0) { // clip is video g.track -= abs_track_diff; } else { // clip is audio g.track += abs_track_diff; } } else { if (same_sign(g.old_track, panel_timeline->drag_track_start)) g.track += track_diff; } } // move selections if (!panel_timeline->importing) { for (int i=0;iselections.size();i++) { Selection& s = panel_timeline->selections[i]; s.in = s.old_in + frame_diff; s.out = s.old_out + frame_diff; s.track = s.old_track; if (panel_timeline->importing) { int abs_track_diff = abs(track_diff); if (s.old_track < 0) { s.track -= abs_track_diff; } else { s.track += abs_track_diff; } } else { if (same_sign(s.track, panel_timeline->drag_track_start)) s.track += track_diff; } } } } } void TimelineWidget::mouseMoveEvent(QMouseEvent *event) { if (panel_timeline->selecting) { QPoint pos = event->pos(); int current_selection_track = getTrackFromScreenPoint(pos.y()); long current_selection_frame = getFrameFromScreenPoint(pos.x(), false);; int selection_count = 1 + qMax(current_selection_track, panel_timeline->drag_track_start) - qMin(current_selection_track, panel_timeline->drag_track_start); if (panel_timeline->selections.size() != selection_count) { panel_timeline->selections.resize(selection_count); } int minimum_selection_track = qMin(current_selection_track, panel_timeline->drag_track_start); for (int i=0;iselections[i]; s->track = minimum_selection_track + i; long in = panel_timeline->drag_frame_start; long out = current_selection_frame; s->in = qMin(in, out); s->out = qMax(in, out); } panel_timeline->playhead = qMin(panel_timeline->drag_frame_start, current_selection_frame); panel_timeline->repaint_timeline(); } else if (panel_timeline->moving_init) { QPoint pos = event->pos(); if (panel_timeline->moving_proc) { update_ghosts(pos); } else { // set up movement // create ghosts for (int i=0;isequence->clip_count();i++) { if (panel_timeline->is_clip_selected(i)) { Clip& c = panel_timeline->sequence->get_clip(i); panel_timeline->ghosts.append({&c, c.timeline_in, c.timeline_out, c.track, c.clip_in}); } } // ripple edit prep if (panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { for (int i=0;ighosts.size();i++) { // get clips before and after ripple point for (int j=0;jsequence->clip_count();j++) { // don't cache any currently selected clips Clip* c = panel_timeline->ghosts.at(i).clip; Clip& cc = panel_timeline->sequence->get_clip(j); bool is_selected = false; for (int k=0;kghosts.size();k++) { if (panel_timeline->ghosts.at(k).clip == &cc) { is_selected = true; break; } } if (!is_selected) { if (cc.timeline_in < c->timeline_in) { // add clip to pre-cache UNLESS there is already a clip on that track closer to the ripple point bool found = false; for (int k=0;ktrack == cc.track) { if (ccc->timeline_in < cc.timeline_in) { // clip is closer to ripple point than the one in cache, replace it ccc = &cc; } found = true; } } if (!found) { // no clip from that track in the cache, add it pre_clips.append(&cc); } } else { // add clip to post-cache UNLESS there is already a clip on that track closer to the ripple point bool found = false; for (int k=0;ktrack == cc.track) { if (ccc->timeline_in > cc.timeline_in) { // clip is closer to ripple point than the one in cache, replace it ccc = &cc; } found = true; } } if (!found) { // no clip from that track in the cache, add it post_clips.append(&cc); } } } } } // debug code - print the information we got // qDebug() << "found" << pre_clips.size() << "preceding clips and" << post_clips.size() << "following"; } init_ghosts(); panel_timeline->moving_proc = true; } panel_timeline->repaint_timeline(); } else if (panel_timeline->splitting) { QPoint pos = event->pos(); int track = getTrackFromScreenPoint(pos.y()); bool repaint = false; for (int i=0;isequence->clip_count();i++) { if (panel_timeline->sequence->get_clip(i).track == track) { panel_timeline->sequence->split_clip(i, panel_timeline->drag_frame_start); repaint = true; } } // redraw clips since we changed them if (repaint) panel_timeline->redraw_all_clips(); } else if (panel_timeline->tool == TIMELINE_TOOL_POINTER || panel_timeline->tool == TIMELINE_TOOL_RIPPLE) { QPoint pos = event->pos(); int lim = 5; int mouse_track = getTrackFromScreenPoint(pos.y()); long mouse_frame_lower = getFrameFromScreenPoint(pos.x()-lim, false)-1; long mouse_frame_upper = getFrameFromScreenPoint(pos.x()+lim, false)+1; bool found = false; for (int i=0;isequence->clip_count();i++) { Clip& c = panel_timeline->sequence->get_clip(i); if (c.track == mouse_track) { if (c.timeline_in > mouse_frame_lower && c.timeline_in < mouse_frame_upper) { panel_timeline->trim_target = i; panel_timeline->trim_in = true; found = true; break; } else if (c.timeline_out > mouse_frame_lower && c.timeline_out < mouse_frame_upper) { panel_timeline->trim_target = i; panel_timeline->trim_in = false; found = true; break; } } } if (found) { setCursor(Qt::SizeHorCursor); } else { unsetCursor(); panel_timeline->trim_target = -1; } } else if (panel_timeline->tool == TIMELINE_TOOL_EDIT || panel_timeline->tool == TIMELINE_TOOL_RAZOR) { // redraw because we have a cursor panel_timeline->repaint_timeline(); } } int color_brightness(int r, int g, int b) { return (0.2126*r + 0.7152*g + 0.0722*b); } void TimelineWidget::redraw_clips() { // Draw clips if (clip_pixmap != NULL) delete clip_pixmap; int panel_width = getScreenPointFromFrame(panel_timeline->sequence->getEndFrame()) + 100; setMinimumWidth(panel_width); clip_pixmap = new QPixmap(panel_width, height()); clip_pixmap->fill(Qt::transparent); QPainter clip_painter(clip_pixmap); int video_track_limit = 0; int audio_track_limit = 0; for (int i=0;isequence->clip_count();i++) { Clip& clip = panel_timeline->sequence->get_clip(i); if (is_track_visible(clip.track)) { if (clip.track < 0 && clip.track < video_track_limit) { // video clip video_track_limit = clip.track; } else if (clip.track > audio_track_limit) { audio_track_limit = clip.track; } QRect clip_rect(getScreenPointFromFrame(clip.timeline_in), getScreenPointFromTrack(clip.track), clip.getLength() * panel_timeline->zoom, track_height); clip_painter.fillRect(clip_rect, QColor(clip.color_r, clip.color_g, clip.color_b)); clip_painter.setPen(Qt::white); clip_painter.drawLine(clip_rect.bottomLeft(), clip_rect.topLeft()); clip_painter.drawLine(clip_rect.topLeft(), clip_rect.topRight()); clip_painter.setPen(QColor(0, 0, 0, 128)); clip_painter.drawLine(clip_rect.bottomLeft(), clip_rect.bottomRight()); clip_painter.drawLine(clip_rect.bottomRight(), clip_rect.topRight()); if (color_brightness(clip.color_r, clip.color_g, clip.color_b) > 160) { clip_painter.setPen(Qt::black); } else { clip_painter.setPen(Qt::white); } QRect text_rect(clip_rect.left() + CLIP_TEXT_PADDING, clip_rect.top() + CLIP_TEXT_PADDING, clip_rect.width() - CLIP_TEXT_PADDING - CLIP_TEXT_PADDING, clip_rect.height() - CLIP_TEXT_PADDING - CLIP_TEXT_PADDING); clip_painter.drawText(text_rect, 0, clip.name, &text_rect); } } // Draw track lines if (show_track_lines) { clip_painter.setPen(QColor(0, 0, 0, 96)); audio_track_limit++; if (video_track_limit == 0) video_track_limit--; if (bottom_align) { // only draw lines for video tracks for (int i=video_track_limit;i<0;i++) { int line_y = getScreenPointFromTrack(i) - 1; clip_painter.drawLine(0, line_y, rect().width(), line_y); } } else { // only draw lines for audio tracks for (int i=0;isequence != NULL) { QPainter p(this); if (clip_pixmap != NULL) p.drawPixmap(0, 0, minimumWidth(), height(), *clip_pixmap); // Draw selections for (int i=0;iselections.size();i++) { const Selection& s = panel_timeline->selections.at(i); if (is_track_visible(s.track)) { int selection_y = getScreenPointFromTrack(s.track); int selection_x = getScreenPointFromFrame(s.in); p.fillRect(selection_x, selection_y, getScreenPointFromFrame(s.out) - selection_x, track_height, QColor(0, 0, 0, 64)); } } // Draw ghosts for (int i=0;ighosts.size();i++) { const Ghost& g = panel_timeline->ghosts.at(i); if (is_track_visible(g.track)) { int ghost_x = getScreenPointFromFrame(g.in); int ghost_y = getScreenPointFromTrack(g.track); int ghost_width = getScreenPointFromFrame(g.out - g.in) - 1; int ghost_height = track_height - 1; p.setPen(QColor(255, 255, 0)); for (int j=0;jsnapping && panel_timeline->snapped) { p.setPen(Qt::white); int snap_x = getScreenPointFromFrame(panel_timeline->snap_point); p.drawLine(snap_x, 0, snap_x, height()); } // Draw edit cursor if (panel_timeline->tool == TIMELINE_TOOL_EDIT || panel_timeline->tool == TIMELINE_TOOL_RAZOR) { QPoint mouse_pos = mapFromGlobal(QCursor::pos()); int track = getTrackFromScreenPoint(mouse_pos.y()); if (is_track_visible(track)) { int cursor_x = getScreenPointFromFrame(getFrameFromScreenPoint(mouse_pos.x(), false)); int cursor_y = getScreenPointFromTrack(track); p.setPen(Qt::gray); p.drawLine(cursor_x, cursor_y, cursor_x, cursor_y + track_height); } } // Draw playhead p.setPen(Qt::red); int playhead_x = getScreenPointFromFrame(panel_timeline->playhead); p.drawLine(playhead_x, rect().top(), playhead_x, rect().bottom()); p.setPen(QColor(0, 0, 0, 64)); int edge_y = (bottom_align) ? rect().height()-1 : 0; p.drawLine(0, edge_y, rect().width(), edge_y); } } bool TimelineWidget::is_track_visible(int track) { return ((bottom_align && track < 0) || (!bottom_align && track >= 0)); } // ************************************** // screen point <-> frame/track functions // ************************************** long TimelineWidget::getFrameFromScreenPoint(int x, bool f) { float div = (float) x / panel_timeline->zoom; if (div < 0) { return 0; } if (f) { return floor(div); } else { return round(div); } } int TimelineWidget::getTrackFromScreenPoint(int y) { if (bottom_align) { y -= rect().bottom(); if (show_track_lines) y -= 1; } else { y -= 1; } int temp_track_height = track_height; if (show_track_lines) temp_track_height--; return (int)floor((float) (y)/ (float) temp_track_height); } int TimelineWidget::getScreenPointFromFrame(long frame) { return (int) round(frame*panel_timeline->zoom); } int TimelineWidget::getScreenPointFromTrack(int track) { int temp_track_height = track_height; if (show_track_lines) temp_track_height++; int y = track * temp_track_height; if (bottom_align) { // video track y += rect().bottom(); if (show_track_lines) y += 1; } else { // audio track y += 1; } return y; } int TimelineWidget::getClipIndexFromCoords(long frame, int track) { for (int i=0;isequence->clip_count();i++) { Clip& c = panel_timeline->sequence->get_clip(i); if (c.track == track) { if (frame >= c.timeline_in && frame < c.timeline_out) { return i; } } } return -1; }