commit
bb9e50e659
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owlbear-rodeo",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^1.12.1",
|
||||
|
@ -1,15 +1,15 @@
|
||||
import React from "react";
|
||||
import { Divider } from "theme-ui";
|
||||
|
||||
function StyledDivider({ vertical, color }) {
|
||||
function StyledDivider({ vertical, color, fill }) {
|
||||
return (
|
||||
<Divider
|
||||
my={vertical ? 0 : 2}
|
||||
mx={vertical ? 2 : 0}
|
||||
bg={color}
|
||||
sx={{
|
||||
height: vertical ? "24px" : "2px",
|
||||
width: vertical ? "2px" : "24px",
|
||||
height: vertical ? (fill ? "100%" : "24px") : "2px",
|
||||
width: vertical ? "2px" : fill ? "100%" : "24px",
|
||||
borderRadius: "2px",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
@ -20,6 +20,7 @@ function StyledDivider({ vertical, color }) {
|
||||
StyledDivider.defaultProps = {
|
||||
vertical: false,
|
||||
color: "text",
|
||||
fill: false,
|
||||
};
|
||||
|
||||
export default StyledDivider;
|
||||
|
@ -12,6 +12,7 @@ function Footer() {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingBottom: "env(safe-area-inset-bottom)",
|
||||
}}
|
||||
>
|
||||
<Link m={2} to="/about" variant="footer">
|
||||
|
@ -18,10 +18,14 @@ function ImageDrop({ onDrop, dropText, children }) {
|
||||
function handleImageDrop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file && file.type.startsWith("image")) {
|
||||
onDrop(file);
|
||||
const files = event.dataTransfer.files;
|
||||
let imageFiles = [];
|
||||
for (let file of files) {
|
||||
if (file.type.startsWith("image")) {
|
||||
imageFiles.push(file);
|
||||
}
|
||||
}
|
||||
onDrop(imageFiles);
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import MapToken from "./MapToken";
|
||||
import MapDrawing from "./MapDrawing";
|
||||
import MapFog from "./MapFog";
|
||||
import MapDice from "./MapDice";
|
||||
import MapGrid from "./MapGrid";
|
||||
|
||||
import TokenDataContext from "../../contexts/TokenDataContext";
|
||||
import MapLoadingContext from "../../contexts/MapLoadingContext";
|
||||
@ -97,8 +98,8 @@ function Map({
|
||||
onMapDraw({ type: "add", shapes: [shape] });
|
||||
}
|
||||
|
||||
function handleMapShapeRemove(shapeId) {
|
||||
onMapDraw({ type: "remove", shapeIds: [shapeId] });
|
||||
function handleMapShapesRemove(shapeIds) {
|
||||
onMapDraw({ type: "remove", shapeIds });
|
||||
}
|
||||
|
||||
const [fogShapes, setFogShapes] = useState([]);
|
||||
@ -107,12 +108,12 @@ function Map({
|
||||
onFogDraw({ type: "add", shapes: [shape] });
|
||||
}
|
||||
|
||||
function handleFogShapeRemove(shapeId) {
|
||||
onFogDraw({ type: "remove", shapeIds: [shapeId] });
|
||||
function handleFogShapesRemove(shapeIds) {
|
||||
onFogDraw({ type: "remove", shapeIds });
|
||||
}
|
||||
|
||||
function handleFogShapeEdit(shape) {
|
||||
onFogDraw({ type: "edit", shapes: [shape] });
|
||||
function handleFogShapesEdit(shapes) {
|
||||
onFogDraw({ type: "edit", shapes });
|
||||
}
|
||||
|
||||
// Replay the draw actions and convert them to shapes for the map drawing
|
||||
@ -174,7 +175,7 @@ function Map({
|
||||
disabledSettings.shape.push("redo");
|
||||
disabledSettings.erase.push("redo");
|
||||
}
|
||||
if (fogShapes.length === 0) {
|
||||
if (!mapState || mapState.fogDrawActionIndex < 0) {
|
||||
disabledSettings.fog.push("undo");
|
||||
}
|
||||
if (
|
||||
@ -275,7 +276,7 @@ function Map({
|
||||
<MapDrawing
|
||||
shapes={mapShapes}
|
||||
onShapeAdd={handleMapShapeAdd}
|
||||
onShapeRemove={handleMapShapeRemove}
|
||||
onShapesRemove={handleMapShapesRemove}
|
||||
selectedToolId={selectedToolId}
|
||||
selectedToolSettings={toolSettings[selectedToolId]}
|
||||
gridSize={gridSizeNormalized}
|
||||
@ -286,14 +287,18 @@ function Map({
|
||||
<MapFog
|
||||
shapes={fogShapes}
|
||||
onShapeAdd={handleFogShapeAdd}
|
||||
onShapeRemove={handleFogShapeRemove}
|
||||
onShapeEdit={handleFogShapeEdit}
|
||||
onShapesRemove={handleFogShapesRemove}
|
||||
onShapesEdit={handleFogShapesEdit}
|
||||
selectedToolId={selectedToolId}
|
||||
selectedToolSettings={toolSettings[selectedToolId]}
|
||||
gridSize={gridSizeNormalized}
|
||||
/>
|
||||
);
|
||||
|
||||
const mapGrid = map && map.showGrid && (
|
||||
<MapGrid map={map} gridSize={gridSizeNormalized} />
|
||||
);
|
||||
|
||||
return (
|
||||
<MapInteraction
|
||||
map={map}
|
||||
@ -308,6 +313,7 @@ function Map({
|
||||
}
|
||||
selectedToolId={selectedToolId}
|
||||
>
|
||||
{mapGrid}
|
||||
{mapDrawing}
|
||||
{mapTokens}
|
||||
{mapFog}
|
||||
|
@ -19,13 +19,15 @@ import useMapBrush from "../../helpers/useMapBrush";
|
||||
function MapDrawing({
|
||||
shapes,
|
||||
onShapeAdd,
|
||||
onShapeRemove,
|
||||
onShapesRemove,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
}) {
|
||||
const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext);
|
||||
const [drawingShape, setDrawingShape] = useState(null);
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
const [erasingShapes, setErasingShapes] = useState([]);
|
||||
|
||||
const shouldHover = selectedToolId === "erase";
|
||||
const isEditing =
|
||||
@ -33,6 +35,14 @@ function MapDrawing({
|
||||
selectedToolId === "shape" ||
|
||||
selectedToolId === "erase";
|
||||
|
||||
const handleBrushUp = useCallback(() => {
|
||||
setIsBrushDown(false);
|
||||
if (erasingShapes.length > 0) {
|
||||
onShapesRemove(erasingShapes.map((shape) => shape.id));
|
||||
setErasingShapes([]);
|
||||
}
|
||||
}, [erasingShapes, onShapesRemove]);
|
||||
|
||||
const handleShapeDraw = useCallback(
|
||||
(brushState, mapBrushPosition) => {
|
||||
function startShape() {
|
||||
@ -65,6 +75,7 @@ function MapDrawing({
|
||||
...commonShapeData,
|
||||
});
|
||||
}
|
||||
setIsBrushDown(true);
|
||||
}
|
||||
|
||||
function continueShape() {
|
||||
@ -119,6 +130,7 @@ function MapDrawing({
|
||||
onShapeAdd(drawingShape);
|
||||
}
|
||||
setDrawingShape(null);
|
||||
handleBrushUp();
|
||||
}
|
||||
|
||||
switch (brushState) {
|
||||
@ -143,48 +155,27 @@ function MapDrawing({
|
||||
onShapeAdd,
|
||||
shapes,
|
||||
drawingShape,
|
||||
handleBrushUp,
|
||||
]
|
||||
);
|
||||
|
||||
useMapBrush(isEditing, handleShapeDraw);
|
||||
|
||||
function handleShapeClick(_, shape) {
|
||||
if (selectedToolId === "erase") {
|
||||
onShapeRemove(shape.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleShapeMouseOver(event, shape) {
|
||||
if (shouldHover) {
|
||||
const path = event.target;
|
||||
const hoverColor = "#BB99FF";
|
||||
path.fill(hoverColor);
|
||||
if (shape.type === "path") {
|
||||
path.stroke(hoverColor);
|
||||
function handleShapeOver(shape, isDown) {
|
||||
if (shouldHover && isDown) {
|
||||
if (erasingShapes.findIndex((s) => s.id === shape.id) === -1) {
|
||||
setErasingShapes((prevShapes) => [...prevShapes, shape]);
|
||||
}
|
||||
path.getLayer().draw();
|
||||
}
|
||||
}
|
||||
|
||||
function handleShapeMouseOut(event, shape) {
|
||||
if (shouldHover) {
|
||||
const path = event.target;
|
||||
const color = colors[shape.color] || shape.color;
|
||||
path.fill(color);
|
||||
if (shape.type === "path") {
|
||||
path.stroke(color);
|
||||
}
|
||||
path.getLayer().draw();
|
||||
}
|
||||
}
|
||||
|
||||
function renderShape(shape) {
|
||||
const defaultProps = {
|
||||
key: shape.id,
|
||||
onMouseOver: (e) => handleShapeMouseOver(e, shape),
|
||||
onMouseOut: (e) => handleShapeMouseOut(e, shape),
|
||||
onClick: (e) => handleShapeClick(e, shape),
|
||||
onTap: (e) => handleShapeClick(e, shape),
|
||||
onMouseMove: () => handleShapeOver(shape, isBrushDown),
|
||||
onTouchOver: () => handleShapeOver(shape, isBrushDown),
|
||||
onMouseDown: () => handleShapeOver(shape, true),
|
||||
onTouchStart: () => handleShapeOver(shape, true),
|
||||
fill: colors[shape.color] || shape.color,
|
||||
opacity: shape.blend ? 0.5 : 1,
|
||||
};
|
||||
@ -245,10 +236,19 @@ function MapDrawing({
|
||||
}
|
||||
}
|
||||
|
||||
function renderErasingShape(shape) {
|
||||
const eraseShape = {
|
||||
...shape,
|
||||
color: "#BB99FF",
|
||||
};
|
||||
return renderShape(eraseShape);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group>
|
||||
{shapes.map(renderShape)}
|
||||
{drawingShape && renderShape(drawingShape)}
|
||||
{erasingShapes.length > 0 && erasingShapes.map(renderErasingShape)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
@ -20,14 +20,16 @@ import useMapBrush from "../../helpers/useMapBrush";
|
||||
function MapFog({
|
||||
shapes,
|
||||
onShapeAdd,
|
||||
onShapeRemove,
|
||||
onShapeEdit,
|
||||
onShapesRemove,
|
||||
onShapesEdit,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
}) {
|
||||
const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext);
|
||||
const [drawingShape, setDrawingShape] = useState(null);
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
const [editingShapes, setEditingShapes] = useState([]);
|
||||
|
||||
const isEditing = selectedToolId === "fog";
|
||||
const shouldHover =
|
||||
@ -37,6 +39,20 @@ function MapFog({
|
||||
|
||||
const [patternImage] = useImage(diagonalPattern);
|
||||
|
||||
const handleBrushUp = useCallback(() => {
|
||||
setIsBrushDown(false);
|
||||
if (editingShapes.length > 0) {
|
||||
if (selectedToolSettings.type === "remove") {
|
||||
onShapesRemove(editingShapes.map((shape) => shape.id));
|
||||
} else if (selectedToolSettings.type === "toggle") {
|
||||
onShapesEdit(
|
||||
editingShapes.map((shape) => ({ ...shape, visible: !shape.visible }))
|
||||
);
|
||||
}
|
||||
setEditingShapes([]);
|
||||
}
|
||||
}, [editingShapes, onShapesRemove, onShapesEdit, selectedToolSettings]);
|
||||
|
||||
const handleShapeDraw = useCallback(
|
||||
(brushState, mapBrushPosition) => {
|
||||
function startShape() {
|
||||
@ -58,6 +74,7 @@ function MapFog({
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
setIsBrushDown(true);
|
||||
}
|
||||
|
||||
function continueShape() {
|
||||
@ -106,6 +123,7 @@ function MapFog({
|
||||
}
|
||||
}
|
||||
setDrawingShape(null);
|
||||
handleBrushUp();
|
||||
}
|
||||
|
||||
switch (brushState) {
|
||||
@ -130,46 +148,17 @@ function MapFog({
|
||||
onShapeAdd,
|
||||
shapes,
|
||||
drawingShape,
|
||||
handleBrushUp,
|
||||
]
|
||||
);
|
||||
|
||||
useMapBrush(isEditing, handleShapeDraw);
|
||||
|
||||
function handleShapeClick(_, shape) {
|
||||
if (!isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedToolSettings.type === "remove") {
|
||||
onShapeRemove(shape.id);
|
||||
} else if (selectedToolSettings.type === "toggle") {
|
||||
onShapeEdit({ ...shape, visible: !shape.visible });
|
||||
}
|
||||
}
|
||||
|
||||
function handleShapeMouseOver(event, shape) {
|
||||
if (shouldHover) {
|
||||
const path = event.target;
|
||||
if (shape.visible) {
|
||||
const hoverColor = "#BB99FF";
|
||||
path.fill(hoverColor);
|
||||
} else {
|
||||
path.opacity(1);
|
||||
function handleShapeOver(shape, isDown) {
|
||||
if (shouldHover && isDown) {
|
||||
if (editingShapes.findIndex((s) => s.id === shape.id) === -1) {
|
||||
setEditingShapes((prevShapes) => [...prevShapes, shape]);
|
||||
}
|
||||
path.getLayer().draw();
|
||||
}
|
||||
}
|
||||
|
||||
function handleShapeMouseOut(event, shape) {
|
||||
if (shouldHover) {
|
||||
const path = event.target;
|
||||
if (shape.visible) {
|
||||
const color = colors[shape.color] || shape.color;
|
||||
path.fill(color);
|
||||
} else {
|
||||
path.opacity(0.5);
|
||||
}
|
||||
path.getLayer().draw();
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,10 +166,10 @@ function MapFog({
|
||||
return (
|
||||
<Line
|
||||
key={shape.id}
|
||||
onMouseOver={(e) => handleShapeMouseOver(e, shape)}
|
||||
onMouseOut={(e) => handleShapeMouseOut(e, shape)}
|
||||
onClick={(e) => handleShapeClick(e, shape)}
|
||||
onTap={(e) => handleShapeClick(e, shape)}
|
||||
onMouseMove={() => handleShapeOver(shape, isBrushDown)}
|
||||
onTouchOver={() => handleShapeOver(shape, isBrushDown)}
|
||||
onMouseDown={() => handleShapeOver(shape, true)}
|
||||
onTouchStart={() => handleShapeOver(shape, true)}
|
||||
points={shape.data.points.reduce(
|
||||
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight],
|
||||
[]
|
||||
@ -203,10 +192,19 @@ function MapFog({
|
||||
);
|
||||
}
|
||||
|
||||
function renderEditingShape(shape) {
|
||||
const editingShape = {
|
||||
...shape,
|
||||
color: "#BB99FF",
|
||||
};
|
||||
return renderShape(editingShape);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group>
|
||||
{shapes.map(renderShape)}
|
||||
{drawingShape && renderShape(drawingShape)}
|
||||
{editingShapes.length > 0 && editingShapes.map(renderEditingShape)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
93
src/components/map/MapGrid.js
Normal file
93
src/components/map/MapGrid.js
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Line, Group } from "react-konva";
|
||||
import useImage from "use-image";
|
||||
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
import { mapSources as defaultMapSources } from "../../maps";
|
||||
|
||||
import { getStrokeWidth } from "../../helpers/drawing";
|
||||
|
||||
const lightnessDetectionOffset = 0.1;
|
||||
|
||||
function MapGrid({ map, gridSize }) {
|
||||
const mapSource = useDataSource(map, defaultMapSources);
|
||||
const [mapImage, mapLoadingStatus] = useImage(mapSource);
|
||||
|
||||
const gridX = map && map.gridX;
|
||||
const gridY = map && map.gridY;
|
||||
|
||||
const { mapWidth, mapHeight } = useContext(MapInteractionContext);
|
||||
|
||||
const lineSpacingX = mapWidth / gridX;
|
||||
const lineSpacingY = mapHeight / gridY;
|
||||
|
||||
const [isImageLight, setIsImageLight] = useState(true);
|
||||
|
||||
// When the map changes find the average lightness of its pixels
|
||||
useEffect(() => {
|
||||
if (mapLoadingStatus === "loaded") {
|
||||
const imageWidth = mapImage.width;
|
||||
const imageHeight = mapImage.height;
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = imageWidth;
|
||||
canvas.height = imageHeight;
|
||||
let context = canvas.getContext("2d");
|
||||
context.drawImage(mapImage, 0, 0);
|
||||
const imageData = context.getImageData(0, 0, imageWidth, imageHeight);
|
||||
|
||||
const data = imageData.data;
|
||||
let lightPixels = 0;
|
||||
let darkPixels = 0;
|
||||
// Loop over every pixels rgba values
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
|
||||
const max = Math.max(Math.max(r, g), b);
|
||||
if (max < 128) {
|
||||
darkPixels++;
|
||||
} else {
|
||||
lightPixels++;
|
||||
}
|
||||
}
|
||||
|
||||
const norm = (lightPixels - darkPixels) / (imageWidth * imageHeight);
|
||||
if (norm + lightnessDetectionOffset < 0) {
|
||||
setIsImageLight(false);
|
||||
} else {
|
||||
setIsImageLight(true);
|
||||
}
|
||||
}
|
||||
}, [mapImage, mapLoadingStatus]);
|
||||
|
||||
const lines = [];
|
||||
for (let x = 1; x < gridX; x++) {
|
||||
lines.push(
|
||||
<Line
|
||||
key={`grid_x_${x}`}
|
||||
points={[x * lineSpacingX, 0, x * lineSpacingX, mapHeight]}
|
||||
stroke={isImageLight ? "black" : "white"}
|
||||
strokeWidth={getStrokeWidth(0.1, gridSize, mapWidth, mapHeight)}
|
||||
opacity={0.8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
for (let y = 1; y < gridY; y++) {
|
||||
lines.push(
|
||||
<Line
|
||||
key={`grid_y_${y}`}
|
||||
points={[0, y * lineSpacingY, mapWidth, y * lineSpacingY]}
|
||||
stroke={isImageLight ? "black" : "white"}
|
||||
strokeWidth={getStrokeWidth(0.1, gridSize, mapWidth, mapHeight)}
|
||||
opacity={0.8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Group>{lines}</Group>;
|
||||
}
|
||||
|
||||
export default MapGrid;
|
@ -3,6 +3,8 @@ import { Flex, Box, Label, Input, Checkbox, IconButton } from "theme-ui";
|
||||
|
||||
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
|
||||
|
||||
import Divider from "../Divider";
|
||||
|
||||
function MapSettings({
|
||||
map,
|
||||
mapState,
|
||||
@ -57,6 +59,27 @@ function MapSettings({
|
||||
{showMore && (
|
||||
<>
|
||||
<Box mt={2} sx={{ flexGrow: 1 }}>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
name="name"
|
||||
value={(map && map.name) || ""}
|
||||
onChange={(e) => onSettingsChange("name", e.target.value)}
|
||||
disabled={!map || map.type === "default"}
|
||||
my={1}
|
||||
/>
|
||||
</Box>
|
||||
<Box my={2}>
|
||||
<Label>
|
||||
<Checkbox
|
||||
checked={map && map.showGrid}
|
||||
disabled={!map || map.type === "default"}
|
||||
onChange={(e) => onSettingsChange("showGrid", e.target.checked)}
|
||||
/>
|
||||
Show Grid
|
||||
</Label>
|
||||
</Box>
|
||||
<Divider fill />
|
||||
<Box my={2} sx={{ flexGrow: 1 }}>
|
||||
<Label>Allow others to edit</Label>
|
||||
<Flex my={1}>
|
||||
<Label>
|
||||
@ -85,16 +108,6 @@ function MapSettings({
|
||||
</Label>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box my={2} sx={{ flexGrow: 1 }}>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
name="name"
|
||||
value={(map && map.name) || ""}
|
||||
onChange={(e) => onSettingsChange("name", e.target.value)}
|
||||
disabled={!map || map.type === "default"}
|
||||
my={1}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
|
@ -48,7 +48,7 @@ function MapToken({
|
||||
const tokenImage = event.target;
|
||||
const tokenImageRect = tokenImage.getClientRect();
|
||||
|
||||
if (token.isVehicle) {
|
||||
if (token && token.isVehicle) {
|
||||
// Find all other tokens on the map
|
||||
const layer = tokenImage.getLayer();
|
||||
const tokens = layer.find(".token");
|
||||
@ -83,7 +83,7 @@ function MapToken({
|
||||
const tokenImage = event.target;
|
||||
|
||||
const mountChanges = {};
|
||||
if (token.isVehicle) {
|
||||
if (token && token.isVehicle) {
|
||||
const layer = tokenImage.getLayer();
|
||||
const mountedTokens = tokenImage.find(".token");
|
||||
for (let mountedToken of mountedTokens) {
|
||||
|
@ -66,7 +66,7 @@ function TokenDragOverlay({
|
||||
function handleTokenDragEnd() {
|
||||
if (isRemoveHovered) {
|
||||
// Handle other tokens when a vehicle gets deleted
|
||||
if (token.isVehicle) {
|
||||
if (token && token.isVehicle) {
|
||||
const layer = tokenImage.getLayer();
|
||||
const mountedTokens = tokenImage.find(".token");
|
||||
for (let mountedToken of mountedTokens) {
|
||||
|
@ -43,6 +43,7 @@ export function MapDataProvider({ children }) {
|
||||
created: Date.now() + i,
|
||||
lastModified: Date.now() + i,
|
||||
gridType: "grid",
|
||||
showGrid: false,
|
||||
});
|
||||
// Add a state for the map if there isn't one already
|
||||
const state = await database.table("states").get(id);
|
||||
|
@ -89,6 +89,17 @@ function loadVersions(db) {
|
||||
}
|
||||
});
|
||||
});
|
||||
// v1.3.1 - Added show grid option
|
||||
db.version(4)
|
||||
.stores({})
|
||||
.upgrade((tx) => {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
map.showGrid = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get the dexie database used in DatabaseContext
|
||||
|
11
src/docs/releaseNotes/v1.3.1.md
Normal file
11
src/docs/releaseNotes/v1.3.1.md
Normal file
@ -0,0 +1,11 @@
|
||||
# v1.3.1
|
||||
|
||||
## Minor Changes
|
||||
|
||||
- Fixed a bug where tokens that were placed on the map then removed from the token select screen could no longer be deleted from the map.
|
||||
- Fixed a bug where fog drawing couldn't be undone if there the last fog shape was deleted.
|
||||
- Added the ability to add multiple new maps or tokens at the same time.
|
||||
- Added a Show Grid option for maps that will overlay a grid on the map. This can be useful for when you have a map with no grid or you want to verify your current grid settings.
|
||||
- Added the ability to erase multiple shapes at a time by dragging over a shape with the eraser tool. This works for fog erase and toggle as well.
|
||||
|
||||
[Reddit]()
|
@ -39,3 +39,14 @@ export function toDegrees(angle) {
|
||||
export function lerp(a, b, alpha) {
|
||||
return a * (1 - alpha) + b * alpha;
|
||||
}
|
||||
|
||||
// Console log an image
|
||||
export function logImage(url, width, height) {
|
||||
const style = [
|
||||
"font-size: 1px;",
|
||||
`padding: ${height}px ${width}px;`,
|
||||
`background: url(${url}) no-repeat;`,
|
||||
"background-size: contain;",
|
||||
].join(" ");
|
||||
console.log("%c ", style);
|
||||
}
|
||||
|
@ -9,3 +9,7 @@ html {
|
||||
.simplebar-scrollbar:before {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled ~ svg {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ const defaultMapProps = {
|
||||
// Grid type
|
||||
// TODO: add support for hex horizontal and hex vertical
|
||||
gridType: "grid",
|
||||
showGrid: false,
|
||||
};
|
||||
|
||||
function SelectMapModal({
|
||||
@ -51,9 +52,17 @@ function SelectMapModal({
|
||||
|
||||
const fileInputRef = useRef();
|
||||
|
||||
function handleImageUpload(file) {
|
||||
async function handleImagesUpload(files) {
|
||||
for (let file of files) {
|
||||
await handleImageUpload(file);
|
||||
}
|
||||
// Set file input to null to allow adding the same image 2 times in a row
|
||||
fileInputRef.current.value = null;
|
||||
}
|
||||
|
||||
async function handleImageUpload(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
return Promise.reject();
|
||||
}
|
||||
let fileGridX = defaultMapSize;
|
||||
let fileGridY = defaultMapSize;
|
||||
@ -86,11 +95,13 @@ function SelectMapModal({
|
||||
let image = new Image();
|
||||
setImageLoading(true);
|
||||
|
||||
blobToBuffer(file).then((buffer) => {
|
||||
// Copy file to avoid permissions issues
|
||||
const blob = new Blob([buffer]);
|
||||
// Create and load the image temporarily to get its dimensions
|
||||
const url = URL.createObjectURL(blob);
|
||||
const buffer = await blobToBuffer(file);
|
||||
// Copy file to avoid permissions issues
|
||||
const blob = new Blob([buffer]);
|
||||
// Create and load the image temporarily to get its dimensions
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = function () {
|
||||
handleMapAdd({
|
||||
// Save as a buffer to send with msgpack
|
||||
@ -109,11 +120,10 @@ function SelectMapModal({
|
||||
});
|
||||
setImageLoading(false);
|
||||
URL.revokeObjectURL(url);
|
||||
resolve();
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.src = url;
|
||||
|
||||
// Set file input to null to allow adding the same image 2 times in a row
|
||||
fileInputRef.current.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
@ -172,12 +182,13 @@ function SelectMapModal({
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
||||
<ImageDrop onDrop={handleImageUpload} dropText="Drop map to upload">
|
||||
<ImageDrop onDrop={handleImagesUpload} dropText="Drop map to upload">
|
||||
<input
|
||||
onChange={(event) => handleImageUpload(event.target.files[0])}
|
||||
onChange={(event) => handleImagesUpload(event.target.files)}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: "none" }}
|
||||
multiple
|
||||
ref={fileInputRef}
|
||||
/>
|
||||
<Flex
|
||||
|
@ -36,7 +36,15 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
addToken(token);
|
||||
}
|
||||
|
||||
function handleImageUpload(file) {
|
||||
async function handleImagesUpload(files) {
|
||||
for (let file of files) {
|
||||
await handleImageUpload(file);
|
||||
}
|
||||
// Set file input to null to allow adding the same image 2 times in a row
|
||||
fileInputRef.current.value = null;
|
||||
}
|
||||
|
||||
async function handleImageUpload(file) {
|
||||
let name = "Unknown Map";
|
||||
if (file.name) {
|
||||
// Remove file extension
|
||||
@ -49,11 +57,14 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
}
|
||||
let image = new Image();
|
||||
setImageLoading(true);
|
||||
blobToBuffer(file).then((buffer) => {
|
||||
// Copy file to avoid permissions issues
|
||||
const blob = new Blob([buffer]);
|
||||
// Create and load the image temporarily to get its dimensions
|
||||
const url = URL.createObjectURL(blob);
|
||||
const buffer = await blobToBuffer(file);
|
||||
|
||||
// Copy file to avoid permissions issues
|
||||
const blob = new Blob([buffer]);
|
||||
// Create and load the image temporarily to get its dimensions
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = function () {
|
||||
handleTokenAdd({
|
||||
file: buffer,
|
||||
@ -68,11 +79,10 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
hideInSidebar: false,
|
||||
});
|
||||
setImageLoading(false);
|
||||
resolve();
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.src = url;
|
||||
|
||||
// Set file input to null to allow adding the same image 2 times in a row
|
||||
fileInputRef.current.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,13 +106,14 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
||||
<ImageDrop onDrop={handleImageUpload} dropText="Drop token to upload">
|
||||
<ImageDrop onDrop={handleImagesUpload} dropText="Drop token to upload">
|
||||
<input
|
||||
onChange={(event) => handleImageUpload(event.target.files[0])}
|
||||
onChange={(event) => handleImagesUpload(event.target.files)}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: "none" }}
|
||||
ref={fileInputRef}
|
||||
multiple
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
|
@ -51,7 +51,7 @@ function Home() {
|
||||
Join Game
|
||||
</Button>
|
||||
<Text variant="caption" as="p" sx={{ textAlign: "center" }}>
|
||||
Beta v1.3.0
|
||||
Beta v1.3.1
|
||||
</Text>
|
||||
<Button
|
||||
m={2}
|
||||
|
@ -9,6 +9,7 @@ const v110 = raw("../docs/releaseNotes/v1.1.0.md");
|
||||
const v120 = raw("../docs/releaseNotes/v1.2.0.md");
|
||||
const v121 = raw("../docs/releaseNotes/v1.2.1.md");
|
||||
const v130 = raw("../docs/releaseNotes/v1.3.0.md");
|
||||
const v131 = raw("../docs/releaseNotes/v1.3.1.md");
|
||||
|
||||
function ReleaseNotes() {
|
||||
return (
|
||||
@ -31,6 +32,9 @@ function ReleaseNotes() {
|
||||
<Text mb={2} variant="heading" as="h1" sx={{ fontSize: 5 }}>
|
||||
Release Notes
|
||||
</Text>
|
||||
<div id="v131">
|
||||
<Markdown source={v131} />
|
||||
</div>
|
||||
<div id="v130">
|
||||
<Markdown source={v130} />
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user