Change storage to use indexDB within binary format

This commit is contained in:
2025-02-12 00:55:15 +01:00
parent 49f1cf008b
commit b9f83b6bd4
12 changed files with 244 additions and 207 deletions
+46
View File
@@ -21,6 +21,7 @@
"copy-dir": "^1.3.0", "copy-dir": "^1.3.0",
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"process": "^0.11.10", "process": "^0.11.10",
"react": "^18.2.0", "react": "^18.2.0",
@@ -2259,6 +2260,12 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -2734,6 +2741,24 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"license": "Apache-2.0",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -5457,6 +5482,11 @@
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true "dev": true
}, },
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"import-fresh": { "import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -5797,6 +5827,22 @@
"type-check": "~0.4.0" "type-check": "~0.4.0"
} }
}, },
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"requires": {
"immediate": "~3.0.5"
}
},
"localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"requires": {
"lie": "3.1.1"
}
},
"locate-path": { "locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+1
View File
@@ -27,6 +27,7 @@
"copy-dir": "^1.3.0", "copy-dir": "^1.3.0",
"esbuild": "^0.20.0", "esbuild": "^0.20.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"process": "^0.11.10", "process": "^0.11.10",
"react": "^18.2.0", "react": "^18.2.0",
+5 -5
View File
@@ -5,12 +5,12 @@ import { Coordinates } from "../../lib/coordinates";
import { Palette } from "../../lib/palette"; import { Palette } from "../../lib/palette";
import { Menu } from "./menu"; import { Menu } from "./menu";
import { MovableWindow } from "./movableWindow"; import { MovableWindow } from "./movableWindow";
import { SelectedMural } from "../../interfaces";
import { Minimap } from "./minimap"; import { Minimap } from "./minimap";
import { Overlay } from "./overlay"; import { Overlay } from "./overlay";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { TC_LOGO } from "../../constants"; import { TC_LOGO } from "../../constants";
import styled from "styled-components"; import styled from "styled-components";
import { MuralEx } from "../../interfaces";
const Img = styled.img` const Img = styled.img`
width: 25px; width: 25px;
@@ -33,7 +33,7 @@ interface Props {
} }
interface State extends StoreSettings{ interface State extends StoreSettings{
selected?: SelectedMural; selected?: MuralEx;
overlays: number[]; overlays: number[];
phantomOverlay: number; phantomOverlay: number;
overlayModify?: OverlayReturn; overlayModify?: OverlayReturn;
@@ -127,14 +127,14 @@ export class Main extends React.Component<Props, State> {
opacity={this.state.opacity} opacity={this.state.opacity}
cords={this.props.cords} cords={this.props.cords}
palette={this.props.palette} palette={this.props.palette}
mural={this.props.store.murals[o]} muralExtended={this.props.store.murals[o]}
/> )} /> )}
{this.props.store.murals[this.state.phantomOverlay] ? <Overlay {this.props.store.murals[this.state.phantomOverlay] ? <Overlay
storage={this.props.storage} storage={this.props.storage}
opacity={50} opacity={50}
cords={this.props.cords} cords={this.props.cords}
palette={this.props.palette} palette={this.props.palette}
mural={this.props.store.murals[this.state.phantomOverlay]} muralExtended={this.props.store.murals[this.state.phantomOverlay]}
/> : null } /> : null }
{this.state.overlayModify ? <Overlay {this.state.overlayModify ? <Overlay
muralObj={this.state.overlayModify.muralObj} muralObj={this.state.overlayModify.muralObj}
@@ -142,7 +142,7 @@ export class Main extends React.Component<Props, State> {
storage={this.props.storage} storage={this.props.storage}
cords={this.props.cords} cords={this.props.cords}
palette={this.props.palette} palette={this.props.palette}
mural={this.props.store.overlayModify!.pixels} muralExtended={this.props.store.overlayModify!.mural}
onChange={(name, x, y, confirm) => { onChange={(name, x, y, confirm) => {
this.state.overlayModify!.cb(name, x, y, confirm); this.state.overlayModify!.cb(name, x, y, confirm);
}} /> : null} }} /> : null}
+8 -5
View File
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { MuralEx, SelectedMural } from "../../interfaces"; import { MuralEx } from "../../interfaces";
import { Store, StoreEvents } from "../../lib/store"; import { Store, StoreEvents } from "../../lib/store";
import { MuralView } from "./muralView"; import { MuralView } from "./muralView";
import styled from "styled-components"; import styled from "styled-components";
@@ -38,7 +38,7 @@ interface Props {
interface State { interface State {
murals: MuralEx[]; murals: MuralEx[];
selected?: SelectedMural; selected?: MuralEx;
} }
export class MuralList extends React.Component<Props, State> { export class MuralList extends React.Component<Props, State> {
@@ -63,9 +63,12 @@ export class MuralList extends React.Component<Props, State> {
}; };
render() { render() {
return <ScrollContainer> {this.state.murals.map((m,i) => { return <ScrollContainer> {this.state.murals.map((m,i) => {
return <MuralView key={i} mural={m} cords={this.props.cords} return <MuralView
palette={this.props.palette} store={this.props.store} key={i}
selected={m === this.state.selected?.m} />; muralExtended={m} cords={this.props.cords}
palette={this.props.palette}
store={this.props.store}
selected={m && m === this.state.selected} />;
})}</ScrollContainer>; })}</ScrollContainer>;
} }
} }
+43 -42
View File
@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { Store } from "../../lib/store"; import { Store } from "../../lib/store";
import { MuralEx, MuralOld, MuralStatus } from "../../interfaces"; import { MuralEx, MuralStatus } from "../../interfaces";
import styled from "styled-components"; import styled from "styled-components";
import { A, Border, Btn, Flex, SELECTED_COLOR } from "../styles"; import { A, Border, Btn, Flex, SELECTED_COLOR } from "../styles";
import { CanvasToCanvasJSX } from "./canvasToCanvasJSX"; import { CanvasToCanvasJSX } from "./canvasToCanvasJSX";
import { convertOldMuralToNewMural, formatNumber, getMuralHeight, getMuralWidth, getPixelStatusMural } from "../../lib/utils"; import { formatNumber, getPixelStatusMural } from "../../lib/utils";
import { import {
IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@@ -66,7 +66,7 @@ export const BtnSmall = styled.button`
`; `;
interface Props { interface Props {
mural: MuralEx; muralExtended: MuralEx;
selected: boolean; selected: boolean;
store: Store; store: Store;
palette: Palette; palette: Palette;
@@ -102,7 +102,7 @@ export class MuralView extends React.Component<Props, State> {
this.props.cords.off(CordType.Url, this.onCordUpdate); this.props.cords.off(CordType.Url, this.onCordUpdate);
} }
componentDidUpdate(prevProps: Readonly<Props> ) { componentDidUpdate(prevProps: Readonly<Props> ) {
if (this.props.mural.ref !== prevProps.mural.ref) { if (this.props.muralExtended.ref !== prevProps.muralExtended.ref) {
this.updateSize(); this.updateSize();
if (typeof this.state.progress === "function") { if (typeof this.state.progress === "function") {
typeof this.state.progress(); typeof this.state.progress();
@@ -116,8 +116,8 @@ export class MuralView extends React.Component<Props, State> {
}; };
updateSize = () => { updateSize = () => {
const w = this.props.mural.ref.width; const w = this.props.muralExtended.ref.width;
const h = this.props.mural.ref.height; const h = this.props.muralExtended.ref.height;
let width = w; let width = w;
let height = h; let height = h;
@@ -153,26 +153,26 @@ export class MuralView extends React.Component<Props, State> {
if ("tagName" in tr) { if ("tagName" in tr) {
const ignore = ["button", "svg", "path"]; const ignore = ["button", "svg", "path"];
if (!ignore.includes(tr.tagName.toLowerCase())) { if (!ignore.includes(tr.tagName.toLowerCase())) {
this.s.select(this.props.selected ? undefined : this.props.mural ); this.s.select(this.props.selected ? undefined : this.props.muralExtended );
} }
} }
}; };
onModify = async () => { onModify = async () => {
const mural = this.props.mural; const muralExtended = this.props.muralExtended;
this.props.store.setOverlayModify({ this.props.store.setOverlayModify({
pixels: this.props.mural.pixels, mural: muralExtended,
cb: (name, x, y, confirm) => { cb: (name, x, y, confirm) => {
if (confirm) { if (confirm) {
mural.name = name; muralExtended.mural.name = name;
mural.x = x; muralExtended.mural.x = x;
mural.y = y; muralExtended.mural.y = y;
this.props.store.updateMural(mural); this.props.store.updateMural(muralExtended.mural);
} }
}, },
muralObj: { muralObj: {
name: this.props.mural.name, name: this.props.muralExtended.mural.name,
x: this.props.mural.x, x: this.props.muralExtended.mural.x,
y: this.props.mural.y y: this.props.muralExtended.mural.y
} }
}); });
@@ -212,39 +212,35 @@ export class MuralView extends React.Component<Props, State> {
// } // }
}; };
onExport = async () => { onExport = async () => {
const rawMural: MuralOld = { const mural = this.props.muralExtended.mural;
name: this.props.mural.name,
x: this.props.mural.x,
y: this.props.mural.y,
pixels: this.props.mural.pixels,
};
const mural = convertOldMuralToNewMural(rawMural);
const buffer = await mural.getBuffer(); const buffer = await mural.getBuffer();
const blob = new Blob([buffer], { type: "octet/stream" }); const blob = new Blob([buffer], { type: "octet/stream" });
const saveName = rawMural.name const saveName = mural.name
.replace(/ /, "_") .replace(/ /, "_")
.replace(/[^a-zA-Z0-9-_]/g, ""); .replace(/[^a-zA-Z0-9-_]/g, "");
saveAs(blob, `${saveName}.${BIN_FORMATS[0]}`); saveAs(blob, `${saveName}.${BIN_FORMATS[0]}`);
}; };
onDelete = async () => { onDelete = async () => {
if (await Popup.confirm(`Are you sure you want to remove "${this.props.mural.name}"`)) { if (await Popup
this.s.remove(this.props.mural); .confirm(`Are you sure you want to remove "${this.props.muralExtended.mural.name}"`)
) {
this.s.remove(this.props.muralExtended);
} }
}; };
onEnter = () => { onEnter = () => {
this.props.store.addPhantomOverlay(this.props.mural); this.props.store.addPhantomOverlay(this.props.muralExtended);
}; };
onLeave = () => { onLeave = () => {
this.props.store.removePhantomOverlay(); this.props.store.removePhantomOverlay();
}; };
get link() { get link() {
const weight = getMuralWidth(this.props.mural); const weight = this.props.muralExtended.mural.w;
const height = getMuralHeight(this.props.mural); const height = this.props.muralExtended.mural.h;
const x = this.props.mural.x + Math.round(weight / 2); const x = this.props.muralExtended.mural.x + Math.round(weight / 2);
const y = this.props.mural.y + Math.round(height / 2); const y = this.props.muralExtended.mural.y + Math.round(height / 2);
const o = origin === "null" ? `${location.href}` : `${origin}/`; const o = origin === "null" ? `${location.href}` : `${origin}/`;
return `${o}@${x},${y},${this.props.cords.uScale}`; return `${o}@${x},${y},${this.props.cords.uScale}`;
} }
@@ -254,10 +250,10 @@ export class MuralView extends React.Component<Props, State> {
}; };
onPreview = () => { onPreview = () => {
if (this.props.store.hasOverlay(this.props.mural)) { if (this.props.store.hasOverlay(this.props.muralExtended)) {
this.props.store.removeOverlay(this.props.mural); this.props.store.removeOverlay(this.props.muralExtended);
} else { } else {
this.props.store.addOverlay(this.props.mural); this.props.store.addOverlay(this.props.muralExtended);
} }
}; };
@@ -267,7 +263,9 @@ export class MuralView extends React.Component<Props, State> {
canceled = true; canceled = true;
}; };
this.setState({progress: cancelFn}); this.setState({progress: cancelFn});
const data = await getPixelStatusMural(this.props.mural, this.props.palette.palette); const data = await getPixelStatusMural(
this.props.muralExtended.mural, this.props.palette.palette
);
if (!canceled) { if (!canceled) {
this.setState({progress: data}); this.setState({progress: data});
} }
@@ -293,18 +291,21 @@ export class MuralView extends React.Component<Props, State> {
cursor: "pointer" }} cursor: "pointer" }}
onMouseEnter={this.onEnter} onMouseLeave={this.onLeave}> onMouseEnter={this.onEnter} onMouseLeave={this.onLeave}>
<Flex> <Flex>
<CanvasToCanvasJSX canvas={this.props.mural.ref} height={this.state.height} width={this.state.width}/> <CanvasToCanvasJSX canvas={this.props.muralExtended.ref} height={this.state.height} width={this.state.width}/>
<Margin style={{flex: "1"}}> <Margin style={{flex: "1"}}>
{this.renderInfoLine("Name", this.props.mural.name)} {this.renderInfoLine("Name", this.props.muralExtended.mural.name)}
{this.renderInfoLine("Pixel count", formatNumber(this.props.mural.pixelCount))} {this.renderInfoLine("Pixel count", formatNumber(this.props.muralExtended.pixelCount))}
{this.renderInfoLine("X", this.props.mural.x.toString() )} {this.renderInfoLine("X", this.props.muralExtended.mural.x.toString() )}
{this.renderInfoLine("Y", this.props.mural.y.toString() )} {this.renderInfoLine("Y", this.props.muralExtended.mural.y.toString() )}
{this.renderInfoLine("Size", `${getMuralWidth(this.props.mural)}x${getMuralHeight(this.props.mural)}`)} {this.renderInfoLine(
"Size",
`${this.props.muralExtended.mural.w}x${this.props.muralExtended.mural.w}`
)}
{this.renderInfoLine("Progress", this.renderProgress())} {this.renderInfoLine("Progress", this.renderProgress())}
</Margin> </Margin>
</Flex> </Flex>
<Flex> <Flex>
{this.btn("Preview", faLayerGroup, this.onPreview, this.props.store.hasOverlay(this.props.mural))} {this.btn("Preview", faLayerGroup, this.onPreview, this.props.store.hasOverlay(this.props.muralExtended))}
{this.btn("Modify", faPenToSquare, this.onModify)} {this.btn("Modify", faPenToSquare, this.onModify)}
{this.btn("Export", faDownload, this.onExport)} {this.btn("Export", faDownload, this.onExport)}
{this.btn("Delete", faTrash, this.onDelete)} {this.btn("Delete", faTrash, this.onDelete)}
+10 -32
View File
@@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { Mural, MuralEx } from "../../interfaces"; import { MuralEx } from "../../interfaces";
import { canvasFromMural, get2DArrHeight, get2DArrWidth } from "../../lib/utils";
import { Coordinates, CordType } from "../../lib/coordinates"; import { Coordinates, CordType } from "../../lib/coordinates";
import { Palette } from "../../lib/palette"; import { Palette } from "../../lib/palette";
import { MovableWindow } from "./movableWindow"; import { MovableWindow } from "./movableWindow";
import { Storage } from "../../lib/storage"; import { Storage } from "../../lib/storage";
import { MuralEditor } from "./muralEditor"; import { MuralEditor } from "./muralEditor";
import { Mural } from "../../lib/mural";
const Canvas = styled.canvas` const Canvas = styled.canvas`
position: fixed; position: fixed;
@@ -15,7 +15,7 @@ const Canvas = styled.canvas`
`; `;
interface Props { interface Props {
mural: MuralEx | Mural | number[][]; muralExtended: MuralEx;
muralObj?: Partial<Mural>; muralObj?: Partial<Mural>;
onChange?: (name: string, x: number, y:number, confirm?: boolean) => void; onChange?: (name: string, x: number, y:number, confirm?: boolean) => void;
cords: Coordinates; cords: Coordinates;
@@ -61,7 +61,7 @@ export class Overlay extends React.Component<Props, State> {
} }
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) { componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
if (prevProps.mural !== this.props.mural) { if (prevProps.muralExtended !== this.props.muralExtended) {
this._refImage = undefined; this._refImage = undefined;
this.setState({ this.setState({
x: 0, y: 0 x: 0, y: 0
@@ -72,43 +72,21 @@ export class Overlay extends React.Component<Props, State> {
} }
} }
private isPixels(pixels: MuralEx | Mural | number[][]): pixels is number[][] {
return Array.isArray(pixels);
}
get pixels() { get pixels() {
if (this.isPixels(this.props.mural)) { return this.props.muralExtended.mural.pixelBuffer;
return this.props.mural;
} else {
return this.props.mural.pixels;
}
} }
get x() { get x() {
if (this.isPixels(this.props.mural)) { return this.props.muralExtended.mural.x;
return this.state.x;
} else {
return this.props.mural.x;
}
} }
get y() { get y() {
if (this.isPixels(this.props.mural)) { return this.props.muralExtended.mural.y;
return this.state.y;
} else {
return this.props.mural.y;
}
} }
get refImg() { get refImg() {
if (!Array.isArray(this.props.mural) && "ref" in this.props.mural) { return this.props.muralExtended.ref;
return this.props.mural.ref;
}
if (this._refImage) {
return this._refImage;
}
this._refImage = canvasFromMural(this.pixels, this.props.palette.hex).canvas;
return this._refImage;
} }
draw = () => { draw = () => {
@@ -122,8 +100,8 @@ export class Overlay extends React.Component<Props, State> {
} }
const { x, y } = cords; const { x, y } = cords;
const width = get2DArrWidth(this.pixels) * scale; const width = this.props.muralExtended.mural.w * scale;
const height = get2DArrHeight(this.pixels) * scale; const height = this.props.muralExtended.mural.h * scale;
if (width !== canvas.width || height !== canvas.height ) { if (width !== canvas.width || height !== canvas.height ) {
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
+18 -46
View File
@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { MuralOld, RGB } from "../interfaces"; import { ImportOutput, MuralOld, RGB } from "../interfaces";
import { canvasToImageData, get2DArrHeight, get2DArrWidth, getColorScore, getExtension, import { canvasToImageData, convertOldMuralToNewMural, getColorScore, getExtension,
imageDataToPaletteIndices, imageToCanvas, loadImageSource, processNumberEvent, imageDataToPaletteIndices, imageToCanvas, loadImageSource, processNumberEvent,
readAsArrayBuffer, readAsArrayBuffer,
readAsDataUrl, readAsString, resize, rgb, validateMural } from "../lib/utils"; readAsDataUrl, readAsString, resize, rgb } from "../lib/utils";
import styled from "styled-components"; import styled from "styled-components";
import { Popup } from "./components/Popup"; import { Popup } from "./components/Popup";
import RgbQuant, { DitheringKernel, RGBQuantOptions } from "rgbquant"; import RgbQuant, { DitheringKernel, RGBQuantOptions } from "rgbquant";
@@ -56,7 +56,7 @@ type DitheringKernelEx = DitheringKernel | "Flat";
interface QuantResult { interface QuantResult {
type: DitheringKernelEx; type: DitheringKernelEx;
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
indices: number[][]; indices: Int8Array;
} }
type DitherSetting = DitheringKernel | "Flat" | "show-all"; type DitherSetting = DitheringKernel | "Flat" | "show-all";
@@ -85,33 +85,35 @@ export interface ImageToMuralOptions {
quantizerSetting: DitherSetting; quantizerSetting: DitherSetting;
} }
export async function importArtWorks(store: Store, cords: Coordinates, palette: Palette) { export async function importArtWorks(
store: Store, cords: Coordinates, palette: Palette
): Promise<Mural[]> {
const files = await importFiles(); const files = await importFiles();
const output: MuralOld[] = []; const output: Mural[] = [];
for (const file of files) { for (const file of files) {
if (file.type === "mural") { if (file.type === "mural") {
output.push(file.data); output.push(file.data);
} else { } else {
const img = file.data as HTMLImageElement; const img = file.data;
const pixels = await imageToMural(img, palette); const pixels = await imageToMural(img, palette);
const mural = await new Promise<MuralOld>((resolve, reject)=> { const mural = await new Promise<Mural>((resolve, reject)=> {
store.setOverlayModify({ store.setOverlayModify({
pixels, pixels,
muralObj: { muralObj: {
name: img.alt, name: img.alt,
x: Math.floor(cords.ux - (get2DArrWidth(pixels) / 2)), x: Math.floor(cords.ux - (img.width / 2)),
y: Math.floor(cords.uy - (get2DArrHeight(pixels) / 2)), y: Math.floor(cords.uy - (img.height / 2)),
}, },
cb: (name, x, y, confirm) => { cb: (name, x, y, confirm) => {
if (confirm) { if (confirm) {
resolve({ name, pixels, x, y }); const mural = new Mural(name, x,y,img.width, img.height, pixels);
resolve(mural);
} else { } else {
reject(new Error("Canceled by user")); reject(new Error("Canceled by user"));
} }
} }
}); });
}); });
validateMural(mural);
output.push(mural); output.push(mural);
} }
} }
@@ -472,7 +474,7 @@ export function imageDataToCanvas(imageData: ImageData) {
return canvas; return canvas;
} }
async function importFiles() { async function importFiles(): Promise<ImportOutput[]> {
const fileInput = new FileInput(); const fileInput = new FileInput();
const images = ["png", "jpg", "jpeg"]; const images = ["png", "jpg", "jpeg"];
fileInput.setAcceptType([...images, ...TEXT_FORMATS, ...BIN_FORMATS]); fileInput.setAcceptType([...images, ...TEXT_FORMATS, ...BIN_FORMATS]);
@@ -489,13 +491,7 @@ async function importFiles() {
throw new Error("Only one image files allowed to be imported at the time"); throw new Error("Only one image files allowed to be imported at the time");
} }
const output: ({ const output: ImportOutput[] = [];
type: "mural",
data: MuralOld
} | {
type: "image",
data: HTMLImageElement
}) [] = [];
for (const file of filteredFiles) { for (const file of filteredFiles) {
const ex = getExtension(file.name); const ex = getExtension(file.name);
const etn = ex.ex.toLowerCase(); const etn = ex.ex.toLowerCase();
@@ -510,38 +506,14 @@ async function importFiles() {
output.push({ output.push({
type: "mural", type: "mural",
data: mural, data: convertOldMuralToNewMural(mural),
}); });
} else if (BIN_FORMATS.includes(etn)) { } else if (BIN_FORMATS.includes(etn)) {
const buffer = await readAsArrayBuffer(file); const buffer = await readAsArrayBuffer(file);
const mural = await Mural.from(new Uint8Array(buffer)); const mural = await Mural.from(new Uint8Array(buffer));
const pixelBuffer = mural.pixelBuffer;
const pixels = Array.from({ length: mural.h }, () => Array(mural.w).fill(0));
let i = 0;
let yy = -1;
leg:
for (;;) {
yy++;
if (pixelBuffer[i] === undefined) {
break;
}
// const ref = [];
// pixels.push(ref);
for (let xx = 0; xx < mural.w; xx++) {
const item = pixelBuffer[i++];
if (item != undefined) {
pixels[yy][xx] = item;
//ref.push(item);
} else {
break leg;
}
}
}
output.push({ output.push({
type: "mural", type: "mural",
data: { data: mural,
name, x: mural.x, y: mural.y, pixels,
},
}); });
} else { } else {
const readData = await readAsDataUrl(file); const readData = await readAsDataUrl(file);
+16 -7
View File
@@ -1,3 +1,5 @@
import { Mural } from "./lib/mural";
export interface RGB { export interface RGB {
r: number; r: number;
g: number; g: number;
@@ -22,13 +24,8 @@ export interface LoadUnload {
unload: () => Promise<void> | void; unload: () => Promise<void> | void;
} }
export interface SelectedMural { export interface MuralEx {
m: MuralEx; mural: Mural;
w: number;
h: number;
}
export interface MuralEx extends MuralOld {
ref: HTMLCanvasElement; ref: HTMLCanvasElement;
pixelCount: number; pixelCount: number;
} }
@@ -44,3 +41,15 @@ export interface Rect {
width: number; width: number;
height: number; height: number;
} }
export interface ImportOutputMural {
type: "mural",
data: Mural
}
export interface ImportOutputImage {
type: "image",
data: HTMLImageElement;
}
export type ImportOutput = ImportOutputImage | ImportOutputMural;
+4
View File
@@ -14,6 +14,10 @@ export class Mural {
this.validate(); this.validate();
} }
clone() {
return new Mural(this.name, this.x, this.y, this.w, this.h, this._b.slice());
}
getBuffer() { getBuffer() {
this.validate(); this.validate();
const encoder = new TextEncoder(); const encoder = new TextEncoder();
+1 -1
View File
@@ -28,7 +28,7 @@ export class Storage {
} }
return Promise.resolve(); return Promise.resolve();
} }
deleteItem(key: string) { removeItem(key: string) {
if (this.chrome) { if (this.chrome) {
return new Promise<void>(r => { return new Promise<void>(r => {
chrome.storage.local.remove(key, r); chrome.storage.local.remove(key, r);
+53 -30
View File
@@ -1,8 +1,13 @@
import { BasicEventEmitter, Listener } from "./eventEmitter"; import { BasicEventEmitter, Listener } from "./eventEmitter";
import { LoadUnload, Mural, MuralEx, SelectedMural } from "../interfaces"; import { LoadUnload, MuralEx, MuralOld } from "../interfaces";
import { Palette } from "./palette"; import { Palette } from "./palette";
import { Storage } from "./storage"; import { Storage } from "./storage";
import { canvasFromMural, getMuralHeight, getMuralWidth, pushUnique, removeItem } from "./utils"; import localforage from "localforage";
import {
convertOldMuralToNewMural, createMuralExtended,
pushUnique, removeItem
} from "./utils";
import { Mural } from "./mural";
export enum StoreEvents { export enum StoreEvents {
MuralAdd = 1, MuralAdd = 1,
@@ -15,7 +20,7 @@ export enum StoreEvents {
} }
export interface OverlayReturn { export interface OverlayReturn {
pixels: number[][]; mural: MuralEx;
muralObj?: Partial<Mural>; muralObj?: Partial<Mural>;
cb: (name: string, x: number, y: number, confirm?: boolean) => void; cb: (name: string, x: number, y: number, confirm?: boolean) => void;
} }
@@ -24,23 +29,53 @@ export class Store implements LoadUnload {
private readonly STORAGE_KEY_MURAL = "_murals"; private readonly STORAGE_KEY_MURAL = "_murals";
private readonly STORAGE_KEY_SELECTED = "_mural"; private readonly STORAGE_KEY_SELECTED = "_mural";
private _murals: MuralEx[] = []; private _murals: MuralEx[] = [];
private _selected: SelectedMural | undefined; private _selected?: MuralEx;
private emitter = new BasicEventEmitter(); private emitter = new BasicEventEmitter();
private _overlayIndices: number[] = []; private _overlayIndices: number[] = [];
private _phantomOverlay = -1; private _phantomOverlay = -1;
private _overlayModify?: OverlayReturn; private _overlayModify?: OverlayReturn;
private db = localforage.createInstance({
name: "pixel-canvas-overlay"
});
constructor(private storage: Storage, private palette: Palette) {} constructor(private storage: Storage, private palette: Palette) {}
async load() { async load() {
this._murals = await this.storage.getItem<MuralEx[]>(this.STORAGE_KEY_MURAL) || []; console.log("loaded");
for (const mural of this._murals) { this._murals = [];
const { canvas, pixels } = canvasFromMural(mural.pixels, this.palette.hex); const rawMurals = await this.db.getItem<Uint8Array[]>(this.STORAGE_KEY_MURAL) || [];
mural.ref = canvas;
mural.pixelCount = pixels; const murals = await Promise.all(rawMurals.map(e => Mural.from(e)));
// backwards compatibility
const oldMurals = await this.storage.getItem<MuralOld[]>(this.STORAGE_KEY_MURAL);
if (oldMurals) {
for (const old of oldMurals) {
const mural = convertOldMuralToNewMural(old);
murals.push(mural);
}
const muralBuffer = await Promise.all(murals.map(e => e.getBuffer()));
this.storage.removeItem(this.STORAGE_KEY_MURAL);
this.db.setItem(this.STORAGE_KEY_MURAL, muralBuffer);
} }
//const murals = await this.storage.getItem<Uint8Array[]>(this.STORAGE_KEY_MURAL);
for (const mural of murals) {
this._murals.push(createMuralExtended(mural, this.palette.hex));
}
const index = await this.storage.getItem<number>(this.STORAGE_KEY_SELECTED) ?? -1; const index = await this.storage.getItem<number>(this.STORAGE_KEY_SELECTED) ?? -1;
this.select(this._murals[index]); if (index != null) {
this.select(this._murals[index]);
this.storage.removeItem(this.STORAGE_KEY_SELECTED);
await this.db.setItem(this.STORAGE_KEY_SELECTED, index);
} else {
const index = await this.db.getItem<number>(this.STORAGE_KEY_SELECTED);
if (index != null) {
this.select(this._murals[index]);
}
}
} }
updateMural(_mural: Mural) { updateMural(_mural: Mural) {
this.save(); this.save();
@@ -50,10 +85,7 @@ export class Store implements LoadUnload {
this.save(); this.save();
} }
add(mural: Mural) { add(mural: Mural) {
const m = mural as MuralEx; const m = createMuralExtended(mural, this.palette.hex);
const { canvas, pixels } = canvasFromMural(mural.pixels, this.palette.hex);
m.ref = canvas;
m.pixelCount = pixels;
this._murals.push(m); this._murals.push(m);
this.emit(StoreEvents.Any); this.emit(StoreEvents.Any);
this.save(); this.save();
@@ -63,7 +95,7 @@ export class Store implements LoadUnload {
this._overlayIndices = []; this._overlayIndices = [];
this._overlayModify = undefined; this._overlayModify = undefined;
this._phantomOverlay = -1; this._phantomOverlay = -1;
if (this._selected && this._selected?.m === mural) { if (this._selected && this._selected === mural) {
this._selected = undefined; this._selected = undefined;
} }
removeItem(this._murals, mural); removeItem(this._murals, mural);
@@ -89,7 +121,7 @@ export class Store implements LoadUnload {
this.emit(StoreEvents.MuralOverlay); this.emit(StoreEvents.MuralOverlay);
this.save(); this.save();
} }
setOverlayModify(overlay: OverlayReturn) { setOverlayModify(overlay: OverlayReturn) { // WTF?
this._overlayModify = overlay; this._overlayModify = overlay;
const cb = overlay.cb; const cb = overlay.cb;
overlay.cb = (name, x, y, done) => { overlay.cb = (name, x, y, done) => {
@@ -105,25 +137,16 @@ export class Store implements LoadUnload {
} }
select(mural: MuralEx | undefined) { select(mural: MuralEx | undefined) {
if (mural) { this._selected = mural;
this._selected = {
m: mural,
h: getMuralHeight(mural),
w: getMuralWidth(mural),
};
} else {
this._selected = undefined;
}
this.emit(StoreEvents.MuralSelect); this.emit(StoreEvents.MuralSelect);
this.save(); this.save();
} }
async save() { async save() {
await this.storage const muralBuffer = await Promise.all(this._murals.map(e => e.mural.getBuffer()));
.setItem(this.STORAGE_KEY_MURAL, this._murals this.db.setItem(this.STORAGE_KEY_MURAL, muralBuffer);
.map(m => ({ name: m.name, pixels: m.pixels, x: m.x, y: m.y } as Mural))); const selected = this._selected ? this._murals.indexOf(this._selected) : -1;
const selected = this._selected?.m ? this._murals.indexOf(this._selected?.m!) : -1; await this.db.setItem(this.STORAGE_KEY_SELECTED, selected);
await this.storage.setItem(this.STORAGE_KEY_SELECTED, selected);
} }
addPhantomOverlay(mural: MuralEx) { addPhantomOverlay(mural: MuralEx) {
const index = this._murals.indexOf(mural); const index = this._murals.indexOf(mural);
+37 -37
View File
@@ -1,5 +1,5 @@
import { clone, isInteger } from "lodash"; import { clone, isInteger } from "lodash";
import { MuralOld, MuralStatus, RGB } from "../interfaces"; import { MuralEx, MuralOld, MuralStatus, RGB } from "../interfaces";
import { fetchCombineTiledImage } from "./canvashot"; import { fetchCombineTiledImage } from "./canvashot";
import { Mural } from "./mural"; import { Mural } from "./mural";
@@ -240,12 +240,12 @@ export function flatQuantizeImageData(imageData: ImageData, palette: RGB[]) {
} }
} }
export async function getPixelStatusMural(mural: MuralOld, palette: RGB[]): Promise<MuralStatus> { export async function getPixelStatusMural(mural: Mural, palette: RGB[]): Promise<MuralStatus> {
const width = getMuralWidth(mural); const width = mural.w;
const height = getMuralHeight(mural); const height = mural.h;
const tile = await fetchCombineTiledImage(mural.x, mural.y, width, height); const tile = await fetchCombineTiledImage(mural.x, mural.y, width, height);
const imageData = tile.getContext("2d")?.getImageData(0, 0, width, height); const imageData = tile.getContext("2d")?.getImageData(0, 0, width, height);
const buffer = mural.pixels.flat(); const buffer = mural.pixelBuffer;
let total = 0; let total = 0;
let bad = 0; let bad = 0;
let good = 0; let good = 0;
@@ -272,36 +272,31 @@ export async function getPixelStatusMural(mural: MuralOld, palette: RGB[]): Prom
export function imageDataToPaletteIndices(imageData: ImageData, palette: RGB[]) { export function imageDataToPaletteIndices(imageData: ImageData, palette: RGB[]) {
const { height, width } = imageData; const { height, width } = imageData;
const pixels: number[][] = []; let k = 0;
for (let i = 0; i < height; i++) { const pixels = new Int8Array(height * width);
pixels.push([]);
}
for (let i = 0; i < imageData.data.length; i += 4) { for (let i = 0; i < imageData.data.length; i += 4) {
const r = imageData.data[i + 0] ?? 0; const r = imageData.data[i + 0] ?? 0;
const g = imageData.data[i + 1] ?? 0; const g = imageData.data[i + 1] ?? 0;
const b = imageData.data[i + 2] ?? 0; const b = imageData.data[i + 2] ?? 0;
const a = imageData.data[i + 3] ?? 0xff; const a = imageData.data[i + 3] ?? 0xff;
const line = Math.floor(Math.floor(i / 4) / width);
const data = pixels[line] || [];
if (a < 25) { if (a < 25) {
data.push(-1); pixels[k++] = -1;
} else { } else {
const index = findClosestFormArray(rgb(r, g, b), palette); const index = findClosestFormArray(rgb(r, g, b), palette);
data.push(index); pixels[k++] = index;
} }
} }
return pixels; return pixels;
} }
export function findClosestFormArray(rgbO: RGB, palette: RGB[]) { export function findClosestFormArray(rgbO: RGB, palette: RGB[]) {
const scores: number[] = []; const scores: number[] = new Array(palette.length).fill(0);
for (const rgb of palette) { for (let i = 0; i < palette.length; i++) {
const r = getColorScore(rgbO.r, rgb.r); const r = getColorScore(rgbO.r, palette[i].r);
const g = getColorScore(rgbO.g, rgb.g); const g = getColorScore(rgbO.g, palette[i].g);
const b = getColorScore(rgbO.b, rgb.b); const b = getColorScore(rgbO.b, palette[i].b);
scores.push(r + g + b); scores[i] = (r + g + b);
} }
const lowest = Math.min(...scores); const lowest = Math.min(...scores);
const index = scores.indexOf(lowest); const index = scores.indexOf(lowest);
@@ -318,24 +313,20 @@ export function imageToCanvas(image: HTMLImageElement) {
} }
export function drawPixelsOntoCanvas( export function drawPixelsOntoCanvas(
canvas: HTMLCanvasElement, pixelsData: number[][], palette: string[], pixelSize = 1 canvas: HTMLCanvasElement, mural: Mural, palette: string[], pixelSize = 1
) { ) {
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
if (!ctx) return 0; if (!ctx) return 0;
const data = pixelsData;
const height = data.length * pixelSize;
const width = data[0] ? data[0].length * pixelSize : 0;
if (!(canvas instanceof OffscreenCanvas)){ if (!(canvas instanceof OffscreenCanvas)){
canvas.width = width; canvas.width = mural.w;
canvas.height = height; canvas.height = mural.h;
canvas.style.width = `${width}px`; canvas.style.width = `${mural.w}px`;
canvas.style.height = `${height}px`; canvas.style.height = `${mural.h}px`;
} }
let pixels = 0; let pixels = 0;
for (let x = 0; x < width; x ++) { for (let x = 0; x < mural.w; x ++) {
for (let y = 0; y < height; y++) { for (let y = 0; y < mural.h; y++) {
const yd = data[y]; const index = mural.getPixel(x, y);
const index = (yd && yd[x]);
if (index != null && index > -1) { if (index != null && index > -1) {
pixels++; pixels++;
const aColor = palette[index]!; const aColor = palette[index]!;
@@ -343,7 +334,6 @@ export function drawPixelsOntoCanvas(
throw new Error(`Unknown color at ${index}`); throw new Error(`Unknown color at ${index}`);
} }
ctx.fillStyle = aColor; ctx.fillStyle = aColor;
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
} }
} }
@@ -373,14 +363,24 @@ export function toChunkY(y: number) {
return Math.floor(y / CHUNK_SIZE) * CHUNK_SIZE; return Math.floor(y / CHUNK_SIZE) * CHUNK_SIZE;
} }
export function canvasFromMural(pixelsToDraw: number[][], hex: string[]) { export function canvasFromMural(mural: Mural, palette: string[]) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = get2DArrWidth(pixelsToDraw); canvas.width = mural.w;
canvas.height = get2DArrHeight(pixelsToDraw); canvas.height = mural.h;
const pixels = drawPixelsOntoCanvas(canvas, pixelsToDraw, hex); const pixels = drawPixelsOntoCanvas(canvas, mural, palette);
return { canvas, pixels }; return { canvas, pixels };
} }
export function createMuralExtended(mural: Mural, palette: string[]): MuralEx {
const { canvas, pixels } = canvasFromMural(mural, palette);
return {
mural,
pixelCount: pixels,
ref: canvas
};
}
let numberFormatter: Intl.NumberFormat; let numberFormatter: Intl.NumberFormat;
try { try {
const userLocale = navigator.language || (navigator as any).userLanguage; const userLocale = navigator.language || (navigator as any).userLanguage;