|
@ -1,8 +1,9 @@
|
|||
REACT_APP_BROKER_URL=https://rocket.owlbear.rodeo
|
||||
REACT_APP_ICE_SERVERS_URL=https://rocket.owlbear.rodeo/iceservers
|
||||
REACT_APP_BROKER_URL=https://connect.owlbear.rodeo
|
||||
REACT_APP_ICE_SERVERS_URL=https://connect.owlbear.rodeo/iceservers
|
||||
REACT_APP_STRIPE_API_KEY=pk_live_MJjzi5djj524Y7h3fL5PNh4e00a852XD51
|
||||
REACT_APP_STRIPE_URL=https://payment.owlbear.rodeo
|
||||
REACT_APP_VERSION=$npm_package_version
|
||||
REACT_APP_PREVIEW=false
|
||||
REACT_APP_LOGGING=true
|
||||
REACT_APP_FATHOM_SITE_ID=VMSHBPKD
|
||||
REACT_APP_FATHOM_SITE_ID=VMSHBPKD
|
||||
REACT_APP_SENTRY_DSN=https://5257021c3a114649baa5e3b8ba775bfe@o467475.ingest.sentry.io/5493956
|
||||
|
|
52
package.json
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"name": "owlbear-rodeo",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "^4.2.0",
|
||||
"@babylonjs/loaders": "^4.2.0",
|
||||
"@mitchemmc/dexie-export-import": "^1.0.1",
|
||||
"@msgpack/msgpack": "^2.3.0",
|
||||
"@sentry/react": "^5.27.1",
|
||||
"@stripe/stripe-js": "^1.3.2",
|
||||
"@tensorflow/tfjs": "^2.6.0",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/react": "^11.2.0",
|
||||
"@testing-library/user-event": "^12.2.2",
|
||||
"@msgpack/msgpack": "^2.4.1",
|
||||
"@sentry/react": "^6.2.2",
|
||||
"@stripe/stripe-js": "^1.13.1",
|
||||
"@tensorflow/tfjs": "^3.3.0",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^13.0.2",
|
||||
"ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778",
|
||||
"case": "^1.6.3",
|
||||
"color": "^3.1.3",
|
||||
|
@ -20,42 +20,44 @@
|
|||
"deep-diff": "^1.0.2",
|
||||
"dexie": "^3.0.3",
|
||||
"dexie-observable": "^3.0.0-beta.10",
|
||||
"err-code": "^2.0.3",
|
||||
"err-code": "^3.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuse.js": "^6.4.1",
|
||||
"interactjs": "^1.9.7",
|
||||
"konva": "^7.1.8",
|
||||
"fuse.js": "^6.4.6",
|
||||
"interactjs": "^1.10.8",
|
||||
"konva": "^7.2.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"polygon-clipping": "^0.15.1",
|
||||
"pretty-bytes": "^5.4.1",
|
||||
"pepjs": "^0.5.3",
|
||||
"polygon-clipping": "^0.15.2",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"raw.macro": "^0.4.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-konva": "^17.0.0-0",
|
||||
"react-konva": "^17.0.1-3",
|
||||
"react-markdown": "4",
|
||||
"react-media": "^2.0.0-rc.1",
|
||||
"react-modal": "^3.11.2",
|
||||
"react-modal": "^3.12.1",
|
||||
"react-resize-detector": "4.2.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-router-hash-link": "^2.2.2",
|
||||
"react-scripts": "4.0.0",
|
||||
"react-select": "^3.1.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-select": "^4.2.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-toast-notifications": "^2.4.0",
|
||||
"react-use-gesture": "^8.0.1",
|
||||
"react-toast-notifications": "^2.4.3",
|
||||
"react-use-gesture": "^9.1.3",
|
||||
"shortid": "^2.2.15",
|
||||
"simple-peer": "feross/simple-peer#694/head",
|
||||
"simplebar-react": "^2.1.0",
|
||||
"simplify-js": "^1.2.4",
|
||||
"socket.io-client": "^3.0.3",
|
||||
"source-map-explorer": "^2.4.2",
|
||||
"socket.io-client": "^4.0.0",
|
||||
"socket.io-msgpack-parser": "^3.0.1",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"theme-ui": "^0.3.1",
|
||||
"use-image": "^1.0.5",
|
||||
"webrtc-adapter": "^7.5.1"
|
||||
"use-image": "^1.0.7",
|
||||
"webrtc-adapter": "^7.7.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"simple-peer/get-browser-rtc": "substack/get-browser-rtc#4/head"
|
||||
|
@ -83,6 +85,6 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"worker-loader": "^3.0.5"
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 99 KiB |
67
src/App.js
|
@ -18,6 +18,7 @@ import { TokenDataProvider } from "./contexts/TokenDataContext";
|
|||
import { MapLoadingProvider } from "./contexts/MapLoadingContext";
|
||||
import { SettingsProvider } from "./contexts/SettingsContext";
|
||||
import { KeyboardProvider } from "./contexts/KeyboardContext";
|
||||
import { ImageSourcesProvider } from "./contexts/ImageSourceContext";
|
||||
|
||||
import { ToastProvider } from "./components/Toast";
|
||||
|
||||
|
@ -29,38 +30,40 @@ function App() {
|
|||
<AuthProvider>
|
||||
<KeyboardProvider>
|
||||
<ToastProvider>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/donate">
|
||||
<Donate />
|
||||
</Route>
|
||||
{/* Legacy support camel case routes */}
|
||||
<Route path={["/howTo", "/how-to"]}>
|
||||
<HowTo />
|
||||
</Route>
|
||||
<Route path={["/releaseNotes", "/release-notes"]}>
|
||||
<ReleaseNotes />
|
||||
</Route>
|
||||
<Route path="/about">
|
||||
<About />
|
||||
</Route>
|
||||
<Route path="/faq">
|
||||
<FAQ />
|
||||
</Route>
|
||||
<Route path="/game/:id">
|
||||
<MapLoadingProvider>
|
||||
<MapDataProvider>
|
||||
<TokenDataProvider>
|
||||
<Game />
|
||||
</TokenDataProvider>
|
||||
</MapDataProvider>
|
||||
</MapLoadingProvider>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
<ImageSourcesProvider>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/donate">
|
||||
<Donate />
|
||||
</Route>
|
||||
{/* Legacy support camel case routes */}
|
||||
<Route path={["/howTo", "/how-to"]}>
|
||||
<HowTo />
|
||||
</Route>
|
||||
<Route path={["/releaseNotes", "/release-notes"]}>
|
||||
<ReleaseNotes />
|
||||
</Route>
|
||||
<Route path="/about">
|
||||
<About />
|
||||
</Route>
|
||||
<Route path="/faq">
|
||||
<FAQ />
|
||||
</Route>
|
||||
<Route path="/game/:id">
|
||||
<MapLoadingProvider>
|
||||
<MapDataProvider>
|
||||
<TokenDataProvider>
|
||||
<Game />
|
||||
</TokenDataProvider>
|
||||
</MapDataProvider>
|
||||
</MapLoadingProvider>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</ImageSourcesProvider>
|
||||
</ToastProvider>
|
||||
</KeyboardProvider>
|
||||
</AuthProvider>
|
||||
|
|
|
@ -4,7 +4,12 @@ import useImage from "use-image";
|
|||
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
|
||||
import { useGrid } from "../contexts/GridContext";
|
||||
import {
|
||||
useGrid,
|
||||
useGridPixelSize,
|
||||
useGridOffset,
|
||||
useGridCellPixelSize,
|
||||
} from "../contexts/GridContext";
|
||||
|
||||
import squarePatternDark from "../images/SquarePatternDark.png";
|
||||
import squarePatternLight from "../images/SquarePatternLight.png";
|
||||
|
@ -12,7 +17,11 @@ import hexPatternDark from "../images/HexPatternDark.png";
|
|||
import hexPatternLight from "../images/HexPatternLight.png";
|
||||
|
||||
function Grid({ stroke }) {
|
||||
const { grid, gridPixelSize, gridOffset, gridCellPixelSize } = useGrid();
|
||||
const grid = useGrid();
|
||||
const gridPixelSize = useGridPixelSize();
|
||||
const gridOffset = useGridOffset();
|
||||
const gridCellPixelSize = useGridCellPixelSize();
|
||||
|
||||
let imageSource;
|
||||
if (grid.type === "square") {
|
||||
if (stroke === "black") {
|
||||
|
|
|
@ -55,11 +55,7 @@ function Tile({
|
|||
e.stopPropagation();
|
||||
onSelect();
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
if (canEdit) {
|
||||
onDoubleClick(e);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
<UIImage
|
||||
sx={{
|
||||
|
@ -71,6 +67,7 @@ function Tile({
|
|||
left: 0,
|
||||
}}
|
||||
src={src}
|
||||
alt={title}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
|
|
|
@ -2,7 +2,13 @@ import React from "react";
|
|||
import Modal from "react-modal";
|
||||
import { useThemeUI, Close } from "theme-ui";
|
||||
|
||||
function Banner({ isOpen, onRequestClose, children, allowClose }) {
|
||||
function Banner({
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
children,
|
||||
allowClose,
|
||||
backgroundColor,
|
||||
}) {
|
||||
const { theme } = useThemeUI();
|
||||
|
||||
return (
|
||||
|
@ -12,7 +18,7 @@ function Banner({ isOpen, onRequestClose, children, allowClose }) {
|
|||
style={{
|
||||
overlay: { bottom: "0", top: "initial", zIndex: 2000 },
|
||||
content: {
|
||||
backgroundColor: theme.colors.highlight,
|
||||
backgroundColor: backgroundColor || theme.colors.highlight,
|
||||
color: "hsl(210, 50%, 96%)",
|
||||
top: "initial",
|
||||
left: "50%",
|
||||
|
@ -21,8 +27,8 @@ function Banner({ isOpen, onRequestClose, children, allowClose }) {
|
|||
bottom: "env(safe-area-inset-bottom)",
|
||||
border: "none",
|
||||
padding: "8px",
|
||||
margin: "8px",
|
||||
paddingRight: "24px",
|
||||
margin: "8px 0",
|
||||
paddingRight: allowClose ? "24px" : "8px",
|
||||
maxWidth: "500px",
|
||||
transform: "translateX(-50%)",
|
||||
},
|
18
src/components/banner/ErrorBanner.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from "react";
|
||||
import { Box, Text } from "theme-ui";
|
||||
|
||||
import Banner from "./Banner";
|
||||
|
||||
function ErrorBanner({ error, onRequestClose }) {
|
||||
return (
|
||||
<Banner isOpen={!!error} onRequestClose={onRequestClose}>
|
||||
<Box p={1}>
|
||||
<Text as="p" variant="body2">
|
||||
Error: {error && error.message}
|
||||
</Text>
|
||||
</Box>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorBanner;
|
33
src/components/banner/OfflineBanner.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
|
||||
import Banner from "./Banner";
|
||||
import OfflineIcon from "../../icons/OfflineIcon";
|
||||
|
||||
function OfflineBanner({ isOpen }) {
|
||||
return (
|
||||
<Banner
|
||||
isOpen={isOpen}
|
||||
onRequestClose={() => {}}
|
||||
allowClose={false}
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
width: "28px",
|
||||
height: "28px",
|
||||
borderRadius: "28px",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
bg="overlay"
|
||||
title="Unable to connect to game, refresh to reconnect."
|
||||
aria-label="Unable to connect to game, refresh to reconnect."
|
||||
>
|
||||
<OfflineIcon />
|
||||
</Flex>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
export default OfflineBanner;
|
33
src/components/banner/ReconnectBanner.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
|
||||
import Banner from "./Banner";
|
||||
import ReconnectingIcon from "../../icons/ReconnectingIcon";
|
||||
|
||||
function ReconnectBanner({ isOpen }) {
|
||||
return (
|
||||
<Banner
|
||||
isOpen={isOpen}
|
||||
onRequestClose={() => {}}
|
||||
allowClose={false}
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
width: "28px",
|
||||
height: "28px",
|
||||
borderRadius: "28px",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
bg="overlay"
|
||||
title="Disconnected. Attempting to reconnect..."
|
||||
aria-label="Disconnected. Attempting to reconnect..."
|
||||
>
|
||||
<ReconnectingIcon />
|
||||
</Flex>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReconnectBanner;
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Box, Text } from "theme-ui";
|
||||
import { Engine } from "@babylonjs/core/Engines/engine";
|
||||
import { Scene } from "@babylonjs/core/scene";
|
||||
import { Vector3, Color4, Matrix } from "@babylonjs/core/Maths/math";
|
||||
|
@ -19,7 +18,7 @@ import ReactResizeDetector from "react-resize-detector";
|
|||
|
||||
import usePreventTouch from "../../hooks/usePreventTouch";
|
||||
|
||||
import Banner from "../Banner";
|
||||
import ErrorBanner from "../banner/ErrorBanner";
|
||||
|
||||
const diceThrowSpeed = 2;
|
||||
|
||||
|
@ -166,13 +165,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
|
|||
style={{ outline: "none" }}
|
||||
/>
|
||||
</ReactResizeDetector>
|
||||
<Banner isOpen={!!error} onRequestClose={() => setError()}>
|
||||
<Box p={1}>
|
||||
<Text as="p" variant="body2">
|
||||
Error: {error && error.message}
|
||||
</Text>
|
||||
</Box>
|
||||
</Banner>
|
||||
<ErrorBanner error={error} onRequestClose={() => setError()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
|
||||
import Tile from "../Tile";
|
||||
|
||||
function DiceTile({ dice, isSelected, onDiceSelect, onDone, large }) {
|
||||
function DiceTile({ dice, isSelected, onDiceSelect, onDone, size }) {
|
||||
return (
|
||||
<Tile
|
||||
src={dice.preview}
|
||||
|
@ -10,7 +10,7 @@ function DiceTile({ dice, isSelected, onDiceSelect, onDone, large }) {
|
|||
isSelected={isSelected}
|
||||
onSelect={() => onDiceSelect(dice)}
|
||||
onDoubleClick={() => onDone(dice)}
|
||||
large={large}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
import SimpleBar from "simplebar-react";
|
||||
import { useMedia } from "react-media";
|
||||
|
||||
import DiceTile from "./DiceTile";
|
||||
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
|
||||
const isSmallScreen = useMedia({ query: "(max-width: 500px)" });
|
||||
const layout = useResponsiveLayout();
|
||||
|
||||
return (
|
||||
<SimpleBar style={{ maxHeight: "300px" }}>
|
||||
<SimpleBar
|
||||
style={{ height: layout.screenSize === "large" ? "600px" : "400px" }}
|
||||
>
|
||||
<Flex
|
||||
p={2}
|
||||
pb={4}
|
||||
bg="muted"
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
borderRadius: "4px",
|
||||
minHeight: layout.screenSize === "large" ? "600px" : "400px",
|
||||
alignContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
{dice.map((dice) => (
|
||||
|
@ -25,7 +31,7 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
|
|||
isSelected={selectedDice && dice.key === selectedDice.key}
|
||||
onDiceSelect={onDiceSelect}
|
||||
onDone={onDone}
|
||||
large={isSmallScreen}
|
||||
size={layout.tileSize}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
|
|
|
@ -209,7 +209,6 @@ function Map({
|
|||
tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup}
|
||||
dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)}
|
||||
token={tokensById[tokenDraggingOptions.tokenState.tokenId]}
|
||||
mapState={mapState}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -2,9 +2,17 @@ import React, { useState, useEffect } from "react";
|
|||
import shortid from "shortid";
|
||||
import { Group, Line, Rect, Circle } from "react-konva";
|
||||
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import {
|
||||
useDebouncedStageScale,
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
useInteractionEmitter,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
import { useGrid } from "../../contexts/GridContext";
|
||||
import {
|
||||
useGridCellNormalizedSize,
|
||||
useGridStrokeWidth,
|
||||
} from "../../contexts/GridContext";
|
||||
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
import {
|
||||
|
@ -25,13 +33,14 @@ function MapDrawing({
|
|||
active,
|
||||
toolSettings,
|
||||
}) {
|
||||
const {
|
||||
stageScale,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
interactionEmitter,
|
||||
} = useMapInteraction();
|
||||
const { gridCellNormalizedSize, gridStrokeWidth } = useGrid();
|
||||
const stageScale = useDebouncedStageScale();
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const interactionEmitter = useInteractionEmitter();
|
||||
|
||||
const gridCellNormalizedSize = useGridCellNormalizedSize();
|
||||
const gridStrokeWidth = useGridStrokeWidth();
|
||||
|
||||
const mapStageRef = useMapStage();
|
||||
const [drawingShape, setDrawingShape] = useState(null);
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useRef, useContext } from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import { Box, IconButton } from "theme-ui";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
|
@ -10,9 +10,9 @@ import useImageCenter from "../../hooks/useImageCenter";
|
|||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
import { getGridDefaultInset, getGridMaxZoom } from "../../helpers/grid";
|
||||
import KonvaBridge from "../../helpers/KonvaBridge";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
import KeyboardContext from "../../contexts/KeyboardContext";
|
||||
import { GridProvider } from "../../contexts/GridContext";
|
||||
|
||||
import ResetMapIcon from "../../icons/ResetMapIcon";
|
||||
|
@ -90,11 +90,9 @@ function MapEditor({ map, onSettingsChange }) {
|
|||
setPreventMapInteraction,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
interactionEmitter: null,
|
||||
};
|
||||
|
||||
// Get keyboard context to pass to Konva
|
||||
const keyboardValue = useContext(KeyboardContext);
|
||||
|
||||
const canEditGrid = map.type !== "default";
|
||||
|
||||
const gridChanged =
|
||||
|
@ -106,77 +104,90 @@ function MapEditor({ map, onSettingsChange }) {
|
|||
const layout = useResponsiveLayout();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: layout.screenSize === "large" ? "500px" : "300px",
|
||||
cursor: "move",
|
||||
touchAction: "none",
|
||||
outline: "none",
|
||||
position: "relative",
|
||||
}}
|
||||
bg="muted"
|
||||
ref={containerRef}
|
||||
>
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||
<Stage
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
ref={mapStageRef}
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: layout.screenSize === "large" ? "500px" : "300px",
|
||||
cursor: "move",
|
||||
touchAction: "none",
|
||||
outline: "none",
|
||||
position: "relative",
|
||||
}}
|
||||
bg="muted"
|
||||
ref={containerRef}
|
||||
>
|
||||
<Layer ref={mapLayerRef}>
|
||||
<Image image={mapImageSource} width={mapWidth} height={mapHeight} />
|
||||
<KeyboardContext.Provider value={keyboardValue}>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||
<KonvaBridge
|
||||
stageRender={(children) => (
|
||||
<Stage
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
ref={mapStageRef}
|
||||
>
|
||||
{children}
|
||||
</Stage>
|
||||
)}
|
||||
>
|
||||
<Layer ref={mapLayerRef}>
|
||||
<Image
|
||||
image={mapImageSource}
|
||||
width={mapWidth}
|
||||
height={mapHeight}
|
||||
/>
|
||||
{showGridControls && canEditGrid && (
|
||||
<GridProvider
|
||||
grid={map.grid}
|
||||
width={mapWidth}
|
||||
height={mapHeight}
|
||||
>
|
||||
<>
|
||||
<MapGrid map={map} />
|
||||
<MapGridEditor map={map} onGridChange={handleGridChange} />
|
||||
</GridProvider>
|
||||
</>
|
||||
)}
|
||||
</MapInteractionProvider>
|
||||
</KeyboardContext.Provider>
|
||||
</Layer>
|
||||
</Stage>
|
||||
</ReactResizeDetector>
|
||||
{gridChanged && (
|
||||
<IconButton
|
||||
title="Reset Grid"
|
||||
aria-label="Reset Grid"
|
||||
onClick={handleMapReset}
|
||||
bg="overlay"
|
||||
sx={{ borderRadius: "50%", position: "absolute", bottom: 0, left: 0 }}
|
||||
m={2}
|
||||
>
|
||||
<ResetMapIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
{canEditGrid && (
|
||||
<IconButton
|
||||
title={showGridControls ? "Hide Grid Controls" : "Show Grid Controls"}
|
||||
aria-label={
|
||||
showGridControls ? "Hide Grid Controls" : "Show Grid Controls"
|
||||
}
|
||||
onClick={() => setShowGridControls(!showGridControls)}
|
||||
bg="overlay"
|
||||
sx={{
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}}
|
||||
m={2}
|
||||
p="6px"
|
||||
>
|
||||
{showGridControls ? <GridOnIcon /> : <GridOffIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Layer>
|
||||
</KonvaBridge>
|
||||
</ReactResizeDetector>
|
||||
{gridChanged && (
|
||||
<IconButton
|
||||
title="Reset Grid"
|
||||
aria-label="Reset Grid"
|
||||
onClick={handleMapReset}
|
||||
bg="overlay"
|
||||
sx={{
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
}}
|
||||
m={2}
|
||||
>
|
||||
<ResetMapIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
{canEditGrid && (
|
||||
<IconButton
|
||||
title={
|
||||
showGridControls ? "Hide Grid Controls" : "Show Grid Controls"
|
||||
}
|
||||
aria-label={
|
||||
showGridControls ? "Hide Grid Controls" : "Show Grid Controls"
|
||||
}
|
||||
onClick={() => setShowGridControls(!showGridControls)}
|
||||
bg="overlay"
|
||||
sx={{
|
||||
borderRadius: "50%",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}}
|
||||
m={2}
|
||||
p="6px"
|
||||
>
|
||||
{showGridControls ? <GridOnIcon /> : <GridOffIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</GridProvider>
|
||||
</MapInteractionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,21 @@ import useImage from "use-image";
|
|||
|
||||
import diagonalPattern from "../../images/DiagonalPattern.png";
|
||||
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import {
|
||||
useDebouncedStageScale,
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
useInteractionEmitter,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
import { useGrid } from "../../contexts/GridContext";
|
||||
import {
|
||||
useGrid,
|
||||
useGridCellPixelSize,
|
||||
useGridCellNormalizedSize,
|
||||
useGridStrokeWidth,
|
||||
useGridCellPixelOffset,
|
||||
useGridOffset,
|
||||
} from "../../contexts/GridContext";
|
||||
import { useKeyboard } from "../../contexts/KeyboardContext";
|
||||
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
@ -30,6 +42,8 @@ import SubtractShapeAction from "../../actions/SubtractShapeAction";
|
|||
|
||||
import useSetting from "../../hooks/useSetting";
|
||||
|
||||
import shortcuts from "../../shortcuts";
|
||||
|
||||
function MapFog({
|
||||
map,
|
||||
shapes,
|
||||
|
@ -41,21 +55,21 @@ function MapFog({
|
|||
toolSettings,
|
||||
editable,
|
||||
}) {
|
||||
const {
|
||||
stageScale,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
interactionEmitter,
|
||||
} = useMapInteraction();
|
||||
const {
|
||||
grid,
|
||||
gridCellNormalizedSize,
|
||||
gridCellPixelSize,
|
||||
gridStrokeWidth,
|
||||
gridCellPixelOffset,
|
||||
gridOffset,
|
||||
} = useGrid();
|
||||
const stageScale = useDebouncedStageScale();
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const interactionEmitter = useInteractionEmitter();
|
||||
|
||||
const grid = useGrid();
|
||||
const gridCellNormalizedSize = useGridCellNormalizedSize();
|
||||
const gridCellPixelSize = useGridCellPixelSize();
|
||||
const gridStrokeWidth = useGridStrokeWidth();
|
||||
const gridCellPixelOffset = useGridCellPixelOffset();
|
||||
const gridOffset = useGridOffset();
|
||||
|
||||
const [gridSnappingSensitivity] = useSetting("map.gridSnappingSensitivity");
|
||||
const [showFogGuides] = useSetting("fog.showGuides");
|
||||
const [editOpacity] = useSetting("fog.editOpacity");
|
||||
const mapStageRef = useMapStage();
|
||||
|
||||
const [drawingShape, setDrawingShape] = useState(null);
|
||||
|
@ -73,11 +87,13 @@ function MapFog({
|
|||
editable &&
|
||||
(toolSettings.type === "toggle" || toolSettings.type === "remove");
|
||||
|
||||
const shouldRenderGuides =
|
||||
const shouldUseGuides =
|
||||
active &&
|
||||
editable &&
|
||||
(toolSettings.type === "rectangle" || toolSettings.type === "polygon");
|
||||
|
||||
const shouldRenderGuides = shouldUseGuides && showFogGuides;
|
||||
|
||||
const [patternImage] = useImage(diagonalPattern);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -90,7 +106,7 @@ function MapFog({
|
|||
function getBrushPosition(snapping = true) {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
let position = getRelativePointerPosition(mapImage);
|
||||
if (snapping && shouldRenderGuides) {
|
||||
if (shouldUseGuides && snapping) {
|
||||
for (let guide of guides) {
|
||||
if (guide.orientation === "vertical") {
|
||||
position.x = guide.start.x * mapWidth;
|
||||
|
@ -157,11 +173,16 @@ function MapFog({
|
|||
) {
|
||||
return prevShape;
|
||||
}
|
||||
const simplified = simplifyPoints(
|
||||
[...prevPoints, brushPosition],
|
||||
gridCellNormalizedSize,
|
||||
stageScale / 4
|
||||
);
|
||||
return {
|
||||
...prevShape,
|
||||
data: {
|
||||
...prevShape.data,
|
||||
points: [...prevPoints, brushPosition],
|
||||
points: simplified,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -192,15 +213,12 @@ function MapFog({
|
|||
drawingShape
|
||||
) {
|
||||
const cut = toolSettings.useFogCut;
|
||||
|
||||
let drawingShapes = [drawingShape];
|
||||
if (!toolSettings.multilayer) {
|
||||
const shapesToSubtract = shapes.filter((shape) =>
|
||||
cut ? !shape.visible : shape.visible
|
||||
);
|
||||
const subtractAction = new SubtractShapeAction(
|
||||
mergeFogShapes(shapesToSubtract, !cut)
|
||||
);
|
||||
const subtractAction = new SubtractShapeAction(shapesToSubtract);
|
||||
const state = subtractAction.execute({
|
||||
[drawingShape.id]: drawingShape,
|
||||
});
|
||||
|
@ -211,24 +229,15 @@ function MapFog({
|
|||
|
||||
if (drawingShapes.length > 0) {
|
||||
drawingShapes = drawingShapes.map((shape) => {
|
||||
let shapeData = {};
|
||||
if (cut) {
|
||||
shapeData = { id: shape.id, type: shape.type };
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
data: shape.data,
|
||||
};
|
||||
} else {
|
||||
shapeData = { ...shape, color: "black" };
|
||||
return { ...shape, color: "black" };
|
||||
}
|
||||
return {
|
||||
...shapeData,
|
||||
data: {
|
||||
...shape.data,
|
||||
points: simplifyPoints(
|
||||
shape.data.points,
|
||||
gridCellNormalizedSize,
|
||||
// Downscale fog as smoothing doesn't currently work with edge snapping
|
||||
Math.max(stageScale, 1) / 2
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (cut) {
|
||||
|
@ -275,10 +284,7 @@ function MapFog({
|
|||
}
|
||||
|
||||
function handlePointerMove() {
|
||||
if (
|
||||
active &&
|
||||
(toolSettings.type === "polygon" || toolSettings.type === "rectangle")
|
||||
) {
|
||||
if (shouldUseGuides) {
|
||||
let guides = [];
|
||||
const brushPosition = getBrushPosition(false);
|
||||
const absoluteBrushPosition = Vector2.multiply(brushPosition, {
|
||||
|
@ -371,9 +377,7 @@ function MapFog({
|
|||
const shapesToSubtract = shapes.filter((shape) =>
|
||||
cut ? !shape.visible : shape.visible
|
||||
);
|
||||
const subtractAction = new SubtractShapeAction(
|
||||
mergeFogShapes(shapesToSubtract, !cut)
|
||||
);
|
||||
const subtractAction = new SubtractShapeAction(shapesToSubtract);
|
||||
const state = subtractAction.execute({
|
||||
[polygonShape.id]: polygonShape,
|
||||
});
|
||||
|
@ -401,16 +405,20 @@ function MapFog({
|
|||
}, [toolSettings, drawingShape, onShapesCut, onShapesAdd, shapes]);
|
||||
|
||||
// Add keyboard shortcuts
|
||||
function handleKeyDown({ key }) {
|
||||
if (key === "Enter" && toolSettings.type === "polygon" && drawingShape) {
|
||||
function handleKeyDown(event) {
|
||||
if (
|
||||
shortcuts.fogFinishPolygon(event) &&
|
||||
toolSettings.type === "polygon" &&
|
||||
drawingShape
|
||||
) {
|
||||
finishDrawingPolygon();
|
||||
}
|
||||
if (key === "Escape" && drawingShape) {
|
||||
if (shortcuts.fogCancelPolygon(event) && drawingShape) {
|
||||
setDrawingShape(null);
|
||||
}
|
||||
// Remove last point from polygon shape if delete pressed
|
||||
if (
|
||||
(key === "Backspace" || key === "Delete") &&
|
||||
shortcuts.delete(event) &&
|
||||
drawingShape &&
|
||||
toolSettings.type === "polygon"
|
||||
) {
|
||||
|
@ -492,14 +500,18 @@ function MapFog({
|
|||
onTouchEnd={eraseHoveredShapes}
|
||||
points={points}
|
||||
stroke={
|
||||
editable ? colors.lightGray : colors[shape.color] || shape.color
|
||||
editable && active
|
||||
? colors.lightGray
|
||||
: colors[shape.color] || shape.color
|
||||
}
|
||||
fill={colors[shape.color] || shape.color}
|
||||
closed
|
||||
lineCap="round"
|
||||
lineJoin="round"
|
||||
strokeWidth={gridStrokeWidth * shape.strokeWidth}
|
||||
opacity={editable ? (!shape.visible ? 0.2 : 0.5) : 1}
|
||||
opacity={
|
||||
editable ? (!shape.visible ? editOpacity / 2 : editOpacity) : 1
|
||||
}
|
||||
fillPatternImage={patternImage}
|
||||
fillPriority={editable && !shape.visible ? "pattern" : "color"}
|
||||
holes={holes}
|
||||
|
@ -566,12 +578,17 @@ function MapFog({
|
|||
|
||||
if (editable) {
|
||||
const visibleShapes = shapes.filter(shapeVisible);
|
||||
setFogShapeBoundingBoxes(getFogShapesBoundingBoxes(visibleShapes));
|
||||
// Only use bounding box guides when rendering them
|
||||
if (shouldRenderGuides) {
|
||||
setFogShapeBoundingBoxes(getFogShapesBoundingBoxes(visibleShapes, 5));
|
||||
} else {
|
||||
setFogShapeBoundingBoxes([]);
|
||||
}
|
||||
setFogShapes(visibleShapes);
|
||||
} else {
|
||||
setFogShapes(mergeFogShapes(shapes));
|
||||
}
|
||||
}, [shapes, editable, active, toolSettings]);
|
||||
}, [shapes, editable, active, toolSettings, shouldRenderGuides]);
|
||||
|
||||
const fogGroupRef = useRef();
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import useImage from "use-image";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import { mapSources as defaultMapSources } from "../../maps";
|
||||
|
||||
import { getImageLightness } from "../../helpers/image";
|
||||
|
@ -17,7 +18,7 @@ function MapGrid({ map }) {
|
|||
mapSourceMap = map.resolutions[resolutionArray[0]];
|
||||
}
|
||||
}
|
||||
const mapSource = useDataSource(mapSourceMap, defaultMapSources);
|
||||
const mapSource = useImageSource(mapSourceMap, defaultMapSources);
|
||||
const [mapImage, mapLoadingStatus] = useImage(mapSource);
|
||||
|
||||
const [isImageLight, setIsImageLight] = useState(true);
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import React, { useRef } from "react";
|
||||
import { Group, Circle, Rect } from "react-konva";
|
||||
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import {
|
||||
useDebouncedStageScale,
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
useSetPreventMapInteraction,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useKeyboard } from "../../contexts/KeyboardContext";
|
||||
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
import shortcuts from "../../shortcuts";
|
||||
|
||||
function MapGridEditor({ map, onGridChange }) {
|
||||
const {
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
stageScale,
|
||||
setPreventMapInteraction,
|
||||
} = useMapInteraction();
|
||||
const stageScale = useDebouncedStageScale();
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const setPreventMapInteraction = useSetPreventMapInteraction();
|
||||
|
||||
const mapSize = { x: mapWidth, y: mapHeight };
|
||||
|
||||
|
@ -163,20 +168,19 @@ function MapGridEditor({ map, onGridChange }) {
|
|||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
const { key, shiftKey } = event;
|
||||
const nudgeAmount = shiftKey ? 2 : 0.5;
|
||||
if (key === "ArrowUp") {
|
||||
const nudgeAmount = event.shiftKey ? 2 : 0.5;
|
||||
if (shortcuts.gridNudgeUp(event)) {
|
||||
// Stop arrow up/down scrolling if overflowing
|
||||
event.preventDefault();
|
||||
nudgeGrid({ x: 0, y: -1 }, nudgeAmount);
|
||||
}
|
||||
if (key === "ArrowLeft") {
|
||||
if (shortcuts.gridNudgeLeft(event)) {
|
||||
nudgeGrid({ x: -1, y: 0 }, nudgeAmount);
|
||||
}
|
||||
if (key === "ArrowRight") {
|
||||
if (shortcuts.gridNudgeRight(event)) {
|
||||
nudgeGrid({ x: 1, y: 0 }, nudgeAmount);
|
||||
}
|
||||
if (key === "ArrowDown") {
|
||||
if (shortcuts.gridNudgeDown(event)) {
|
||||
event.preventDefault();
|
||||
nudgeGrid({ x: 0, y: 1 }, nudgeAmount);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef, useEffect, useState, useContext } from "react";
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Box } from "theme-ui";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
|
@ -10,18 +10,15 @@ import useStageInteraction from "../../hooks/useStageInteraction";
|
|||
import useImageCenter from "../../hooks/useImageCenter";
|
||||
|
||||
import { getGridMaxZoom } from "../../helpers/grid";
|
||||
import KonvaBridge from "../../helpers/KonvaBridge";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
import { MapStageProvider, useMapStage } from "../../contexts/MapStageContext";
|
||||
import AuthContext, { useAuth } from "../../contexts/AuthContext";
|
||||
import SettingsContext, { useSettings } from "../../contexts/SettingsContext";
|
||||
import KeyboardContext from "../../contexts/KeyboardContext";
|
||||
import TokenDataContext, {
|
||||
useTokenData,
|
||||
} from "../../contexts/TokenDataContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
import { GridProvider } from "../../contexts/GridContext";
|
||||
import { useKeyboard } from "../../contexts/KeyboardContext";
|
||||
|
||||
import shortcuts from "../../shortcuts";
|
||||
|
||||
function MapInteraction({
|
||||
map,
|
||||
mapState,
|
||||
|
@ -116,12 +113,12 @@ function MapInteraction({
|
|||
|
||||
function handleKeyDown(event) {
|
||||
// Change to move tool when pressing space
|
||||
if (event.key === " " && selectedToolId === "move") {
|
||||
if (shortcuts.move(event) && selectedToolId === "move") {
|
||||
// Stop active state on move icon from being selected
|
||||
event.preventDefault();
|
||||
}
|
||||
if (
|
||||
event.key === " " &&
|
||||
shortcuts.move(event) &&
|
||||
selectedToolId !== "move" &&
|
||||
!disabledControls.includes("move")
|
||||
) {
|
||||
|
@ -131,35 +128,33 @@ function MapInteraction({
|
|||
}
|
||||
|
||||
// Basic keyboard shortcuts
|
||||
if (event.key === "w" && !disabledControls.includes("move")) {
|
||||
if (shortcuts.moveTool(event) && !disabledControls.includes("move")) {
|
||||
onSelectedToolChange("move");
|
||||
}
|
||||
if (event.key === "d" && !disabledControls.includes("drawing")) {
|
||||
if (shortcuts.drawingTool(event) && !disabledControls.includes("drawing")) {
|
||||
onSelectedToolChange("drawing");
|
||||
}
|
||||
if (event.key === "f" && !disabledControls.includes("fog")) {
|
||||
if (shortcuts.fogTool(event) && !disabledControls.includes("fog")) {
|
||||
onSelectedToolChange("fog");
|
||||
}
|
||||
if (event.key === "m" && !disabledControls.includes("measure")) {
|
||||
if (shortcuts.measureTool(event) && !disabledControls.includes("measure")) {
|
||||
onSelectedToolChange("measure");
|
||||
}
|
||||
if (event.key === "q" && !disabledControls.includes("pointer")) {
|
||||
if (shortcuts.pointerTool(event) && !disabledControls.includes("pointer")) {
|
||||
onSelectedToolChange("pointer");
|
||||
}
|
||||
if (event.key === "n" && !disabledControls.includes("note")) {
|
||||
if (shortcuts.noteTool(event) && !disabledControls.includes("note")) {
|
||||
onSelectedToolChange("note");
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyUp(event) {
|
||||
if (event.key === " " && selectedToolId === "move") {
|
||||
if (shortcuts.move(event) && selectedToolId === "move") {
|
||||
onSelectedToolChange(previousSelectedToolRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
useKeyboard(handleKeyDown, handleKeyUp);
|
||||
// Get keyboard context to pass to Konva
|
||||
const keyboardValue = useContext(KeyboardContext);
|
||||
|
||||
function getCursorForTool(tool) {
|
||||
switch (tool) {
|
||||
|
@ -167,9 +162,7 @@ function MapInteraction({
|
|||
return "move";
|
||||
case "fog":
|
||||
case "drawing":
|
||||
return settings.settings[tool].type === "move"
|
||||
? "pointer"
|
||||
: "crosshair";
|
||||
return "crosshair";
|
||||
case "measure":
|
||||
case "pointer":
|
||||
case "note":
|
||||
|
@ -179,10 +172,6 @@ function MapInteraction({
|
|||
}
|
||||
}
|
||||
|
||||
const auth = useAuth();
|
||||
const settings = useSettings();
|
||||
const tokenData = useTokenData();
|
||||
|
||||
const mapInteraction = {
|
||||
stageScale,
|
||||
stageWidth,
|
||||
|
@ -194,61 +183,49 @@ function MapInteraction({
|
|||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
position: "relative",
|
||||
cursor: getCursorForTool(selectedToolId),
|
||||
touchAction: "none",
|
||||
outline: "none",
|
||||
}}
|
||||
ref={containerRef}
|
||||
className="map"
|
||||
>
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||
<Stage
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
ref={mapStageRef}
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
position: "relative",
|
||||
cursor: getCursorForTool(selectedToolId),
|
||||
touchAction: "none",
|
||||
outline: "none",
|
||||
}}
|
||||
ref={containerRef}
|
||||
className="map"
|
||||
>
|
||||
<Layer ref={mapLayerRef}>
|
||||
<Image
|
||||
image={mapLoaded && mapImageSource}
|
||||
width={mapWidth}
|
||||
height={mapHeight}
|
||||
id="mapImage"
|
||||
ref={mapImageRef}
|
||||
/>
|
||||
{/* Forward auth context to konva elements */}
|
||||
<AuthContext.Provider value={auth}>
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<KeyboardContext.Provider value={keyboardValue}>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
<GridProvider
|
||||
grid={map?.grid}
|
||||
width={mapWidth}
|
||||
height={mapHeight}
|
||||
>
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
<TokenDataContext.Provider value={tokenData}>
|
||||
{mapLoaded && children}
|
||||
</TokenDataContext.Provider>
|
||||
</MapStageProvider>
|
||||
</GridProvider>
|
||||
</MapInteractionProvider>
|
||||
</KeyboardContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
</Layer>
|
||||
</Stage>
|
||||
</ReactResizeDetector>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}>
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||
<KonvaBridge
|
||||
stageRender={(children) => (
|
||||
<Stage
|
||||
width={stageWidth}
|
||||
height={stageHeight}
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
ref={mapStageRef}
|
||||
>
|
||||
{children}
|
||||
</Stage>
|
||||
)}
|
||||
>
|
||||
<Layer ref={mapLayerRef}>
|
||||
<Image
|
||||
image={mapLoaded && mapImageSource}
|
||||
width={mapWidth}
|
||||
height={mapHeight}
|
||||
id="mapImage"
|
||||
ref={mapImageRef}
|
||||
/>
|
||||
|
||||
{mapLoaded && children}
|
||||
</Layer>
|
||||
</KonvaBridge>
|
||||
</ReactResizeDetector>
|
||||
{controls}
|
||||
</GridProvider>
|
||||
</MapInteractionProvider>
|
||||
</Box>
|
||||
</Box>
|
||||
</GridProvider>
|
||||
</MapInteractionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Group, Line, Text, Label, Tag } from "react-konva";
|
||||
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import {
|
||||
useDebouncedStageScale,
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
useInteractionEmitter,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
import { useGrid } from "../../contexts/GridContext";
|
||||
import {
|
||||
useGrid,
|
||||
useGridCellPixelSize,
|
||||
useGridCellNormalizedSize,
|
||||
useGridStrokeWidth,
|
||||
useGridOffset,
|
||||
} from "../../contexts/GridContext";
|
||||
|
||||
import {
|
||||
getDefaultShapeData,
|
||||
|
@ -16,19 +27,17 @@ import { parseGridScale, gridDistance } from "../../helpers/grid";
|
|||
import useGridSnapping from "../../hooks/useGridSnapping";
|
||||
|
||||
function MapMeasure({ map, active }) {
|
||||
const {
|
||||
stageScale,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
interactionEmitter,
|
||||
} = useMapInteraction();
|
||||
const {
|
||||
grid,
|
||||
gridCellNormalizedSize,
|
||||
gridStrokeWidth,
|
||||
gridCellPixelSize,
|
||||
gridOffset,
|
||||
} = useGrid();
|
||||
const stageScale = useDebouncedStageScale();
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const interactionEmitter = useInteractionEmitter();
|
||||
|
||||
const grid = useGrid();
|
||||
const gridCellNormalizedSize = useGridCellNormalizedSize();
|
||||
const gridCellPixelSize = useGridCellPixelSize();
|
||||
const gridStrokeWidth = useGridStrokeWidth();
|
||||
const gridOffset = useGridOffset();
|
||||
|
||||
const mapStageRef = useMapStage();
|
||||
const [drawingShapeData, setDrawingShapeData] = useState(null);
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
|
|||
import shortid from "shortid";
|
||||
import { Group } from "react-konva";
|
||||
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import { useInteractionEmitter } from "../../contexts/MapInteractionContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
|
||||
|
@ -27,7 +27,7 @@ function MapNotes({
|
|||
onNoteDragEnd,
|
||||
fadeOnHover,
|
||||
}) {
|
||||
const { interactionEmitter } = useMapInteraction();
|
||||
const interactionEmitter = useInteractionEmitter();
|
||||
const { userId } = useAuth();
|
||||
const mapStageRef = useMapStage();
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { Group } from "react-konva";
|
||||
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import {
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
useInteractionEmitter,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
import { useGrid } from "../../contexts/GridContext";
|
||||
import { useGridStrokeWidth } from "../../contexts/GridContext";
|
||||
|
||||
import {
|
||||
getRelativePointerPositionNormalized,
|
||||
|
@ -22,8 +26,10 @@ function MapPointer({
|
|||
visible,
|
||||
color,
|
||||
}) {
|
||||
const { mapWidth, mapHeight, interactionEmitter } = useMapInteraction();
|
||||
const { gridStrokeWidth } = useGrid();
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const interactionEmitter = useInteractionEmitter();
|
||||
const gridStrokeWidth = useGridStrokeWidth();
|
||||
const mapStageRef = useMapStage();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
5
src/components/map/MapTest.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
function MapTest() {}
|
||||
|
||||
export default MapTest;
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
|
||||
import Tile from "../Tile";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
import { mapSources as defaultMapSources, unknownSource } from "../../maps";
|
||||
|
||||
function MapTile({
|
||||
|
@ -15,11 +15,11 @@ function MapTile({
|
|||
canEdit,
|
||||
badges,
|
||||
}) {
|
||||
const isDefault = map.type === "default";
|
||||
const mapSource = useDataSource(
|
||||
isDefault ? map : map.thumbnail,
|
||||
const mapSource = useImageSource(
|
||||
map,
|
||||
defaultMapSources,
|
||||
unknownSource
|
||||
unknownSource,
|
||||
map.type === "file"
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -29,7 +29,7 @@ function MapTile({
|
|||
isSelected={isSelected}
|
||||
onSelect={() => onMapSelect(map)}
|
||||
onEdit={() => onMapEdit(map.id)}
|
||||
onDoubleClick={onDone}
|
||||
onDoubleClick={() => canEdit && onDone()}
|
||||
size={size}
|
||||
canEdit={canEdit}
|
||||
badges={badges}
|
||||
|
|
|
@ -4,14 +4,19 @@ import { useSpring, animated } from "react-spring/konva";
|
|||
import useImage from "use-image";
|
||||
import Konva from "konva";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import useDebounce from "../../hooks/useDebounce";
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
import useGridSnapping from "../../hooks/useGridSnapping";
|
||||
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import { useGrid } from "../../contexts/GridContext";
|
||||
import {
|
||||
useSetPreventMapInteraction,
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
useDebouncedStageScale,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useGridCellPixelSize } from "../../contexts/GridContext";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import TokenStatus from "../token/TokenStatus";
|
||||
import TokenLabel from "../token/TokenLabel";
|
||||
|
@ -26,20 +31,19 @@ function MapToken({
|
|||
onTokenDragStart,
|
||||
onTokenDragEnd,
|
||||
draggable,
|
||||
mapState,
|
||||
fadeOnHover,
|
||||
map,
|
||||
}) {
|
||||
const { userId } = useAuth();
|
||||
const {
|
||||
setPreventMapInteraction,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
stageScale,
|
||||
} = useMapInteraction();
|
||||
const { gridCellPixelSize } = useGrid();
|
||||
|
||||
const tokenSource = useDataSource(token, tokenSources, unknownSource);
|
||||
const stageScale = useDebouncedStageScale();
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const setPreventMapInteraction = useSetPreventMapInteraction();
|
||||
|
||||
const gridCellPixelSize = useGridCellPixelSize();
|
||||
|
||||
const tokenSource = useImageSource(token, tokenSources, unknownSource);
|
||||
const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource);
|
||||
const [tokenAspectRatio, setTokenAspectRatio] = useState(1);
|
||||
|
||||
|
@ -106,7 +110,6 @@ function MapToken({
|
|||
mountedToken.moveTo(parent);
|
||||
mountedToken.absolutePosition(position);
|
||||
mountChanges[mountedToken.id()] = {
|
||||
...mapState.tokens[mountedToken.id()],
|
||||
x: mountedToken.x() / mapWidth,
|
||||
y: mountedToken.y() / mapHeight,
|
||||
lastModifiedBy: userId,
|
||||
|
@ -119,7 +122,6 @@ function MapToken({
|
|||
onTokenStateChange({
|
||||
...mountChanges,
|
||||
[tokenState.id]: {
|
||||
...tokenState,
|
||||
x: tokenGroup.x() / mapWidth,
|
||||
y: tokenGroup.y() / mapHeight,
|
||||
lastModifiedBy: userId,
|
||||
|
@ -207,8 +209,6 @@ function MapToken({
|
|||
),
|
||||
});
|
||||
image.drawHitFromCache();
|
||||
// Force redraw
|
||||
image.getLayer().draw();
|
||||
}
|
||||
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus, token]);
|
||||
|
||||
|
|
|
@ -119,7 +119,6 @@ function MapTokens({
|
|||
!(tokenState.id in disabledTokens) &&
|
||||
!tokenState.locked
|
||||
}
|
||||
mapState={mapState}
|
||||
fadeOnHover={selectedToolId === "drawing"}
|
||||
map={map}
|
||||
/>
|
||||
|
|
|
@ -24,6 +24,8 @@ import Divider from "../../Divider";
|
|||
|
||||
import { useKeyboard } from "../../../contexts/KeyboardContext";
|
||||
|
||||
import shortcuts from "../../../shortcuts";
|
||||
|
||||
function DrawingToolSettings({
|
||||
settings,
|
||||
onSettingChange,
|
||||
|
@ -31,36 +33,26 @@ function DrawingToolSettings({
|
|||
disabledActions,
|
||||
}) {
|
||||
// Keyboard shotcuts
|
||||
function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) {
|
||||
if (key === "b") {
|
||||
function handleKeyDown(event) {
|
||||
if (shortcuts.drawBrush(event)) {
|
||||
onSettingChange({ type: "brush" });
|
||||
} else if (key === "p") {
|
||||
} else if (shortcuts.drawPaint(event)) {
|
||||
onSettingChange({ type: "paint" });
|
||||
} else if (key === "l") {
|
||||
} else if (shortcuts.drawLine(event)) {
|
||||
onSettingChange({ type: "line" });
|
||||
} else if (key === "r") {
|
||||
} else if (shortcuts.drawRect(event)) {
|
||||
onSettingChange({ type: "rectangle" });
|
||||
} else if (key === "c") {
|
||||
} else if (shortcuts.drawCircle(event)) {
|
||||
onSettingChange({ type: "circle" });
|
||||
} else if (key === "t") {
|
||||
} else if (shortcuts.drawTriangle(event)) {
|
||||
onSettingChange({ type: "triangle" });
|
||||
} else if (key === "e") {
|
||||
} else if (shortcuts.drawErase(event)) {
|
||||
onSettingChange({ type: "erase" });
|
||||
} else if (key === "o") {
|
||||
} else if (shortcuts.drawBlend(event)) {
|
||||
onSettingChange({ useBlending: !settings.useBlending });
|
||||
} else if (
|
||||
(key === "z" || key === "Z") &&
|
||||
(ctrlKey || metaKey) &&
|
||||
shiftKey &&
|
||||
!disabledActions.includes("redo")
|
||||
) {
|
||||
} else if (shortcuts.redo(event) && !disabledActions.includes("redo")) {
|
||||
onToolAction("mapRedo");
|
||||
} else if (
|
||||
key === "z" &&
|
||||
(ctrlKey || metaKey) &&
|
||||
!shiftKey &&
|
||||
!disabledActions.includes("undo")
|
||||
) {
|
||||
} else if (shortcuts.undo(event) && !disabledActions.includes("undo")) {
|
||||
onToolAction("mapUndo");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import Divider from "../../Divider";
|
|||
|
||||
import { useKeyboard } from "../../../contexts/KeyboardContext";
|
||||
|
||||
import shortcuts from "../../../shortcuts";
|
||||
|
||||
function BrushToolSettings({
|
||||
settings,
|
||||
onSettingChange,
|
||||
|
@ -29,36 +31,26 @@ function BrushToolSettings({
|
|||
disabledActions,
|
||||
}) {
|
||||
// Keyboard shortcuts
|
||||
function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) {
|
||||
if (key === "p") {
|
||||
function handleKeyDown(event) {
|
||||
if (shortcuts.fogPolygon(event)) {
|
||||
onSettingChange({ type: "polygon" });
|
||||
} else if (key === "b") {
|
||||
} else if (shortcuts.fogBrush(event)) {
|
||||
onSettingChange({ type: "brush" });
|
||||
} else if (key === "t") {
|
||||
} else if (shortcuts.fogToggle(event)) {
|
||||
onSettingChange({ type: "toggle" });
|
||||
} else if (key === "e") {
|
||||
} else if (shortcuts.fogErase(event)) {
|
||||
onSettingChange({ type: "remove" });
|
||||
} else if (key === "l") {
|
||||
} else if (shortcuts.fogLayer(event)) {
|
||||
onSettingChange({ multilayer: !settings.multilayer });
|
||||
} else if (key === "f") {
|
||||
} else if (shortcuts.fogPreview(event)) {
|
||||
onSettingChange({ preview: !settings.preview });
|
||||
} else if (key === "c") {
|
||||
} else if (shortcuts.fogCut(event)) {
|
||||
onSettingChange({ useFogCut: !settings.useFogCut });
|
||||
} else if (key === "r") {
|
||||
} else if (shortcuts.fogRectangle(event)) {
|
||||
onSettingChange({ type: "rectangle" });
|
||||
} else if (
|
||||
(key === "z" || key === "Z") &&
|
||||
(ctrlKey || metaKey) &&
|
||||
shiftKey &&
|
||||
!disabledActions.includes("redo")
|
||||
) {
|
||||
} else if (shortcuts.redo(event) && !disabledActions.includes("redo")) {
|
||||
onToolAction("fogRedo");
|
||||
} else if (
|
||||
key === "z" &&
|
||||
(ctrlKey || metaKey) &&
|
||||
!shiftKey &&
|
||||
!disabledActions.includes("undo")
|
||||
) {
|
||||
} else if (shortcuts.undo(event) && !disabledActions.includes("undo")) {
|
||||
onToolAction("fogUndo");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,12 @@ import { Rect, Text } from "react-konva";
|
|||
import { useSpring, animated } from "react-spring/konva";
|
||||
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import { useGrid } from "../../contexts/GridContext";
|
||||
import {
|
||||
useSetPreventMapInteraction,
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useGridCellPixelSize } from "../../contexts/GridContext";
|
||||
|
||||
import colors from "../../helpers/colors";
|
||||
|
||||
|
@ -24,8 +28,12 @@ function Note({
|
|||
fadeOnHover,
|
||||
}) {
|
||||
const { userId } = useAuth();
|
||||
const { mapWidth, mapHeight, setPreventMapInteraction } = useMapInteraction();
|
||||
const { gridCellPixelSize } = useGrid();
|
||||
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const setPreventMapInteraction = useSetPreventMapInteraction();
|
||||
|
||||
const gridCellPixelSize = useGridCellPixelSize();
|
||||
|
||||
const minCellSize = Math.min(
|
||||
gridCellPixelSize.width,
|
||||
|
@ -142,7 +150,7 @@ function Note({
|
|||
}
|
||||
|
||||
findFontSize();
|
||||
}, [note, note.text, noteWidth, noteHeight, notePadding]);
|
||||
}, [note, note.text, note.visible, noteWidth, noteHeight, notePadding]);
|
||||
|
||||
const textRef = useRef();
|
||||
|
||||
|
|
|
@ -14,11 +14,32 @@ import DiceRoll from "./DiceRoll";
|
|||
|
||||
import { getDiceRollTotal } from "../../helpers/dice";
|
||||
|
||||
const diceIcons = [
|
||||
{ type: "d20", Icon: D20Icon },
|
||||
{ type: "d12", Icon: D12Icon },
|
||||
{ type: "d10", Icon: D10Icon },
|
||||
{ type: "d8", Icon: D8Icon },
|
||||
{ type: "d6", Icon: D6Icon },
|
||||
{ type: "d4", Icon: D4Icon },
|
||||
{ type: "d100", Icon: D100Icon },
|
||||
];
|
||||
|
||||
function DiceRolls({ rolls }) {
|
||||
const total = getDiceRollTotal(rolls);
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
let expandedRolls = [];
|
||||
for (let icon of diceIcons) {
|
||||
if (rolls.some((roll) => roll.type === icon.type)) {
|
||||
expandedRolls.push(
|
||||
<DiceRoll rolls={rolls} type={icon.type} key={icon.type}>
|
||||
<icon.Icon />
|
||||
</DiceRoll>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
total > 0 && (
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
|
@ -40,27 +61,7 @@ function DiceRolls({ rolls }) {
|
|||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<DiceRoll rolls={rolls} type="d20">
|
||||
<D20Icon />
|
||||
</DiceRoll>
|
||||
<DiceRoll rolls={rolls} type="d12">
|
||||
<D12Icon />
|
||||
</DiceRoll>
|
||||
<DiceRoll rolls={rolls} type="d10">
|
||||
<D10Icon />
|
||||
</DiceRoll>
|
||||
<DiceRoll rolls={rolls} type="d8">
|
||||
<D8Icon />
|
||||
</DiceRoll>
|
||||
<DiceRoll rolls={rolls} type="d6">
|
||||
<D6Icon />
|
||||
</DiceRoll>
|
||||
<DiceRoll rolls={rolls} type="d4">
|
||||
<D4Icon />
|
||||
</DiceRoll>
|
||||
<DiceRoll rolls={rolls} type="d100">
|
||||
<D100Icon />
|
||||
</DiceRoll>
|
||||
{expandedRolls}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
|
|
@ -90,8 +90,6 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) {
|
|||
bg="background"
|
||||
sx={{
|
||||
position: "relative",
|
||||
// width: fullScreen ? "0" : "112px",
|
||||
// minWidth: fullScreen ? "0" : "112px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Text, IconButton, Box, Flex } from "theme-ui";
|
|||
|
||||
import StreamMuteIcon from "../../icons/StreamMuteIcon";
|
||||
|
||||
import Banner from "../Banner";
|
||||
import Banner from "../banner/Banner";
|
||||
import Slider from "../Slider";
|
||||
|
||||
function Stream({ stream, nickname }) {
|
||||
|
|
|
@ -2,16 +2,17 @@ import React, { useRef } from "react";
|
|||
import { Box, Image } from "theme-ui";
|
||||
|
||||
import usePreventTouch from "../../hooks/usePreventTouch";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import { tokenSources, unknownSource } from "../../tokens";
|
||||
|
||||
function ListToken({ token, className }) {
|
||||
const isDefault = token.type === "default";
|
||||
const tokenSource = useDataSource(
|
||||
isDefault ? token : token.thumbnail,
|
||||
const tokenSource = useImageSource(
|
||||
token,
|
||||
tokenSources,
|
||||
unknownSource
|
||||
unknownSource,
|
||||
token.type === "file"
|
||||
);
|
||||
|
||||
const imageRef = useRef();
|
||||
|
@ -33,6 +34,8 @@ function ListToken({ token, className }) {
|
|||
}}
|
||||
// pass id into the dom element which is then used by the ProxyToken
|
||||
data-id={token.id}
|
||||
alt={token.name}
|
||||
title={token.name}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React from "react";
|
||||
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import {
|
||||
useMapWidth,
|
||||
useMapHeight,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
|
||||
import DragOverlay from "../DragOverlay";
|
||||
|
||||
|
@ -12,10 +15,11 @@ function TokenDragOverlay({
|
|||
tokenState,
|
||||
tokenGroup,
|
||||
dragging,
|
||||
mapState,
|
||||
}) {
|
||||
const { userId } = useAuth();
|
||||
const { mapWidth, mapHeight } = useMapInteraction();
|
||||
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
|
||||
function handleTokenRemove() {
|
||||
// Handle other tokens when a vehicle gets deleted
|
||||
|
@ -29,7 +33,6 @@ function TokenDragOverlay({
|
|||
mountedToken.absolutePosition(position);
|
||||
onTokenStateChange({
|
||||
[mountedToken.id()]: {
|
||||
...mapState.tokens[mountedToken.id()],
|
||||
x: mountedToken.x() / mapWidth,
|
||||
y: mountedToken.y() / mapHeight,
|
||||
lastModifiedBy: userId,
|
||||
|
|
|
@ -45,7 +45,15 @@ function TokenLabel({ tokenState, width, height }) {
|
|||
}
|
||||
|
||||
findFontSize();
|
||||
}, [tokenState.label, width, height, tokenState, labelSize, paddingX]);
|
||||
}, [
|
||||
tokenState.label,
|
||||
tokenState.visible,
|
||||
width,
|
||||
height,
|
||||
tokenState,
|
||||
labelSize,
|
||||
paddingX,
|
||||
]);
|
||||
|
||||
const [rectWidth, setRectWidth] = useState(0);
|
||||
useEffect(() => {
|
||||
|
|
|
@ -51,8 +51,7 @@ function TokenMenu({
|
|||
|
||||
function handleLabelChange(event) {
|
||||
const label = event.target.value.substring(0, 144);
|
||||
tokenState &&
|
||||
onTokenStateChange({ [tokenState.id]: { ...tokenState, label: label } });
|
||||
tokenState && onTokenStateChange({ [tokenState.id]: { label: label } });
|
||||
}
|
||||
|
||||
function handleStatusChange(status) {
|
||||
|
@ -66,35 +65,34 @@ function TokenMenu({
|
|||
statuses.add(status);
|
||||
}
|
||||
onTokenStateChange({
|
||||
[tokenState.id]: { ...tokenState, statuses: [...statuses] },
|
||||
[tokenState.id]: { statuses: [...statuses] },
|
||||
});
|
||||
}
|
||||
|
||||
function handleSizeChange(event) {
|
||||
const newSize = parseFloat(event.target.value);
|
||||
tokenState &&
|
||||
onTokenStateChange({ [tokenState.id]: { ...tokenState, size: newSize } });
|
||||
tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } });
|
||||
}
|
||||
|
||||
function handleRotationChange(event) {
|
||||
const newRotation = parseInt(event.target.value);
|
||||
tokenState &&
|
||||
onTokenStateChange({
|
||||
[tokenState.id]: { ...tokenState, rotation: newRotation },
|
||||
[tokenState.id]: { rotation: newRotation },
|
||||
});
|
||||
}
|
||||
|
||||
function handleVisibleChange() {
|
||||
tokenState &&
|
||||
onTokenStateChange({
|
||||
[tokenState.id]: { ...tokenState, visible: !tokenState.visible },
|
||||
[tokenState.id]: { visible: !tokenState.visible },
|
||||
});
|
||||
}
|
||||
|
||||
function handleLockChange() {
|
||||
tokenState &&
|
||||
onTokenStateChange({
|
||||
[tokenState.id]: { ...tokenState, locked: !tokenState.locked },
|
||||
[tokenState.id]: { locked: !tokenState.locked },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ import useImage from "use-image";
|
|||
|
||||
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
|
||||
import useStageInteraction from "../../hooks/useStageInteraction";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import useImageCenter from "../../hooks/useImageCenter";
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
import { GridProvider } from "../../contexts/GridContext";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import GridOnIcon from "../../icons/GridOnIcon";
|
||||
import GridOffIcon from "../../icons/GridOffIcon";
|
||||
|
@ -27,7 +27,7 @@ function TokenPreview({ token }) {
|
|||
}
|
||||
}, [token, tokenSourceData]);
|
||||
|
||||
const tokenSource = useDataSource(
|
||||
const tokenSource = useImageSource(
|
||||
tokenSourceData,
|
||||
tokenSources,
|
||||
unknownSource
|
||||
|
|
|
@ -2,7 +2,8 @@ import React from "react";
|
|||
|
||||
import Tile from "../Tile";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import {
|
||||
tokenSources as defaultTokenSources,
|
||||
unknownSource,
|
||||
|
@ -17,11 +18,11 @@ function TokenTile({
|
|||
canEdit,
|
||||
badges,
|
||||
}) {
|
||||
const isDefault = token.type === "default";
|
||||
const tokenSource = useDataSource(
|
||||
isDefault ? token : token.thumbnail,
|
||||
const tokenSource = useImageSource(
|
||||
token,
|
||||
defaultTokenSources,
|
||||
unknownSource
|
||||
unknownSource,
|
||||
token.type === "file"
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -41,7 +41,11 @@ function Tokens({ onMapTokenStateCreate }) {
|
|||
visible: true,
|
||||
});
|
||||
// Update last used for cache invalidation
|
||||
updateToken(token.id, { lastUsed: Date.now() });
|
||||
// Keep last modified the same
|
||||
updateToken(token.id, {
|
||||
lastUsed: Date.now(),
|
||||
lastModified: token.lastModified,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { Box, Text } from "theme-ui";
|
||||
import * as Comlink from "comlink";
|
||||
|
||||
import Banner from "../components/Banner";
|
||||
import ErrorBanner from "../components/banner/ErrorBanner";
|
||||
|
||||
import { getDatabase } from "../database";
|
||||
|
||||
import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax
|
||||
|
||||
const DatabaseContext = React.createContext();
|
||||
|
||||
const worker = Comlink.wrap(new DatabaseWorker());
|
||||
|
||||
export function DatabaseProvider({ children }) {
|
||||
const [database, setDatabase] = useState();
|
||||
const [databaseStatus, setDatabaseStatus] = useState("loading");
|
||||
|
@ -40,13 +44,19 @@ export function DatabaseProvider({ children }) {
|
|||
};
|
||||
|
||||
function handleDatabaseError(event) {
|
||||
if (event.reason?.name === "QuotaExceededError") {
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
if (event.reason?.message.startsWith("QuotaExceededError")) {
|
||||
setDatabaseError({
|
||||
name: event.reason.name,
|
||||
message: "Storage Quota Exceeded Please Clear Space and Try Again.",
|
||||
});
|
||||
} else {
|
||||
setDatabaseError({
|
||||
name: event.reason.name,
|
||||
message: "Something went wrong, please refresh your browser.",
|
||||
});
|
||||
}
|
||||
console.error(event.reason);
|
||||
}
|
||||
window.addEventListener("unhandledrejection", handleDatabaseError);
|
||||
|
||||
|
@ -59,21 +69,16 @@ export function DatabaseProvider({ children }) {
|
|||
database,
|
||||
databaseStatus,
|
||||
databaseError,
|
||||
worker,
|
||||
};
|
||||
return (
|
||||
<DatabaseContext.Provider value={value}>
|
||||
<>
|
||||
{children}
|
||||
<Banner
|
||||
isOpen={!!databaseError}
|
||||
<ErrorBanner
|
||||
error={databaseError}
|
||||
onRequestClose={() => setDatabaseError()}
|
||||
>
|
||||
<Box p={1}>
|
||||
<Text as="p" variant="body2">
|
||||
{databaseError && databaseError.message}
|
||||
</Text>
|
||||
</Box>
|
||||
</Banner>
|
||||
/>
|
||||
</>
|
||||
</DatabaseContext.Provider>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext } from "react";
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
import Size from "../helpers/Size";
|
||||
|
@ -37,55 +37,105 @@ const defaultValue = {
|
|||
gridCellPixelOffset: new Vector2(0, 0),
|
||||
};
|
||||
|
||||
const GridContext = React.createContext(defaultValue);
|
||||
export const GridContext = React.createContext(defaultValue.grid);
|
||||
export const GridPixelSizeContext = React.createContext(
|
||||
defaultValue.gridPixelSize
|
||||
);
|
||||
export const GridCellPixelSizeContext = React.createContext(
|
||||
defaultValue.gridCellPixelSize
|
||||
);
|
||||
export const GridCellNormalizedSizeContext = React.createContext(
|
||||
defaultValue.gridCellNormalizedSize
|
||||
);
|
||||
export const GridOffsetContext = React.createContext(defaultValue.gridOffset);
|
||||
export const GridStrokeWidthContext = React.createContext(
|
||||
defaultValue.gridStrokeWidth
|
||||
);
|
||||
export const GridCellPixelOffsetContext = React.createContext(
|
||||
defaultValue.gridCellPixelOffset
|
||||
);
|
||||
|
||||
const defaultStrokeWidth = 1 / 10;
|
||||
|
||||
export function GridProvider({ grid, width, height, children }) {
|
||||
export function GridProvider({ grid: inputGrid, width, height, children }) {
|
||||
let grid = inputGrid;
|
||||
|
||||
if (!grid?.size.x || !grid?.size.y) {
|
||||
return (
|
||||
<GridContext.Provider value={defaultValue}>
|
||||
{children}
|
||||
</GridContext.Provider>
|
||||
grid = defaultValue.grid;
|
||||
}
|
||||
|
||||
const [gridPixelSize, setGridPixelSize] = useState(
|
||||
defaultValue.gridCellPixelSize
|
||||
);
|
||||
const [gridCellPixelSize, setGridCellPixelSize] = useState(
|
||||
defaultValue.gridCellPixelSize
|
||||
);
|
||||
const [gridCellNormalizedSize, setGridCellNormalizedSize] = useState(
|
||||
defaultValue.gridCellNormalizedSize
|
||||
);
|
||||
const [gridOffset, setGridOffset] = useState(defaultValue.gridOffset);
|
||||
const [gridStrokeWidth, setGridStrokeWidth] = useState(
|
||||
defaultValue.gridStrokeWidth
|
||||
);
|
||||
const [gridCellPixelOffset, setGridCellPixelOffset] = useState(
|
||||
defaultValue.gridCellPixelOffset
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const _gridPixelSize = getGridPixelSize(grid, width, height);
|
||||
const _gridCellPixelSize = getCellPixelSize(
|
||||
grid,
|
||||
_gridPixelSize.width,
|
||||
_gridPixelSize.height
|
||||
);
|
||||
}
|
||||
const _gridCellNormalizedSize = new Size(
|
||||
_gridCellPixelSize.width / width,
|
||||
_gridCellPixelSize.height / height
|
||||
);
|
||||
const _gridOffset = Vector2.multiply(grid.inset.topLeft, {
|
||||
x: width,
|
||||
y: height,
|
||||
});
|
||||
const _gridStrokeWidth =
|
||||
(_gridCellPixelSize.width < _gridCellPixelSize.height
|
||||
? _gridCellPixelSize.width
|
||||
: _gridCellPixelSize.height) * defaultStrokeWidth;
|
||||
|
||||
const gridPixelSize = getGridPixelSize(grid, width, height);
|
||||
const gridCellPixelSize = getCellPixelSize(
|
||||
grid,
|
||||
gridPixelSize.width,
|
||||
gridPixelSize.height
|
||||
let _gridCellPixelOffset = { x: 0, y: 0 };
|
||||
// Move hex tiles to top left
|
||||
if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
|
||||
_gridCellPixelOffset = Vector2.multiply(_gridCellPixelSize, 0.5);
|
||||
}
|
||||
|
||||
setGridPixelSize(_gridPixelSize);
|
||||
setGridCellPixelSize(_gridCellPixelSize);
|
||||
setGridCellNormalizedSize(_gridCellNormalizedSize);
|
||||
setGridOffset(_gridOffset);
|
||||
setGridStrokeWidth(_gridStrokeWidth);
|
||||
setGridCellPixelOffset(_gridCellPixelOffset);
|
||||
}, [grid, width, height]);
|
||||
|
||||
return (
|
||||
<GridContext.Provider value={grid}>
|
||||
<GridPixelSizeContext.Provider value={gridPixelSize}>
|
||||
<GridCellPixelSizeContext.Provider value={gridCellPixelSize}>
|
||||
<GridCellNormalizedSizeContext.Provider
|
||||
value={gridCellNormalizedSize}
|
||||
>
|
||||
<GridOffsetContext.Provider value={gridOffset}>
|
||||
<GridStrokeWidthContext.Provider value={gridStrokeWidth}>
|
||||
<GridCellPixelOffsetContext.Provider
|
||||
value={gridCellPixelOffset}
|
||||
>
|
||||
{children}
|
||||
</GridCellPixelOffsetContext.Provider>
|
||||
</GridStrokeWidthContext.Provider>
|
||||
</GridOffsetContext.Provider>
|
||||
</GridCellNormalizedSizeContext.Provider>
|
||||
</GridCellPixelSizeContext.Provider>
|
||||
</GridPixelSizeContext.Provider>
|
||||
</GridContext.Provider>
|
||||
);
|
||||
const gridCellNormalizedSize = new Size(
|
||||
gridCellPixelSize.width / width,
|
||||
gridCellPixelSize.height / height
|
||||
);
|
||||
const gridOffset = Vector2.multiply(grid.inset.topLeft, {
|
||||
x: width,
|
||||
y: height,
|
||||
});
|
||||
const gridStrokeWidth =
|
||||
(gridCellPixelSize.width < gridCellPixelSize.height
|
||||
? gridCellPixelSize.width
|
||||
: gridCellPixelSize.height) * defaultStrokeWidth;
|
||||
|
||||
let gridCellPixelOffset = { x: 0, y: 0 };
|
||||
// Move hex tiles to top left
|
||||
if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
|
||||
gridCellPixelOffset = Vector2.multiply(gridCellPixelSize, 0.5);
|
||||
}
|
||||
|
||||
const value = {
|
||||
grid,
|
||||
gridPixelSize,
|
||||
gridCellPixelSize,
|
||||
gridCellNormalizedSize,
|
||||
gridOffset,
|
||||
gridStrokeWidth,
|
||||
gridCellPixelOffset,
|
||||
};
|
||||
|
||||
return <GridContext.Provider value={value}>{children}</GridContext.Provider>;
|
||||
}
|
||||
|
||||
export function useGrid() {
|
||||
|
@ -96,4 +146,54 @@ export function useGrid() {
|
|||
return context;
|
||||
}
|
||||
|
||||
export default GridContext;
|
||||
export function useGridPixelSize() {
|
||||
const context = useContext(GridPixelSizeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useGridPixelSize must be used within a GridProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useGridCellPixelSize() {
|
||||
const context = useContext(GridCellPixelSizeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useGridCellPixelSize must be used within a GridProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useGridCellNormalizedSize() {
|
||||
const context = useContext(GridCellNormalizedSizeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useGridCellNormalizedSize must be used within a GridProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useGridOffset() {
|
||||
const context = useContext(GridOffsetContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useGridOffset must be used within a GridProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useGridStrokeWidth() {
|
||||
const context = useContext(GridStrokeWidthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useGridStrokeWidth must be used within a GridProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useGridCellPixelOffset() {
|
||||
const context = useContext(GridCellPixelOffsetContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useGridCellPixelOffset must be used within a GridProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
157
src/contexts/ImageSourceContext.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
import React, { useContext, useState, useEffect } from "react";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
|
||||
export const ImageSourcesStateContext = React.createContext();
|
||||
export const ImageSourcesUpdaterContext = React.createContext(() => {});
|
||||
|
||||
/**
|
||||
* Helper to manage sharing of custom image sources between uses of useImageSource
|
||||
*/
|
||||
export function ImageSourcesProvider({ children }) {
|
||||
const [imageSources, setImageSources] = useState({});
|
||||
|
||||
// Revoke url when no more references
|
||||
useEffect(() => {
|
||||
let sourcesToCleanup = [];
|
||||
for (let source of Object.values(imageSources)) {
|
||||
if (source.references <= 0) {
|
||||
URL.revokeObjectURL(source.url);
|
||||
sourcesToCleanup.push(source.id);
|
||||
}
|
||||
}
|
||||
if (sourcesToCleanup.length > 0) {
|
||||
setImageSources((prevSources) => omit(prevSources, sourcesToCleanup));
|
||||
}
|
||||
}, [imageSources]);
|
||||
|
||||
return (
|
||||
<ImageSourcesStateContext.Provider value={imageSources}>
|
||||
<ImageSourcesUpdaterContext.Provider value={setImageSources}>
|
||||
{children}
|
||||
</ImageSourcesUpdaterContext.Provider>
|
||||
</ImageSourcesStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id from image data
|
||||
*/
|
||||
function getImageFileId(data, thumbnail) {
|
||||
if (thumbnail) {
|
||||
return `${data.id}-thumbnail`;
|
||||
}
|
||||
if (data.resolutions) {
|
||||
// Check is a resolution is specified
|
||||
if (data.quality && data.resolutions[data.quality]) {
|
||||
return `${data.id}-${data.quality}`;
|
||||
} else if (!data.file) {
|
||||
// Fallback to the highest resolution
|
||||
const resolutionArray = Object.keys(data.resolutions);
|
||||
const resolution = resolutionArray[resolutionArray.length - 1];
|
||||
return `${data.id}-${resolution.id}`;
|
||||
}
|
||||
}
|
||||
return data.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to load either file or default image into a URL
|
||||
*/
|
||||
export function useImageSource(data, defaultSources, unknownSource, thumbnail) {
|
||||
const imageSources = useContext(ImageSourcesStateContext);
|
||||
if (imageSources === undefined) {
|
||||
throw new Error(
|
||||
"useImageSource must be used within a ImageSourcesProvider"
|
||||
);
|
||||
}
|
||||
const setImageSources = useContext(ImageSourcesUpdaterContext);
|
||||
if (setImageSources === undefined) {
|
||||
throw new Error(
|
||||
"useImageSource must be used within a ImageSourcesProvider"
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || data.type !== "file") {
|
||||
return;
|
||||
}
|
||||
const id = getImageFileId(data, thumbnail);
|
||||
|
||||
function updateImageSource(file) {
|
||||
if (file) {
|
||||
setImageSources((prevSources) => {
|
||||
if (id in prevSources) {
|
||||
// Check if the image source is already added
|
||||
return {
|
||||
...prevSources,
|
||||
[id]: {
|
||||
...prevSources[id],
|
||||
// Increase references
|
||||
references: prevSources[id].references + 1,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const url = URL.createObjectURL(new Blob([file]));
|
||||
return {
|
||||
...prevSources,
|
||||
[id]: { url, id, references: 1 },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
updateImageSource(data.thumbnail.file);
|
||||
} else if (data.resolutions) {
|
||||
// Check is a resolution is specified
|
||||
if (data.quality && data.resolutions[data.quality]) {
|
||||
updateImageSource(data.resolutions[data.quality].file);
|
||||
}
|
||||
// If no file available fallback to the highest resolution
|
||||
else if (!data.file) {
|
||||
const resolutionArray = Object.keys(data.resolutions);
|
||||
updateImageSource(
|
||||
data.resolutions[resolutionArray[resolutionArray.length - 1]].file
|
||||
);
|
||||
} else {
|
||||
updateImageSource(data.file);
|
||||
}
|
||||
} else {
|
||||
updateImageSource(data.file);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Decrease references
|
||||
setImageSources((prevSources) => {
|
||||
if (id in prevSources) {
|
||||
return {
|
||||
...prevSources,
|
||||
[id]: {
|
||||
...prevSources[id],
|
||||
references: prevSources[id].references - 1,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return prevSources;
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [data, unknownSource, thumbnail, setImageSources]);
|
||||
|
||||
if (!data) {
|
||||
return unknownSource;
|
||||
}
|
||||
|
||||
if (data.type === "default") {
|
||||
return defaultSources[data.key];
|
||||
}
|
||||
|
||||
if (data.type === "file") {
|
||||
const id = getImageFileId(data, thumbnail);
|
||||
return imageSources[id]?.url;
|
||||
}
|
||||
|
||||
return unknownSource;
|
||||
}
|
|
@ -74,4 +74,21 @@ export function useKeyboard(onKeyDown, onKeyUp) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler to handle a blur event. Useful when using a shortcut that uses the Alt or Cmd
|
||||
* @param {FocusEvent} onBlur
|
||||
*/
|
||||
export function useBlur(onBlur) {
|
||||
useEffect(() => {
|
||||
if (onBlur) {
|
||||
window.addEventListener("blur", onBlur);
|
||||
}
|
||||
return () => {
|
||||
if (onBlur) {
|
||||
window.removeEventListener("blur", onBlur);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default KeyboardContext;
|
||||
|
|
|
@ -6,13 +6,11 @@ import React, {
|
|||
useRef,
|
||||
} from "react";
|
||||
import * as Comlink from "comlink";
|
||||
import { decode } from "@msgpack/msgpack";
|
||||
import { decode, encode } from "@msgpack/msgpack";
|
||||
|
||||
import { useAuth } from "./AuthContext";
|
||||
import { useDatabase } from "./DatabaseContext";
|
||||
|
||||
import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax
|
||||
|
||||
import { maps as defaultMaps } from "../maps";
|
||||
|
||||
const MapDataContext = React.createContext();
|
||||
|
@ -29,10 +27,8 @@ const defaultMapState = {
|
|||
notes: {},
|
||||
};
|
||||
|
||||
const worker = Comlink.wrap(new DatabaseWorker());
|
||||
|
||||
export function MapDataProvider({ children }) {
|
||||
const { database, databaseStatus } = useDatabase();
|
||||
const { database, databaseStatus, worker } = useDatabase();
|
||||
const { userId } = useAuth();
|
||||
|
||||
const [maps, setMaps] = useState([]);
|
||||
|
@ -74,6 +70,7 @@ export function MapDataProvider({ children }) {
|
|||
let storedMaps = [];
|
||||
// Try to load maps with worker, fallback to database if failed
|
||||
const packedMaps = await worker.loadData("maps");
|
||||
// let packedMaps;
|
||||
if (packedMaps) {
|
||||
storedMaps = decode(packedMaps);
|
||||
} else {
|
||||
|
@ -93,7 +90,7 @@ export function MapDataProvider({ children }) {
|
|||
}
|
||||
|
||||
loadMaps();
|
||||
}, [userId, database, databaseStatus]);
|
||||
}, [userId, database, databaseStatus, worker]);
|
||||
|
||||
const mapsRef = useRef(maps);
|
||||
useEffect(() => {
|
||||
|
@ -218,12 +215,21 @@ export function MapDataProvider({ children }) {
|
|||
*/
|
||||
const putMap = useCallback(
|
||||
async (map) => {
|
||||
await database.table("maps").put(map);
|
||||
// Attempt to use worker to put map to avoid UI lockup
|
||||
const packedMap = encode(map);
|
||||
const success = await worker.putData(
|
||||
Comlink.transfer(packedMap, [packedMap.buffer]),
|
||||
"maps",
|
||||
false
|
||||
);
|
||||
if (!success) {
|
||||
await database.table("maps").put(map);
|
||||
}
|
||||
if (map.owner !== userId) {
|
||||
await updateCache();
|
||||
}
|
||||
},
|
||||
[database, updateCache, userId]
|
||||
[database, updateCache, userId, worker]
|
||||
);
|
||||
|
||||
// Create DB observable to sync creating and deleting
|
||||
|
|
|
@ -1,24 +1,125 @@
|
|||
import React, { useContext } from "react";
|
||||
import useDebounce from "../hooks/useDebounce";
|
||||
|
||||
const MapInteractionContext = React.createContext({
|
||||
stageScale: 1,
|
||||
stageWidth: 1,
|
||||
stageHeight: 1,
|
||||
setPreventMapInteraction: () => {},
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
interactionEmitter: null,
|
||||
});
|
||||
export const MapInteractionProvider = MapInteractionContext.Provider;
|
||||
export const StageScaleContext = React.createContext();
|
||||
export const DebouncedStageScaleContext = React.createContext();
|
||||
export const StageWidthContext = React.createContext();
|
||||
export const StageHeightContext = React.createContext();
|
||||
export const SetPreventMapInteractionContext = React.createContext();
|
||||
export const MapWidthContext = React.createContext();
|
||||
export const MapHeightContext = React.createContext();
|
||||
export const InteractionEmitterContext = React.createContext();
|
||||
|
||||
export function useMapInteraction() {
|
||||
const context = useContext(MapInteractionContext);
|
||||
export function MapInteractionProvider({ value, children }) {
|
||||
const {
|
||||
stageScale,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
setPreventMapInteraction,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
interactionEmitter,
|
||||
} = value;
|
||||
const debouncedStageScale = useDebounce(stageScale, 200);
|
||||
return (
|
||||
<InteractionEmitterContext.Provider value={interactionEmitter}>
|
||||
<SetPreventMapInteractionContext.Provider
|
||||
value={setPreventMapInteraction}
|
||||
>
|
||||
<StageWidthContext.Provider value={stageWidth}>
|
||||
<StageHeightContext.Provider value={stageHeight}>
|
||||
<MapWidthContext.Provider value={mapWidth}>
|
||||
<MapHeightContext.Provider value={mapHeight}>
|
||||
<StageScaleContext.Provider value={stageScale}>
|
||||
<DebouncedStageScaleContext.Provider
|
||||
value={debouncedStageScale || 1}
|
||||
>
|
||||
{children}
|
||||
</DebouncedStageScaleContext.Provider>
|
||||
</StageScaleContext.Provider>
|
||||
</MapHeightContext.Provider>
|
||||
</MapWidthContext.Provider>
|
||||
</StageHeightContext.Provider>
|
||||
</StageWidthContext.Provider>
|
||||
</SetPreventMapInteractionContext.Provider>
|
||||
</InteractionEmitterContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useInteractionEmitter() {
|
||||
const context = useContext(InteractionEmitterContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useMapInteraction must be used within a MapInteractionProvider"
|
||||
"useInteractionEmitter must be used within a MapInteractionProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export default MapInteractionContext;
|
||||
export function useSetPreventMapInteraction() {
|
||||
const context = useContext(SetPreventMapInteractionContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useSetPreventMapInteraction must be used within a MapInteractionProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useStageWidth() {
|
||||
const context = useContext(StageWidthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useStageWidth must be used within a MapInteractionProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useStageHeight() {
|
||||
const context = useContext(StageHeightContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useStageHeight must be used within a MapInteractionProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useMapWidth() {
|
||||
const context = useContext(MapWidthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useMapWidth must be used within a MapInteractionProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useMapHeight() {
|
||||
const context = useContext(MapHeightContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useMapHeight must be used within a MapInteractionProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useStageScale() {
|
||||
const context = useContext(StageScaleContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useStageScale must be used within a MapInteractionProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useDebouncedStageScale() {
|
||||
const context = useContext(DebouncedStageScaleContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useDebouncedStageScale must be used within a MapInteractionProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -62,32 +62,50 @@ export function PlayerProvider({ session, children }) {
|
|||
}, [playerState, database, databaseStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
setPlayerState((prevState) => ({
|
||||
...prevState,
|
||||
userId,
|
||||
}));
|
||||
if (userId) {
|
||||
setPlayerState((prevState) => {
|
||||
if (prevState) {
|
||||
return {
|
||||
...prevState,
|
||||
userId,
|
||||
};
|
||||
}
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
}, [userId, setPlayerState]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateSessionId() {
|
||||
setPlayerState((prevState) => {
|
||||
if (prevState) {
|
||||
return {
|
||||
...prevState,
|
||||
sessionId: session.id,
|
||||
};
|
||||
}
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
function handleSocketConnect() {
|
||||
// Set the player state to trigger a sync
|
||||
setPlayerState({ ...playerState, sessionId: session.id });
|
||||
updateSessionId();
|
||||
}
|
||||
|
||||
function handleSocketStatus(status) {
|
||||
if (status === "joined") {
|
||||
setPlayerState({ ...playerState, sessionId: session.id });
|
||||
updateSessionId();
|
||||
}
|
||||
}
|
||||
|
||||
session.on("status", handleSocketStatus);
|
||||
session.socket?.on("connect", handleSocketConnect);
|
||||
session.socket?.on("reconnect", handleSocketConnect);
|
||||
session.socket?.io.on("reconnect", handleSocketConnect);
|
||||
|
||||
return () => {
|
||||
session.off("status", handleSocketStatus);
|
||||
session.socket?.off("connect", handleSocketConnect);
|
||||
session.socket?.off("reconnect", handleSocketConnect);
|
||||
session.socket?.io.off("reconnect", handleSocketConnect);
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -5,24 +5,19 @@ import React, {
|
|||
useCallback,
|
||||
useRef,
|
||||
} from "react";
|
||||
import * as Comlink from "comlink";
|
||||
import { decode } from "@msgpack/msgpack";
|
||||
|
||||
import { useAuth } from "./AuthContext";
|
||||
import { useDatabase } from "./DatabaseContext";
|
||||
|
||||
import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax
|
||||
|
||||
import { tokens as defaultTokens } from "../tokens";
|
||||
|
||||
const TokenDataContext = React.createContext();
|
||||
|
||||
const cachedTokenMax = 100;
|
||||
|
||||
const worker = Comlink.wrap(new DatabaseWorker());
|
||||
|
||||
export function TokenDataProvider({ children }) {
|
||||
const { database, databaseStatus } = useDatabase();
|
||||
const { database, databaseStatus, worker } = useDatabase();
|
||||
const { userId } = useAuth();
|
||||
|
||||
/**
|
||||
|
@ -71,7 +66,7 @@ export function TokenDataProvider({ children }) {
|
|||
}
|
||||
|
||||
loadTokens();
|
||||
}, [userId, database, databaseStatus]);
|
||||
}, [userId, database, databaseStatus, worker]);
|
||||
|
||||
const tokensRef = useRef(tokens);
|
||||
useEffect(() => {
|
||||
|
@ -135,7 +130,7 @@ export function TokenDataProvider({ children }) {
|
|||
|
||||
const updateToken = useCallback(
|
||||
async (id, update) => {
|
||||
const change = { ...update, lastModified: Date.now() };
|
||||
const change = { lastModified: Date.now(), ...update };
|
||||
await database.table("tokens").update(id, change);
|
||||
},
|
||||
[database]
|
||||
|
@ -143,7 +138,7 @@ export function TokenDataProvider({ children }) {
|
|||
|
||||
const updateTokens = useCallback(
|
||||
async (ids, update) => {
|
||||
const change = { ...update, lastModified: Date.now() };
|
||||
const change = { lastModified: Date.now(), ...update };
|
||||
await Promise.all(
|
||||
ids.map((id) => database.table("tokens").update(id, change))
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
import Dexie, { Version, DexieOptions } from "dexie";
|
||||
import "dexie-observable";
|
||||
import shortid from "shortid";
|
||||
|
||||
import blobToBuffer from "./helpers/blobToBuffer";
|
||||
import { getGridDefaultInset } from "./helpers/grid";
|
||||
|
@ -414,9 +415,25 @@ const versions = {
|
|||
21(v) {
|
||||
v.stores({});
|
||||
},
|
||||
// v1.8.1 - Shorten fog shape ids
|
||||
22(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
for (let id of Object.keys(state.fogShapes)) {
|
||||
const newId = shortid.generate();
|
||||
state.fogShapes[newId] = state.fogShapes[id];
|
||||
state.fogShapes[newId].id = newId;
|
||||
delete state.fogShapes[id];
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const latestVersion = 21;
|
||||
const latestVersion = 22;
|
||||
|
||||
/**
|
||||
* Load versions onto a database up to a specific version number
|
||||
|
|
|
@ -27,13 +27,15 @@ class Dice {
|
|||
const mesh = await this.loadMesh(source, material, scene);
|
||||
meshes[type] = mesh;
|
||||
};
|
||||
await addToMeshes("d4", d4Source);
|
||||
await addToMeshes("d6", d6Source);
|
||||
await addToMeshes("d8", d8Source);
|
||||
await addToMeshes("d10", d10Source);
|
||||
await addToMeshes("d12", d12Source);
|
||||
await addToMeshes("d20", d20Source);
|
||||
await addToMeshes("d100", d100Source);
|
||||
await Promise.all([
|
||||
addToMeshes("d4", d4Source),
|
||||
addToMeshes("d6", d6Source),
|
||||
addToMeshes("d8", d8Source),
|
||||
addToMeshes("d10", d10Source),
|
||||
addToMeshes("d12", d12Source),
|
||||
addToMeshes("d20", d20Source),
|
||||
addToMeshes("d100", d100Source),
|
||||
]);
|
||||
return meshes;
|
||||
}
|
||||
|
||||
|
@ -51,9 +53,14 @@ class Dice {
|
|||
|
||||
static async loadMaterial(materialName, textures, scene) {
|
||||
let pbr = new PBRMaterial(materialName, scene);
|
||||
pbr.albedoTexture = await importTextureAsync(textures.albedo);
|
||||
pbr.normalTexture = await importTextureAsync(textures.normal);
|
||||
pbr.metallicTexture = await importTextureAsync(textures.metalRoughness);
|
||||
let [albedo, normal, metalRoughness] = await Promise.all([
|
||||
importTextureAsync(textures.albedo),
|
||||
importTextureAsync(textures.normal),
|
||||
importTextureAsync(textures.metalRoughness),
|
||||
]);
|
||||
pbr.albedoTexture = albedo;
|
||||
pbr.normalTexture = normal;
|
||||
pbr.metallicTexture = metalRoughness;
|
||||
pbr.useRoughnessFromMetallicTextureAlpha = false;
|
||||
pbr.useRoughnessFromMetallicTextureGreen = true;
|
||||
pbr.useMetallnessFromMetallicTextureBlue = true;
|
||||
|
|
|
@ -117,17 +117,33 @@ class DiceTray {
|
|||
}
|
||||
|
||||
async loadMeshes() {
|
||||
this.singleMesh = (
|
||||
await SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene)
|
||||
).meshes[1];
|
||||
let [
|
||||
singleMeshes,
|
||||
doubleMeshes,
|
||||
singleAlbedoTexture,
|
||||
singleNormalTexture,
|
||||
singleMetalRoughnessTexture,
|
||||
doubleAlbedoTexture,
|
||||
doubleNormalTexture,
|
||||
doubleMetalRoughnessTexture,
|
||||
] = await Promise.all([
|
||||
SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene),
|
||||
SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene),
|
||||
importTextureAsync(singleAlbedo),
|
||||
importTextureAsync(singleNormal),
|
||||
importTextureAsync(singleMetalRoughness),
|
||||
importTextureAsync(doubleAlbedo),
|
||||
importTextureAsync(doubleNormal),
|
||||
importTextureAsync(doubleMetalRoughness),
|
||||
]);
|
||||
|
||||
this.singleMesh = singleMeshes.meshes[1];
|
||||
this.singleMesh.id = "dice_tray_single";
|
||||
this.singleMesh.name = "dice_tray";
|
||||
let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene);
|
||||
singleMaterial.albedoTexture = await importTextureAsync(singleAlbedo);
|
||||
singleMaterial.normalTexture = await importTextureAsync(singleNormal);
|
||||
singleMaterial.metallicTexture = await importTextureAsync(
|
||||
singleMetalRoughness
|
||||
);
|
||||
singleMaterial.albedoTexture = singleAlbedoTexture;
|
||||
singleMaterial.normalTexture = singleNormalTexture;
|
||||
singleMaterial.metallicTexture = singleMetalRoughnessTexture;
|
||||
singleMaterial.useRoughnessFromMetallicTextureAlpha = false;
|
||||
singleMaterial.useRoughnessFromMetallicTextureGreen = true;
|
||||
singleMaterial.useMetallnessFromMetallicTextureBlue = true;
|
||||
|
@ -137,17 +153,13 @@ class DiceTray {
|
|||
this.shadowGenerator.addShadowCaster(this.singleMesh);
|
||||
this.singleMesh.isVisible = this.size === "single";
|
||||
|
||||
this.doubleMesh = (
|
||||
await SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene)
|
||||
).meshes[1];
|
||||
this.doubleMesh = doubleMeshes.meshes[1];
|
||||
this.doubleMesh.id = "dice_tray_double";
|
||||
this.doubleMesh.name = "dice_tray";
|
||||
let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene);
|
||||
doubleMaterial.albedoTexture = await importTextureAsync(doubleAlbedo);
|
||||
doubleMaterial.normalTexture = await importTextureAsync(doubleNormal);
|
||||
doubleMaterial.metallicTexture = await importTextureAsync(
|
||||
doubleMetalRoughness
|
||||
);
|
||||
doubleMaterial.albedoTexture = doubleAlbedoTexture;
|
||||
doubleMaterial.normalTexture = doubleNormalTexture;
|
||||
doubleMaterial.metallicTexture = doubleMetalRoughnessTexture;
|
||||
doubleMaterial.useRoughnessFromMetallicTextureAlpha = false;
|
||||
doubleMaterial.useRoughnessFromMetallicTextureGreen = true;
|
||||
doubleMaterial.useMetallnessFromMetallicTextureBlue = true;
|
||||
|
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 265 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 513 KiB |
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 241 KiB |
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 27 KiB |
|
@ -20,9 +20,14 @@ class GemstoneDice extends Dice {
|
|||
|
||||
static async loadMaterial(materialName, textures, scene) {
|
||||
let pbr = new PBRMaterial(materialName, scene);
|
||||
pbr.albedoTexture = await importTextureAsync(textures.albedo);
|
||||
pbr.normalTexture = await importTextureAsync(textures.normal);
|
||||
pbr.metallicTexture = await importTextureAsync(textures.metalRoughness);
|
||||
let [albedo, normal, metalRoughness] = await Promise.all([
|
||||
importTextureAsync(textures.albedo),
|
||||
importTextureAsync(textures.normal),
|
||||
importTextureAsync(textures.metalRoughness),
|
||||
]);
|
||||
pbr.albedoTexture = albedo;
|
||||
pbr.normalTexture = normal;
|
||||
pbr.metallicTexture = metalRoughness;
|
||||
pbr.useRoughnessFromMetallicTextureAlpha = false;
|
||||
pbr.useRoughnessFromMetallicTextureGreen = true;
|
||||
pbr.useMetallnessFromMetallicTextureBlue = true;
|
||||
|
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 374 KiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 245 KiB |
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 27 KiB |
|
@ -20,8 +20,13 @@ class GlassDice extends Dice {
|
|||
|
||||
static async loadMaterial(materialName, textures, scene) {
|
||||
let pbr = new PBRMaterial(materialName, scene);
|
||||
pbr.albedoTexture = await importTextureAsync(textures.albedo);
|
||||
pbr.normalTexture = await importTextureAsync(textures.normal);
|
||||
let [albedo, normal, mask] = await Promise.all([
|
||||
importTextureAsync(textures.albedo),
|
||||
importTextureAsync(textures.normal),
|
||||
importTextureAsync(textures.mask),
|
||||
]);
|
||||
pbr.albedoTexture = albedo;
|
||||
pbr.normalTexture = normal;
|
||||
pbr.roughness = 0.25;
|
||||
pbr.metallic = 0;
|
||||
pbr.subSurface.isRefractionEnabled = true;
|
||||
|
@ -32,7 +37,7 @@ class GlassDice extends Dice {
|
|||
pbr.subSurface.minimumThickness = 10;
|
||||
pbr.subSurface.maximumThickness = 10;
|
||||
pbr.subSurface.tintColor = new Color3(43 / 255, 1, 115 / 255);
|
||||
pbr.subSurface.thicknessTexture = await importTextureAsync(textures.mask);
|
||||
pbr.subSurface.thicknessTexture = mask;
|
||||
pbr.subSurface.useMaskFromThicknessTexture = true;
|
||||
|
||||
return pbr;
|
||||
|
|
Before Width: | Height: | Size: 585 KiB After Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 316 KiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 199 KiB |
Before Width: | Height: | Size: 613 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 277 KiB |
Before Width: | Height: | Size: 972 KiB After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 916 KiB After Width: | Height: | Size: 153 KiB |
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 363 KiB |
Before Width: | Height: | Size: 911 KiB After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 47 KiB |
|
@ -2,6 +2,6 @@
|
|||
|
||||
---
|
||||
|
||||
### Database is diasbled.
|
||||
### Database is disabled.
|
||||
|
||||
Owlbear Rodeo uses a local database to store saved data. If you are seeing a database is disabled message this usually means you have data storage disabled. The most common occurrences of this is if you are using Private Browsing modes or in Firefox have the Never Remember History option enabled. The site will still function in these cases however all data will be lost when the page closes or reloads.
|
||||
Owlbear Rodeo uses a local database to store saved data. If you are seeing a database is disabled message this usually means you have data storage disabled. The most common occurrences of this is if you are using Private Browsing modes or in Firefox have the Never Remember History option enabled. The site will still function in these cases however all data will be lost when the page closes or reloads.
|
||||
|
|
|
@ -14,7 +14,7 @@ This means that the token hasn’t loaded in just yet. If they still haven’t l
|
|||
|
||||
### How big can my maps be?
|
||||
|
||||
Owlbear Rodeo doesn't impose a limit on map sizes but keep in mind the larger the map you upload the longer it will take for your players to load. We recommend trying to keep your maps under 10MB with a good internet connection and under 5MB with slower internet. If you accidently upload a map that is too big you can use the quality option in the map's settings to lower the size without needing to re-upload your map.
|
||||
Owlbear Rodeo has a map size limit of 50MB but keep in mind the larger the map you upload the longer it will take for your players to load. We recommend trying to keep your maps under 10MB in size and under 8000px on the largest edge in resolution. If you accidently upload a map that is too big you can use the quality option in the map's settings to lower the size without needing to re-upload your map.
|
||||
|
||||
### Where are my maps stored?
|
||||
|
||||
|
@ -31,3 +31,7 @@ We encourage users to create games every 24hrs. Any maps you have added or made
|
|||
### I can join my game but the spinner is constantly loading, why?
|
||||
|
||||
This could mean that the service is currently down. Please visit us on Twitter or Reddit and let us know.
|
||||
|
||||
### Unable to find owner for map?
|
||||
|
||||
If you see a message labelled "Unable to find owner for map" please ensure that the owner of the map is connected to the game. If they are, they may need to refresh their browser to reconnect.
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
## General
|
||||
|
||||
| Shortcut | Description |
|
||||
| ---------------- | ------------ |
|
||||
| W | Move Tool |
|
||||
| Space Bar (Hold) | Move Tool |
|
||||
| F | Fog Tool |
|
||||
| D | Drawing Tool |
|
||||
| M | Measure Tool |
|
||||
| Q | Pointer Tool |
|
||||
| N | Note Tool |
|
||||
| + | Zoom In |
|
||||
| - | Zoom Out |
|
||||
| Shortcut | Description |
|
||||
| ---------------- | -------------- |
|
||||
| W | Move Tool |
|
||||
| Space Bar (Hold) | Move Tool |
|
||||
| F | Fog Tool |
|
||||
| D | Drawing Tool |
|
||||
| M | Measure Tool |
|
||||
| Q | Pointer Tool |
|
||||
| N | Note Tool |
|
||||
| + | Zoom In |
|
||||
| - | Zoom Out |
|
||||
| Shift + Zoom | Precision Zoom |
|
||||
|
||||
## Fog Tool
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
[embed:](https://www.youtube.com/embed/MBy0VLsesL0)
|
||||
|
||||
## Major Changes
|
||||
|
||||
### Hex Grid Support
|
||||
|
|