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

View File

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

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState } from "react"; 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 Konva from "konva";
import Slider from "../Slider";
import TextareaAutosize from "../TextareaAutoSize"; import TextareaAutosize from "../TextareaAutoSize";
import MapMenu from "../map/MapMenu"; import MapMenu from "../map/MapMenu";
@ -27,8 +26,6 @@ import {
import { Note } from "../../types/Note"; import { Note } from "../../types/Note";
import { Map } from "../../types/Map"; import { Map } from "../../types/Map";
const defaultNoteMaxSize = 6;
type NoteMenuProps = { type NoteMenuProps = {
isOpen: boolean; isOpen: boolean;
onRequestClose: RequestCloseEventHandler; onRequestClose: RequestCloseEventHandler;
@ -50,12 +47,10 @@ function NoteMenu({
const wasOpen = usePrevious(isOpen); const wasOpen = usePrevious(isOpen);
const [noteMaxSize, setNoteMaxSize] = useState(defaultNoteMaxSize);
const [menuLeft, setMenuLeft] = useState(0); const [menuLeft, setMenuLeft] = useState(0);
const [menuTop, setMenuTop] = useState(0); const [menuTop, setMenuTop] = useState(0);
useEffect(() => { useEffect(() => {
if (isOpen && !wasOpen && note) { if (isOpen && !wasOpen && note) {
setNoteMaxSize(Math.max(note.size, defaultNoteMaxSize));
// Update menu position // Update menu position
if (noteNode) { if (noteNode) {
const nodeRect = noteNode.getClientRect(); const nodeRect = noteNode.getClientRect();
@ -64,8 +59,8 @@ function NoteMenu({
const mapRect = mapElement.getBoundingClientRect(); const mapRect = mapElement.getBoundingClientRect();
// Center X for the menu which is 156px wide // Center X for the menu which is 156px wide
setMenuLeft(mapRect.left + nodeRect.x + nodeRect.width / 2 - 156 / 2); setMenuLeft(mapRect.left + nodeRect.x + nodeRect.width / 2 - 156 / 2);
// Y 12px from the bottom // Y 20px from the bottom
setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 12); setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 20);
} }
} }
} }
@ -83,11 +78,6 @@ function NoteMenu({
onNoteChange({ [note.id]: { color: color } }); 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() { function handleVisibleChange() {
note && onNoteChange({ [note.id]: { visible: !note.visible } }); note && onNoteChange({ [note.id]: { visible: !note.visible } });
} }
@ -198,24 +188,6 @@ function NoteMenu({
</Box> </Box>
))} ))}
</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 */} {/* Only show hide and lock token actions to map owners */}
{map && map.owner === userId && ( {map && map.owner === userId && (
<Flex sx={{ alignItems: "center", justifyContent: "space-around" }}> <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 { Note as NoteType } from "../../types/Note";
import { import {
NoteCreateEventHander, NoteCreateEventHander,
NoteChangeEventHandler,
NoteDragEventHandler,
NoteMenuOpenEventHandler, NoteMenuOpenEventHandler,
} from "../../types/Events"; } from "../../types/Events";
@ -29,26 +27,16 @@ type MapNoteProps = {
map: Map | null; map: Map | null;
active: boolean; active: boolean;
onNoteCreate: NoteCreateEventHander; onNoteCreate: NoteCreateEventHander;
onNoteChange: NoteChangeEventHandler;
notes: NoteType[];
onNoteMenuOpen: NoteMenuOpenEventHandler; onNoteMenuOpen: NoteMenuOpenEventHandler;
draggable: boolean; children: React.ReactNode;
onNoteDragStart: NoteDragEventHandler;
onNoteDragEnd: NoteDragEventHandler;
fadeOnHover: boolean;
}; };
function NoteTool({ function NoteTool({
map, map,
active, active,
onNoteCreate, onNoteCreate,
onNoteChange,
notes,
onNoteMenuOpen, onNoteMenuOpen,
draggable, children,
onNoteDragStart,
onNoteDragEnd,
fadeOnHover,
}: MapNoteProps) { }: MapNoteProps) {
const interactionEmitter = useInteractionEmitter(); const interactionEmitter = useInteractionEmitter();
const userId = useUserId(); const userId = useUserId();
@ -101,6 +89,7 @@ function NoteTool({
locked: false, locked: false,
color: "yellow", color: "yellow",
textOnly: false, textOnly: false,
rotation: 0,
}); });
setIsBrushDown(true); setIsBrushDown(true);
} }
@ -147,21 +136,11 @@ function NoteTool({
return ( return (
<Group id="notes"> <Group id="notes">
{notes.map((note) => ( {children}
<Note
note={note}
map={map}
key={note.id}
onNoteMenuOpen={onNoteMenuOpen}
draggable={draggable && !note.locked}
onNoteChange={onNoteChange}
onNoteDragStart={onNoteDragStart}
onNoteDragEnd={onNoteDragEnd}
fadeOnHover={fadeOnHover}
/>
))}
<Group ref={creatingNoteRef}> <Group ref={creatingNoteRef}>
{isBrushDown && noteData && <Note note={noteData} map={map} />} {isBrushDown && noteData && (
<Note note={noteData} map={map} selected={false} />
)}
</Group> </Group>
</Group> </Group>
); );

View File

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

View File

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

View File

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

View File

@ -834,9 +834,22 @@ export const versions: Record<number, VersionCallback> = {
_uncommittedChanges: null, _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 * Load versions onto a database up to a specific version number

View File

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