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
+ },
+ },
+ },
+ })
+}