New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
<amp-bodymovin-actions> Add actions play/pause/stop/seekTo and noautoplay attribute #14040
Changes from 37 commits
c4d5c82
2377934
7a46387
730541d
0c1b1f4
80300b9
429add4
b38b2a0
9565eb5
38b2153
3c5de97
4e2fe75
ba88af7
e23b8ae
4870c85
52c3c5f
e5d57ef
c6b528a
7c413b9
a5b2e1c
e610b6c
e54f90f
85c132b
4c33fc5
d2f1ec8
ad9f99e
123585f
67110ee
4a2cb58
4749181
18461dc
8b2bbdc
6d4ab99
2fd6e28
4ac7cd6
08c34ae
efd7623
cc64ffe
ffddbda
e570df0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,8 @@ | |
* limitations under the License. | ||
*/ | ||
|
||
import {dict} from '../src/utils/object'; | ||
import {getData} from '../src/event-helper'; | ||
import {loadScript} from './3p'; | ||
import {parseJson} from '../src/json'; | ||
|
||
|
@@ -23,28 +25,50 @@ import {parseJson} from '../src/json'; | |
* @param {function(!Object)} cb | ||
*/ | ||
|
||
let animationHandler; | ||
|
||
function getBodymovinAnimationSdk(global, cb) { | ||
loadScript(global, 'https://cdnjs.cloudflare.com/ajax/libs/bodymovin/4.13.0/bodymovin_light.min.js', function() { | ||
cb(global.bodymovin); | ||
}); | ||
} | ||
|
||
function parseMessage(event) { | ||
const eventMessage = parseJson(getData(event)); | ||
const action = eventMessage['action']; | ||
if (action == 'play') { | ||
animationHandler.play(); | ||
} else if (action == 'pause') { | ||
animationHandler.pause(); | ||
} else if (action == 'stop') { | ||
animationHandler.stop(); | ||
} else if (action == 'seekTo') { | ||
animationHandler.goToAndStop(eventMessage['value'], | ||
eventMessage['valueType'] !== 'time'); | ||
} | ||
} | ||
|
||
export function bodymovinanimation(global) { | ||
const dataReceived = parseJson(global.name)['attributes']._context; | ||
const dataLoop = dataReceived['loop']; | ||
const animationData = dataReceived['animationData']; | ||
const animatingContainer = global.document.createElement('div'); | ||
|
||
global.document.getElementById('c').appendChild(animatingContainer); | ||
const shouldLoop = dataLoop != 'false'; | ||
const loop = !isNaN(dataLoop) ? dataLoop : shouldLoop; | ||
getBodymovinAnimationSdk(global, function(bodymovin) { | ||
bodymovin.loadAnimation({ | ||
animationHandler = bodymovin.loadAnimation({ | ||
container: animatingContainer, | ||
renderer: 'svg', | ||
loop, | ||
autoplay: true, | ||
animationData, | ||
autoplay: dataReceived['autoplay'], | ||
animationData: dataReceived['animationData'], | ||
}); | ||
const message = JSON.stringify(dict({ | ||
'action': 'ready', | ||
})); | ||
global.parent. /*OK*/postMessage(message, '*'); | ||
}); | ||
global.addEventListener('message', parseMessage, false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd move this above There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -279,8 +279,13 @@ FB.init; | |
var gist; | ||
gist.gistid; | ||
|
||
var bodymovin | ||
bodymovin.loadAnimation | ||
var bodymovin; | ||
bodymovin.loadAnimation; | ||
var animationHandler; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like closure compiler doesn't like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment isn't relevant to this patch. The amp.extern.js is fine in this run. 99 problems but |
||
animationHandler.play; | ||
animationHandler.pause; | ||
animationHandler.stop; | ||
animationHandler.goToAndStop; | ||
|
||
// Validator | ||
var amp; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,12 +14,19 @@ | |
* limitations under the License. | ||
*/ | ||
|
||
import {ActionTrust} from '../../../src/action-trust'; | ||
import {Services} from '../../../src/services'; | ||
import {assertHttpsUrl} from '../../../src/url'; | ||
import {batchFetchJsonFor} from '../../../src/batched-json'; | ||
import {clamp} from '../../../src/utils/math'; | ||
import {dict} from '../../../src/utils/object'; | ||
import {getData, listen} from '../../../src/event-helper'; | ||
import {getIframe, preloadBootstrap} from '../../../src/3p-frame'; | ||
import {isFiniteNumber, isObject} from '../../../src/types'; | ||
import {isLayoutSizeDefined} from '../../../src/layout'; | ||
import {parseJson} from '../../../src/json'; | ||
import {removeElement} from '../../../src/dom'; | ||
import {startsWith} from '../../../src/string'; | ||
import {user} from '../../../src/log'; | ||
|
||
const TAG = 'amp-bodymovin-animation'; | ||
|
@@ -33,15 +40,26 @@ export class AmpBodymovinAnimation extends AMP.BaseElement { | |
/** @private @const */ | ||
this.ampdoc_ = Services.ampdoc(this.element); | ||
|
||
/** @private {?HTMLIFrameElement} */ | ||
/** @private {?Element} */ | ||
this.iframe_ = null; | ||
|
||
/** @private {?string} */ | ||
this.loop_ = null; | ||
|
||
/** @private {?boolean} */ | ||
this.autoplay_ = null; | ||
|
||
/** @private {?string} */ | ||
this.src_ = null; | ||
|
||
/** @private {?Promise} */ | ||
this.playerReadyPromise_ = null; | ||
|
||
/** @private {?Function} */ | ||
this.playerReadyResolver_ = null; | ||
|
||
/** @private {?Function} */ | ||
this.unlistenMessage_ = null; | ||
} | ||
|
||
/** @override */ | ||
|
@@ -61,10 +79,25 @@ export class AmpBodymovinAnimation extends AMP.BaseElement { | |
/** @override */ | ||
buildCallback() { | ||
this.loop_ = this.element.getAttribute('loop') || 'true'; | ||
this.autoplay_ = !this.element.hasAttribute('noautoplay'); | ||
user().assert(this.element.hasAttribute('src'), | ||
'The src attribute must be specified for <amp-bodymovin-animation>'); | ||
assertHttpsUrl(this.element.getAttribute('src'), this.element); | ||
this.src_ = this.element.getAttribute('src'); | ||
this.playerReadyPromise_ = new Promise(resolve => { | ||
this.playerReadyResolver_ = resolve; | ||
}); | ||
|
||
// Register relevant actions | ||
this.registerAction('play', () => { this.play_(); }, ActionTrust.LOW); | ||
this.registerAction('pause', () => { this.pause_(); }, ActionTrust.LOW); | ||
this.registerAction('stop', () => { this.stop_(); }, ActionTrust.LOW); | ||
this.registerAction('seekTo', invocation => { | ||
const args = invocation.args; | ||
if (args) { | ||
this.seekTo_(args); | ||
} | ||
}, ActionTrust.LOW); | ||
} | ||
|
||
/** @override */ | ||
|
@@ -73,16 +106,24 @@ export class AmpBodymovinAnimation extends AMP.BaseElement { | |
return animData.then(data => { | ||
const opt_context = { | ||
loop: this.loop_, | ||
autoplay: this.autoplay_, | ||
animationData: data, | ||
}; | ||
const iframe = getIframe( | ||
this.win, this.element, 'bodymovinanimation', opt_context); | ||
return Services.vsyncFor(this.win).mutatePromise(() => { | ||
this.applyFillContent(iframe); | ||
this.unlistenMessage_ = listen( | ||
this.win, | ||
'message', | ||
this.handleBodymovinMessages_.bind(this) | ||
); | ||
this.element.appendChild(iframe); | ||
this.iframe_ = iframe; | ||
}).then(() => { | ||
return this.loadPromise(this.iframe_); | ||
const loaded = this.loadPromise(this.iframe_); | ||
this.playerReadyResolver_(loaded); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we shouldn't call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
return loaded; | ||
}); | ||
}); | ||
} | ||
|
@@ -93,8 +134,78 @@ export class AmpBodymovinAnimation extends AMP.BaseElement { | |
removeElement(this.iframe_); | ||
this.iframe_ = null; | ||
} | ||
this.playerReadyPromise_ = new Promise(resolve => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also needs a
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
this.playerReadyResolver_ = resolve; | ||
}); | ||
return true; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: jsdoc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
handleBodymovinMessages_(event) { | ||
if (event.source != this.iframe_.contentWindow) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
return; | ||
} | ||
if (!getData(event) || !(isObject(getData(event)) | ||
|| startsWith(/** @type {string} */ (getData(event)), '{'))) { | ||
return; // Doesn't look like JSON. | ||
} | ||
|
||
/** @const {?JsonObject} */ | ||
const eventData = /** @type {?JsonObject} */ (isObject(getData(event)) | ||
? getData(event) | ||
: parseJson(getData(event))); | ||
if (eventData === undefined) { | ||
return; // We only process valid JSON. | ||
} | ||
if (eventData['action'] == 'ready') { | ||
this.playerReadyResolver_(this.iframe_); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to resolve with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
} | ||
} | ||
|
||
/** | ||
* Sends a command to the player through postMessage. | ||
* @param {string} action | ||
* @param {string=} opt_valueType | ||
* @param {number=} opt_value | ||
* @private | ||
* */ | ||
sendCommand_(action, opt_valueType, opt_value) { | ||
this.playerReadyPromise_.then(function(iframe) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with few small exceptions in tests, we always use also no need to get the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
this.iframe_ = iframe; | ||
if (this.iframe_ && this.iframe_.contentWindow) { | ||
const message = JSON.stringify(dict({ | ||
'action': action, | ||
'valueType': opt_valueType || '', | ||
'value': opt_value || '', | ||
})); | ||
this.iframe_.contentWindow. /*OK*/postMessage(message, '*'); | ||
} | ||
}); | ||
} | ||
|
||
play_() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
this.sendCommand_('play'); | ||
} | ||
|
||
pause_() { | ||
this.sendCommand_('pause'); | ||
} | ||
|
||
stop_() { | ||
this.sendCommand_('stop'); | ||
} | ||
|
||
seekTo_(args) { | ||
const time = parseFloat(args && args['time']); | ||
// time based seek | ||
if (isFiniteNumber(time)) { | ||
this.sendCommand_('seekTo', 'time', time); | ||
} | ||
// percent based seek | ||
const percent = parseFloat(args && args['percent']); | ||
if (isFiniteNumber(percent)) { | ||
this.sendCommand_('seekTo', 'percent', clamp(percent, 0, 1)); | ||
} | ||
} | ||
} | ||
|
||
AMP.extension(TAG, '0.1', AMP => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,10 @@ The path to the exported Bodymovin animation object | |
|
||
Whether the animation should be looping or not. `true` by default. Values can be: `true`|`false`|`number` | ||
|
||
##### noautoplay (optional) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's put a link to the actions
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
By default, an animation autoplays. If this attribute is added the video waits for an action to start playing. | ||
|
||
##### common attributes | ||
|
||
This element includes [common attributes](https://www.ampproject.org/docs/reference/common_attributes) extended to AMP components. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,10 @@ tags: { # <amp-bodymovin-animation> | |
name: "loop" | ||
value_regex_casei: "(false|number|true)" | ||
} | ||
attrs: { | ||
name: "noautoplay" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. may want to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
value: "" | ||
} | ||
attrs: { | ||
name: "src" | ||
mandatory: true | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -314,6 +314,24 @@ event.response</pre></td> | |
</tr> | ||
</table> | ||
|
||
### amp-bodymovin-animation | ||
<table> | ||
<tr> | ||
<th>Action</th> | ||
<th>Description</th> | ||
</tr> | ||
<tr> | ||
<td><code>play</code></td> | ||
<td>Plays the animation.</td> | ||
<td><code>pause</code></td> | ||
<td>Pauses the animation.</td> | ||
<td><code>stop</code></td> | ||
<td>Stops the animation.</td> | ||
<td><code>seekTo(time=INTEGER)</code></td> | ||
<td>Sets the currentTime of the animation to the specified value and pauses animation. </td> | ||
</tr> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
</table> | ||
|
||
### amp-carousel[type="slides"] | ||
<table> | ||
<tr> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice that Lottie already accepts percent as value!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I KNOW - I just need to read better next time. :o
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aghassemi / @nainar, where do you see that it accepts a percentage as value? I put together this JSFiddle to test this idea but it doesn't seem to work with values between 0 and 1. Thus, when I try to use
amp-bodymovin-animation
withamp-position-observer
, it doesn't work. Any ideas?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @jessepinho according to the API you also have to pass in an isFrame attribute set to
true
I believe for it to read it as a percentage value. Otherwise it uses a timebased value.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nainar When
isFrame
istrue
, the first parameter is the actual frame number. So, to get it to be a percentage, you'd have to multiply that number by the total number of frames.I can make a pull request to fix this if you're open to it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nainar See #15321