Add progress display
This commit is contained in:
Vendored
+121
-90
File diff suppressed because one or more lines are too long
@@ -138,7 +138,7 @@ export class Menu extends React.Component<Props, State> {
|
|||||||
<PercentageDiv>{this.props.opacity}%</PercentageDiv>
|
<PercentageDiv>{this.props.opacity}%</PercentageDiv>
|
||||||
<Btn onClick={() => this.props.onCollapsedChanged(!this.props.collapsed)}><FontAwesomeIcon icon={ this.props.collapsed ? faCaretDown : faCaretUp } /></Btn>
|
<Btn onClick={() => this.props.onCollapsedChanged(!this.props.collapsed)}><FontAwesomeIcon icon={ this.props.collapsed ? faCaretDown : faCaretUp } /></Btn>
|
||||||
</Flex>
|
</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>;
|
</Container>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Store, StoreEvents } from "../../lib/store";
|
|||||||
import { MuralView } from "./muralView";
|
import { MuralView } from "./muralView";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Coordinates } from "../../lib/coordinates";
|
import { Coordinates } from "../../lib/coordinates";
|
||||||
|
import { Palette } from "../../lib/palette";
|
||||||
|
|
||||||
const ScrollContainer = styled.div`
|
const ScrollContainer = styled.div`
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -32,11 +33,12 @@ const ScrollContainer = styled.div`
|
|||||||
interface Props {
|
interface Props {
|
||||||
store: Store;
|
store: Store;
|
||||||
cords: Coordinates;
|
cords: Coordinates;
|
||||||
|
palette: Palette;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
murals: MuralEx[];
|
murals: MuralEx[];
|
||||||
selected?: SelectedMural
|
selected?: SelectedMural;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MuralList extends React.Component<Props, State> {
|
export class MuralList extends React.Component<Props, State> {
|
||||||
@@ -61,7 +63,7 @@ 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} 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>;
|
})}</ScrollContainer>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Store } from "../../lib/store";
|
import { Store } from "../../lib/store";
|
||||||
import { Mural, MuralEx } from "../../interfaces";
|
import { Mural, MuralEx, MuralStatus } from "../../interfaces";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Border, Btn, Flex, SELECTED_COLOR } from "../styles";
|
import { Border, Btn, Flex, SELECTED_COLOR } from "../styles";
|
||||||
import { CanvasToCanvasJSX } from "./canvasToCanvasJSX";
|
import { CanvasToCanvasJSX } from "./canvasToCanvasJSX";
|
||||||
import { formatNumber, getMuralHeight, getMuralWidth } from "../../lib/utils";
|
import { formatNumber, getMuralHeight, getMuralWidth, getPixelStatusMural } from "../../lib/utils";
|
||||||
import { IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Coordinates } from "../../lib/coordinates";
|
import { Coordinates } from "../../lib/coordinates";
|
||||||
import { TEXT_FORMATS } from "../importMural";
|
import { TEXT_FORMATS } from "../importMural";
|
||||||
import saveAs from "file-saver";
|
import saveAs from "file-saver";
|
||||||
import { Popup } from "./Popup";
|
import { Popup } from "./Popup";
|
||||||
|
import { Palette } from "../../lib/palette";
|
||||||
|
|
||||||
const Margin = styled.div`
|
const Margin = styled.div`
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
@@ -27,11 +28,46 @@ const Line = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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 {
|
interface Props {
|
||||||
mural: MuralEx;
|
mural: MuralEx;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
store: Store;
|
store: Store;
|
||||||
|
palette: Palette;
|
||||||
cords: Coordinates;
|
cords: Coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +75,7 @@ interface State {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
overlay: boolean;
|
overlay: boolean;
|
||||||
|
progress?: MuralStatus | (() => void /* loading */);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +96,10 @@ export class MuralView extends React.Component<Props, State> {
|
|||||||
componentDidUpdate ( prevProps: Readonly<Props> ) {
|
componentDidUpdate ( prevProps: Readonly<Props> ) {
|
||||||
if (this.props.mural.ref !== prevProps.mural.ref) {
|
if (this.props.mural.ref !== prevProps.mural.ref) {
|
||||||
this.updateSize();
|
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
|
height, width
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
renderInfoLine(title: string, description: string) {
|
renderInfoLine(title: string, description: string | React.JSX.Element) {
|
||||||
return <Line><H5>{title}:</H5><span> {description}</span></Line>;
|
return <Line><H5>{title}:</H5><span>{description}</span></Line>;
|
||||||
}
|
}
|
||||||
|
|
||||||
btn(context: string, icon: IconDefinition, onClick: () => void, selected?: boolean) {
|
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() {
|
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}>
|
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>
|
<Flex>
|
||||||
<CanvasToCanvasJSX canvas={this.props.mural.ref} height={this.state.height} width={this.state.width}/>
|
<CanvasToCanvasJSX canvas={this.props.mural.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.mural.name)}
|
||||||
{this.renderInfoLine("PixelCount", formatNumber(this.props.mural.pixelCount))}
|
{this.renderInfoLine("Pixel count", formatNumber(this.props.mural.pixelCount))}
|
||||||
{this.renderInfoLine("x", this.props.mural.x.toString() )}
|
{this.renderInfoLine("X", this.props.mural.x.toString() )}
|
||||||
{this.renderInfoLine("y", this.props.mural.y.toString() )}
|
{this.renderInfoLine("Y", this.props.mural.y.toString() )}
|
||||||
{this.renderInfoLine("size", `${getMuralWidth(this.props.mural)}x${getMuralHeight(this.props.mural)}`)}
|
{this.renderInfoLine("Size", `${getMuralWidth(this.props.mural)}x${getMuralHeight(this.props.mural)}`)}
|
||||||
|
{this.renderInfoLine("Progress", this.renderProgress())}
|
||||||
</Margin>
|
</Margin>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex>
|
<Flex>
|
||||||
@@ -217,4 +284,4 @@ export class MuralView extends React.Component<Props, State> {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Border>;
|
</Border>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ export interface Mural {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MuralStatus {
|
||||||
|
total: number;
|
||||||
|
good: number;
|
||||||
|
bad: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoadUnload {
|
export interface LoadUnload {
|
||||||
load: () => Promise<void> | void;
|
load: () => Promise<void> | void;
|
||||||
unload: () => 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) {
|
export async function takeCanvasShot(coordinates: Coordinates) {
|
||||||
const rect = await getSelectionArea();
|
const rect = await getSelectionArea();
|
||||||
const s = coordinates.uScale;
|
|
||||||
const cords = coordinates.screenToGrid(rect.x, rect.y);
|
const cords = coordinates.screenToGrid(rect.x, rect.y);
|
||||||
const cordsEnd = coordinates.screenToGrid(rect.x + rect.width, rect.height + rect.y);
|
const cordsEnd = coordinates.screenToGrid(rect.x + rect.width, rect.height + rect.y);
|
||||||
if (!cords || !cordsEnd) {
|
if (!cords || !cordsEnd) {
|
||||||
return; // unavailable
|
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 {
|
try {
|
||||||
await Promise.all(awaiters);
|
const canvas = await fetchCombineTiledImage(cords.x, cords.y, rect.height, rect.width, coordinates.uScale);
|
||||||
|
|
||||||
const url = canvas.toDataURL();
|
const url = canvas.toDataURL();
|
||||||
Popup.custom(<img style={{border: "1px solid black"}} src={url} />, [{
|
Popup.custom(<img style={{border: "1px solid black"}} src={url} />, [{
|
||||||
content: "Download",
|
content: "Download",
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
import { BasicEventEmitter, Listener } from "./eventEmitter";
|
import { BasicEventEmitter, Listener } from "./eventEmitter";
|
||||||
import { LoadUnload, Mural, MuralEx, SelectedMural } from "./interfaces";
|
import { LoadUnload, Mural, MuralEx, SelectedMural } 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 { canvasFromMural, getMuralHeight, getMuralWidth, pushUnique, removeItem } from "./utils";
|
||||||
|
|
||||||
|
|||||||
+32
-1
@@ -1,5 +1,6 @@
|
|||||||
import { clone, isInteger } from "lodash";
|
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;
|
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[]) {
|
export function imageDataToPaletteIndices(imageData: ImageData, palette: RGB[]) {
|
||||||
const { height, width } = imageData;
|
const { height, width } = imageData;
|
||||||
const pixels: number[][] = [];
|
const pixels: number[][] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user