Add progress display
This commit is contained in:
@@ -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,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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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[][] = [];
|
||||
|
||||
Reference in New Issue
Block a user