etro 0.8.1 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/nodejs.yml +5 -2
- package/.github/workflows/shipjs-trigger.yml +29 -0
- package/CHANGELOG.md +38 -14
- package/CONTRIBUTING.md +1 -0
- package/README.md +1 -1
- package/dist/etro-cjs.js +53 -6446
- package/dist/etro-iife.js +53 -6446
- package/dist/layer/audio-source.d.ts +1 -2
- package/dist/movie.d.ts +3 -1
- package/karma.conf.js +17 -1
- package/package.json +8 -10
- package/scripts/gen-effect-samples.html +27 -18
- package/scripts/save-effect-samples.js +6 -1
- package/ship.config.js +80 -0
- package/src/effect/stack.ts +3 -2
- package/src/layer/audio-source.ts +4 -5
- package/src/layer/visual.ts +7 -1
- package/src/movie.ts +53 -27
- package/src/util.ts +1 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { AudioContext, IAudioNode } from 'standardized-audio-context';
|
|
2
1
|
import { Base, BaseOptions } from './base';
|
|
3
2
|
declare type Constructor<T> = new (...args: unknown[]) => T;
|
|
4
3
|
interface AudioSource extends Base {
|
|
5
4
|
readonly source: HTMLMediaElement;
|
|
6
|
-
readonly audioNode:
|
|
5
|
+
readonly audioNode: AudioNode;
|
|
7
6
|
playbackRate: number;
|
|
8
7
|
/** The audio source node for the media */
|
|
9
8
|
sourceStartTime: number;
|
package/dist/movie.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module movie
|
|
3
3
|
*/
|
|
4
|
-
import { AudioContext } from 'standardized-audio-context';
|
|
5
4
|
import { Dynamic } from './util';
|
|
6
5
|
import { Base as BaseLayer } from './layer/index';
|
|
7
6
|
import { Base as BaseEffect } from './effect/index';
|
|
8
7
|
declare global {
|
|
8
|
+
interface Window {
|
|
9
|
+
webkitAudioContext: typeof AudioContext;
|
|
10
|
+
}
|
|
9
11
|
interface HTMLCanvasElement {
|
|
10
12
|
captureStream(frameRate?: number): MediaStream;
|
|
11
13
|
}
|
package/karma.conf.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Karma configuration
|
|
2
2
|
// Generated on Thu Sep 19 2019 02:05:06 GMT-0400 (Eastern Daylight Time)
|
|
3
3
|
|
|
4
|
+
process.env.CHROME_BIN = require('puppeteer').executablePath()
|
|
5
|
+
|
|
4
6
|
module.exports = function (config) {
|
|
5
7
|
config.set({
|
|
6
8
|
|
|
@@ -49,7 +51,21 @@ module.exports = function (config) {
|
|
|
49
51
|
|
|
50
52
|
// start these browsers
|
|
51
53
|
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
|
52
|
-
browsers: ['
|
|
54
|
+
browsers: ['FirefoxHeadless'],
|
|
55
|
+
|
|
56
|
+
customLaunchers: {
|
|
57
|
+
'FirefoxHeadless': {
|
|
58
|
+
base: 'Firefox',
|
|
59
|
+
flags: ['-headless'],
|
|
60
|
+
prefs: {
|
|
61
|
+
'network.proxy.type': 0
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
client: {
|
|
67
|
+
captureConsole: true
|
|
68
|
+
},
|
|
53
69
|
|
|
54
70
|
// Continuous Integration mode
|
|
55
71
|
// if true, Karma captures browsers, runs the tests and exits
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "etro",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "An extendable video-editing framework for the browser and Node",
|
|
5
5
|
"browser": "dist/etro-cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
"example": "examples",
|
|
10
10
|
"test": "spec"
|
|
11
11
|
},
|
|
12
|
-
"dependencies": {
|
|
13
|
-
"standardized-audio-context": "^25.1.13"
|
|
14
|
-
},
|
|
15
12
|
"devDependencies": {
|
|
16
13
|
"@types/dom-mediacapture-record": "^1.0.7",
|
|
17
14
|
"@typescript-eslint/eslint-plugin": "^4.15.2",
|
|
@@ -28,14 +25,13 @@
|
|
|
28
25
|
"ev": "0.0.7",
|
|
29
26
|
"http-server": "^0.12.3",
|
|
30
27
|
"jasmine": "^3.4.0",
|
|
31
|
-
"jsdoc": "^3.6.3",
|
|
32
|
-
"jsdoc-export-default-interop": "^0.3.1",
|
|
33
28
|
"karma": "^6.1.1",
|
|
34
|
-
"karma-chrome-launcher": "^3.1.0",
|
|
35
29
|
"karma-es6-shim": "^1.0.0",
|
|
30
|
+
"karma-firefox-launcher": "^2.1.2",
|
|
36
31
|
"karma-jasmine": "^2.0.1",
|
|
37
32
|
"karma-requirejs": "^1.1.0",
|
|
38
33
|
"karma-super-dots-reporter": "^0.2.0",
|
|
34
|
+
"keep-a-changelog": "^0.10.4",
|
|
39
35
|
"puppeteer": "^2.0.0",
|
|
40
36
|
"resemblejs": "^3.2.5",
|
|
41
37
|
"rollup": "^1.19.4",
|
|
@@ -44,12 +40,13 @@
|
|
|
44
40
|
"rollup-plugin-node-resolve": "^5.2.0",
|
|
45
41
|
"rollup-plugin-typescript2": "^0.29.0",
|
|
46
42
|
"rollup-plugin-uglify-es": "^0.0.1",
|
|
47
|
-
"
|
|
43
|
+
"shipjs": "0.23.3",
|
|
44
|
+
"typedoc": "^0.22.11",
|
|
48
45
|
"typescript": "^4.1.3"
|
|
49
46
|
},
|
|
50
47
|
"scripts": {
|
|
51
48
|
"build": "rollup -c",
|
|
52
|
-
"doc": "rm -rf docs && npx typedoc src/etro.ts --excludePrivate --readme none
|
|
49
|
+
"doc": "rm -rf docs && npx typedoc src/etro.ts --excludePrivate --readme none",
|
|
53
50
|
"assets": "git fetch origin example-assets:example-assets && git cherry-pick example-assets && git reset --soft HEAD^ && git reset HEAD examples/assets",
|
|
54
51
|
"effects": "node scripts/save-effect-samples.js",
|
|
55
52
|
"lint": "npm run --silent lint:main && npm run --silent lint:test && npm run --silent lint:examples",
|
|
@@ -57,7 +54,8 @@
|
|
|
57
54
|
"lint:test": "eslint -c eslint.test-conf.js spec",
|
|
58
55
|
"lint:examples": "eslint -c eslint.example-conf.js --ext .html examples",
|
|
59
56
|
"start": "http-server",
|
|
60
|
-
"test": "karma start"
|
|
57
|
+
"test": "karma start",
|
|
58
|
+
"release": "shipjs prepare"
|
|
61
59
|
},
|
|
62
60
|
"repository": {
|
|
63
61
|
"type": "git",
|
|
@@ -43,26 +43,35 @@
|
|
|
43
43
|
/**
|
|
44
44
|
* Save an effect sample to the disk
|
|
45
45
|
*/
|
|
46
|
-
function saveSample(original, effect, path) {
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
effect
|
|
46
|
+
async function saveSample(original, effect, path) {
|
|
47
|
+
// Create movie (needed for layer to render)
|
|
48
|
+
const movie = new etro.Movie({
|
|
49
|
+
canvas: document.createElement('canvas'),
|
|
50
|
+
autoRefresh: false
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Convert canvas to image
|
|
54
|
+
const originalImg = new Image()
|
|
55
|
+
await new Promise(resolve => {
|
|
56
|
+
originalImg.onload = resolve
|
|
57
|
+
originalImg.src = original.toDataURL()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Add an image layer with the effect to the movie
|
|
61
|
+
const layer = new etro.layer.Image({
|
|
62
|
+
startTime: 0,
|
|
63
|
+
duration: 1,
|
|
64
|
+
source: originalImg
|
|
65
|
+
})
|
|
66
|
+
layer.effects.push(effect)
|
|
67
|
+
movie.layers.push(layer)
|
|
61
68
|
|
|
62
|
-
save
|
|
69
|
+
// Render and save the layer
|
|
70
|
+
layer.render()
|
|
71
|
+
save(layer.canvas, path)
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
window.onload = () => {
|
|
74
|
+
window.onload = async () => {
|
|
66
75
|
const original = genRandomNoise(16, 16)
|
|
67
76
|
save(original, 'original.png')
|
|
68
77
|
|
|
@@ -91,7 +100,7 @@
|
|
|
91
100
|
|
|
92
101
|
for (let path in samples) {
|
|
93
102
|
const effect = samples[path]
|
|
94
|
-
saveSample(original, effect, path)
|
|
103
|
+
await saveSample(original, effect, path)
|
|
95
104
|
}
|
|
96
105
|
window.done = true
|
|
97
106
|
}
|
|
@@ -20,8 +20,13 @@ function createDirs(filePath) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
(async () => {
|
|
23
|
-
const browser = await puppeteer.launch(
|
|
23
|
+
const browser = await puppeteer.launch({
|
|
24
|
+
args: ['--autoplay-policy=no-user-gesture-required']
|
|
25
|
+
})
|
|
24
26
|
const page = await browser.newPage()
|
|
27
|
+
page.on('console', msg => {
|
|
28
|
+
console.log(`[CONSOLE] ${msg.text()}`)
|
|
29
|
+
})
|
|
25
30
|
|
|
26
31
|
await page.goto(`file://${__dirname}/gen-effect-samples.html`)
|
|
27
32
|
await page.waitForFunction(() => window.done);
|
package/ship.config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { parser } = require('keep-a-changelog')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const semver = require('semver')
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
updateChangelog: false,
|
|
7
|
+
formatCommitMessage: ({ version }) => `Release v${version}`,
|
|
8
|
+
formatPullRequestTitle: ({ version }) => `Release v${version}`,
|
|
9
|
+
getNextVersion: ({ currentVersion, dir }) => {
|
|
10
|
+
const changelog = new Changelog(`${dir}/CHANGELOG.md`)
|
|
11
|
+
return changelog.nextVersion(currentVersion)
|
|
12
|
+
},
|
|
13
|
+
versionUpdated: async ({ version, _releaseType, dir, _exec }) => {
|
|
14
|
+
const parsedVersion = semver.parse(version)
|
|
15
|
+
if (parsedVersion.prerelease.length)
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
// Release 'Unreleased' section in changelog
|
|
19
|
+
const changelogFile = `${dir}/CHANGELOG.md`
|
|
20
|
+
const oldChangelog = fs.readFileSync(changelogFile, 'utf8')
|
|
21
|
+
const parsed = parser(oldChangelog)
|
|
22
|
+
const release = parsed.findRelease() // get 'Unreleased' section
|
|
23
|
+
release.setVersion(version) // release
|
|
24
|
+
release.setDate(new Date()) // today
|
|
25
|
+
const newChangelog = parsed.toString()
|
|
26
|
+
fs.writeFileSync(changelogFile, newChangelog, 'utf8')
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class Changelog {
|
|
31
|
+
constructor (path) {
|
|
32
|
+
const data = fs.readFileSync(path, 'utf8')
|
|
33
|
+
const lines = data.split(/\r?\n/)
|
|
34
|
+
const headings = []
|
|
35
|
+
let unreleased = false
|
|
36
|
+
|
|
37
|
+
this.releaseTag = 'latest'
|
|
38
|
+
lines.every((line) => {
|
|
39
|
+
if (line.startsWith('## [Unreleased]')) {
|
|
40
|
+
unreleased = true
|
|
41
|
+
const tagMatch = line.match(/## \[Unreleased\]\[(.*)\]/)
|
|
42
|
+
if (tagMatch)
|
|
43
|
+
this.releaseTag = tagMatch[1].trim()
|
|
44
|
+
} else if (line.startsWith('## ')) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (unreleased)
|
|
49
|
+
if (line.startsWith('### ')) {
|
|
50
|
+
headings.push(line.match(/### (.*)/)[1].trim())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return true
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (headings.includes('Changed'))
|
|
57
|
+
this.releaseType = 'major'
|
|
58
|
+
else if (headings.includes('Added'))
|
|
59
|
+
this.releaseType = 'minor'
|
|
60
|
+
else
|
|
61
|
+
this.releaseType = 'patch'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
nextVersion (version) {
|
|
65
|
+
const parsedVersion = semver.parse(version)
|
|
66
|
+
|
|
67
|
+
if (this.releaseTag !== 'latest')
|
|
68
|
+
if (parsedVersion.prerelease.length) {
|
|
69
|
+
parsedVersion.inc('prerelease', this.releaseTag)
|
|
70
|
+
} else {
|
|
71
|
+
parsedVersion.inc(this.releaseType)
|
|
72
|
+
parsedVersion.prerelease = [this.releaseTag, 0]
|
|
73
|
+
parsedVersion.format()
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
parsedVersion.inc(this.releaseType)
|
|
77
|
+
|
|
78
|
+
return parsedVersion.version
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/effect/stack.ts
CHANGED
|
@@ -46,7 +46,7 @@ export class Stack extends Base {
|
|
|
46
46
|
|
|
47
47
|
attach (movie: Movie): void {
|
|
48
48
|
super.attach(movie)
|
|
49
|
-
this.effects.forEach(effect => {
|
|
49
|
+
this.effects.filter(effect => !!effect).forEach(effect => {
|
|
50
50
|
effect.detach()
|
|
51
51
|
effect.attach(movie)
|
|
52
52
|
})
|
|
@@ -54,7 +54,7 @@ export class Stack extends Base {
|
|
|
54
54
|
|
|
55
55
|
detach (): void {
|
|
56
56
|
super.detach()
|
|
57
|
-
this.effects.forEach(effect => {
|
|
57
|
+
this.effects.filter(effect => !!effect).forEach(effect => {
|
|
58
58
|
effect.detach()
|
|
59
59
|
})
|
|
60
60
|
}
|
|
@@ -62,6 +62,7 @@ export class Stack extends Base {
|
|
|
62
62
|
apply (target: Movie | Visual, reltime: number): void {
|
|
63
63
|
for (let i = 0; i < this.effects.length; i++) {
|
|
64
64
|
const effect = this.effects[i]
|
|
65
|
+
if (!effect) continue
|
|
65
66
|
effect.apply(target, reltime)
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AudioContext, IAudioNode, IAudioDestinationNode } from 'standardized-audio-context'
|
|
2
1
|
import { Movie } from '../movie'
|
|
3
2
|
import { subscribe } from '../event'
|
|
4
3
|
import { applyOptions, val } from '../util'
|
|
@@ -8,7 +7,7 @@ type Constructor<T> = new (...args: unknown[]) => T
|
|
|
8
7
|
|
|
9
8
|
interface AudioSource extends Base {
|
|
10
9
|
readonly source: HTMLMediaElement
|
|
11
|
-
readonly audioNode:
|
|
10
|
+
readonly audioNode: AudioNode
|
|
12
11
|
playbackRate: number
|
|
13
12
|
/** The audio source node for the media */
|
|
14
13
|
sourceStartTime: number
|
|
@@ -41,7 +40,7 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
41
40
|
readonly source: HTMLMediaElement
|
|
42
41
|
|
|
43
42
|
private __startTime: number
|
|
44
|
-
private _audioNode:
|
|
43
|
+
private _audioNode: AudioNode
|
|
45
44
|
private _sourceStartTime: number
|
|
46
45
|
private _unstretchedDuration: number
|
|
47
46
|
private _playbackRate: number
|
|
@@ -117,12 +116,12 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
|
|
|
117
116
|
// Spy on connect and disconnect to remember if it connected to
|
|
118
117
|
// actx.destination (for Movie#record).
|
|
119
118
|
const oldConnect = this._audioNode.connect.bind(this.audioNode)
|
|
120
|
-
this._audioNode.connect = <T extends
|
|
119
|
+
this._audioNode.connect = <T extends AudioDestinationNode>(destination: T, outputIndex?: number, inputIndex?: number): AudioNode => {
|
|
121
120
|
this._connectedToDestination = destination === movie.actx.destination
|
|
122
121
|
return oldConnect(destination, outputIndex, inputIndex)
|
|
123
122
|
}
|
|
124
123
|
const oldDisconnect = this._audioNode.disconnect.bind(this.audioNode)
|
|
125
|
-
this._audioNode.disconnect = <T extends
|
|
124
|
+
this._audioNode.disconnect = <T extends AudioDestinationNode>(destination?: T | number, output?: number, input?: number): AudioNode => {
|
|
126
125
|
if (this._connectedToDestination &&
|
|
127
126
|
destination === movie.actx.destination)
|
|
128
127
|
this._connectedToDestination = false
|
package/src/layer/visual.ts
CHANGED
|
@@ -83,6 +83,12 @@ class Visual extends Base {
|
|
|
83
83
|
* Render visual output
|
|
84
84
|
*/
|
|
85
85
|
render (): void {
|
|
86
|
+
// Prevent empty canvas errors if the width or height is 0
|
|
87
|
+
const width = val(this, 'width', this.currentTime)
|
|
88
|
+
const height = val(this, 'height', this.currentTime)
|
|
89
|
+
if (width === 0 || height === 0)
|
|
90
|
+
return
|
|
91
|
+
|
|
86
92
|
this.beginRender()
|
|
87
93
|
this.doRender()
|
|
88
94
|
this.endRender()
|
|
@@ -125,7 +131,7 @@ class Visual extends Base {
|
|
|
125
131
|
_applyEffects (): void {
|
|
126
132
|
for (let i = 0; i < this.effects.length; i++) {
|
|
127
133
|
const effect = this.effects[i]
|
|
128
|
-
if (effect.enabled)
|
|
134
|
+
if (effect && effect.enabled)
|
|
129
135
|
// Pass relative time
|
|
130
136
|
effect.apply(this, this.movie.currentTime - this.startTime)
|
|
131
137
|
}
|
package/src/movie.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* @module movie
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { AudioContext } from 'standardized-audio-context'
|
|
6
5
|
import { subscribe, publish } from './event'
|
|
7
6
|
import { Dynamic, val, clearCachedValues, applyOptions, watchPublic } from './util'
|
|
8
7
|
import { Base as BaseLayer, Audio as AudioLayer, Video as VideoLayer, Visual } from './layer/index' // `Media` mixins
|
|
@@ -10,6 +9,10 @@ import { AudioSource } from './layer/audio-source' // not exported from ./layer/
|
|
|
10
9
|
import { Base as BaseEffect } from './effect/index'
|
|
11
10
|
|
|
12
11
|
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
webkitAudioContext: typeof AudioContext
|
|
14
|
+
}
|
|
15
|
+
|
|
13
16
|
interface HTMLCanvasElement {
|
|
14
17
|
captureStream(frameRate?: number): MediaStream
|
|
15
18
|
}
|
|
@@ -75,7 +78,11 @@ export class Movie {
|
|
|
75
78
|
constructor (options: MovieOptions) {
|
|
76
79
|
// TODO: move into multiple methods!
|
|
77
80
|
// Set actx option manually, because it's readonly.
|
|
78
|
-
this.actx = options.actx ||
|
|
81
|
+
this.actx = options.actx ||
|
|
82
|
+
options.audioContext ||
|
|
83
|
+
new AudioContext() ||
|
|
84
|
+
// eslint-disable-next-line new-cap
|
|
85
|
+
new window.webkitAudioContext()
|
|
79
86
|
delete options.actx
|
|
80
87
|
|
|
81
88
|
// Proxy that will be returned by constructor
|
|
@@ -255,6 +262,10 @@ export class Movie {
|
|
|
255
262
|
if (!this.paused)
|
|
256
263
|
throw new Error('Cannot record movie while already playing or recording')
|
|
257
264
|
|
|
265
|
+
const mimeType = options.type || 'video/webm'
|
|
266
|
+
if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType))
|
|
267
|
+
throw new Error('Please pass a valid MIME type for the exported video')
|
|
268
|
+
|
|
258
269
|
return new Promise((resolve, reject) => {
|
|
259
270
|
const canvasCache = this.canvas
|
|
260
271
|
// Record on a temporary canvas context
|
|
@@ -285,7 +296,11 @@ export class Movie {
|
|
|
285
296
|
)
|
|
286
297
|
}
|
|
287
298
|
const stream = new MediaStream(tracks)
|
|
288
|
-
const
|
|
299
|
+
const mediaRecorderOptions = {
|
|
300
|
+
...(options.mediaRecorderOptions || {}),
|
|
301
|
+
mimeType
|
|
302
|
+
}
|
|
303
|
+
const mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions)
|
|
289
304
|
mediaRecorder.ondataavailable = event => {
|
|
290
305
|
// if (this._paused) reject(new Error("Recording was interrupted"));
|
|
291
306
|
if (event.data.size > 0)
|
|
@@ -293,6 +308,7 @@ export class Movie {
|
|
|
293
308
|
}
|
|
294
309
|
// TODO: publish to movie, not layers
|
|
295
310
|
mediaRecorder.onstop = () => {
|
|
311
|
+
this._paused = true
|
|
296
312
|
this._ended = true
|
|
297
313
|
this._canvas = canvasCache
|
|
298
314
|
this._cctx = this.canvas.getContext('2d')
|
|
@@ -303,7 +319,7 @@ export class Movie {
|
|
|
303
319
|
// Construct the exported video out of all the frame blobs.
|
|
304
320
|
resolve(
|
|
305
321
|
new Blob(recordedChunks, {
|
|
306
|
-
type:
|
|
322
|
+
type: mimeType
|
|
307
323
|
})
|
|
308
324
|
)
|
|
309
325
|
}
|
|
@@ -324,11 +340,13 @@ export class Movie {
|
|
|
324
340
|
pause (): Movie {
|
|
325
341
|
this._paused = true
|
|
326
342
|
// Deactivate all layers
|
|
327
|
-
for (let i = 0; i < this.layers.length; i++)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
343
|
+
for (let i = 0; i < this.layers.length; i++)
|
|
344
|
+
if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
|
|
345
|
+
const layer = this.layers[i]
|
|
346
|
+
layer.stop()
|
|
347
|
+
layer.active = false
|
|
348
|
+
}
|
|
349
|
+
|
|
332
350
|
publish(this, 'movie.pause', {})
|
|
333
351
|
return this
|
|
334
352
|
}
|
|
@@ -370,27 +388,31 @@ export class Movie {
|
|
|
370
388
|
const end = this.duration
|
|
371
389
|
const ended = this.currentTime > end
|
|
372
390
|
if (ended) {
|
|
373
|
-
publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
|
|
374
|
-
// TODO: only reset currentTime if repeating
|
|
375
|
-
this._currentTime = 0 // don't use setter
|
|
376
|
-
publish(this, 'movie.timeupdate', { movie: this })
|
|
377
391
|
this._lastPlayed = performance.now()
|
|
378
392
|
this._lastPlayedOffset = 0 // this.currentTime
|
|
379
393
|
this._renderingFrame = false
|
|
380
394
|
if (!this.repeat || this.recording) {
|
|
395
|
+
this._paused = true
|
|
381
396
|
this._ended = true
|
|
382
397
|
// Deactivate all layers
|
|
383
|
-
for (let i = 0; i < this.layers.length; i++)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
398
|
+
for (let i = 0; i < this.layers.length; i++)
|
|
399
|
+
if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
|
|
400
|
+
const layer = this.layers[i]
|
|
401
|
+
// A layer that has been deleted before layers.length has been updated
|
|
402
|
+
// (see the layers proxy in the constructor).
|
|
403
|
+
if (!layer)
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
layer.stop()
|
|
407
|
+
layer.active = false
|
|
408
|
+
}
|
|
393
409
|
}
|
|
410
|
+
|
|
411
|
+
publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
|
|
412
|
+
|
|
413
|
+
// TODO: only reset currentTime if repeating
|
|
414
|
+
this._currentTime = 0 // don't use setter
|
|
415
|
+
publish(this, 'movie.timeupdate', { movie: this })
|
|
394
416
|
}
|
|
395
417
|
|
|
396
418
|
// Stop playback or recording if done
|
|
@@ -421,8 +443,8 @@ export class Movie {
|
|
|
421
443
|
return
|
|
422
444
|
}
|
|
423
445
|
|
|
424
|
-
window.requestAnimationFrame(
|
|
425
|
-
this._render(repeat,
|
|
446
|
+
window.requestAnimationFrame(() => {
|
|
447
|
+
this._render(repeat, undefined, done)
|
|
426
448
|
}) // TODO: research performance cost
|
|
427
449
|
}
|
|
428
450
|
|
|
@@ -456,6 +478,8 @@ export class Movie {
|
|
|
456
478
|
private _renderLayers () {
|
|
457
479
|
let frameFullyLoaded = true
|
|
458
480
|
for (let i = 0; i < this.layers.length; i++) {
|
|
481
|
+
if (!Object.prototype.hasOwnProperty.call(this.layers, i)) continue
|
|
482
|
+
|
|
459
483
|
const layer = this.layers[i]
|
|
460
484
|
// A layer that has been deleted before layers.length has been updated
|
|
461
485
|
// (see the layers proxy in the constructor).
|
|
@@ -532,7 +556,8 @@ export class Movie {
|
|
|
532
556
|
*/
|
|
533
557
|
private _publishToLayers (type, event) {
|
|
534
558
|
for (let i = 0; i < this.layers.length; i++)
|
|
535
|
-
|
|
559
|
+
if (Object.prototype.hasOwnProperty.call(this.layers, i))
|
|
560
|
+
publish(this.layers[i], type, event)
|
|
536
561
|
}
|
|
537
562
|
|
|
538
563
|
/**
|
|
@@ -606,7 +631,8 @@ export class Movie {
|
|
|
606
631
|
this._currentTime = time
|
|
607
632
|
publish(this, 'movie.seek', {})
|
|
608
633
|
// Render single frame to match new time
|
|
609
|
-
this.
|
|
634
|
+
if (this.autoRefresh)
|
|
635
|
+
this.refresh()
|
|
610
636
|
}
|
|
611
637
|
|
|
612
638
|
/**
|
package/src/util.ts
CHANGED
|
@@ -425,7 +425,7 @@ export function watchPublic (target: EtroObject): EtroObject {
|
|
|
425
425
|
publish(proxy, `${target.type}.change.modify`, { property: getPath(receiver, prop), newValue: val })
|
|
426
426
|
}
|
|
427
427
|
const canWatch = (receiver, prop) => !prop.startsWith('_') &&
|
|
428
|
-
(
|
|
428
|
+
(receiver.publicExcludes === undefined || !receiver.publicExcludes.includes(prop))
|
|
429
429
|
|
|
430
430
|
// The path to each child property (each is a unique proxy)
|
|
431
431
|
const paths = new WeakMap()
|