base/Frame.js

  1. /*
  2. * Copyright 2020 WICKLETS LLC
  3. *
  4. * This file is part of Wick Engine.
  5. *
  6. * Wick Engine is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Wick Engine is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Wick Engine. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. /**
  20. * A class representing a frame.
  21. */
  22. Wick.Frame = class extends Wick.Tickable {
  23. /**
  24. * Create a new frame.
  25. * @param {number} start - The start of the frame. Optional, defaults to 1.
  26. * @param {number} end - The end of the frame. Optional, defaults to be the same as start.
  27. */
  28. constructor(args) {
  29. if (!args) args = {};
  30. super(args);
  31. this.start = args.start || 1;
  32. this.end = args.end || this.start;
  33. this._soundAssetUUID = null;
  34. this._soundID = null;
  35. this._soundVolume = 1.0;
  36. this._soundLoop = false;
  37. this._soundStart = 0;
  38. this._originalLayerIndex = -1;
  39. }
  40. _serialize(args) {
  41. var data = super._serialize(args);
  42. data.start = this.start;
  43. data.end = this.end;
  44. data.sound = this._soundAssetUUID;
  45. data.soundVolume = this._soundVolume;
  46. data.soundLoop = this._soundLoop;
  47. data.soundStart = this._soundStart;
  48. data.originalLayerIndex = this.layerIndex !== -1 ? this.layerIndex : this._originalLayerIndex;
  49. return data;
  50. }
  51. _deserialize(data) {
  52. super._deserialize(data);
  53. this.start = data.start;
  54. this.end = data.end;
  55. this._soundAssetUUID = data.sound;
  56. this._soundVolume = data.soundVolume === undefined ? 1.0 : data.soundVolume;
  57. this._soundLoop = data.soundLoop === undefined ? false : data.soundLoop;
  58. this._soundStart = data.soundStart === undefined ? 0 : data.soundStart;
  59. this._originalLayerIndex = data.originalLayerIndex;
  60. }
  61. get classname() {
  62. return 'Frame';
  63. }
  64. /**
  65. * The length of the frame.
  66. * @type {number}
  67. */
  68. get length() {
  69. return this.end - this.start + 1;
  70. }
  71. set length(length) {
  72. length = Math.max(1, length);
  73. var diff = length - this.length;
  74. this.end += diff;
  75. }
  76. /**
  77. * The midpoint of the frame.
  78. * @type {number}
  79. */
  80. get midpoint() {
  81. return this.start + (this.end - this.start) / 2;
  82. }
  83. /**
  84. * Is true if the frame is currently visible.
  85. * @type {boolean}
  86. */
  87. get onScreen() {
  88. if (!this.parent) return true;
  89. return this.inPosition(this.parentTimeline.playheadPosition) && this.parentClip.onScreen;
  90. }
  91. /**
  92. * The sound attached to the frame.
  93. * @type {Wick.SoundAsset}
  94. */
  95. get sound() {
  96. var uuid = this._soundAssetUUID;
  97. return uuid ? this.project.getAssetByUUID(uuid) : null;
  98. }
  99. set sound(soundAsset) {
  100. if (!soundAsset) {
  101. this.removeSound();
  102. return;
  103. }
  104. this._soundAssetUUID = soundAsset.uuid;
  105. }
  106. /**
  107. * The volume of the sound attached to the frame.
  108. * @type {number}
  109. */
  110. get soundVolume() {
  111. return this._soundVolume
  112. }
  113. set soundVolume(soundVolume) {
  114. this._soundVolume = soundVolume;
  115. }
  116. /**
  117. * Whether or not the sound loops.
  118. * @type {boolean}
  119. */
  120. get soundLoop() {
  121. return this._soundLoop;
  122. }
  123. set soundLoop(soundLoop) {
  124. this._soundLoop = soundLoop;
  125. }
  126. /**
  127. * True if this frame should currently be onion skinned.
  128. */
  129. get onionSkinned () {
  130. if (!this.project || !this.project.onionSkinEnabled) {
  131. return false;
  132. }
  133. // Don't onion skin if we're in the playhead's position.
  134. var playheadPosition = this.project.focus.timeline.playheadPosition;
  135. if (this.inPosition(playheadPosition)) {
  136. return false;
  137. }
  138. // Determine if we're in onion skinning range.
  139. var onionSkinSeekBackwards = this.project.onionSkinSeekBackwards;
  140. var onionSkinSeekForwards = this.project.onionSkinSeekForwards;
  141. return this.inRange(playheadPosition - onionSkinSeekBackwards,
  142. playheadPosition + onionSkinSeekForwards);
  143. }
  144. /**
  145. * Removes the sound attached to this frame.
  146. */
  147. removeSound() {
  148. this._soundAssetUUID = null;
  149. }
  150. /**
  151. * Plays the sound attached to this frame.
  152. */
  153. playSound() {
  154. if (!this.sound) {
  155. return;
  156. }
  157. var options = {
  158. seekMS: this.playheadSoundOffsetMS + this.soundStart,
  159. volume: this.soundVolume,
  160. loop: this.soundLoop,
  161. frame: this,
  162. };
  163. this._soundID = this.project.playSoundFromAsset(this.sound, options);
  164. }
  165. /**
  166. * Stops the sound attached to this frame.
  167. */
  168. stopSound() {
  169. if (this.sound) {
  170. this.sound.stop(this._soundID);
  171. this._soundID = null;
  172. }
  173. }
  174. /**
  175. * Check if the sound on this frame is playing.
  176. * @returns {boolean} true if the sound is playing
  177. */
  178. isSoundPlaying() {
  179. return this._soundID !== null;
  180. }
  181. /**
  182. * The amount of time, in milliseconds, that the frame's sound should play before stopping.
  183. * @type {number}
  184. */
  185. get playheadSoundOffsetMS() {
  186. var offsetFrames = this.parentTimeline.playheadPosition - this.start;
  187. var offsetMS = (1000 / this.project.framerate) * offsetFrames;
  188. return offsetMS;
  189. }
  190. /**
  191. * The amount of time the sound playing should be offset, in milliseconds. If this is 0,
  192. * the sound plays normally. A negative value means the sound should start at a later point
  193. * in the track. THIS DOES NOT DETERMINE WHEN A SOUND PLAYS.
  194. * @type {number}
  195. */
  196. get soundStart() {
  197. return this._soundStart;
  198. }
  199. set soundStart(val) {
  200. this._soundStart = val;
  201. }
  202. /**
  203. * When should the sound start, in milliseconds.
  204. * @type {number}
  205. */
  206. get soundStartMS() {
  207. return (1000 / this.project.framerate) * (this.start - 1);
  208. }
  209. /**
  210. * When should the sound end, in milliseconds.
  211. * @type {number}
  212. */
  213. get soundEndMS() {
  214. return (1000 / this.project.framerate) * this.end;
  215. }
  216. /**
  217. * Returns the frame's start position in relation to the root timeline.
  218. */
  219. get projectFrameStart () {
  220. if (this.parentClip.isRoot) {
  221. return this.start;
  222. } else {
  223. let val = this.start + this.parentClip.parentFrame.projectFrameStart - 1;
  224. return val;
  225. }
  226. }
  227. /**
  228. * The paths on the frame.
  229. * @type {Wick.Path[]}
  230. */
  231. get paths() {
  232. return this.getChildren('Path');
  233. }
  234. /**
  235. * The paths that are text and have identifiers, for dynamic text.
  236. * @type {Wick.Path[]}
  237. */
  238. get dynamicTextPaths() {
  239. return this.paths.filter(path => {
  240. return path.isDynamicText;
  241. });
  242. }
  243. /**
  244. * The clips on the frame.
  245. * @type {Wick.Clip[]}
  246. */
  247. get clips() {
  248. return this.getChildren(['Clip', 'Button']);
  249. }
  250. /**
  251. * The drawable objectson the frame.
  252. * @type {Wick.Base[]}
  253. */
  254. get drawable() {
  255. return this.getChildren(['Clip', 'Button', 'Path']);
  256. }
  257. /**
  258. * The tweens on this frame.
  259. * @type {Wick.Tween[]}
  260. */
  261. get tweens() {
  262. // Ensure no tweens are outside of this frame's length.
  263. var tweens = this.getChildren('Tween')
  264. tweens.forEach(tween => {
  265. tween.restrictToFrameSize();
  266. });
  267. return this.getChildren('Tween');
  268. }
  269. /**
  270. * True if there are clips or paths on the frame.
  271. * @type {boolean}
  272. */
  273. get contentful () {
  274. return this.paths.filter(path => {
  275. return !path.view.item.data._isPlaceholder;
  276. }).length > 0 || this.clips.length > 0;
  277. }
  278. /**
  279. * The index of the parent layer.
  280. * @type {number}
  281. */
  282. get layerIndex() {
  283. return this.parentLayer ? this.parentLayer.index : -1;
  284. }
  285. /**
  286. * The index of the layer that this frame last belonged to. Used when copying and pasting frames.
  287. * @type {number}
  288. */
  289. get originalLayerIndex() {
  290. return this._originalLayerIndex;
  291. }
  292. /**
  293. * Removes this frame from its parent layer.
  294. */
  295. remove() {
  296. this.parent.removeFrame(this);
  297. }
  298. /**
  299. * True if the playhead is on this frame.
  300. * @param {number} playheadPosition - the position of the playhead.
  301. * @return {boolean}
  302. */
  303. inPosition(playheadPosition) {
  304. return this.start <= playheadPosition &&
  305. this.end >= playheadPosition;
  306. }
  307. /**
  308. * True if the frame exists within the given range.
  309. * @param {number} start - the start of the range to check.
  310. * @param {number} end - the end of the range to check.
  311. * @return {boolean}
  312. */
  313. inRange(start, end) {
  314. return this.inPosition(start) ||
  315. this.inPosition(end) ||
  316. (this.start >= start && this.start <= end) ||
  317. (this.end >= start && this.end <= end);
  318. }
  319. /**
  320. * True if the frame is contained fully within a given range.
  321. * @param {number} start - the start of the range to check.
  322. * @param {number} end - the end of the range to check.
  323. * @return {boolean}
  324. */
  325. containedWithin(start, end) {
  326. return this.start >= start && this.end <= end;
  327. }
  328. /**
  329. * The number of frames that this frame is from a given playhead position.
  330. * @param {number} playheadPosition
  331. */
  332. distanceFrom(playheadPosition) {
  333. // playhead position is inside frame, distance is zero.
  334. if (this.start <= playheadPosition && this.end >= playheadPosition) {
  335. return 0;
  336. }
  337. // otherwise, find the distance from the nearest end
  338. if (this.start >= playheadPosition) {
  339. return this.start - playheadPosition;
  340. } else if (this.end <= playheadPosition) {
  341. return playheadPosition - this.end;
  342. }
  343. }
  344. /**
  345. * Add a clip to the frame.
  346. * @param {Wick.Clip} clip - the clip to add.
  347. */
  348. addClip(clip) {
  349. if (clip.parent) {
  350. clip.remove();
  351. }
  352. this.addChild(clip);
  353. // Pre-render the clip's frames
  354. // (this fixes an issue where clips created from ClipAssets would be "missing" frames)
  355. clip.timeline.getAllFrames(true).forEach(frame => {
  356. frame.view.render();
  357. });
  358. }
  359. /**
  360. * Remove a clip from the frame.
  361. * @param {Wick.Clip} clip - the clip to remove.
  362. */
  363. removeClip(clip) {
  364. this.removeChild(clip);
  365. }
  366. /**
  367. * Add a path to the frame.
  368. * @param {Wick.Path} path - the path to add.
  369. */
  370. addPath(path) {
  371. if (path.parent) {
  372. path.remove();
  373. }
  374. this.addChild(path);
  375. }
  376. /**
  377. * Remove a path from the frame.
  378. * @param {Wick.Path} path - the path to remove.
  379. */
  380. removePath(path) {
  381. this.removeChild(path);
  382. }
  383. /**
  384. * Add a tween to the frame.
  385. * @param {Wick.Tween} tween - the tween to add.
  386. */
  387. addTween(tween) {
  388. // New tweens eat existing tweens.
  389. var otherTween = this.getTweenAtPosition(tween.playheadPosition);
  390. if (otherTween) {
  391. otherTween.remove();
  392. }
  393. this.addChild(tween);
  394. tween.restrictToFrameSize();
  395. }
  396. /**
  397. * Automatically creates a tween at the current playhead position. Converts all objects into one clip if needed.
  398. */
  399. createTween() {
  400. // Don't make a tween if one already exits
  401. var playheadPosition = this.getRelativePlayheadPosition();
  402. if (this.getTweenAtPosition(playheadPosition)) {
  403. return;
  404. }
  405. // If more than one object exists on the frame, or if there is only one path, create a clip from those objects
  406. var clips = this.clips;
  407. var paths = this.paths;
  408. if ((clips.length === 0 && paths.length === 1) || (clips.length + paths.length) > 1) {
  409. var allDrawables = paths.concat(clips);
  410. var center = this.project.selection.view._getObjectsBounds(allDrawables).center;
  411. var clip = new Wick.Clip({
  412. transformation: new Wick.Transformation({
  413. x: center.x,
  414. y: center.y,
  415. }),
  416. });
  417. this.addClip(clip);
  418. clip.addObjects(allDrawables);
  419. }
  420. // Create the tween (if there's not already a tween at the current playhead position)
  421. var clip = this.clips[0];
  422. this.addTween(new Wick.Tween({
  423. playheadPosition: playheadPosition,
  424. transformation: clip ? clip.transformation.copy() : new Wick.Transformation(),
  425. }));
  426. }
  427. /**
  428. * Remove a tween from the frame.
  429. * @param {Wick.Tween} tween - the tween to remove.
  430. */
  431. removeTween(tween) {
  432. this.removeChild(tween);
  433. }
  434. /**
  435. * Remove all tweens from this frame.
  436. */
  437. removeAllTweens(tween) {
  438. this.tweens.forEach(tween => {
  439. tween.remove();
  440. });
  441. }
  442. /**
  443. * Get the tween at the given playhead position. Returns null if there is no tween.
  444. * @param {number} playheadPosition - the playhead position to look for tweens at.
  445. * @returns {Wick.Tween || null} the tween at the given playhead position.
  446. */
  447. getTweenAtPosition(playheadPosition) {
  448. return this.tweens.find(tween => {
  449. return tween.playheadPosition === playheadPosition;
  450. }) || null;
  451. }
  452. /**
  453. * Returns the tween at the current playhead position, if one exists on the frame. Null otherwise.
  454. * @returns {Wick.Tween || null}
  455. */
  456. getTweenAtCurrentPlayheadPosition() {
  457. let playheadPosition = this.getRelativePlayheadPosition();
  458. return this.getTweenAtPosition(playheadPosition);
  459. }
  460. /**
  461. * The tween being used to transform the objects on the frame.
  462. * @returns {Wick.Tween || null} tween - the active tween. Null if there is no active tween.
  463. */
  464. getActiveTween() {
  465. if (!this.parentTimeline) return null;
  466. var playheadPosition = this.getRelativePlayheadPosition();
  467. var tween = this.getTweenAtPosition(playheadPosition);
  468. if (tween) {
  469. return tween;
  470. }
  471. var seekBackwardsTween = this.seekTweenBehind(playheadPosition);
  472. var seekForwardsTween = this.seekTweenInFront(playheadPosition);
  473. if (seekBackwardsTween && seekForwardsTween) {
  474. return Wick.Tween.interpolate(seekBackwardsTween, seekForwardsTween, playheadPosition);
  475. } else if (seekForwardsTween) {
  476. return seekForwardsTween;
  477. } else if (seekBackwardsTween) {
  478. return seekBackwardsTween;
  479. } else {
  480. return null;
  481. }
  482. }
  483. /**
  484. * Applies the transformation of current tween to the objects on the frame.
  485. */
  486. applyTweenTransforms() {
  487. var tween = this.getActiveTween();
  488. if (tween) {
  489. this.clips.forEach(clip => {
  490. tween.applyTransformsToClip(clip);
  491. });
  492. }
  493. }
  494. /**
  495. * Applies single frame positions to timelines if necessary.
  496. */
  497. applyClipSingleFramePositions () {
  498. this.clips.forEach(clip => {
  499. clip.applySingleFramePosition();
  500. });
  501. }
  502. /**
  503. * Update all clip timelines for their animation type.
  504. */
  505. updateClipTimelinesForAnimationType () {
  506. this.clips.forEach(clip => {
  507. clip.updateTimelineForAnimationType();
  508. })
  509. }
  510. /**
  511. * The asset of the sound attached to this frame, if one exists
  512. * @returns {Wick.Asset[]}
  513. */
  514. getLinkedAssets() {
  515. var linkedAssets = [];
  516. if (this.sound) {
  517. linkedAssets.push(this.sound);
  518. }
  519. return linkedAssets;
  520. }
  521. /**
  522. * Cut this frame in half using the parent timeline's playhead position.
  523. */
  524. cut() {
  525. // Can't cut a frame that doesn't beolong to a timeline + layer
  526. if (!this.parentTimeline) return;
  527. // Can't cut a frame with length 1
  528. if (this.length === 1) return;
  529. // Can't cut a frame that isn't under the playhead
  530. var playheadPosition = this.parentTimeline.playheadPosition;
  531. if (!this.inPosition(playheadPosition)) return;
  532. // Create right half (leftover) frame
  533. var rightHalf = this.copy();
  534. rightHalf.identifier = null;
  535. rightHalf.removeSound();
  536. rightHalf.removeAllTweens();
  537. rightHalf.start = playheadPosition = playheadPosition;
  538. // Cut this frame shorter
  539. this.end = playheadPosition - 1;
  540. // Add right frame
  541. this.parentLayer.addFrame(rightHalf);
  542. }
  543. /**
  544. * Extend this frame by one and push all frames right of this frame to the right.
  545. */
  546. extendAndPushOtherFrames() {
  547. this.parentLayer.getFramesInRange(this.end + 1, Infinity).forEach(frame => {
  548. frame.start += 1;
  549. frame.end += 1;
  550. });
  551. this.end += 1;
  552. }
  553. /**
  554. * Shrink this frame by one and pull all frames left of this frame to the left.
  555. */
  556. shrinkAndPullOtherFrames() {
  557. if (this.length === 1) return;
  558. this.parentLayer.getFramesInRange(this.end + 1, Infinity).forEach(frame => {
  559. frame.start -= 1;
  560. frame.end -= 1;
  561. });
  562. this.end -= 1;
  563. }
  564. /**
  565. * Import SVG data into this frame. SVGs containing mulitple paths will be split into multiple Wick Paths.
  566. * @param {string} svg - the SVG data to parse and import.
  567. */
  568. /*
  569. importSVG (svg) {
  570. this.view.importSVG(svg);
  571. }
  572. */
  573. /**
  574. * Get the position of this frame in relation to the parent timeline's playhead position.
  575. * @returns {number}
  576. */
  577. getRelativePlayheadPosition() {
  578. return this.parentTimeline.playheadPosition - this.start + 1;
  579. }
  580. /**
  581. * Find the first tween on this frame that exists behind the given playhead position.
  582. * @returns {Wick.Tween}
  583. */
  584. seekTweenBehind(playheadPosition) {
  585. var seekBackwardsPosition = playheadPosition;
  586. var seekBackwardsTween = null;
  587. while (seekBackwardsPosition > 0) {
  588. seekBackwardsTween = this.getTweenAtPosition(seekBackwardsPosition);
  589. seekBackwardsPosition--;
  590. if (seekBackwardsTween) break;
  591. }
  592. return seekBackwardsTween;
  593. }
  594. /**
  595. * Find the first tween on this frame that exists past the given playhead position.
  596. * @returns {Wick.Tween}
  597. */
  598. seekTweenInFront(playheadPosition) {
  599. var seekForwardsPosition = playheadPosition;
  600. var seekForwardsTween = null;
  601. while (seekForwardsPosition <= this.end) {
  602. seekForwardsTween = this.getTweenAtPosition(seekForwardsPosition);
  603. seekForwardsPosition++;
  604. if (seekForwardsTween) break;
  605. }
  606. return seekForwardsTween;
  607. }
  608. _onInactive() {
  609. super._onInactive();
  610. this._tickChildren();
  611. }
  612. _onActivated() {
  613. super._onActivated();
  614. this.playSound();
  615. this._tickChildren();
  616. }
  617. _onActive() {
  618. super._onActive();
  619. this._tickChildren();
  620. }
  621. _onDeactivated() {
  622. super._onDeactivated();
  623. this.stopSound();
  624. this._tickChildren();
  625. }
  626. _tickChildren() {
  627. this.clips.forEach(clip => {
  628. clip.tick();
  629. });
  630. }
  631. _attachChildClipReferences() {
  632. this.clips.forEach(clip => {
  633. if (clip.identifier) {
  634. this[clip.identifier] = clip;
  635. clip._attachChildClipReferences();
  636. }
  637. });
  638. }
  639. }