Merge pull request #23 from mitchemmc/release/v1.6.1

Release/v1.6.1
This commit is contained in:
Mitchell McCaffrey 2020-10-24 09:59:28 +11:00 committed by GitHub
commit b6b6a86454
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 355 additions and 223 deletions

View File

@ -1,6 +1,6 @@
{
"name": "owlbear-rodeo",
"version": "1.6.0",
"version": "1.6.1",
"private": true,
"dependencies": {
"@babylonjs/core": "^4.1.0",

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, useContext } from "react";
import React, { useState, useRef, useContext } from "react";
import { Box, IconButton } from "theme-ui";
import { Stage, Layer, Image } from "react-konva";
import ReactResizeDetector from "react-resize-detector";
@ -6,7 +6,8 @@ import ReactResizeDetector from "react-resize-detector";
import useMapImage from "../../helpers/useMapImage";
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
import useStageInteraction from "../../helpers/useStageInteraction";
import { getMapDefaultInset } from "../../helpers/map";
import useImageCenter from "../../helpers/useImageCenter";
import { getMapDefaultInset, getMapMaxZoom } from "../../helpers/map";
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
import KeyboardContext from "../../contexts/KeyboardContext";
@ -25,19 +26,6 @@ function MapEditor({ map, onSettingsChange }) {
const [stageHeight, setStageHeight] = useState(1);
const [stageScale, setStageScale] = useState(1);
const stageRatio = stageWidth / stageHeight;
const mapRatio = map ? map.width / map.height : 1;
let mapWidth;
let mapHeight;
if (stageRatio > mapRatio) {
mapWidth = map ? stageHeight / (map.height / map.width) : stageWidth;
mapHeight = stageHeight;
} else {
mapWidth = stageWidth;
mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
}
const defaultInset = getMapDefaultInset(
map.width,
map.height,
@ -46,6 +34,7 @@ function MapEditor({ map, onSettingsChange }) {
);
const stageTranslateRef = useRef({ x: 0, y: 0 });
const mapStageRef = useRef();
const mapLayerRef = useRef();
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
@ -54,45 +43,32 @@ function MapEditor({ map, onSettingsChange }) {
setStageHeight(height);
}
// Reset map translate and scale
useEffect(() => {
const layer = mapLayerRef.current;
const containerRect = containerRef.current.getBoundingClientRect();
if (layer) {
let newTranslate;
if (stageRatio > mapRatio) {
newTranslate = {
x: -(mapWidth - containerRect.width) / 2,
y: 0,
};
} else {
newTranslate = {
x: 0,
y: -(mapHeight - containerRect.height) / 2,
};
}
const containerRef = useRef();
usePreventOverscroll(containerRef);
layer.x(newTranslate.x);
layer.y(newTranslate.y);
layer.draw();
stageTranslateRef.current = newTranslate;
setStageScale(1);
}
}, [map.id, mapWidth, mapHeight, stageRatio, mapRatio]);
const [mapWidth, mapHeight] = useImageCenter(
map,
mapStageRef,
stageWidth,
stageHeight,
stageTranslateRef,
setStageScale,
mapLayerRef,
containerRef,
true
);
const bind = useStageInteraction(
mapLayerRef.current,
mapStageRef.current,
stageScale,
setStageScale,
stageTranslateRef,
mapLayerRef.current,
getMapMaxZoom(map),
"pan",
preventMapInteraction
);
const containerRef = useRef();
usePreventOverscroll(containerRef);
function handleGridChange(inset) {
onSettingsChange("grid", {
...map.grid,
@ -128,7 +104,6 @@ function MapEditor({ map, onSettingsChange }) {
map.grid.inset.topLeft.y !== defaultInset.topLeft.y ||
map.grid.inset.bottomRight.x !== defaultInset.bottomRight.x ||
map.grid.inset.bottomRight.y !== defaultInset.bottomRight.y;
return (
<Box
sx={{
@ -148,19 +123,17 @@ function MapEditor({ map, onSettingsChange }) {
width={stageWidth}
height={stageHeight}
scale={{ x: stageScale, y: stageScale }}
x={stageWidth / 2}
y={stageHeight / 2}
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
ref={mapStageRef}
>
<Layer ref={mapLayerRef}>
<Image image={mapImageSource} width={mapWidth} height={mapHeight} />
<KeyboardContext.Provider value={keyboardValue}>
<MapInteractionProvider value={mapInteraction}>
{showGridControls && canEditGrid && (
<MapGrid map={map} strokeWidth={0.5} />
)}
{showGridControls && canEditGrid && (
<MapGridEditor map={map} onGridChange={handleGridChange} />
<>
<MapGrid map={map} strokeWidth={0.5} />
<MapGridEditor map={map} onGridChange={handleGridChange} />
</>
)}
</MapInteractionProvider>
</KeyboardContext.Provider>

View File

@ -8,6 +8,8 @@ import useMapImage from "../../helpers/useMapImage";
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
import useKeyboard from "../../helpers/useKeyboard";
import useStageInteraction from "../../helpers/useStageInteraction";
import useImageCenter from "../../helpers/useImageCenter";
import { getMapMaxZoom } from "../../helpers/map";
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
import MapStageContext, {
@ -42,52 +44,42 @@ function MapInteraction({
const [stageScale, setStageScale] = useState(1);
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
const stageWidthRef = useRef(stageWidth);
const stageHeightRef = useRef(stageHeight);
// Avoid state udpates when panning the map by using a ref and updating the konva element directly
const stageTranslateRef = useRef({ x: 0, y: 0 });
// Reset transform when map changes
const previousMapIdRef = useRef();
useEffect(() => {
const layer = mapLayerRef.current;
const previousMapId = previousMapIdRef.current;
if (map && layer && previousMapId !== map.id) {
const mapHeight = stageWidthRef.current * (map.height / map.width);
const newTranslate = {
x: 0,
y: -(mapHeight - stageHeightRef.current) / 2,
};
layer.x(newTranslate.x);
layer.y(newTranslate.y);
layer.draw();
stageTranslateRef.current = newTranslate;
setStageScale(1);
}
previousMapIdRef.current = map && map.id;
}, [map]);
const mapStageRef = useContext(MapStageContext);
const mapLayerRef = useRef();
const mapImageRef = useRef();
function handleResize(width, height) {
setStageWidth(width);
setStageHeight(height);
stageWidthRef.current = width;
stageHeightRef.current = height;
}
const mapStageRef = useContext(MapStageContext);
const mapLayerRef = useRef();
const mapImageRef = useRef();
const containerRef = useRef();
usePreventOverscroll(containerRef);
const [mapWidth, mapHeight] = useImageCenter(
map,
mapStageRef,
stageWidth,
stageHeight,
stageTranslateRef,
setStageScale,
mapLayerRef,
containerRef
);
const previousSelectedToolRef = useRef(selectedToolId);
const [interactionEmitter] = useState(new EventEmitter());
const bind = useStageInteraction(
mapLayerRef.current,
mapStageRef.current,
stageScale,
setStageScale,
stageTranslateRef,
mapLayerRef.current,
getMapMaxZoom(map),
selectedToolId,
preventMapInteraction,
{
@ -169,12 +161,6 @@ function MapInteraction({
}
}
const containerRef = useRef();
usePreventOverscroll(containerRef);
const mapWidth = stageWidth;
const mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
const auth = useContext(AuthContext);
const settings = useContext(SettingsContext);
@ -206,9 +192,6 @@ function MapInteraction({
width={stageWidth}
height={stageHeight}
scale={{ x: stageScale, y: stageScale }}
x={stageWidth / 2}
y={stageHeight / 2}
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
ref={mapStageRef}
>
<Layer ref={mapLayerRef}>

View File

@ -160,7 +160,7 @@ function MapSettings({
onSettingsChange("showGrid", e.target.checked)
}
/>
Show Grid
Draw Grid
</Label>
<Label>
<Checkbox

View File

@ -109,7 +109,7 @@ function MapTiles({
<Box
sx={{
position: "absolute",
top: 0,
top: "39px",
left: 0,
right: 0,
textAlign: "center",

View File

@ -7,6 +7,7 @@ import useImage from "use-image";
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
import useStageInteraction from "../../helpers/useStageInteraction";
import useDataSource from "../../helpers/useDataSource";
import useImageCenter from "../../helpers/useImageCenter";
import GridOnIcon from "../../icons/GridOnIcon";
import GridOffIcon from "../../icons/GridOffIcon";
@ -29,80 +30,43 @@ function TokenPreview({ token }) {
unknownSource
);
const [tokenSourceImage] = useImage(tokenSource);
const [tokenRatio, setTokenRatio] = useState(1);
useEffect(() => {
if (tokenSourceImage) {
setTokenRatio(tokenSourceImage.width / tokenSourceImage.height);
}
}, [tokenSourceImage]);
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
const [stageScale, setStageScale] = useState(1);
const stageRatio = stageWidth / stageHeight;
let tokenWidth;
let tokenHeight;
if (stageRatio > tokenRatio) {
tokenWidth = tokenSourceImage
? stageHeight / (tokenSourceImage.height / tokenSourceImage.width)
: stageWidth;
tokenHeight = stageHeight;
} else {
tokenWidth = stageWidth;
tokenHeight = tokenSourceImage
? stageWidth * (tokenSourceImage.height / tokenSourceImage.width)
: stageHeight;
}
const stageTranslateRef = useRef({ x: 0, y: 0 });
const mapLayerRef = useRef();
const tokenStageRef = useRef();
const tokenLayerRef = useRef();
function handleResize(width, height) {
setStageWidth(width);
setStageHeight(height);
}
// Reset map translate and scale
useEffect(() => {
const layer = mapLayerRef.current;
const containerRect = containerRef.current.getBoundingClientRect();
if (layer) {
let newTranslate;
if (stageRatio > tokenRatio) {
newTranslate = {
x: -(tokenWidth - containerRect.width) / 2,
y: 0,
};
} else {
newTranslate = {
x: 0,
y: -(tokenHeight - containerRect.height) / 2,
};
}
const containerRef = useRef();
usePreventOverscroll(containerRef);
layer.x(newTranslate.x);
layer.y(newTranslate.y);
layer.draw();
stageTranslateRef.current = newTranslate;
setStageScale(1);
}
}, [token.id, tokenWidth, tokenHeight, stageRatio, tokenRatio]);
const [tokenWidth, tokenHeight] = useImageCenter(
token,
tokenStageRef,
stageWidth,
stageHeight,
stageTranslateRef,
setStageScale,
tokenLayerRef,
containerRef,
true
);
const bind = useStageInteraction(
mapLayerRef.current,
tokenStageRef.current,
stageScale,
setStageScale,
stageTranslateRef,
"pan"
tokenLayerRef.current
);
const containerRef = useRef();
usePreventOverscroll(containerRef);
const [showGridPreview, setShowGridPreview] = useState(true);
const gridWidth = tokenWidth;
const gridX = token.defaultSize;
@ -133,11 +97,9 @@ function TokenPreview({ token }) {
width={stageWidth}
height={stageHeight}
scale={{ x: stageScale, y: stageScale }}
x={stageWidth / 2}
y={stageHeight / 2}
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
ref={tokenStageRef}
>
<Layer ref={mapLayerRef}>
<Layer ref={tokenLayerRef}>
<Image
image={tokenSourceImage}
width={tokenWidth}

View File

@ -114,7 +114,7 @@ function TokenTiles({
<Box
sx={{
position: "absolute",
top: 0,
top: "39px",
left: 0,
right: 0,
textAlign: "center",

View File

@ -19,7 +19,7 @@ try {
}
export function AuthProvider({ children }) {
const { database } = useContext(DatabaseContext);
const { database, databaseStatus } = useContext(DatabaseContext);
const [password, setPassword] = useState(storage.getItem("auth") || "");
@ -31,7 +31,7 @@ export function AuthProvider({ children }) {
const [userId, setUserId] = useState();
useEffect(() => {
if (!database) {
if (!database || databaseStatus === "loading") {
return;
}
async function loadUserId() {
@ -46,11 +46,11 @@ export function AuthProvider({ children }) {
}
loadUserId();
}, [database]);
}, [database, databaseStatus]);
const [nickname, setNickname] = useState("");
useEffect(() => {
if (!database) {
if (!database || databaseStatus === "loading") {
return;
}
async function loadNickname() {
@ -65,13 +65,17 @@ export function AuthProvider({ children }) {
}
loadNickname();
}, [database]);
}, [database, databaseStatus]);
useEffect(() => {
if (nickname !== undefined && database !== undefined) {
if (
nickname !== undefined &&
database !== undefined &&
databaseStatus !== "loading"
) {
database.table("user").update("nickname", { value: nickname });
}
}, [nickname, database]);
}, [nickname, database, databaseStatus]);
const value = {
userId,

View File

@ -11,11 +11,14 @@ export function DatabaseProvider({ children }) {
useEffect(() => {
// Create a test database and open it to see if indexedDB is enabled
let testDBRequest = window.indexedDB.open("__test");
testDBRequest.onsuccess = function () {
testDBRequest.onsuccess = async function () {
testDBRequest.result.close();
let db = getDatabase();
let db = getDatabase({ autoOpen: false });
setDatabase(db);
setDatabaseStatus("loaded");
db.on("ready", () => {
setDatabaseStatus("loaded");
});
await db.open();
window.indexedDB.deleteDatabase("__test");
};
// If indexedb disabled create an in memory database
@ -23,9 +26,12 @@ export function DatabaseProvider({ children }) {
console.warn("Database is disabled, no state will be saved");
const indexedDB = await import("fake-indexeddb");
const IDBKeyRange = await import("fake-indexeddb/lib/FDBKeyRange");
let db = getDatabase({ indexedDB, IDBKeyRange });
let db = getDatabase({ indexedDB, IDBKeyRange, autoOpen: false });
setDatabase(db);
setDatabaseStatus("disabled");
db.on("ready", () => {
setDatabaseStatus("disabled");
});
await db.open();
window.indexedDB.deleteDatabase("__test");
};
}, []);

View File

@ -23,14 +23,14 @@ const defaultMapState = {
};
export function MapDataProvider({ children }) {
const { database } = useContext(DatabaseContext);
const { database, databaseStatus } = useContext(DatabaseContext);
const { userId } = useContext(AuthContext);
const [maps, setMaps] = useState([]);
const [mapStates, setMapStates] = useState([]);
// Load maps from the database and ensure state is properly setup
useEffect(() => {
if (!userId || !database) {
if (!userId || !database || databaseStatus === "loading") {
return;
}
async function getDefaultMaps() {
@ -71,7 +71,7 @@ export function MapDataProvider({ children }) {
}
loadMaps();
}, [userId, database]);
}, [userId, database, databaseStatus]);
/**
* Adds a map to the database, also adds an assosiated state for that map
@ -131,7 +131,14 @@ export function MapDataProvider({ children }) {
}
async function updateMap(id, update) {
await database.table("maps").update(id, update);
// fake-indexeddb throws an error when updating maps in production.
// Catch that error and use put when it fails
try {
await database.table("maps").update(id, update);
} catch (error) {
const map = (await getMapFromDB(id)) || {};
await database.table("maps").put({ ...map, id, ...update });
}
setMaps((prevMaps) => {
const newMaps = [...prevMaps];
const i = newMaps.findIndex((map) => map.id === id);
@ -219,7 +226,8 @@ export function MapDataProvider({ children }) {
}
async function getMapFromDB(mapId) {
return await database.table("maps").get(mapId);
let map = await database.table("maps").get(mapId);
return map;
}
const ownedMaps = maps.filter((map) => map.owner === userId);

View File

@ -10,13 +10,13 @@ const TokenDataContext = React.createContext();
const cachedTokenMax = 100;
export function TokenDataProvider({ children }) {
const { database } = useContext(DatabaseContext);
const { database, databaseStatus } = useContext(DatabaseContext);
const { userId } = useContext(AuthContext);
const [tokens, setTokens] = useState([]);
useEffect(() => {
if (!userId || !database) {
if (!userId || !database || databaseStatus === "loading") {
return;
}
function getDefaultTokes() {
@ -43,7 +43,7 @@ export function TokenDataProvider({ children }) {
}
loadTokens();
}, [userId, database]);
}, [userId, database, databaseStatus]);
async function addToken(token) {
await database.table("tokens").add(token);

View File

@ -242,6 +242,32 @@ function loadVersions(db) {
token.group = "";
});
});
// v1.6.1 - Added width and height to tokens
db.version(15)
.stores({})
.upgrade(async (tx) => {
const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
let tokenSizes = {};
for (let token of tokens) {
const url = URL.createObjectURL(new Blob([token.file]));
let image = new Image();
tokenSizes[token.id] = await Dexie.waitFor(
new Promise((resolve) => {
image.onload = () => {
resolve({ width: image.width, height: image.height });
};
image.src = url;
})
);
}
return tx
.table("tokens")
.toCollection()
.modify((token) => {
token.width = tokenSizes[token.id].width;
token.height = tokenSizes[token.id].height;
});
});
}
// Get the dexie database used in DatabaseContext

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 159 KiB

View File

@ -50,7 +50,7 @@ To get access to these settings, click the Show More button under the Name input
A brief summary of these settings is listed below.
- Grid Type: Change the type of grid to use for the map. Currently only the Square type is supported however Hex will be added in a future release.
- Show Grid: When enabled Owlbear Rodeo will draw a grid on top of your map, this is useful if a custom map you have uploaded doesn't include a grid.
- Draw Grid: When enabled Owlbear Rodeo will draw a grid on top of your map, this is useful if a custom map you have uploaded doesn't include a grid.
- Snap to Grid: When enabled tokens, drawing, fog and measurements will attempt to snap to the grid.
- Quality: When uploading a map Owlbear Rodeo will automatically generate various quality options, selecting a lower quality may help speed up map sending in resource constrained environments.
- Allow others to edit: These properties control what other party members can edit when viewing your map.

View File

@ -0,0 +1,20 @@
## Minor Changes
This release fixes a few map sending bugs and reworks map zooming.
- Change map max zoom to be based off of the maps grid size. This means you should always be able to zoom up to see a grid cell almost fullscreen.
- Updated map zoom speed to be linear throughout it's zoom range.
- Update map zoom to be centered on the mouse instead of screen.
- Updated map grid from file name functionality to better reject non-grid values.
- Fixed a bug that caused sending large maps to fail.
- Made advanced map options shown by default.
- Renamed Show Grid map option to Draw Grid to help with the similarity with the Show Grid Controls option.
- Fixed a bug where map states sometimes wouldn't be saved if another player changed the map.
- Fixed a bug that would cause custom maps to not send to Firefox private browsing sessions.
[Reddit]()
[Twitter]()
---
October 24 2020

View File

@ -39,7 +39,7 @@ const gridSizeStd = { x: 14.438842, y: 15.582376 };
const minGridSize = 10;
const maxGridSize = 200;
function gridSizeVaild(x, y) {
export function gridSizeVaild(x, y) {
return (
x > minGridSize && y > minGridSize && x < maxGridSize && y < maxGridSize
);
@ -133,7 +133,16 @@ async function gridSizeML(image, candidates) {
export async function getGridSize(image) {
const candidates = dividers(image.width, image.height);
let prediction = await gridSizeML(image, candidates);
let prediction;
// Try and use ML grid detection
// TODO: Fix possible error on Android
try {
prediction = await gridSizeML(image, candidates);
} catch (error) {
console.error(error);
}
if (!prediction) {
prediction = gridSizeHeuristic(image, candidates);
}
@ -143,3 +152,11 @@ export async function getGridSize(image) {
return prediction;
}
export function getMapMaxZoom(map) {
if (!map) {
return 10;
}
// Return max grid size / 2
return Math.max(Math.min(map.grid.size.x, map.grid.size.y) / 2, 5);
}

View File

@ -0,0 +1,71 @@
import { useEffect, useRef } from "react";
function useImageCenter(
data,
stageRef,
stageWidth,
stageHeight,
stageTranslateRef,
setStageScale,
imageLayerRef,
containerRef,
responsive = false
) {
const stageRatio = stageWidth / stageHeight;
const imageRatio = data ? data.width / data.height : 1;
let imageWidth;
let imageHeight;
if (stageRatio > imageRatio) {
imageWidth = data ? stageHeight / (data.height / data.width) : stageWidth;
imageHeight = stageHeight;
} else {
imageWidth = stageWidth;
imageHeight = data ? stageWidth * (data.height / data.width) : stageHeight;
}
// Reset image translate and stage scale
const previousDataIdRef = useRef();
const previousStageRatioRef = useRef(stageRatio);
useEffect(() => {
if (!data) {
return;
}
const layer = imageLayerRef.current;
const containerRect = containerRef.current.getBoundingClientRect();
const previousDataId = previousDataIdRef.current;
const previousStageRatio = previousStageRatioRef.current;
// Update when the id has changed and if responsive update when the stage changes
const shouldUpdate = responsive
? previousDataId !== data.id || previousStageRatio !== stageRatio
: previousDataId !== data.id;
if (layer && shouldUpdate) {
let newTranslate;
if (stageRatio > imageRatio) {
newTranslate = {
x: -(imageWidth - containerRect.width) / 2,
y: 0,
};
} else {
newTranslate = {
x: 0,
y: -(imageHeight - containerRect.height) / 2,
};
}
layer.position(newTranslate);
stageRef.current.position({ x: 0, y: 0 });
stageTranslateRef.current = { x: 0, y: 0 };
setStageScale(1);
}
previousDataIdRef.current = data.id;
previousStageRatioRef.current = stageRatio;
});
return [imageWidth, imageHeight];
}
export default useImageCenter;

View File

@ -2,16 +2,17 @@ import { useRef } from "react";
import { useGesture } from "react-use-gesture";
import normalizeWheel from "normalize-wheel";
const wheelZoomSpeed = -0.001;
const wheelZoomSpeed = -1;
const touchZoomSpeed = 0.005;
const minZoom = 0.1;
const maxZoom = 10;
function useStageInteraction(
layer,
stage,
stageScale,
onStageScaleChange,
stageTranslateRef,
layer,
maxZoom = 10,
tool = "pan",
preventInteraction = false,
gesture = {}
@ -36,42 +37,85 @@ function useStageInteraction(
return;
}
const newScale = Math.min(
Math.max(stageScale + pixelY * wheelZoomSpeed, minZoom),
Math.max(
stageScale +
(pixelY * wheelZoomSpeed * stageScale) / window.innerHeight,
minZoom
),
maxZoom
);
// Center on pointer
const pointer = stage.getPointerPosition();
const newTranslate = {
x: pointer.x - ((pointer.x - stage.x()) / stageScale) * newScale,
y: pointer.y - ((pointer.y - stage.y()) / stageScale) * newScale,
};
stage.position(newTranslate);
stageTranslateRef.current = newTranslate;
onStageScaleChange(newScale);
gesture.onWheel && gesture.onWheel(props);
},
onPinch: (props) => {
const { da, origin, first } = props;
onPinchStart: (props) => {
const { event } = props;
isInteractingWithCanvas.current =
event.target === layer.getCanvas()._canvas;
const { da, origin } = props;
const [distance] = da;
const [originX, originY] = origin;
if (first) {
pinchPreviousDistanceRef.current = distance;
pinchPreviousOriginRef.current = { x: originX, y: originY };
pinchPreviousDistanceRef.current = distance;
pinchPreviousOriginRef.current = { x: originX, y: originY };
gesture.onPinchStart && gesture.onPinchStart(props);
},
onPinch: (props) => {
if (preventInteraction || !isInteractingWithCanvas.current) {
return;
}
const { da, origin } = props;
const [distance] = da;
const [originX, originY] = origin;
// Apply scale
const distanceDelta = distance - pinchPreviousDistanceRef.current;
const originXDelta = originX - pinchPreviousOriginRef.current.x;
const originYDelta = originY - pinchPreviousOriginRef.current.y;
const newScale = Math.min(
Math.max(stageScale + distanceDelta * touchZoomSpeed, minZoom),
Math.max(
stageScale + distanceDelta * touchZoomSpeed * stageScale,
minZoom
),
maxZoom
);
onStageScaleChange(newScale);
// Apply translate
const stageTranslate = stageTranslateRef.current;
const newTranslate = {
x: stageTranslate.x + originXDelta / newScale,
y: stageTranslate.y + originYDelta / newScale,
const canvasRect = layer.getCanvas()._canvas.getBoundingClientRect();
const relativeOrigin = {
x: originX - canvasRect.left,
y: originY - canvasRect.top,
};
layer.x(newTranslate.x);
layer.y(newTranslate.y);
layer.draw();
// Center on pinch origin
const centeredTranslate = {
x:
relativeOrigin.x -
((relativeOrigin.x - stage.x()) / stageScale) * newScale,
y:
relativeOrigin.y -
((relativeOrigin.y - stage.y()) / stageScale) * newScale,
};
// Add pinch movement
const newTranslate = {
x: centeredTranslate.x + originXDelta,
y: centeredTranslate.y + originYDelta,
};
stage.position(newTranslate);
stageTranslateRef.current = newTranslate;
onStageScaleChange(newScale);
pinchPreviousDistanceRef.current = distance;
pinchPreviousOriginRef.current = { x: originX, y: originY };
gesture.onPinch && gesture.onPinch(props);
@ -92,12 +136,11 @@ function useStageInteraction(
const stageTranslate = stageTranslateRef.current;
if (tool === "pan") {
const newTranslate = {
x: stageTranslate.x + dx / stageScale,
y: stageTranslate.y + dy / stageScale,
x: stageTranslate.x + dx,
y: stageTranslate.y + dy,
};
layer.x(newTranslate.x);
layer.y(newTranslate.y);
layer.draw();
stage.position(newTranslate);
stage.draw();
stageTranslateRef.current = newTranslate;
}
gesture.onDrag && gesture.onDrag(props);

View File

@ -96,7 +96,7 @@ function EditMapModal({ isOpen, onDone, map, mapState }) {
...mapStateSettingChanges,
};
const [showMoreSettings, setShowMoreSettings] = useState(false);
const [showMoreSettings, setShowMoreSettings] = useState(true);
return (
<Modal

View File

@ -16,7 +16,7 @@ import blobToBuffer from "../helpers/blobToBuffer";
import useKeyboard from "../helpers/useKeyboard";
import { resizeImage } from "../helpers/image";
import { useSearch, useGroup, handleItemSelect } from "../helpers/select";
import { getMapDefaultInset, getGridSize } from "../helpers/map";
import { getMapDefaultInset, getGridSize, gridSizeVaild } from "../helpers/map";
import MapDataContext from "../contexts/MapDataContext";
import AuthContext from "../contexts/AuthContext";
@ -120,11 +120,14 @@ function SelectMapModal({
// Match against a regex to find the grid size in the file name
// e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
const gridMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)];
if (gridMatches.length > 0) {
const lastMatch = gridMatches[gridMatches.length - 1];
const matchX = parseInt(lastMatch[1]);
const matchY = parseInt(lastMatch[3]);
if (!isNaN(matchX) && !isNaN(matchY)) {
for (let match of gridMatches) {
const matchX = parseInt(match[1]);
const matchY = parseInt(match[3]);
if (
!isNaN(matchX) &&
!isNaN(matchY) &&
gridSizeVaild(matchX, matchY)
) {
gridSize = { x: matchX, y: matchY };
}
}

View File

@ -109,6 +109,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
category: "character",
hideInSidebar: false,
group: "",
width: image.width,
height: image.height,
});
setImageLoading(false);
resolve();

View File

@ -2,7 +2,7 @@ import SimplePeer from "simple-peer";
import { encode, decode } from "@msgpack/msgpack";
import shortid from "shortid";
import blobToBuffer from "./blobToBuffer";
import blobToBuffer from "../helpers/blobToBuffer";
// Limit buffer size to 16kb to avoid issues with chrome packet size
// http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/
@ -63,7 +63,8 @@ class Connection extends SimplePeer {
const chunks = this.chunk(packedData);
for (let chunk of chunks) {
if (this.dataChannels[channel]) {
this.dataChannels[channel].send(encode(chunk));
// Write to the stream to allow for buffer / backpressure handling
this.dataChannels[channel].write(encode(chunk));
} else {
super.send(encode(chunk));
}

View File

@ -10,7 +10,7 @@ import { omit } from "../helpers/shared";
import useDebounce from "../helpers/useDebounce";
// Load session for auto complete
// eslint-disable-next-line no-unused-vars
import Session from "../helpers/Session";
import Session from "./Session";
import Map from "../components/map/Map";
import Tokens from "../components/token/Tokens";
@ -33,7 +33,9 @@ function NetworkedMapAndTokens({ session }) {
} = useContext(MapLoadingContext);
const { putToken, getToken, updateToken } = useContext(TokenDataContext);
const { putMap, updateMap, getMapFromDB } = useContext(MapDataContext);
const { putMap, updateMap, getMapFromDB, updateMapState } = useContext(
MapDataContext
);
const [currentMap, setCurrentMap] = useState(null);
const [currentMapState, setCurrentMapState] = useState(null);
@ -53,11 +55,9 @@ function NetworkedMapAndTokens({ session }) {
currentMap.owner === userId &&
database
) {
// Update the database directly to avoid re-renders
database
.table("states")
.update(debouncedMapState.mapId, debouncedMapState);
updateMapState(debouncedMapState.mapId, debouncedMapState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentMap, debouncedMapState, userId, database]);
function handleMapChange(newMap, newMapState) {
@ -318,9 +318,10 @@ function NetworkedMapAndTokens({ session }) {
}
}
if (id === "mapResponse") {
await updateMap(data.id, data);
const newMap = await getMapFromDB(data.id);
setCurrentMap(newMap);
const { id, ...update } = data;
await updateMap(id, update);
const updatedMap = await getMapFromDB(data.id);
setCurrentMap(updatedMap);
}
if (id === "mapState") {
setCurrentMapState(data);

View File

@ -2,7 +2,7 @@ import React, { useContext, useState, useEffect, useCallback } from "react";
// Load session for auto complete
// eslint-disable-next-line no-unused-vars
import Session from "../helpers/Session";
import Session from "./Session";
import { isStreamStopped, omit, fromEntries } from "../helpers/shared";
import AuthContext from "../contexts/AuthContext";

View File

@ -3,7 +3,7 @@ import { EventEmitter } from "events";
import Connection from "./Connection";
import { omit } from "./shared";
import { omit } from "../helpers/shared";
/**
* @typedef {object} SessionPeer

View File

@ -10,11 +10,12 @@ import AuthModal from "../modals/AuthModal";
import AuthContext from "../contexts/AuthContext";
import { MapStageProvider } from "../contexts/MapStageContext";
import DatabaseContext from "../contexts/DatabaseContext";
import NetworkedMapAndTokens from "../network/NetworkedMapAndTokens";
import NetworkedParty from "../network/NetworkedParty";
import Session from "../helpers/Session";
import Session from "../network/Session";
const session = new Session();
@ -25,6 +26,7 @@ function Game() {
password,
setAuthenticationStatus,
} = useContext(AuthContext);
const { databaseStatus } = useContext(DatabaseContext);
// Handle authentication status
useEffect(() => {
@ -82,8 +84,10 @@ function Game() {
// Join game
useEffect(() => {
session.joinParty(gameId, password);
}, [gameId, password]);
if (databaseStatus !== "loading") {
session.joinParty(gameId, password);
}
}, [gameId, password, databaseStatus]);
// A ref to the Konva stage
// the ref will be assigned in the MapInteraction component

View File

@ -21,6 +21,7 @@ const v150 = raw("../docs/releaseNotes/v1.5.0.md");
const v151 = raw("../docs/releaseNotes/v1.5.1.md");
const v152 = raw("../docs/releaseNotes/v1.5.2.md");
const v160 = raw("../docs/releaseNotes/v1.6.0.md");
const v161 = raw("../docs/releaseNotes/v1.6.1.md");
function ReleaseNotes() {
const location = useLocation();
@ -45,7 +46,12 @@ function ReleaseNotes() {
<Text mb={2} variant="heading" as="h1" sx={{ fontSize: 5 }}>
Release Notes
</Text>
<div id="v152">
<div id="v161">
<Accordion heading="v1.6.1" defaultOpen>
<Markdown source={v161} />
</Accordion>
</div>
<div id="v160">
<Accordion heading="v1.6.0" defaultOpen>
<Markdown source={v160} />
</Accordion>

View File

@ -87,6 +87,8 @@ export const tokens = Object.keys(tokenSources).map((key) => ({
defaultSize: getDefaultTokenSize(key),
category: "character",
hideInSidebar: false,
width: 256,
height: 256,
}));
export const unknownSource = unknown;