Make overlay
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
import React, { createRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const Box = styled.div`
|
||||
min-width: 250px;
|
||||
min-height: 50px;
|
||||
max-width: fit-content;
|
||||
max-height: fit-content;
|
||||
margin: 15px auto 15px auto;
|
||||
padding: 10px;
|
||||
pointer-events: all;
|
||||
touch-action: auto;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
color: rgb(10, 10, 10);
|
||||
border-radius: 12px;
|
||||
border: 2px solid black;
|
||||
`;
|
||||
|
||||
const P = styled.p`
|
||||
white-space: pre-warp;
|
||||
`;
|
||||
|
||||
const BtnFlex = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
margin: 2px;
|
||||
padding: 5px;
|
||||
outline: none !important;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
color: rgb(10, 10, 10);
|
||||
border-radius: 12px;
|
||||
border: 2px solid black;
|
||||
`;
|
||||
|
||||
const Btn = styled.button`
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
color: rgb(10, 10, 10);
|
||||
border-radius: 12px;
|
||||
border: 2px solid black;
|
||||
|
||||
width: 100%;
|
||||
border-radius: none;
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
outline: none !important;
|
||||
&:hover {
|
||||
background-color: rgba(158, 158, 158, 0.75);
|
||||
}
|
||||
transition: background-color 250ms;
|
||||
`;
|
||||
|
||||
export class Popup {
|
||||
private static _ref: HTMLDivElement;
|
||||
private static active?: () => void;
|
||||
private static root: any;
|
||||
static alert(message: string, title?: string) {
|
||||
return new Promise<void>(resolve => {
|
||||
Popup.clear();
|
||||
const onClick = () => {
|
||||
resolve();
|
||||
this.active = undefined;
|
||||
this.clear();
|
||||
};
|
||||
this.active = onClick;
|
||||
|
||||
Popup.renderContent(
|
||||
<div>
|
||||
<h5>{title || `${location.host} says`}</h5>
|
||||
<P>{message}</P>
|
||||
<Btn onClick={() => onClick()}>OK</Btn>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
}
|
||||
static async confirm(message: string, title?: string): Promise<boolean> {
|
||||
return new Promise<boolean>(resolve => {
|
||||
Popup.clear();
|
||||
const onClick = (value: boolean) => {
|
||||
resolve(value);
|
||||
this.active = undefined;
|
||||
this.clear();
|
||||
};
|
||||
this.active = () => onClick(false);
|
||||
|
||||
Popup.renderContent(
|
||||
<div>
|
||||
<h5>{title || `${location.host} says`}</h5>
|
||||
<P>{message}</P>
|
||||
<BtnFlex>
|
||||
<Btn onClick={() => onClick(true)}>OK</Btn>
|
||||
<Btn onClick={() => onClick(false)}>Cancel</Btn>
|
||||
</BtnFlex>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
}
|
||||
static async prompt(message?: string, _default?: string, title?: string) {
|
||||
return new Promise<string | null>(resolve => {
|
||||
Popup.clear();
|
||||
const onClick = (value: string | null) => {
|
||||
resolve(value);
|
||||
this.active = undefined;
|
||||
this.clear();
|
||||
};
|
||||
this.active = () => onClick("");
|
||||
const inputRef = createRef<HTMLInputElement>();
|
||||
Popup.renderContent(
|
||||
<div>
|
||||
<h5>{title || `${location.host} says`}</h5>
|
||||
<P>{message}</P>
|
||||
<Input ref={inputRef} type='text' />
|
||||
<BtnFlex>
|
||||
<Btn onClick={() => onClick(inputRef.current && inputRef.current.value)}>OK</Btn>
|
||||
<Btn onClick={() => onClick(null)}>Cancel</Btn>
|
||||
</BtnFlex>
|
||||
</div>,
|
||||
);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.value = _default || "";
|
||||
inputRef.current.addEventListener("keyup", event => {
|
||||
if (event.key.toLowerCase() === "enter") {
|
||||
if (inputRef.current) {
|
||||
onClick(inputRef.current.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error("Missing reference");
|
||||
}
|
||||
});
|
||||
}
|
||||
static custom(
|
||||
element: JSX.Element,
|
||||
buttons?: {
|
||||
content: string | JSX.Element;
|
||||
click: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void | boolean | null | Promise<any>;
|
||||
}[],
|
||||
) {
|
||||
return new Promise<void>(resolve => {
|
||||
Popup.clear();
|
||||
const onClick = () => {
|
||||
this.active = undefined;
|
||||
resolve();
|
||||
this.clear();
|
||||
};
|
||||
this.active = () => onClick();
|
||||
Popup.renderContent(
|
||||
<div>
|
||||
{element}
|
||||
<BtnFlex>
|
||||
{buttons
|
||||
? buttons.map((e, i) => (
|
||||
<Btn
|
||||
key={i}
|
||||
onClick={async ev => {
|
||||
const raw = e.click(ev);
|
||||
let stayActive: any;
|
||||
if (raw instanceof Promise) {
|
||||
stayActive = await raw;
|
||||
} else {
|
||||
stayActive = raw;
|
||||
}
|
||||
|
||||
if (!stayActive) {
|
||||
this.active = undefined;
|
||||
this.clear();
|
||||
resolve();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{e.content}
|
||||
</Btn>
|
||||
))
|
||||
: null}
|
||||
</BtnFlex>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
}
|
||||
static close() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
private static clear() {
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
this.root = undefined;
|
||||
}
|
||||
// while (Popup.ref.children.length) {
|
||||
// const child = Popup._ref.children[0];
|
||||
// child.parentNode.removeChild(child);
|
||||
// }
|
||||
const style = Popup.ref.style;
|
||||
style.pointerEvents = "none";
|
||||
style.touchAction = "none";
|
||||
style.backgroundColor = "rgba(0, 0, 0, 0)";
|
||||
if (this.active) {
|
||||
this.active();
|
||||
}
|
||||
this.active = undefined;
|
||||
}
|
||||
|
||||
private static renderContent(content: JSX.Element) {
|
||||
const rootElement = Popup.createBase(content);
|
||||
this.root = createRoot(Popup.ref);
|
||||
this.root.render(rootElement);
|
||||
const style = Popup.ref.style;
|
||||
style.pointerEvents = "all";
|
||||
style.touchAction = "auto";
|
||||
style.backgroundColor = "rgba(0, 0, 0, 0.5)";
|
||||
}
|
||||
|
||||
private static createBase(content: JSX.Element) {
|
||||
return <Box>{content}</Box>;
|
||||
}
|
||||
|
||||
private static get ref() {
|
||||
if (Popup._ref) {
|
||||
return Popup._ref;
|
||||
}
|
||||
const ref = document.createElement("div");
|
||||
document.body.appendChild(ref);
|
||||
Popup._ref = ref;
|
||||
const style = ref.style;
|
||||
style.position = "fixed";
|
||||
style.zIndex = `${Number.MAX_SAFE_INTEGER}`;
|
||||
style.width = "100%";
|
||||
style.height = "100%";
|
||||
style.pointerEvents = "none";
|
||||
style.touchAction = "none";
|
||||
style.backgroundColor = "rgba(0, 0, 0, 0)";
|
||||
style.transition = "background-color 1s";
|
||||
style.overflowY = "auto";
|
||||
style.overflowX = "hidden";
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { createRef, ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
canvas: HTMLCanvasElement;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export class CanvasToCanvasJSX extends React.Component<{ canvas: HTMLCanvasElement, width?: number, height?: number }> {
|
||||
private ref = createRef<HTMLCanvasElement>();
|
||||
|
||||
override componentDidMount(): void {
|
||||
this.update();
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<Props> ) {
|
||||
if (prevProps.canvas !== this.props.canvas || prevProps.width !== this.props.width || prevProps.height !== this.props.height) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
update() {
|
||||
const canvas = this.ref.current!;
|
||||
const width = (canvas.width = this.props.width || this.props.canvas.width);
|
||||
const height = (canvas.height = this.props.height || this.props.canvas.height);
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.drawImage(this.props.canvas, 0, 0, width, height);
|
||||
}
|
||||
override render(): ReactNode {
|
||||
return <canvas ref={this.ref} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import { OverlayReturn, Store, StoreEvents } from "../../store";
|
||||
import { Storage } from "../../storage";
|
||||
import { Coordinates } from "../../coordinates";
|
||||
import { Palette } from "../../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";
|
||||
|
||||
|
||||
interface StoreSettings {
|
||||
opacity: number;
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
store: Store;
|
||||
storage: Storage;
|
||||
cords: Coordinates;
|
||||
palette: Palette;
|
||||
}
|
||||
|
||||
interface State extends StoreSettings{
|
||||
selected?: SelectedMural;
|
||||
overlays: number[];
|
||||
phantomOverlay: number;
|
||||
overlayModify?: OverlayReturn;
|
||||
}
|
||||
|
||||
export class Main extends React.Component<Props, State> {
|
||||
private readonly STORAGE_KEY = "__OPACITY_SETTINGS";
|
||||
private destroyed = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
overlays: [],
|
||||
phantomOverlay: -1,
|
||||
opacity: 50,
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.store.on(StoreEvents.Any, this.update);
|
||||
this.update();
|
||||
console.error(this.STORAGE_KEY);
|
||||
this.props.storage.getItem<StoreSettings>(this.STORAGE_KEY).then(data => {
|
||||
console.error(data, this.STORAGE_KEY, data !== null, !this.destroyed);
|
||||
if (data !== null && !this.destroyed) {
|
||||
console.error("loadingnn????????", data);
|
||||
this.setState(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.destroyed = true;
|
||||
this.actualSave({collapsed: this.state.collapsed, opacity: this.state .opacity});
|
||||
}
|
||||
actualSave(settings: StoreSettings) {
|
||||
return this.props.storage.setItem(this.STORAGE_KEY, settings);
|
||||
}
|
||||
|
||||
save = debounce((opacity: number) => {
|
||||
this.actualSave({collapsed: this.state.collapsed, opacity});
|
||||
}, 1000);
|
||||
|
||||
opacityChange = (opacity: number) => {
|
||||
this.setState({
|
||||
opacity
|
||||
});
|
||||
this.save(opacity);
|
||||
};
|
||||
|
||||
update = () => {
|
||||
this.setState({
|
||||
selected: this.props.store.selected,
|
||||
overlays: this.props.store.overlays,
|
||||
overlayModify: this.props.store.overlayModify,
|
||||
});
|
||||
};
|
||||
|
||||
renderMap() {
|
||||
if (this.state.selected) {
|
||||
return <MovableWindow title="Minimap" storage={this.props.storage} storageKey="minimap">
|
||||
<Minimap cords={this.props.cords} palette={this.props.palette} selected={this.state.selected} storage={this.props.storage} store={this.props.store} ></Minimap>
|
||||
</MovableWindow>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
render() {
|
||||
return <>
|
||||
<MovableWindow title="Overlay" storage={this.props.storage} storageKey="main" >
|
||||
<Menu
|
||||
onOpacityChange={this.opacityChange}
|
||||
cords={this.props.cords}
|
||||
store={this.props.store}
|
||||
storage={this.props.storage}
|
||||
palette={this.props.palette}
|
||||
opacity={this.state.opacity}
|
||||
collapsed={this.state.collapsed}
|
||||
onCollapsedChanged={collapsed => {
|
||||
this.setState({collapsed});
|
||||
this.actualSave({opacity: this.state.opacity, collapsed});
|
||||
}}
|
||||
/>
|
||||
</MovableWindow>
|
||||
{this.renderMap()}
|
||||
{this.state.overlays.map((o, i) => <Overlay key={i} storage={this.props.storage} opacity={this.state.opacity} cords={this.props.cords} palette={this.props.palette} mural={this.props.store.murals[o]}/> )}
|
||||
{this.props.store.murals[this.state.phantomOverlay] ? <Overlay storage={this.props.storage} opacity={50} cords={this.props.cords} palette={this.props.palette} mural={this.props.store.murals[this.state.phantomOverlay]}/> : null }
|
||||
{this.state.overlayModify ? <Overlay muralObj={this.state.overlayModify.muralObj} opacity={this.state.opacity} storage={this.props.storage} cords={this.props.cords} palette={this.props.palette} mural={this.props.store.overlayModify!.pixels} onChange={(name, x, y, confirm) => {
|
||||
this.state.overlayModify!.cb(name, x, y, confirm);
|
||||
}} /> : null}
|
||||
</>;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { faCaretDown, faCaretUp, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Btn, Flex } from "../styles";
|
||||
import { Store } from "../../store";
|
||||
import { MuralList } from "./muralList";
|
||||
import { Coordinates } from "../../coordinates";
|
||||
import { Palette } from "../../palette";
|
||||
import { importArtWork } from "../importMural";
|
||||
import { Popup } from "./Popup";
|
||||
import { Storage } from "../../storage";
|
||||
|
||||
const Container = styled.div`
|
||||
|
||||
`;
|
||||
|
||||
|
||||
const PercentageDiv = styled.div`
|
||||
margin: 2px 4px;
|
||||
`;
|
||||
|
||||
export const InputRange = styled.input`
|
||||
&[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&[type="range"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[type="range"]::-webkit-slider-runnable-track {
|
||||
background-color: rgba(10, 10, 10);
|
||||
border-radius: 0;
|
||||
height: 4px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-top: -12px;
|
||||
|
||||
background-color: black;
|
||||
height: 25px;
|
||||
width: 15px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&[type="range"]::-moz-range-track {
|
||||
background-color: rgba(10, 10, 10);
|
||||
border-radius: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&[type="range"]::-moz-range-thumb {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: rgba(10, 10, 10);
|
||||
height: 2rem;
|
||||
width: 1rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
`;
|
||||
interface Props {
|
||||
store: Store;
|
||||
cords: Coordinates;
|
||||
palette: Palette;
|
||||
storage: Storage;
|
||||
opacity: number;
|
||||
collapsed: boolean;
|
||||
onOpacityChange: (n: number) => void;
|
||||
onCollapsedChanged: (b: boolean) => void;
|
||||
}
|
||||
|
||||
export class Menu extends React.Component<Props> {
|
||||
|
||||
import = async () => {
|
||||
try {
|
||||
const mural = await importArtWork(this.props.store, this.props.cords, this.props.palette);
|
||||
if (mural) {
|
||||
this.props.store.add(mural);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
Popup.alert(error.name);
|
||||
}
|
||||
};
|
||||
|
||||
inputElement = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const percentage = parseInt((event.target as HTMLInputElement).value);
|
||||
this.props.onOpacityChange(percentage);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Container>
|
||||
<Flex>
|
||||
<Btn onClick={this.import}><FontAwesomeIcon icon={ faUpload } /> Import</Btn>
|
||||
<InputRange type="range" min={0} max={100} value={this.props.opacity} onInput={this.inputElement} />
|
||||
<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}/> }
|
||||
</Container>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Coordinates, CordType } from "../../coordinates";
|
||||
import { SelectedMural } from "../../interfaces";
|
||||
import React from "react";
|
||||
import { Btn, SELECTED_COLOR } from "../styles";
|
||||
import { faCrosshairs, faMagnifyingGlassMinus, faMagnifyingGlassPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import styled from "styled-components";
|
||||
import { createCanvas, drawPixelsOntoCanvas } from "../../utils";
|
||||
import { Palette } from "../../palette";
|
||||
import { clamp, debounce } from "lodash";
|
||||
import { Store } from "../../store";
|
||||
import { Storage } from "../../storage";
|
||||
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const ColorBox = styled.div`
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
border: 1px solid black;
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
`;
|
||||
|
||||
const Scale = styled.span`
|
||||
width: 25px;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
const Canvas = styled.canvas`
|
||||
border-radius: 12px;
|
||||
border: 2px solid black;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
selected: SelectedMural;
|
||||
cords: Coordinates;
|
||||
store: Store;
|
||||
storage: Storage;
|
||||
palette: Palette;
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
size: number;
|
||||
color: number;
|
||||
colorAssistant: boolean;
|
||||
}
|
||||
|
||||
interface StorageSettings {
|
||||
colorAssistant: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export class Minimap extends React.Component<Props, Stats> {
|
||||
private ref = React.createRef<HTMLCanvasElement>();
|
||||
private cache = new Map<number, HTMLCanvasElement>();
|
||||
private transparentGrid = ["#c5c5c540", "#8d8d8d40"];
|
||||
private highlighColor = "#00000040";
|
||||
private lastColor = -1;
|
||||
private storageKey = "_minimap_settings";
|
||||
private destroyed = false;
|
||||
|
||||
constructor(props:Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
size: 16,
|
||||
color: -1,
|
||||
colorAssistant: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate ( prevProps: Readonly<Props>, prevState: Readonly<Stats>): void {
|
||||
if (prevProps.selected.m.ref !== this.props.selected.m.ref) {
|
||||
this.cache.clear();
|
||||
this.draw();
|
||||
} else if (prevState.size !== this.state.size) {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private click = debounce((colorIndex: number) => {
|
||||
if (colorIndex >= 0 && colorIndex < this.props.palette.palette.length) {
|
||||
if (!this.props.palette.buttons[colorIndex].classList.contains("outline-2")) {
|
||||
if (this.state.colorAssistant) {
|
||||
this.props.palette.buttons[colorIndex].click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
|
||||
|
||||
componentDidMount () {
|
||||
this.props.cords.on(CordType.Div, this.update);
|
||||
this.props.cords.on(CordType.Div, this.onCords);
|
||||
this.props.storage.getItem<StorageSettings>(this.storageKey).then(settings => {
|
||||
if (settings && !this.destroyed) {
|
||||
this.setState({
|
||||
colorAssistant: settings.colorAssistant,
|
||||
size: settings.size
|
||||
});
|
||||
}
|
||||
});
|
||||
this.draw();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.destroyed = true;
|
||||
this.props.cords.off(CordType.Div, this.update);
|
||||
this.props.cords.off(CordType.Div, this.onCords);
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
this.props.storage.setItem(this.storageKey, {
|
||||
colorAssistant: this.state.colorAssistant,
|
||||
size: this.state.size
|
||||
});
|
||||
}
|
||||
|
||||
onCords = (x: number, y: number) => {
|
||||
const store = this.props.store;
|
||||
if (store.selected && store.selected.m.x < x && store.selected.m.y < y && store.selected.w + store.selected.m.x > x && store.selected.h + store.selected.m.y > y) {
|
||||
const xx = x - store.selected.m.x;
|
||||
const yy = y - store.selected.m.y;
|
||||
const colorIndex = store.selected.m.pixels[yy][xx];
|
||||
if (this.lastColor !== colorIndex) {
|
||||
this.lastColor = colorIndex;
|
||||
this.click(colorIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
drawGrid(ctx: CanvasRenderingContext2D, gridSize: number, width: number, height: number) {
|
||||
for (let y = 0; y < height; y += gridSize) {
|
||||
for (let x = 0; x < width; x += gridSize) {
|
||||
const isEvenRow = Math.floor(y / gridSize) % 2 === 0;
|
||||
const isEvenColumn = Math.floor(x / gridSize) % 2 === 0;
|
||||
const isEvenCell = (isEvenRow && isEvenColumn) || (!isEvenRow && !isEvenColumn);
|
||||
|
||||
ctx.fillStyle = isEvenCell ? this.transparentGrid[0] : this.transparentGrid[1];
|
||||
ctx.fillRect(x, y, gridSize, gridSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update = () => {
|
||||
const m = this.props.selected.m;
|
||||
const xx = this.props.cords.x - m.x;
|
||||
const yy = this.props.cords.y - m.y;
|
||||
|
||||
if (m.pixels[yy] !== undefined) {
|
||||
if (m.pixels[yy][xx] !== undefined) {
|
||||
const colorIndex = m.pixels[yy]![xx]!;
|
||||
this.setState({
|
||||
color: colorIndex
|
||||
});
|
||||
this.draw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
color: -1,
|
||||
});
|
||||
this.draw();
|
||||
};
|
||||
|
||||
getCachedImage(size: number) {
|
||||
const image = this.cache.get(size);
|
||||
if (image) {
|
||||
return image;
|
||||
}
|
||||
|
||||
const s = this.props.selected;
|
||||
const canvas = createCanvas();
|
||||
drawPixelsOntoCanvas(canvas, s.m.pixels, this.props.palette.hex, size);
|
||||
this.cache.set(size, canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
draw = () => {
|
||||
const canvas = this.ref.current!;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const w = canvas.width = 200;
|
||||
const h = canvas.height = 200;
|
||||
const hh = w / 2;
|
||||
const hw = h / 2;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
const s = this.state.size;
|
||||
this.drawGrid(ctx, s * 4, w, h);
|
||||
|
||||
const pixelSize = (1 * s) / 2;
|
||||
const dx = ((this.props.selected.m.x - this.props.cords.x) * s) + Math.ceil(hh - pixelSize);
|
||||
const dy = ((this.props.selected.m.y - this.props.cords.y) * s) + Math.ceil(hw - pixelSize);
|
||||
//const img = this.props.selected.m.ref;
|
||||
const img = this.getCachedImage(s);
|
||||
|
||||
ctx.drawImage(img, dx, dy, img.width, img.height);
|
||||
if (s != 1) {
|
||||
ctx.fillStyle = this.highlighColor;
|
||||
const halfHO = (h / 2);
|
||||
const halfWO = (h / 2);
|
||||
ctx.fillRect(0, 0, w, halfHO - pixelSize);
|
||||
ctx.fillRect(0, halfHO + pixelSize, w, halfHO - pixelSize);
|
||||
ctx.fillRect(0, halfHO - pixelSize, halfWO - pixelSize, pixelSize * 2);
|
||||
ctx.fillRect(halfWO + pixelSize, halfHO - pixelSize, halfWO - pixelSize, pixelSize * 2);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
up = () => {
|
||||
this.setState({
|
||||
size: this.clamp(this.state.size * 2)
|
||||
});
|
||||
};
|
||||
|
||||
down = () => {
|
||||
this.setState({
|
||||
size: this.clamp(this.state.size / 2)
|
||||
});
|
||||
};
|
||||
setSize(size: number) {
|
||||
this.setState({size});
|
||||
}
|
||||
|
||||
clamp(size: number) {
|
||||
return clamp(size, 1, 32);
|
||||
}
|
||||
|
||||
renderColor() {
|
||||
if (this.state.color !== -1) {
|
||||
return <ColorBox style={{backgroundColor: this.props.palette.hex[this.state.color]}}></ColorBox>;
|
||||
} else {
|
||||
return <ColorBox> / </ColorBox>;
|
||||
}
|
||||
}
|
||||
toggleColorAssistant = () => {
|
||||
this.setState({
|
||||
colorAssistant: !this.state.colorAssistant,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<Flex>
|
||||
<Btn onClick={this.up}><FontAwesomeIcon icon={ faMagnifyingGlassPlus }></FontAwesomeIcon></Btn>
|
||||
<Scale>{this.state.size}</Scale>
|
||||
<Btn onClick={this.down}><FontAwesomeIcon icon={ faMagnifyingGlassMinus }></FontAwesomeIcon></Btn>
|
||||
<span style={{flex: 1}}>
|
||||
<Btn title="Color assistant" style={{ borderColor: this.state.colorAssistant ? SELECTED_COLOR : ""} } onClick={() => this.toggleColorAssistant()}>Assist<FontAwesomeIcon icon={faCrosshairs} /> </Btn>
|
||||
</span>
|
||||
{this.renderColor()}
|
||||
</Flex>
|
||||
<Canvas ref={this.ref} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import React, { createRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Point } from "../../interfaces";
|
||||
import { clamp } from "lodash";
|
||||
import { Storage } from "../../storage";
|
||||
|
||||
const Movable = styled.div`
|
||||
position: fixed;
|
||||
padding-top: 20px;
|
||||
padding: 5px;
|
||||
z-index: 2;
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
color: rgb(10, 10, 10);
|
||||
border-radius: 12px;
|
||||
border: 2px solid black;
|
||||
`;
|
||||
|
||||
const DragArea = styled.div`
|
||||
border: 1px black dotted;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
storage: Storage;
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
x: number;
|
||||
y: number;
|
||||
moving: boolean;
|
||||
}
|
||||
|
||||
interface PointEx extends Point {
|
||||
xOff: number;
|
||||
yOff: number;
|
||||
}
|
||||
|
||||
export class MovableWindow extends React.Component<Props, State> {
|
||||
private ref = createRef<HTMLDivElement>();
|
||||
private dragRef = createRef<HTMLDivElement>();
|
||||
private moving?: PointEx;
|
||||
private destroyed = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
x: -window.innerWidth,
|
||||
y: -window.innerHeight,
|
||||
moving: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener("mousemove", this.onMouseMove);
|
||||
window.addEventListener("mouseup", this.onMouseUp);
|
||||
window.addEventListener("resize", this.fixBounds);
|
||||
this.props.storage.getItem<Point>(this.storageKey).then(point => {
|
||||
if (!this.destroyed && point) {
|
||||
this.setState({x: point.x, y: point.y});
|
||||
} else {
|
||||
this.setState({x: 0, y: 0});
|
||||
}
|
||||
this.fixBounds();
|
||||
});
|
||||
}
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener("mousemove", this.onMouseMove);
|
||||
window.removeEventListener("mouseup", this.onMouseUp);
|
||||
window.removeEventListener("resize", this.fixBounds);
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
onTouchMove = (event: TouchEvent) => {
|
||||
if (!this.moving) return;
|
||||
const lastTouch = event.touches[event.touches.length - 1];
|
||||
const div = this.dragRef.current!;
|
||||
const { x, y, width, height} = div.getBoundingClientRect();
|
||||
const offsetX = (lastTouch.clientX - x) / width * div.offsetWidth;
|
||||
const offsetY = (lastTouch.clientY - y) / height * div.offsetHeight;
|
||||
this.setMoving(lastTouch.clientX, lastTouch.clientY, offsetX, offsetY);
|
||||
};
|
||||
|
||||
setMoving = (clientX: number, clientY: number, offsetX: number, offsetY: number) => {
|
||||
this.moving = {
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
xOff: offsetX,
|
||||
yOff: offsetY,
|
||||
};
|
||||
};
|
||||
|
||||
private onMouseMove = (event: MouseEvent) => {
|
||||
if (!this.moving) return;
|
||||
if (!(event instanceof MouseEvent) || !event.isTrusted || event.ctrlKey || event.altKey || event.metaKey){
|
||||
return;
|
||||
}
|
||||
const { clientX, clientY } = event;
|
||||
this.handleMoveLogic(clientX, clientY);
|
||||
};
|
||||
private onMouseUp = () => {
|
||||
this.moving = undefined;
|
||||
this.setState({moving: false});
|
||||
};
|
||||
private fixBounds = () => {
|
||||
// if(this.shown) {
|
||||
// const {left, top, width, height} = this.container.getBoundingClientRect();
|
||||
// const s = this.container.style;
|
||||
// if (width > window.innerWidth || height > window.innerHeight) {
|
||||
// const padding = 10;
|
||||
// if (width > window.innerWidth) {
|
||||
// s.width = `${window.innerWidth - padding}px`;
|
||||
// }
|
||||
// if (height > window.innerHeight) {
|
||||
// s.height = `${window.innerHeight - padding}px`;
|
||||
// }
|
||||
// s.overflow = "auto";
|
||||
// } else {
|
||||
// s.width = "";
|
||||
// s.height = "";
|
||||
// s.overflow = "";
|
||||
// if (left < 0) {
|
||||
// s.left = `0px`;
|
||||
// }
|
||||
// if (top < 0) {
|
||||
// s.top = `0px`;
|
||||
// }
|
||||
// if(left + width > window.innerWidth) {
|
||||
// s.left = `${window.innerWidth - width}px`;
|
||||
// }
|
||||
// if (top + height > window.innerHeight) {
|
||||
// s.top = `${window.innerHeight - height}px`;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
private handleMoveLogic(clientX: number, clientY: number) {
|
||||
const ref = this.dragRef.current?.getBoundingClientRect();
|
||||
if (ref && this.moving) {
|
||||
const xx = clamp(clientX, this.moving.xOff, window.innerWidth - ref!.width + this.moving.xOff);
|
||||
const yy = clamp(clientY, this.moving.yOff, window.innerHeight - ref!.height + this.moving.yOff) ;
|
||||
|
||||
const x = xx - this.moving!.xOff;
|
||||
const y = yy - this.moving!.yOff;
|
||||
const point = { x, y };
|
||||
this.props.storage.setItem(this.storageKey, point);
|
||||
this.setState(point);
|
||||
}
|
||||
}
|
||||
get storageKey() {
|
||||
return `__storage__${this.props.storageKey}`;
|
||||
}
|
||||
render() {
|
||||
return <Movable ref={this.ref} style={{left: `${this.state.x}px`, top: `${this.state.y}px`}}>
|
||||
<DragArea ref={this.dragRef} style={{cursor: this.state.moving ? "grabbing" : ""}} onMouseDown={event => {
|
||||
this.moving = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
xOff: event.nativeEvent.offsetX,
|
||||
yOff: event.nativeEvent.offsetY,
|
||||
};
|
||||
this.setState({moving: true});
|
||||
|
||||
}}> <h2>{this.props.title}</h2> </DragArea>
|
||||
|
||||
|
||||
{this.props.children}
|
||||
</Movable>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { Border, Btn, Flex, Input } from "../styles";
|
||||
import { processNumberEvent } from "../../utils";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
onName: (name: string) => void;
|
||||
onY: (y: number) => void;
|
||||
onX: (x: number) => void;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export class MuralEditor extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
return <Border>
|
||||
<div>
|
||||
<span>Name</span> <Input type="text" value={this.props.name} onChange={e => this.props.onName(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<span>X</span> <Input type="number" value={this.props.x} onChange={e => processNumberEvent(e, this.props.onX)}/>
|
||||
</div>
|
||||
<div>
|
||||
<span>Y</span> <Input type="number" value={this.props.y} onChange={e => processNumberEvent(e, this.props.onY)} />
|
||||
</div>
|
||||
<Flex>
|
||||
<Btn onClick={() => this.props.onConfirm()} style={{flex: 1}}>Confirm</Btn><Btn onClick={() => this.props.onCancel()} style={{flex: 1}}>Cancel</Btn>
|
||||
</Flex>
|
||||
<small>Overlay does not work on scale -1. To fix that zoom in.</small>
|
||||
</Border>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { MuralEx, SelectedMural } from "../../interfaces";
|
||||
import { Store, StoreEvents } from "../../store";
|
||||
import { MuralView } from "./muralView";
|
||||
import styled from "styled-components";
|
||||
import { Coordinates } from "../../coordinates";
|
||||
|
||||
const ScrollContainer = styled.div`
|
||||
overflow: auto;
|
||||
max-height: 250pt;
|
||||
min-width: 200pt;
|
||||
border-radius: 12px;
|
||||
&::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border: 1px solid black;
|
||||
border-radius: 12px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(10, 10, 10);
|
||||
border-radius: 12px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
store: Store;
|
||||
cords: Coordinates;
|
||||
}
|
||||
|
||||
interface State {
|
||||
murals: MuralEx[];
|
||||
selected?: SelectedMural
|
||||
}
|
||||
|
||||
export class MuralList extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
murals: [],
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.props.store.on(StoreEvents.Any, this.update);
|
||||
this.update();
|
||||
}
|
||||
componentWillUnmount (){
|
||||
this.props.store.off(StoreEvents.Any, this.update);
|
||||
}
|
||||
update = () => {
|
||||
this.setState({
|
||||
murals: this.props.store.murals,
|
||||
selected: this.props.store.selected,
|
||||
});
|
||||
};
|
||||
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} />;
|
||||
})}</ScrollContainer>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import React from "react";
|
||||
import { Store } from "../../store";
|
||||
import { Mural, MuralEx } from "../../interfaces";
|
||||
import styled from "styled-components";
|
||||
import { Border, Btn, Flex, SELECTED_COLOR } from "../styles";
|
||||
import { CanvasToCanvasJSX } from "./canvasToCanvasJSX";
|
||||
import { formatNumber, getMuralHeight, getMuralWidth } from "../../utils";
|
||||
import { IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Coordinates } from "../../coordinates";
|
||||
import { TEXT_FORMATS } from "../importMural";
|
||||
import saveAs from "file-saver";
|
||||
|
||||
const Margin = styled.div`
|
||||
margin: 2px;
|
||||
margin-left: 6px;
|
||||
`;
|
||||
|
||||
const H5 = styled.h5`
|
||||
font-weight: bolder;
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
const Line = styled.div`
|
||||
font-size: 10pt;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
mural: MuralEx;
|
||||
selected: boolean;
|
||||
store: Store;
|
||||
cords: Coordinates;
|
||||
}
|
||||
|
||||
interface State {
|
||||
width: number;
|
||||
height: number;
|
||||
overlay: boolean;
|
||||
}
|
||||
|
||||
|
||||
export class MuralView extends React.Component<Props, State> {
|
||||
private readonly MAX_SIZE = 100;
|
||||
constructor(props:Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: this.MAX_SIZE,
|
||||
width: this.MAX_SIZE,
|
||||
overlay: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.updateSize();
|
||||
}
|
||||
componentDidUpdate ( prevProps: Readonly<Props> ) {
|
||||
if (this.props.mural.ref !== prevProps.mural.ref) {
|
||||
this.updateSize();
|
||||
}
|
||||
}
|
||||
|
||||
updateSize = () => {
|
||||
const w = this.props.mural.ref.width;
|
||||
const h = this.props.mural.ref.height;
|
||||
|
||||
let width = w;
|
||||
let height = h;
|
||||
if (w > h) {
|
||||
if (w > this.MAX_SIZE) {
|
||||
height = this.MAX_SIZE * (h / w);
|
||||
width = this.MAX_SIZE;
|
||||
}
|
||||
} else {
|
||||
if (h > this.MAX_SIZE) {
|
||||
width = this.MAX_SIZE * (w / h);
|
||||
height = this.MAX_SIZE;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
height, width
|
||||
});
|
||||
};
|
||||
renderInfoLine(title: string, description: string) {
|
||||
return <Line><H5>{title}:</H5><span> {description}</span></Line>;
|
||||
}
|
||||
|
||||
btn(context: string, icon: IconDefinition, onClick: () => void, selected?: boolean) {
|
||||
return <Btn style={{ borderColor: selected ? SELECTED_COLOR : "" }} onClick={() => onClick()}>{context} <FontAwesomeIcon icon={icon} /> </Btn>;
|
||||
}
|
||||
get s() {
|
||||
return this.props.store;
|
||||
}
|
||||
|
||||
onLoad = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
const tr = event.target as HTMLElement;
|
||||
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 );
|
||||
}
|
||||
}
|
||||
};
|
||||
onModify = async () => {
|
||||
const mural = this.props.mural;
|
||||
this.props.store.setOverlayModify({
|
||||
pixels: this.props.mural.pixels,
|
||||
cb: (name, x, y, confirm) => {
|
||||
if (confirm) {
|
||||
mural.name = name;
|
||||
mural.x = x;
|
||||
mural.y = y;
|
||||
this.props.store.updateMural(mural);
|
||||
}
|
||||
},
|
||||
muralObj: {
|
||||
name: this.props.mural.name,
|
||||
x: this.props.mural.x,
|
||||
y: this.props.mural.y
|
||||
}
|
||||
});
|
||||
|
||||
// let x: number | null = null;
|
||||
// let y: number | null = null;
|
||||
// await Popup.custom(<div>
|
||||
// <h5>Enter new coordinates</h5>
|
||||
// <div>
|
||||
// <span>X:</span>
|
||||
// <Input type="number" min={0} placeholder={"0"} onChange={ev => processNumberEvent(ev, n => {x = n})} />
|
||||
// </div>
|
||||
// <div>
|
||||
// <span>Y:</span>
|
||||
// <Input type="number" min={0} placeholder={"10000"} onChange={ev => processNumberEvent(ev, n => {y = n})} />
|
||||
// </div>
|
||||
// </div>, [
|
||||
// {
|
||||
// content: "Confirm",
|
||||
// click: () => {
|
||||
// Popup.close();
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// content: "Close",
|
||||
// click: () => {
|
||||
// x = null;
|
||||
// y = null;
|
||||
// Popup.close();
|
||||
// }
|
||||
// }
|
||||
// ]);
|
||||
|
||||
// if (typeof x === "number" && typeof y === "number") {
|
||||
// this.props.mural.x = x;
|
||||
// this.props.mural.y = y;
|
||||
// this.props.store.updateMural(this.props.mural);
|
||||
// }
|
||||
};
|
||||
onExport = () => {
|
||||
const rawMural: Mural = {
|
||||
name: this.props.mural.name,
|
||||
x: this.props.mural.x,
|
||||
y: this.props.mural.y,
|
||||
pixels: this.props.mural.pixels,
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(rawMural)], { type: "application/json;charset=utf-8" });
|
||||
saveAs(blob, `${rawMural.name}.${TEXT_FORMATS[0]}`);
|
||||
};
|
||||
onDelete = () => {
|
||||
this.s.remove(this.props.mural);
|
||||
};
|
||||
onEnter = () => {
|
||||
this.props.store.addPhantomOverlay(this.props.mural);
|
||||
};
|
||||
onLeave = () => {
|
||||
this.props.store.removePhantomOverlay();
|
||||
};
|
||||
|
||||
onGoto = () => {
|
||||
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 url = `${origin}/@${x},${y},${this.props.cords.uScale}`;
|
||||
location.href = url;
|
||||
};
|
||||
|
||||
onPreview = () => {
|
||||
if (this.props.store.hasOverlay(this.props.mural)) {
|
||||
this.props.store.removeOverlay(this.props.mural);
|
||||
} else {
|
||||
this.props.store.addOverlay(this.props.mural);
|
||||
}
|
||||
};
|
||||
|
||||
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)}`)}
|
||||
</Margin>
|
||||
</Flex>
|
||||
<Flex>
|
||||
{this.btn("Preview", faLayerGroup, this.onPreview, this.props.store.hasOverlay(this.props.mural))}
|
||||
{this.btn("Modify", faPenToSquare, this.onModify)}
|
||||
{this.btn("Export", faDownload, this.onExport)}
|
||||
{this.btn("Delete", faTrash, this.onDelete)}
|
||||
{this.btn("Goto", faLocation, this.onGoto)}
|
||||
</Flex>
|
||||
</Border>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Mural, MuralEx } from "../../interfaces";
|
||||
import { canvasFromMural, get2DArrHeight, get2DArrWidth } from "../../utils";
|
||||
import { Coordinates, CordType } from "../../coordinates";
|
||||
import { Palette } from "../../palette";
|
||||
import { MovableWindow } from "./movableWindow";
|
||||
import { Storage } from "../../storage";
|
||||
import { MuralEditor } from "./muralEditor";
|
||||
|
||||
const Canvas = styled.canvas`
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
mural: MuralEx | Mural | number[][];
|
||||
muralObj?: Partial<Mural>;
|
||||
onChange?: (name: string, x: number, y:number, confirm?: boolean) => void;
|
||||
cords: Coordinates;
|
||||
storage: Storage;
|
||||
palette: Palette;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export class Overlay extends React.Component<Props, State> {
|
||||
private ref = React.createRef<HTMLCanvasElement>();
|
||||
private _refImage?: HTMLCanvasElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
this.props.cords.on(CordType.Url, this.draw);
|
||||
window.addEventListener("mousemove", this.draw);
|
||||
if (this.props.muralObj) {
|
||||
this.setState({
|
||||
name: this.props.muralObj.name || "",
|
||||
x: this.props.muralObj.x ?? 0,
|
||||
y: this.props.muralObj.y ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.cords.off(CordType.Url, this.draw);
|
||||
window.removeEventListener("mousemove", this.draw);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||
if (prevProps.mural !== this.props.mural) {
|
||||
this._refImage = undefined;
|
||||
this.setState({
|
||||
x: 0, y: 0
|
||||
});
|
||||
}
|
||||
if (prevState.x !== this.state.x || prevState.y !== this.state.y) {
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
get x() {
|
||||
if (this.isPixels(this.props.mural)) {
|
||||
return this.state.x;
|
||||
} else {
|
||||
return this.props.mural.x;
|
||||
}
|
||||
}
|
||||
|
||||
get y() {
|
||||
if (this.isPixels(this.props.mural)) {
|
||||
return this.state.y;
|
||||
} else {
|
||||
return this.props.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;
|
||||
}
|
||||
|
||||
draw = () => {
|
||||
const canvas = this.ref.current!;
|
||||
const scale = Math.pow(2, this.props.cords.uScale);
|
||||
const cords = this.props.cords.gridToScreen(this.x, this.y)!;
|
||||
if (!cords) {
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
return;
|
||||
}
|
||||
const { x, y } = cords;
|
||||
|
||||
const width = get2DArrWidth(this.pixels) * scale;
|
||||
const height = get2DArrHeight(this.pixels) * scale;
|
||||
if (width !== canvas.width || height !== canvas.height ) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
}
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
canvas.style.left = `${x}px`;
|
||||
canvas.style.top = `${y}px`;
|
||||
|
||||
ctx.drawImage(this.refImg, 0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
|
||||
private get style(): React.CSSProperties {
|
||||
return { left: this.state.x, top: this.state.y, opacity: this.props.opacity / 100};
|
||||
}
|
||||
|
||||
renderModifyWindow() {
|
||||
if (this.props.onChange) {
|
||||
const copy = {...this.state};
|
||||
return <MovableWindow storage={this.props.storage} storageKey="overlay-set" title="Position editor">
|
||||
<MuralEditor
|
||||
name={this.state.name}
|
||||
x={this.state.x}
|
||||
y={this.state.y}
|
||||
onX={x => this.setState({x})}
|
||||
onY={y => this.setState({y})}
|
||||
onName={name => this.setState({name})}
|
||||
onCancel={() => {
|
||||
this.setState(copy);
|
||||
this.props.onChange!(copy.name, copy.x, copy.y, false);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
this.props.onChange!(this.state.name, this.state.x, this.state.y, true);
|
||||
|
||||
}}
|
||||
></MuralEditor>
|
||||
</MovableWindow>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<Canvas ref={this.ref} style={this.style} />
|
||||
{this.renderModifyWindow()}
|
||||
</>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user