etro 0.8.5 → 0.9.1

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.
Files changed (47) hide show
  1. package/.github/workflows/nodejs.yml +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/CONTRIBUTING.md +13 -26
  4. package/README.md +8 -15
  5. package/dist/effect/base.d.ts +5 -5
  6. package/dist/effect/visual.d.ts +11 -0
  7. package/dist/etro-cjs.js +84 -53
  8. package/dist/etro-iife.js +84 -53
  9. package/dist/layer/image.d.ts +2 -2
  10. package/dist/layer/text.d.ts +3 -3
  11. package/dist/layer/visual-source.d.ts +18 -3
  12. package/dist/layer/visual.d.ts +5 -5
  13. package/dist/movie.d.ts +13 -4
  14. package/dist/object.d.ts +5 -1
  15. package/dist/util.d.ts +2 -0
  16. package/eslint.conf.js +2 -0
  17. package/eslint.test-conf.js +1 -2
  18. package/karma.conf.js +17 -8
  19. package/package.json +19 -19
  20. package/scripts/gen-effect-samples.html +24 -0
  21. package/scripts/save-effect-samples.js +1 -1
  22. package/src/effect/base.ts +6 -6
  23. package/src/effect/stack.ts +2 -2
  24. package/src/effect/transform.ts +2 -2
  25. package/src/effect/visual.ts +16 -1
  26. package/src/layer/audio-source.ts +4 -1
  27. package/src/layer/base.ts +3 -2
  28. package/src/layer/image.ts +3 -3
  29. package/src/layer/text.ts +4 -4
  30. package/src/layer/visual-source.ts +27 -7
  31. package/src/layer/visual.ts +7 -7
  32. package/src/movie.ts +55 -37
  33. package/src/object.ts +5 -1
  34. package/src/util.ts +2 -0
  35. package/tsconfig.json +3 -1
  36. package/examples/application/readme-screenshot.html +0 -85
  37. package/examples/application/video-player.html +0 -130
  38. package/examples/application/webcam.html +0 -28
  39. package/examples/introduction/audio.html +0 -64
  40. package/examples/introduction/effects.html +0 -79
  41. package/examples/introduction/export.html +0 -83
  42. package/examples/introduction/functions.html +0 -37
  43. package/examples/introduction/hello-world1.html +0 -37
  44. package/examples/introduction/hello-world2.html +0 -32
  45. package/examples/introduction/keyframes.html +0 -79
  46. package/examples/introduction/media.html +0 -63
  47. package/examples/introduction/text.html +0 -31
