commit
b6b6a86454
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "owlbear-rodeo",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "^4.1.0",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -160,7 +160,7 @@ function MapSettings({
|
|||
onSettingsChange("showGrid", e.target.checked)
|
||||
}
|
||||
/>
|
||||
Show Grid
|
||||
Draw Grid
|
||||
</Label>
|
||||
<Label>
|
||||
<Checkbox
|
||||
|
|
|
@ -109,7 +109,7 @@ function MapTiles({
|
|||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
top: "39px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -114,7 +114,7 @@ function TokenTiles({
|
|||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
top: "39px",
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
};
|
||||
}, []);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 |
|
@ -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.
|
||||
|
|
20
src/docs/releaseNotes/v1.6.1.md
Normal file
20
src/docs/releaseNotes/v1.6.1.md
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
|
71
src/helpers/useImageCenter.js
Normal file
71
src/helpers/useImageCenter.js
Normal 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;
|
|
@ -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);
|
||||
|
|
|
@ -96,7 +96,7 @@ function EditMapModal({ isOpen, onDone, map, mapState }) {
|
|||
...mapStateSettingChanges,
|
||||
};
|
||||
|
||||
const [showMoreSettings, setShowMoreSettings] = useState(false);
|
||||
const [showMoreSettings, setShowMoreSettings] = useState(true);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,6 +109,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
|||
category: "character",
|
||||
hideInSidebar: false,
|
||||
group: "",
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
setImageLoading(false);
|
||||
resolve();
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -3,7 +3,7 @@ import { EventEmitter } from "events";
|
|||
|
||||
import Connection from "./Connection";
|
||||
|
||||
import { omit } from "./shared";
|
||||
import { omit } from "../helpers/shared";
|
||||
|
||||
/**
|
||||
* @typedef {object} SessionPeer
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user