grungnet/src/components/map/MapGridEditor.js
2021-02-11 10:06:06 +11:00

261 lines
7.9 KiB
JavaScript

import React, { useRef } from "react";
import { Group, Circle, Rect } from "react-konva";
import { useMapInteraction } from "../../contexts/MapInteractionContext";
import { useKeyboard } from "../../contexts/KeyboardContext";
import Vector2 from "../../helpers/Vector2";
function MapGridEditor({ map, onGridChange }) {
const {
mapWidth,
mapHeight,
stageScale,
setPreventMapInteraction,
} = useMapInteraction();
const mapSize = { x: mapWidth, y: mapHeight };
function getHandlePositions() {
const topLeft = Vector2.multiply(map.grid.inset.topLeft, mapSize);
const bottomRight = Vector2.multiply(map.grid.inset.bottomRight, mapSize);
const size = Vector2.subtract(bottomRight, topLeft);
const offset = Vector2.multiply(topLeft, -1);
return {
topLeft,
topRight: { x: bottomRight.x, y: topLeft.y },
bottomRight,
bottomLeft: { x: topLeft.x, y: bottomRight.y },
size,
offset,
};
}
const handlePositions = getHandlePositions();
const handlePreviousPositionRef = useRef();
function handleScaleCircleDragStart(event) {
const handle = event.target;
const position = getHandleNormalizedPosition(handle);
handlePreviousPositionRef.current = position;
}
function handleScaleCircleDragMove(event) {
const handle = event.target;
onGridChange(getHandleInset(handle));
handlePreviousPositionRef.current = getHandleNormalizedPosition(handle);
}
function handleScaleCircleDragEnd(event) {
onGridChange(getHandleInset(event.target));
setPreventMapInteraction(false);
}
function handleInteractivePointerDown() {
setPreventMapInteraction(true);
}
function handleInteractivePointerUp() {
setPreventMapInteraction(false);
}
function getHandleInset(handle) {
const name = handle.name();
// Find distance and direction of dragging
const previousPosition = handlePreviousPositionRef.current;
const position = getHandleNormalizedPosition(handle);
const distance = Vector2.distance(previousPosition, position);
const direction = Vector2.normalize(
Vector2.subtract(position, previousPosition)
);
const inset = map.grid.inset;
if (direction.x === 0 && direction.y === 0) {
return inset;
}
// Scale the grid direction by the distance dragged and the
// dot product between the drag direction and the grid direction
// This drags the handle while keeping the aspect ratio
if (name === "topLeft") {
// Top left to bottom right
const gridDirection = Vector2.normalize(
Vector2.subtract(inset.topLeft, inset.bottomRight)
);
const dot = Vector2.dot(direction, gridDirection);
const offset = Vector2.multiply(gridDirection, distance * dot);
const newPosition = Vector2.add(previousPosition, offset);
return {
topLeft: newPosition,
bottomRight: inset.bottomRight,
};
} else if (name === "topRight") {
// Top right to bottom left
const gridDirection = Vector2.normalize(
Vector2.subtract(
{ x: inset.bottomRight.x, y: inset.topLeft.y },
{ x: inset.topLeft.x, y: inset.bottomRight.y }
)
);
const dot = Vector2.dot(direction, gridDirection);
const offset = Vector2.multiply(gridDirection, distance * dot);
const newPosition = Vector2.add(previousPosition, offset);
return {
topLeft: { x: inset.topLeft.x, y: newPosition.y },
bottomRight: { x: newPosition.x, y: inset.bottomRight.y },
};
} else if (name === "bottomRight") {
// Bottom right to top left
const gridDirection = Vector2.normalize(
Vector2.subtract(inset.bottomRight, inset.topLeft)
);
const dot = Vector2.dot(direction, gridDirection);
const offset = Vector2.multiply(gridDirection, distance * dot);
const newPosition = Vector2.add(previousPosition, offset);
return {
topLeft: inset.topLeft,
bottomRight: newPosition,
};
} else if (name === "bottomLeft") {
// Bottom left to top right
const gridDirection = Vector2.normalize(
Vector2.subtract(
{ x: inset.topLeft.x, y: inset.bottomRight.y },
{ x: inset.bottomRight.x, y: inset.topLeft.y }
)
);
const dot = Vector2.dot(direction, gridDirection);
const offset = Vector2.multiply(gridDirection, distance * dot);
const newPosition = Vector2.add(previousPosition, offset);
return {
topLeft: { x: newPosition.x, y: inset.topLeft.y },
bottomRight: { x: inset.bottomRight.x, y: newPosition.y },
};
} else if (name === "center") {
const offset = Vector2.subtract(position, previousPosition);
return {
topLeft: Vector2.add(inset.topLeft, offset),
bottomRight: Vector2.add(inset.bottomRight, offset),
};
} else {
return inset;
}
}
function nudgeGrid(direction, scale) {
const inset = map.grid.inset;
const gridSizeNormalized = Vector2.divide(
Vector2.subtract(inset.bottomRight, inset.topLeft),
map.grid.size
);
const offset = Vector2.multiply(
Vector2.multiply(direction, gridSizeNormalized),
Math.min(scale / (stageScale * stageScale), 1)
);
onGridChange({
topLeft: Vector2.add(inset.topLeft, offset),
bottomRight: Vector2.add(inset.bottomRight, offset),
});
}
function handleKeyDown(event) {
const { key, shiftKey } = event;
const nudgeAmount = shiftKey ? 2 : 0.5;
if (key === "ArrowUp") {
// Stop arrow up/down scrolling if overflowing
event.preventDefault();
nudgeGrid({ x: 0, y: -1 }, nudgeAmount);
}
if (key === "ArrowLeft") {
nudgeGrid({ x: -1, y: 0 }, nudgeAmount);
}
if (key === "ArrowRight") {
nudgeGrid({ x: 1, y: 0 }, nudgeAmount);
}
if (key === "ArrowDown") {
event.preventDefault();
nudgeGrid({ x: 0, y: 1 }, nudgeAmount);
}
}
useKeyboard(handleKeyDown);
function getHandleNormalizedPosition(handle) {
return Vector2.divide({ x: handle.x(), y: handle.y() }, mapSize);
}
const editCircleRadius = Math.max(
(Math.min(mapWidth, mapHeight) / 30) * Math.max(1 / stageScale, 1),
1
);
const editCircleProps = {
radius: editCircleRadius,
fill: "rgba(0, 0, 0, 0.5)",
stroke: "white",
strokeWidth: editCircleRadius / 5,
draggable: true,
onDragStart: handleScaleCircleDragStart,
onDragMove: handleScaleCircleDragMove,
onDragEnd: handleScaleCircleDragEnd,
onMouseDown: handleInteractivePointerDown,
onMouseUp: handleInteractivePointerUp,
onTouchStart: handleInteractivePointerDown,
onTouchEnd: handleInteractivePointerUp,
};
const editRectProps = {
fill: "transparent",
stroke: "rgba(255, 255, 255, 0.75)",
strokeWidth: editCircleRadius / 10,
};
return (
<Group>
<Rect
width={handlePositions.size.x}
height={handlePositions.size.y}
offset={handlePositions.offset}
{...editRectProps}
/>
<Circle
x={handlePositions.topLeft.x}
y={handlePositions.topLeft.y}
name="topLeft"
{...editCircleProps}
/>
<Circle
x={handlePositions.topRight.x}
y={handlePositions.topRight.y}
name="topRight"
{...editCircleProps}
/>
<Circle
x={handlePositions.bottomRight.x}
y={handlePositions.bottomRight.y}
name="bottomRight"
{...editCircleProps}
/>
<Circle
x={handlePositions.bottomLeft.x}
y={handlePositions.bottomLeft.y}
name="bottomLeft"
{...editCircleProps}
/>
<Circle
x={(handlePositions.topLeft.x + handlePositions.bottomRight.x) / 2}
y={(handlePositions.topLeft.y + handlePositions.bottomRight.y) / 2}
name="center"
{...editCircleProps}
radius={editCircleRadius / 1.5}
/>
</Group>
);
}
export default MapGridEditor;