Add transformer to note

This commit is contained in:
Mitchell McCaffrey 2021-08-05 16:24:46 +10:00
parent 59d46e1d27
commit d1e62e850a
9 changed files with 170 additions and 135 deletions

View File

@ -20,10 +20,13 @@ import { Note as NoteType } from "../../types/Note";
import {
NoteChangeEventHandler,
NoteDragEventHandler,
NoteMenuCloseEventHandler,
NoteMenuOpenEventHandler,
} from "../../types/Events";
import { Map } from "../../types/Map";
import Transformer from "./Transformer";
const defaultFontSize = 16;
type NoteProps = {
@ -31,10 +34,12 @@ type NoteProps = {
map: Map | null;
onNoteChange?: NoteChangeEventHandler;
onNoteMenuOpen?: NoteMenuOpenEventHandler;
onNoteMenuClose?: NoteMenuCloseEventHandler;
draggable: boolean;
onNoteDragStart?: NoteDragEventHandler;
onNoteDragEnd?: NoteDragEventHandler;
fadeOnHover: boolean;
selected: boolean;
};
function Note({
@ -42,10 +47,12 @@ function Note({
map,
onNoteChange,
onNoteMenuOpen,
onNoteMenuClose,
draggable,
onNoteDragStart,
onNoteDragEnd,
fadeOnHover,
selected,
}: NoteProps) {
const userId = useUserId();
@ -173,6 +180,30 @@ function Note({
const textRef = useRef<Konva.Text>(null);
const noteRef = useRef<Konva.Group>(null);
const [isTransforming, setIsTransforming] = useState(false);
function handleTransformStart() {
setIsTransforming(true);
onNoteMenuClose?.();
}
function handleTransformEnd(event: Konva.KonvaEventObject<Event>) {
if (noteRef.current) {
const sizeChange = event.target.scaleX();
const rotation = event.target.rotation();
onNoteChange?.({
[note.id]: {
size: note.size * sizeChange,
rotation: rotation,
},
});
onNoteMenuOpen?.(note.id, noteRef.current);
noteRef.current.scaleX(1);
noteRef.current.scaleY(1);
}
setIsTransforming(false);
}
// Animate to new note positions if edited by others
const noteX = note.x * mapWidth;
const noteY = note.y * mapHeight;
@ -194,62 +225,73 @@ function Note({
const noteName = `note${note.locked ? "-locked" : ""}`;
return (
<animated.Group
{...props}
id={note.id}
onClick={handleClick}
onTap={handleClick}
width={noteWidth}
height={note.textOnly ? undefined : noteHeight}
offsetX={noteWidth / 2}
offsetY={noteHeight / 2}
draggable={draggable}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragMove={handleDragMove}
onMouseDown={handlePointerDown}
onMouseUp={handlePointerUp}
onTouchStart={handlePointerDown}
onTouchEnd={handlePointerUp}
onMouseEnter={handlePointerEnter}
onMouseLeave={handlePointerLeave}
opacity={note.visible ? noteOpacity : 0.5}
name={noteName}
>
{!note.textOnly && (
<Rect
width={noteWidth}
height={noteHeight}
shadowColor="rgba(0, 0, 0, 0.16)"
shadowOffset={{ x: 0, y: 3 }}
shadowBlur={6}
cornerRadius={0.25}
fill={colors[note.color]}
<>
<animated.Group
{...props}
id={note.id}
onClick={handleClick}
onTap={handleClick}
width={noteWidth}
height={note.textOnly ? undefined : noteHeight}
rotation={note.rotation}
offsetX={noteWidth / 2}
offsetY={noteHeight / 2}
draggable={draggable}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragMove={handleDragMove}
onMouseDown={handlePointerDown}
onMouseUp={handlePointerUp}
onTouchStart={handlePointerDown}
onTouchEnd={handlePointerUp}
onMouseEnter={handlePointerEnter}
onMouseLeave={handlePointerLeave}
opacity={note.visible ? noteOpacity : 0.5}
name={noteName}
ref={noteRef}
>
{!note.textOnly && (
<Rect
width={noteWidth}
height={noteHeight}
shadowColor="rgba(0, 0, 0, 0.16)"
shadowOffset={{ x: 0, y: 3 }}
shadowBlur={6}
cornerRadius={0.25}
fill={colors[note.color]}
/>
)}
<Text
text={note.text}
fill={
note.textOnly
? colors[note.color]
: note.color === "black" || note.color === "darkGray"
? "white"
: "black"
}
align="left"
verticalAlign="middle"
padding={notePadding / fontScale}
fontSize={defaultFontSize}
// Scale font instead of changing font size to avoid kerning issues with Firefox
scaleX={fontScale}
scaleY={fontScale}
width={noteWidth / fontScale}
height={note.textOnly ? undefined : noteHeight / fontScale}
wrap="word"
/>
)}
<Text
text={note.text}
fill={
note.textOnly
? colors[note.color]
: note.color === "black" || note.color === "darkGray"
? "white"
: "black"
}
align="left"
verticalAlign="middle"
padding={notePadding / fontScale}
fontSize={defaultFontSize}
// Scale font instead of changing font size to avoid kerning issues with Firefox
scaleX={fontScale}
scaleY={fontScale}
width={noteWidth / fontScale}
height={note.textOnly ? undefined : noteHeight / fontScale}
wrap="word"
{/* Use an invisible text block to work out text sizing */}
<Text visible={false} ref={textRef} text={note.text} wrap="none" />
</animated.Group>
<Transformer
active={(!note.locked && selected) || isTransforming}
nodeRef={noteRef}
onTransformEnd={handleTransformEnd}
onTransformStart={handleTransformStart}
gridScale={map?.grid.measurement.scale || ""}
/>
{/* Use an invisible text block to work out text sizing */}
<Text visible={false} ref={textRef} text={note.text} wrap="none" />
</animated.Group>
</>
);
}

View File

@ -148,7 +148,7 @@ function Transformer({
if (movingAnchor === "rotater") {
text.text(`${node.rotation().toFixed(0)}°`);
} else {
const nodeRect = node.getClientRect();
const nodeRect = node.getClientRect({ skipShadow: true });
const nodeScale = Vector2.divide(
{ x: nodeRect.width, y: nodeRect.height },
gridCellAbsoluteSize

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState } from "react";
import { Box, Flex, Text, IconButton } from "theme-ui";
import { Box, Flex, IconButton } from "theme-ui";
import Konva from "konva";
import Slider from "../Slider";
import TextareaAutosize from "../TextareaAutoSize";
import MapMenu from "../map/MapMenu";
@ -27,8 +26,6 @@ import {
import { Note } from "../../types/Note";
import { Map } from "../../types/Map";
const defaultNoteMaxSize = 6;
type NoteMenuProps = {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
@ -50,12 +47,10 @@ function NoteMenu({
const wasOpen = usePrevious(isOpen);
const [noteMaxSize, setNoteMaxSize] = useState(defaultNoteMaxSize);
const [menuLeft, setMenuLeft] = useState(0);
const [menuTop, setMenuTop] = useState(0);
useEffect(() => {
if (isOpen && !wasOpen && note) {
setNoteMaxSize(Math.max(note.size, defaultNoteMaxSize));
// Update menu position
if (noteNode) {
const nodeRect = noteNode.getClientRect();
@ -64,8 +59,8 @@ function NoteMenu({
const mapRect = mapElement.getBoundingClientRect();
// Center X for the menu which is 156px wide
setMenuLeft(mapRect.left + nodeRect.x + nodeRect.width / 2 - 156 / 2);
// Y 12px from the bottom
setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 12);
// Y 20px from the bottom
setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 20);
}
}
}
@ -83,11 +78,6 @@ function NoteMenu({
onNoteChange({ [note.id]: { color: color } });
}
function handleSizeChange(event: React.ChangeEvent<HTMLInputElement>) {
const newSize = parseFloat(event.target.value);
note && onNoteChange({ [note.id]: { size: newSize } });
}
function handleVisibleChange() {
note && onNoteChange({ [note.id]: { visible: !note.visible } });
}
@ -198,24 +188,6 @@ function NoteMenu({
</Box>
))}
</Box>
<Flex sx={{ alignItems: "center" }}>
<Text
as="label"
variant="body2"
sx={{ width: "40%", fontSize: "16px" }}
p={1}
>
Size:
</Text>
<Slider
value={(note && note.size) || 1}
onChange={handleSizeChange}
step={0.5}
min={0.5}
max={noteMaxSize}
mr={1}
/>
</Flex>
{/* Only show hide and lock token actions to map owners */}
{map && map.owner === userId && (
<Flex sx={{ alignItems: "center", justifyContent: "space-around" }}>

View File

@ -18,8 +18,6 @@ import { Map } from "../../types/Map";
import { Note as NoteType } from "../../types/Note";
import {
NoteCreateEventHander,
NoteChangeEventHandler,
NoteDragEventHandler,
NoteMenuOpenEventHandler,
} from "../../types/Events";
@ -29,26 +27,16 @@ type MapNoteProps = {
map: Map | null;
active: boolean;
onNoteCreate: NoteCreateEventHander;
onNoteChange: NoteChangeEventHandler;
notes: NoteType[];
onNoteMenuOpen: NoteMenuOpenEventHandler;
draggable: boolean;
onNoteDragStart: NoteDragEventHandler;
onNoteDragEnd: NoteDragEventHandler;
fadeOnHover: boolean;
children: React.ReactNode;
};
function NoteTool({
map,
active,
onNoteCreate,
onNoteChange,
notes,
onNoteMenuOpen,
draggable,
onNoteDragStart,
onNoteDragEnd,
fadeOnHover,
children,
}: MapNoteProps) {
const interactionEmitter = useInteractionEmitter();
const userId = useUserId();
@ -101,6 +89,7 @@ function NoteTool({
locked: false,
color: "yellow",
textOnly: false,
rotation: 0,
});
setIsBrushDown(true);
}
@ -147,21 +136,11 @@ function NoteTool({
return (
<Group id="notes">
{notes.map((note) => (
<Note
note={note}
map={map}
key={note.id}
onNoteMenuOpen={onNoteMenuOpen}
draggable={draggable && !note.locked}
onNoteChange={onNoteChange}
onNoteDragStart={onNoteDragStart}
onNoteDragEnd={onNoteDragEnd}
fadeOnHover={fadeOnHover}
/>
))}
{children}
<Group ref={creatingNoteRef}>
{isBrushDown && noteData && <Note note={noteData} map={map} />}
{isBrushDown && noteData && (
<Note note={noteData} map={map} selected={false} />
)}
</Group>
</Group>
);

View File

@ -1,6 +1,7 @@
import Konva from "konva";
import { KonvaEventObject } from "konva/lib/Node";
import { useState } from "react";
import Note from "../components/konva/Note";
import NoteDragOverlay from "../components/note/NoteDragOverlay";
import NoteMenu from "../components/note/NoteMenu";
import NoteTool from "../components/tools/NoteTool";
@ -12,7 +13,11 @@ import {
} from "../types/Events";
import { Map, MapToolId } from "../types/Map";
import { MapState } from "../types/MapState";
import { Note, NoteDraggingOptions, NoteMenuOptions } from "../types/Note";
import {
Note as NoteType,
NoteDraggingOptions,
NoteMenuOptions,
} from "../types/Note";
function useMapNotes(
map: Map | null,
@ -37,6 +42,10 @@ function useMapNotes(
setIsNoteMenuOpen(true);
}
function handleNoteMenuClose() {
setIsNoteMenuOpen(false);
}
function handleNoteDragStart(_: KonvaEventObject<DragEvent>, noteId: string) {
setNoteDraggingOptions({ dragging: true, noteId });
}
@ -56,29 +65,43 @@ function useMapNotes(
map={map}
active={selectedToolId === "note"}
onNoteCreate={onNoteCreate}
onNoteChange={onNoteChange}
notes={
mapState
? Object.values(mapState.notes).sort((a, b) =>
sortNotes(a, b, noteDraggingOptions)
)
: []
}
onNoteMenuOpen={handleNoteMenuOpen}
draggable={
allowNoteEditing &&
(selectedToolId === "note" || selectedToolId === "move")
}
onNoteDragStart={handleNoteDragStart}
onNoteDragEnd={handleNoteDragEnd}
fadeOnHover={selectedToolId === "drawing"}
/>
>
{(mapState
? Object.values(mapState.notes).sort((a, b) =>
sortNotes(a, b, noteDraggingOptions)
)
: []
).map((note) => (
<Note
note={note}
map={map}
key={note.id}
onNoteMenuOpen={handleNoteMenuOpen}
onNoteMenuClose={handleNoteMenuClose}
draggable={
allowNoteEditing &&
(selectedToolId === "note" || selectedToolId === "move") &&
!note.locked
}
onNoteChange={onNoteChange}
onNoteDragStart={handleNoteDragStart}
onNoteDragEnd={handleNoteDragEnd}
fadeOnHover={selectedToolId === "drawing"}
selected={
!!noteMenuOptions &&
isNoteMenuOpen &&
noteMenuOptions.noteId === note.id
}
/>
))}
</NoteTool>
);
const noteMenu = (
<NoteMenu
isOpen={isNoteMenuOpen}
onRequestClose={() => setIsNoteMenuOpen(false)}
onRequestClose={handleNoteMenuClose}
onNoteChange={onNoteChange}
note={noteMenuOptions && mapState?.notes[noteMenuOptions.noteId]}
noteNode={noteMenuOptions?.noteNode}
@ -100,8 +123,8 @@ function useMapNotes(
export default useMapNotes;
function sortNotes(
a: Note,
b: Note,
a: NoteType,
b: NoteType,
noteDraggingOptions?: NoteDraggingOptions
) {
if (

View File

@ -46,6 +46,7 @@ export type NoteMenuOpenEventHandler = (
noteId: string,
noteNode: Konva.Node
) => void;
export type NoteMenuCloseEventHandler = () => void;
export type NoteDragEventHandler = (
event: Konva.KonvaEventObject<DragEvent>,
noteId: string

View File

@ -13,6 +13,7 @@ export type Note = {
visible: boolean;
x: number;
y: number;
rotation: number;
};
export type NoteMenuOptions = {

View File

@ -834,9 +834,22 @@ export const versions: Record<number, VersionCallback> = {
_uncommittedChanges: null,
});
},
// v1.10.0 - Add rotation to notes
37(v, onUpgrade) {
v.stores({}).upgrade((tx) => {
onUpgrade?.(37);
tx.table("states")
.toCollection()
.modify((state) => {
for (let id in state.notes) {
state.notes[id].rotation = 0;
}
});
});
},
};
export const latestVersion = 36;
export const latestVersion = 37;
/**
* Load versions onto a database up to a specific version number

View File

@ -40,6 +40,9 @@ export const NoteSchema: JSONSchemaType<Note> = {
y: {
type: "number",
},
rotation: {
type: "number",
},
},
required: [
"color",
@ -53,6 +56,7 @@ export const NoteSchema: JSONSchemaType<Note> = {
"visible",
"x",
"y",
"rotation",
],
type: "object",
};