diff --git a/package-lock.json b/package-lock.json index 01e342f..1ff2370 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "copy-dir": "^1.3.0", "esbuild": "^0.20.0", "file-saver": "^2.0.5", + "localforage": "^1.10.0", "lodash": "^4.17.21", "process": "^0.11.10", "react": "^18.2.0", @@ -2259,6 +2260,12 @@ "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": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2734,6 +2741,24 @@ "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": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5457,6 +5482,11 @@ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5797,6 +5827,22 @@ "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": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/package.json b/package.json index 48614cf..bf312da 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "copy-dir": "^1.3.0", "esbuild": "^0.20.0", "file-saver": "^2.0.5", + "localforage": "^1.10.0", "lodash": "^4.17.21", "process": "^0.11.10", "react": "^18.2.0", diff --git a/src/UI/components/main.tsx b/src/UI/components/main.tsx index ad7c53e..ed82120 100644 --- a/src/UI/components/main.tsx +++ b/src/UI/components/main.tsx @@ -5,12 +5,12 @@ import { Coordinates } from "../../lib/coordinates"; import { Palette } from "../../lib/palette"; import { Menu } from "./menu"; import { MovableWindow } from "./movableWindow"; -import { SelectedMural } from "../../interfaces"; 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"; const Img = styled.img` width: 25px; @@ -33,7 +33,7 @@ interface Props { } interface State extends StoreSettings{ - selected?: SelectedMural; + selected?: MuralEx; overlays: number[]; phantomOverlay: number; overlayModify?: OverlayReturn; @@ -127,14 +127,14 @@ export class Main extends React.Component { opacity={this.state.opacity} cords={this.props.cords} palette={this.props.palette} - mural={this.props.store.murals[o]} + muralExtended={this.props.store.murals[o]} /> )} {this.props.store.murals[this.state.phantomOverlay] ? : null } {this.state.overlayModify ? { storage={this.props.storage} cords={this.props.cords} palette={this.props.palette} - mural={this.props.store.overlayModify!.pixels} + muralExtended={this.props.store.overlayModify!.mural} onChange={(name, x, y, confirm) => { this.state.overlayModify!.cb(name, x, y, confirm); }} /> : null} diff --git a/src/UI/components/muralList.tsx b/src/UI/components/muralList.tsx index 0e7cf94..7a56023 100644 --- a/src/UI/components/muralList.tsx +++ b/src/UI/components/muralList.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { MuralEx, SelectedMural } from "../../interfaces"; +import { MuralEx } from "../../interfaces"; import { Store, StoreEvents } from "../../lib/store"; import { MuralView } from "./muralView"; import styled from "styled-components"; @@ -38,7 +38,7 @@ interface Props { interface State { murals: MuralEx[]; - selected?: SelectedMural; + selected?: MuralEx; } export class MuralList extends React.Component { @@ -63,9 +63,12 @@ export class MuralList extends React.Component { }; render() { return {this.state.murals.map((m,i) => { - return ; + return ; })}; } } diff --git a/src/UI/components/muralView.tsx b/src/UI/components/muralView.tsx index 3b57120..12dd98e 100644 --- a/src/UI/components/muralView.tsx +++ b/src/UI/components/muralView.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Store } from "../../lib/store"; -import { MuralEx, MuralOld, MuralStatus } from "../../interfaces"; +import { MuralEx, MuralStatus } from "../../interfaces"; import styled from "styled-components"; import { A, Border, Btn, Flex, SELECTED_COLOR } from "../styles"; import { CanvasToCanvasJSX } from "./canvasToCanvasJSX"; -import { convertOldMuralToNewMural, formatNumber, getMuralHeight, getMuralWidth, getPixelStatusMural } from "../../lib/utils"; +import { formatNumber, getPixelStatusMural } from "../../lib/utils"; import { IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons"; @@ -66,7 +66,7 @@ export const BtnSmall = styled.button` `; interface Props { - mural: MuralEx; + muralExtended: MuralEx; selected: boolean; store: Store; palette: Palette; @@ -102,7 +102,7 @@ export class MuralView extends React.Component { this.props.cords.off(CordType.Url, this.onCordUpdate); } componentDidUpdate(prevProps: Readonly ) { - if (this.props.mural.ref !== prevProps.mural.ref) { + if (this.props.muralExtended.ref !== prevProps.muralExtended.ref) { this.updateSize(); if (typeof this.state.progress === "function") { typeof this.state.progress(); @@ -116,8 +116,8 @@ export class MuralView extends React.Component { }; updateSize = () => { - const w = this.props.mural.ref.width; - const h = this.props.mural.ref.height; + const w = this.props.muralExtended.ref.width; + const h = this.props.muralExtended.ref.height; let width = w; let height = h; @@ -153,26 +153,26 @@ export class MuralView extends React.Component { if ("tagName" in tr) { const ignore = ["button", "svg", "path"]; 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 () => { - const mural = this.props.mural; + const muralExtended = this.props.muralExtended; this.props.store.setOverlayModify({ - pixels: this.props.mural.pixels, + mural: muralExtended, cb: (name, x, y, confirm) => { if (confirm) { - mural.name = name; - mural.x = x; - mural.y = y; - this.props.store.updateMural(mural); + muralExtended.mural.name = name; + muralExtended.mural.x = x; + muralExtended.mural.y = y; + this.props.store.updateMural(muralExtended.mural); } }, muralObj: { - name: this.props.mural.name, - x: this.props.mural.x, - y: this.props.mural.y + name: this.props.muralExtended.mural.name, + x: this.props.muralExtended.mural.x, + y: this.props.muralExtended.mural.y } }); @@ -212,39 +212,35 @@ export class MuralView extends React.Component { // } }; onExport = async () => { - const rawMural: MuralOld = { - 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 mural = this.props.muralExtended.mural; const buffer = await mural.getBuffer(); const blob = new Blob([buffer], { type: "octet/stream" }); - const saveName = rawMural.name + const saveName = mural.name .replace(/ /, "_") .replace(/[^a-zA-Z0-9-_]/g, ""); saveAs(blob, `${saveName}.${BIN_FORMATS[0]}`); }; onDelete = async () => { - if (await Popup.confirm(`Are you sure you want to remove "${this.props.mural.name}"`)) { - this.s.remove(this.props.mural); + if (await Popup + .confirm(`Are you sure you want to remove "${this.props.muralExtended.mural.name}"`) + ) { + this.s.remove(this.props.muralExtended); } }; onEnter = () => { - this.props.store.addPhantomOverlay(this.props.mural); + this.props.store.addPhantomOverlay(this.props.muralExtended); }; onLeave = () => { this.props.store.removePhantomOverlay(); }; get link() { - const weight = getMuralWidth(this.props.mural); - const height = getMuralHeight(this.props.mural); - const x = this.props.mural.x + Math.round(weight / 2); - const y = this.props.mural.y + Math.round(height / 2); + const weight = this.props.muralExtended.mural.w; + const height = this.props.muralExtended.mural.h; + const x = this.props.muralExtended.mural.x + Math.round(weight / 2); + const y = this.props.muralExtended.mural.y + Math.round(height / 2); const o = origin === "null" ? `${location.href}` : `${origin}/`; return `${o}@${x},${y},${this.props.cords.uScale}`; } @@ -254,10 +250,10 @@ export class MuralView extends React.Component { }; onPreview = () => { - if (this.props.store.hasOverlay(this.props.mural)) { - this.props.store.removeOverlay(this.props.mural); + if (this.props.store.hasOverlay(this.props.muralExtended)) { + this.props.store.removeOverlay(this.props.muralExtended); } 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 { canceled = true; }; 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) { this.setState({progress: data}); } @@ -293,18 +291,21 @@ export class MuralView extends React.Component { cursor: "pointer" }} onMouseEnter={this.onEnter} onMouseLeave={this.onLeave}> - + - {this.renderInfoLine("Name", this.props.mural.name)} - {this.renderInfoLine("Pixel count", formatNumber(this.props.mural.pixelCount))} - {this.renderInfoLine("X", this.props.mural.x.toString() )} - {this.renderInfoLine("Y", this.props.mural.y.toString() )} - {this.renderInfoLine("Size", `${getMuralWidth(this.props.mural)}x${getMuralHeight(this.props.mural)}`)} + {this.renderInfoLine("Name", this.props.muralExtended.mural.name)} + {this.renderInfoLine("Pixel count", formatNumber(this.props.muralExtended.pixelCount))} + {this.renderInfoLine("X", this.props.muralExtended.mural.x.toString() )} + {this.renderInfoLine("Y", this.props.muralExtended.mural.y.toString() )} + {this.renderInfoLine( + "Size", + `${this.props.muralExtended.mural.w}x${this.props.muralExtended.mural.w}` + )} {this.renderInfoLine("Progress", this.renderProgress())} - {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("Export", faDownload, this.onExport)} {this.btn("Delete", faTrash, this.onDelete)} diff --git a/src/UI/components/overlay.tsx b/src/UI/components/overlay.tsx index 33ab626..9055327 100644 --- a/src/UI/components/overlay.tsx +++ b/src/UI/components/overlay.tsx @@ -1,12 +1,12 @@ import React from "react"; import styled from "styled-components"; -import { Mural, MuralEx } from "../../interfaces"; -import { canvasFromMural, get2DArrHeight, get2DArrWidth } from "../../lib/utils"; +import { MuralEx } from "../../interfaces"; import { Coordinates, CordType } from "../../lib/coordinates"; 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,7 +15,7 @@ const Canvas = styled.canvas` `; interface Props { - mural: MuralEx | Mural | number[][]; + muralExtended: MuralEx; muralObj?: Partial; onChange?: (name: string, x: number, y:number, confirm?: boolean) => void; cords: Coordinates; @@ -61,7 +61,7 @@ export class Overlay extends React.Component { } componentDidUpdate(prevProps: Readonly, prevState: Readonly) { - if (prevProps.mural !== this.props.mural) { + if (prevProps.muralExtended !== this.props.muralExtended) { this._refImage = undefined; this.setState({ x: 0, y: 0 @@ -72,43 +72,21 @@ export class Overlay extends React.Component { } } - private isPixels(pixels: MuralEx | Mural | number[][]): pixels is number[][] { - return Array.isArray(pixels); - } get pixels() { - if (this.isPixels(this.props.mural)) { - return this.props.mural; - } else { - return this.props.mural.pixels; - } + return this.props.muralExtended.mural.pixelBuffer; } get x() { - if (this.isPixels(this.props.mural)) { - return this.state.x; - } else { - return this.props.mural.x; - } + return this.props.muralExtended.mural.x; } get y() { - if (this.isPixels(this.props.mural)) { - return this.state.y; - } else { - return this.props.mural.y; - } + return this.props.muralExtended.mural.y; } get refImg() { - if (!Array.isArray(this.props.mural) && "ref" in this.props.mural) { - return this.props.mural.ref; - } - if (this._refImage) { - return this._refImage; - } - this._refImage = canvasFromMural(this.pixels, this.props.palette.hex).canvas; - return this._refImage; + return this.props.muralExtended.ref; } draw = () => { @@ -122,8 +100,8 @@ export class Overlay extends React.Component { } const { x, y } = cords; - const width = get2DArrWidth(this.pixels) * scale; - const height = get2DArrHeight(this.pixels) * scale; + 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; diff --git a/src/UI/importMural.tsx b/src/UI/importMural.tsx index 0d309b8..3cc2320 100644 --- a/src/UI/importMural.tsx +++ b/src/UI/importMural.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { MuralOld, RGB } from "../interfaces"; -import { canvasToImageData, get2DArrHeight, get2DArrWidth, getColorScore, getExtension, +import { ImportOutput, MuralOld, RGB } from "../interfaces"; +import { canvasToImageData, convertOldMuralToNewMural, getColorScore, getExtension, imageDataToPaletteIndices, imageToCanvas, loadImageSource, processNumberEvent, readAsArrayBuffer, - readAsDataUrl, readAsString, resize, rgb, validateMural } from "../lib/utils"; + readAsDataUrl, readAsString, resize, rgb } from "../lib/utils"; import styled from "styled-components"; import { Popup } from "./components/Popup"; import RgbQuant, { DitheringKernel, RGBQuantOptions } from "rgbquant"; @@ -56,7 +56,7 @@ type DitheringKernelEx = DitheringKernel | "Flat"; interface QuantResult { type: DitheringKernelEx; canvas: HTMLCanvasElement; - indices: number[][]; + indices: Int8Array; } type DitherSetting = DitheringKernel | "Flat" | "show-all"; @@ -85,33 +85,35 @@ export interface ImageToMuralOptions { quantizerSetting: DitherSetting; } -export async function importArtWorks(store: Store, cords: Coordinates, palette: Palette) { +export async function importArtWorks( + store: Store, cords: Coordinates, palette: Palette +): Promise { const files = await importFiles(); - const output: MuralOld[] = []; + const output: Mural[] = []; for (const file of files) { if (file.type === "mural") { output.push(file.data); } else { - const img = file.data as HTMLImageElement; + const img = file.data; const pixels = await imageToMural(img, palette); - const mural = await new Promise((resolve, reject)=> { + const mural = await new Promise((resolve, reject)=> { store.setOverlayModify({ pixels, muralObj: { name: img.alt, - x: Math.floor(cords.ux - (get2DArrWidth(pixels) / 2)), - y: Math.floor(cords.uy - (get2DArrHeight(pixels) / 2)), + x: Math.floor(cords.ux - (img.width / 2)), + y: Math.floor(cords.uy - (img.height / 2)), }, cb: (name, x, y, confirm) => { if (confirm) { - resolve({ name, pixels, x, y }); + const mural = new Mural(name, x,y,img.width, img.height, pixels); + resolve(mural); } else { reject(new Error("Canceled by user")); } } }); }); - validateMural(mural); output.push(mural); } } @@ -472,7 +474,7 @@ export function imageDataToCanvas(imageData: ImageData) { return canvas; } -async function importFiles() { +async function importFiles(): Promise { const fileInput = new FileInput(); const images = ["png", "jpg", "jpeg"]; 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"); } - const output: ({ - type: "mural", - data: MuralOld - } | { - type: "image", - data: HTMLImageElement - }) [] = []; + const output: ImportOutput[] = []; for (const file of filteredFiles) { const ex = getExtension(file.name); const etn = ex.ex.toLowerCase(); @@ -507,41 +503,17 @@ async function importFiles() { mural.name = await Popup .prompt("Missing name for this mural. Please enter it manually", name) || ""; } - + output.push({ type: "mural", - data: mural, + data: convertOldMuralToNewMural(mural), }); } else if (BIN_FORMATS.includes(etn)) { const buffer = await readAsArrayBuffer(file); 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({ type: "mural", - data: { - name, x: mural.x, y: mural.y, pixels, - }, + data: mural, }); } else { const readData = await readAsDataUrl(file); diff --git a/src/interfaces.ts b/src/interfaces.ts index ffbcc06..edbdf90 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,3 +1,5 @@ +import { Mural } from "./lib/mural"; + export interface RGB { r: number; g: number; @@ -22,13 +24,8 @@ export interface LoadUnload { unload: () => Promise | void; } -export interface SelectedMural { - m: MuralEx; - w: number; - h: number; -} - -export interface MuralEx extends MuralOld { +export interface MuralEx { + mural: Mural; ref: HTMLCanvasElement; pixelCount: number; } @@ -43,4 +40,16 @@ export interface Rect { y: number; width: number; height: number; -} \ No newline at end of file +} + +export interface ImportOutputMural { + type: "mural", + data: Mural +} + +export interface ImportOutputImage { + type: "image", + data: HTMLImageElement; +} + +export type ImportOutput = ImportOutputImage | ImportOutputMural; diff --git a/src/lib/mural.ts b/src/lib/mural.ts index ebedb4d..e2215e3 100644 --- a/src/lib/mural.ts +++ b/src/lib/mural.ts @@ -14,6 +14,10 @@ export class Mural { this.validate(); } + clone() { + return new Mural(this.name, this.x, this.y, this.w, this.h, this._b.slice()); + } + getBuffer() { this.validate(); const encoder = new TextEncoder(); diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 3979b09..3b9bb93 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -28,7 +28,7 @@ export class Storage { } return Promise.resolve(); } - deleteItem(key: string) { + removeItem(key: string) { if (this.chrome) { return new Promise(r => { chrome.storage.local.remove(key, r); diff --git a/src/lib/store.ts b/src/lib/store.ts index 4524eee..cb955af 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -1,8 +1,13 @@ import { BasicEventEmitter, Listener } from "./eventEmitter"; -import { LoadUnload, Mural, MuralEx, SelectedMural } from "../interfaces"; +import { LoadUnload, MuralEx, MuralOld } from "../interfaces"; import { Palette } from "./palette"; 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 { MuralAdd = 1, @@ -15,7 +20,7 @@ export enum StoreEvents { } export interface OverlayReturn { - pixels: number[][]; + mural: MuralEx; muralObj?: Partial; 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_SELECTED = "_mural"; private _murals: MuralEx[] = []; - private _selected: SelectedMural | undefined; + private _selected?: MuralEx; private emitter = new BasicEventEmitter(); private _overlayIndices: number[] = []; private _phantomOverlay = -1; private _overlayModify?: OverlayReturn; + private db = localforage.createInstance({ + name: "pixel-canvas-overlay" + }); constructor(private storage: Storage, private palette: Palette) {} async load() { - this._murals = await this.storage.getItem(this.STORAGE_KEY_MURAL) || []; - for (const mural of this._murals) { - const { canvas, pixels } = canvasFromMural(mural.pixels, this.palette.hex); - mural.ref = canvas; - mural.pixelCount = pixels; + console.log("loaded"); + this._murals = []; + const rawMurals = await this.db.getItem(this.STORAGE_KEY_MURAL) || []; + + const murals = await Promise.all(rawMurals.map(e => Mural.from(e))); + // backwards compatibility + const oldMurals = await this.storage.getItem(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(this.STORAGE_KEY_MURAL); + + for (const mural of murals) { + this._murals.push(createMuralExtended(mural, this.palette.hex)); + } + + const index = await this.storage.getItem(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(this.STORAGE_KEY_SELECTED); + if (index != null) { + this.select(this._murals[index]); + } + } } updateMural(_mural: Mural) { this.save(); @@ -50,10 +85,7 @@ export class Store implements LoadUnload { this.save(); } add(mural: Mural) { - const m = mural as MuralEx; - const { canvas, pixels } = canvasFromMural(mural.pixels, this.palette.hex); - m.ref = canvas; - m.pixelCount = pixels; + const m = createMuralExtended(mural, this.palette.hex); this._murals.push(m); this.emit(StoreEvents.Any); this.save(); @@ -63,7 +95,7 @@ export class Store implements LoadUnload { this._overlayIndices = []; this._overlayModify = undefined; this._phantomOverlay = -1; - if (this._selected && this._selected?.m === mural) { + if (this._selected && this._selected === mural) { this._selected = undefined; } removeItem(this._murals, mural); @@ -89,7 +121,7 @@ export class Store implements LoadUnload { this.emit(StoreEvents.MuralOverlay); this.save(); } - setOverlayModify(overlay: OverlayReturn) { + setOverlayModify(overlay: OverlayReturn) { // WTF? this._overlayModify = overlay; const cb = overlay.cb; overlay.cb = (name, x, y, done) => { @@ -105,25 +137,16 @@ export class Store implements LoadUnload { } select(mural: MuralEx | undefined) { - if (mural) { - this._selected = { - m: mural, - h: getMuralHeight(mural), - w: getMuralWidth(mural), - }; - } else { - this._selected = undefined; - } + this._selected = mural; this.emit(StoreEvents.MuralSelect); this.save(); } async save() { - await this.storage - .setItem(this.STORAGE_KEY_MURAL, this._murals - .map(m => ({ name: m.name, pixels: m.pixels, x: m.x, y: m.y } as Mural))); - const selected = this._selected?.m ? this._murals.indexOf(this._selected?.m!) : -1; - await this.storage.setItem(this.STORAGE_KEY_SELECTED, selected); + const muralBuffer = await Promise.all(this._murals.map(e => e.mural.getBuffer())); + this.db.setItem(this.STORAGE_KEY_MURAL, muralBuffer); + const selected = this._selected ? this._murals.indexOf(this._selected) : -1; + await this.db.setItem(this.STORAGE_KEY_SELECTED, selected); } addPhantomOverlay(mural: MuralEx) { const index = this._murals.indexOf(mural); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 05df32a..bb0dad8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,5 @@ import { clone, isInteger } from "lodash"; -import { MuralOld, MuralStatus, RGB } from "../interfaces"; +import { MuralEx, MuralOld, MuralStatus, RGB } from "../interfaces"; import { fetchCombineTiledImage } from "./canvashot"; import { Mural } from "./mural"; @@ -240,12 +240,12 @@ export function flatQuantizeImageData(imageData: ImageData, palette: RGB[]) { } } -export async function getPixelStatusMural(mural: MuralOld, palette: RGB[]): Promise { - const width = getMuralWidth(mural); - const height = getMuralHeight(mural); +export async function getPixelStatusMural(mural: Mural, palette: RGB[]): Promise { + const width = mural.w; + const height = mural.h; const tile = await fetchCombineTiledImage(mural.x, mural.y, 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 bad = 0; let good = 0; @@ -272,36 +272,31 @@ export async function getPixelStatusMural(mural: MuralOld, palette: RGB[]): Prom export function imageDataToPaletteIndices(imageData: ImageData, palette: RGB[]) { const { height, width } = imageData; - const pixels: number[][] = []; - for (let i = 0; i < height; i++) { - pixels.push([]); - } + let k = 0; + const pixels = new Int8Array(height * width); 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] ?? 0xff; - - const line = Math.floor(Math.floor(i / 4) / width); - const data = pixels[line] || []; if (a < 25) { - data.push(-1); + pixels[k++] = -1; } else { const index = findClosestFormArray(rgb(r, g, b), palette); - data.push(index); + pixels[k++] = index; } } return pixels; } export function findClosestFormArray(rgbO: RGB, palette: RGB[]) { - const scores: number[] = []; + const scores: number[] = new Array(palette.length).fill(0); - for (const rgb of 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); + for (let i = 0; i < palette.length; i++) { + const r = getColorScore(rgbO.r, palette[i].r); + const g = getColorScore(rgbO.g, palette[i].g); + const b = getColorScore(rgbO.b, palette[i].b); + scores[i] = (r + g + b); } const lowest = Math.min(...scores); const index = scores.indexOf(lowest); @@ -318,24 +313,20 @@ export function imageToCanvas(image: HTMLImageElement) { } export function drawPixelsOntoCanvas( - canvas: HTMLCanvasElement, pixelsData: number[][], palette: string[], pixelSize = 1 + canvas: HTMLCanvasElement, mural: Mural, palette: string[], pixelSize = 1 ) { const ctx = canvas.getContext("2d")!; 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)){ - canvas.width = width; - canvas.height = height; - canvas.style.width = `${width}px`; - canvas.style.height = `${height}px`; + canvas.width = mural.w; + canvas.height = mural.h; + canvas.style.width = `${mural.w}px`; + canvas.style.height = `${mural.h}px`; } let pixels = 0; - for (let x = 0; x < width; x ++) { - for (let y = 0; y < height; y++) { - const yd = data[y]; - const index = (yd && yd[x]); + for (let x = 0; x < mural.w; x ++) { + for (let y = 0; y < mural.h; y++) { + const index = mural.getPixel(x, y); if (index != null && index > -1) { pixels++; const aColor = palette[index]!; @@ -343,7 +334,6 @@ export function drawPixelsOntoCanvas( throw new Error(`Unknown color at ${index}`); } ctx.fillStyle = aColor; - 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; } -export function canvasFromMural(pixelsToDraw: number[][], hex: string[]) { +export function canvasFromMural(mural: Mural, palette: string[]) { const canvas = document.createElement("canvas"); - canvas.width = get2DArrWidth(pixelsToDraw); - canvas.height = get2DArrHeight(pixelsToDraw); - const pixels = drawPixelsOntoCanvas(canvas, pixelsToDraw, hex); + canvas.width = mural.w; + canvas.height = mural.h; + const pixels = drawPixelsOntoCanvas(canvas, mural, palette); 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; try { const userLocale = navigator.language || (navigator as any).userLanguage;