summaryrefslogtreecommitdiff
path: root/editty/segment.py
diff options
context:
space:
mode:
Diffstat (limited to 'editty/segment.py')
-rw-r--r--editty/segment.py290
1 files changed, 290 insertions, 0 deletions
diff --git a/editty/segment.py b/editty/segment.py
new file mode 100644
index 0000000..dd6faab
--- /dev/null
+++ b/editty/segment.py
@@ -0,0 +1,290 @@
1# -*- coding: utf-8 -*-
2# Copyright (C) 2019 James E. Blair <corvus@gnu.org>
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import logging
18import uuid
19
20import urwid
21
22from editty.source import Frame
23
24class Segment(object):
25 def __init__(self):
26 super().__init__()
27 self.visible_cursor = True
28
29 def __repr__(self):
30 return '<%s from %s to %s (%s seconds)>' % (self.__class__.__name__, self.start, self.end, self.duration)
31
32 @classmethod
33 def fromJSON(cls, data, sources):
34 if data['type'] == 'clip':
35 ret = Clip.fromJSON(data, sources)
36 elif data['type'] == 'freeze-frame':
37 ret = FreezeFrame.fromJSON(data, sources)
38 elif data['type'] == 'black':
39 ret = Black.fromJSON(data, sources)
40 elif data['type'] == 'dissolve':
41 ret = Dissolve.fromJSON(data, sources)
42 else:
43 raise Exception("Unknown segment type: %s" % data.get('type'))
44 ret.visible_cursor = data.get('visible_cursor', True)
45 return ret
46
47 def toJSON(self):
48 return dict(visible_cursor=self.visible_cursor)
49
50 def updateCopy(self, copy):
51 copy.visible_cursor = self.visible_cursor
52 return copy
53
54class Clip(Segment):
55 def __init__(self, source, start, end, **kw):
56 super().__init__(**kw)
57 self.source = source
58 self.start = start
59 self.end = end
60
61 def toJSON(self):
62 d = super().toJSON()
63 d.update(dict(type='clip',
64 source=self.source.uuid,
65 start=self.start,
66 end=self.end))
67 return d
68
69 @classmethod
70 def fromJSON(cls, data, sources):
71 return Clip(sources[data['source']],
72 data['start'],
73 data['end'])
74
75 @property
76 def duration(self):
77 return self.end - self.start
78
79 def copy(self):
80 ret = Clip(self.source, self.start, self.end)
81 return super().updateCopy(ret)
82
83 def __iter__(self):
84 for fi in self.source.getFrames(self.start, self.end):
85 yield (fi[0]-self.start, fi[1])
86
87class Still(Segment):
88 def __init__(self, duration):
89 super(Still, self).__init__()
90 self.duration = duration
91
92 @property
93 def start(self):
94 return 0.0
95
96 @start.setter
97 def start(self, start):
98 self.duration -= start
99
100 @property
101 def end(self):
102 return self.duration
103
104 @end.setter
105 def end(self, end):
106 delta = end - self.duration
107 self.duration -= delta
108
109class FreezeFrame(Still):
110 def __init__(self, source, timecode, duration):
111 super(FreezeFrame, self).__init__(duration)
112 self.source = source
113 self.start = 0.0
114 self.timecode = timecode
115 self.end = duration
116
117 def toJSON(self):
118 return dict(type='freeze-frame',
119 source=self.source.uuid,
120 timecode=self.timecode,
121 duration=self.end)
122
123 @classmethod
124 def fromJSON(cls, data, sources):
125 return FreezeFrame(sources[data['source']],
126 data['timecode'],
127 data['duration'])
128
129 def copy(self):
130 ret = FreezeFrame(self.source, self.timecode, self.duration)
131 return super().updateCopy(ret)
132
133class Black(Still):
134 def __init__(self, duration):
135 super(Black, self).__init__(duration)
136 self.start = 0.0
137 self.end = duration
138
139 def toJSON(self):
140 return dict(type='black',
141 duration=self.end)
142
143 @classmethod
144 def fromJSON(cls, data, sources):
145 return Black(data['duration'])
146
147 def copy(self):
148 ret = Black(self.duration)
149 return super().updateCopy(ret)
150
151class Dissolve(Segment):
152 def __init__(self, start_source, start_timecode, end_source, end_timecode, duration, **kw):
153 super().__init__(**kw)
154 self.log = logging.getLogger('program')
155 self.start_source = start_source
156 self.start_timecode = start_timecode
157 self.end_source = end_source
158 self.end_timecode = end_timecode
159 self.duration = duration
160 self._cache = []
161 self._update()
162
163 def copy(self):
164 ret = Dissolve(self.start_source, self.start_timecode,
165 self.end_source, self.end_timecode, self.duration)
166 return super().updateCopy(ret)
167
168 @classmethod
169 def fromJSON(cls, data, sources):
170 return Dissolve(sources[data['start_source']],
171 data['start_timecode'],
172 sources[data['end_source']],
173 data['end_timecode'],
174 data['duration'])
175
176 def toJSON(self):
177 d = super().toJSON()
178 d.update(dict(type='dissolve',
179 start_source=self.start_source.uuid,
180 start_timecode=self.start_timecode,
181 end_source=self.end_source.uuid,
182 end_timecode=self.end_timecode,
183 duration=self.duration))
184 return d
185
186 @property
187 def start(self):
188 return 0.0
189
190 @start.setter
191 def start(self, start):
192 self.duration -= start
193 self._update()
194
195 @property
196 def end(self):
197 return self.duration
198
199 @end.setter
200 def end(self, end):
201 delta = end - self.duration
202 self.duration -= delta
203 self._update()
204
205 def __iter__(self):
206 for x in self._cache:
207 yield x
208
209 def _update(self):
210 start = list(self.start_source.getFrames(self.start_timecode, self.start_timecode))[0]
211 end = list(self.end_source.getFrames(self.end_timecode, self.end_timecode))[0]
212 self._cache = []
213 num_frames = int(self.duration * 10)
214 for tween_index in range(num_frames):
215 tween_frame = self._render(start[1], end[1], tween_index / (self.duration*10.0))
216 self._cache.append((self.start+(tween_index/10.0), tween_frame))
217
218 def _fixrgb(self, rgb, background):
219 ret = []
220 for i in range(len(rgb)):
221 if rgb[i] is None:
222 ret.append(background[i])
223 else:
224 ret.append(rgb[i])
225 return ret
226
227 def _render(self, start, end, progress):
228 line_list = []
229 attr_list = []
230 line_text = ''
231 line_attrs = []
232 current_attr = [None, 0]
233 current_rgb = None
234 current_props = None
235 ret_content = []
236 background = urwid.AttrSpec('light gray', 'black')
237 for line_i in range(len(start.content)):
238 ret_line = []
239 for char_i in range(len(start.content[line_i])):
240 if line_i == 1 and char_i == 0:
241 self.log.debug("tween %s %s", start.content[line_i][char_i], end.content[line_i][char_i])
242 oldattr, oldcs, oldchar = start.content[line_i][char_i]
243 newattr, newcs, newchar = end.content[line_i][char_i]
244 if oldattr is None:
245 oldrgb = background.get_rgb_values()
246 else:
247 oldrgb = oldattr.get_rgb_values()
248 oldrgb = self._fixrgb(oldrgb, background.get_rgb_values())
249 if newattr is None:
250 newrgb = background.get_rgb_values()
251 else:
252 newrgb = newattr.get_rgb_values()
253 newrgb = self._fixrgb(newrgb, background.get_rgb_values())
254 if newchar == b' ' and oldchar != b' ':
255 char = oldchar
256 charattr = oldattr
257 newrgb = newrgb[3:]*2
258 elif oldchar == b' ' and newchar != b' ':
259 char = newchar
260 charattr = newattr
261 oldrgb = oldrgb[3:]*2
262 elif progress >= 0.5:
263 char = newchar
264 charattr = newattr
265 else:
266 char = oldchar
267 charattr = oldattr
268 rgb = []
269 props = []
270 if charattr and charattr.bold:
271 props.append('bold')
272 if charattr and charattr.underline:
273 props.append('underline')
274 if charattr and charattr.standout:
275 props.append('standout')
276 if charattr and charattr.blink:
277 props.append('blink')
278 for x in range(len(oldrgb)):
279 rgb.append(int(((newrgb[x]-oldrgb[x])*progress)+oldrgb[x])>>4)
280 fg = ', '.join(props + ['#%x%x%x' % tuple(rgb[:3])])
281 bg = '#%x%x%x' % tuple(rgb[3:])
282 attr = urwid.AttrSpec(fg, bg)
283 ret_line.append((attr, oldcs, char))
284 ret_content.append(ret_line)
285 if progress > 0.5:
286 which = end
287 else:
288 which = start
289 frame = Frame(None, which.timeline_color, content=ret_content, cursor=which.cursor)
290 return frame