Commit ef2582df authored by Malay Keshav's avatar Malay Keshav Committed by Commit Bot

WebUI: Add cr-lottie element for lottie animations on CrOS

This patch introduces a new polymer element for chrome OS that is
responsible for running lottie animations on a third party player on a
worker thread.

Added unit tests.

Bug: 976057
Change-Id: I32323d758e432d7506fed6d9346b019cd9234653
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1725322
Commit-Queue: Malay Keshav <malaykeshav@chromium.org>
Reviewed-by: default avatarDan Beam <dbeam@chromium.org>
Reviewed-by: default avatarSteven Bennetts <stevenjb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#688677}
parent 8f3eea10
......@@ -669,3 +669,34 @@ TEST_F('CrElementsViewManagerTest', 'VisibilityTest', function() {
TEST_F('CrElementsViewManagerTest', 'EventFiringTest', function() {
runMochaTest(this.suiteName, cr_view_manager_test.TestNames.EventFiring);
});
GEN('#if defined(OS_CHROMEOS)');
/**
* @constructor
* @extends {CrElementsBrowserTest}
*/
function CrElementsLottieTest() {}
CrElementsLottieTest.prototype = {
__proto__: CrElementsBrowserTest.prototype,
/** @override */
browsePreload: 'chrome://resources/cr_elements/chromeos/cr_lottie/' +
'cr_lottie.html',
/** @override */
commandLineSwitches: [{
switchName: 'enable-pixel-output-in-tests',
}],
/** @override */
extraLibraries: CrElementsBrowserTest.prototype.extraLibraries.concat([
'../test_util.js',
'cr_lottie_tests.js',
]),
};
TEST_F('CrElementsLottieTest', 'All', function() {
mocha.run();
});
GEN('#endif');
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @fileoverview Suite of tests for cr-lottie. */
cr.define('cr_lottie_test', function() {
/**
* A data url that produces a sample json lottie animation.
* @type {string}
*/
const SAMPLE_LOTTIE = 'data:application/json;base64,eyJ2IjoiNC42LjkiLCJmci' +
'I6NjAsImlwIjowLCJvcCI6MjAwLCJ3Ijo4MDAsImgiOjYwMCwiZGRkIjowLCJhc3NldHM' +
'iOltdLCJsYXllcnMiOlt7ImluZCI6MSwidHkiOjEsInNjIjoiIzAwZmYwMCIsImFvIjow' +
'LCJpcCI6MCwib3AiOjIwMCwic3QiOjAsInNyIjoxLCJzdyI6ODAwLCJzaCI6NjAwLCJib' +
'SI6MCwia3MiOnsibyI6eyJhIjowLCJrIjoxMDB9LCJyIjp7ImEiOjAsImsiOlswLDAsMF' +
'19LCJwIjp7ImEiOjAsImsiOlszMDAsMjAwLDBdfSwiYSI6eyJhIjowLCJrIjpbMzAwLDI' +
'wMCwwXX0sInMiOnsiYSI6MCwiayI6WzEwMCwxMDAsMTAwXX19fV19';
/**
* A dataURL of an image for how a frame of the above |sampleLottie| animation
* looks like.
* @type {string}
*/
const EXPECTED_FRAME = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD' +
'/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1R' +
'V19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY' +
'2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCADIASwDASIAAhEBAx' +
'EB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAMGBwX/xAAdEAEAAAYDAAAAAAAAAAAAAAAAAQI' +
'DBDRzBrHB/8QAGAEBAAMBAAAAAAAAAAAAAAAAAAIEBgX/xAAbEQEAAgIDAAAAAAAAAAAA' +
'AAAAAQIyMwQFgf/aAAwDAQACEQMRAD8A5+ADotlhW+uXpZGywrfXL0szNspY++UgCKIAA' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAyfL82hr9i1jJ8vzaGv2K5wt0L3Xb49eAA7jSAAOi2WF' +
'b65elkbLCt9cvSzM2ylj75SAIogAAAAAAAAAAAAAAAAAAAAAAAAAAADJ8vzaGv2LWMny/' +
'Noa/YrnC3Qvddvj14ADuNIAA6LZYVvrl6WRssK31y9LMzbKWPvlIAiiAAAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAMny/Noa/YtYyfL82hr9iucLdC912+PXgAO40gADotlhW+uXpZGywrfX' +
'L0szNspY++UgCKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAyfL82hr9i1jJ8vzaGv2K5wt0L3' +
'Xb49eAA7jSAAOi2WFb65elkbLCt9cvSzM2ylj75SAIogAAAAAAAAAAAAAAAAAAAAAAAAA' +
'AADJ8vzaGv2LWMny/Noa/YrnC3Qvddvj14ADuNIAA6LZYVvrl6WRssK31y9LMzbKWPvlI' +
'AiiAAAAAAAAAAAAAAAAAAAAAAAAAAAAMny/Noa/YtYyfL82hr9iucLdC912+PXgAO40gA' +
'DotlhW+uXpZGywrfXL0szNspY++UgCKIAAAAAAAAAAAAAAAAAAAAAAAAAAAAyfL82hr9i' +
'1jJ8vzaGv2K5wt0L3Xb49eAA7jSAAOi2WFb65elkbLCt9cvSzM2ylj75SAIogAAAAAAAA' +
'AAAAAAAAAAAAAAAAAAADJ8vzaGv2LWMny/Noa/YrnC3Qvddvj14ADuNIAA6LZYVvrl6WR' +
'ssK31y9LMzbKWPvlIAiiAAAAAAAAAAAAAAAAAAAAAAAAAAAAMny/Noa/YtYyfL82hr9iu' +
'cLdC912+PXgAO40gADotlhW+uXpZGywrfXL0szNspY++UgCKIAAAAAAAAAAAAAAAAAAAA' +
'AAAAAAAAyfL82hr9i1jJ8vzaGv2K5wt0L3Xb49eAA7jSAAOi2WFb65elkbLCt9cvSzM2y' +
'lj75SAIogAAAAAAAAAAAAAAAAAAAAAAAAAAADJ8vzaGv2LWMny/Noa/YrnC3Qvddvj14A' +
'DuNIAA6LZYVvrl6WRssK31y9LMzbKWPvlIAiiAAAAAAAAAAAAAAAAAAAAAAAAAAAAMny/' +
'Noa/YtYyfL82hr9iucLdC912+PXgAO40gADotlhW+uXpYGZtlLH3ykARRAAAAAAAAAAAA' +
'AAAAAAAAAAAAAAAAGT5fm0NfsQXOFuhe67fHrwAHcaR//Z';
/** @type {!CrLottieElement} */
let crLottieElement;
/** @type {!HTMLDivElement} */
let container;
/** @type {?HTMLCanvasElement} */
let canvas = null;
setup(function() {
PolymerTest.clearBody();
crLottieElement = document.createElement('cr-lottie');
crLottieElement.animationUrl = SAMPLE_LOTTIE;
container = document.createElement('div');
container.style.width = '300px';
container.style.height = '200px';
document.body.appendChild(container);
container.appendChild(crLottieElement);
canvas = crLottieElement.offscreenCanvas_;
Polymer.dom.flush();
});
test('TestInitializeAnimationAndAutoPlay', async () => {
assertFalse(crLottieElement.isAnimationLoaded_);
const waitForInitializeEvent =
test_util.eventToPromise('cr-lottie-initialized', crLottieElement);
await waitForInitializeEvent;
assertTrue(crLottieElement.isAnimationLoaded_);
const waitForPlayingEvent =
test_util.eventToPromise('cr-lottie-playing', crLottieElement);
await waitForPlayingEvent;
});
test('TestResize', async () => {
const waitForInitializeEvent =
test_util.eventToPromise('cr-lottie-initialized', crLottieElement);
await waitForInitializeEvent;
const waitForPlayingEvent =
test_util.eventToPromise('cr-lottie-playing', crLottieElement);
await waitForPlayingEvent;
const newHeight = 300;
const newWidth = 400;
const waitForResizeEvent =
test_util.eventToPromise('cr-lottie-resized', crLottieElement)
.then(function(e) {
assertEquals(e.detail.height, newHeight);
assertEquals(e.detail.width, newWidth);
});
// Update size of parent div container to see if the canvas is resized.
container.style.width = newWidth + 'px';
container.style.height = newHeight + 'px';
await waitForResizeEvent;
});
test('TestRenderFrame', async () => {
// Offscreen canvas has a race issue when used in this test framework. To
// ensure that we capture a frame from the animation and not an empty frame,
// we delay the capture by 2 seconds.
// Note: This issue is only observed in tests.
const kRaceTimeout = 2000;
const waitForInitializeEvent =
test_util.eventToPromise('cr-lottie-initialized', crLottieElement);
await waitForInitializeEvent;
const waitForPlayingEvent =
test_util.eventToPromise('cr-lottie-playing', crLottieElement);
await waitForPlayingEvent;
const waitForFrameRender = new Promise(function(resolve) {
setTimeout(resolve, kRaceTimeout);
}).then(function() {
const actualFrame =
crLottieElement.canvasElement_.toDataURL('image/jpeg', 0.5);
assertEquals(actualFrame, EXPECTED_FRAME);
});
await waitForFrameRender;
});
});
......@@ -78,6 +78,9 @@ const std::map<std::string, std::string> CreatePathPrefixAliasesMap() {
};
#if defined(OS_CHROMEOS)
// Add lottie library for Chrome OS.
aliases["../../../third_party/lottie/"] = "lottie/";
if (UsingMultiplePolymerVersions())
return aliases;
#endif // defined(OS_CHROMEOS)
......@@ -421,6 +424,10 @@ void SharedResourcesDataSource::DisablePolymer2ForHost(
disabled_polymer2_host_ = host;
}
std::string SharedResourcesDataSource::GetContentSecurityPolicyWorkerSrc() {
return "worker-src blob: 'self';";
}
// Returns true if the WebContents making the request has disabled Polymer 2.
bool SharedResourcesDataSource::IsPolymer2DisabledForPage(
const WebContents::Getter& wc_getter) {
......
......@@ -34,6 +34,7 @@ class SharedResourcesDataSource : public URLDataSource {
const std::string& origin) override;
#if defined(OS_CHROMEOS)
void DisablePolymer2ForHost(const std::string& host) override;
std::string GetContentSecurityPolicyWorkerSrc() override;
#endif // defined (OS_CHROMEOS)
private:
......
......@@ -8,6 +8,7 @@ assert(is_chromeos, "Only ChromeOS components belong here.")
group("closure_compile") {
deps = [
"cr_lottie:closure_compile",
"cr_picture:closure_compile",
"network:closure_compile",
]
......
import("//third_party/closure_compiler/compile_js.gni")
js_type_check("closure_compile") {
deps = [
":cr_lottie",
]
}
js_library("cr_lottie") {
deps = [
"//ui/webui/resources/js:cr",
]
externs_list = [ "$externs_path/pending.js" ]
}
<link rel="import" href="../../../html/assert.html">
<link rel="import" href="../../../html/polymer.html">
<dom-module id="cr-lottie">
<template>
<style>
canvas {
height: 100%;
width: 100%;
}
</style>
<canvas id="canvas"></canvas>
</template>
<script src="cr_lottie.js"></script>
</dom-module>
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview 'cr-lottie' is a wrapper around the player for lottie
* animations.
* Fires a 'cr-lottie-initialized' event when the animation was successfully
* initialized.
* Fires a 'cr-lottie-playing' event when the animation starts playing.
* Fires a 'cr-lottie-resized' event when the canvas the animation is being
* drawn on is resized.
*/
/**
* The resource url for the lottier web worker script.
* @const {string}
*/
const LOTTIE_JS_URL = 'chrome://resources/lottie/lottie_worker.min.js';
Polymer({
is: 'cr-lottie',
properties: {
animationUrl: {
type: String,
value: '',
},
},
/** @private {?HTMLCanvasElement} */
canvasElement_: null,
/** @private {boolean} True if the animation has loaded successfully */
isAnimationLoaded_: false,
/** @private {?OffscreenCanvas} */
offscreenCanvas_: null,
/** @private {?ResizeObserver} */
resizeObserver_: null,
/** @private {?Worker} */
worker_: null,
/** @override */
attached: function() {
// CORS blocks loading worker script from a different origin but
// loading scripts as blob and then instantiating it as web worker
// is possible.
this.sendXmlHttpRequest_(LOTTIE_JS_URL, 'blob', function(response) {
if (this.isAttached) {
this.worker_ = new Worker(URL.createObjectURL(response));
this.worker_.onmessage = this.onMessage_.bind(this);
this.initialize_();
}
}.bind(this));
},
/** @override */
detached: function() {
if (this.resizeObserver_) {
this.resizeObserver_.disconnect();
}
if (this.worker_) {
this.worker_.terminate();
this.worker_ = null;
}
},
/**
* Initializes all the members of this polymer element.
* @private
*/
initialize_: function() {
// Generate an offscreen canvas.
this.canvasElement_ =
/** @type {HTMLCanvasElement} */ (this.$.canvas);
this.offscreenCanvas_ = this.canvasElement_.transferControlToOffscreen();
this.resizeObserver_ =
new ResizeObserver(this.onCanvasElementResized_.bind(this));
this.resizeObserver_.observe(this.canvasElement_);
if (this.isAnimationLoaded_) {
return;
}
// Open animation file and start playing the animation.
this.sendXmlHttpRequest_(
this.animationUrl, 'json', this.startAnimation_.bind(this));
},
/**
* Computes the draw buffer size for the canvas. This ensures that the
* rasterization is crisp and sharp rather than blurry.
* @return {Object} Size of the canvas draw buffer
* @private
*/
getCanvasDrawBufferSize_: function() {
const canvasElement = this.$.canvas;
const devicePixelRatio = window.devicePixelRatio;
const clientRect = canvasElement.getBoundingClientRect();
const drawSize = {
width: clientRect.width * devicePixelRatio,
height: clientRect.height * devicePixelRatio
};
return drawSize;
},
/**
* Returns true if the |maybeValidUrl| provided is safe to use in an
* XMLHTTPRequest.
* @param {string} maybeValidUrl The url string to check for validity.
* @return {boolean}
* @private
*/
isValidUrl_: function(maybeValidUrl) {
const url = new URL(maybeValidUrl, document.location.href);
return url.protocol === 'chrome:' ||
(url.protocol == 'data:' &&
url.pathname.startsWith('application/json;'));
},
/**
* Sends an XMLHTTPRequest to load a resource and runs the callback on
* getting a successful response.
* @param {string} url The URL to load the resource.
* @param {string} responseType The type of response the request would
* give on success.
* @param {function((Object|null|string))} successCallback The callback to run
* when a successful response is received.
* @private
*/
sendXmlHttpRequest_: function(url, responseType, successCallback) {
assert(this.isValidUrl_(url), 'Invalid scheme or data url used.');
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = responseType;
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
successCallback(xhr.response);
}
};
},
/**
* Handles the canvas element resize event. This informs the offscreen
* canvas worker of the new canvas size.
* @private
*/
onCanvasElementResized_: function() {
if (this.isAnimationLoaded_) {
this.worker_.postMessage({drawSize: this.getCanvasDrawBufferSize_()});
}
},
/**
* Starts playing the animation on the canvas.
* @param {Object|null|string} animationData The animation that will be
* played.
* @private
*/
startAnimation_: function(animationData) {
this.worker_.postMessage(
{
canvas: this.offscreenCanvas_,
animationData: animationData,
drawSize: this.getCanvasDrawBufferSize_(),
params: {loop: true, autoplay: true}
},
[this.offscreenCanvas_]);
},
/**
* Handles the messages sent from the web worker to its parent thread.
* @param {Event} event Event sent by the web worker.
* @private
*/
onMessage_: function(event) {
if (event.data.name == 'initialized' && event.data.success) {
this.isAnimationLoaded_ = true;
this.fire('cr-lottie-initialized');
} else if (event.data.name == 'playing') {
this.fire('cr-lottie-playing');
} else if (event.data.name == 'resized') {
this.fire('cr-lottie-resized', event.data.size);
}
},
});
......@@ -272,6 +272,14 @@
type="chrome_html"
preprocess="true"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CHROMEOS_CR_LOTTIE_HTML"
file="cr_elements/chromeos/cr_lottie/cr_lottie.html"
type="chrome_html"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CHROMEOS_CR_LOTTIE_JS"
file="cr_elements/chromeos/cr_lottie/cr_lottie.js"
type="chrome_html"
compress="gzip" />
</if>
<structure name="IDR_CR_ELEMENTS_CR_POLICY_INDICATOR_HTML"
file="cr_elements/policy/cr_policy_indicator.html"
......
......@@ -428,6 +428,13 @@ without changes to the corresponding grd file. -->
compress="gzip" />
</if>
<if expr="chromeos">
<structure name="IDR_LOTTIE_LOTTIE_WORKER_MIN_JS"
file="../../../third_party/lottie/lottie_worker.min.js"
type="chrome_html"
compress="gzip" />
</if>
<if expr="not is_android and not is_ios">
<part file="cr_components/cr_components_resources.grdp" />
<part file="cr_elements_resources.grdp" />
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment