Add progress display

This commit is contained in:
2024-07-28 23:02:58 +02:00
parent cad3ec6fd7
commit 81cc47efd0
8 changed files with 269 additions and 128 deletions
+121 -90
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -138,7 +138,7 @@ export class Menu extends React.Component<Props, State> {
<PercentageDiv>{this.props.opacity}%</PercentageDiv>
<Btn onClick={() => this.props.onCollapsedChanged(!this.props.collapsed)}><FontAwesomeIcon icon={ this.props.collapsed ? faCaretDown : faCaretUp } /></Btn>
</Flex>
{this.props.collapsed ? null : <MuralList store={this.props.store} cords={this.props.cords}/> }
{this.props.collapsed ? null : <MuralList store={this.props.store} palette={this.props.palette} cords={this.props.cords}/> }
</Container>;
}
+4 -2
View File
@@ -4,6 +4,7 @@ import { Store, StoreEvents } from "../../lib/store";
import { MuralView } from "./muralView";
import styled from "styled-components";
import { Coordinates } from "../../lib/coordinates";
import { Palette } from "../../lib/palette";
const ScrollContainer = styled.div`
overflow: auto;
@@ -32,11 +33,12 @@ const ScrollContainer = styled.div`
interface Props {
store: Store;
cords: Coordinates;
palette: Palette;
}
interface State {
murals: MuralEx[];
selected?: SelectedMural
selected?: SelectedMural;
}
export class MuralList extends React.Component<Props, State> {
@@ -61,7 +63,7 @@ export class MuralList extends React.Component<Props, State> {
};
render() {
return <ScrollContainer> {this.state.murals.map((m,i) => {
return <MuralView key={i} mural={m} cords={this.props.cords} store={this.props.store} selected={m === this.state.selected?.m} />;
return <MuralView key={i} mural={m} cords={this.props.cords} palette={this.props.palette} store={this.props.store} selected={m === this.state.selected?.m} />;
})}</ScrollContainer>;
}
}
+77 -10
View File
@@ -1,16 +1,17 @@
import React from "react";
import { Store } from "../../lib/store";
import { Mural, MuralEx } from "../../interfaces";
import { Mural, MuralEx, MuralStatus } from "../../interfaces";
import styled from "styled-components";
import { Border, Btn, Flex, SELECTED_COLOR } from "../styles";
import { CanvasToCanvasJSX } from "./canvasToCanvasJSX";
import { formatNumber, getMuralHeight, getMuralWidth } from "../../lib/utils";
import { IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
import { formatNumber, getMuralHeight, getMuralWidth, getPixelStatusMural } from "../../lib/utils";
import { IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Coordinates } from "../../lib/coordinates";
import { TEXT_FORMATS } from "../importMural";
import saveAs from "file-saver";
import { Popup } from "./Popup";
import { Palette } from "../../lib/palette";
const Margin = styled.div`
margin: 2px;
@@ -27,11 +28,46 @@ const Line = styled.div`
display: flex;
flex-direction: row;
`;
const Strong = styled.strong`
margin-right: 2px;
`;
export const BtnSmall = styled.button`
border: 1px solid white;
font-size: 50pt;
width: 22px;
background-color: rgba(255, 255, 255, 0.75);
color: rgb(10, 10, 10);
border-radius: 12px;
border: 2px solid black;
margin: 1px;
margin-left: 3px;
display: flex;
padding: 3px;
outline: none !important;
text-decoration: none;
&:hover {
background-color: rgba(158, 158, 158, 0.75);
}
&:visited {
text-decoration: none;
}
&:disabled {
color: gray;
border-color: gray;
cursor: not-allowed;
}
transition: background-color 250ms;
`;
interface Props {
mural: MuralEx;
selected: boolean;
store: Store;
palette: Palette;
cords: Coordinates;
}
@@ -39,6 +75,7 @@ interface State {
width: number;
height: number;
overlay: boolean;
progress?: MuralStatus | (() => void /* loading */);
}
@@ -59,6 +96,10 @@ export class MuralView extends React.Component<Props, State> {
componentDidUpdate ( prevProps: Readonly<Props> ) {
if (this.props.mural.ref !== prevProps.mural.ref) {
this.updateSize();
if (typeof this.state.progress === "function") {
typeof this.state.progress();
this.setState({progress: undefined});
}
}
}
@@ -83,8 +124,8 @@ export class MuralView extends React.Component<Props, State> {
height, width
});
};
renderInfoLine(title: string, description: string) {
return <Line><H5>{title}:</H5><span> {description}</span></Line>;
renderInfoLine(title: string, description: string | React.JSX.Element) {
return <Line><H5>{title}:</H5><span>{description}</span></Line>;
}
btn(context: string, icon: IconDefinition, onClick: () => void, selected?: boolean) {
@@ -196,16 +237,42 @@ export class MuralView extends React.Component<Props, State> {
}
};
updateProgress = async () => {
let canceled = false;
const cancelFn = () => {
canceled = true;
};
this.setState({progress: cancelFn});
const data = await getPixelStatusMural(this.props.mural, this.props.palette.palette);
if (!canceled) {
this.setState({progress: data});
}
};
renderProgress() {
if (typeof this.state.progress === "function") {
return "Loading...";
}
const ref = <BtnSmall onClick={this.updateProgress}><FontAwesomeIcon icon={faRefresh} /></BtnSmall>;
if (this.state.progress && this.state.progress.total !== 0) {
const { total, bad, good } = this.state.progress;
return <Flex><Strong>{Math.floor(good / total * 100)}%</Strong> {bad > 0 ? formatNumber(bad) : ""}{ref}</Flex>;
} else {
return ref;
}
}
render() {
return <Border onClick={this.onLoad} style={{ border: this.props.selected ? "3px solid black" : "3px dotted black", cursor: "pointer" }} onMouseEnter={this.onEnter} onMouseLeave={this.onLeave}>
<Flex>
<CanvasToCanvasJSX canvas={this.props.mural.ref} height={this.state.height} width={this.state.width}/>
<Margin style={{flex: "1"}}>
{this.renderInfoLine("Name", this.props.mural.name)}
{this.renderInfoLine("PixelCount", 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("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("Progress", this.renderProgress())}
</Margin>
</Flex>
<Flex>
@@ -217,4 +284,4 @@ export class MuralView extends React.Component<Props, State> {
</Flex>
</Border>;
}
}
}
+6
View File
@@ -11,6 +11,12 @@ export interface Mural {
y: number;
}
export interface MuralStatus {
total: number;
good: number;
bad: number;
}
export interface LoadUnload {
load: () => Promise<void> | void;
unload: () => Promise<void> | void;
+27 -22
View File
@@ -141,37 +141,42 @@ export function getSelectionArea() {
});
}
export async function fetchCombineTiledImage(x: number, y: number, width: number, height: number, uScale = 0) {
const canvas = createCanvas();
canvas.width = width / Math.pow(2, uScale);
canvas.height = height / Math.pow(2, uScale);
const ctx = canvas.getContext("2d")!;
const offX = Math.floor((x % (CHUNK_SIZE)));
const offY = Math.floor((y % (CHUNK_SIZE)));
const awaiters: Promise<void>[] = [];
for (let xx = 0; xx < width + CHUNK_SIZE; xx += CHUNK_SIZE) {
for (let yy = 0; yy < height + CHUNK_SIZE; yy += CHUNK_SIZE) {
const promise = new Promise<void>((resolve, reject) => {
fetchTile(x + xx, y + yy).then(image => {
const offXX = x > 0 ? -offX : (-(CHUNK_SIZE + (offX)) % CHUNK_SIZE);
const offYY = y > 0 ? -offY : (-(CHUNK_SIZE + (offY)) % CHUNK_SIZE);
ctx.drawImage(image, xx + offXX, yy + offYY);
resolve();
}).catch(reject);
});
awaiters.push(promise);
}
}
await Promise.all(awaiters);
return canvas;
}
export async function takeCanvasShot(coordinates: Coordinates) {
const rect = await getSelectionArea();
const s = coordinates.uScale;
const cords = coordinates.screenToGrid(rect.x, rect.y);
const cordsEnd = coordinates.screenToGrid(rect.x + rect.width, rect.height + rect.y);
if (!cords || !cordsEnd) {
return; // unavailable
}
const canvas = createCanvas();
canvas.width = rect.width / Math.pow(2, s);
canvas.height = rect.height / Math.pow(2, s);
const ctx = canvas.getContext("2d")!;
const offX = Math.floor((cords.x % (CHUNK_SIZE)));
const offY = Math.floor((cords.y % (CHUNK_SIZE)));
const awaiters: Promise<void>[] = [];
for (let x = 0; x < rect.width + CHUNK_SIZE; x += CHUNK_SIZE) {
for (let y = 0; y < rect.height + CHUNK_SIZE; y += CHUNK_SIZE) {
const promise = new Promise<void>((resolve, reject) => {
fetchTile(cords.x + x, cords.y + y).then(image => {
const offXX = cords.x > 0 ? -offX : (-(CHUNK_SIZE + (offX)) % CHUNK_SIZE);
const offYY = cords.y > 0 ? -offY : (-(CHUNK_SIZE + (offY)) % CHUNK_SIZE);
ctx.drawImage(image, x + offXX, y + offYY);
resolve();
}).catch(reject);
});
awaiters.push(promise);
}
}
try {
await Promise.all(awaiters);
const canvas = await fetchCombineTiledImage(cords.x, cords.y, rect.height, rect.width, coordinates.uScale);
const url = canvas.toDataURL();
Popup.custom(<img style={{border: "1px solid black"}} src={url} />, [{
content: "Download",
+1 -2
View File
@@ -1,7 +1,6 @@
import { BasicEventEmitter, Listener } from "./eventEmitter";
import { LoadUnload, Mural, MuralEx, SelectedMural } from "./interfaces";
import { LoadUnload, Mural, MuralEx, SelectedMural } from "../interfaces";
import { Palette } from "./palette";
import { Storage } from "./storage";
import { canvasFromMural, getMuralHeight, getMuralWidth, pushUnique, removeItem } from "./utils";
+32 -1
View File
@@ -1,5 +1,6 @@
import { clone, isInteger } from "lodash";
import { Mural, RGB } from "../interfaces";
import { Mural, MuralStatus, RGB } from "../interfaces";
import { fetchCombineTiledImage } from "./canvashot";
export const CHUNK_SIZE = 512;
@@ -222,6 +223,36 @@ export function flatQuantizeImageData(imageData: ImageData, palette: RGB[]) {
}
}
export async function getPixelStatusMural(mural: Mural, palette: RGB[]): Promise<MuralStatus> {
const width = getMuralWidth(mural);
const height = getMuralHeight(mural);
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();
let total = 0;
let bad = 0;
let good = 0;
if (imageData) {
for (let i = 0, j = 0; i < imageData.data.length; i += 4, j++) {
if (buffer[j] >= 0) {
const r = imageData.data[i + 0];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
const obj = rgb(r, g, b);
total++;
if (findClosestIndexColor(obj, palette) === buffer[j]) {
good++;
} else {
bad++;
}
}
}
}
return { total, good, bad };
}
export function imageDataToPaletteIndices(imageData: ImageData, palette: RGB[]) {
const { height, width } = imageData;
const pixels: number[][] = [];