index.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. /**
  2. * This traverses all json files located on the examples folder, then iterates
  3. * over each file and opens a puppeteer page to a screenshot of all frames
  4. * combined in a single page.
  5. */
  6. const puppeteer = require('puppeteer');
  7. const express = require('express');
  8. const fs = require('fs');
  9. const { promises: { readFile } } = require('fs');
  10. const commandLineArgs = require('command-line-args');
  11. const PNG = require('pngjs').PNG;
  12. const pixelmatch = require('pixelmatch');
  13. const examplesDirectory = '/test/animations/';
  14. const createDirectory = 'screenshots/create';
  15. const compareDirectory = 'screenshots/compare';
  16. function createDirectoryPath(path) {
  17. const directories = path.split('/');
  18. directories.reduce((acc, current) => {
  19. let dir = acc + '/' + current
  20. if (!fs.existsSync(dir)) {
  21. fs.mkdirSync(dir);
  22. }
  23. return dir
  24. }, '.')
  25. }
  26. const animations = [
  27. {
  28. fileName: 'pigeon.json',
  29. renderer: 'svg',
  30. },
  31. {
  32. fileName: 'banner.json',
  33. renderer: 'svg',
  34. },
  35. {
  36. fileName: 'adrock.json',
  37. renderer: 'canvas',
  38. },
  39. {
  40. fileName: 'bm_ronda.json',
  41. renderer: 'svg',
  42. },
  43. {
  44. fileName: 'bodymovin.json',
  45. renderer: 'svg',
  46. },
  47. {
  48. fileName: 'bodymovin.json',
  49. renderer: 'canvas',
  50. },
  51. {
  52. fileName: 'dalek.json',
  53. renderer: 'svg',
  54. },
  55. {
  56. fileName: 'navidad.json',
  57. renderer: 'svg',
  58. },
  59. {
  60. fileName: 'monster.json',
  61. renderer: 'svg',
  62. },
  63. {
  64. fileName: 'bacon.json',
  65. renderer: 'svg',
  66. },
  67. {
  68. fileName: 'lights.json',
  69. renderer: 'svg',
  70. },
  71. {
  72. fileName: 'ripple.json',
  73. renderer: 'svg',
  74. },
  75. {
  76. fileName: 'starfish.json',
  77. renderer: 'svg',
  78. },
  79. {
  80. directory: 'footage',
  81. fileName: 'data.json',
  82. renderer: 'svg',
  83. },
  84. ]
  85. const getSettings = async () => {
  86. const defaultValues = {
  87. step: 'create',
  88. }
  89. const opts = [
  90. {
  91. name: 'step',
  92. alias: 's',
  93. type: (val) => {
  94. return val === 'compare' ? 'compare' : 'create';
  95. },
  96. description: 'Whether it is the create or the compare step',
  97. }
  98. ];
  99. const settings = {
  100. ...defaultValues,
  101. ...commandLineArgs(opts),
  102. };
  103. return settings;
  104. };
  105. const wait = (time) => new Promise((resolve) => setTimeout(resolve, time));
  106. const filesData = [
  107. {
  108. path: '/test/index.html',
  109. filePath: './test/index.html',
  110. type: 'html',
  111. },
  112. {
  113. path: '/lottie.min.js',
  114. filePath: './build/player/lottie.min.js',
  115. type: 'js',
  116. },
  117. ];
  118. const getEncoding = (() => {
  119. const encodingMap = {
  120. js: 'utf8',
  121. json: 'utf8',
  122. html: 'utf8',
  123. };
  124. return (fileType) => encodingMap[fileType];
  125. })();
  126. const getContentTypeHeader = (() => {
  127. const contentTypeMap = {
  128. js: { 'Content-Type': 'application/javascript' },
  129. json: { 'Content-Type': 'application/json' },
  130. html: { 'Content-Type': 'text/html; charset=utf-8' },
  131. wasm: { 'Content-Type': 'application/wasm' },
  132. };
  133. return (fileType) => contentTypeMap[fileType];
  134. })();
  135. const startServer = async () => {
  136. const app = express();
  137. await Promise.all(filesData.map(async (file) => {
  138. const fileData = await readFile(file.filePath, getEncoding(file.type));
  139. app.get(file.path, async (req, res) => {
  140. res.writeHead(200, getContentTypeHeader(file.type));
  141. // TODO: comment line. Only for live updates.
  142. const fileData = await readFile(file.filePath, getEncoding(file.type));
  143. res.end(fileData);
  144. });
  145. return file;
  146. }));
  147. app.get('/*', async (req, res) => {
  148. try {
  149. if (req.originalUrl.indexOf('.json') !== -1) {
  150. const file = await readFile(`.${req.originalUrl}`, 'utf8');
  151. res.send(file);
  152. } else {
  153. const data = await readFile(`.${req.originalUrl}`);
  154. res.writeHead(200, { 'Content-Type': 'image/jpeg' });
  155. res.end(data);
  156. }
  157. } catch (err) {
  158. res.send('');
  159. }
  160. });
  161. app.listen('9999');
  162. };
  163. const getBrowser = async () => puppeteer.launch({ defaultViewport: null });
  164. const startPage = async (browser, path, renderer) => {
  165. const targetURL = `http://localhost:9999/test/index.html\
  166. ?path=${encodeURIComponent(path)}&renderer=${renderer}`;
  167. const page = await browser.newPage();
  168. page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); // eslint-disable-line no-console
  169. await page.setViewport({
  170. width: 1024,
  171. height: 768,
  172. });
  173. await page.goto(targetURL);
  174. return page;
  175. };
  176. const createBridgeHelper = async (page) => {
  177. let resolveScoped;
  178. let animationLoadedPromise;
  179. const messageHandler = (event) => {
  180. resolveScoped(event);
  181. };
  182. const onAnimationLoaded = () => {
  183. if (animationLoadedPromise) {
  184. animationLoadedPromise()
  185. }
  186. }
  187. await page.exposeFunction('onAnimationLoaded', onAnimationLoaded);
  188. await page.exposeFunction('onMessageReceivedEvent', messageHandler);
  189. const waitForMessage = () => new Promise((resolve) => {
  190. resolveScoped = resolve;
  191. });
  192. const waitForAnimationLoaded = () => new Promise((resolve) => {
  193. animationLoadedPromise = resolve;
  194. });
  195. const continueExecution = async () => {
  196. page.evaluate(() => {
  197. window.continueExecution();
  198. });
  199. };
  200. return {
  201. waitForAnimationLoaded,
  202. waitForMessage,
  203. continueExecution,
  204. };
  205. };
  206. const compareFiles = (folderName, fileName) => {
  207. const createPath = `${createDirectory}/${folderName}/${fileName}`;
  208. const comparePath = `${compareDirectory}/${folderName}/${fileName}`;
  209. const img1 = PNG.sync.read(fs.readFileSync(createPath));
  210. const img2 = PNG.sync.read(fs.readFileSync(comparePath));
  211. const {width, height} = img1;
  212. const diff = new PNG({width, height});
  213. const result = pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: 0.1});
  214. // Using 50 as threshold because it should be an acceptable difference
  215. // that doesn't raise false positives
  216. if (result > 200) {
  217. console.log('RESULT NOT ZERO: ', result);
  218. throw new Error(`Animation failed: ${folderName} at frame: ${fileName}`)
  219. }
  220. }
  221. const createIndividualAssets = async (page, folderName, settings) => {
  222. const filePath = `${settings.step === 'create' ? createDirectory : compareDirectory}/${folderName}`;
  223. createDirectoryPath(filePath);
  224. let isLastFrame = false;
  225. const bridgeHelper = await (createBridgeHelper(page));
  226. page.evaluate(() => {
  227. window.startProcess();
  228. });
  229. await bridgeHelper.waitForAnimationLoaded();
  230. while (!isLastFrame) {
  231. // Disabling rule because execution can't be parallelized
  232. /* eslint-disable no-await-in-loop */
  233. await wait(1);
  234. page.evaluate(() => {
  235. window.continueExecution();
  236. });
  237. const message = await bridgeHelper.waitForMessage();
  238. const fileNumber = message.currentFrame.toString().padStart(5, '0');
  239. const fileName = `image_${fileNumber}.png`;
  240. const localDestinationPath = `${filePath}/${fileName}`;
  241. await page.screenshot({
  242. path: localDestinationPath,
  243. fullPage: false,
  244. });
  245. if (settings.step === 'compare') {
  246. try {
  247. compareFiles(folderName, fileName);
  248. } catch (err) {
  249. console.log('FAILED AT FRAME: ', message.currentFrame);
  250. throw err;
  251. }
  252. }
  253. isLastFrame = message.isLast;
  254. }
  255. };
  256. const getDirFiles = async (directory) => (
  257. new Promise((resolve, reject) => {
  258. fs.readdir(directory, (err, files) => {
  259. if (err) {
  260. reject(err);
  261. } else {
  262. resolve(files);
  263. }
  264. });
  265. })
  266. );
  267. async function processPage(browser, settings, directory, animation) {
  268. let fullDirectory = `${directory}`;
  269. if (animation.directory) {
  270. fullDirectory += `${animation.directory}/`;
  271. }
  272. const fileName = animation.fileName;
  273. const page = await startPage(browser, fullDirectory + fileName, animation.renderer);
  274. const fileNameWithoutExtension = fileName.replace(/\.[^/.]+$/, '');
  275. let fullName = `${fileNameWithoutExtension}_${animation.renderer}`
  276. if (animation.directory) {
  277. fullName = `${animation.directory}_` + fullName;
  278. }
  279. await createIndividualAssets(page, fullName, settings);
  280. }
  281. const iteratePages = async (browser, settings) => {
  282. const failedAnimations = [];
  283. for (let i = 0; i < animations.length; i += 1) {
  284. const animation = animations[i];
  285. let fileName = `${animation.renderer}_${animation.fileName}`;
  286. if (animation.directory) {
  287. fileName = `${animation.directory}_` + fileName;
  288. }
  289. try {
  290. // eslint-disable-next-line no-await-in-loop
  291. await processPage(browser, settings, examplesDirectory, animation);
  292. if (settings.step === 'create') {
  293. console.log(`Creating animation: ${fileName}`);
  294. }
  295. if (settings.step === 'compare') {
  296. console.log(`Animation passed: ${fileName}`);
  297. }
  298. } catch (error) {
  299. if (settings.step === 'compare') {
  300. failedAnimations.push({
  301. fileName: fileName
  302. })
  303. }
  304. }
  305. }
  306. if (failedAnimations.length) {
  307. failedAnimations.forEach(animation => {
  308. console.log(`Animation failed: ${animation.fileName}`);
  309. })
  310. throw new Error('Animations failed');
  311. }
  312. };
  313. const takeImageStrip = async () => {
  314. try {
  315. await startServer();
  316. const settings = await getSettings();
  317. await wait(1);
  318. const browser = await getBrowser();
  319. await iteratePages(browser, settings);
  320. process.exit(0);
  321. } catch (error) {
  322. console.log(error); // eslint-disable-line no-console
  323. process.exit(1);
  324. }
  325. };
  326. takeImageStrip();