diff --git a/.postcssrc.js b/.postcssrc.js new file mode 100644 index 0000000..e68b81d --- /dev/null +++ b/.postcssrc.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + "postcss-preset-env": true, + }, +} diff --git a/package.json b/package.json index 8f016ff..e7e2da0 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,16 @@ "start": "parcel src/index.html", "build": "parcel build src/index.html" }, - "dependencies": {}, + "browserslist": "> 1% and last 2 years", + "dependencies": { + "clsx": "^1.1.1", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "recharts": "^2.0.0", + "use-element-dimensions": "^2.1.3" + }, "devDependencies": { - "parcel-bundler": "^1.12.4" + "parcel-bundler": "^1.12.4", + "postcss-preset-env": "^6.7.0" } } diff --git a/src/ProgressPane.jsx b/src/ProgressPane.jsx new file mode 100644 index 0000000..42ad500 --- /dev/null +++ b/src/ProgressPane.jsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect } from "react" +import { createPortal } from "react-dom" +import { Area, XAxis, YAxis, AreaChart, CartesianGrid, Tooltip } from "recharts" +import useDimensions from "use-element-dimensions" + +const csvVersions = { + "1": { + timestamp: parseInt, + commit: s => s, + totalFuncs: parseInt, + nonMatchingFuncs: parseInt, + matchingFuncs: parseInt, + totalBytes: b => parseInt(b), + nonMatchingBytes: b => parseInt(b), + matchingBytes: b => parseInt(b), + }, +} + +async function fetchData() { + const csv = await fetch("https://papermar.io/reports/progress.csv") + .then(response => response.text()) + + return csv + .split("\n") + .filter(row => row.length) + .map(row => { + const [version, ...data] = row.split(",") + const structure = csvVersions[version] + const obj = {} + + for (const [key, transform] of Object.entries(structure)) { + obj[key] = transform(data.shift()) + } + + obj.percentBytes = Math.round((obj.matchingBytes / obj.totalBytes) * 100) + + return obj + }) +} + +export default function ProgressPane({ captionPortal }) { + const [data, setData] = useState(null) + + useEffect(() => { + fetchData() + .then(data => setData(data)) + }, []) + + // TODO: cute spin animation when data loads + + return
+ {data && } + {!data && "Loading..."} +
+} + +const MONTH = 2678400 + +function DataView({ data, captionPortal }) { + const [chartDimensions, chartRef] = useDimensions() + const latest = data[data.length - 1] + const oldest = data[0] + + let monthDates = [] + for (let i = new Date(1583020800000); i < new Date(latest.timestamp); i.setMonth(i.getMonth() + 1)) { + if (i > new Date(oldest.timestamp)) { + monthDates.push(i) + } + } + + console.log(monthDates) + + const [selectedEntry, setSelectedEntry] = useState(null) + + function renderTooltip(tip) { + const entry = data.find(row => row.timestamp === tip.label) + + setSelectedEntry(entry) + + return + } + + return <> + + + + + + + +
Matched{Math.round((latest.matchingBytes / latest.totalBytes) * 10000) / 100}%
+ +
+
+
+ {chartDimensions.width > 0 && + + + + + + + + + } +
+
+ + +
+ + {selectedEntry && captionPortal.current && createPortal(, captionPortal.current)} + +} + +function formatDate(timestamp, options={}) { + const date = new Date(timestamp * 1000) + + return new Intl.DateTimeFormat([], options).format(date) +} + +function EntryInfo({ entry }) { + /*const [commitMessage, setCommitMessage] = useState(null) + + useEffect(async () => { + fetch(`https://api.github.com/repos/ethteck/papermario/commits/${entry.commit}`) + .then(resp => resp.json()) + .then(resp => { + setCommitMessage(resp.commit.message.split("\n")[0]) + }) + }, [entry.commit])*/ + + return
+ + {entry.commit.substr(0, 8)} + + + + + + + + +
Matched + {Math.round((entry.matchingBytes / entry.totalBytes) * 10000) / 100}% + ({entry.matchingFuncs}/{entry.totalFuncs} functions) +
+
+} diff --git a/src/bg/caption-checker.png b/src/bg/caption-checker.png new file mode 100644 index 0000000..d221284 Binary files /dev/null and b/src/bg/caption-checker.png differ diff --git a/src/bg/red-checker.png b/src/bg/red-checker.png new file mode 100644 index 0000000..3228625 Binary files /dev/null and b/src/bg/red-checker.png differ diff --git a/src/bg/yellow-checker.png b/src/bg/yellow-checker.png new file mode 100644 index 0000000..d5f1e0a Binary files /dev/null and b/src/bg/yellow-checker.png differ diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..382c37e --- /dev/null +++ b/src/index.css @@ -0,0 +1,214 @@ +@font-face { + font-family: "Paper Mario Dialog Redesigned"; + src: url("pmdialog2.woff2") format("woff2"); +} + +html { + --dark: #313131; + --light: #d6d6ce; + + font-size: 28px; + font-family: "Paper Mario Dialog Redesigned", sans-serif; + font-weight: 700; + line-height: 1.2; + + color: var(--light); + --text-outline: var(--dark); + + background: #090942; /* TODO: dynamic star bg */ + + overflow: hidden; +} + +* { + box-sizing: border-box; + + /* https://owumaro.github.io/text-stroke-generator/ */ + text-shadow: var(--text-outline) 3px 0px 0px, var(--text-outline) 2.83487px 0.981584px 0px, var(--text-outline) 2.35766px 1.85511px 0px, var(--text-outline) 1.62091px 2.52441px 0px, var(--text-outline) 0.705713px 2.91581px 0px, var(--text-outline) -0.287171px 2.98622px 0px, var(--text-outline) -1.24844px 2.72789px 0px, var(--text-outline) -2.07227px 2.16926px 0px, var(--text-outline) -2.66798px 1.37182px 0px, var(--text-outline) -2.96998px 0.42336px 0px, var(--text-outline) -2.94502px -0.571704px 0px, var(--text-outline) -2.59586px -1.50383px 0px, var(--text-outline) -1.96093px -2.27041px 0px, var(--text-outline) -1.11013px -2.78704px 0px, var(--text-outline) -0.137119px -2.99686px 0px, var(--text-outline) 0.850987px -2.87677px 0px, var(--text-outline) 1.74541px -2.43999px 0px, var(--text-outline) 2.44769px -1.73459px 0px, var(--text-outline) 2.88051px -0.838247px 0px; +} + +a:any-link { + color: #3796ff; + --text-outline: #d3e5f9; + text-decoration: none; +} + +button { + all: unset; + + border-radius: 100em; + padding: 0 .3em .2em .3em; + cursor: pointer; + + filter: brightness(1.0); + will-change: filter; + transition: filter 200ms; +} + +button.red { + background: linear-gradient(#f04e54, #f04e54 10%, #b4313e 25%, #b4313e 90%, #7f1729 95%); +} + +button.yellow { + background: linear-gradient(#f0c74e, #f0c74e 10%, #b48b31 25%, #b48b31 90%, #7f5617 95%); +} + +button.inactive { + filter: brightness(0.6); +} + +button:hover { + filter: brightness(1.0); +} + +.tab { + margin-right: .25em; +} + +#container { + /* TODO: use flex on body */ + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + display: flex; + flex-direction: column; + align-items: center; + + width: 1400px; + max-width: calc(100vw - 10vh); + + height: 1200px; + max-height: calc(100vh - 10vw); +} + +nav { + margin-bottom: -.6em; + z-index: 1; +} + +main { + flex: 1; + width: 100%; + + background-repeat: repeat; + background-size: 32px; + background-position: center; + image-rendering: pixelated; + + border-radius: 1rem; + + padding: 1em; + + overflow-y: auto; + + display: flex; + flex-direction: column; + + transition: 600ms transform ease-in-out; +} + +main.red { + background-color: #e2b6b3; + background-image: url(bg/red-checker.png); +} + +main.yellow { + background-color: #e2d8b3; + background-image: url(bg/yellow-checker.png); +} + +main > * { + image-rendering: initial; +} + +.shadow-box { + background: #ffffff66; + padding: 1em; + margin: .5em; + margin-bottom: 1em; + + border-radius: 16px; + box-shadow: .5em .5em 3px -.2em #00000044; + + border-top: 4px solid #ffffff33; + padding-top: calc(1em - 2px); + + border-left: 2px solid #ffffff33; + border-right: 2px solid #00000055; + border-bottom: 2px solid #00000055; + + position: relative; +} + +.shadow-box-inner { + border-radius: 12px; + box-shadow: inset .3em .3em 3px #00000033; + + width: 100%; + height: 100%; + + display: flex; + overflow: hidden; + + background: white; +} + +button.shadow-box-title { + position: absolute; + left: 50%; + bottom: -1em; + transform: translateX(-50%); + + width: 60%; + + text-align: center; + + border-radius: 1em; + + padding-top: .2em; + height: 1.2em; +} + +.caption { + background-image: url(bg/caption-checker.png); + background-repeat: repeat; + background-size: 32px; + background-position: center; + image-rendering: pixelated; + + border-radius: 1rem; + width: 85%; + height: 3rem; + + padding: 8px 16px; + margin-top: -14px; + z-index: 1; +} + +.progress-chart { + flex: 1; + + font-size: 12px; + overflow: hidden; + + user-select: none; + -webkit-user-select: none; +} + +.outline-invert { + color: var(--dark); + --text-outline: var(--light); +} + +.thin { + font-weight: 400; + text-shadow: var(--text-outline) 2px 0px 0px, var(--text-outline) 1.75517px 0.958851px 0px, var(--text-outline) 1.0806px 1.68294px 0px, var(--text-outline) 0.141474px 1.99499px 0px, var(--text-outline) -0.832294px 1.81859px 0px, var(--text-outline) -1.60229px 1.19694px 0px, var(--text-outline) -1.97998px 0.28224px 0px, var(--text-outline) -1.87291px -0.701566px 0px, var(--text-outline) -1.30729px -1.5136px 0px, var(--text-outline) -0.421592px -1.95506px 0px, var(--text-outline) 0.567324px -1.91785px 0px, var(--text-outline) 1.41734px -1.41108px 0px, var(--text-outline) 1.92034px -0.558831px 0px; +} + +.flex-row { display: flex; flex-direction: row } +.flex-grow { flex: 1; height: 0 } +.flex-spacer { width: 1em; height: 1em } + +.align-right { text-align: right } diff --git a/src/index.html b/src/index.html index 5ea616c..f8f7a81 100644 --- a/src/index.html +++ b/src/index.html @@ -1,11 +1,48 @@ - + Paper Mario Reverse Engineering + + + - Hello world! +
+ +
diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..9a062a7 --- /dev/null +++ b/src/index.js @@ -0,0 +1,14 @@ +import moment from "moment" + +import("./progress.js").then(async ({ fetchData, functionsChart }) => { + const data = await fetchData() + + functionsChart(data, document.getElementById("progress-chart")) + + const first = data[0] + const latest = data[data.length - 1] + + document.getElementById("matched-rom-percent").innerText = Math.round((latest.matchingBytes / latest.totalBytes) * 10000) / 100 + "% matched" // TODO: include data + document.getElementById("functions-ratio").innerText = latest.matchingFuncs + "/" + latest.totalFuncs + document.getElementById("play-time").innerText = moment(latest.timestamp).from(first.timestamp, true) +}).catch(console.error) diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..3af4661 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,94 @@ +import React, { useState, useRef, useEffect } from "react" +import ReactDOM from "react-dom" +import clsx from "clsx" + +import ProgressPane from "./ProgressPane" + +const tabs = [ + { + slug: "/", + name: "Info", + color: "red", + pane: () =>
I am the info page
, + }, + { + slug: "/progress", + name: "Progress", + color: "yellow", + pane: (props) => + }, +] + +let routedTabIndex = tabs.findIndex(tab => tab.slug === document.location.pathname) +if (routedTabIndex == -1) routedTabIndex = 0 + +function App() { + const [paneIndex, setPaneIndex] = useState(routedTabIndex) + const [tabIndex, setTabIndex] = useState(routedTabIndex) + const [rotation, setRotation] = useState(0) + const [flip, setFlip] = useState(false) + + const pane = useRef() + let lockTabs = false // not state + + function switchToTab(index) { + if (index === paneIndex || index === tabIndex) return + + console.info("switching to tab", index) + + setRotation(rotation - 180) + setTabIndex(index) + + history.pushState(null, tabs[index].name, tabs[index].slug) + + setTimeout(() => { + setPaneIndex(index) + setFlip(!flip) + }, 300) // half the animation time + } + + useEffect(() => { + function listener() { + switchToTab(tabs.findIndex(tab => tab.slug === document.location.pathname)) + } + + window.addEventListener("popstate", listener) + return () => window.removeEventListener("popstate", listener) + }, [rotation, flip, tabIndex, paneIndex]) + + const captionPortal = useRef() + + return <> + +
+
+ {tabs[paneIndex].pane({ captionPortal })} +
+
+
+ +} + +ReactDOM.render(, document.getElementById("container")) diff --git a/src/pmdialog2.woff2 b/src/pmdialog2.woff2 new file mode 100644 index 0000000..60c1b9e Binary files /dev/null and b/src/pmdialog2.woff2 differ diff --git a/src/progress.js b/src/progress.js new file mode 100644 index 0000000..d4fd472 --- /dev/null +++ b/src/progress.js @@ -0,0 +1,121 @@ +import Chart from "chart.js" + +const csvVersions = { + "1": { + timestamp: s => new Date(parseInt(s) * 1000), + commit: s => s, + totalFuncs: parseInt, + nonMatchingFuncs: parseInt, + matchingFuncs: parseInt, + totalBytes: parseInt, + nonMatchingBytes: parseInt, + matchingBytes: parseInt, + }, +} + +export async function fetchData() { + const csv = await fetch("https://papermar.io/reports/progress.csv") + .then(response => response.text()) + + return csv + .split("\n") + .filter(row => row.length) + .map(row => { + const [version, ...data] = row.split(",") + const structure = csvVersions[version] + const obj = {} + + for (const [key, transform] of Object.entries(structure)) { + obj[key] = transform(data.shift()) + } + + return obj + }) +} + +export async function functionsChart(data, ctx) { + return new Chart(ctx, { + type: "line", + data: { + datasets: [ + { + label: "Matching Functions", + borderColor: "transparent", + backgroundColor: "#7f5617", + pointBackgroundColor: "transparent", + pointBorderColor: "black", + data: data.map(row => { + return { + x: row.timestamp, + y: row.matchingFuncs, + } + }), + lineTension: 0, + }, + { + label: "Split Functions", + borderColor: "transparent", + backgroundColor: "#b48b31", + pointBackgroundColor: "transparent", + pointBorderColor: "black", + data: data.map(row => { + return { + x: row.timestamp, + y: row.totalFuncs, + } + }), + lineTension: 0, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + xAxes: [ + { + type: "time", + //distribution: "series", + ticks: { + max: Date.now(), + fontFamily: "Paper Mario Dialog Redesigned", + }, + time: { + isoWeekday: true, + unit: "month", + }, + gridLines: { + drawBorder: true, + } + }, + ], + yAxes: [ + { + label: "Functions", + ticks: { + fontFamily: "Paper Mario Dialog Redesigned", + } + } + ] + }, + tooltips: { + enabled: false, + intersect: false, + custom(tooltipModel) { + const title = document.getElementById("progress-chart-tooltip-title") + const desc = document.getElementById("rogress-chart-tooltip-description") + + if (!tooltipModel.dataPoints) { + title.innerText = "" + desc.innerText = "" + } + + const row = data[tooltipModel.dataPoints[0].index] + console.log(row) + + title.innerText = row.commit + }, + }, + }, + }) +}