Greasy Fork is available in English.

YouTube Live Filled Up View

Get maximized video-and-chat view with no margins on YouTube Live or Premieres.

  1. // ==UserScript==
  2. // @name YouTube Live Filled Up View
  3. // @name:ja YouTube Live Filled Up View
  4. // @name:zh-CN YouTube Live Filled Up View
  5. // @description Get maximized video-and-chat view with no margins on YouTube Live or Premieres.
  6. // @description:ja YouTube Live やプレミア公開のチャット付きビューで、余白を切り詰めて映像を最大化します。
  7. // @description:zh-CN 在油管中的 YouTube Live 或首映公开的带聊天视图中,截取空白以最大化映像。
  8. // @namespace knoa.jp
  9. // @include https://www.youtube.com/*
  10. // @version 1.2.5
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function(){
  15. const SCRIPTID = 'YouTubeLiveFilledUpView';
  16. const SCRIPTNAME = 'YouTube Live Filled Up View';
  17. const DEBUG = false;/*
  18. [update]
  19. Now compatibile for Firefox + Greasemonkey. Minor fixes.
  20.  
  21. [bug]
  22.  
  23. [todo]
  24. /live 待機ページで左サイドバーが常時表示化されてる?自然と直った?
  25. 右側に表示されるチャプター一覧にも対応してほしいらしいね
  26.  
  27. https://greasyfork.org/ja/scripts/394945-youtube-live-filled-up-view/discussions/64579
  28. Maybe I'll modify the script as below:
  29. - normal mode with chat for keeping current "Filled Up View"
  30. - normal mode with chat closed for (meaningless) YouTube's default
  31. - theater mode with chat for "Filled Up View" with no distracting elements.
  32. - theater mode with chat closed for "Filled Up View" with neither distracting elements nor chats.
  33.  
  34. コメ欄の比率カスタマイズ (greasyforkでの要望)
  35. ついでにフォントサイズ、アイコンの有無、余白なども?
  36. 上部ヘッダをマウスオーバーでのみとかなんとか、なんらかの対処はしたい
  37. チャットを非表示もXボタン化してヘッダ右上に
  38.  
  39. [possible]
  40.  
  41. [research]
  42. ヘッダを隠す方法
  43. LIVEに限らずBigTubeの代替を目指すか
  44. 上部マウスホバーで出現、スクロール上端からさらに上にスクロールでもx秒出現、下スクロールで出現、くらいかな
  45. 動画上部とカチャッと上部のコントロール要素をクリックできない問題は回避しなければならない
  46. あくまでスクロー位置で調整することにして、多少の前後は自動スクロールでカバーするとか
  47. きほんシアターモードへの移行でスクロール位置調整
  48. むしろ本来的にヘッダを隠すのはシアターモードだけでいいよね
  49. スクロールイベントで3秒表示という手段
  50. https://greasyfork.org/ja/scripts/414234-youtube-auto-hide-header
  51. infoの高さが変わりうるwindow.onresizeに対応か(パフォーマンスはスロットルすれば平気か)
  52.  
  53. [memo]
  54. ダークモードはそれ用のユーザースタイルに任せるべき。
  55. YouTubeのヘッダが常時表示なのはちょっと気にかかるが、高さにはまだ余裕がある。
  56. 横幅1920時のinnerHeight: 910px, ChromeのUI: 約100px, Windowsタスクバー: 約40px
  57. */
  58. if(window === top && console.time) console.time(SCRIPTID);
  59. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  60. const INTERVAL = 1*SECOND;/*for core.checkUrl*/
  61. const DEFAULTRATIO = 9/16;/*for waiting page*/
  62. const VIDEOURLS = [/*for core.checkUrl*/
  63. /^https:\/\/www\.youtube\.com\/watch\?/,
  64. /^https:\/\/www\.youtube\.com\/channel\/[^/]+\/live/,
  65. ];
  66. const CHATURLS = [/*for core.checkUrl*/
  67. /^https:\/\/www\.youtube\.com\/live_chat\?/,
  68. /^https:\/\/www\.youtube\.com\/live_chat_replay\?/,
  69. ];
  70. let site = {
  71. videoTargets: {
  72. watchFlexy: () => $('ytd-watch-flexy'),
  73. video: () => $('#movie_player video'),
  74. info: () => $('ytd-video-primary-info-renderer'),
  75. chat: () => $('ytd-live-chat-frame#chat'),
  76. },
  77. is: {
  78. video: () => VIDEOURLS.some(url => url.test(location.href)),
  79. chat: () => CHATURLS.some(url => url.test(location.href)),
  80. opened: (chat) => (chat.isConnected === true && chat.hasAttribute('collapsed') === false),
  81. theater: () => (elements.watchFlexy.theater === true),/* YouTube uses this property */
  82. },
  83. get: {
  84. sizeButton: () => $('button.ytp-size-button'),
  85. chatFrameToggleButton: () => $('ytd-live-chat-frame paper-button'),
  86. },
  87. chatTargets: {
  88. items: () => $('yt-live-chat-item-list-renderer #items'),
  89. },
  90. };
  91. let elements = {}, timers = {}, sizes = {};
  92. let core = {
  93. initialize: function(){
  94. elements.html = document.documentElement;
  95. elements.html.classList.add(SCRIPTID);
  96. if(site.is.chat()){
  97. core.readyForChat();
  98. }else{
  99. core.checkUrl();
  100. }
  101. },
  102. checkUrl: function(){
  103. let previousUrl = '';
  104. timers.checkUrl = setInterval(function(){
  105. if(document.hidden) return;
  106. /* The page is visible, so... */
  107. if(location.href === previousUrl) return;
  108. else previousUrl = location.href;
  109. /* The URL has changed, so... */
  110. core.clearVideoStyle();
  111. if(site.is.video()) return core.readyForVideo();
  112. }, INTERVAL);
  113. },
  114. clearVideoStyle: function(){
  115. if(elements.videoStyle && elements.videoStyle.isConnected){
  116. document.head.removeChild(elements.videoStyle);
  117. }
  118. },
  119. addVideoStyle: function(e){
  120. let chat = elements.chat;
  121. if(site.is.opened(chat) === false) return;
  122. /* it also replaces old style */
  123. let video = elements.video;
  124. /* adapt to the aspect ratio */
  125. if(video.videoWidth){
  126. sizes.videoAspectRatio = video.videoHeight / video.videoWidth;
  127. core.addStyle('videoStyle');
  128. }else{
  129. sizes.videoAspectRatio = DEFAULTRATIO;
  130. core.addStyle('videoStyle');
  131. video.addEventListener('canplay', core.addVideoStyle, {once: true});
  132. }
  133. /* for Ads replacing */
  134. if(video.dataset.observed === 'true') return;
  135. video.dataset.observed = 'true';
  136. observe(video, function(records){
  137. video.addEventListener('canplay', core.addVideoStyle, {once: true});
  138. }, {attributes: true, attributeFilter: ['src']});
  139. },
  140. readyForVideo: function(){
  141. core.getTargets(site.videoTargets).then(() => {
  142. log("I'm ready for Video with Chat.");
  143.  
  144. sizes.scrollbarWidth = getScrollbarWidth();
  145. core.getInfoHeight();
  146. core.replacePlayerSize();
  147. core.observeChatFrame();
  148. core.enterDefaultMode();
  149. }).catch(e => {
  150. console.error(`${SCRIPTID}:${e.lineNumber} ${e.name}: ${e.message}`);
  151. });
  152. },
  153. getInfoHeight: function(){
  154. let info = elements.info;
  155. if(info.offsetHeight === 0) return setTimeout(core.getInfoHeight, 1000);
  156. sizes.infoHeight = elements.info.offsetHeight;
  157. },
  158. replacePlayerSize: function(){
  159. /* update the size */
  160. setTimeout(() => window.dispatchEvent(new Event('resize')), 1000);
  161. let watchFlexy = elements.watchFlexy;
  162. if(watchFlexy.calculateNormalPlayerSize_original) return;
  163. /* Thanks for Iridium.user.js > initializeBypasses */
  164. watchFlexy.calculateNormalPlayerSize_original = watchFlexy.calculateNormalPlayerSize_;
  165. watchFlexy.calculateCurrentPlayerSize_original = watchFlexy.calculateCurrentPlayerSize_;
  166. watchFlexy.calculateNormalPlayerSize_ = watchFlexy.calculateCurrentPlayerSize_ = function(){
  167. let video = elements.video, chat = elements.chat;
  168. if(site.is.opened(chat) === false){/* chat is closed */
  169. return watchFlexy.calculateCurrentPlayerSize_original();
  170. }else{
  171. if(watchFlexy.theater) return {width: NaN, height: NaN};
  172. else return {width: video.offsetWidth, height: video.offsetHeight};
  173. }
  174. };
  175. },
  176. observeChatFrame: function(){
  177. let chat = elements.chat, isOpened = site.is.opened(chat), button = site.get.chatFrameToggleButton();
  178. if(!isOpened && button){
  179. button.click();
  180. isOpened = !isOpened;
  181. }
  182. core.addVideoStyle();
  183. observe(chat, function(records){
  184. if(site.is.opened(chat) === isOpened) return;
  185. isOpened = !isOpened;
  186. window.dispatchEvent(new Event('resize'));/*for updating controls tooltip positions*/
  187. if(isOpened) return core.addVideoStyle();
  188. else return core.clearVideoStyle();
  189. }, {attributes: true});
  190. },
  191. enterDefaultMode: function(){
  192. if(site.is.theater() === false) return;/* already in default mode */
  193. let sizeButton = site.get.sizeButton();
  194. if(sizeButton) sizeButton.click();
  195. },
  196. readyForChat: function(){
  197. core.getTargets(site.chatTargets).then(() => {
  198. log("I'm ready for Chat.");
  199. core.addStyle('chatStyle');
  200. }).catch(e => {
  201. console.error(`${SCRIPTID}:${e.lineNumber} ${e.name}: ${e.message}`);
  202. });
  203. },
  204. getTarget: function(selector, retry = 10, interval = 1*SECOND){
  205. const key = selector.name;
  206. const get = function(resolve, reject){
  207. let selected = selector();
  208. if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
  209. else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
  210. else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject);
  211. else return reject(new Error(`Not found: ${selector.name}, I give up.`));
  212. elements[key] = selected;
  213. resolve(selected);
  214. };
  215. return new Promise(function(resolve, reject){
  216. get(resolve, reject);
  217. });
  218. },
  219. getTargets: function(selectors, retry = 10, interval = 1*SECOND){
  220. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval)));
  221. },
  222. addStyle: function(name = 'style'){
  223. if(html[name] === undefined) return;
  224. let style = createElement(html[name]());
  225. document.head.appendChild(style);
  226. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  227. elements[name] = style;
  228. },
  229. };
  230. const html = {
  231. videoStyle: () => `
  232. <style type="text/css" id="${SCRIPTID}-videoStyle">
  233. /* common */
  234. ytd-watch-flexy{
  235. --${SCRIPTID}-header-height: var(--ytd-watch-flexy-masthead-height);
  236. --${SCRIPTID}-info-height: ${sizes.infoHeight}px;
  237. --ytd-watch-flexy-width-ratio: 1 !important;/*fix for Iridium bug*/
  238. --ytd-watch-width-ratio: 1 !important;/*fix for Iridium bug*/
  239. --ytd-watch-flexy-height-ratio: ${sizes.videoAspectRatio} !important;/*fix for Iridium bug*/
  240. --ytd-watch-height-ratio: ${sizes.videoAspectRatio} !important;/*fix for Iridium bug*/
  241. }
  242. ytd-watch-flexy[is-two-columns_]{
  243. --${SCRIPTID}-primary-width: calc(100vw - ${sizes.scrollbarWidth}px - var(--ytd-watch-flexy-sidebar-width));
  244. --${SCRIPTID}-secondary-width: var(--ytd-watch-flexy-sidebar-width);
  245. --${SCRIPTID}-video-height: calc(var(--${SCRIPTID}-primary-width) * ${sizes.videoAspectRatio});
  246. }
  247. ytd-watch-flexy:not([is-two-columns_]){
  248. --${SCRIPTID}-primary-width: 100vw;
  249. --${SCRIPTID}-secondary-width: 100vw;
  250. --${SCRIPTID}-video-height: calc(100vw * ${sizes.videoAspectRatio});
  251. }
  252. /* columns */
  253. #columns{
  254. max-width: 100% !important;
  255. }
  256. #primary{
  257. max-width: var(--${SCRIPTID}-primary-width) !important;
  258. min-width: var(--${SCRIPTID}-primary-width) !important;
  259. padding: 0 !important;
  260. margin: 0 !important;
  261. }
  262. #secondary{
  263. max-width: var(--${SCRIPTID}-secondary-width) !important;
  264. min-width: var(--${SCRIPTID}-secondary-width) !important;
  265. padding: 0 !important;
  266. margin: 0 !important;
  267. }
  268. #player-container-outer,
  269. yt-live-chat-app{
  270. max-width: 100% !important;
  271. min-width: 100% !important;
  272. }
  273. #primary-inner > *:not(#player){
  274. padding: 0 24px 0;
  275. }
  276. /* video */
  277. #player,
  278. #player *{
  279. max-height: calc(100vh - var(--${SCRIPTID}-header-height)) !important;
  280. }
  281. #movie_player .html5-video-container{
  282. height: 100% !important;
  283. }
  284. #movie_player .ytp-chrome-bottom/*controls*/{
  285. width: calc(100% - 24px) !important;/*fragile!!*/
  286. max-width: calc(100% - 24px) !important;/*fragile!!*/
  287. }
  288. #movie_player video{
  289. max-width: 100% !important;
  290. width: 100% !important;
  291. height: 100% !important;
  292. left: 0px !important;
  293. background: black;
  294. }
  295. .ended-mode video{
  296. display: none !important;/*avoid conflicting with Iridium*/
  297. }
  298. /* chatframe */
  299. ytd-watch-flexy[is-two-columns_] ytd-live-chat-frame#chat{
  300. height: calc(var(--${SCRIPTID}-video-height) + var(--${SCRIPTID}-info-height)) !important;
  301. min-height: auto !important;
  302. max-height: calc(100vh - var(--${SCRIPTID}-header-height)) !important;
  303. border-right: none;
  304. }
  305. ytd-watch-flexy:not([is-two-columns_]) ytd-live-chat-frame#chat{
  306. padding: 0 !important;
  307. margin: 0 !important;
  308. height: calc(100vh - var(--${SCRIPTID}-header-height) - var(--${SCRIPTID}-video-height)) !important;
  309. min-height: auto !important;
  310. border-top: none;
  311. }
  312. </style>
  313. `,
  314. chatStyle: () => `
  315. <style type="text/css" id="${SCRIPTID}-chatStyle">
  316. /* common */
  317. :root{
  318. --${SCRIPTID}-slight-shadow: drop-shadow(0 0 1px rgba(0,0,0,.1));
  319. }
  320. /* header and footer */
  321. yt-live-chat-header-renderer/*header*/{
  322. filter: var(--${SCRIPTID}-slight-shadow);
  323. z-index: 100;
  324. }
  325. #contents > #ticker/*superchat*/{
  326. filter: var(--${SCRIPTID}-slight-shadow);
  327. }
  328. #contents > #ticker/*superchat*/ > yt-live-chat-ticker-renderer > #container > *{
  329. padding-top: 4px;
  330. padding-bottom: 4px;
  331. }
  332. iron-pages#panel-pages/*footer*/{
  333. border-top: 1px solid rgba(128,128,128,.125);
  334. }
  335. /* body */
  336. #docked-item.yt-live-chat-docked-message-renderer/*sticky on the top*/,
  337. #undocking-item.yt-live-chat-docked-message-renderer/*sticky on the top*/{
  338. margin: 8px 0;
  339. }
  340. #docked-item.yt-live-chat-docked-message-renderer/*sticky on the top*/ > *,
  341. #undocking-item.yt-live-chat-docked-message-renderer/*sticky on the top*/ > *{
  342. filter: var(--${SCRIPTID}-slight-shadow);
  343. }
  344. #docked-item.yt-live-chat-docked-message-renderer/*sticky on the top*/ > *,
  345. #undocking-item.yt-live-chat-docked-message-renderer/*sticky on the top*/ > *,
  346. #items.yt-live-chat-item-list-renderer/*normal chats*/ > *:not(yt-live-chat-placeholder-item-renderer){
  347. padding: 2px 10px !important;
  348. }
  349. </style>
  350. `,
  351. };
  352. const setTimeout = window.setTimeout.bind(window), clearTimeout = window.clearTimeout.bind(window), setInterval = window.setInterval.bind(window), clearInterval = window.clearInterval.bind(window), requestAnimationFrame = window.requestAnimationFrame.bind(window);
  353. const alert = window.alert.bind(window), confirm = window.confirm.bind(window), prompt = window.prompt.bind(window), getComputedStyle = window.getComputedStyle.bind(window), fetch = window.fetch.bind(window);
  354. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  355. class Storage{
  356. static key(key){
  357. return (SCRIPTID) ? (SCRIPTID + '-' + key) : key;
  358. }
  359. static save(key, value, expire = null){
  360. key = Storage.key(key);
  361. localStorage[key] = JSON.stringify({
  362. value: value,
  363. saved: Date.now(),
  364. expire: expire,
  365. });
  366. }
  367. static read(key){
  368. key = Storage.key(key);
  369. if(localStorage[key] === undefined) return undefined;
  370. let data = JSON.parse(localStorage[key]);
  371. if(data.value === undefined) return data;
  372. if(data.expire === undefined) return data;
  373. if(data.expire === null) return data.value;
  374. if(data.expire < Date.now()) return localStorage.removeItem(key);
  375. return data.value;
  376. }
  377. static delete(key){
  378. key = Storage.key(key);
  379. delete localStorage.removeItem(key);
  380. }
  381. static saved(key){
  382. key = Storage.key(key);
  383. if(localStorage[key] === undefined) return undefined;
  384. let data = JSON.parse(localStorage[key]);
  385. if(data.saved) return data.saved;
  386. else return undefined;
  387. }
  388. }
  389. const $ = function(s, f){
  390. let target = document.querySelector(s);
  391. if(target === null) return null;
  392. return f ? f(target) : target;
  393. };
  394. const $$ = function(s){return document.querySelectorAll(s)};
  395. const createElement = function(html = '<span></span>'){
  396. let outer = document.createElement('div');
  397. outer.innerHTML = html;
  398. return outer.firstElementChild;
  399. };
  400. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  401. let observer = new MutationObserver(callback.bind(element));
  402. observer.observe(element, options);
  403. return observer;
  404. };
  405. const getScrollbarWidth = function(){
  406. let div = document.createElement('div');
  407. div.textContent = 'dummy';
  408. document.body.appendChild(div);
  409. div.style.overflowY = 'scroll';
  410. let clientWidth = div.clientWidth;
  411. div.style.overflowY = 'hidden';
  412. let offsetWidth = div.offsetWidth;
  413. document.body.removeChild(div);
  414. return offsetWidth - clientWidth;
  415. };
  416. const log = function(){
  417. if(!DEBUG) return;
  418. let l = log.last = log.now || new Date(), n = log.now = new Date();
  419. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  420. //console.log(error.stack);
  421. console.log(
  422. (SCRIPTID || '') + ':',
  423. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  424. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  425. /* :00 */ ':' + line,
  426. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  427. /* caller */ (callers[1] || '') + '()',
  428. ...arguments
  429. );
  430. };
  431. log.formats = [{
  432. name: 'Firefox Scratchpad',
  433. detector: /MARKER@Scratchpad/,
  434. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  435. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  436. }, {
  437. name: 'Firefox Console',
  438. detector: /MARKER@debugger/,
  439. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  440. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  441. }, {
  442. name: 'Firefox Greasemonkey 3',
  443. detector: /\/gm_scripts\//,
  444. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  445. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  446. }, {
  447. name: 'Firefox Greasemonkey 4+',
  448. detector: /MARKER@user-script:/,
  449. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 0,
  450. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  451. }, {
  452. name: 'Firefox Tampermonkey',
  453. detector: /MARKER@moz-extension:/,
  454. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 2,
  455. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  456. }, {
  457. name: 'Chrome Console',
  458. detector: /at MARKER \(<anonymous>/,
  459. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  460. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  461. }, {
  462. name: 'Chrome Tampermonkey',
  463. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?name=/,
  464. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 1,
  465. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  466. }, {
  467. name: 'Chrome Extension',
  468. detector: /at MARKER \(chrome-extension:/,
  469. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  470. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  471. }, {
  472. name: 'Edge Console',
  473. detector: /at MARKER \(eval/,
  474. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  475. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  476. }, {
  477. name: 'Edge Tampermonkey',
  478. detector: /at MARKER \(Function/,
  479. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  480. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  481. }, {
  482. name: 'Safari',
  483. detector: /^MARKER$/m,
  484. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  485. getCallers: (e) => e.stack.split('\n'),
  486. }, {
  487. name: 'Default',
  488. detector: /./,
  489. getLine: (e) => 0,
  490. getCallers: (e) => [],
  491. }];
  492. log.format = log.formats.find(function MARKER(f){
  493. if(!f.detector.test(new Error().stack)) return false;
  494. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  495. return true;
  496. });
  497. const time = function(label){
  498. if(!DEBUG) return;
  499. const BAR = '|', TOTAL = 100;
  500. switch(true){
  501. case(label === undefined):/* time() to output total */
  502. let total = 0;
  503. Object.keys(time.records).forEach((label) => total += time.records[label].total);
  504. Object.keys(time.records).forEach((label) => {
  505. console.log(
  506. BAR.repeat((time.records[label].total / total) * TOTAL),
  507. label + ':',
  508. (time.records[label].total).toFixed(3) + 'ms',
  509. '(' + time.records[label].count + ')',
  510. );
  511. });
  512. time.records = {};
  513. break;
  514. case(!time.records[label]):/* time('label') to create and start the record */
  515. time.records[label] = {count: 0, from: performance.now(), total: 0};
  516. break;
  517. case(time.records[label].from === null):/* time('label') to re-start the lap */
  518. time.records[label].from = performance.now();
  519. break;
  520. case(0 < time.records[label].from):/* time('label') to add lap time to the record */
  521. time.records[label].total += performance.now() - time.records[label].from;
  522. time.records[label].from = null;
  523. time.records[label].count += 1;
  524. break;
  525. }
  526. };
  527. time.records = {};
  528. core.initialize();
  529. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  530. })();