skeuo aspect ratio

This commit is contained in:
2025-10-27 01:55:37 +01:00
parent b260065245
commit 84a3a9a5b5
3 changed files with 143 additions and 63 deletions
+1
View File
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { import {
DownloadIcon, DownloadIcon,
EditIcon, EditIcon,
-1
View File
@@ -55,7 +55,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
transition: width 0.28s ease, height 0.28s ease;
} }
.skeuo-item:focus-visible { .skeuo-item:focus-visible {
+119 -39
View File
@@ -60,9 +60,12 @@ const SkeuoPreviewCard = ({
prefetch, prefetch,
}); });
const { currentUrl, cardinality, canGoPrev, canGoNext } = navigator; const { currentUrl, cardinality, canGoPrev, canGoNext, currentMetadata, ordinal } = navigator;
const docId = doc?.id ?? null; const docId = doc?.id ?? null;
const metadataWidth = Number(currentMetadata?.width);
const metadataHeight = Number(currentMetadata?.height);
useEffect(() => { useEffect(() => {
if (!onNavigatorSnapshot || !docId) { if (!onNavigatorSnapshot || !docId) {
return undefined; return undefined;
@@ -74,6 +77,9 @@ const SkeuoPreviewCard = ({
canGoNext, canGoNext,
goPrev: navigator.goPrev, goPrev: navigator.goPrev,
goNext: navigator.goNext, goNext: navigator.goNext,
ordinal,
width: Number.isFinite(metadataWidth) && metadataWidth > 0 ? metadataWidth : null,
height: Number.isFinite(metadataHeight) && metadataHeight > 0 ? metadataHeight : null,
}; };
onNavigatorSnapshot(docId, snapshot); onNavigatorSnapshot(docId, snapshot);
return () => onNavigatorSnapshot(docId, null); return () => onNavigatorSnapshot(docId, null);
@@ -83,6 +89,9 @@ const SkeuoPreviewCard = ({
title, title,
canGoPrev, canGoPrev,
canGoNext, canGoNext,
ordinal,
metadataWidth,
metadataHeight,
navigator.goPrev, navigator.goPrev,
navigator.goNext, navigator.goNext,
onNavigatorSnapshot, onNavigatorSnapshot,
@@ -555,6 +564,37 @@ const clamp = (value, min, max) => {
return value; return value;
}; };
const clampCardDimensions = (width, height) => {
let nextWidth = Number(width);
let nextHeight = Number(height);
if (!Number.isFinite(nextWidth) || nextWidth <= 0 || !Number.isFinite(nextHeight) || nextHeight <= 0) {
return null;
}
const scaleDown = Math.min(1, CARD_MAX / nextWidth, CARD_MAX / nextHeight);
nextWidth *= scaleDown;
nextHeight *= scaleDown;
const minDim = Math.min(nextWidth, nextHeight);
if (minDim > 0 && minDim < CARD_MIN) {
const scaleUp = CARD_MIN / minDim;
nextWidth *= scaleUp;
nextHeight *= scaleUp;
const adjust = Math.min(1, CARD_MAX / nextWidth, CARD_MAX / nextHeight);
nextWidth *= adjust;
nextHeight *= adjust;
}
nextWidth = clamp(nextWidth, CARD_MIN, CARD_MAX);
nextHeight = clamp(nextHeight, CARD_MIN, CARD_MAX);
return {
width: nextWidth,
height: nextHeight,
};
};
const formatTransform = (x, y, rotation = 0, scale = 1) => const formatTransform = (x, y, rotation = 0, scale = 1) =>
`translate3d(${x}px, ${y}px, 0) rotate(${rotation}deg) scale(${scale})`; `translate3d(${x}px, ${y}px, 0) rotate(${rotation}deg) scale(${scale})`;
@@ -602,17 +642,54 @@ const SkeuomorphicWorkspace = ({
const [overlayOriginRect, setOverlayOriginRect] = useState(null); const [overlayOriginRect, setOverlayOriginRect] = useState(null);
const [overlayOriginTransform, setOverlayOriginTransform] = useState(null); const [overlayOriginTransform, setOverlayOriginTransform] = useState(null);
const [previewSnapshots, setPreviewSnapshots] = useState(() => new Map()); const [previewSnapshots, setPreviewSnapshots] = useState(() => new Map());
const [docSizeVersion, setDocSizeVersion] = useState(0);
const [tagDropTargetId, setTagDropTargetId] = useState(null); const [tagDropTargetId, setTagDropTargetId] = useState(null);
const [pendingTagDocId, setPendingTagDocId] = useState(null); const [pendingTagDocId, setPendingTagDocId] = useState(null);
const [pendingRemovalTag, setPendingRemovalTag] = useState(null); const [pendingRemovalTag, setPendingRemovalTag] = useState(null);
const draggingTagRef = useRef(null); const draggingTagRef = useRef(null);
const pendingDocTagDragRef = useRef(null); const pendingDocTagDragRef = useRef(null);
const docSizeMapRef = useRef(new Map()); const docSizeMapRef = useRef(new Map());
const documentLookupRef = useRef(new Map());
const removalCursorActiveRef = useRef(false); const removalCursorActiveRef = useRef(false);
const handleNavigatorSnapshot = useCallback((docId, snapshot) => { const applySnapshotDimensions = useCallback(
(docId, snapshot) => {
if (!docId || !snapshot) {
return;
}
const doc = documentLookupRef.current.get(docId);
if (!doc) {
return;
}
const width = Number(snapshot.width);
const height = Number(snapshot.height);
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
return;
}
const normalized = clampCardDimensions(width, height);
if (!normalized) {
return;
}
const key = resolveSizeKey(doc);
const existing = docSizeMapRef.current.get(key);
if (existing && existing.width === normalized.width && existing.height === normalized.height) {
return;
}
docSizeMapRef.current.set(key, normalized);
setDocSizeVersion((value) => value + 1);
},
[],
);
const handleNavigatorSnapshot = useCallback(
(docId, snapshot) => {
if (!docId) { if (!docId) {
return; return;
} }
setPreviewSnapshots((previous) => { setPreviewSnapshots((previous) => {
const prevSnapshot = previous.get(docId); const prevSnapshot = previous.get(docId);
if (!snapshot) { if (!snapshot) {
@@ -623,6 +700,7 @@ const SkeuomorphicWorkspace = ({
next.delete(docId); next.delete(docId);
return next; return next;
} }
const next = new Map(previous); const next = new Map(previous);
const sameSnapshot = const sameSnapshot =
prevSnapshot && prevSnapshot &&
@@ -631,14 +709,23 @@ const SkeuomorphicWorkspace = ({
prevSnapshot.canGoPrev === snapshot.canGoPrev && prevSnapshot.canGoPrev === snapshot.canGoPrev &&
prevSnapshot.canGoNext === snapshot.canGoNext && prevSnapshot.canGoNext === snapshot.canGoNext &&
prevSnapshot.goPrev === snapshot.goPrev && prevSnapshot.goPrev === snapshot.goPrev &&
prevSnapshot.goNext === snapshot.goNext; prevSnapshot.goNext === snapshot.goNext &&
prevSnapshot.ordinal === snapshot.ordinal &&
prevSnapshot.width === snapshot.width &&
prevSnapshot.height === snapshot.height;
if (sameSnapshot) { if (sameSnapshot) {
return previous; return previous;
} }
next.set(docId, snapshot); next.set(docId, snapshot);
return next; return next;
}); });
}, []);
if (snapshot) {
applySnapshotDimensions(docId, snapshot);
}
},
[applySnapshotDimensions],
);
const activeTagSet = useMemo(() => { const activeTagSet = useMemo(() => {
if (!Array.isArray(activeTagIds) || activeTagIds.length === 0) { if (!Array.isArray(activeTagIds) || activeTagIds.length === 0) {
return new Set(); return new Set();
@@ -663,6 +750,15 @@ const SkeuomorphicWorkspace = ({
const resolvePreviewDimensions = useCallback( const resolvePreviewDimensions = useCallback(
(doc) => { (doc) => {
if (!doc) return null; if (!doc) return null;
const snapshot = previewSnapshots.get(doc.id);
if (snapshot && snapshot.width && snapshot.height) {
return {
width: snapshot.width,
height: snapshot.height,
};
}
const asset = resolvePreviewAsset(doc); const asset = resolvePreviewAsset(doc);
const view = createAssetView(asset); const view = createAssetView(asset);
const metadata = view.getPrimaryMetadata() || {}; const metadata = view.getPrimaryMetadata() || {};
@@ -673,7 +769,7 @@ const SkeuomorphicWorkspace = ({
} }
return null; return null;
}, },
[resolvePreviewAsset], [previewSnapshots, resolvePreviewAsset],
); );
useEffect(() => { useEffect(() => {
@@ -780,48 +876,26 @@ const SkeuomorphicWorkspace = ({
if (cache) { if (cache) {
return cache; return cache;
} }
let width;
let height;
const intrinsic = resolvePreviewDimensions(doc); const intrinsic = resolvePreviewDimensions(doc);
let normalized = null;
if (intrinsic?.width && intrinsic?.height) { if (intrinsic?.width && intrinsic?.height) {
width = intrinsic.width; normalized = clampCardDimensions(intrinsic.width, intrinsic.height);
height = intrinsic.height; }
} else {
if (!normalized) {
const seed = seededRandom(`${key}:size`); const seed = seededRandom(`${key}:size`);
width = CARD_MIN + seed * (Math.min(CARD_MAX, CARD_MAX / EMPTY_CARD_ASPECT) - CARD_MIN); let width = CARD_MIN + seed * (Math.min(CARD_MAX, CARD_MAX / EMPTY_CARD_ASPECT) - CARD_MIN);
width = clamp(width, CARD_MIN, Math.min(CARD_MAX, CARD_MAX / EMPTY_CARD_ASPECT)); width = clamp(width, CARD_MIN, Math.min(CARD_MAX, CARD_MAX / EMPTY_CARD_ASPECT));
height = width * EMPTY_CARD_ASPECT; const height = width * EMPTY_CARD_ASPECT;
normalized = clampCardDimensions(width, height);
} }
if (!Number.isFinite(width) || width <= 0) { if (!normalized) {
width = CARD_MIN; normalized = { width: CARD_MIN, height: CARD_MIN };
}
if (!Number.isFinite(height) || height <= 0) {
height = CARD_MIN;
} }
const scaleDown = Math.min(1, CARD_MAX / width, CARD_MAX / height); docSizeMapRef.current.set(key, normalized);
width *= scaleDown; return normalized;
height *= scaleDown;
const minDim = Math.min(width, height);
if (minDim < CARD_MIN) {
const scaleUp = CARD_MIN / minDim;
width *= scaleUp;
height *= scaleUp;
const adjust = Math.min(1, CARD_MAX / width, CARD_MAX / height);
width *= adjust;
height *= adjust;
}
width = clamp(width, CARD_MIN, CARD_MAX);
height = clamp(height, CARD_MIN, CARD_MAX);
const size = { width, height };
docSizeMapRef.current.set(key, size);
return size;
}, [resolvePreviewDimensions]); }, [resolvePreviewDimensions]);
const documentLookup = useMemo(() => { const documentLookup = useMemo(() => {
@@ -834,8 +908,13 @@ const SkeuomorphicWorkspace = ({
return map; return map;
}, [items]); }, [items]);
useEffect(() => {
documentLookupRef.current = documentLookup;
}, [documentLookup]);
useEffect(() => { useEffect(() => {
docSizeMapRef.current = new Map(); docSizeMapRef.current = new Map();
setDocSizeVersion((value) => value + 1);
}, [items]); }, [items]);
const overlayDisplay = useMemo(() => { const overlayDisplay = useMemo(() => {
@@ -1020,6 +1099,7 @@ const SkeuomorphicWorkspace = ({
items, items,
canvasSize.width, canvasSize.width,
canvasSize.height, canvasSize.height,
docSizeVersion,
ensureDocumentSize, ensureDocumentSize,
syncLayoutSnapshot, syncLayoutSnapshot,
]); ]);