-
-
-
Users Every 10 Minutes
@@ -68,7 +71,10 @@
Best Pony
Misty
-
+
+
Creator
+
MercurialPony
+
@@ -88,8 +94,8 @@
-
Hours Wasted
-
0
+
Your pixels
+
0
@@ -117,11 +123,4 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/stats/script.js b/public/stats/script.js
index df4be68..b2b0c4a 100644
--- a/public/stats/script.js
+++ b/public/stats/script.js
@@ -1,330 +1,119 @@
-const usersChartElement = document.getElementById("users-chart");
-let usersChart;
-
-const pixelsChartElement = document.getElementById("pixels-chart");
-let pixelsChart;
-
-const colorsChartElement = document.getElementById("colors-chart");
-let colorsChart;
-
-
-const pixelCount = document.getElementById("pixel-count");
-const daysSpent = document.getElementById("days-spent");
-const hoursSpent = document.getElementById("hours-spent");
+import StateTracker from "../scripts/state.tracker.js";
+import AudioMixer from "../scripts/audio.mixer.js";
+import * as Components from "./scripts/stats.components.js";
+
+
+
+// ============ Setup ===============
+
+const COMPONENT_STATE = StateTracker({
+ sizeX: 0,
+ sizeY: 0,
+ pivotX: 0,
+ pivotY: 0,
+ pixelCount: 0,
+ pixelCountByColor: {},
+ pixelCountOverTime: {},
+ pixelCountInterval: 0,
+ userCount: 0,
+ uniqueUserCount: 0,
+ userCountOverTime: {},
+ hasPersonalStats: false,
+ personalPixels: []
+});
-const userCount = document.getElementById("user-count");
-const uniqueUserCount = document.getElementById("unique-user-count");
+const MIXER = new AudioMixer(new AudioContext());
+MIXER.getChannel("master").gain.value = 0.2;
-const loginButtonContainer = document.getElementById("login-button-container");
-const personalStatsContainer = document.getElementById("personal-stats-container");
-const hoursWasted = document.getElementById("hours-wasted");
-const heatmap = document.getElementById("heatmap");
+// ============ Assets ===============
+const CONFETTI_SOUND = await MIXER.load("../assets/sounds/confetti.mp3");
-const favoriteColorsChartElement = document.getElementById("favorite-colors-chart");
-let favoriteColorsChart;
-const yourPixelsChartElement = document.getElementById("your-pixels-chart");
-let yourPixelsChart;
+// ============ Setup elements ===============
+COMPONENT_STATE.subscribe("pixelCount", Components.PixelCountComponent());
+COMPONENT_STATE.subscribe("userCount", Components.UserCountComponent());
+COMPONENT_STATE.subscribe("uniqueUserCount", Components.UniqueUserCountComponent());
+COMPONENT_STATE.subscribe("mostConcurrentUsers", Components.MostConcurrentUsersComponent());
+COMPONENT_STATE.subscribe("userCountOverTime", Components.UserCountChartComponent());
+COMPONENT_STATE.subscribe("pixelCountOverTime", "pixelCountInterval", Components.PixelCountChartComponent());
+COMPONENT_STATE.subscribe("pixelCountByColor", Components.ColorChartComponent());
+COMPONENT_STATE.subscribe("hasPersonalStats", Components.LoginButtonComponent());
+COMPONENT_STATE.subscribe("hasPersonalStats", Components.YourStatsComponent());
+COMPONENT_STATE.subscribe("personalPixels", Components.YourPixelCountComponent());
+COMPONENT_STATE.subscribe("personalPixels", "sizeX", "sizeY", "pivotX", "pivotY", Components.YourPixelMapComponent(2));
+COMPONENT_STATE.subscribe("personalPixels", Components.YourFavoriteColorsChartComponent());
+COMPONENT_STATE.subscribe("personalPixels", Components.YourPixelCountChart(1 * 60 * 60 * 1000));
+document.getElementById("login-button").onclick = () => window.location.href = "/login?from=stats";
-const confettiSound = new Howl({ src: [ "../sounds/confetti.mp3" ], volume: 0.2 });
+// ============ Load data ===============
-function rgbIntToHex(rgbInt)
+async function loadStats()
{
- return "#" + Number(rgbInt).toString(16).padStart(6, "0");
-}
+ const stats = await fetch("../statistics").then(r => r.json());
-// https://stackoverflow.com/questions/36721830/convert-hsl-to-rgb-and-hex
-function hslToHex(h, s, l)
-{
- l /= 100;
+ console.info(stats);
- const a = s * Math.min(l, 1 - l) / 100;
+ COMPONENT_STATE.pixelCount = stats.global.pixelCount;
+ COMPONENT_STATE.pixelCountByColor = stats.global.pixelCountByColor;
+ COMPONENT_STATE.pixelCountOverTime = stats.global.pixelCountOverTime;
+ COMPONENT_STATE.pixelCountInterval = stats.global.pixelCountInterval;
+ COMPONENT_STATE.userCount = stats.global.userCount;
+ COMPONENT_STATE.uniqueUserCount = stats.global.uniqueUserCount;
+ COMPONENT_STATE.userCountOverTime = stats.global.userCountOverTime;
+ COMPONENT_STATE.mostConcurrentUsers = stats.global.mostConcurrentUsers;
- const f = n =>
- {
- const k = (n + h / 30) % 12;
- const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
- return Math.round(255 * color).toString(16).padStart(2, "0");
- }
+ if (!stats.personal) return;
- return `#${f(0)}${f(8)}${f(4)}`;
-}
+ const canvas = await fetch("../canvas/state").then(r => r.json());
-function randomInt(min, max)
-{
- return Math.floor(Math.random() * (max - min + 1)) + min;
-}
+ COMPONENT_STATE.sizeX = canvas.sizeX;
+ COMPONENT_STATE.sizeY = canvas.sizeY;
+ COMPONENT_STATE.pivotX = canvas.pivotX;
+ COMPONENT_STATE.pivotY = canvas.pivotY;
-// https://gist.github.com/bendc/76c48ce53299e6078a76
-function generateNiceHexColor()
-{
- return hslToHex(randomInt(0, 360), randomInt(42, 98), randomInt(40, 90));
+ COMPONENT_STATE.hasPersonalStats = true;
+ COMPONENT_STATE.personalPixels = stats.personal.pixels;
}
-Array.prototype.groupBy = function(criteria)
-{
- return this.reduce((groupings, item) =>
- {
- const key = criteria(item);
+await loadStats();
- if(key != null) // also checks undefined
- {
- groupings[key] ??= [];
- groupings[key].push(item);
- }
- return groupings;
- }, {});
-}
-function startInterval(intervalTimeMs, action)
-{
- setInterval(action, intervalTimeMs);
- action();
-}
+// ============ Easter egg ===============
-function objectToDataset(dataset, mapKey, mapValue, mapColor, properties)
-{
- const labels = [];
- const data = [];
- let backgroundColor;
+let congratulated = false;
- for(const key in dataset)
- {
- labels.push(mapKey ? mapKey(key) : key);
- data.push(mapValue ? mapValue(dataset[key]) : dataset[key]);
-
- if(typeof mapColor === "function")
- {
- backgroundColor ??= [];
- backgroundColor.push(mapColor(key, dataset[key]));
- }
- else
- {
- backgroundColor = mapColor;
- }
- }
-
- return { labels, datasets: [ Object.assign(properties || {}, { data, backgroundColor }) ] };
-}
-
-startInterval(5 * 60 * 1000 /* 5 mins */, async () =>
+function congratulate()
{
- const res = await fetch("https://place.manechat.net/stats-json");
- const stats = await res.json();
-
- console.log(stats);
-
- pixelCount.innerHTML = stats.global.pixelCount;
- daysSpent.innerHTML = (stats.global.pixelCount / 24 / 60).toFixed(2);
- hoursSpent.innerHTML = (stats.global.pixelCount / 60).toFixed(2);
-
- userCount.innerHTML = stats.global.userCount;
- uniqueUserCount.innerHTML = stats.global.uniqueUserCount;
-
- const startTimeMs = Date.now() - 24 * 60 * 60 * 1000; // 24 hrs
-
- if(usersChart)
- {
- usersChart.destroy();
- }
-
- usersChart = new Chart(usersChartElement,
- {
- type: "line",
- data: objectToDataset(stats.global.userCountOverTime, key => Number(key), null, generateNiceHexColor(), { tension: 0.3 }),
- options:
- {
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- scales: { x: { type: "time", min: startTimeMs } }
- }
- });
-
- if(pixelsChart)
- {
- pixelsChart.destroy();
- }
-
- pixelsChart = new Chart(pixelsChartElement,
- {
- type: "line",
- data: objectToDataset(stats.global.pixelCountOverTime, key => Number(key), null, generateNiceHexColor(), { tension: 0.1 }),
- options:
- {
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- scales: { x: { type: "time", min: startTimeMs } }
- }
- });
-
- if(colorsChart)
- {
- colorsChart.destroy();
- }
-
- colorsChart = new Chart(colorsChartElement,
- {
- type: "bar",
- data: objectToDataset(stats.global.colorCounts, rgbIntToHex, null, rgbIntToHex),
- options:
- {
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- }
- });
-
-
-
- if(!stats.personal)
- {
- loginButtonContainer.classList.remove("hidden");
- personalStatsContainer.classList.add("hidden");
- return;
- }
-
- loginButtonContainer.classList.add("hidden");
- personalStatsContainer.classList.remove("hidden");
-
-
-
- const heatmapScale = 2;
-
- // TODO Get this from the server
- heatmap.width = 500 * heatmapScale;
- heatmap.height = 500 * heatmapScale;
- heatmap.style.maxWidth = `${500 * heatmapScale}px`;
- heatmap.style.maxHeight = `${500 * heatmapScale}px`;
-
- const heatmapCtx = heatmap.getContext("2d");
-
-
-
- hoursWasted.innerHTML = (stats.personal.pixelEvents.length / 60).toFixed(2);
-
-
-
- const favoriteColorCounts = {};
-
- for(const pixelEvent of stats.personal.pixelEvents)
- {
- favoriteColorCounts[pixelEvent.color] ??= 0;
- favoriteColorCounts[pixelEvent.color]++;
-
- heatmapCtx.fillStyle = rgbIntToHex(pixelEvent.color);
- heatmapCtx.fillRect(pixelEvent.x * heatmapScale, pixelEvent.y * heatmapScale, heatmapScale, heatmapScale);
- }
-
- if(favoriteColorsChart)
- {
- favoriteColorsChart.destroy();
- }
-
- favoriteColorsChart = new Chart(favoriteColorsChartElement,
- {
- type: "bar",
- data: objectToDataset(favoriteColorCounts, rgbIntToHex, null, rgbIntToHex),
- options:
- {
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- }
- });
-
+ if (congratulated) return;
+ congratulated = true;
- if(yourPixelsChart)
+ const fire = (particleRatio, opts) =>
{
- yourPixelsChart.destroy();
+ confetti({ origin: { y: 0.7 }, particleCount: Math.floor(200 * particleRatio), ...opts });
}
- const yourStartTimeMs = Date.now() - 7 * 24 * 60 * 60 * 1000 /* 7 days */;
- const yourIntervalTimeMs = 60 * 60 * 1000 /* 60 min */;
+ fire(0.25, { spread: 26, startVelocity: 55, });
+ fire(0.2, { spread: 60, });
+ fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 });
+ fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 });
+ fire(0.1, { spread: 120, startVelocity: 45, });
- const yourPixelsOverTime = stats.personal.pixelEvents.groupBy(pixelEvent =>
- {
- const intervalStartTimeMs = Math.floor( (pixelEvent.timestamp - yourStartTimeMs) / yourIntervalTimeMs ) * yourIntervalTimeMs;
-
- return pixelEvent.timestamp < yourStartTimeMs ? undefined : intervalStartTimeMs + yourStartTimeMs;
- } );
-
- for(const timestamp in yourPixelsOverTime)
- {
- yourPixelsOverTime[timestamp] = yourPixelsOverTime[timestamp].length;
- }
-
- // all below is very YIKES
- let newTimeMs = yourStartTimeMs;
-
- const nowMs = Date.now();
-
- const newYourPixelsOverTime = {};
-
- while(newTimeMs < nowMs)
- {
- newYourPixelsOverTime[newTimeMs] = yourPixelsOverTime[newTimeMs] || 0;
-
- newTimeMs += yourIntervalTimeMs;
- }
-
- yourPixelsChart = new Chart(yourPixelsChartElement,
- {
- type: "line",
- data: objectToDataset(newYourPixelsOverTime, key => Number(key), null, generateNiceHexColor(), { tension: 0.3 }),
- options:
- {
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- scales: { x: { type: "time", min: yourStartTimeMs, time: { unit: "days" }, ticks: { callback: val => new Date(val).toLocaleDateString("en-US", { weekday: "long" }) } } }
- }
- });
-});
-
-function login()
-{
- window.location.href = "/auth/discord?from=stats";
+ CONFETTI_SOUND.play();
}
-function isElementInViewport(el)
+const OBSERVER = new IntersectionObserver(entries =>
{
- const rect = el.getBoundingClientRect();
+ for (const e of entries) if (e.intersectionRatio > 0) congratulate();
+}, { threshold: 1 });
- return rect.top >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight);
-}
-
-const confettiInterval = setInterval(() =>
-{
- if(isElementInViewport(hoursWasted) && !personalStatsContainer.classList.contains("hidden"))
- {
- clearInterval(confettiInterval);
-
- const count = 200;
- const defaults = { origin: { y: 0.7 } };
-
- function fire(particleRatio, opts)
- {
- confetti(
- {
- ...defaults,
- ...opts,
- particleCount: Math.floor(count * particleRatio)
- });
- }
-
- fire(0.25, { spread: 26, startVelocity: 55, });
- fire(0.2, { spread: 60, });
- fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 });
- fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 });
- fire(0.1, { spread: 120, startVelocity: 45, });
-
- confettiSound.play();
- }
-}, 200);
\ No newline at end of file
+OBSERVER.observe(document.getElementById("favorite-colors-chart"));
\ No newline at end of file
diff --git a/public/stats/scripts/stats.components.js b/public/stats/scripts/stats.components.js
new file mode 100644
index 0000000..cb202f7
--- /dev/null
+++ b/public/stats/scripts/stats.components.js
@@ -0,0 +1,342 @@
+// ============= Utility =============
+
+function createChart(element, type, scales)
+{
+ return new Chart(element, {
+ type: type,
+ options: {
+ maintainAspectRatio: false,
+ plugins: { legend: { display: false } },
+ elements: { point: { radius: 0 } },
+ scales
+ }
+ });
+}
+
+class Dataset
+{
+ constructor(object)
+ {
+ this._object = object;
+
+ this._sorter = null;
+ this._keyMapper = null;
+ this._valueMapper = null;
+ this._propertyMapper = {};
+ }
+
+ static from(object)
+ {
+ return new Dataset(object);
+ }
+
+ sort(sorter)
+ {
+ this._sorter = sorter;
+ return this;
+ }
+
+ keys(mapper)
+ {
+ this._keyMapper = mapper;
+ return this;
+ }
+
+ values(mapper)
+ {
+ this._valueMapper = mapper;
+ return this;
+ }
+
+ with(mapper)
+ {
+ Object.assign(this._propertyMapper, mapper);
+ return this;
+ }
+
+ create()
+ {
+ const entries = Object.entries(this._object);
+ if (this._sorter) entries.sort(this._sorter);
+
+ const labels = [];
+ const data = [];
+
+ for(const [ key, value ] of entries)
+ {
+ labels.push(this._keyMapper ? this._keyMapper(key) : key);
+ data.push(this._valueMapper ? this._valueMapper(value) : value);
+ }
+
+ const dataset = { data };
+
+ for (const property in this._propertyMapper)
+ {
+ const value = this._propertyMapper[property];
+ dataset[property] = typeof value === "function" ? entries.map(e => value(...e)) : value;
+ }
+
+ return { labels, datasets: [ dataset ] };
+ }
+}
+
+// Pad an aligned timestamp -> count map with zeroes during idle moments
+function padIdleCounts(events, interval)
+{
+ const first = Math.min(...Object.keys(events));
+ const now = Date.now();
+
+ for (let timestamp = first; timestamp < now; timestamp += interval)
+ {
+ events[timestamp] ??= 0;
+ }
+}
+
+function rgbIntToHex(rgbInt)
+{
+ return "#" + Number(rgbInt).toString(16).padStart(6, "0");
+}
+
+// https://stackoverflow.com/questions/36721830/convert-hsl-to-rgb-and-hex
+function hslToHex(h, s, l)
+{
+ l /= 100;
+
+ const a = s * Math.min(l, 1 - l) / 100;
+
+ const f = n =>
+ {
+ const k = (n + h / 30) % 12;
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
+ return Math.round(255 * color).toString(16).padStart(2, "0");
+ }
+
+ return `#${f(0)}${f(8)}${f(4)}`;
+}
+
+function randomInt(min, max)
+{
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+// https://gist.github.com/bendc/76c48ce53299e6078a76
+function generateNiceHexColor()
+{
+ return hslToHex(randomInt(0, 360), randomInt(42, 98), randomInt(40, 60));
+}
+
+function align(number, alignment)
+{
+ return Math.floor(number / alignment) * alignment;
+}
+
+
+
+
+
+// ============= Components =============
+
+export function PixelCountComponent()
+{
+ const element = document.getElementById("pixel-count");
+
+ return state => // pixelCount
+ {
+ element.textContent = state.pixelCount;
+ };
+}
+
+export function UserCountComponent()
+{
+ const element = document.getElementById("user-count");
+
+ return state => // userCount
+ {
+ element.textContent = state.userCount;
+ };
+}
+
+export function UniqueUserCountComponent()
+{
+ const element = document.getElementById("unique-user-count");
+
+ return state => // uniqueUserCount
+ {
+ element.textContent = state.uniqueUserCount;
+ };
+}
+
+export function MostConcurrentUsersComponent()
+{
+ const element = document.getElementById("max-concurrent-user-count");
+
+ return state => // mostConcurrentUsers
+ {
+ element.textContent = state.mostConcurrentUsers;
+ };
+}
+
+export function UserCountChartComponent()
+{
+ const chart = createChart(document.getElementById("users-chart"), "line", { x: { type: "time", /* min: startTimeMs */ }, y: { beginAtZero: true } });
+
+ return state => // userCountOverTime
+ {
+ const color = generateNiceHexColor();
+
+ chart.data = Dataset.from(state.userCountOverTime)
+ .sort(([ k1 ], [ k2 ]) => k1 - k2)
+ .keys(k => +k)
+ .with({ borderColor: color })
+ .with({ backgroundColor: color + "40" })
+ .with({ fill: true })
+ .with({ borderWidth: 0.95 })
+ .create();
+
+ chart.update();
+ };
+}
+
+export function PixelCountChartComponent()
+{
+ const chart = createChart(document.getElementById("pixels-chart"), "line", { x: { type: "time", /* min: startTimeMs */ } });
+
+ return state => // pixelCountOverTime, pixelCountInterval
+ {
+ const pixelCountOverTime = { ...state.pixelCountOverTime };
+ padIdleCounts(pixelCountOverTime, state.pixelCountInterval || 10 * 60 * 1000);
+
+ const color = generateNiceHexColor();
+
+ chart.data = Dataset.from(pixelCountOverTime)
+ .sort(([ k1 ], [ k2 ]) => k1 - k2)
+ .keys(k => +k)
+ .with({ backgroundColor: color + "40" })
+ .with({ borderColor: color })
+ .with({ fill: true })
+ .with({ borderWidth: 0.95 })
+ .create();
+
+ chart.update();
+ };
+}
+
+export function ColorChartComponent()
+{
+ const chart = createChart(document.getElementById("colors-chart"), "bar");
+
+ return state => // pixelCountByColor
+ {
+ chart.data = Dataset.from(state.pixelCountByColor)
+ .keys(rgbIntToHex)
+ .with({ backgroundColor: rgbIntToHex })
+ .create();
+
+ chart.update();
+ };
+}
+
+export function LoginButtonComponent()
+{
+ const element = document.getElementById("login-button-container");
+
+ return state => // hasPersonalStats
+ {
+ element.classList.toggle("hidden", state.hasPersonalStats);
+ };
+}
+
+export function YourStatsComponent()
+{
+ const element = document.getElementById("personal-stats-container");
+
+ return state => // hasPersonalStats
+ {
+ element.classList.toggle("hidden", !state.hasPersonalStats);
+ };
+}
+
+export function YourPixelCountComponent()
+{
+ const element = document.getElementById("your-pixels");
+
+ return state => // personalPixels
+ {
+ element.textContent = state.personalPixels.length;
+ };
+}
+
+export function YourPixelMapComponent(scale)
+{
+ const element = document.getElementById("heatmap");
+
+ return state => // personalPixels, sizeX, sizeY, pivotX, pivotY
+ {
+ element.width = state.sizeX * scale;
+ element.height = state.sizeY * scale;
+
+ const ctx = element.getContext("2d");
+
+ for (const pixel of state.personalPixels)
+ {
+ ctx.fillStyle = rgbIntToHex(pixel.color);
+ ctx.fillRect((pixel.x + state.pivotX) * scale, (pixel.y + state.pivotY) * scale, scale, scale);
+ }
+ };
+}
+
+export function YourFavoriteColorsChartComponent()
+{
+ const chart = createChart(document.getElementById("favorite-colors-chart"), "bar");
+
+ return state => // personalPixels
+ {
+ const favoriteColorCounts = {};
+
+ for (const pixel of state.personalPixels)
+ {
+ favoriteColorCounts[pixel.color] ??= 0;
+ favoriteColorCounts[pixel.color]++;
+ }
+
+ chart.data = Dataset.from(favoriteColorCounts)
+ .keys(rgbIntToHex)
+ .with({ backgroundColor: rgbIntToHex })
+ .create();
+
+ chart.update();
+ };
+}
+
+export function YourPixelCountChart(interval)
+{
+ const scales = { x: { type: "time", time: { unit: "days" }, ticks: { callback: v => new Date(v).toLocaleDateString("en-US", { weekday: "long" }) } } };
+ const chart = createChart(document.getElementById("your-pixels-chart"), "line", scales);
+
+ return state => // personalPixels
+ {
+ const yourPixelCountOverTime = {};
+
+ for (const pixel of state.personalPixels)
+ {
+ const alignedTimestamp = align(pixel.timestamp, interval);
+ yourPixelCountOverTime[alignedTimestamp] ??= 0;
+ yourPixelCountOverTime[alignedTimestamp]++;
+ }
+
+ padIdleCounts(yourPixelCountOverTime, interval);
+
+ const color = generateNiceHexColor();
+
+ chart.data = Dataset.from(yourPixelCountOverTime)
+ .sort(([ k1 ], [ k2 ]) => k1 - k2)
+ .keys(k => +k)
+ .with({ borderColor: color })
+ .with({ backgroundColor: color + "40" })
+ .with({ fill: true })
+ .with({ borderWidth: 0.95 })
+ .create();
+
+ chart.update();
+ };
+}
\ No newline at end of file
diff --git a/public/stats/scripts/stats.constants.js b/public/stats/scripts/stats.constants.js
new file mode 100644
index 0000000..e69de29
diff --git a/public/styles.css b/public/styles.css
index de2f6af..7a6a88a 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -1,418 +1,214 @@
-/*
-* Josh's Custom CSS Reset
-* https://www.joshwcomeau.com/css/custom-css-reset/
-*/
-
-/* TODO: Css redo all min/max things, do properly */
-/* TODO: constants for colors, etc */
-/* TODO: space out */
-
-*, *::before, *::after
-{
- box-sizing: border-box;
-}
-
-*
-{
- margin: 0;
- padding: 0;
-}
-
html, body
{
- width: 100%;
- height: 100%;
-}
-
-body
-{
- line-height: 1.5;
-}
+ position: fixed;
-img, picture, video, canvas, svg
-{
- display: block;
- max-width: 100%;
-}
+ height: 100%;
+ width: 100%;
-input, button, textarea, select
-{
- font: inherit;
+ font-family: "Figtree", sans-serif;
}
-/*
- * General styles
- */
-
-.fs-s
+canvas
{
- font-size: 13px;
-}
+ position: absolute;
-.fs-m
-{
- font-size: 16px;
-}
+ height: 100%;
+ width: 100%;
-.fs-l
-{
- font-size: 18px;
-}
+ image-rendering: pixelated;
-.hidden
-{
- visibility: hidden;
+ touch-action: none;
}
-.shadow
+#footer
{
- box-shadow: 10px 10px #111111C0;
-}
-
-/*
- * Site styles
- */
+ position: absolute;
-html, body
-{
- position: fixed;
-}
+ width: 100%;
+ height: max(10%, 130px);
-body
-{
- background-color: #333333;
- overflow: hidden;
- font-family: "Noto Sans", sans-serif;
-}
+ bottom: 0;
-#main
-{
- /* This has to be absolute because otherwise iOS will blur the canvas when scaling it */
- position: absolute;
+ pointer-events: none;
}
-#loading-screen
+#place
{
- position: absolute;
+ min-width: 180px;
+ min-height: 60px;
- width: 100%;
- height: 100%;
-
- background-color: #E5E5E5;
+ margin: 0 max(2.5%, 40px);
+ padding: 7px 10px;
+ line-height: 1.5;
- display: flex;
- justify-content: center;
- align-items: center;
flex-direction: column;
- transition: 0.5s ease;
}
-#loading-screen.hidden
+#explain,
+#more
{
- opacity: 0%;
+ width: 50px;
+ height: 50px;
}
-#canvas
+#picker
{
position: absolute;
- background-color: white;
- image-rendering: pixelated;
-}
-#selector
-{
- position: absolute;
- width: 1px;
- height: 1px;
-}
+ width: 100%;
-#selector #selector-border
-{
- position: absolute;
- image-rendering: pixelated;
- transform: scale(1.5);
-}
+ bottom: 0;
-#selector-pixel, #pixel-border, #pixel-color
-{
- position: absolute;
- width: 100%;
- height: 100%;
+ transition: transform 0.2s ease;
}
-#pixel-border
+#picker.lowered
{
- background-color: black;
- transform: scale(1.2);
+ transform: translateY(100%);
}
-#pixel-color
+/* This is needed to catch clicks on the backdrop */
+.modal
{
- transform: scale(1.1);
+ width: min(80vw, 400px);
+
+ padding: 20px 30px;
}
-#ui
+.modal .close, .modal .back
{
position: absolute;
- width: 100%;
- height: max(10%, 130px);
- bottom: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- pointer-events: none;
-}
-.button
-{
- border: 3px solid #111111;
- display: flex;
- justify-content: center;
- align-items: center;
- pointer-events: all;
-}
+ height: 40px;
+ width: 40px;
-@media (hover: hover)
-{
- .button:hover
- {
- cursor: pointer;
- filter: brightness(80%);
- }
+ top: -3px;
}
-.button:active
+.modal .close
{
- transform: scale(0.95);
+ right: -3px;
}
-.orange
+.modal .back
{
- color: white;
- background-color: #FF4500;
+ left: -3px;
}
-.orange.inactive
+.modal .close:focus
{
- background-color: #2C3C41;
+ outline: none;
}
-.white
+.modal > .title
{
- background-color: white;
+ margin: 20px 0;
+
+ font-weight: 500;
}
-.gray
+.modal > .low.title
{
- background-color: #E9EDEE;
+ margin: 30px 0 20px 0;
}
-#place
+.modal > .option
{
- margin: 0 max(2.5%, 40px) 0 max(2.5%, 40px);
- width: clamp(150px, 10%, 200px);
height: 60px;
- flex-direction: column;
-}
+ margin-bottom: 10px;
-#share, #settings
-{
- width: 50px;
- height: 50px;
+ justify-content: start;
}
-#picker
+.modal .option > *
{
- position: absolute;
- padding: 10px;
- width: 100%;
- background-color: white;
- border-top: 3px solid #111111;
- bottom: 0;
- transition: transform 0.2s ease-out;
- transform: translateY(100%);
- pointer-events: all;
- display: flex;
- justify-content: center;
- flex-direction: column;
+ margin-left: 15px;
}
-#picker.open
+.modal .option > .toggle
{
- transform: translateY(0px);
+ margin-left: auto;
+ margin-right: 15px;
}
-#picker .container
+.modal .option > .block
{
- width: 100%;
- padding: 10px;
- display: flex;
- justify-content: center;
- align-items: center;
-}
+ min-width: 55px;
+ min-height: 55px;
-#colors
-{
- width: min(700px, 100%);
- height: 100%;
- display: flex;
- justify-content: center;
- align-content: center;
- align-items: center;
- flex-wrap: wrap;
- gap: 2px;
-}
+ margin-left: 0;
-.color
-{
- border: 1px solid #E5E5E5;
- aspect-ratio: 1 / 1;
- width: min(8%, 40px);
- pointer-events: all;
+ user-select: none;
}
-.color.picked
+.modal .text
{
- transform: scale(1.3);
- border: 2px solid #111111;
- box-shadow: 7px 7px #111111E0;
-}
+ padding: 0 16px;
-#cancel, #confirm
-{
- width: min(35%, 270px);
- height: 55px;
- margin: 0 15px 0 15px;
+ background-color: #f4f4f4;
}
-#confirm
+.modal .input
{
-
- transition: background-color 0.3s ease;
-}
+ width: 100%;
-#share-tooltip
-{
- background-color: black;
- border: 2px solid white;
- color: white;
- top: -10px;
+ border: none;
+ border-bottom: 3px solid #8d8d8d;
}
-#placer-tooltip
+.modal .input:hover
{
- background: white;
- border: 2px solid black;
- color: black;
- transform: translate(0%, -56%) scale(0.05);
- transition: none; /* this cannot have a transition, otherwise the canvas starts getting blurred */
- white-space: nowrap;
+ background: #ececec;
}
-.tooltip
+.modal .input:focus
{
- position: absolute;
- padding: 4px 10px 4px 10px;
-
- opacity: 0%;
- visibility: hidden;
- transition: 0.2s ease;
+ outline: none;
}
-.tooltip.visible
+#help .option:not(:last-child)
{
- opacity: 100%;
- visibility: visible;
-}
-
-.tooltip::after
-{
- content: "";
- position: absolute;
- top: 100%;
- left: 50%;
- height: 15px;
- width: 15px;
- transform: translate(-50%, -40%) rotate(45deg);
- background: inherit;
- border-bottom: inherit;
- border-right: inherit;
+ margin-bottom: 20px;
}
-#extra-screen
+#letsgo, #command
{
- position: absolute;
-
- width: 100%;
- height: 100%;
-
- display: flex;
justify-content: center;
- align-items: center;
-
- background-color: rgba(0, 0, 0, 0.5);
- transition: 0.15s ease;
-
- pointer-events: all;
-}
-
-#extra-screen.hidden
-{
- opacity: 0%;
}
-.modal
+#placer
{
- position: absolute;
- border: 3px solid #111111;
+ transition: opacity 0.2s ease;
}
-#extra-screen .modal
-{
- width: min(80%, 400px);
-}
-
-#close-extra
+.tooltip
{
position: absolute;
- height: 40px;
- width: 40px;
-
- top: -1px;
- right: -1px;
+ padding: 4px 10px;
- border-top: none;
- border-right: none;
+ /* Centers the tooltip on the bottom arrow */
+ transform: translate(-50%, -130%);
- background-color: rgb(0, 0, 0, 0);
+ pointer-events: none;
}
-.modal-content
+.tooltip::after
{
- margin-top: 30px;
-
- padding: 25px;
+ position: inherit;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
-}
+ height: 15px;
+ width: 15px;
-.modal-option
-{
- width: 100%;
- height: 60px;
+ top: 100%;
+ left: 50%;
- margin: 5px;
- padding-left: 5px;
+ transform: translate(-50%, -36%) rotate(45deg);
- justify-content: start;
-}
+ background: inherit;
+ border-bottom: inherit;
+ border-right: inherit;
-.modal-option *
-{
- margin-left: 15px;
+ content: "";
}
\ No newline at end of file
diff --git a/server.js b/server.js
deleted file mode 100644
index f6f9a59..0000000
--- a/server.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const GreenlockExpress = require("greenlock-express");
-const ExpressWS = require("express-ws");
-
-const app = require("./main.js");
-
-GreenlockExpress
- .init({ packageRoot: __dirname, configDir: "./greenlock.d", cluster: false, maintainerEmail: "mercurialpone@gmail.com" })
- .ready(glx =>
- {
- ExpressWS(app, glx.httpsServer());
- app.setUpSockets();
- })
- .serve(app);
\ No newline at end of file
diff --git a/util.js b/util.js
new file mode 100644
index 0000000..e353e19
--- /dev/null
+++ b/util.js
@@ -0,0 +1,21 @@
+export class LazyMap extends Map
+{
+ get(key, provider)
+ {
+ let value = super.get(key);
+ if (!value && provider) this.set(key, value = provider(key));
+ return value;
+ }
+}
+
+
+
+export function intersects(a, b)
+{
+ return a && b ? a.some(e => b.includes(e)) : false;
+}
+
+export function align(number, alignment)
+{
+ return Math.floor(number / alignment) * alignment;
+}
\ No newline at end of file
diff --git a/utils.js b/utils.js
deleted file mode 100644
index 3378366..0000000
--- a/utils.js
+++ /dev/null
@@ -1,23 +0,0 @@
-Array.prototype.groupBy = function(criteria)
-{
- return this.reduce((groupings, item) =>
- {
- const key = criteria(item);
-
- if(key != null) // also checks undefined
- {
- groupings[key] ??= [];
- groupings[key].push(item);
- }
-
- return groupings;
- }, {});
-}
-
-function startInterval(intervalTimeMs, action)
-{
- setInterval(action, intervalTimeMs);
- action();
-}
-
-module.exports = { startInterval };
\ No newline at end of file