diff --git a/perf-test/app/page.tsx b/perf-test/app/page.tsx index 75c836f..227c0b8 100644 --- a/perf-test/app/page.tsx +++ b/perf-test/app/page.tsx @@ -1,124 +1,132 @@ -'use client' -import { Listbox, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'; -import { Fragment, useEffect, useState } from 'react'; -import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; -import { DotLottieReact, setWasmUrl as setDotLottieWasmUrl } from '@lottiefiles/dotlottie-react'; -import { Player } from '@lottiefiles/react-lottie-player'; -import { isMobile } from 'react-device-detect'; +"use client"; +import { + Listbox, + ListboxOption, + ListboxOptions, + Transition, +} from "@headlessui/react"; +import { Fragment, useEffect, useRef, useState } from "react"; +import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid"; +import { + DotLottieReact, + setWasmUrl as setDotLottieWasmUrl, +} from "@lottiefiles/dotlottie-react"; +import { Player } from "@lottiefiles/react-lottie-player"; +import { isMobile } from "react-device-detect"; import reactLottiePlayerPkg from "@lottiefiles/react-lottie-player/package.json"; import dotLottieReactPkg from "@lottiefiles/dotlottie-react/package.json"; import dotLottieWasmUrl from "../node_modules/@lottiefiles/dotlottie-web/dist/dotlottie-player.wasm"; -import SkottiePlayer, { setCanvasKit } from '../components/SkottiePlayer'; +import SkottiePlayer, { setCanvasKit } from "../components/SkottiePlayer"; import skottieWasmUrl from "../node_modules/canvaskit-wasm/bin/full/canvaskit.wasm"; -import InitCanvasKit from 'canvaskit-wasm/bin/full/canvaskit'; -import wasmUrl from "../node_modules/@thorvg/lottie-player/dist/thorvg-wasm.wasm"; +import InitCanvasKit from "canvaskit-wasm/bin/full/canvaskit"; setDotLottieWasmUrl(dotLottieWasmUrl); const animations = [ - '1643-exploding-star.json', - '5317-fireworkds.json', - '5344-honey-sack-hud.json', - '11555.json', - '27746-joypixels-partying-face-emoji-animation.json', - 'a_mountain.json', - 'abstract_circle.json', - 'alien.json', - 'anubis.json', - 'balloons_with_string.json', - 'birth_stone_logo.json', - 'calculator.json', - 'card_hover.json', - 'cat_loader.json', - 'coin.json', - 'confetti.json', - 'confetti2.json', - 'confettiBird.json', - 'dancing_book.json', - 'dancing_star.json', - 'dash-offset.json', - 'day_to_night.json', - 'dodecahedron.json', - 'down.json', - 'dropball.json', - 'duck.json', - 'emoji_enjoying.json', - 'emoji.json', - 'fleche.json', - 'flipping_page.json', - 'fly_in_beaker.json', - 'focal_test.json', - 'foodrating.json', - 'frog_vr.json', - 'fun_animation.json', - 'funky_chicken.json', - 'game_finished.json', - 'geometric.json', - 'glow_loading.json', - 'ghost.json', - 'ghost2.json', - 'gradient_background.json', - 'gradient_infinite.json', - 'gradient_sleepy_loader.json', - 'gradient_smoke.json', - 'graph.json', - 'growup.json', - 'guitar.json', - 'hamburger.json', - 'happy_holidays.json', - 'happy_trio.json', - 'heart_fill.json', - 'hola.json', - 'holdanimation.json', - 'hourglass.json', - 'isometric.json', - 'kote.json', - 'la_communaute.json', - '1f409.json', - 'like_button.json', - 'like.json', - 'loading_rectangle.json', - 'lolo_walk.json', - 'lolo.json', - 'loveface_emoji.json', - 'masking.json', - 'material_wave_loading.json', - 'merging_shapes.json', - 'message.json', - 'monkey.json', - 'morphing_anim.json', - 'new_design.json', - 'page_slide.json', - 'personal_character.json', - 'polystar_anim.json', - 'polystar.json', - 'property_market.json', - 'pumpkin.json', - 'ripple_loading_animation.json', - 'rufo.json', - 'sample.json', - 'seawalk.json', - 'shutup.json', - 'skullboy.json', - 'starburst.json', - 'starstrips.json', - 'starts_transparent.json', - 'stroke_dash.json', - 'swinging.json', - 'text_anim.json', - 'text2.json', - 'textblock.json', - 'textrange.json', - 'threads.json', - 'train.json', - 'uk_flag.json', - 'voice_recognition.json', - 'water_filling.json', - 'waves.json', - 'yarn_loading.json' + "1643-exploding-star.json", + "5317-fireworkds.json", + "5344-honey-sack-hud.json", + "11555.json", + "27746-joypixels-partying-face-emoji-animation.json", + "a_mountain.json", + "abstract_circle.json", + "alien.json", + "anubis.json", + "balloons_with_string.json", + "birth_stone_logo.json", + "calculator.json", + "card_hover.json", + "cat_loader.json", + "coin.json", + "confetti.json", + "confetti2.json", + "confettiBird.json", + "dancing_book.json", + "dancing_star.json", + "dash-offset.json", + "day_to_night.json", + "dodecahedron.json", + "down.json", + "dropball.json", + "duck.json", + "emoji_enjoying.json", + "emoji.json", + "fleche.json", + "flipping_page.json", + "fly_in_beaker.json", + "focal_test.json", + "foodrating.json", + "frog_vr.json", + "fun_animation.json", + "funky_chicken.json", + "game_finished.json", + "geometric.json", + "glow_loading.json", + "ghost.json", + "ghost2.json", + "gradient_background.json", + "gradient_infinite.json", + "gradient_sleepy_loader.json", + "gradient_smoke.json", + "graph.json", + "growup.json", + "guitar.json", + "hamburger.json", + "happy_holidays.json", + "happy_trio.json", + "heart_fill.json", + "hola.json", + "holdanimation.json", + "hourglass.json", + "isometric.json", + "kote.json", + "la_communaute.json", + "1f409.json", + "like_button.json", + "like.json", + "loading_rectangle.json", + "lolo_walk.json", + "lolo.json", + "loveface_emoji.json", + "masking.json", + "material_wave_loading.json", + "merging_shapes.json", + "message.json", + "monkey.json", + "morphing_anim.json", + "new_design.json", + "page_slide.json", + "personal_character.json", + "polystar_anim.json", + "polystar.json", + "property_market.json", + "pumpkin.json", + "ripple_loading_animation.json", + "rufo.json", + "sample.json", + "seawalk.json", + "shutup.json", + "skullboy.json", + "starburst.json", + "starstrips.json", + "starts_transparent.json", + "stroke_dash.json", + "swinging.json", + "text_anim.json", + "text2.json", + "textblock.json", + "textrange.json", + "threads.json", + "train.json", + "uk_flag.json", + "voice_recognition.json", + "water_filling.json", + "waves.json", + "yarn_loading.json", ]; -const urlPrefix = 'https://raw.githubusercontent.com/thorvg/thorvg/main/examples/resources/lottie/'; +const urlPrefix = + "https://raw.githubusercontent.com/thorvg/thorvg/main/examples/resources/lottie/"; const countOptions = [ { id: 0, name: 10 }, @@ -131,79 +139,94 @@ const countOptions = [ ]; const playerOptions = [ - { id: 1, name: 'ThorVG(Software)' }, - { id: 2, name: 'ThorVG(WebGPU)' }, + { id: 1, name: "ThorVG(Software)" }, + { id: 2, name: "ThorVG(WebGPU)" }, //{ id: 3, name: `dotlottie-web@${dotLottieReactPkg.dependencies["@lottiefiles/dotlottie-web"]}` }, //{ id: 4, name: `lottie-web@${reactLottiePlayerPkg.dependencies["lottie-web"]}` }, //{ id: 5, name: 'skia/skottie' }, ]; function classNames(...classes: any) { - return classes.filter(Boolean).join(' ') + return classes.filter(Boolean).join(" "); } function setQueryStringParameter(name: string, value: any) { const params = new URLSearchParams(window.location.search); params.set(name, value); - window.history.replaceState({}, '', decodeURIComponent(`${window.location.pathname}?${params}`)); + window.history.replaceState( + {}, + "", + decodeURIComponent(`${window.location.pathname}?${params}`) + ); } export default function Home() { - const size = isMobile ? { width: 150, height: 150 } : { width: 180, height: 180}; - let initialized = false; - + const wasmUrl = + typeof window !== "undefined" ? "/thorvg-wasm.wasm" : undefined; + + const size = isMobile + ? { width: 150, height: 150 } + : { width: 180, height: 180 }; + const initialized = useRef(false); + const [count, setCount] = useState(countOptions[1]); const [player, setPlayer] = useState(playerOptions[0]); const [playerId, setPlayerId] = useState(1); - const [text, setText] = useState(''); + const [text, setText] = useState(""); const [animationList, setAnimationList] = useState([]); useEffect(() => { - if (initialized) { - return; - } - initialized = true; - - // @ts-ignore - import("@thorvg/lottie-player"); - - let count: number = countOptions[1].name; - let seed: string = ''; - let playerId = 1; - - if (window.location.search) { - const params = new URLSearchParams(window.location.search); - const player = params.get('player'); - count = parseInt(params.get('count') ?? '20'); - seed = params.get('seed') ?? ''; - - if (count) { - const _count = countOptions.find((c) => c.name === count) || countOptions[1]; - setCount(_count); - } - - if (player) { - const _player = playerOptions.find((p) => p.name === player) || playerOptions[0]; - playerId = _player.id; - setPlayer(_player); - setPlayerId(_player.id); + const init = async () => { + if (initialized.current) { + return; } - } - - setTimeout(async () => { - if (playerId === 4) { - await loadCanvasKit(); + initialized.current = true; + + // @ts-ignore + await import("@thorvg/lottie-player"); + + let count: number = countOptions[1].name; + let seed: string = ""; + let playerId = 1; + + if (window.location.search) { + const params = new URLSearchParams(window.location.search); + const player = params.get("player"); + count = parseInt(params.get("count") ?? "20"); + seed = params.get("seed") ?? ""; + + if (count) { + const _count = + countOptions.find((c) => c.name === count) || countOptions[1]; + setCount(_count); + } + + if (player) { + const _player = + playerOptions.find((p) => p.name === player) || playerOptions[0]; + playerId = _player.id; + setPlayer(_player); + setPlayerId(_player.id); + } } - loadProfiler(); + setTimeout(async () => { + if (playerId === 4) { + await loadCanvasKit(); + } - if (seed) { - loadSeed(seed); - return; - } + loadProfiler(); - loadAnimationByCount(count); - }, 500); + if (seed) { + loadSeed(seed); + return; + } + + loadAnimationByCount(count); + }, 500); + }; + + init(); }, []); const loadCanvasKit = async () => { @@ -211,13 +234,26 @@ export default function Home() { locateFile: (_) => skottieWasmUrl, }); setCanvasKit(canvasKit); - } + }; const loadProfiler = () => { - const script = document.createElement("script"); - script.src = "/profiler.js"; - document.body.appendChild(script); - } + const s = document.createElement("script"); + s.type = "module"; + s.src = "/profiler.js"; + s.onload = async () => { + const res = await window.startProfiler?.(); + window.__profilerCleanup__ = () => { + try { + res?.dispose?.(); + } catch {} + try { + delete window.__profilerCleanup__; + } catch {} + }; + }; + s.onerror = (e) => console.error("[page] profiler load error", e); + document.head.appendChild(s); + }; const loadAnimationByCount = async (_count = count.name) => { const newAnimationList = []; @@ -226,7 +262,7 @@ export default function Home() { const _anim = animations[Math.floor(Math.random() * animations.length)]; newAnimationList.push({ - name: _anim.split('/').pop()?.split('.')[0] || 'Unknown', + name: _anim.split("/").pop()?.split(".")[0] || "Unknown", lottieURL: `${urlPrefix}${_anim}`, }); } @@ -239,16 +275,18 @@ export default function Home() { }; const saveCurrentSeed = (animationList: any[]) => { - const nameList = animationList.map((v: any) => v.name).join(','); + const nameList = animationList.map((v: any) => v.name).join(","); const seed = btoa(nameList); - setQueryStringParameter('seed', seed); - } + setQueryStringParameter("seed", seed); + }; const loadSeed = (seed: string) => { - const nameList = atob(seed).split(','); + const nameList = atob(seed).split(","); console.log(nameList); const newAnimationList = nameList.map((name: string) => { - const _anim = animations.find((anim) => anim === `${name.trim()}.json`) || animations[0]; + const _anim = + animations.find((anim) => anim === `${name.trim()}.json`) || + animations[0]; return { name: name, @@ -257,7 +295,7 @@ export default function Home() { }); setAnimationList(newAnimationList); - } + }; const spawnAnimation = () => { if (!text.trimEnd().trimStart()) { @@ -268,252 +306,287 @@ export default function Home() { // random 0 to animationList.length const randomIndex = Math.floor(Math.random() * animationList.length); animationList[randomIndex].lottieURL = text; - animationList[randomIndex].name = text.split('/').pop()?.split('.')[0] || 'Unknown'; + animationList[randomIndex].name = + text.split("/").pop()?.split(".")[0] || "Unknown"; setAnimationList(animationList.slice()); setTimeout(() => { - document.querySelector(`.${animationList[randomIndex].name}-${randomIndex}`)?.scrollIntoView({ behavior: 'smooth' }); + document + .querySelector(`.${animationList[randomIndex].name}-${randomIndex}`) + ?.scrollIntoView({ behavior: "smooth" }); }, 150); }; + if (typeof window === "undefined") return
1
; + return (
- -

Player:

- - { - setPlayer(v); - setQueryStringParameter('player', v.name); - }}> - {({ open }) => ( - <> -
- - {player.name} - - - - - + Player:{" "} + + + { + setPlayer(v); + setQueryStringParameter("player", v.name); + }} > - - {playerOptions.map((player) => ( - - classNames( - active ? 'bg-indigo-600 text-white' : 'text-gray-900', - 'relative cursor-default select-none py-2 pl-3 pr-9' - ) - } - value={player} - > - {({ selected, active }) => ( - <> - - {player.name} - - - {selected ? ( - ( + <> +
+ + {player.name} + + + + + + + {playerOptions.map((player) => ( + + classNames( + active + ? "bg-indigo-600 text-white" + : "text-gray-900", + "relative cursor-default select-none py-2 pl-3 pr-9" + ) + } + value={player} > - - ))} - - -
- - )} -
- - { - setCount(v); - setQueryStringParameter('count', v.name); - }}> - {({ open }) => ( - <> -
- - {count.name} - - - - - - - {countOptions.map((option) => ( - - classNames( - active ? 'bg-indigo-600 text-white' : 'text-gray-900', - 'relative cursor-default select-none py-2 pl-3 pr-9' - ) - } - value={option} - > - {({ selected, active }) => ( - <> - - {option.name} - - - {selected ? ( - ( + <> + + {player.name} + + + {selected ? ( + + + ) : null} + )} + + ))} + + +
+ + )} +
+ + { + setCount(v); + setQueryStringParameter("count", v.name); + }} + > + {({ open }) => ( + <> +
+ + {count.name} + + + + + + + {countOptions.map((option) => ( + + classNames( + active + ? "bg-indigo-600 text-white" + : "text-gray-900", + "relative cursor-default select-none py-2 pl-3 pr-9" + ) + } + value={option} > - - ))} - - -
- - )} -
- - + {({ selected, active }) => ( + <> + + {option.name} + + + {selected ? ( + + + ) : null} + + )} + + ))} + +
+
+ + )} +
+ +
- - setText(e.target.value)} - /> - - -
+ + setText(e.target.value)} + /> + + +
- ) + ); } diff --git a/perf-test/declarations.d.ts b/perf-test/declarations.d.ts index ac7a900..1747980 100644 --- a/perf-test/declarations.d.ts +++ b/perf-test/declarations.d.ts @@ -9,3 +9,11 @@ declare module "*.wasm" { const url: string; export default url; } + +interface Window { + startProfiler?: ( + options?: ProfilerOptions + ) => Promise<{ dispose: () => void } | undefined>; + __statsBridge?: ProfilerBridge; + __profilerCleanup__?: () => void; +} diff --git a/perf-test/public/profiler.js b/perf-test/public/profiler.js index 16e12d3..ad0bfab 100644 --- a/perf-test/public/profiler.js +++ b/perf-test/public/profiler.js @@ -1,2 +1,124 @@ -// stats.js - http://github.com/mrdoob/stats.js -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Stats=e()}(this,function(){var t=function(){function e(t){return a.appendChild(t.dom),t}function n(t){for(var e=0;e=o+1e3&&(d.update(1e3*s/(t-o),100),o=t,s=0,r)){var e=performance.memory;r.update(e.usedJSHeapSize/1048576,e.jsHeapSizeLimit/1048576)}return t},update:function(){l=this.end()},domElement:a,setMode:n}};return t.Panel=function(t,e,n){var i=1/0,a=0,l=Math.round,o=l(window.devicePixelRatio||1),s=80*o,d=48*o,$=3*o,r=2*o,f=3*o,p=15*o,c=74*o,m=30*o,u=document.createElement("canvas");u.width=s,u.height=d,u.style.cssText="width:80px;height:48px";var y=u.getContext("2d");return y.font="bold "+9*o+"px Helvetica,Arial,sans-serif",y.textBaseline="top",y.fillStyle=n,y.fillRect(0,0,s,d),y.fillStyle=e,y.fillText(t,$,r),y.fillRect(f,p,c,m),y.fillStyle=n,y.globalAlpha=.9,y.fillRect(f,p,c,m),{dom:u,update:function(d,x){i=Math.min(i,d),a=Math.max(a,d),y.fillStyle=n,y.globalAlpha=1,y.fillRect(0,0,s,p),y.fillStyle=e,y.fillText(l(d)+" "+t+" ("+l(i)+"-"+l(a)+")",$,r),y.drawImage(u,f+o,p,c-o,m,f,p,c-o,m),y.fillRect(f+c-o,p,o,m),y.fillStyle=n,y.globalAlpha=.9,y.fillRect(f+c-o,p,o,l((1-d/x)*m))}}},t});var statsMB,statsFPS=new Stats;statsFPS.showPanel(0),statsFPS.dom.style.cssText="position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000",document.body.appendChild(statsFPS.dom);var statsMS=new Stats;function animate(){statsFPS.begin(),statsMS.begin(),statsMB&&statsMB.begin(),statsFPS.end(),statsMS.end(),statsMB&&statsMB.end(),requestAnimationFrame(animate)}statsMS.showPanel(1),statsMS.dom.style.cssText="position:fixed;top:0;left:80px;cursor:pointer;opacity:0.9;z-index:10000",document.body.appendChild(statsMS.dom),self.performance&&self.performance.memory&&((statsMB=new Stats).showPanel(2),statsMB.dom.style.cssText="position:fixed;top:0;left:160px;cursor:pointer;opacity:0.9;z-index:10000",document.body.appendChild(statsMB.dom)),requestAnimationFrame(animate); \ No newline at end of file +async function getStatsCtor() { + try { + const mod = await import("/stats.js"); + return mod.default || mod.Stats || window.Stats; + } catch { + return window.Stats; + } +} + +export async function startProfiler(options = {}) { + if (typeof document === "undefined") { + throw new Error("startProfiler must run in a browser."); + } + + const { + tiles = ["fps", "ms", "mb"], + zIndex = 10000, + maxFps = 144, + maxMs = 33, + } = options; + + const Stats = await getStatsCtor(); + if (!Stats) throw new Error("Stats constructor not found."); + + const root = document.createElement("div"); + root.style.cssText = + "position:fixed;top:8px;left:8px;z-index:" + + zIndex + + ";display:flex;align-items:flex-start;gap:4px;"; + document.body.appendChild(root); + + const container = document.createElement("div"); + container.style.cssText = "display:flex;cursor:pointer;"; + root.appendChild(container); + + const toggle = document.createElement("button"); + toggle.textContent = "▾"; + toggle.setAttribute("aria-label", "Toggle stats"); + toggle.style.cssText = + "display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:9999px;border:none;background:rgba(255,255,255,0.7);color:#000;font-weight:700;cursor:pointer;"; + root.appendChild(toggle); + + let collapsed = false; + const setCollapsed = (v) => { + collapsed = v; + container.style.display = v ? "none" : "flex"; + toggle.textContent = v ? "▸" : "▾"; + }; + toggle.addEventListener("click", (e) => { + e.stopPropagation(); + setCollapsed(!collapsed); + }); + + const instances = []; + + const addStats = (panelIndex) => { + const s = new Stats(); + s.showPanel(panelIndex); + Object.assign(s.dom.style, { cursor: "pointer", opacity: "0.9" }); + container.appendChild(s.dom); + instances.push(s); + return s; + }; + + if (tiles.includes("fps")) addStats(0); + if (tiles.includes("ms")) addStats(1); + if (tiles.includes("mb") && performance && performance.memory) addStats(2); + + let rafId = 0; + let last = performance.now(); + let secStart = last; + let frames = 0; + let sumDt = 0, + cntDt = 0; + + const tick = () => { + const now = performance.now(); + const dt = now - last; + last = now; + + frames++; + sumDt += dt; + cntDt++; + + if (now - secStart >= 1000) { + const span = now - secStart; + const fps = (frames * 1000) / span; + const msAvg = cntDt ? sumDt / cntDt : 0; + + for (const s of instances) { + if (s.__panels?.fps) s.__panels.fps.update(fps, maxFps); + if (s.__panels?.ms) s.__panels.ms.update(msAvg, maxMs); + if (s.__panels?.mb && performance && performance.memory) { + const m = performance.memory; + s.__panels.mb.update( + m.usedJSHeapSize / 1048576, + m.jsHeapSizeLimit / 1048576 + ); + } + } + + frames = 0; + sumDt = 0; + cntDt = 0; + secStart = now; + } + + rafId = requestAnimationFrame(tick); + }; + + rafId = requestAnimationFrame(tick); + + return { + dispose() { + cancelAnimationFrame(rafId); + instances.forEach((s) => s.dom.remove()); + root.remove(); + }, + }; +} + +if (typeof window !== "undefined" && !window.startProfiler) { + window.startProfiler = (...args) => startProfiler(...args); +} diff --git a/perf-test/public/stats.js b/perf-test/public/stats.js new file mode 100644 index 0000000..1bba250 --- /dev/null +++ b/perf-test/public/stats.js @@ -0,0 +1,159 @@ +/** + * @author mrdoob + * @license MIT + * Based on stats.js by mrdoob + * Original: https://github.com/mrdoob/stats.js + * Modified: external-updates only (no internal timing) + */ + +var Stats = function () { + var mode = 0; + + var container = document.createElement("div"); + container.style.cssText = "display:flex;cursor:pointer;opacity:0.95;"; + container.addEventListener( + "click", + function (event) { + event.preventDefault(); + showPanel(++mode % container.children.length); + }, + false + ); + + function addPanel(panel) { + container.appendChild(panel.dom); + return panel; + } + + function showPanel(id) { + for (var i = 0; i < container.children.length; i++) { + container.children[i].style.display = i === id ? "block" : "none"; + } + mode = id; + } + + var fpsPanel = addPanel(new Stats.Panel("FPS", "#0ff", "#002")); + var msPanel = addPanel(new Stats.Panel("MS", "#0f0", "#020")); + var memPanel = null; + + if (self.performance && self.performance.memory) { + memPanel = addPanel(new Stats.Panel("MB", "#f08", "#201")); + } + + showPanel(0); + + return { + REVISION: 16, + + dom: container, + addPanel: addPanel, + showPanel: showPanel, + + __panels: { fps: fpsPanel, ms: msPanel, mb: memPanel }, + + // Backwards Compatibility + domElement: container, + setMode: showPanel, + }; +}; + +Stats.Panel = function (name, fg, bg) { + var min = Infinity, + max = 0, + round = Math.round; + var acc = null; // acc = (acc + value) / 2 + + var PR = round(window.devicePixelRatio || 1); + + var WIDTH = 110 * PR, + HEIGHT = 60 * PR, + TEXT_X = 3 * PR, + TEXT_Y = 2 * PR, + LINE_HEIGHT = 10 * PR, + GRAPH_X = 3 * PR, + GRAPH_Y = 24 * PR, + GRAPH_WIDTH = 104 * PR, + GRAPH_HEIGHT = 32 * PR; + + var canvas = document.createElement("canvas"); + canvas.width = WIDTH; + canvas.height = HEIGHT; + canvas.style.cssText = "width:110px;height:60px"; + + var context = canvas.getContext("2d"); + context.font = "bold " + 10 * PR + "px Helvetica,Arial,sans-serif"; + context.textBaseline = "top"; + + context.fillStyle = bg; + context.fillRect(0, 0, WIDTH, HEIGHT); + + context.fillStyle = fg; + context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT); + + context.fillStyle = bg; + context.globalAlpha = 0.9; + context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT); + context.globalAlpha = 1; + + return { + dom: canvas, + + update: function (value, maxValue) { + if (Number.isFinite(value)) { + min = Math.min(min, value); + max = Math.max(max, value); + acc = acc == null ? value : (acc + value) / 2; + } + + context.fillStyle = bg; + context.globalAlpha = 1; + context.fillRect(0, 0, WIDTH, GRAPH_Y); + + var firstLine = + name === "MS" + ? `${name}: ${value.toFixed(1)}` + : `${name}: ${round(value)}`; + + var secondLine; + if (name === "MS") { + secondLine = `acc: ${acc.toFixed(1)} (${min.toFixed(1)} ~ ${max.toFixed( + 1 + )})`; + } else { + secondLine = `acc: ${round(acc)} (${round(min)} ~ ${round(max)})`; + } + + context.fillStyle = fg; + context.fillText(firstLine, TEXT_X, TEXT_Y); + context.fillText(secondLine, TEXT_X, TEXT_Y + LINE_HEIGHT); + + context.drawImage( + canvas, + GRAPH_X + PR, + GRAPH_Y, + GRAPH_WIDTH - PR, + GRAPH_HEIGHT, + GRAPH_X, + GRAPH_Y, + GRAPH_WIDTH - PR, + GRAPH_HEIGHT + ); + + context.fillStyle = fg; + context.fillRect(GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT); + + context.fillStyle = bg; + context.globalAlpha = 0.9; + + var denom = maxValue > 0 ? maxValue : 1; + var h = Math.round((1 - value / denom) * GRAPH_HEIGHT); + if (!Number.isFinite(h)) h = GRAPH_HEIGHT; + + context.fillRect(GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, h); + + context.globalAlpha = 1; + }, + }; +}; + +export { Stats as default };