OpenShot Video Editor  2.0.0
controllers.js
Go to the documentation of this file.
1 /**
2  * @file
3  * @brief The AngularJS controller used by the OpenShot Timeline
4  * @author Jonathan Thomas <jonathan@openshot.org>
5  * @author Cody Parker <cody@yourcodepro.com>
6  *
7  * @section LICENSE
8  *
9  * Copyright (c) 2008-2014 OpenShot Studios, LLC
10  * <http://www.openshotstudios.com/>. This file is part of
11  * OpenShot Video Editor, an open-source project dedicated to
12  * delivering high quality video editing and animation solutions to the
13  * world. For more information visit <http://www.openshot.org/>.
14  *
15  * OpenShot Video Editor is free software: you can redistribute it
16  * and/or modify it under the terms of the GNU General Public License
17  * as published by the Free Software Foundation, either version 3 of the
18  * License, or (at your option) any later version.
19  *
20  * OpenShot Video Editor is distributed in the hope that it will be
21  * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
22  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23  * GNU General Public License for more details.
24  *
25  * You should have received a copy of the GNU General Public License
26  * along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
27  */
28 
29 
30 // Initialize the main controller module
31 App.controller('TimelineCtrl',function($scope) {
32 
33  // DEMO DATA (used when debugging outside of Qt using Chrome)
34  $scope.project =
35  {
36  fps: {
37  num : 24,
38  den : 1
39  },
40  duration : 300, //length of project in seconds
41  scale : 16.0, //seconds per tick
42  tick_pixels : 100, //pixels between tick mark
43  playhead_position : 10, //position of play head
44  clips : [
45  {
46  id : '1',
47  layer : 1,
48  image : './media/images/thumbnail.png',
49  locked : false,
50  duration : 32, //max length in seconds
51  start : 0,
52  end : 32,
53  position : 0.0,
54  title : 'Clip U2V5ENELDY',
55  effects : [
56  { type : 'Saturation', icon : 'bw.png'},
57  { type : 'ChromaKey',icon : 'om.png'},
58  { type : 'Negate',icon : 'neg.png'},
59  { type : 'Blur', icon: 'blur.png'},
60  { type : 'Brightness', icon: 'cartoon.png'}
61  ],
62  images : {start: 1, end: 4},
63  show_audio : false,
64 
65  alpha: {
66  Points: [
67  {
68  "interpolation": 2,
69  "co": {
70  "Y": 0,
71  "X": 0
72  }
73  },
74  {
75  "interpolation": 1,
76  "co": {
77  "Y": 0,
78  "X": 250
79  }
80  },
81  {
82  "interpolation": 1,
83  "co": {
84  "Y": 1,
85  "X": 500
86  }
87  }
88  ]
89  },
90  location_x: { Points: [] },
91  location_y: { Points: [] },
92  scale_x: { Points: [] },
93  scale_y: { Points: [] },
94  rotation: { Points: [] },
95  time: { Points: [] },
96  volume: { Points: [] }
97 
98  },
99  {
100  id : '2',
101  layer : 2,
102  image : './media/images/thumbnail.png',
103  locked : false,
104  duration : 45,
105  start : 0,
106  end : 45,
107  position : 0.0,
108  title : 'Clip B',
109  effects : [],
110  images : {start: 3, end: 7},
111  show_audio : false,
112  alpha: { Points: [] },
113  location_x: { Points: [] },
114  location_y: { Points: [] },
115  scale_x: { Points: [] },
116  scale_y: { Points: [] },
117  rotation: { Points: [] },
118  time: { Points: [] },
119  volume: { Points: [] }
120  },
121  {
122  id : '3',
123  layer : 3,
124  image : './media/images/thumbnail.png',
125  locked : false,
126  duration : 120,
127  start : 0,
128  end : 120,
129  position : 32.0,
130  title : 'Clip C',
131  effects : [
132  { type : 'Deinterlace',icon : 'om.png'},
133  { type : 'Blur', icon: 'blur.png'},
134  { type : 'Mask', icon: 'cartoon.png'}
135  ],
136  images : { start: 5, end: 10 },
137  show_audio : false,
138  audio_data : [.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3,.5, .6, .7, .7, .6, .5, .4, .1, 0, -0.1, -0.3, -0.6, -0.6, -0.3, -0.1, 0, .2, .3, ],
139  alpha: { Points: [] },
140  location_x: { Points: [] },
141  location_y: { Points: [] },
142  scale_x: { Points: [] },
143  scale_y: { Points: [] },
144  rotation: { Points: [] },
145  time: { Points: [] },
146  volume: { Points: [] }
147  },
148  ],
149 
150  effects : [
151  {
152  id : '5',
153  layer : 4,
154  title : 'Transition',
155  position : 20.0,
156  start : 0,
157  end : 30
158  },
159  {
160  id : '6',
161  layer : 3,
162  title : 'Transition',
163  position : 137.5,
164  start : 0,
165  end : 30
166  },
167  {
168  id : '7',
169  layer : 2,
170  title : 'Transition',
171  position : 30.5,
172  start : 0,
173  end : 30
174  }
175 
176  ],
177 
178  layers : [
179  {id: 'L0', number:0, y:0, label: '', lock: false},
180  {id: 'L1', number:1, y:0, label: '', lock: false},
181  {id: 'L2', number:2, y:0, label: '', lock: false},
182  {id: 'L3', number:3, y:0, label: '', lock: false},
183  {id: 'L4', number:4, y:0, label: '', lock: false}
184 
185  ],
186 
187  markers : [
188  {
189  id : 'M1',
190  position : 16,
191  icon : 'yellow.png'
192  },
193  {
194  id : 'M2',
195  position: 120,
196  icon : 'green.png'
197  },
198  {
199  id : 'M3',
200  position: 300,
201  icon : 'red.png'
202  },
203  {
204  id : 'M4',
205  position: 10,
206  icon : 'purple.png'
207  },
208  ],
209 
210  progress : [
211  [0, 30, 'rendering'],
212  [40, 50, 'complete'],
213  [100, 150, 'complete'],
214  ]
215  };
216 
217  // Additional variables used to control the rendering of HTML
218  $scope.pixelsPerSecond = parseFloat($scope.project.tick_pixels) / parseFloat($scope.project.scale);
219  $scope.playheadOffset = 0;
220  $scope.keyframePointOffset = 3;
221  $scope.playhead_animating = false;
222  $scope.playhead_height = 300;
223  $scope.playheadTime = secondsToTime($scope.project.playhead_position, $scope.project.fps.num, $scope.project.fps.den);
224  $scope.shift_pressed = false;
225  $scope.snapline_position = 0.0;
226  $scope.snapline = false;
227  $scope.enable_snapping = true;
228  $scope.debug = false;
229  $scope.min_width = 1024;
230  $scope.track_label = "Track %s";
231  $scope.enable_sorting = true;
232 
233  // Method to set if Qt is detected (which clears demo data)
234  $scope.Qt = false;
235  $scope.EnableQt = function() {
236  $scope.Qt = true;
237  $scope.project.clips = [];
238  $scope.project.markers = [];
239  $scope.project.effects = [];
240  $scope.project.progress = [];
241  timeline.qt_log("$scope.Qt = true;");
242  };
243 
244  // Move the playhead to a specific time
245  $scope.MovePlayhead = function(position_seconds) {
246  // Update internal scope (in seconds)
247  $scope.project.playhead_position = position_seconds;
248  $scope.playheadTime = secondsToTime(position_seconds, $scope.project.fps.num, $scope.project.fps.den);
249 
250  // Use JQuery to move playhead (for performance reasons) - scope.apply is too expensive here
251  $(".playhead-top").css("left", (($scope.project.playhead_position * $scope.pixelsPerSecond) + $scope.playheadOffset) + "px");
252  $(".playhead-line").css("left", (($scope.project.playhead_position * $scope.pixelsPerSecond) + $scope.playheadOffset) + "px");
253  $("#ruler_time").text($scope.playheadTime.hour + ":" + $scope.playheadTime.min + ":" + $scope.playheadTime.sec + ":" + $scope.playheadTime.frame);
254  };
255 
256  // Move the playhead to a specific frame
257  $scope.MovePlayheadToFrame = function(position_frames) {
258  // Don't move the playhead if it's currently animating
259  if ($scope.playhead_animating)
260  return;
261 
262  // Determine seconds
263  var frames_per_second = $scope.project.fps.num / $scope.project.fps.den;
264  var position_seconds = ((position_frames - 1) / frames_per_second);
265 
266  // Update internal scope (in seconds)
267  $scope.MovePlayhead(position_seconds);
268  };
269 
270  // Move the playhead to a specific time
271  $scope.PreviewFrame = function(position_seconds) {
272  // Determine frame
273  var frames_per_second = $scope.project.fps.num / $scope.project.fps.den;
274  var frame = (position_seconds * frames_per_second) + 1;
275 
276  // Update GUI with position (to the preview can be updated)
277  if ($scope.Qt)
278  timeline.PlayheadMoved(position_seconds, frame, secondsToTime(position_seconds, $scope.project.fps.num, $scope.project.fps.den));
279  };
280 
281 
282  // Move the playhead to a specific time
283  $scope.PreviewClipFrame = function(clip_id, position_seconds) {
284  // Determine frame
285  var frames_per_second = $scope.project.fps.num / $scope.project.fps.den;
286  var frame = (position_seconds * frames_per_second) + 1;
287 
288  // Update GUI with position (to the preview can be updated)
289  if ($scope.Qt)
290  timeline.PreviewClipFrame(clip_id, frame);
291  };
292 
293  // Get an array of keyframe points for the selected clips
294  $scope.getKeyframes = function(object){
295  // List of keyframes
296  keyframes = {};
297 
298  var frames_per_second = $scope.project.fps.num / $scope.project.fps.den;
299  var clip_start_x = Math.round(object.start * frames_per_second) + 1.0;
300  var clip_end_x = Math.round(object.end * frames_per_second) + 1.0;
301 
302  // Loop through properties of an object (clip/transition), looking for keyframe points
303  for (child in object) {
304  if (!object.hasOwnProperty(child)) {
305  //The current property is not a direct property of p
306  continue;
307  }
308 
309  // Determine if this property is a Keyframe
310  if (typeof object[child] == "object" && "Points" in object[child])
311  for (var point = 0; point < object[child].Points.length; point++) {
312  var co = object[child].Points[point].co;
313  if (co.X >= clip_start_x && co.X <= clip_end_x)
314  // Only add keyframe coordinates that are within the bounds of the clip
315  keyframes[co.X] = co.Y;
316  }
317  }
318 
319  // Determine if this property contains effects (i.e. clips have their own effects)
320  if ("effects" in object)
321  for (effect in object["effects"]) {
322 
323  // Loop through properties of an effect, looking for keyframe points
324  for (child in object["effects"][effect]) {
325  if (!object["effects"][effect].hasOwnProperty(child)) {
326  //The current property is not a direct property of p
327  continue;
328  }
329 
330  // Determine if this property is a Keyframe
331  if (typeof object["effects"][effect][child] == "object" && "Points" in object["effects"][effect][child])
332  for (var point = 0; point < object["effects"][effect][child].Points.length; point++) {
333  var co = object["effects"][effect][child].Points[point].co;
334  if (co.X >= clip_start_x && co.X <= clip_end_x)
335  // Only add keyframe coordinates that are within the bounds of the clip
336  keyframes[co.X] = co.Y;
337  }
338  }
339  }
340 
341  // Return keyframe array
342  return keyframes;
343  };
344 
345 
346  // Determine track top (in vertical pixels)
347  $scope.getTrackTop = function(layer){
348  // Get scrollbar position
349  var vert_scroll_offset = $("#scrolling_tracks").scrollTop();
350  var horz_scroll_offset = $("#scrolling_tracks").scrollLeft();
351 
352  // Find this tracks Y location
353  var track_id = "div#track_" + layer;
354  if ($(track_id).length)
355  return $(track_id).position().top + vert_scroll_offset;
356  else
357  return 0;
358  };
359 
360 
361 // ############# QT FUNCTIONS #################### //
362 
363  // Change the scale and apply to scope
364  $scope.setScale = function(scaleVal){
365  $scope.$apply(function(){
366  $scope.project.scale = scaleVal;
367  $scope.pixelsPerSecond = parseFloat($scope.project.tick_pixels) / parseFloat($scope.project.scale);
368  });
369  };
370 
371  // Set the audio data for a clip
372  $scope.setAudioData = function(clip_id, audio_data){
373  // Find matching clip
374  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++)
375  if ($scope.project.clips[clip_index].id == clip_id) {
376  // Set audio data
377  $scope.$apply(function(){
378  $scope.project.clips[clip_index].audio_data = audio_data;
379  $scope.project.clips[clip_index].show_audio = true;
380  });
381  timeline.qt_log("Audio data successful set on clip JSON");
382  break;
383  }
384 
385  // Draw audio data
386  drawAudio($scope, clip_id);
387  };
388 
389  // Hide the audio waveform for a clip
390  $scope.hideAudioData = function(clip_id, audio_data){
391  // Find matching clip
392  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++)
393  if ($scope.project.clips[clip_index].id == clip_id) {
394  // Set audio data
395  $scope.$apply(function(){
396  $scope.project.clips[clip_index].show_audio = false;
397  $scope.project.clips[clip_index].audio_data = [];
398  });
399  break;
400  }
401  };
402 
403  // Redraw all audio waveforms on the timeline (if any)
404  $scope.reDrawAllAudioData = function(){
405  // Find matching clip
406  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++) {
407  if ("audio_data" in $scope.project.clips[clip_index] && $scope.project.clips[clip_index].audio_data.length > 0) {
408  // Redraw audio data (since it has audio data)
409  drawAudio($scope, $scope.project.clips[clip_index].id);
410  }
411  }
412  };
413 
414  // Does clip have audio_data?
415  $scope.hasAudioData = function(clip_id){
416  // Find matching clip
417  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++) {
418  if ($scope.project.clips[clip_index].id == clip_id && "audio_data" in $scope.project.clips[clip_index] && $scope.project.clips[clip_index].audio_data.length > 0) {
419  return true;
420  break;
421  }
422  }
423 
424  return false;
425  };
426 
427  // Change the snapping mode
428  $scope.SetSnappingMode = function(enable_snapping){
429  $scope.$apply(function(){
430  $scope.enable_snapping = enable_snapping;
431  if (enable_snapping)
432  $(".droppable").draggable("option", "snapTolerance", 20);
433  else
434  $(".droppable").draggable("option", "snapTolerance", 0);
435  });
436  };
437 
438  // Get the color of an effect
439  $scope.GetEffectColor = function(effect_type){
440  switch (effect_type)
441  {
442  case "Blur":
443  return "#0095bf";
444  case "Brightness":
445  return "#5500ff";
446  case "ChromaKey":
447  return "#00ad2d";
448  case "Deinterlace":
449  return "#006001";
450  case "Mask":
451  return "#cb0091";
452  case "Negate":
453  return "#ff9700";
454  case "Saturation":
455  return "#ff3d00";
456  default:
457  return "#000000";
458  }
459  };
460 
461  // Add a new clip to the timeline
462  $scope.AddClip = function(x, y, clip_json){
463  $scope.$apply(function(){
464 
465  // Convert x and y into timeline vars
466  var scrolling_tracks_offset = $("#scrolling_tracks").offset().left;
467  var clip_position = parseFloat(x - scrolling_tracks_offset) / parseFloat($scope.pixelsPerSecond);
468  clip_json.position = clip_position;
469  clip_json.layer = $scope.GetTrackAtY(y).number;
470 
471  // Push new clip onto stack
472  $scope.project.clips.push(clip_json);
473 
474  });
475  };
476 
477  // Clear all selections
478  $scope.ClearAllSelections = function() {
479  // Clear the selections on the main window
480  $scope.SelectTransition("", true);
481  $scope.SelectEffect("", true);
482 
483  // Update scope
484  $scope.$apply(function() {
485  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++)
486  $scope.project.clips[clip_index].selected = false;
487  for (var effect_index = 0; effect_index < $scope.project.effects.length; effect_index++)
488  $scope.project.effects[effect_index].selected = false;
489  });
490  };
491 
492  // Select all clips and transitions
493  $scope.SelectAll = function() {
494  $scope.$apply(function() {
495  // Select all clips
496  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++) {
497  $scope.project.clips[clip_index].selected = true;
498  timeline.addSelection($scope.project.clips[clip_index].id, "clip", false);
499  }
500  // Select all transitions
501  for (var effect_index = 0; effect_index < $scope.project.effects.length; effect_index++) {
502  $scope.project.effects[effect_index].selected = true;
503  timeline.addSelection($scope.project.effects[effect_index].id, "transition", false);
504  }
505  });
506  };
507 
508 
509  // Select clip in scope
510  $scope.SelectClip = function(clip_id, clear_selections, event) {
511  // Trim clip_id
512  var id = clip_id.replace("clip_", "");
513 
514  // Clear transitions also (if needed)
515  if (id != "" && clear_selections) {
516  $scope.SelectTransition("", clear_selections);
517  $scope.SelectEffect("", clear_selections);
518  }
519 
520  // Is CTRL pressed?
521  is_ctrl = false;
522  if (event && event.ctrlKey)
523  is_ctrl = true;
524 
525  // Unselect all clips
526  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++)
527  if ($scope.project.clips[clip_index].id == id) {
528  $scope.project.clips[clip_index].selected = true;
529  if ($scope.Qt)
530  timeline.addSelection(id, "clip", clear_selections);
531  }
532  else if (clear_selections && !is_ctrl) {
533  $scope.project.clips[clip_index].selected = false;
534  if ($scope.Qt)
535  timeline.removeSelection($scope.project.clips[clip_index].id, "clip");
536  }
537  };
538 
539  // Select transition in scope
540  $scope.SelectTransition = function(tran_id, clear_selections, event) {
541  // Trim tran_id
542  var id = tran_id.replace("transition_", "");
543 
544  // Clear clips also (if needed)
545  if (id != "" && clear_selections) {
546  $scope.SelectClip("", true);
547  $scope.SelectEffect("", true);
548  }
549 
550  // Is CTRL pressed?
551  is_ctrl = false;
552  if (event && event.ctrlKey)
553  is_ctrl = true;
554 
555  // Unselect all transitions
556  for (var tran_index = 0; tran_index < $scope.project.effects.length; tran_index++)
557  if ($scope.project.effects[tran_index].id == id) {
558  $scope.project.effects[tran_index].selected = true;
559  if ($scope.Qt)
560  timeline.addSelection(id, "transition", clear_selections);
561  }
562  else if (clear_selections && !is_ctrl) {
563  $scope.project.effects[tran_index].selected = false;
564  if ($scope.Qt)
565  timeline.removeSelection($scope.project.effects[tran_index].id, "transition");
566  }
567  };
568 
569  // Format the thumbnail path
570  $scope.FormatThumbPath = function(image_url) {
571  if (image_url.charAt(0) == ".")
572  return image_url;
573  else
574  return "file:///" + image_url;
575  };
576 
577  // Select transition in scope
578  $scope.SelectEffect = function(effect_id) {
579  if ($scope.Qt)
580  timeline.addSelection(effect_id, "effect", true);
581  };
582 
583 // Find the furthest right edge on the timeline (and resize it if too small)
584  $scope.ResizeTimeline = function() {
585 
586  // Unselect all clips
587  var furthest_right_edge = 0;
588  for (var clip_index = 0; clip_index < $scope.project.clips.length; clip_index++)
589  {
590  var clip = $scope.project.clips[clip_index];
591  var right_edge = clip.position + (clip.end - clip.start);
592  if (right_edge > furthest_right_edge)
593  furthest_right_edge = right_edge;
594  }
595 
596  // Resize timeline
597  if (furthest_right_edge > $scope.project.duration)
598  if ($scope.Qt) {
599  timeline.resizeTimeline(furthest_right_edge + 10)
600  $scope.project.duration = furthest_right_edge + 10;
601  }
602  };
603 
604  // Show clip context menu
605  $scope.ShowClipMenu = function(clip_id) {
606  if ($scope.Qt) {
607  timeline.qt_log("$scope.ShowClipMenu");
608  timeline.ShowClipMenu(clip_id);
609  }
610  };
611 
612  // Show clip context menu
613  $scope.ShowEffectMenu = function(effect_id) {
614  if ($scope.Qt) {
615  timeline.qt_log("$scope.ShowEffectMenu");
616  timeline.ShowEffectMenu(effect_id);
617  }
618  };
619 
620  // Show transition context menu
621  $scope.ShowTransitionMenu = function(tran_id) {
622  if ($scope.Qt) {
623  timeline.qt_log("$scope.ShowTransitionMenu");
624  timeline.ShowTransitionMenu(tran_id);
625  }
626  };
627 
628  // Show track context menu
629  $scope.ShowTrackMenu = function(layer_id) {
630  if ($scope.Qt) {
631  timeline.qt_log("$scope.ShowTrackMenu");
632  timeline.ShowTrackMenu(layer_id);
633  }
634  };
635 
636  // Show marker context menu
637  $scope.ShowMarkerMenu = function(marker_id) {
638  if ($scope.Qt) {
639  timeline.qt_log("$scope.ShowMarkerMenu");
640  timeline.ShowMarkerMenu(marker_id);
641  }
642  };
643 
644  // Show playhead context menu
645  $scope.ShowPlayheadMenu = function(position) {
646  if ($scope.Qt) {
647  timeline.qt_log("$scope.ShowPlayheadMenu");
648  timeline.ShowPlayheadMenu(position);
649  }
650  };
651 
652  // Show timeline context menu
653  $scope.ShowTimelineMenu = function(e, layer_number) {
654  if ($scope.Qt) {
655  timeline.ShowTimelineMenu($scope.GetJavaScriptPosition(e.pageX), layer_number);
656  }
657  };
658 
659  // Get the name of the track
660  $scope.GetTrackName = function(layer_label, layer_number){
661  // Determine custom label or default track name
662  if (layer_label.length > 0)
663  return layer_label;
664  else
665  return $scope.track_label.replace('%s', layer_number.toString());
666  };
667 
668 $scope.SetTrackLabel = function (label){
669  $scope.track_label = label;
670 };
671 
672  // Get the width of the timeline in pixels
673  $scope.GetTimelineWidth = function(min_value){
674  // Adjust for minimim length
675  return Math.max(min_value, $scope.project.duration * $scope.pixelsPerSecond);
676  };
677 
678 
679  // Get Position of item (used by Qt)
680  $scope.GetJavaScriptPosition = function(x){
681  // Adjust for scrollbar position
682  var horz_scroll_offset = $("#scrolling_tracks").scrollLeft();
683  var scrolling_tracks_offset_left = $("#scrolling_tracks").offset().left;
684  x += horz_scroll_offset;
685 
686  // Convert x into position in seconds
687  var clip_position = parseFloat(x - scrolling_tracks_offset_left) / parseFloat($scope.pixelsPerSecond);
688  if (clip_position < 0)
689  clip_position = 0;
690 
691  // Return position in seconds
692  return clip_position;
693  };
694 
695  // Get Track number of item (used by Qt)
696  $scope.GetJavaScriptTrack = function(y){
697  // Adjust for scrollbar position
698  var vert_scroll_offset = $("#scrolling_tracks").scrollTop();
699  y += vert_scroll_offset;
700 
701  // Return number of track
702  var track_number = parseInt($scope.GetTrackAtY(y).number);
703  return track_number;
704  };
705 
706  // Get JSON of most recent item (used by Qt)
707  $scope.UpdateRecentItemJSON = function(item_type, item_id) {
708 
709  // Find item in JSON
710  var item_object = null;
711  if (item_type == 'clip') {
712  item_object = findElement($scope.project.clips, "id", item_id);
713  } else if (item_type == 'transition') {
714  item_object = findElement($scope.project.effects, "id", item_id);
715  } else {
716  // Bail out if no id found
717  return;
718  }
719 
720  // Get position of item
721  var scrolling_tracks_offset_top = $("#scrolling_tracks").offset().top;
722  var clip_position = parseFloat(bounding_box.left) / parseFloat($scope.pixelsPerSecond);
723  var layer_num = $scope.GetTrackAtY(bounding_box.track_position - scrolling_tracks_offset_top).number;
724 
725  // update scope with final position of items
726  $scope.$apply(function() {
727  // update item
728  item_object.position = clip_position;
729  item_object.layer = layer_num;
730  });
731 
732  // update clip in Qt (very important =)
733  if (item_type == 'clip')
734  timeline.update_clip_data(JSON.stringify(item_object));
735  else if (item_type == 'transition')
736  timeline.update_transition_data(JSON.stringify(item_object));
737 
738  // Resize timeline if it's too small to contain all clips
739  $scope.ResizeTimeline();
740 
741  // Hide snapline (if any)
742  $scope.HideSnapline();
743 
744  // Remove CSS class (after the drag)
745  bounding_box = {};
746  };
747 
748  // Init bounding boxes for manual move
749  $scope.StartManualMove = function(item_type, item_id){
750  // Select the item
751  $scope.$apply(function() {
752  if (item_type == 'clip')
753  $scope.SelectClip(item_id, true);
754  else if (item_type == 'transition')
755  $scope.SelectTransition(item_id, true);
756  });
757 
758  // JQuery selector for element (clip or transition)
759  var element_id = "#" + item_type + "_" + item_id;
760 
761  // Init bounding box
762  bounding_box = {};
763  setBoundingBox($(element_id));
764 
765  // Init some variables to track the changing position
766  bounding_box.previous_x = bounding_box.left;
767  bounding_box.previous_y = bounding_box.top;
768  bounding_box.offset_x = 0;
769  bounding_box.offset_y = 0;
770  bounding_box.element = $(element_id);
771  bounding_box.track_position = 0;
772  };
773 
774  // Move a new clip to the timeline
775  $scope.MoveItem = function(x, y, item_type) {
776 
777  var vert_scroll_offset = $("#scrolling_tracks").scrollTop();
778  var horz_scroll_offset = $("#scrolling_tracks").scrollLeft();
779  x += horz_scroll_offset;
780  y += vert_scroll_offset;
781 
782  // Convert x and y into timeline vars
783  var scrolling_tracks_offset_left = $("#scrolling_tracks").offset().left;
784  var scrolling_tracks_offset_top = $("#scrolling_tracks").offset().top;
785 
786  // Calculate the x,y of cursor
787  var left = parseFloat(x - scrolling_tracks_offset_left);
788  var top = parseFloat(y - scrolling_tracks_offset_top);
789 
790  // Calculate amount to move transitions
791  var x_offset = left - bounding_box.previous_x;
792  var y_offset = top - bounding_box.previous_y;
793 
794  // Move the bounding box and apply snapping rules
795  results = moveBoundingBox($scope, bounding_box.previous_x, bounding_box.previous_y, x_offset, y_offset, left, top);
796 
797  // Track previous values
798  bounding_box.previous_x = results.position.left;
799  bounding_box.previous_y = results.position.top;
800 
801  var clip_position = parseFloat(results.position.left) / parseFloat($scope.pixelsPerSecond);
802  if (clip_position < 0)
803  clip_position = 0;
804 
805  // Loop through each layer (looking for the closest track based on Y coordinate)
806  bounding_box.track_position = 0;
807  for (var layer_index = $scope.project.layers.length - 1; layer_index >= 0 ; layer_index--) {
808  var layer = $scope.project.layers[layer_index];
809 
810  // Compare position of track to Y param (for unlocked tracks)
811  if (!layer.lock)
812  if ((top < layer.y && top > bounding_box.track_position) || bounding_box.track_position==0)
813  // return first matching layer
814  bounding_box.track_position = layer.y;
815  }
816 
817  //change the element location
818  bounding_box.element.css('left', results.position.left);
819  bounding_box.element.css('top', bounding_box.track_position - scrolling_tracks_offset_top);
820  };
821 
822  // Update X,Y indexes of tracks / layers (anytime the project.layers scope changes)
823  $scope.UpdateLayerIndex = function(){
824 
825  if ($scope.Qt)
826  timeline.qt_log('UpdateLayerIndex');
827 
828  var vert_scroll_offset = $("#scrolling_tracks").scrollTop();
829  var horz_scroll_offset = $("#scrolling_tracks").scrollLeft();
830 
831  // Get scrollbar offsets
832  var scrolling_tracks_offset_left = $("#scrolling_tracks").offset().left;
833  var scrolling_tracks_offset_top = $("#scrolling_tracks").offset().top;
834 
835  $scope.$apply(function(){
836 
837  // Loop through each layer
838  for (var layer_index = 0; layer_index < $scope.project.layers.length; layer_index++) {
839  var layer = $scope.project.layers[layer_index];
840 
841  // Find element on screen (bound to this layer)
842  var layer_elem = $("#track_" + layer.number);
843  if (layer_elem) {
844  // Update the top offset
845  layer.y = layer_elem.offset().top + vert_scroll_offset;
846  }
847  }
848 
849  // Update playhead height
850  $scope.playhead_height = $("#track-container").height();
851  $(".playhead-line").height($scope.playhead_height);
852  });
853  };
854 
855  // Sort clips and transitions by position
856  $scope.SortItems = function(){
857  if (!$scope.enable_sorting)
858  // Sorting is disabled, do nothing
859  return;
860 
861  if ($scope.Qt)
862  timeline.qt_log('SortItems');
863 
864  $scope.$apply(function(){
865  // Sort by position second
866  $scope.project.clips = $scope.project.clips.sort(function(a,b) {
867  if ( a.position < b.position )
868  return -1;
869  if ( a.position > b.position )
870  return 1;
871  return 0;
872  });
873 
874  // Sort transitions by position second
875  $scope.project.effects = $scope.project.effects.sort(function(a,b) {
876  if ( a.position < b.position )
877  return -1;
878  if ( a.position > b.position )
879  return 1;
880  return 0;
881  });
882 
883  // Sort tracks by position second
884  $scope.project.layers = $scope.project.layers.sort(function(a,b) {
885  if ( a.number < b.number )
886  return -1;
887  if ( a.number > b.number )
888  return 1;
889  return 0;
890  });
891 
892  });
893  };
894 
895  // Find overlapping clips
896  $scope.GetMissingTransitions = function(original_clip) {
897 
898  var transition_size = null;
899 
900  // Get clip that matches this id
901  var original_left = original_clip.position;
902  var original_right = original_clip.position + (original_clip.end - original_clip.start);
903 
904  // Search through all other clips on this track, and look for overlapping ones
905  for (var index = 0; index < $scope.project.clips.length; index++) {
906  var clip = $scope.project.clips[index];
907 
908  // skip clips that are not on the same layer
909  if (original_clip.layer != clip.layer)
910  continue;
911 
912  // is clip overlapping
913  var clip_left = clip.position;
914  var clip_right = clip.position + (clip.end - clip.start);
915 
916  if (original_left < clip_right && original_left > clip_left)
917  transition_size = { "position" : original_left, "layer" : clip.layer, "start" : 0, "end" : (clip_right - original_left) };
918  else if (original_right > clip_left && original_right < clip_right)
919  transition_size = { "position" : clip_left, "layer" : clip.layer, "start" : 0, "end" : (original_right - clip_left) };
920 
921  if (transition_size != null && transition_size.end >= 0.5)
922  // Found a possible missing transition
923  break;
924  else if (transition_size != null && transition_size.end < 0.5)
925  // Too small to be a missing transitions, clear and continue looking
926  transition_size = null;
927 
928  }
929 
930  // Search through all existing transitions, and don't overlap an existing one
931  if (transition_size != null)
932  for (var tran_index = 0; tran_index < $scope.project.effects.length; tran_index++) {
933  var tran = $scope.project.effects[tran_index];
934 
935  // skip transitions that are not on the same layer
936  if (tran.layer != transition_size.layer)
937  continue;
938 
939  var tran_left = tran.position;
940  var tran_right = tran.position + (tran.end - tran.start);
941 
942  var new_tran_left = transition_size.position;
943  var new_tran_right = transition_size.position + (transition_size.end - transition_size.start);
944 
945  var TOLERANCE = 0.01;
946  // Check for overlapping transitions
947  if (Math.abs(tran_left - new_tran_left) < TOLERANCE || Math.abs(tran_right - new_tran_right) < TOLERANCE) {
948  transition_size = null; // this transition already exists
949  break;
950  }
951  }
952 
953  return transition_size;
954  };
955 
956  // Search through clips and transitions to find the closest element within a given threashold
957  $scope.GetNearbyPosition = function(pixel_positions, threashold, ignore_ids){
958  // init some vars
959  var smallest_diff = 900.0;
960  var smallest_abs_diff = 900.0;
961  var snapping_position = 0.0;
962  var diffs = [];
963 
964  // Loop through each pixel position (supports multiple positions: i.e. left and right side of bounding box)
965  for (var pos_index = 0; pos_index < pixel_positions.length; pos_index++) {
966  var pixel_position = pixel_positions[pos_index];
967  var position = pixel_position / $scope.pixelsPerSecond;
968 
969  // Add clip positions to array
970  for (var index = 0; index < $scope.project.clips.length; index++) {
971  var clip = $scope.project.clips[index];
972 
973  // exit out if this item is in ignore_ids
974  if (ignore_ids.hasOwnProperty(clip.id))
975  continue;
976 
977  diffs.push({'diff' : position - clip.position, 'position' : clip.position}, // left side of clip
978  {'diff' : position - (clip.position + (clip.end - clip.start)), 'position' : clip.position + (clip.end - clip.start)}); // right side of clip
979  }
980 
981  // Add transition positions to array
982  for (var index = 0; index < $scope.project.effects.length; index++) {
983  var transition = $scope.project.effects[index];
984 
985  // exit out if this item is in ignore_ids
986  if (ignore_ids.hasOwnProperty(transition.id))
987  continue;
988 
989  diffs.push({'diff' : position - transition.position, 'position' : transition.position}, // left side of transition
990  {'diff' : position - (transition.position + (transition.end - transition.start)), 'position' : transition.position + (transition.end - transition.start)}); // right side of transition
991  }
992 
993  // Add marker positions to array
994  for (var index = 0; index < $scope.project.markers.length; index++) {
995  var marker = $scope.project.markers[index];
996 
997  diffs.push({'diff' : position - marker.position, 'position' : marker.position}, // left side of marker
998  {'diff' : position - (marker.position + (marker.end - marker.start)), 'position' : marker.position + (marker.end - marker.start)}); // right side of marker
999  }
1000 
1001  // Add playhead position to array
1002  var playhead_diff = position - $scope.project.playhead_position;
1003  diffs.push({'diff' : playhead_diff, 'position' : $scope.project.playhead_position });
1004 
1005  // Loop through diffs (and find the smallest one)
1006  for (var diff_index = 0; diff_index < diffs.length; diff_index++) {
1007  var diff = diffs[diff_index].diff;
1008  var position = diffs[diff_index].position;
1009  var abs_diff = Math.abs(diff);
1010 
1011  // Check if this clip is nearby
1012  if (abs_diff < smallest_abs_diff && abs_diff <= threashold) {
1013  // This one is smaller
1014  smallest_diff = diff;
1015  smallest_abs_diff = abs_diff;
1016  snapping_position = position;
1017  }
1018  }
1019  }
1020 
1021  // no nearby found?
1022  if (smallest_diff == 900.0)
1023  smallest_diff = 0.0;
1024 
1025  // Return closest nearby position
1026  return [smallest_diff, snapping_position];
1027  };
1028 
1029  // Show the nearby snapping line
1030  $scope.ShowSnapline = function(position){
1031  if (position != $scope.snapline_position || !$scope.snapline) {
1032  // Only update if value has changed
1033  $scope.$apply(function(){
1034  $scope.snapline_position = position;
1035  $scope.snapline = true;
1036  });
1037  }
1038  };
1039 
1040  // Hide the nearby snapping line
1041  $scope.HideSnapline = function(){
1042  if ($scope.snapline) {
1043  // Only hide if not already hidden
1044  $scope.$apply(function(){
1045  $scope.snapline = false;
1046  });
1047  }
1048  };
1049 
1050  // Find a track JSON object at a given y coordinate (if any)
1051  $scope.GetTrackAtY = function(y){
1052 
1053  // Loop through each layer (looking for the closest track based on Y coordinate)
1054  for (var layer_index = $scope.project.layers.length - 1; layer_index >= 0 ; layer_index--) {
1055  var layer = $scope.project.layers[layer_index];
1056 
1057  // Compare position of track to Y param
1058  if (layer.y > y)
1059  // return first matching layer
1060  return layer;
1061  }
1062 
1063  // no layer found (return top layer... if any)
1064  if ($scope.project.layers.length > 0)
1065  return $scope.project.layers[0];
1066  else
1067  return null;
1068  };
1069 
1070  // Determine which CSS classes are used on a track
1071  $scope.GetTrackStyle = function(lock){
1072 
1073  if (lock)
1074  return "track track_disabled";
1075  else
1076  return "track";
1077  };
1078 
1079  // Apply JSON diff from UpdateManager (this is how the user interface communicates changes
1080  // to the timeline. A change can be an insert, update, or delete. The change is passed in
1081  // as JSON, which represents the change.
1082  $scope.ApplyJsonDiff = function(jsonDiff){
1083 
1084  // Loop through each UpdateAction
1085  for (var action_index = 0; action_index < jsonDiff.length; action_index++) {
1086  var action = jsonDiff[action_index];
1087 
1088  // Iterate through the key levels (looking for a matching element in the $scope.project)
1089  var previous_object = null;
1090  var current_object = $scope.project;
1091  var current_position = 0;
1092  var current_key = "";
1093  for (var key_index = 0; key_index < action.key.length; key_index++) {
1094  var key_value = action.key[key_index];
1095 
1096  // Check the key type
1097  if (key_value.constructor == String) {
1098  // Does the key value exist in scope
1099  if (!current_object.hasOwnProperty(key_value))
1100  // No match, bail out
1101  return false;
1102 
1103  // set current level and previous level
1104  previous_object = current_object;
1105  current_object = current_object[key_value];
1106  current_key = key_value;
1107 
1108  } else if (key_value.constructor == Object) {
1109  // Get the id from the object (if any)
1110  var id = null;
1111  if ("id" in key_value)
1112  id = key_value["id"];
1113 
1114  // Be sure the current_object is an Array
1115  if (current_object.constructor == Array) {
1116  // Filter the current_object for a specific id
1117  current_position = 0;
1118  for (var child_index = 0; child_index < current_object.length; child_index++) {
1119  var child_object = current_object[child_index];
1120 
1121  // Find matching child
1122  if (child_object.hasOwnProperty("id") && child_object.id == id) {
1123  // set current level and previous level
1124  previous_object = current_object;
1125  current_object = child_object;
1126  break; // found child, stop looping
1127  }
1128 
1129  // increment index
1130  current_position++;
1131  }
1132  }
1133  }
1134  }
1135 
1136  // Now that we have a matching object in the $scope.project...
1137  if (current_object){
1138  // INSERT OBJECT
1139  if (action.type == "insert") {
1140 
1141  // Insert action's value into current_object
1142  if (current_object.constructor == Array)
1143  // push new element into array
1144  $scope.$apply(function(){
1145  current_object.push(action.value);
1146  });
1147  else {
1148  // replace the entire value
1149  if (previous_object.constructor == Array) {
1150  // replace entire value in OBJECT
1151  $scope.$apply(function(){
1152  previous_object[current_position] = action.value;
1153  });
1154 
1155  } else if (previous_object.constructor == Object) {
1156  // replace entire value in OBJECT
1157  $scope.$apply(function(){
1158  previous_object[current_key] = action.value;
1159  });
1160  }
1161  }
1162 
1163  } else if (action.type == "update") {
1164  // UPDATE OBJECT
1165  // Update: If action and current object are Objects
1166  if (current_object.constructor == Object && action.value.constructor == Object) {
1167  for (var update_key in action.value)
1168  if (update_key in current_object)
1169  // Only copy over keys that exist in both action and current_object
1170  $scope.$apply(function () {
1171  current_object[update_key] = action.value[update_key];
1172  });
1173  }
1174  else {
1175  // replace the entire value
1176  if (previous_object.constructor == Array) {
1177  // replace entire value in OBJECT
1178  $scope.$apply(function(){
1179  previous_object[current_position] = action.value;
1180  });
1181 
1182  } else if (previous_object.constructor == Object) {
1183  // replace entire value in OBJECT
1184  $scope.$apply(function(){
1185  previous_object[current_key] = action.value;
1186  });
1187  }
1188  }
1189 
1190 
1191  } else if (action.type == "delete") {
1192  // DELETE OBJECT
1193  // delete current object from it's parent (previous object)
1194  $scope.$apply(function(){
1195  previous_object.splice(current_position, 1);
1196  });
1197  }
1198 
1199  // Resize timeline if it's too small to contain all clips
1200  $scope.ResizeTimeline();
1201 
1202  // Re-sort clips and transitions array
1203  $scope.SortItems();
1204 
1205  // Re-index Layer Y values
1206  $scope.UpdateLayerIndex();
1207 
1208  // Lock / unlock any items
1209  $scope.LockItems();
1210  }
1211  }
1212 
1213  // return true
1214  return true;
1215  };
1216 
1217 
1218  // Load entire project data JSON from UpdateManager (i.e. user opened an existing project)
1219  $scope.LoadJson = function(EntireProjectJson){
1220 
1221  $scope.$apply(function(){
1222  // Update the entire JSON object for the entire timeline
1223  $scope.project = EntireProjectJson.value;
1224 
1225  // Un-select any selected items
1226  $scope.SelectClip("", true);
1227  });
1228 
1229  // Re-sort clips and transitions array
1230  $scope.SortItems;
1231 
1232  // Re-index Layer Y values
1233  $scope.UpdateLayerIndex();
1234 
1235  // Lock / unlock any items
1236  $scope.LockItems();
1237 
1238  // return true
1239  return true;
1240  };
1241 
1242  // Lock and unlock items
1243  $scope.LockItems = function(){
1244 
1245  // Enable all items
1246  //$(".clip").draggable("enable")
1247 
1248  // Disable any locked items
1249  // for (layer in $scope.project.layers)
1250  // {
1251  // timeline.qt_log(layer);
1252  // }
1253  };
1254 
1255 // ############# END QT FUNCTIONS #################### //
1256 
1257 
1258 
1259 // ############ DEBUG STUFFS ################## //
1260 
1261  $scope.ToggleDebug = function() {
1262  if ($scope.debug == true)
1263  $scope.debug = false;
1264  else
1265  $scope.debug = true;
1266  };
1267 
1268  // Debug method to add clips to the $scope
1269  $scope.addClips = function(numClips) {
1270  startNum = $scope.project.clips.length + 1;
1271  positionNum = 0;
1272  for (var x = 0; x < parseInt(numClips); x++) {
1273  $scope.project.clips.push({
1274  id : x.toString(),
1275  layer : 0,
1276  image : './media/images/thumbnail.png',
1277  locked : false,
1278  duration : 5,
1279  start : 0,
1280  end : 5,
1281  position : positionNum,
1282  title : 'Clip B',
1283  effects : [],
1284  images : {start: 3, end: 7},
1285  show_audio : false,
1286  alpha: { Points: [] },
1287  location_x: { Points: [] },
1288  location_y: { Points: [] },
1289  scale_x: { Points: [] },
1290  scale_y: { Points: [] },
1291  rotation: { Points: [] },
1292  time: { Points: [] },
1293  volume: { Points: [] }
1294  });
1295  startNum++;
1296  positionNum+=5;
1297  };
1298 
1299  $scope.numClips = "";
1300 
1301  };
1302 
1303  // Debug method to add effects to a clip's $scope
1304  $scope.addEffect = function(clipNum){
1305  //find the clip in the json data
1306  elm = findElement($scope.project.clips, "number", clipNum);
1307  elm.effects.push({
1308  effect : 'Old Movie',
1309  icon : 'om.png'
1310  });
1311  $scope.clipNum = "";
1312 
1313  };
1314 
1315  // Debug method to add a marker to the $scope
1316  $scope.addMarker = function(markLoc){
1317  $scope.project.markers.push({
1318  location: parseInt(markLoc),
1319  icon: 'blue.png'
1320  });
1321  $scope.markLoc = "";
1322  };
1323 
1324  // Debug method to change a clip's image
1325  $scope.changeImage = function(startImage){
1326  console.log(startImage);
1327  $scope.project.clips[2].images.start=startImage;
1328  $scope.startImage = "";
1329  };
1330 
1331 });
jQuery fx start
Definition: jquery.js:9518
jQuery fn offset
Definition: jquery.js:9546
if(window.getComputedStyle)
Definition: jquery.js:7083
function findElement(arr, propName, propValue)
Definition: functions.js:31
var bounding_box
Definition: functions.js:179
Definition: clip.py:1
function moveBoundingBox(scope, previous_x, previous_y, x_offset, y_offset, left, top)
Definition: functions.js:214
Definition: effect.py:1
var App
Definition: app.js:31
function setBoundingBox(item)
Definition: functions.js:182
var a[b] e
Tween propHooks scrollTop
Definition: jquery.js:9274
Definition: marker.py:1
function drawAudio(scope, clip_id)
Definition: functions.js:52
function secondsToTime(secs, fps_num, fps_den)
Definition: functions.js:120