Add website's pixel count tracker

This commit is contained in:
2024-07-27 14:57:12 +02:00
parent 6aebac4578
commit d5aaccd616
11 changed files with 986 additions and 37255 deletions
+20 -1
View File
@@ -37,7 +37,10 @@ async function main(dev) {
const scriptInput = path.join(process.cwd(), "src", "index.ts");
const scriptOutput = path.join(buildVersionDir, "assets", "scripts", "content.js");
const injectScriptInput = path.join(process.cwd(), "src", "inject.ts");
const injectScriptOutput = path.join(buildVersionDir, "assets", "scripts", "inject.js");
deleteFolder(distDir);
ensureFolder(buildDir);
ensureFolder(distDir);
@@ -52,6 +55,19 @@ async function main(dev) {
platform: "browser",
}
await esbuild.build({
...common,
entryPoints: [injectScriptInput],
sourcemap: !dev,
define: {
DEV: JSON.stringify(dev),
ENVIRONMENT: JSON.stringify("user-script")
},
outfile: injectScriptOutput,
});
const raw = JSON.stringify(fs.readFileSync(injectScriptOutput, "utf-8"));
const injectCode = raw.substring(1, raw.length - 1);
await esbuild.build({
...common,
sourcemap: !dev,
@@ -75,6 +91,9 @@ async function main(dev) {
outfile: userScriptOutput,
});
[scriptOutput, userScriptOutput].forEach(path => {
fs.writeFileSync(path, fs.readFileSync(path, "utf-8").replace("/* CROSS_WORLD_INJECT_CODE */", injectCode));
});
const code = fs.readFileSync(userScriptOutput, "utf-8");
let template = fs.readFileSync(userScriptTemplateLocation, "utf-8");
const date = new Date();
+798 -37046
View File
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -6,7 +6,12 @@
"48": "assets/images/icon_48.png",
"128": "assets/images/icon_128.png"
},
"web_accessible_resources": [],
"web_accessible_resources": [
{
"resources": ["assets/scripts/inject.js"],
"matches": ["<all_urls>"]
}
],
"permissions": [
"storage"
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pixelcanvas-overlay",
"version": "1.0.3",
"version": "1.0.4",
"description": "Pixel canvas overlay",
"main": "index.js",
"scripts": {
+3 -2
View File
@@ -9,11 +9,12 @@ import { SelectedMural } from "../../interfaces";
import { Minimap } from "./minimap";
import { Overlay } from "./overlay";
import { debounce } from "lodash";
import { TCLogo } from "../../constants";
import { TC_LOGO } from "../../constants";
import styled from "styled-components";
const Img = styled.img`
width: 25px;
margin-right: 5px;
display: inline;
pointer-events: none;
touch-action: none;
@@ -98,7 +99,7 @@ export class Main extends React.Component<Props, State> {
}
title() {
return <span title={"Terncode's fork"}>
<Img src={TCLogo} alt="TC-Logo"/>
<Img src={TC_LOGO} alt="TC-Logo"/>
<span>Overlay</span>
</span>;
}
+73
View File
@@ -0,0 +1,73 @@
import React from "react";
import { PixelPlaced } from "../../lib/pixelPlaced";
import styled from "styled-components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSquare } from "@fortawesome/free-solid-svg-icons";
import { formatNumber } from "../../lib/utils";
const CountBox = styled.div`
position: fixed;
display: flex;
flex-direction: row;
bottom: 107px;
right: 16px;
background-color: rgba(255, 255, 255, 0.75);
border: 2px solid black;
border-radius: 12px;
margin: 5px auto;
padding: 5px;
height: 40px;
`;
const Count = styled.span`
margin-left: 5px;
`;
const Icon = styled.span`
margin: 2px 5px;
width: 19px;
`;
interface Props {
pixelCount: PixelPlaced;
}
interface State {
count: number;
}
export class PixelCount extends React.Component<Props, State> {
constructor(props:Props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
this.props.pixelCount.count.then(countMaybe => {
if (typeof countMaybe === "number") {
this.updateCount(countMaybe);
}
});
this.props.pixelCount.on(this.updateCount);
}
componentWillUnmount() {
this.props.pixelCount.on(this.updateCount);
}
updateCount = (count: number) => {
this.setState({count});
};
render() {
if (!this.state.count) return null;
return (
<CountBox>
<Count>{formatNumber(this.state.count)}</Count>
<Icon><FontAwesomeIcon icon={ faSquare }></FontAwesomeIcon></Icon>
</CountBox>
);
}
}
+10 -201
View File
@@ -1,8 +1,3 @@
import { Mural } from "../interfaces";
import { canvasToImageData, createCanvas, flatQuantizeImageData,
getExtension, getMuralHeight, getMuralWidth, imageDataToPaletteIndices,
imageToCanvas, loadImageSource, readAsDataUrl, readAsString, resize, validateMural } from "../lib/utils";
import { FileInput } from "../lib/fileinput";
import { Palette } from "../lib/palette";
import { Store } from "../lib/store";
import React from "react";
@@ -10,11 +5,17 @@ import { Coordinates } from "../lib/coordinates";
import { Storage } from "../lib/storage";
import { Main } from "./components/main";
import { createRoot } from "react-dom/client";
import { PixelPlaced } from "../lib/pixelPlaced";
import { PixelCount } from "./components/pixelCount";
const TEXT_FORMATS = ["muraljson", "json"];
export function createUI(store: Store, storage: Storage, cords: Coordinates, palette: Palette) {
return appendWindow(<Main cords={cords} store={store} storage={storage} palette={palette} />);
export function createUI(store: Store, storage: Storage, cords: Coordinates, palette: Palette, pixels: PixelPlaced) {
const unmounts = [
appendWindow(<PixelCount pixelCount={pixels} />),
appendWindow(<Main cords={cords} store={store} storage={storage} palette={palette}/>),
];
return () => {
return unmounts.map(e => e());
};
}
function appendWindow(children: React.ReactNode) {
@@ -25,195 +26,3 @@ function appendWindow(children: React.ReactNode) {
rootElement.parentElement?.removeChild(rootElement);
};
}
export class UI {
private container: HTMLDivElement;
private list: HTMLDivElement;
constructor(private store: Store, private palette: Palette) {
this.container = document.createElement("div");
const s = this.container.style;
this.container.setAttribute("mod", "pixel-canvas-overlay");
s.position = "fixed";
s.zIndex = "1000";
s.left = "0px";
s.bottom = "0px";
const container = document.createElement("div");
const ss = container.style;
this.container.appendChild(container);
ss.backgroundColor = "black";
ss.color = "white";
ss.margin = "5px";
ss.padding = "5px";
ss.border = "1px solid white";
const h = document.createElement("h3");
h.textContent = "Pixel Canvas overlay";
const input = this.btn("Import", async () => {
const mural = await this.import();
if (mural) {
this.store.add(mural);
this.renderList();
}
});
container.appendChild(h);
container.appendChild(input);
this.list = document.createElement("div");
this.list.style.display = "flex";
this.list.style.flexDirection = "column";
container.appendChild(this.list);
}
private btn(value: string, onClick: () => void) {
const button = document.createElement("button");
button.style.backgroundColor = "black";
button.style.color = "white";
button.style.border = "1px solid white";
button.style.padding = "2px";
button.textContent = value;
button.addEventListener("click", onClick);
return button;
}
import = async () => {
const file = await this.importFile();
if (file) {
if (file.type === "mural") {
return file.data as Mural;
} else {
const pixels = await this.imageToMural(file.data as HTMLImageElement);
await new Promise<Mural>((resolve, reject)=> {
this.store.setOverlayModify({
pixels,
cb: (name, x, y, confirm) => {
if (confirm) {
resolve({ name, pixels, x, y });
} else {
reject(new Error("Canceled by user"));
}
}
});
});
const heights = prompt(`Enter name and location [name, x, y]`) || "";
const data = heights.split(",");
const numbers = [data[1], data[2]].map(e=> parseInt(e.trim(), 10));
if (typeof numbers[1] === "number" && typeof numbers[2] === "number") {
alert("Invalid size");
}
const mural: Mural = {
name: data[0],
x: numbers[0],
y: numbers[1],
pixels
};
try {
validateMural(mural);
} catch (error) {
alert(error.message);
}
return mural;
}
}
};
private async importFile() {
const fileInput = new FileInput();
fileInput.setAcceptType(["png", "jpg", "jpeg", ...TEXT_FORMATS]);
const files = await fileInput.show();
const fileData = files[0];
if (!fileData) {
throw new Error("Empty file");
}
const ex = getExtension(fileData.name);
const name = ex.text;
if (TEXT_FORMATS.includes(ex.ex)) {
const content = await readAsString(fileData);
const mural = JSON.parse(content) as Mural;
if (!mural.name) {
mural.name = prompt("Missing name for this mural. Please enter it manually", name) || "";
}
validateMural(mural);
return {
type: "mural",
data: mural,
};
} else {
const readData = await readAsDataUrl(fileData);
return {
type: "image",
data: await loadImageSource(`${ex.text}.${ex.ex}`, readData),
};
}
}
private async imageToMural(image: HTMLImageElement) {
const actualImage = await this.getImageSettingFromUser(image);
const canvas = imageToCanvas(actualImage);
const imageData = canvasToImageData(canvas);
flatQuantizeImageData(imageData, this.palette.palette);
return imageDataToPaletteIndices(imageData, this.palette.palette);
}
private async getImageSettingFromUser(image: HTMLImageElement) {
const should = confirm(`You are importing image with size ${image.width}x${image.height}. Do you want to preform any image manipulations?`);
if (should) {
if (confirm(`Do you want to resize?\n Image ${image.width}x${image.height}?`)) {
const heights = prompt(`Enter new [width x height]`) || "";
const numbers = heights.split("x").map(e=> parseInt(e.trim(), 10));
if (typeof numbers[0] === "number" && typeof numbers[1] === "number" && numbers[0] > 0 && numbers[1] > 0) {
alert("Invalid size");
return image;
}
image = await resize(image, numbers[0], numbers[1]);
}
}
return image;
}
private renderList() {
while (this.list.children.length) {
this.list.removeChild(this.list.children[0]);
}
for (const mural of this.store.murals) {
const container = document.createElement("div");
container.style.border = `${this.store.selected?.m === mural ? 3 : 1}px solid white`;
container.style.padding = "2px";
const h5 = document.createElement("h5");
h5.textContent = mural.name;
const canvas = createCanvas();
canvas.style.maxHeight = "100px";
canvas.style.maxWidth = "100px";
const ctx = canvas.getContext("2d")!;
canvas.width = getMuralWidth(mural);
canvas.height = getMuralHeight(mural);
ctx.drawImage(mural.ref, 0, 0, canvas.width, canvas.height);
const wh = document.createElement("div");
wh.textContent = `Size ${getMuralWidth(mural)}x${getMuralHeight(mural)}`;
const at = document.createElement("div");
at.textContent = `At: ${mural.x}x${mural.y}`;
container.appendChild(h5);
container.appendChild(wh);
container.appendChild(at);
container.appendChild(canvas);
const select = this.btn("select", () => {
this.store.select(mural);
this.renderList();
});
const del = this.btn("delete", () => {
this.store.remove(mural);
this.renderList();
});
container.appendChild(select);
container.appendChild(del);
this.list.appendChild(container);
}
}
async append() {
document.body.appendChild(this.container);
this.renderList();
}
destroy() {
if (this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
}
}
+3 -1
View File
File diff suppressed because one or more lines are too long
+5 -2
View File
@@ -5,19 +5,22 @@ import { createUI } from "./UI/ui";
import { waitForDraw } from "./lib/utils";
import { Storage } from "./lib/storage";
import { Store } from "./lib/store";
import { PixelPlaced } from "./lib/pixelPlaced";
async function main() {
const palette = new Palette();
while(!palette.init()) { await waitForDraw();}
const storage = new Storage(ENVIRONMENT === "browser-extension");
const pixelPlaced = PixelPlaced.create(storage);
const store = new Store(storage, palette);
await store.load();
const coordinates = new Coordinates();
await coordinates.init();
createUI(store,storage, coordinates, palette);
createUI(store,storage, coordinates, palette, pixelPlaced);
console.log("%cOverlay by 0xa663 loaded", "color: red; font-size: 20px; font-weight: bold; text-shadow: 2px 2px 4px #000000;");
console.log("%cOverlay by 0xa663", "color: red; font-size: 15px; font-weight: bold; text-shadow: 2px 2px 4px #000000;");
console.log("%cTerncode's fork", `color: #4a94cc; font-size: 10px; font-weight: bold; text-shadow: 1px 1px 0px #000000, -1px 1px 0px #000000, 1px -1px 0px #000000, -1px -1px 0px #000000;`);
}
if (document.readyState === "complete") {
+14
View File
@@ -0,0 +1,14 @@
// Creating artificial API between two worlds
const log = console.log;
console.log = (...args: any) => {
const firstArg = args[0];
if (firstArg && typeof firstArg === "object" && "pixelsPlaced" in firstArg) {
const pixelsPlaced = new CustomEvent("__pixelsPlaced", {
detail: firstArg.pixelsPlaced,
});
document.dispatchEvent(pixelsPlaced);
}
log(...args);
};
+53
View File
@@ -0,0 +1,53 @@
import { INJECT_SCRIPT_PIXEL_OBSERVER } from "../constants";
import { BasicEventEmitter } from "./eventEmitter";
import { Storage } from "./storage";
export class PixelPlaced {
private emitter = new BasicEventEmitter();
private static instance: PixelPlaced;
private readonly storageKey = "__pixelsPlaced";
private constructor(private storage: Storage) {
// The website is logging pixels as object onto pixelsPlaced onto console with .log.
// With extension we need to cross 2 worlds. Adding artificial api to document
const api = document.createElement("script");
api.addEventListener("load", () => {
document.body.removeChild(api);
}, { once: true });
api.addEventListener("error", error => {
console.error(error);
});
if (ENVIRONMENT === "browser-extension") {
api.src = chrome.runtime.getURL("/assets/scripts/inject.js");
} else {
api.textContent = INJECT_SCRIPT_PIXEL_OBSERVER;
}
document.body.appendChild(api);
(document as any).addEventListener("__pixelsPlaced", (event: CustomEvent<number>) => {
if (typeof event.detail === "number") {
this.storage.setItem(this.storageKey, event.detail);
this.emitter.emit(0, event.detail);
}
});
}
static create(storage: Storage) {
if (!this.instance) {
this.instance = new PixelPlaced(storage);
}
return this.instance;
}
on(fn: (count: number) => void) {
this.emitter.on(0, fn);
}
off(fn: (count: number) => void) {
this.emitter.off(0, fn);
}
get count() {
return this.storage.getItem(this.storageKey);
}
}