From 343b15c917e219e25c38f1d7cecbe510a6b6fe95 Mon Sep 17 00:00:00 2001 From: Aaron Boodman Date: Tue, 9 Mar 2021 17:43:01 -1000 Subject: [PATCH] Refactor: give up on svg layout. Working with svg as a layout system is frustrating and buggy. It's easier if you put each shape in its own svg container and move that around using normal HTML primitives. Also factored out a Rect shape which is stateless and a RectController which has the smarts. I think this will make things easier to follow when I add resize. --- frontend/collaborator.module.css | 8 -- frontend/collaborator.tsx | 20 ++-- frontend/designer.tsx | 166 +++++++++++-------------------- frontend/events.tsx | 10 ++ frontend/nav.tsx | 5 +- frontend/rect-controller.tsx | 98 ++++++++++++++++++ frontend/rect.tsx | 57 ++++++----- frontend/selection.tsx | 14 --- pages/api/replicache-pull.ts | 2 +- 9 files changed, 208 insertions(+), 172 deletions(-) create mode 100644 frontend/events.tsx create mode 100644 frontend/rect-controller.tsx delete mode 100644 frontend/selection.tsx diff --git a/frontend/collaborator.module.css b/frontend/collaborator.module.css index a95882c..17ad63f 100644 --- a/frontend/collaborator.module.css +++ b/frontend/collaborator.module.css @@ -8,14 +8,6 @@ pointer-events: none; } -.collaborator > svg { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; -} - .cursor { position: absolute; font-family: "Inter", sans-serif; diff --git a/frontend/collaborator.tsx b/frontend/collaborator.tsx index 67de743..5919a36 100644 --- a/frontend/collaborator.tsx +++ b/frontend/collaborator.tsx @@ -72,17 +72,15 @@ export function Collaborator({ return (
{clientInfo.selectedID && ( - - - + )}
(null); - const nodeRef = useRef(null); - - const onMouseDown = (id: string, pageX: number, pageY: number) => { - data.selectShape({ clientID: data.clientID, shapeID: id }); - setLastDrag({ pageX, pageY }); - }; - - const onMouseMove = (pageX: number, pageY: number) => { - if (!nodeRef.current) { - return; - } - - data.setCursor({ - id: data.clientID, - x: pageX, - y: pageY - nodeRef.current.offsetTop, - }); - - if (!lastDrag) { - return; - } - - // This is subtle, and worth drawing attention to: - // In order to properly resolve conflicts, what we want to capture in - // mutation arguments is the *intent* of the mutation, not the effect. - // In this case, the intent is the amount the mouse was moved by, locally. - // We will apply this movement to whatever the state happens to be when we - // replay. If somebody else was moving the object at the same moment, we'll - // then end up with a union of the two vectors, which is what we want! - data.moveShape({ - id: selectedID, - dx: pageX - lastDrag.pageX, - dy: pageY - lastDrag.pageY, - }); - setLastDrag({ pageX, pageY }); - }; - - const onMouseUp = () => { - setLastDrag(null); - }; + const ref = useRef(null); + const [dragging, setDragging] = useState(false); const handlers = { moveLeft: () => data.moveShape({ id: selectedID, dx: -20, dy: 0 }), @@ -68,6 +27,16 @@ export function Designer({ data }: { data: Data }) { }, }; + const onMouseMove = ({ pageX, pageY }: { pageX: number; pageY: number }) => { + if (ref && ref.current) { + data.setCursor({ + id: data.clientID, + x: pageX, + y: pageY - ref.current.offsetTop, + }); + } + }; + return (
onMouseMove(e.pageX, e.pageY), + style: { + position: "relative", + display: "flex", + flex: 1, + overflow: "hidden", + }, + onMouseMove, onTouchMove: (e) => touchToMouse(e, onMouseMove), - onMouseUp, - onTouchEnd: () => onMouseUp(), }} > - - {ids.map((id) => ( - // shapes + {ids.map((id) => ( + // draggable rects + + ))} + + { + // self-highlight + !dragging && overID && ( - data.overShape({ clientID: data.clientID, shapeID: id }), - onMouseLeave: () => - data.overShape({ clientID: data.clientID, shapeID: "" }), - onMouseDown: (e) => onMouseDown(id, e.pageX, e.pageY), - onTouchStart: (e) => - touchToMouse(e, (pageX, pageY) => - onMouseDown(id, pageX, pageY) - ), + id: overID, + highlight: true, }} /> - ))} + ) + } - { - // self-highlight - !lastDrag && overID && ( - - ) - } + { + // self-selection + selectedID && ( + + ) + } - { - // self-selection - selectedID && ( - - ) - } - { // collaborators // foreignObject seems super buggy in Safari, so instead we do the @@ -170,13 +126,3 @@ const keyMap = { moveDown: ["down", "shift+down"], deleteShape: ["del", "backspace"], }; - -function touchToMouse( - e: TouchEvent, - handler: (pageX: number, pageY: number) => void -) { - if (e.touches.length == 1) { - const t = e.touches[0]; - handler(t.pageX, t.pageY); - } -} diff --git a/frontend/events.tsx b/frontend/events.tsx new file mode 100644 index 0000000..f86f304 --- /dev/null +++ b/frontend/events.tsx @@ -0,0 +1,10 @@ +import { TouchEvent } from "react"; + +export function touchToMouse( + e: TouchEvent, + handler: ({ pageX, pageY }: { pageX: number; pageY: number }) => void +) { + if (e.touches.length == 1) { + handler(e.touches[0]); + } +} diff --git a/frontend/nav.tsx b/frontend/nav.tsx index f87e059..378c12c 100644 --- a/frontend/nav.tsx +++ b/frontend/nav.tsx @@ -13,14 +13,15 @@ export function Nav({ data }: { data: Data | null }) { if (!data) { return; } + const s = randInt(100, 400); await data.createShape({ id: newID(), shape: { type: "rect", x: randInt(0, 400), y: randInt(0, 400), - width: randInt(100, 400), - height: randInt(100, 400), + width: s, + height: s, rotate: randInt(0, 359), fill: colors[randInt(0, colors.length - 1)], }, diff --git a/frontend/rect-controller.tsx b/frontend/rect-controller.tsx new file mode 100644 index 0000000..2564116 --- /dev/null +++ b/frontend/rect-controller.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { Data } from "./data"; +import { touchToMouse } from "./events"; +import { Rect } from "./rect"; + +// TODO: In the future I imagine this becoming ShapeController and +// there also be a Shape that wraps Rect and also knows how to draw Circle, etc. +export function RectController({ + data, + id, + onDrag, +}: { + data: Data; + id: string; + onDrag: (dragging: boolean) => void; +}) { + const [lastDrag, setLastDrag] = useState<{ + pageX: number; + pageY: number; + } | null>(null); + const shape = data.useShapeByID(id); + + const onMouseEnter = () => + data.overShape({ clientID: data.clientID, shapeID: id }); + const onMouseLeave = () => + data.overShape({ clientID: data.clientID, shapeID: "" }); + + const onMouseDown = ({ pageX, pageY }: { pageX: number; pageY: number }) => { + data.selectShape({ clientID: data.clientID, shapeID: id }); + setLastDrag({ pageX, pageY }); + onDrag && onDrag(true); + }; + + const onTouchMove = (e: any) => touchToMouse(e, onMouseMove); + const onMouseMove = ({ pageX, pageY }: { pageX: number; pageY: number }) => { + if (!lastDrag) { + return; + } + + // This is subtle, and worth drawing attention to: + // In order to properly resolve conflicts, what we want to capture in + // mutation arguments is the *intent* of the mutation, not the effect. + // In this case, the intent is the amount the mouse was moved by, locally. + // We will apply this movement to whatever the state happens to be when we + // replay. If somebody else was moving the object at the same moment, we'll + // then end up with a union of the two vectors, which is what we want! + data.moveShape({ + id, + dx: pageX - lastDrag.pageX, + dy: pageY - lastDrag.pageY, + }); + setLastDrag({ pageX, pageY }); + }; + + const onMouseUp = () => { + setLastDrag(null); + onDrag && onDrag(false); + }; + + const dragListeners = { + mousemove: (e: any) => onMouseMove(e), + touchmove: (e: any) => onTouchMove(e), + mouseup: onMouseUp, + touchend: onMouseUp, + }; + + useEffect(() => { + if (!lastDrag) { + return; + } + Object.entries(dragListeners).forEach(([key, val]) => + window.addEventListener(key, val) + ); + return () => { + Object.entries(dragListeners).forEach(([key, val]) => + window.removeEventListener(key, val) + ); + }; + }); + + if (!shape) { + return null; + } + + return ( + onMouseDown(e), + onTouchStart: (e: any) => touchToMouse(e, onMouseDown), + onMouseEnter, + onMouseLeave, + }} + /> + ); +} diff --git a/frontend/rect.tsx b/frontend/rect.tsx index b12d90e..1c9b1af 100644 --- a/frontend/rect.tsx +++ b/frontend/rect.tsx @@ -1,5 +1,4 @@ -import React, { MouseEventHandler, TouchEventHandler, useState } from "react"; -import { Shape } from "../shared/shape"; +import React, { MouseEventHandler, TouchEventHandler } from "react"; import { Data } from "./data"; export function Rect({ @@ -7,53 +6,59 @@ export function Rect({ id, highlight = false, highlightColor = "rgb(74,158,255)", - onMouseEnter, - onMouseLeave, onMouseDown, onTouchStart, + onMouseEnter, + onMouseLeave, }: { data: Data; id: string; highlight?: boolean; highlightColor?: string; - onMouseEnter?: MouseEventHandler; - onMouseLeave?: MouseEventHandler; onMouseDown?: MouseEventHandler; onTouchStart?: TouchEventHandler; + onMouseEnter?: MouseEventHandler; + onMouseLeave?: MouseEventHandler; }) { const shape = data.useShapeByID(id); if (!shape) { return null; } + const enableEvents = + onMouseDown || onTouchStart || onMouseEnter || onMouseLeave; + return ( - + > + + ); } - -function getTransformMatrix(shape: Shape): any { - if (!shape.rotate) { - return null; - } - let centerX = shape.width / 2 + shape.x; - let centerY = shape.height / 2 + shape.y; - return `rotate(${shape.rotate} ${centerX} ${centerY})`; -} diff --git a/frontend/selection.tsx b/frontend/selection.tsx deleted file mode 100644 index 4a2916d..0000000 --- a/frontend/selection.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Data } from "./data"; -import { Rect } from "./rect"; - -export function Selection({ data, shapeID }: { data: Data; shapeID: string }) { - return ( - - ); -} diff --git a/pages/api/replicache-pull.ts b/pages/api/replicache-pull.ts index 28e2e93..95754d4 100644 --- a/pages/api/replicache-pull.ts +++ b/pages/api/replicache-pull.ts @@ -86,7 +86,7 @@ const pullResponse = t.type({ t.type({ op: t.literal("put"), key: t.string, - value: t.any, // TODO: Define a JSON type? + value: t.any, // TODO: Define a JSON type? }), t.type({ op: t.literal("del"),