Merge pull request #39 from mitchemmc/release/v1.8.1

Release/v1.8.1
This commit is contained in:
Mitchell McCaffrey 2021-04-21 20:22:02 +10:00 committed by GitHub
commit deb1a5dbd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
174 changed files with 3371 additions and 1415 deletions

View File

@ -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

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -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>

View File

@ -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") {

View File

@ -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={{

View File

@ -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%)",
},

View 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;

View 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;

View 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;

View File

@ -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>
);
}

View File

@ -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}
/>
);
}

View File

@ -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>

View File

@ -209,7 +209,6 @@ function Map({
tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup}
dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)}
token={tokensById[tokenDraggingOptions.tokenState.tokenId]}
mapState={mapState}
/>
);

View File

@ -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);

View File

@ -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>
);
}

View File

@ -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();

View File

@ -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);

View File

@ -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);
}

View File

@ -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>
);
}

View File

@ -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);

View File

@ -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);

View File

@ -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(() => {

View File

@ -0,0 +1,5 @@
import React from "react";
function MapTest() {}
export default MapTest;

View File

@ -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}

View File

@ -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]);

View File

@ -119,7 +119,6 @@ function MapTokens({
!(tokenState.id in disabledTokens) &&
!tokenState.locked
}
mapState={mapState}
fadeOnHover={selectedToolId === "drawing"}
map={map}
/>

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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();

View File

@ -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>

View File

@ -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

View File

@ -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 }) {

View File

@ -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>
);

View File

@ -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,

View File

@ -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(() => {

View File

@ -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 },
});
}

View File

@ -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

View File

@ -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 (

View File

@ -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,
});
}
}

View File

@ -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>
);

View File

@ -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;
}

View 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;
}

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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);
};
});

View File

@ -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))
);

View File

@ -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

View File

@ -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;

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -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.

View File

@ -14,7 +14,7 @@ This means that the token hasnt loaded in just yet. If they still havent 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.

View File

@ -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

View File

@ -1,3 +1,5 @@
[embed:](https://www.youtube.com/embed/MBy0VLsesL0)
## Major Changes
### Hex Grid Support

Some files were not shown because too many files have changed in this diff Show More