Compare commits
10
Commits
0cd0d713de
...
57a1a1f28d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57a1a1f28d | ||
|
|
137e424a79 | ||
|
|
58baae133b | ||
|
|
e11a847110 | ||
|
|
e0d1da3f11 | ||
|
|
607754eb0f | ||
|
|
cdc69bd932 | ||
|
|
f2e59cfda3 | ||
|
|
75994c110d | ||
|
|
5587dd7909 |
Vendored
+907
-77188
File diff suppressed because one or more lines are too long
Generated
+1
-1
@@ -6,7 +6,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pixelcanvas-overlay",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pixelcanvas-overlay",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.0",
|
||||
"description": "Pixel canvas overlay",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -8,10 +8,10 @@ import { MovableWindow } from "./movableWindow";
|
||||
import { Minimap } from "./minimap";
|
||||
import { Overlay } from "./overlay";
|
||||
import { debounce } from "lodash";
|
||||
import { TC_LOGO } from "../../constants";
|
||||
import styled from "styled-components";
|
||||
import { MuralEx } from "../../interfaces";
|
||||
import { PieCharts } from "./pixelCharts";
|
||||
import { TC_LOGO } from "../../assets";
|
||||
|
||||
const Img = styled.img`
|
||||
width: 25px;
|
||||
@@ -146,7 +146,7 @@ export class Main extends React.Component<Props, State> {
|
||||
muralExtended={this.props.store.murals[this.state.phantomOverlay]}
|
||||
/> : null }
|
||||
{this.state.overlayModify ? <Overlay
|
||||
muralObj={this.state.overlayModify.muralObj}
|
||||
muralModifier={this.state.overlayModify.muralModify}
|
||||
opacity={this.state.opacity}
|
||||
storage={this.props.storage}
|
||||
cords={this.props.cords}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { faCamera, faCaretDown, faCaretUp, faPieChart, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCamera, faCaretDown, faCaretUp, faLocationCrosshairs, faPieChart, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
@@ -83,6 +83,7 @@ interface Props {
|
||||
interface State {
|
||||
takingCanvasShot: boolean;
|
||||
canvasShotAvailable: boolean;
|
||||
locationPrecision: boolean;
|
||||
}
|
||||
|
||||
export class Menu extends React.Component<Props, State> {
|
||||
@@ -92,6 +93,7 @@ export class Menu extends React.Component<Props, State> {
|
||||
this.state = {
|
||||
takingCanvasShot: false,
|
||||
canvasShotAvailable: true,
|
||||
locationPrecision: props.cords.highPrecision
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,11 +118,15 @@ export class Menu extends React.Component<Props, State> {
|
||||
Popup.alert(error.name);
|
||||
}
|
||||
};
|
||||
|
||||
inputElement = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const percentage = parseInt((event.target as HTMLInputElement).value);
|
||||
this.props.onOpacityChange(percentage);
|
||||
};
|
||||
togglePrecision = () => {
|
||||
const value = !this.state.locationPrecision;
|
||||
this.setState({ locationPrecision: value });
|
||||
this.props.cords.toggleHigherPrecision(value);
|
||||
};
|
||||
screenshot = async () => {
|
||||
if (this.props.cords.uScale < 0) {
|
||||
//Popup.alert("Cannot take screenshot at sc")
|
||||
@@ -148,6 +154,14 @@ export class Menu extends React.Component<Props, State> {
|
||||
onClick={() => this.props.showChart()}
|
||||
title="Pixels graph">
|
||||
<FontAwesomeIcon icon={ faPieChart } />
|
||||
</Btn>
|
||||
<Btn
|
||||
title="Runtime coordinate calculator. Uses more resources"
|
||||
onClick={this.togglePrecision}
|
||||
style={{
|
||||
border: this.state.locationPrecision ? "" : "2px dotted black"
|
||||
}}>
|
||||
<FontAwesomeIcon icon={ faLocationCrosshairs } />
|
||||
</Btn>
|
||||
<InputRange type="range" min={0} max={100} value={this.props.opacity} onInput={this.inputElement} />
|
||||
<PercentageDiv>{this.props.opacity}%</PercentageDiv>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { A, Border, Btn, Flex, SELECTED_COLOR } from "../styles";
|
||||
import { CanvasToCanvasJSX } from "./canvasToCanvasJSX";
|
||||
import { formatNumber, getPixelStatusMural } from "../../lib/utils";
|
||||
import {
|
||||
IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash
|
||||
IconDefinition, faDownload, faImage, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Coordinates, CordType } from "../../lib/coordinates";
|
||||
@@ -211,16 +211,29 @@ export class MuralView extends React.Component<Props, State> {
|
||||
// this.props.store.updateMural(this.props.mural);
|
||||
// }
|
||||
};
|
||||
|
||||
get saveFileName() {
|
||||
const mural = this.props.muralExtended.mural;
|
||||
|
||||
return mural.name
|
||||
.replace(/ /, "_")
|
||||
.replace(/[^a-zA-Z0-9-_]/g, "");
|
||||
}
|
||||
onExport = async () => {
|
||||
const mural = this.props.muralExtended.mural;
|
||||
const buffer = await mural.getBuffer();
|
||||
const blob = new Blob([buffer], { type: "octet/stream" });
|
||||
|
||||
const saveName = mural.name
|
||||
.replace(/ /, "_")
|
||||
.replace(/[^a-zA-Z0-9-_]/g, "");
|
||||
|
||||
saveAs(blob, `${saveName}.${BIN_FORMATS[0]}`);
|
||||
saveAs(blob, `${this.saveFileName}.${BIN_FORMATS[0]}`);
|
||||
};
|
||||
onImage = async () => {
|
||||
this.props.muralExtended.ref.toBlob(blob => {
|
||||
if (blob) {
|
||||
saveAs(blob, `${this.saveFileName}.png`);
|
||||
} else {
|
||||
Popup.alert("Failed to export as PNG");
|
||||
}
|
||||
}, "image/png");
|
||||
};
|
||||
onDelete = async () => {
|
||||
if (await Popup
|
||||
@@ -308,6 +321,7 @@ export class MuralView extends React.Component<Props, State> {
|
||||
{this.btn("Preview", faLayerGroup, this.onPreview, this.props.store.hasOverlay(this.props.muralExtended))}
|
||||
{this.btn("Modify", faPenToSquare, this.onModify)}
|
||||
{this.btn("Export", faDownload, this.onExport)}
|
||||
{this.btn("PNG", faImage, this.onImage)}
|
||||
{this.btn("Delete", faTrash, this.onDelete)}
|
||||
<A href={this.link}>Goto <FontAwesomeIcon icon={faLocation}/></A>
|
||||
</Flex>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Palette } from "../../lib/palette";
|
||||
import { MovableWindow } from "./movableWindow";
|
||||
import { Storage } from "../../lib/storage";
|
||||
import { MuralEditor } from "./muralEditor";
|
||||
import { Mural } from "../../lib/mural";
|
||||
|
||||
const Canvas = styled.canvas`
|
||||
position: fixed;
|
||||
@@ -15,9 +14,9 @@ const Canvas = styled.canvas`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
muralExtended: MuralEx;
|
||||
muralObj?: Partial<Mural>;
|
||||
onChange?: (name: string, x: number, y:number, confirm?: boolean) => void;
|
||||
muralExtended: Readonly<MuralEx>;
|
||||
muralModifier?: { x: number, y: number, name: string };
|
||||
onChange?: (name: string, x: number, y: number, confirm?: boolean) => void;
|
||||
cords: Coordinates;
|
||||
storage: Storage;
|
||||
palette: Palette;
|
||||
@@ -32,7 +31,6 @@ interface State {
|
||||
|
||||
export class Overlay extends React.Component<Props, State> {
|
||||
private ref = React.createRef<HTMLCanvasElement>();
|
||||
private _refImage?: HTMLCanvasElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -44,25 +42,61 @@ export class Overlay extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
this.resize();
|
||||
|
||||
this.ref.current!.style.top = this.ref.current!.style.left = `0px`;
|
||||
this.props.cords.on(CordType.Url, this.draw);
|
||||
window.addEventListener("mousemove", this.draw);
|
||||
if (this.props.muralObj) {
|
||||
this.props.cords.on(CordType.UrlPredict, this.draw);
|
||||
this.props.cords.on(CordType.Div, this.updateIfMouseDown);
|
||||
window.addEventListener("mousemove", this.onMouseMove);
|
||||
window.addEventListener("touchmove", this.onTouchMove);
|
||||
window.addEventListener("resize", this.resize);
|
||||
if (this.props.muralModifier) {
|
||||
this.setState({
|
||||
name: this.props.muralObj.name || "",
|
||||
x: this.props.muralObj.x ?? 0,
|
||||
y: this.props.muralObj.y ?? 0,
|
||||
name: this.props.muralModifier.name || "",
|
||||
x: this.props.muralModifier.x ?? 0,
|
||||
y: this.props.muralModifier.y ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.cords.off(CordType.Url, this.draw);
|
||||
window.removeEventListener("mousemove", this.draw);
|
||||
this.props.cords.off(CordType.UrlPredict, this.draw);
|
||||
this.props.cords.off(CordType.Div, this.updateIfMouseDown);
|
||||
window.removeEventListener("mousemove", this.onMouseMove);
|
||||
window.removeEventListener("touchmove", this.onTouchMove);
|
||||
window.removeEventListener("resize", this.resize);
|
||||
}
|
||||
updateIfMouseDown = () => {
|
||||
if (this.props.cords.dragging) {
|
||||
this.draw();
|
||||
}
|
||||
};
|
||||
onMouseMove = (event: MouseEvent) => {
|
||||
if (event.buttons === 1) {
|
||||
this.draw();
|
||||
}
|
||||
};
|
||||
|
||||
onTouchMove = (event: TouchEvent) => {
|
||||
if (event.touches.length) {
|
||||
this.draw();
|
||||
}
|
||||
};
|
||||
|
||||
resize = () => {
|
||||
const canvas = this.ref.current!;
|
||||
if (canvas.height !== window.outerHeight || canvas.width !== window.outerWidth) {
|
||||
canvas.width = window.outerWidth;
|
||||
canvas.height = window.outerHeight;
|
||||
canvas.style.width = `${window.outerWidth}px`;
|
||||
canvas.style.height = `${window.outerHeight}px`;
|
||||
this.draw();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||
if (prevProps.muralExtended !== this.props.muralExtended) {
|
||||
this._refImage = undefined;
|
||||
this.setState({
|
||||
x: 0, y: 0
|
||||
});
|
||||
@@ -78,11 +112,11 @@ export class Overlay extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
get x() {
|
||||
return this.props.muralExtended.mural.x;
|
||||
return this.props.muralModifier ? this.state.x : this.props.muralExtended.mural.x;
|
||||
}
|
||||
|
||||
get y() {
|
||||
return this.props.muralExtended.mural.y;
|
||||
return this.props.muralModifier ? this.state.y : this.props.muralExtended.mural.y;
|
||||
}
|
||||
|
||||
get refImg() {
|
||||
@@ -92,33 +126,37 @@ export class Overlay extends React.Component<Props, State> {
|
||||
draw = () => {
|
||||
const canvas = this.ref.current!;
|
||||
const scale = Math.pow(2, this.props.cords.uScale);
|
||||
|
||||
const cords = this.props.cords.gridToScreen(this.x, this.y)!;
|
||||
if (!cords) {
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
return;
|
||||
}
|
||||
const { x, y } = cords;
|
||||
const x = cords.x;
|
||||
const y = cords.y;
|
||||
|
||||
const width = this.props.muralExtended.mural.w * scale;
|
||||
const height = this.props.muralExtended.mural.h * scale;
|
||||
if (width !== canvas.width || height !== canvas.height ) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
}
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
canvas.style.left = `${x}px`;
|
||||
canvas.style.top = `${y}px`;
|
||||
|
||||
ctx.drawImage(this.refImg, 0, 0, canvas.width, canvas.height);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(this.refImg, x, y, width, height);
|
||||
// if (true) {
|
||||
// ctx.strokeStyle = "#000000";
|
||||
// ctx.strokeText(`${x}px ${y}px`, 11, 11);
|
||||
// ctx.strokeText(`${x}px ${y}px`, 9, 9);
|
||||
// ctx.strokeText(`${x}px ${y}px`, 9, 11);
|
||||
// ctx.strokeText(`${x}px ${y}px`, 11, 9);
|
||||
// ctx.fillStyle = "#ffffff";
|
||||
// ctx.fillText(`${x}px ${y}px`, 10, 10);
|
||||
// }
|
||||
};
|
||||
|
||||
|
||||
private get style(): React.CSSProperties {
|
||||
return { left: this.state.x, top: this.state.y, opacity: this.props.opacity / 100};
|
||||
return { opacity: this.props.opacity / 100};
|
||||
}
|
||||
|
||||
renderModifyWindow() {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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";
|
||||
import { StoreEvents } from "../../lib/store";
|
||||
import { Store, StoreEvents } from "../../lib/store";
|
||||
|
||||
const CountBox = styled.div<{width: number}>`
|
||||
position: fixed;
|
||||
@@ -31,7 +30,7 @@ const Icon = styled.span`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
pixelCount: PixelPlaced;
|
||||
store: Store;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -50,15 +49,15 @@ export class PixelCount extends React.Component<Props, State> {
|
||||
}
|
||||
componentDidMount() {
|
||||
this.updateCount();
|
||||
this.props.pixelCount.store.on(StoreEvents.PixelLog, this.updateCount);
|
||||
this.props.store.on(StoreEvents.PixelLog, this.updateCount);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.pixelCount.store.off(StoreEvents.PixelLog, this.updateCount);
|
||||
this.props.store.off(StoreEvents.PixelLog, this.updateCount);
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
updateCount = async () => {
|
||||
this.setState({count: await this.props.pixelCount.getCount()});
|
||||
this.setState({count: await this.props.store.getPixelCount()});
|
||||
};
|
||||
onResize = () => {
|
||||
this.setState({ width: window.innerWidth });
|
||||
|
||||
+15
-23
@@ -1,10 +1,12 @@
|
||||
|
||||
import React from "react";
|
||||
import { ImportOutput, MuralOld, RGB } from "../interfaces";
|
||||
import { canvasToImageData, convertOldMuralToNewMural, getColorScore, getExtension,
|
||||
import { ImportOutput, MuralOld } from "../interfaces";
|
||||
import {
|
||||
canvasToImageData, convertOldMuralToNewMural,
|
||||
createClosestIndexColor, createMuralExtended, getExtension,
|
||||
imageDataToPaletteIndices, imageToCanvas, loadImageSource, processNumberEvent,
|
||||
readAsArrayBuffer,
|
||||
readAsDataUrl, readAsString, resize, rgb } from "../lib/utils";
|
||||
readAsDataUrl, readAsString, resize } from "../lib/utils";
|
||||
import styled from "styled-components";
|
||||
import { Popup } from "./components/Popup";
|
||||
import RgbQuant, { DitheringKernel, RGBQuantOptions } from "rgbquant";
|
||||
@@ -97,13 +99,17 @@ export async function importArtWorks(
|
||||
} else {
|
||||
const img = file.data;
|
||||
const pixels = await imageToMural(img, palette);
|
||||
const x = Math.floor(cords.ux - (img.width / 2));
|
||||
const y = Math.floor(cords.uy - (img.height / 2));
|
||||
const fakeMural = new Mural(img.alt, x, y, img.width, img.height, pixels);
|
||||
const muralExtended = createMuralExtended(fakeMural, palette.hex);
|
||||
const mural = await new Promise<Mural>((resolve, reject)=> {
|
||||
store.setOverlayModify({
|
||||
mural,
|
||||
muralObj: {
|
||||
mural: muralExtended,
|
||||
muralModify: {
|
||||
name: img.alt,
|
||||
x: Math.floor(cords.ux - (img.width / 2)),
|
||||
y: Math.floor(cords.uy - (img.height / 2)),
|
||||
x,
|
||||
y,
|
||||
},
|
||||
cb: (name, x, y, confirm) => {
|
||||
if (confirm) {
|
||||
@@ -436,13 +442,13 @@ function quantizeImage(
|
||||
}
|
||||
|
||||
export function flatQuantizeImageData(imageData: ImageData, palette: Palette) {
|
||||
const indexColor = createClosestIndexColor(palette.palette);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
const r = imageData.data[i + 0] ?? 0;
|
||||
const g = imageData.data[i + 1] ?? 0;
|
||||
const b = imageData.data[i + 2] ?? 0;
|
||||
const a = imageData.data[i + 3] ?? 0;
|
||||
const obj = rgb(r, g, b);
|
||||
const index = findClosestIndexColor(obj, palette);
|
||||
const index = indexColor(r, g, b);
|
||||
const color = palette.palette[index];
|
||||
if (!color) throw new Error(`Unknown color index ${index}`);
|
||||
imageData.data[i + 0] = color.r;
|
||||
@@ -452,20 +458,6 @@ export function flatQuantizeImageData(imageData: ImageData, palette: Palette) {
|
||||
}
|
||||
}
|
||||
|
||||
export function findClosestIndexColor(rgbO: RGB, palette: Palette) {
|
||||
const scores: number[] = [];
|
||||
|
||||
for (const rgb of palette.palette) {
|
||||
const r = getColorScore(rgbO.r, rgb.r);
|
||||
const g = getColorScore(rgbO.g, rgb.g);
|
||||
const b = getColorScore(rgbO.b, rgb.b);
|
||||
scores.push(r + g + b);
|
||||
}
|
||||
const lowest = Math.min(...scores);
|
||||
const index = scores.indexOf(lowest);
|
||||
return index;
|
||||
}
|
||||
|
||||
export function imageDataToCanvas(imageData: ImageData) {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
+2
-3
@@ -5,14 +5,13 @@ 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";
|
||||
|
||||
export function createUI(
|
||||
store: Store, storage: Storage, cords: Coordinates, palette: Palette, pixels: PixelPlaced
|
||||
store: Store, storage: Storage, cords: Coordinates, palette: Palette
|
||||
) {
|
||||
const unmounts = [
|
||||
appendWindow(<PixelCount pixelCount={pixels} />),
|
||||
appendWindow(<PixelCount store={store} />),
|
||||
appendWindow(<Main cords={cords} store={store} storage={storage} palette={palette}/>),
|
||||
];
|
||||
return () => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
+6
-2
File diff suppressed because one or more lines are too long
+5
-4
@@ -5,8 +5,8 @@ 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";
|
||||
import process from "process";
|
||||
import { Injector } from "./lib/injector";
|
||||
|
||||
async function main() {
|
||||
globalThis.process = process;
|
||||
@@ -16,11 +16,12 @@ async function main() {
|
||||
const storage = new Storage(ENVIRONMENT === "browser-extension");
|
||||
const store = new Store(storage, palette);
|
||||
await store.load();
|
||||
const pixelPlaced = PixelPlaced.create(store);
|
||||
const coordinates = new Coordinates();
|
||||
const injector = new Injector(store);
|
||||
await injector.inject();
|
||||
const coordinates = new Coordinates(injector, store);
|
||||
await coordinates.init();
|
||||
|
||||
createUI(store,storage, coordinates, palette, pixelPlaced);
|
||||
createUI(store,storage, coordinates, palette);
|
||||
|
||||
console.log(
|
||||
"%cOverlay by 0xa663",
|
||||
|
||||
+13
-88
@@ -1,99 +1,15 @@
|
||||
// Creating artificial API between two worlds
|
||||
|
||||
export type InterceptBefore = (requestInfo: string, init?: RequestInit) => Promise<void> | void;
|
||||
export type InterceptAfter = (requestInfo: string, response: Response, init?: RequestInit) => Promise<void> | void;
|
||||
|
||||
export class Interceptor {
|
||||
|
||||
private static fetch = window.fetch;
|
||||
private static overwritten = false;
|
||||
private static before = new Map<string, InterceptBefore[]>();
|
||||
private static after = new Map<string, InterceptAfter[]>();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
private static overrideFetch() {
|
||||
if (this.overwritten) {
|
||||
return;
|
||||
}
|
||||
this.overwritten = true;
|
||||
(window as any).fetch = async (...args: any) => {
|
||||
const url = ((args[0] as RequestInfo | URL) || "").toString();
|
||||
const init = args[1];
|
||||
|
||||
const befores = [...(this.before.get(url) || []), ...(this.before.get("*") || [])];
|
||||
if (befores) {
|
||||
for (const before of befores) {
|
||||
await before(url, init);
|
||||
}
|
||||
}
|
||||
|
||||
const response = (this.fetch as any)(...args);
|
||||
const afters = [...(this.after.get(url) || []), ...(this.after.get("*") || [])];
|
||||
if (afters) {
|
||||
const result = await response;
|
||||
for (const after of afters) {
|
||||
try {
|
||||
await after(url, result, init);
|
||||
} catch (_) {}
|
||||
}
|
||||
return response;
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static onAfter(url: string, cb: InterceptAfter) {
|
||||
Interceptor.overrideFetch();
|
||||
const arr = Interceptor.after.get(url) || [];
|
||||
if (!arr.includes(cb)) {
|
||||
arr.push(cb);
|
||||
}
|
||||
Interceptor.after.set(url, arr);
|
||||
}
|
||||
static offAfter(url: string, cb: InterceptAfter) {
|
||||
const arr = Interceptor.after.get(url) || [];
|
||||
const index = arr.indexOf(cb);
|
||||
if (index !== -1) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
if (arr.length) {
|
||||
Interceptor.after.set(url, arr);
|
||||
} else {
|
||||
Interceptor.after.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
static onBefore(url: string, cb: InterceptBefore) {
|
||||
Interceptor.overrideFetch();
|
||||
const arr = Interceptor.before.get(url) || [];
|
||||
if (!arr.includes(cb)) {
|
||||
arr.push(cb);
|
||||
}
|
||||
Interceptor.before.set(url, arr);
|
||||
}
|
||||
|
||||
static offBefore(url: string, cb: InterceptBefore) {
|
||||
const arr = Interceptor.before.get(url) || [];
|
||||
const index = arr.indexOf(cb);
|
||||
if (index !== -1) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
if (arr.length) {
|
||||
Interceptor.before.set(url, arr);
|
||||
} else {
|
||||
Interceptor.before.delete(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { EVENT_CROSS_WORLD_INJECTED, EVENT_CROSS_WORLD_PIXEL_PLACED, EVENT_CROSS_WORLD_URL_UPDATE } from "./constants";
|
||||
import { Interceptor } from "./lib/interceptor";
|
||||
import { HistoryPushStateSpy } from "./lib/urlHistorySpy.ts";
|
||||
|
||||
Interceptor.onAfter("/api/pixel", async (_, response, init) => {
|
||||
try {
|
||||
if (response.status === 200 && init && "body" in init) {
|
||||
const body = JSON.parse(init.body as any);
|
||||
if ("x" in body && "y" in body && "color" in body) {
|
||||
const pixelsPlaced = new CustomEvent("__pixelsPlaced", {
|
||||
const pixelsPlaced = new CustomEvent(EVENT_CROSS_WORLD_PIXEL_PLACED, {
|
||||
detail: { color: body.color, x: body.x, y: body.y },
|
||||
});
|
||||
document.dispatchEvent(pixelsPlaced);
|
||||
@@ -103,3 +19,12 @@ Interceptor.onAfter("/api/pixel", async (_, response, init) => {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
HistoryPushStateSpy.onPushState((_, __, url) => {
|
||||
const pixelsPlaced = new CustomEvent(EVENT_CROSS_WORLD_URL_UPDATE, {
|
||||
detail: url,
|
||||
});
|
||||
document.dispatchEvent(pixelsPlaced);
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent(EVENT_CROSS_WORLD_INJECTED));
|
||||
@@ -1,9 +1,10 @@
|
||||
import saveAs from "file-saver";
|
||||
import { createCanvas, CHUNK_SIZE, fetchTile } from "./utils";
|
||||
import { createCanvas, fetchTile } from "./utils";
|
||||
import { Coordinates } from "./coordinates";
|
||||
import { Rect } from "../interfaces";
|
||||
import { Popup } from "../UI/components/Popup";
|
||||
import React from "react";
|
||||
import { CHUNK_SIZE } from "../constants";
|
||||
|
||||
export function getSelectionArea() {
|
||||
return new Promise<Rect>(resolve => {
|
||||
|
||||
+113
-23
@@ -1,14 +1,20 @@
|
||||
import { CHUNK_SIZE } from "../constants";
|
||||
import { CoordinatePredictor } from "./coordinatesPredictor";
|
||||
import { Listener, BasicEventEmitter } from "./eventEmitter";
|
||||
import { CHUNK_SIZE, waitForDraw } from "./utils";
|
||||
import { InjectEvents, Injector } from "./injector";
|
||||
import { Store } from "./store";
|
||||
import { waitForDraw } from "./utils";
|
||||
|
||||
export enum CordType {
|
||||
Div,
|
||||
Url
|
||||
Url,
|
||||
UrlPredict
|
||||
}
|
||||
|
||||
export class Coordinates {
|
||||
private cords: HTMLDivElement;
|
||||
private emitter = new BasicEventEmitter();
|
||||
private observer: MutationObserver;
|
||||
private _x = 0;
|
||||
private _y = 0;
|
||||
private frame: number;
|
||||
@@ -16,17 +22,91 @@ export class Coordinates {
|
||||
private _ux = 0;
|
||||
private _uy = 0;
|
||||
private _uScale = 0;
|
||||
private down?: number[];
|
||||
private coordinatePredictor: CoordinatePredictor;
|
||||
private _highPrecision = false;
|
||||
//div: HTMLDivElement;
|
||||
constructor() {
|
||||
// this.div = document.createElement("div");
|
||||
// this.div.style.position = "fixed";
|
||||
// this.div.style.backgroundColor = "orange";
|
||||
// document.body.appendChild(this.div);
|
||||
// this.div.style.opacity = "0.75";
|
||||
// this.div.style.zIndex = "10px";
|
||||
// this.div.style.pointerEvents = "none";
|
||||
constructor(injector: Injector, private storage: Store) {
|
||||
injector.on(InjectEvents.UrlChange, url => {
|
||||
this.onUrlCordsUpdate(url);
|
||||
});
|
||||
this.onUrlCordsUpdate();
|
||||
this.coordinatePredictor = new CoordinatePredictor(this, (x, y, scale) => {
|
||||
this._ux = Math.round(x);
|
||||
this._uy = Math.round(y);
|
||||
this._uScale = Math.round(scale);
|
||||
this.emitter.emit(CordType.UrlPredict, x, y, scale);
|
||||
});
|
||||
}
|
||||
|
||||
toggleHigherPrecision(value: boolean) {
|
||||
if (this._highPrecision === value) {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
this.coordinatePredictor.enable();
|
||||
window.removeEventListener("touchstart", this.touchStart);
|
||||
window.removeEventListener("touchmove", this.touchMove);
|
||||
window.removeEventListener("touchend", this.endMovement);
|
||||
window.removeEventListener("mousedown", this.mouseDown);
|
||||
window.removeEventListener("mousemove", this.mouseMove);
|
||||
window.removeEventListener("mouseup", this.endMovement);
|
||||
} else {
|
||||
this.coordinatePredictor.disable();
|
||||
window.addEventListener("touchstart", this.touchStart);
|
||||
window.addEventListener("touchmove", this.touchMove);
|
||||
window.addEventListener("touchend", this.endMovement);
|
||||
window.addEventListener("mousedown", this.mouseDown);
|
||||
window.addEventListener("mousemove", this.mouseMove);
|
||||
window.addEventListener("mouseup", this.endMovement);
|
||||
}
|
||||
this.storage.toggleHighPrecision(value);
|
||||
this._highPrecision = value;
|
||||
}
|
||||
get highPrecision() {
|
||||
return this._highPrecision;
|
||||
}
|
||||
|
||||
private touchStart = (event: TouchEvent) => {
|
||||
if (event.target instanceof HTMLCanvasElement && event.touches.length === 1) {
|
||||
this.down = [event.touches[0].clientX, event.touches[0].clientY, this.ux, this.uy];
|
||||
}
|
||||
};
|
||||
private touchMove = (event: TouchEvent) => {
|
||||
if (this.down && event.touches.length === 1) {
|
||||
const mx = Math.round((this.down[0] - event.touches[0].clientX) / Math.pow(2, this._uScale));
|
||||
const my = Math.round((this.down[1] - event.touches[0].clientY) / Math.pow(2, this._uScale));
|
||||
const ux = this._ux;
|
||||
const uy = this._uy;
|
||||
this._ux = this.down[2] + mx,
|
||||
this._uy = this.down[3] + my;
|
||||
if (ux !== this._ux || uy !== this._uy) {
|
||||
this.emitter.emit(CordType.Url, this._ux, this._uy, this._uScale);
|
||||
}
|
||||
}
|
||||
};
|
||||
private mouseDown = (event: MouseEvent) => {
|
||||
if (event.target instanceof HTMLCanvasElement) {
|
||||
this.down = [event.x, event.y, this.ux, this.uy];
|
||||
}
|
||||
};
|
||||
private mouseMove = (event: MouseEvent) => {
|
||||
if (this.down) {
|
||||
const mx = Math.round((this.down[0] - event.x) / Math.pow(2, this._uScale));
|
||||
const my = Math.round((this.down[1] - event.y) / Math.pow(2, this._uScale));
|
||||
const ux = this._ux;
|
||||
const uy = this._uy;
|
||||
this._ux = this.down[2] + mx,
|
||||
this._uy = this.down[3] + my;
|
||||
if (ux !== this._ux || uy !== this._uy) {
|
||||
this.emitter.emit(CordType.Url, this._ux, this._uy, this._uScale);
|
||||
}
|
||||
}
|
||||
};
|
||||
private endMovement = () => {
|
||||
this.down = undefined;
|
||||
};
|
||||
|
||||
on(event: CordType, listener: Listener<[number, number, number]>) {
|
||||
this.emitter.on(event, listener);
|
||||
}
|
||||
@@ -40,16 +120,25 @@ export class Coordinates {
|
||||
for (const cord of cords) {
|
||||
if (cord.children.length === 0) {
|
||||
this.cords = cord;
|
||||
this.frame = requestAnimationFrame(this.observe);
|
||||
this.observer = new MutationObserver(() => {
|
||||
this.updateDivCords();
|
||||
});
|
||||
this.observer.observe(cord, {
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
await waitForDraw();
|
||||
}
|
||||
const value = await this.storage.enabledHighPrecision();
|
||||
this.toggleHigherPrecision(value);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.frame) {
|
||||
cancelAnimationFrame(this.frame);
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +204,6 @@ export class Coordinates {
|
||||
screenToGrid(sx: number, sy: number) {
|
||||
const c = this.centerCanvas;
|
||||
if (!c) return null;
|
||||
this.getCordsFromUrl();
|
||||
const chunkX = Math.floor(this.ux / CHUNK_SIZE) * CHUNK_SIZE;
|
||||
const chunkY = Math.floor(this.uy / CHUNK_SIZE) * CHUNK_SIZE;
|
||||
const xx = ((sx - c.bounds.left) / this.pixelSize) + chunkX;
|
||||
@@ -126,7 +214,6 @@ export class Coordinates {
|
||||
gridToScreen(gridX: number, gridY: number) {
|
||||
const c = this.centerCanvas;
|
||||
if (!c) return null;
|
||||
|
||||
const chunkX = Math.floor(this.ux / CHUNK_SIZE) * CHUNK_SIZE;
|
||||
const chunkY = Math.floor(this.uy / CHUNK_SIZE) * CHUNK_SIZE;
|
||||
const screenX = (gridX - chunkX);
|
||||
@@ -137,9 +224,9 @@ export class Coordinates {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
getCordsFromUrl() {
|
||||
updateURLcords(url?: string) {
|
||||
const pathNames = location.pathname.split("/").filter(e => e);
|
||||
const cordsRaw = pathNames[0];
|
||||
const cordsRaw = url || pathNames[0];
|
||||
if (cordsRaw) {
|
||||
const cordsMatch = cordsRaw.match(/-?\d+/g);
|
||||
if (cordsMatch) {
|
||||
@@ -150,7 +237,6 @@ export class Coordinates {
|
||||
this._uScale = cords[2];
|
||||
return cords;
|
||||
}
|
||||
console.log(cordsMatch);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -162,7 +248,7 @@ export class Coordinates {
|
||||
this._y = arr[1];
|
||||
}
|
||||
|
||||
observe = () => {
|
||||
updateDivCords() {
|
||||
const x = this._x;
|
||||
const y = this._y;
|
||||
this.parse();
|
||||
@@ -170,17 +256,18 @@ export class Coordinates {
|
||||
if (x !== this._x || y !== this._y) {
|
||||
this.emitter.emit(CordType.Div, this._x, this._y);
|
||||
}
|
||||
|
||||
}
|
||||
onUrlCordsUpdate(url?: string) {
|
||||
const ux = this._ux;
|
||||
const uy = this._uy;
|
||||
const uScale = this._uScale;
|
||||
this.getCordsFromUrl();
|
||||
this.updateURLcords(url);
|
||||
this.down = undefined;
|
||||
if (ux !== this._ux || uy !== this._uy || uScale !== this._uScale) {
|
||||
this.emitter.emit(CordType.Url, this._ux, this._uy, this._uScale);
|
||||
}
|
||||
}
|
||||
|
||||
this.frame = requestAnimationFrame(this.observe);
|
||||
};
|
||||
get x() {
|
||||
return this._x;
|
||||
}
|
||||
@@ -197,4 +284,7 @@ export class Coordinates {
|
||||
get uScale() {
|
||||
return this._uScale;
|
||||
}
|
||||
get dragging() {
|
||||
return !!this.down;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { CHUNK_SIZE } from "../constants";
|
||||
import { Coordinates, CordType } from "./coordinates";
|
||||
|
||||
export class CoordinatePredictor {
|
||||
private frame?: number;
|
||||
tileRef?: HTMLCanvasElement;
|
||||
private lastX?: number;
|
||||
private lastY?: number;
|
||||
private lastScale?: number;
|
||||
private cords: number[];
|
||||
|
||||
constructor(
|
||||
private coordinates: Coordinates,
|
||||
private predict: (x: number, y: number, scale: number) => void
|
||||
) {
|
||||
coordinates.on(CordType.Url, (x, y, scale) => {
|
||||
if (this.cords) {
|
||||
this.cords[0] = x;
|
||||
this.cords[1] = y;
|
||||
this.cords[2] = scale;
|
||||
} else {
|
||||
this.cords = [x, y, scale];
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private cordUpdate = (x: number, y: number, s: number) => {
|
||||
this.cords = [x, y, s];
|
||||
};
|
||||
syncCords() {
|
||||
const cords = this.coordinates;
|
||||
this.cordUpdate(cords.ux, cords.uy, cords.uScale);
|
||||
}
|
||||
enable() {
|
||||
if (this.frame)
|
||||
return;
|
||||
|
||||
this.syncCords();
|
||||
this.coordinates.on(CordType.Url, this.cordUpdate);
|
||||
this.frame = requestAnimationFrame(this.tick);
|
||||
}
|
||||
disable() {
|
||||
if (this.frame) {
|
||||
this.coordinates.off(CordType.Url, this.cordUpdate);
|
||||
cancelAnimationFrame(this.frame);
|
||||
this.frame = undefined;
|
||||
}
|
||||
}
|
||||
get enabled() {
|
||||
return !!this.frame;
|
||||
}
|
||||
tick = () => {
|
||||
if (!this.tileRef || !document.body.contains(this.tileRef)) {
|
||||
this.tileRef = [...document.getElementsByTagName("canvas")]
|
||||
.find(e => e.classList.contains("leaflet-tile"));
|
||||
|
||||
if (this.tileRef) {
|
||||
const rect = this.tileRef?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
this.syncCords();
|
||||
this.lastScale = this.getScale(rect.width);
|
||||
this.lastX = rect?.left;
|
||||
this.lastY = rect?.top;
|
||||
}
|
||||
this.nextFrame();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const rect = this.tileRef?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
if (this.lastX != null && this.lastY != null && this.lastScale != null) {
|
||||
const scale = this.getScale(rect.width);
|
||||
const x = rect?.left;
|
||||
const y = rect?.top;
|
||||
if (x !== this.lastX || y !== this.lastY || scale != this.lastScale) {
|
||||
const mx = this.lastX - rect?.left;
|
||||
const my = this.lastY - rect?.top;
|
||||
//const ms = this.lastScale - this.lastScale;
|
||||
// console.log(this.lastX - rect?.left, this.lastY - rect?.top, scale);
|
||||
// console.log(x / this.lastScale);
|
||||
// console.log(y / this.lastScale);
|
||||
this.lastX = x;
|
||||
this.lastY = y;
|
||||
this.lastScale = scale;
|
||||
this.cords[0] += (mx / (rect.width / CHUNK_SIZE));
|
||||
this.cords[1] += (my / (rect.width / CHUNK_SIZE));
|
||||
this.predict(this.cords[0], this.cords[1], this.cords[2]);
|
||||
this.cords[2] = scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.nextFrame();
|
||||
};
|
||||
|
||||
private nextFrame() {
|
||||
if (this.frame) {
|
||||
this.frame = requestAnimationFrame(this.tick);
|
||||
}
|
||||
}
|
||||
|
||||
private getScale(width: number) {
|
||||
const scale = width / CHUNK_SIZE;
|
||||
return Math.log(scale) / Math.log(2);
|
||||
}
|
||||
|
||||
// private get centerCanvas() {
|
||||
// const hww = window.innerWidth / 2;
|
||||
// const hhw = window.innerHeight / 2;
|
||||
// const canvasObject = [
|
||||
// ...document.getElementsByTagName("canvas")]
|
||||
// .filter(c => c.classList.contains("leaflet-tile"))
|
||||
// .map(c => {
|
||||
// const bounds = c.getBoundingClientRect();
|
||||
// return {
|
||||
// canvas: c,
|
||||
// bounds: {
|
||||
// width: bounds.width,
|
||||
// height: bounds.height,
|
||||
// top: bounds.top,
|
||||
// bottom: bounds.bottom,
|
||||
// left: bounds.left,
|
||||
// right: bounds.right,
|
||||
// },
|
||||
// };
|
||||
// }).find(r => hww > r.bounds.left && hww < r.bounds.right && hhw > r.bounds.top && hhw < r.bounds.bottom);
|
||||
|
||||
// if (canvasObject) {
|
||||
// const ratio = canvasObject.canvas.width / CHUNK_SIZE;
|
||||
// if (ratio === 2) { // scale -1
|
||||
// const hw = canvasObject.bounds.left + canvasObject.bounds.width / 2;
|
||||
// const hh = canvasObject.bounds.top + canvasObject.bounds.height / 2;
|
||||
// const chunkW = CHUNK_SIZE / canvasObject.canvas.width;
|
||||
// const chunkH = CHUNK_SIZE / canvasObject.canvas.height;
|
||||
// canvasObject.bounds.width = chunkW;
|
||||
// canvasObject.bounds.height = chunkH;
|
||||
|
||||
// if (hww <= hw && hhw <= hh) {
|
||||
// // top left
|
||||
// } else if (hww > hw && hhw < hh) {
|
||||
// // top right
|
||||
// canvasObject.bounds.top += chunkW;
|
||||
// } else if (hww > hw && hhw > hh) {
|
||||
// // bottom left
|
||||
// canvasObject.bounds.left += chunkH;
|
||||
// } else {
|
||||
// // bottom left right
|
||||
// canvasObject.bounds.left += chunkW;
|
||||
// canvasObject.bounds.top += chunkH;
|
||||
// }
|
||||
// canvasObject.bounds.bottom = canvasObject.bounds.top + chunkW;
|
||||
// canvasObject.bounds.right = canvasObject.bounds.left + chunkH;
|
||||
// return null;
|
||||
// }
|
||||
// canvasObject.bounds.width /= ratio;
|
||||
// canvasObject.bounds.height /= ratio;
|
||||
// canvasObject.bounds.right = canvasObject.bounds.left + canvasObject.bounds.width;
|
||||
// canvasObject.bounds.bottom = canvasObject.bounds.bottom + canvasObject.bounds.height;
|
||||
// }
|
||||
|
||||
// return canvasObject;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
EVENT_CROSS_WORLD_INJECTED,
|
||||
EVENT_CROSS_WORLD_PIXEL_PLACED,
|
||||
EVENT_CROSS_WORLD_URL_UPDATE,
|
||||
INJECT_SCRIPT_PIXEL_OBSERVER
|
||||
} from "../constants";
|
||||
import { BasicEventEmitter } from "./eventEmitter";
|
||||
import { Store } from "./store";
|
||||
|
||||
interface Pixel {
|
||||
x: number;
|
||||
y: number;
|
||||
color: number;
|
||||
}
|
||||
export enum InjectEvents {
|
||||
PixelPlaced = 1,
|
||||
UrlChange
|
||||
}
|
||||
|
||||
|
||||
export class Injector {
|
||||
private eventEmitter = new BasicEventEmitter();
|
||||
|
||||
constructor(private store: Store) {}
|
||||
|
||||
inject() {
|
||||
const api = document.createElement("script");
|
||||
api.addEventListener("load", () => {
|
||||
document.body.removeChild(api);
|
||||
}, { once: true });
|
||||
api.addEventListener("error", error => {
|
||||
console.error(error);
|
||||
});
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const d = document as any;
|
||||
api.addEventListener("error", (error) => {
|
||||
reject(error.error);
|
||||
}, { once: true });
|
||||
d.addEventListener(EVENT_CROSS_WORLD_INJECTED, () => {
|
||||
resolve();
|
||||
}, { once: true});
|
||||
|
||||
if (ENVIRONMENT === "browser-extension") {
|
||||
api.src = chrome.runtime.getURL("/assets/scripts/inject.js");
|
||||
} else {
|
||||
api.textContent = INJECT_SCRIPT_PIXEL_OBSERVER;
|
||||
}
|
||||
|
||||
|
||||
document.body.appendChild(api);
|
||||
d.addEventListener(EVENT_CROSS_WORLD_PIXEL_PLACED, (event: CustomEvent<Pixel>) => {
|
||||
const body = event.detail;
|
||||
if (typeof body === "object" && "x" in body && "y" in body && "color" in body
|
||||
&& typeof body.color === "number" && typeof body.x === "number"
|
||||
&& typeof body.y === "number"
|
||||
) {
|
||||
this.store.addPixelLog(body.x, body.y, body.color);
|
||||
this.eventEmitter.emit(InjectEvents.PixelPlaced, body);
|
||||
}
|
||||
});
|
||||
d.addEventListener(EVENT_CROSS_WORLD_URL_UPDATE, (event: CustomEvent<string>) => {
|
||||
const body = event.detail;
|
||||
if (typeof body === "string") {
|
||||
this.eventEmitter.emit(InjectEvents.UrlChange, body);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
on(event: InjectEvents.PixelPlaced, cb: (pixel: Pixel) => any): this;
|
||||
on(event: InjectEvents.UrlChange, cb: (url: string) => any): this;
|
||||
on(event: InjectEvents, cb: any) {
|
||||
this.eventEmitter.on(event, cb);
|
||||
return this;
|
||||
}
|
||||
off(event: InjectEvents.PixelPlaced, cb: (pixel: Pixel) => any): this;
|
||||
off(event: InjectEvents.UrlChange, cb: (url: string) => any): this;
|
||||
off(event: InjectEvents, cb: any) {
|
||||
this.eventEmitter.off(event, cb);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { INJECT_SCRIPT_PIXEL_OBSERVER } from "../constants";
|
||||
import { BasicEventEmitter } from "./eventEmitter";
|
||||
import { Store } from "./store";
|
||||
|
||||
interface Pixel {
|
||||
x: number;
|
||||
y: number;
|
||||
color: number;
|
||||
}
|
||||
|
||||
export class PixelPlaced {
|
||||
private static instance: PixelPlaced;
|
||||
private constructor(public readonly store: Store) {
|
||||
// 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<Pixel>) => {
|
||||
const body = event.detail;
|
||||
console.log(body);
|
||||
if (typeof body === "object" && "x" in body && "y" in body && "color" in body
|
||||
&& typeof body.color === "number" && typeof body.x === "number"
|
||||
&& typeof body.y === "number"
|
||||
) {
|
||||
const data = event.detail;
|
||||
this.store.addPixelLog(data.x, data.y, data.color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static create(store: Store) {
|
||||
if (!this.instance) {
|
||||
this.instance = new PixelPlaced(store);
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
getCount() {
|
||||
return this.store.getPixelCount();
|
||||
}
|
||||
|
||||
}
|
||||
+9
-2
@@ -22,7 +22,7 @@ export enum StoreEvents {
|
||||
|
||||
export interface OverlayReturn {
|
||||
mural: MuralEx;
|
||||
muralObj?: Partial<Mural>;
|
||||
muralModify?: { name: string, x: number, y: number };
|
||||
cb: (name: string, x: number, y: number, confirm?: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ export interface PixelLogEx extends PixelLog {
|
||||
}
|
||||
|
||||
export class Store implements LoadUnload {
|
||||
private readonly STORAGE_KEY_HIGH_PRECISION = "_high-precision";
|
||||
private readonly STORAGE_KEY_MURAL = "_murals";
|
||||
private readonly STORAGE_KEY_SELECTED = "_mural";
|
||||
private readonly STORAGE_KEY_LOG_STORE = "records";
|
||||
private readonly STORAGE_KEY_PIXEL_LOG = "_pixel-log";
|
||||
private _murals: MuralEx[] = [];
|
||||
private _selected?: MuralEx;
|
||||
private emitter = new BasicEventEmitter();
|
||||
@@ -290,6 +290,13 @@ export class Store implements LoadUnload {
|
||||
this.emit(StoreEvents.MuralPhantomOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
async enabledHighPrecision() {
|
||||
return (await this.storage.getItem<boolean>(this.STORAGE_KEY_HIGH_PRECISION)) ?? false;
|
||||
}
|
||||
toggleHighPrecision(value: boolean) {
|
||||
return this.storage.setItem(this.STORAGE_KEY_HIGH_PRECISION, value);
|
||||
}
|
||||
get murals() {
|
||||
return this._murals;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export type SpyHistoryPushStateSpy = (data: any, unused: string, url?: string | URL | null) => Promise<void> | void;
|
||||
|
||||
export class HistoryPushStateSpy {
|
||||
private static pushState = window.history.pushState;
|
||||
private static overwritten = false;
|
||||
private static listeners: SpyHistoryPushStateSpy[] = [];
|
||||
|
||||
private static overridePushState() {
|
||||
if (this.overwritten) {
|
||||
return;
|
||||
}
|
||||
this.overwritten = true;
|
||||
const listeners = this.listeners;
|
||||
const pushState = this.pushState;
|
||||
(window as any).history.pushState = function (...args: any) {
|
||||
for (const listener of listeners) {
|
||||
(listener as any).apply(this, args);
|
||||
}
|
||||
return (pushState as any).apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
static onPushState(cb: SpyHistoryPushStateSpy) {
|
||||
this.overridePushState();
|
||||
if (!this.listeners.includes(cb)) {
|
||||
this.listeners.push(cb);
|
||||
}
|
||||
}
|
||||
static offPushState(cb: SpyHistoryPushStateSpy) {
|
||||
const index = this.listeners.indexOf(cb);
|
||||
if (index !== -1) {
|
||||
this.listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-5
@@ -2,8 +2,7 @@ import { clone, isInteger } from "lodash";
|
||||
import { MuralEx, MuralOld, MuralStatus, RGB } from "../interfaces";
|
||||
import { fetchCombineTiledImage } from "./canvasShot";
|
||||
import { Mural } from "./mural";
|
||||
|
||||
export const CHUNK_SIZE = 512;
|
||||
import { CHUNK_SIZE } from "../constants";
|
||||
|
||||
export function pushUnique<T>(items: T[], item: T) {
|
||||
const index = items.indexOf(item);
|
||||
@@ -224,13 +223,13 @@ export function canvasToImageData(canvas: HTMLCanvasElement) {
|
||||
}
|
||||
|
||||
export function flatQuantizeImageData(imageData: ImageData, palette: RGB[]) {
|
||||
const indexColor = createClosestIndexColor(palette);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
const r = imageData.data[i + 0] ?? 0;
|
||||
const g = imageData.data[i + 1] ?? 0;
|
||||
const b = imageData.data[i + 2] ?? 0;
|
||||
const a = imageData.data[i + 3] ?? 0;
|
||||
const obj = rgb(r, g, b);
|
||||
const index = findClosestIndexColor(obj, palette);
|
||||
const index = indexColor(r, g, b);
|
||||
const color = palette[index];
|
||||
if (!color) throw new Error(`Unknown color index ${index}`);
|
||||
imageData.data[i + 0] = color.r;
|
||||
@@ -273,6 +272,7 @@ export async function getPixelStatusMural(mural: Mural, palette: RGB[]): Promise
|
||||
export function imageDataToPaletteIndices(imageData: ImageData, palette: RGB[]) {
|
||||
const { height, width } = imageData;
|
||||
let k = 0;
|
||||
const indexColor = createClosestIndexColor(palette);
|
||||
const pixels = new Int8Array(height * width);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
const r = imageData.data[i + 0] ?? 0;
|
||||
@@ -282,7 +282,7 @@ export function imageDataToPaletteIndices(imageData: ImageData, palette: RGB[])
|
||||
if (a < 25) {
|
||||
pixels[k++] = -1;
|
||||
} else {
|
||||
const index = findClosestFormArray(rgb(r, g, b), palette);
|
||||
const index = indexColor(r, g, b);
|
||||
pixels[k++] = index;
|
||||
}
|
||||
}
|
||||
@@ -381,6 +381,24 @@ export function createMuralExtended(mural: Mural, palette: string[]): MuralEx {
|
||||
};
|
||||
}
|
||||
|
||||
export function createClosestIndexColor(palette: RGB[]) {
|
||||
const scores = new Uint16Array(palette.length);
|
||||
let lowest = Number.MAX_SAFE_INTEGER;
|
||||
return (r: number, g: number, b: number) => {
|
||||
lowest = Number.MAX_SAFE_INTEGER;
|
||||
for (let i = 0; i < palette.length; i++) {
|
||||
const rgb = palette[i];
|
||||
const rr = getColorScore(r, rgb.r);
|
||||
const gg = getColorScore(g, rgb.g);
|
||||
const bb = getColorScore(b, rgb.b);
|
||||
const value = rr + gg + bb;
|
||||
lowest = lowest < value ? lowest : value;
|
||||
scores[i] = value;
|
||||
}
|
||||
return scores.indexOf(lowest);
|
||||
};
|
||||
}
|
||||
|
||||
let numberFormatter: Intl.NumberFormat;
|
||||
try {
|
||||
const userLocale = navigator.language || (navigator as any).userLanguage;
|
||||
|
||||
Reference in New Issue
Block a user