mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add the crate dependency graph visualization to the contributor guide (#3907)
* Add the crate dependency graph visualization to the contributor guide * Code review fixes * And more
This commit is contained in:
181
website/static/js/page/contributor-guide/crate-hierarchy.js
Normal file
181
website/static/js/page/contributor-guide/crate-hierarchy.js
Normal file
@@ -0,0 +1,181 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const container = document.querySelector(".crate-hierarchy");
|
||||
if (!container) return;
|
||||
|
||||
const svg = container.querySelector("svg");
|
||||
if (!svg) return;
|
||||
|
||||
// Wrap SVG in a viewport container
|
||||
const viewport = document.createElement("div");
|
||||
viewport.className = "crate-hierarchy-viewport";
|
||||
svg?.parentNode?.insertBefore(viewport, svg);
|
||||
viewport.appendChild(svg);
|
||||
|
||||
// Remove any width/height attributes so CSS controls sizing
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
|
||||
// Create zoom controls
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "crate-hierarchy-controls";
|
||||
controls.innerHTML = `<button class="zoom-in"></button><button class="zoom-out"></button>`;
|
||||
container.insertBefore(controls, viewport);
|
||||
const zoomInBtn = controls.querySelector(".zoom-in");
|
||||
const zoomOutBtn = controls.querySelector(".zoom-out");
|
||||
if (!(zoomInBtn instanceof HTMLButtonElement) || !(zoomOutBtn instanceof HTMLButtonElement)) return;
|
||||
|
||||
// Lock the viewport height to the SVG's natural rendered height (ignoring any zoom transform)
|
||||
const updateViewportHeight = () => {
|
||||
const prevTransform = svg.style.transform;
|
||||
svg.style.transform = "";
|
||||
viewport.style.height = `${svg.getBoundingClientRect().height}px`;
|
||||
svg.style.transform = prevTransform;
|
||||
};
|
||||
updateViewportHeight();
|
||||
window.addEventListener("resize", () => {
|
||||
updateViewportHeight();
|
||||
applyTransform();
|
||||
});
|
||||
|
||||
const MIN_SCALE = 1;
|
||||
const MAX_SCALE = 4;
|
||||
const ZOOM_STEP = 0.15;
|
||||
const BUTTON_ZOOM_STEP = 0.5;
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
let scale = MIN_SCALE;
|
||||
let panX = 0;
|
||||
let panY = 0;
|
||||
let animationFrameId = 0;
|
||||
let isDragging = false;
|
||||
let dragStartX = 0;
|
||||
let dragStartY = 0;
|
||||
let panStartX = 0;
|
||||
let panStartY = 0;
|
||||
|
||||
function clampPan() {
|
||||
const viewportRect = viewport.getBoundingClientRect();
|
||||
const viewportW = viewportRect.width;
|
||||
const viewportH = viewportRect.height;
|
||||
|
||||
// The SVG is scaled to fill the viewport width at scale=1
|
||||
const scaledW = viewportW * scale;
|
||||
const scaledH = svg?.getBoundingClientRect()?.height || 0;
|
||||
|
||||
// How much overflow exists on each axis
|
||||
const overflowX = Math.max(0, scaledW - viewportW);
|
||||
const overflowY = Math.max(0, scaledH - viewportH);
|
||||
|
||||
// Pan is constrained so scaled content edges don't pull away from viewport edges
|
||||
panX = Math.min(0, Math.max(-overflowX, panX));
|
||||
panY = Math.min(0, Math.max(-overflowY, panY));
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
if (zoomInBtn instanceof HTMLButtonElement) zoomInBtn.disabled = scale >= MAX_SCALE;
|
||||
if (zoomOutBtn instanceof HTMLButtonElement) zoomOutBtn.disabled = scale <= MIN_SCALE;
|
||||
}
|
||||
|
||||
function applyTransform() {
|
||||
clampPan();
|
||||
if (svg) svg.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function zoomAt(/** @type {number} */ clientX, /** @type {number} */ clientY, /** @type {number} */ newScale) {
|
||||
const viewportRect = viewport.getBoundingClientRect();
|
||||
|
||||
// Point in viewport-local coordinates
|
||||
const pointX = clientX - viewportRect.left;
|
||||
const pointY = clientY - viewportRect.top;
|
||||
|
||||
// Where this point maps in the pre-zoom content
|
||||
const contentX = (pointX - panX) / scale;
|
||||
const contentY = (pointY - panY) / scale;
|
||||
|
||||
scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale));
|
||||
|
||||
// Adjust pan so the same content point stays under the cursor
|
||||
panX = pointX - contentX * scale;
|
||||
panY = pointY - contentY * scale;
|
||||
|
||||
applyTransform();
|
||||
}
|
||||
|
||||
function animateZoomAt(/** @type {number} */ clientX, /** @type {number} */ clientY, /** @type {number} */ newTargetScale) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
|
||||
const targetScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newTargetScale));
|
||||
const startScale = scale;
|
||||
const startPanX = panX;
|
||||
const startPanY = panY;
|
||||
|
||||
const viewportRect = viewport.getBoundingClientRect();
|
||||
const pointX = clientX - viewportRect.left;
|
||||
const pointY = clientY - viewportRect.top;
|
||||
const contentX = (pointX - panX) / scale;
|
||||
const contentY = (pointY - panY) / scale;
|
||||
|
||||
const targetPanX = pointX - contentX * targetScale;
|
||||
const targetPanY = pointY - contentY * targetScale;
|
||||
|
||||
const startTime = performance.now();
|
||||
const step = (/** @type {number} */ now) => {
|
||||
const t = Math.min(1, (now - startTime) / ANIMATION_DURATION);
|
||||
const ease = t * (2 - t); // ease-out quadratic
|
||||
scale = startScale + (targetScale - startScale) * ease;
|
||||
panX = startPanX + (targetPanX - startPanX) * ease;
|
||||
panY = startPanY + (targetPanY - startPanY) * ease;
|
||||
applyTransform();
|
||||
if (t < 1) animationFrameId = requestAnimationFrame(step);
|
||||
};
|
||||
animationFrameId = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
// Scroll wheel zoom
|
||||
viewport.addEventListener(
|
||||
"wheel",
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
|
||||
zoomAt(e.clientX, e.clientY, scale + delta);
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
// Button zoom (animated, zoom toward center of viewport)
|
||||
zoomInBtn?.addEventListener("click", () => {
|
||||
const rect = viewport.getBoundingClientRect();
|
||||
animateZoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, scale + BUTTON_ZOOM_STEP);
|
||||
});
|
||||
zoomOutBtn?.addEventListener("click", () => {
|
||||
const rect = viewport.getBoundingClientRect();
|
||||
animateZoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, scale - BUTTON_ZOOM_STEP);
|
||||
});
|
||||
|
||||
// Click-drag to pan
|
||||
viewport.addEventListener("pointerdown", (e) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
isDragging = true;
|
||||
dragStartX = e.clientX;
|
||||
dragStartY = e.clientY;
|
||||
panStartX = panX;
|
||||
panStartY = panY;
|
||||
viewport.setPointerCapture(e.pointerId);
|
||||
viewport.style.cursor = "grabbing";
|
||||
});
|
||||
window.addEventListener("pointermove", (e) => {
|
||||
if (!isDragging) return;
|
||||
panX = panStartX + (e.clientX - dragStartX);
|
||||
panY = panStartY + (e.clientY - dragStartY);
|
||||
applyTransform();
|
||||
});
|
||||
window.addEventListener("pointerup", () => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
viewport.style.cursor = "";
|
||||
});
|
||||
|
||||
applyTransform();
|
||||
});
|
||||
Reference in New Issue
Block a user