Add screenshot feature
This commit is contained in:
Vendored
+51
-51
File diff suppressed because one or more lines are too long
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pixelcanvas-overlay",
|
"name": "pixelcanvas-overlay",
|
||||||
"version": "1.0.0",
|
"version": "1.0.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pixelcanvas-overlay",
|
"name": "pixelcanvas-overlay",
|
||||||
"version": "1.0.0",
|
"version": "1.0.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pixelcanvas-overlay",
|
"name": "pixelcanvas-overlay",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Pixel canvas overlay",
|
"description": "Pixel canvas overlay",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "0xa663",
|
"authors": ["0xa663", "Terncode"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { faCaretDown, faCaretUp, faUpload } from "@fortawesome/free-solid-svg-icons";
|
import { faCamera, faCaretDown, faCaretUp, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Btn, Flex } from "../styles";
|
import { Btn, Flex } from "../styles";
|
||||||
import { Store } from "../../store";
|
import { Store } from "../../store";
|
||||||
import { MuralList } from "./muralList";
|
import { MuralList } from "./muralList";
|
||||||
import { Coordinates } from "../../coordinates";
|
import { Coordinates, CordType } from "../../coordinates";
|
||||||
import { Palette } from "../../palette";
|
import { Palette } from "../../palette";
|
||||||
import { importArtWork } from "../importMural";
|
import { importArtWork } from "../importMural";
|
||||||
import { Popup } from "./Popup";
|
import { Popup } from "./Popup";
|
||||||
import { Storage } from "../../storage";
|
import { Storage } from "../../storage";
|
||||||
|
import { takeCanvasShot } from "../../canvashot";
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
|
||||||
@@ -78,8 +79,31 @@ interface Props {
|
|||||||
onOpacityChange: (n: number) => void;
|
onOpacityChange: (n: number) => void;
|
||||||
onCollapsedChanged: (b: boolean) => void;
|
onCollapsedChanged: (b: boolean) => void;
|
||||||
}
|
}
|
||||||
|
interface State {
|
||||||
|
takingCanvasShot: boolean;
|
||||||
|
canvasShotAvailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class Menu extends React.Component<Props> {
|
export class Menu extends React.Component<Props, State> {
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
takingCanvasShot: false,
|
||||||
|
canvasShotAvailable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.props.cords.on(CordType.Url, this.checkIsScreenShotAvailable)
|
||||||
|
}
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
this.props.cords.off(CordType.Url, this.checkIsScreenShotAvailable)
|
||||||
|
}
|
||||||
|
checkIsScreenShotAvailable = () => {
|
||||||
|
this.setState({canvasShotAvailable: this.props.cords.uScale >= 0})
|
||||||
|
console.log(this.props.cords.uScale);
|
||||||
|
}
|
||||||
|
|
||||||
import = async () => {
|
import = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -97,11 +121,20 @@ export class Menu extends React.Component<Props> {
|
|||||||
const percentage = parseInt((event.target as HTMLInputElement).value);
|
const percentage = parseInt((event.target as HTMLInputElement).value);
|
||||||
this.props.onOpacityChange(percentage);
|
this.props.onOpacityChange(percentage);
|
||||||
};
|
};
|
||||||
|
screenshot = async () => {
|
||||||
|
if (this.props.cords.uScale < 0) {
|
||||||
|
//Popup.alert("Cannot take screenshot at sc")
|
||||||
|
//return;
|
||||||
|
}
|
||||||
|
this.setState({takingCanvasShot: true});
|
||||||
|
await takeCanvasShot(this.props.cords);
|
||||||
|
this.setState({takingCanvasShot: false});
|
||||||
|
};
|
||||||
render() {
|
render() {
|
||||||
return <Container>
|
return <Container>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Btn onClick={this.import}><FontAwesomeIcon icon={ faUpload } /> Import</Btn>
|
<Btn onClick={this.screenshot} disabled={this.state.takingCanvasShot || !this.state.canvasShotAvailable} title="screenshot"><FontAwesomeIcon icon={ faCamera } /></Btn>
|
||||||
|
<Btn onClick={this.import} title="Import"><FontAwesomeIcon icon={ faUpload } /></Btn>
|
||||||
<InputRange type="range" min={0} max={100} value={this.props.opacity} onInput={this.inputElement} />
|
<InputRange type="range" min={0} max={100} value={this.props.opacity} onInput={this.inputElement} />
|
||||||
<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>
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import saveAs from "file-saver";
|
||||||
|
import { createCanvas, CHUNK_SIZE, fetchTile } from "./utils";
|
||||||
|
import { Coordinates } from "./coordinates";
|
||||||
|
import { Rect } from "./interfaces";
|
||||||
|
import { Popup } from "./UI/components/Popup";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function getSelectionArea() {
|
||||||
|
return new Promise<Rect>(resolve => {
|
||||||
|
const curtain = document.createElement("div");
|
||||||
|
const selectionSquare = document.createElement("div");
|
||||||
|
const cs = curtain.style;
|
||||||
|
cs.position = "fixed";
|
||||||
|
cs.zIndex = "100000";
|
||||||
|
cs.left = cs.top = "0";
|
||||||
|
cs.width = `${window.innerWidth}px`;
|
||||||
|
cs.height = `${window.innerHeight}px`;
|
||||||
|
document.body.appendChild(curtain);
|
||||||
|
cs.backgroundColor = "rgba(0, 0, 0, 0.50)";
|
||||||
|
|
||||||
|
const sss = selectionSquare.style;
|
||||||
|
sss.position = "fixed";
|
||||||
|
sss.zIndex = "100001";
|
||||||
|
sss.width = `5px`;
|
||||||
|
sss.height = `5px`;
|
||||||
|
sss.border = "2px solid white";
|
||||||
|
|
||||||
|
let moving: { x: number, y: number, xOff: number, yOff: number } | undefined = undefined;
|
||||||
|
|
||||||
|
const setMoving = (clientX: number, clientY: number, offsetX: number, offsetY: number) => {
|
||||||
|
moving = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY,
|
||||||
|
xOff: offsetX,
|
||||||
|
yOff: offsetY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (event: MouseEvent) => {
|
||||||
|
setMoving(event.clientX, event.clientY, event.offsetX, event.offsetY);
|
||||||
|
};
|
||||||
|
const onTouchStart = (event: TouchEvent) => {
|
||||||
|
const lastTouch = event.touches[event.touches.length - 1];
|
||||||
|
const div = event.target as HTMLDivElement;
|
||||||
|
const { x, y, width, height} = div.getBoundingClientRect();
|
||||||
|
const offsetX = (lastTouch.clientX - x) / width * div.offsetWidth;
|
||||||
|
const offsetY = (lastTouch.clientY - y) / height * div.offsetHeight;
|
||||||
|
setMoving(lastTouch.clientX, lastTouch.clientY, offsetX, offsetY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRect = (x: number, y: number): Rect => {
|
||||||
|
if (!moving)
|
||||||
|
return {x, y, width:0, height: 0 };
|
||||||
|
const minX = Math.min(x, moving.x);
|
||||||
|
const maxX = Math.max(x, moving.x);
|
||||||
|
const minY = Math.min(y, moving.y);
|
||||||
|
const maxY = Math.max(y, moving.y);
|
||||||
|
const width = maxX - minX;
|
||||||
|
const height = maxY - minY;
|
||||||
|
return {
|
||||||
|
x: Math.floor(minX),
|
||||||
|
y: Math.floor(minY),
|
||||||
|
width: Math.ceil(width),
|
||||||
|
height: Math.ceil(height)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const move = (x: number, y: number) => {
|
||||||
|
if (!moving) return;
|
||||||
|
if (!document.body.contains(selectionSquare)) {
|
||||||
|
document.body.appendChild(selectionSquare);
|
||||||
|
}
|
||||||
|
const rect = getRect(x, y);
|
||||||
|
const s = selectionSquare.style;
|
||||||
|
s.width = `${rect.width}px`;
|
||||||
|
s.height = `${rect.height}px`;
|
||||||
|
s.left = `${rect.x}px`;
|
||||||
|
s.top = `${rect.y}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (event: MouseEvent) => {
|
||||||
|
if (!moving) return;
|
||||||
|
if (!(event instanceof MouseEvent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
move(clientX, clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchMove = (event: TouchEvent) => {
|
||||||
|
if (!moving) return;
|
||||||
|
if (!(event instanceof TouchEvent) || !event.isTrusted){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const touch = event.touches[0];
|
||||||
|
sss.borderWidth = "6px";
|
||||||
|
if (touch) {
|
||||||
|
const { clientX, clientY } = touch;
|
||||||
|
move(clientX, clientY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const eventEnd = (x: number, y: number) => {
|
||||||
|
if (moving) {
|
||||||
|
const rect = getRect(x, y);
|
||||||
|
moving = undefined;
|
||||||
|
|
||||||
|
window.removeEventListener("mousedown", onMouseUp);
|
||||||
|
window.removeEventListener("touchend", onTouchEnd);
|
||||||
|
window.removeEventListener("mousedown", onMouseDown);
|
||||||
|
window.removeEventListener("touchstart", onTouchStart);
|
||||||
|
window.removeEventListener("mouseup", onMouseMove);
|
||||||
|
window.removeEventListener("touchmove", onTouchMove);
|
||||||
|
document.body.removeChild(curtain);
|
||||||
|
if (document.body.contains(selectionSquare)) {
|
||||||
|
document.body.removeChild(selectionSquare);
|
||||||
|
}
|
||||||
|
resolve(rect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const onTouchEnd = (event: TouchEvent) => {
|
||||||
|
const touch = event.touches[0] || event.changedTouches[0];
|
||||||
|
if (touch) {
|
||||||
|
const { clientX, clientY } = touch;
|
||||||
|
eventEnd(clientX, clientY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (event: MouseEvent) => {
|
||||||
|
eventEnd(event.clientX, event.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
window.addEventListener("touchend", onTouchEnd);
|
||||||
|
window.addEventListener("mousedown", onMouseDown);
|
||||||
|
window.addEventListener("touchstart", onTouchStart);
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
window.addEventListener("touchmove", onTouchMove);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 url = canvas.toDataURL();
|
||||||
|
Popup.custom(<img style={{border: "1px solid black"}} src={url} />, [{
|
||||||
|
content: "Download",
|
||||||
|
click: () => {
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const xCenter= Math.floor(cords.x + rect.width / 2);
|
||||||
|
const yCenter = Math.floor(cords.x + rect.height / 2);
|
||||||
|
if (blob) {
|
||||||
|
saveAs(blob, `${xCenter}:${yCenter}:${new Date().toLocaleDateString()}.png`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
content: "Cancel",
|
||||||
|
click: () => {}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
} catch(error) {
|
||||||
|
Popup.alert("Something went wrong while trying to capture screenshot");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,3 +31,10 @@ export interface Point {
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Rect {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
@@ -354,3 +354,22 @@ export function processNumberEvent(ev: React.ChangeEvent<HTMLInputElement>, cb:
|
|||||||
cb(parseInt(ev.target.value, 10));
|
cb(parseInt(ev.target.value, 10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchTile(x: number, y: number) {
|
||||||
|
const url = `https://pixelcanvas.io/tile/${toChunkX(x)}/${toChunkX(y)}.png`;
|
||||||
|
const result = await fetch(url);
|
||||||
|
const blob = await result.blob();
|
||||||
|
const src = URL.createObjectURL(blob);
|
||||||
|
const image = new Image();
|
||||||
|
image.src = src;
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
image.addEventListener("load", () => {
|
||||||
|
URL.revokeObjectURL(src);
|
||||||
|
resolve(image);
|
||||||
|
});
|
||||||
|
image.addEventListener("error", () => {
|
||||||
|
URL.revokeObjectURL(src);
|
||||||
|
reject(new Error("Unable to load image"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version $VERSION$
|
// @version $VERSION$
|
||||||
// @description Scripts renders pixelated overlays over the tile map!
|
// @description Scripts renders pixelated overlays over the tile map!
|
||||||
// @author 0xa663
|
// @author 0xa663, Terncode
|
||||||
// @match https://pixelcanvas.io/*
|
// @match https://pixelcanvas.io/*
|
||||||
// @icon https://www.google.com/s2/favicons?sz=64&domain=pixelcanvas.io
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=pixelcanvas.io
|
||||||
// @grant none
|
// @grant none
|
||||||
|
|||||||
Reference in New Issue
Block a user