fix zomming
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import type { Card, Viewport } from "./types";
|
||||
import { ViewportContext } from "./viewport";
|
||||
import { NoteCardView } from "./cards/NoteCardView";
|
||||
import "./canvas.css";
|
||||
|
||||
@@ -119,17 +120,14 @@ export function Canvas({ initialCards }: CanvasProps) {
|
||||
backgroundSize: `${40 * vp.scale}px ${40 * vp.scale}px`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="canvas-world"
|
||||
style={{ transform: `translate(${vp.x}px, ${vp.y}px) scale(${vp.scale})` }}
|
||||
>
|
||||
<ViewportContext.Provider value={vp}>
|
||||
{cards.map((c) => {
|
||||
if (c.kind === "note") {
|
||||
return <NoteCardView key={c.id} card={c} onUpdate={(p) => updateCard(c.id, p)} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</ViewportContext.Provider>
|
||||
<div className="canvas-hud">
|
||||
<span>x {vp.x.toFixed(0)}</span>
|
||||
<span>y {vp.y.toFixed(0)}</span>
|
||||
|
||||
@@ -25,15 +25,6 @@
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
.canvas-world {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.canvas-hud {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
@@ -47,24 +38,25 @@
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
color: var(--text);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: absolute;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
border: calc(1px * var(--scale, 1)) solid var(--card-border);
|
||||
border-radius: calc(8px * var(--scale, 1));
|
||||
box-shadow: 0 calc(4px * var(--scale, 1)) calc(16px * var(--scale, 1)) rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 6px 10px;
|
||||
padding: 0.46em 0.77em;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
font-size: 11px;
|
||||
border-bottom: calc(1px * var(--scale, 1)) solid var(--card-border);
|
||||
font-size: 0.85em;
|
||||
cursor: move;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -83,6 +75,6 @@
|
||||
resize: none;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
font-size: 1em;
|
||||
padding: 0.77em;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useRef } from "react";
|
||||
import type { NoteCard } from "../types";
|
||||
import { useViewport } from "../viewport";
|
||||
|
||||
interface Props {
|
||||
card: NoteCard;
|
||||
onUpdate: (patch: Partial<NoteCard>) => void;
|
||||
}
|
||||
|
||||
const BODY_FONT_BASE = 13;
|
||||
|
||||
export function NoteCardView({ card, onUpdate }: Props) {
|
||||
const vp = useViewport();
|
||||
const dragState = useRef<{ startX: number; startY: number; cardX: number; cardY: number } | null>(
|
||||
null,
|
||||
);
|
||||
@@ -19,25 +23,43 @@ export function NoteCardView({ card, onUpdate }: Props) {
|
||||
};
|
||||
|
||||
const onHeaderPointerMove = (e: React.PointerEvent) => {
|
||||
if (!dragState.current) return;
|
||||
const worldEl = (e.currentTarget as HTMLElement).closest(".canvas-world") as HTMLElement;
|
||||
const scale = worldEl ? parseTransformScale(worldEl.style.transform) : 1;
|
||||
const dx = (e.clientX - dragState.current.startX) / scale;
|
||||
const dy = (e.clientY - dragState.current.startY) / scale;
|
||||
onUpdate({ x: dragState.current.cardX + dx, y: dragState.current.cardY + dy });
|
||||
const ds = dragState.current;
|
||||
if (!ds) return;
|
||||
const dx = (e.clientX - ds.startX) / vp.scale;
|
||||
const dy = (e.clientY - ds.startY) / vp.scale;
|
||||
onUpdate({ x: ds.cardX + dx, y: ds.cardY + dy });
|
||||
};
|
||||
|
||||
const onHeaderPointerUp = (e: React.PointerEvent) => {
|
||||
if (dragState.current) {
|
||||
(e.target as Element).releasePointerCapture(e.pointerId);
|
||||
try {
|
||||
(e.target as Element).releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
// already released
|
||||
}
|
||||
dragState.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const screenLeft = vp.x + card.x * vp.scale;
|
||||
const screenTop = vp.y + card.y * vp.scale;
|
||||
const screenW = card.width * vp.scale;
|
||||
const screenH = card.height * vp.scale;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card note-card"
|
||||
style={{ left: card.x, top: card.y, width: card.width, height: card.height, zIndex: card.z }}
|
||||
style={
|
||||
{
|
||||
left: screenLeft,
|
||||
top: screenTop,
|
||||
width: screenW,
|
||||
height: screenH,
|
||||
zIndex: card.z,
|
||||
fontSize: BODY_FONT_BASE * vp.scale,
|
||||
"--scale": vp.scale,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="card-header"
|
||||
@@ -58,8 +80,3 @@ export function NoteCardView({ card, onUpdate }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseTransformScale(transform: string): number {
|
||||
const m = transform.match(/scale\(([^)]+)\)/);
|
||||
return m ? parseFloat(m[1]) : 1;
|
||||
}
|
||||
|
||||
5
src/canvas/viewport.ts
Normal file
5
src/canvas/viewport.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import type { Viewport } from "./types";
|
||||
|
||||
export const ViewportContext = createContext<Viewport>({ x: 0, y: 0, scale: 1 });
|
||||
export const useViewport = () => useContext(ViewportContext);
|
||||
Reference in New Issue
Block a user