@@ -1,85 +0,0 @@
1
- <!-- TODO: fix -->
2
- <!DOCTYPE html>
3
- <html>
4
- <head>
5
- <title>Fun with Etro</title>
6
- <script src="../../dist/etro-iife.js"></script>
7
- </head>
8
- <body>
9
- <script>
10
- const cols = 2; const rows = 2
11
- let movie, sample
12
-
13
- const begin = () => {
14
- const canvas = document.createElement('canvas')
15
- document.body.appendChild(canvas)
16
- movie = new etro.Movie({ canvas })
17
- etro.event.subscribe(movie, 'movie.timeupdate', () => {
18
- if (movie.currentTime >= 11)
19
- movie.pause()
20
- })
21
-
22
- sample = document.createElement('video')
23
- sample.src = '../assets/desert.mp4'
24
-
25
- sample.onloadedmetadata = addLayers
26
- }
27
-
28
- const addLayers = () => {
29
- movie.width = sample.videoWidth // you could also do canvas.width = sample.videoWidth;
30
- movie.height = sample.videoHeight
31
- const width = movie.width / cols; const height = movie.height / rows
32
- let numbLoaded = 0
33
-
34
- for (let y = 0; y < movie.height; y += height)
35
- for (let x = 0; x < movie.width; x += width) {
36
- const video = document.createElement('video')
37
- video.src = '../assets/desert.mp4'
38
- if (x * y)
39
- video.onplay = event => {
40
- console.log(event)
41
- }
42
-
43
- video.onloadedmetadata = () => {
44
- const layer = new etro.layer.Video({
45
- startTime: 0, source: video, x, y, width, height
46
- // sourceStartTime: 0
47
- }).addEffect(new etro.effect.Channels({
48
- r: 0.5 + Math.random() * 0.5,
49
- g: 0.5 + Math.random() * 0.5,
50
- b: 0.5 + Math.random() * 0.5
51
- }))
52
- movie.addLayer(layer)
53
- numbLoaded++
54
-
55
- if (numbLoaded === rows * cols)
56
- end()
57
- }
58
- }
59
- }
60
-
61
- const end = () => {
62
- movie
63
- .addLayer(new etro.layer.Text({
64
- startTime: 0,
65
- duration: sample.duration,
66
- text: 'etro',
67
- width: movie.width / 2,
68
- height: movie.height / 2,
69
- x: movie.width / 4,
70
- y: movie.height / 4,
71
- textX: -movie.width / 4 + movie.width / 2,
72
- textY: -movie.height / 4 + movie.height / 2,
73
- background: 'rgba(255,0,200,0.4)',
74
- color: 'rgba(255,255,255,0.7)',
75
- font: '24px monospace',
76
- textAlign: 'center',
77
- textBaseline: 'middle'
78
- }))
79
- .play()
80
- }
81
-
82
- window.addEventListener('load', begin)
83
- </script>
84
- </body>
85
- </html>
@@ -1,130 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Video player in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- <style>
7
- body {
8
- background: #333;
9
- }
10
-
11
- /* TODO: make chrome-compatable */
12
- input[type=range]::-moz-range-progress {
13
- background-color: #d0d0d0;
14
- }
15
- input[type=range]::-moz-range-track {
16
- background-color: #808080;
17
- }
18
-
19
- .center-x {
20
- text-align: center;
21
- }
22
-
23
- img, video {
24
- display: none;
25
- }
26
-
27
- /* https://css-tricks.com/making-pure-css-playpause-button/ */
28
-
29
- .play {
30
- background: transparent;
31
- width: 24px;
32
- height: 24px;
33
- box-sizing: border-box;
34
- border-style: solid;
35
- border-color: transparent transparent transparent #d0d0d0;
36
- border-width: 12px 0 12px 24px;
37
- }
38
-
39
- .pause {
40
- background: transparent;
41
- width: 24px;
42
- height: 24px;
43
- border-style: double;
44
- border-color: #d0d0d0;
45
- border-width: 0 0 0 12px;
46
- }
47
- .pause:focus {
48
- /* TODO: hide outline */
49
- }
50
-
51
- #progress {
52
- width: calc(100% - 64px); /* TODO: make better */
53
- float: right;
54
- }
55
-
56
- #container {
57
- display: inline-block;
58
- }
59
- </style>
60
- </head>
61
- <body>
62
- <script>
63
- window.addEventListener('load', () => {
64
- const canvas = document.getElementById('output')
65
-
66
- const controls = {
67
- playPause: document.getElementById('pause-play'),
68
- progressBar: document.getElementById('progress')
69
- }
70
-
71
- const movie = new etro.Movie({ canvas, repeat: true })
72
-
73
- // FIXME: resetting to beginning every time paused and played
74
- controls.playPause.onclick = event => {
75
- if (controls.playPause.className === 'play') {
76
- movie.play()
77
- controls.playPause.className = 'pause'
78
- } else {
79
- movie.pause()
80
- controls.playPause.className = 'play'
81
- }
82
- }
83
- canvas.addEventListener('click', controls.playPause.click) // FIXME: isn"t firing
84
- document.body.addEventListener('keyup', event => {
85
- const key = event.which || event.keyCode || 0
86
- if (key === 32)
87
- controls.playPause.click()
88
- // spacebar
89
- })
90
- etro.event.subscribe(movie, 'movie.ended', event => {
91
- if (!event.repeat)
92
- controls.playPause.className = 'play'
93
- })
94
-
95
- controls.progressBar.min = 0
96
- /* controls.progressBar.step = movie.duration / controls.progressBar.style.getComputedStyle().width;
97
- controls.progressBar.addEventListener("resize", () => {
98
- controls.progressBar.step = movie.duration / controls.progressBar.style.width;
99
- }); */
100
- controls.progressBar.step = 0.5 // half of a second
101
- controls.progressBar.value = 0 // just for sure, it's weird
102
- controls.progressBar.addEventListener('input', () => {
103
- movie.currentTime = 1 * controls.progressBar.value
104
- movie.refresh()
105
- })
106
- etro.event.subscribe(movie, 'movie.timeupdate', () => {
107
- controls.progressBar.value = movie.currentTime
108
- })
109
-
110
- const video = document.querySelector('video')
111
- canvas.width = video.videoWidth
112
- canvas.height = video.videoHeight
113
-
114
- // For simplicity, only use one layer
115
- movie.addLayer(
116
- new etro.layer.Video({ startTime: 0, source: video })
117
- )
118
-
119
- controls.progressBar.max = movie.duration
120
- })
121
- </script>
122
- <div id="container">
123
- <video src="../assets/desert.mp4"></video>
124
- <img src="../assets/lake.jpg"/>
125
- <canvas id="output"></canvas><br>
126
- <button id="pause-play" class="play"><!-- styled --></button>
127
- <input id="progress" type="range" value="0"/>
128
- </div>
129
- </body>
130
- </html>
@@ -1,28 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>getUserMedia()</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <video style="display:none"></video>
9
- <canvas></canvas>
10
- <script>
11
- const video = document.querySelector('video')
12
- navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
13
- video.srcObject = stream
14
- video.onloadedmetadata = () => {
15
- movie.width = video.videoWidth
16
- movie.height = video.videoHeight
17
- movie.play()
18
- }
19
- })
20
-
21
- const canvas = document.querySelector('canvas')
22
- const movie = new etro.Movie({ canvas })
23
- .addLayer(new etro.layer.Visual({ startTime: 0, duration: Infinity, background: 'black' }))
24
- .addLayer(new etro.layer.Video({ startTime: 0, source: video })
25
- .addEffect(new etro.effect.ChromaKey(etro.parseColor('black'), 100)))
26
- </script>
27
- </body>
28
- </html>
@@ -1,64 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Manipulating Audio in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <script>
9
- const start = () => {
10
- button.disabled = true
11
- // initialize movie
12
- const canvas = document.createElement('canvas')
13
- const movie = new etro.Movie({ canvas })
14
- // initialize layers (all with durations clipped to 5 seconds)
15
- const audio1 = document.getElementById('layer1')
16
- const audio2 = document.getElementById('layer2')
17
- const audio3 = document.getElementById('layer3')
18
- const layer1 = new etro.layer.Audio({ startTime: 0, source: audio1, duration: 5 })
19
- const layer2 = new etro.layer.Audio({ startTime: layer1.duration, source: audio2, duration: 5 })
20
- const layer3 = new etro.layer.Audio({
21
- startTime: layer2.startTime + layer2.duration,
22
- source: audio3,
23
- duration: 5
24
- })
25
-
26
- /*
27
- Once a media layer is added to the movie, it will automatically be connected to the movie's `actx`
28
- property.
29
- */
30
- // add layers to movie (first, so we can reconnect it to the graph)
31
- // movie.layers.push.apply(movie.layers, [layer1, layer2, layer3])
32
- movie.layers.push(layer1, layer2, layer3)
33
-
34
- // Now, disconnect the layers from the audio node graph and reconnect them with intersourcete audio
35
- // effect nodes.
36
-
37
- layer1.audioNode.disconnect(movie.actx.destination)
38
- const muter = movie.actx.createGain()
39
- muter.gain.value = 0
40
- layer1.audioNode.connect(muter).connect(movie.actx.destination)
41
-
42
- layer2.audioNode.disconnect(movie.actx.destination)
43
- const panner = movie.actx.createStereoPanner()
44
- panner.pan.value = +1 // completely to the right
45
- layer2.audioNode.connect(panner).connect(movie.actx.destination)
46
-
47
- // Manipulating the audio graph is not always necessary
48
- layer3.playbackRate = 2 // double the speed
49
-
50
- etro.event.subscribe(movie, 'movie.ended', () => {
51
- button.disabled = false
52
- })
53
- movie.play()
54
- }
55
-
56
- const button = document.querySelector('button')
57
- button.addEventListener('click', start)
58
- </script>
59
- <button>Start</button>
60
- <audio id="layer1" src="../assets/strings.wav"></audio>
61
- <audio id="layer2" src="../assets/strings.wav"></audio>
62
- <audio id="layer3" src="../assets/strings.wav"></audio>
63
- </body>
64
- </html>
@@ -1,79 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Effects in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <script>
9
- let movie
10
- window.addEventListener('load', () => {
11
- const canvas = document.createElement('canvas')
12
- document.body.appendChild(canvas)
13
-
14
- initMovie(canvas)
15
- })
16
-
17
- const initMovie = canvas => {
18
- movie = new etro.Movie({ canvas })
19
-
20
- const image = document.querySelector('img')
21
- canvas.width = image.width
22
- canvas.height = image.height
23
- // create a red 400x400 rectangle that starts at time 0 and lasts 2 seconds
24
- movie
25
- .addLayer(new etro.layer.Image({ startTime: 0, duration: 2, source: image }))
26
- // create a transparent blue 500x200 at (50, 20) that starts at time 1 and lasts 2 seconds
27
- .addLayer(
28
- new etro.layer.Image({ startTime: 2, duration: 2, source: image }).addEffect(
29
- new etro.effect.GaussianBlur({ radius: 3 })
30
- )
31
- )
32
- .addLayer(
33
- new etro.layer.Image({ startTime: 4, duration: 2, source: image }).addEffect(
34
- // you can also use keyframes for almost any property in Etro
35
- new etro.effect.Channels({
36
- factors: new etro.KeyFrame(
37
- [0, { r: 2, g: 0.5 }],
38
- [2, { r: 0.5, g: 2 }]
39
- )
40
- })
41
- )
42
- )
43
- .addLayer(
44
- new etro.layer.Image({
45
- startTime: 6,
46
- duration: 2,
47
- source: image,
48
- // allow rotated image to fill entire screen by setting the size of the layer, which is not the image
49
- width: movie.width,
50
- height: movie.height
51
- }).addEffect(
52
- new etro.effect.Transform({
53
- matrix: new etro.effect.Transform.Matrix().rotate(Math.PI / 6) // 30d
54
- })
55
- )
56
- )
57
- .addLayer(
58
- new etro.layer.Image({ startTime: 8, duration: 2, source: image }).addEffect(
59
- new etro.effect.EllipticalMask({
60
- x: image.width / 2,
61
- y: image.height / 2,
62
- radiusX: image.width / 2,
63
- radiusY: image.height / 2
64
- })
65
- )
66
- )
67
- // .addEffect(new etro.effect.GaussianBlur({ radius: 5 }))
68
-
69
- setTimeout(() => {
70
- // console.log(movie.layers[1].effects[0].effects[1].shape)
71
- }, 3100)
72
-
73
- movie
74
- .play()
75
- }
76
- </script>
77
- <img src="../assets/lake.jpg" style="display: none;"/>
78
- </body>
79
- </html>
@@ -1,83 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Media in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <div id="input">
9
- <img src="../assets/lake.jpg" style="display:none"/>
10
- <video src="../assets/desert.mp4" style="display:none"></video>
11
- <audio src="../assets/strings.wav" style="display:none"></audio>
12
- </div>
13
- <div id="output">
14
- <video controls></video><br>
15
- </div>
16
- <button>Record</button>
17
-
18
- <script>
19
- // TODO: test audio output on a device that actually has drivers
20
- let movie
21
- const btn = document.querySelector('button')
22
- btn.addEventListener('click', () => {
23
- btn.disabled = true
24
- const canvas = document.createElement('canvas')
25
- canvas.width = 600
26
- canvas.height = 400
27
- // no need to add to body, as we don't need to say the movie but just the exported video
28
-
29
- initMovie(canvas)
30
- })
31
-
32
- const initMovie = canvas => {
33
- movie = new etro.Movie({ canvas })
34
- const video = document.querySelector('#input video')
35
- movie.width = video.videoWidth
36
- movie.height = video.videoHeight
37
- movie
38
- .addLayer(new etro.layer.Image({
39
- startTime: 0,
40
- duration: 3,
41
- source: document.querySelector('#input img'),
42
- // crop @ (150, 150) extending (200, 200)
43
- sourceX: 100,
44
- sourceY: 100,
45
- sourceWidth: 400,
46
- sourceHeight: 400,
47
- x: 100,
48
- y: 100,
49
- destWidth: 400,
50
- destHeight: 400
51
- }))
52
- .addLayer(new etro.layer.Video({
53
- startTime: 3,
54
- source: video,
55
- // trim video to only include 3 seconds starting 2 minutes into the video in the video
56
- sourceStartTime: 5,
57
- duration: 3
58
- }))
59
- .addLayer(new etro.layer.Audio({
60
- startTime: 6,
61
- source: document.querySelector('#input audio'),
62
- sourceStartTime: 9, // start audio at 9s
63
- duration: 3 // last 3s
64
- // volume: 0.25 // 25% of default volume (same as setting volume attribute on audio element)
65
- }))
66
-
67
- .record({ frameRate: 25 })
68
- .then(blob => {
69
- const video = document.querySelector('#output video')
70
- video.src = URL.createObjectURL(blob)
71
- })
72
- .catch(error => {
73
- throw error
74
- })
75
-
76
- window.addEventListener('unload', () => {
77
- const video = document.querySelector('#output video')
78
- URL.revokeObjectURL(video.src)
79
- })
80
- }
81
- </script>
82
- </body>
83
- </html>
@@ -1,37 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Functions in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <script>
9
- let movie
10
- window.addEventListener('load', () => {
11
- const canvas = document.createElement('canvas')
12
- document.body.appendChild(canvas)
13
-
14
- initMovie(canvas)
15
- })
16
-
17
- const initMovie = canvas => {
18
- movie = new etro.Movie({ canvas })
19
-
20
- canvas.width = canvas.height = 400
21
- movie
22
- // Functions are callbacks that are called every time a property value is queried.
23
- // For instance, you can set a layer's opacity to the function `Math.random`, which will make the
24
- // layer's opacity randomized each animation frame.
25
- .addLayer(new etro.layer.Visual({
26
- startTime: 0,
27
- duration: 3,
28
- // omitting width or height sets the respective element to fill the screen
29
- background: 'green',
30
-
31
- opacity: (element, reltime) => Math.random()
32
- }))
33
- .play()
34
- }
35
- </script>
36
- </body>
37
- </html>
@@ -1,37 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Hello World in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <script>
9
- window.addEventListener('load', () => {
10
- const canvas = document.createElement('canvas')
11
- canvas.width = 600
12
- canvas.height = 400
13
- document.body.appendChild(canvas)
14
-
15
- new etro.Movie({ canvas })
16
- // create a red rectangle that starts at time 0 and lasts 4 seconds and fills the entire screen
17
- .addLayer(new etro.layer.Visual({
18
- startTime: 0,
19
- duration: 4,
20
- background: 'red'
21
- }))
22
- // create a transparent blue 500x200 at (50, 20) that starts at time 1 and lasts 2 seconds
23
- .addLayer(new etro.layer.Visual({
24
- startTime: 1,
25
- duration: 2,
26
- background: '#0000ff80',
27
- border: { color: '#00f', thickness: 4 },
28
- width: 500,
29
- height: 200,
30
- x: 50,
31
- y: 20
32
- }))
33
- .play()
34
- })
35
- </script>
36
- </body>
37
- </html>
@@ -1,32 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Hello World in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <script>
9
- window.addEventListener('load', () => {
10
- const canvas = document.createElement('canvas')
11
- document.body.appendChild(canvas)
12
-
13
- const video = document.createElement('video')
14
- video.src = '../assets/desert.mp4'
15
-
16
- video.onloadeddata = () => {
17
- canvas.width = video.videoWidth // you could also do movie.width = video.videoWidth;
18
- canvas.height = video.videoHeight
19
- const movie = new etro.Movie({ canvas })
20
- movie
21
- // add a video layer at 0s in the timeline
22
- .addLayer(new etro.layer.Video({ startTime: 0, source: video }))
23
- .play()
24
-
25
- // FOR TESTING issue#8 (don't delete):
26
- // movie.setCurrentTime(1);
27
- // movie.play();
28
- }
29
- })
30
- </script>
31
- </body>
32
- </html>
@@ -1,79 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Keyframes in Etro</title>
5
- <script src="../../dist/etro-iife.js"></script>
6
- </head>
7
- <body>
8
- <script>
9
- let movie
10
- window.addEventListener('load', () => {
11
- const canvas = document.createElement('canvas')
12
- document.body.appendChild(canvas)
13
-
14
- initMovie(canvas)
15
- })
16
-
17
- const initMovie = canvas => {
18
- movie = new etro.Movie({ canvas })
19
-
20
- canvas.width = canvas.height = 400
21
- movie
22
- // create a red 400x400 rectangle that starts at time 0 and lasts 2 seconds
23
- // Keyframes let you make a dynamic property that interpolates.
24
- // For instance, you can set a layer's opacity to decrease over time, effectively making it fade out
25
- // Numbers and objects interpolate (animate smoothly)
26
- .addLayer(new etro.layer.Visual({
27
- startTime: 0,
28
- duration: 3,
29
- // omitting width or height sets the respective element to fill the screen
30
- background: 'green',
31
- // opacity=1 @ 0s (relative to the layer) -> opacity=0 @ 1s (relative to the layer)
32
- opacity: new etro.KeyFrame([0, 1], [3, 0])
33
- }))
34
- // Because strings don't interpolate, you need to convert colors and fonts to objects
35
- // for a smooth effect (which will then be automatically `.toString()`ed when set on the canvas
36
- // context).
37
- .addLayer(new etro.layer.Visual({
38
- startTime: 3,
39
- duration: 3,
40
- background: new etro.KeyFrame([0, etro.parseColor('red')], [3, new etro.Color(0, 0, 255)])
41
- }))
42
- // You can use other types in keyframes, but they will be used sequentially without interpolation
43
- .addLayer(new etro.layer.Text({
44
- startTime: 6,
45
- duration: 3,
46
- text: new etro.KeyFrame([0, 'Hello ...'], [1.5, '...world'])
47
- }))
48
-
49
- // When interpolating, you can specify how the keyframes will be interpolated
50
- .addLayer(new etro.layer.Visual({
51
- startTime: 9,
52
- duration: 3,
53
- width: new etro.KeyFrame([0, movie.width, etro.linearInterp], [3, 0]), // (obviously) linear
54
- height: new etro.KeyFrame([0, movie.height, etro.linearInterp], [3, 0]), // (obviously) linear
55
- background: 'blue'
56
- }))
57
-
58
- // Of course, you can have more than two keyframes
59
- .addLayer(new etro.layer.Text({
60
- text: 'Etro',
61
- startTime: 12,
62
- duration: 6,
63
- background: new etro.KeyFrame(
64
- [0, etro.parseColor('#0ff')],
65
- [2, etro.parseColor('#ff0')],
66
- [4, etro.parseColor('#f0f')],
67
- [6, etro.parseColor('#fff')]
68
- ),
69
- // let's just add another property (fonts can be parsed into objects just like colors)
70
- font: new etro.KeyFrame(
71
- [0, etro.parseFont('28px monospace'), etro.cosineInterp],
72
- [6, etro.parseFont('36px monospace')]
73
- )
74
- }))
75
- .play()
76
- }
77
- </script>
78
- </body>
79
- </html>