feat: add configurable people maps
Add independent map profiles, offline city lookup, bundled world artwork and collision-free portrait placement with optional person details. Deploy with updated requirements, migrations 0029-0031 and collectstatic.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
.people-map{--map-ocean:#0c182a;--map-land:#7697bc;--map-accent:#54ded0;--map-ink:#edf5ff;background:var(--map-ocean);color:var(--map-ink);border:1px solid #7e9bb333;border-radius:24px;overflow:hidden;box-shadow:0 20px 60px #0002}
|
||||
.people-map--paper{--map-ocean:#f2f6f6;--map-land:#769a9a;--map-accent:#08786d;--map-ink:#153c42}
|
||||
.people-map--classic{--map-ocean:#000;--map-land:#8197c6;--map-accent:#24c6bb;--map-ink:#fff;border-radius:0}
|
||||
.people-map-heading{padding:36px 36px 12px}.people-map-heading h2{color:var(--map-ink);margin:10px 0;font-size:clamp(1.6rem,3vw,2.5rem)}.people-map-heading p{color:var(--map-ink);opacity:.75;max-width:65ch}.people-map-eyebrow{color:var(--map-accent);font-size:.7rem;letter-spacing:.16em;font-weight:700}
|
||||
.people-map-scroll{overflow-x:auto;padding:12px 0;scrollbar-color:var(--map-accent) var(--map-ocean)}.people-map-scroll:focus-visible{outline:2px solid var(--map-accent);outline-offset:-3px}
|
||||
.people-map-canvas{position:relative;min-width:900px;min-height:450px;background:radial-gradient(ellipse at 45% 40%,#5fa9bb15,transparent 65%)}
|
||||
.people-map-land{position:absolute;top:0;left:0;width:100%;height:auto;aspect-ratio:2/1;object-fit:fill;opacity:.7}.people-map--classic .people-map-land{opacity:1}.people-map-lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;color:var(--map-accent)}.people-map-lines line{stroke:currentColor;stroke-width:1;opacity:.55}.people-map-lines circle{fill:currentColor}
|
||||
.people-map-pin{visibility:hidden;position:absolute;width:112px;height:98px;display:flex;align-items:center;flex-direction:column;text-align:center;color:var(--map-ink)!important;text-decoration:none!important;font:600 11px/1.25 Inter,Arial,sans-serif;z-index:1}.people-map-pin img,.people-map-initial{width:56px;height:56px;flex-shrink:0;border-radius:50%;object-fit:cover;border:3px solid var(--map-accent);background:var(--map-ocean);box-shadow:0 4px 16px #0005}.people-map-initial{display:grid;place-items:center;font-size:24px}.people-map-label{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;max-width:112px;margin-top:5px;padding:2px 4px;border-radius:4px;background:var(--map-ocean)}.people-map-pin:focus-visible{outline:2px solid var(--map-accent);border-radius:8px}.people-map-pin:hover img{box-shadow:0 0 18px var(--map-accent)}
|
||||
.people-map-directory{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr));gap:1px;background:#8095aa33;border-top:1px solid #8095aa33}.people-map-person{background:var(--map-ocean);padding:22px;display:flex;gap:14px;scroll-margin-top:100px}.people-map-person:target{box-shadow:inset 0 0 0 2px var(--map-accent)}.people-map-person>img{width:48px;height:48px;border-radius:50%;object-fit:cover}.people-map-person h3{color:var(--map-ink);font-size:1rem;margin:0 0 6px}.people-map-person p{font-size:.85rem;margin:3px 0;overflow-wrap:anywhere}.people-map-person small{opacity:.65}.people-map-person a{color:var(--map-accent);font-size:.85rem}.people-map--classic .people-map-label{color:var(--map-accent)}
|
||||
|
||||
.people-map--paper .people-map-land{filter:saturate(.35);opacity:.6}
|
||||
|
||||
.people-map .people-map-land--preset{opacity:1;filter:none}
|
||||
|
||||
.people-map-heading,.people-map-person{overflow-wrap:anywhere}
|
||||
.people-map-person>div{min-width:0}
|
||||
.people-map-person>img{flex-shrink:0}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 195 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 77 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 77 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 76 KiB |
@@ -0,0 +1,62 @@
|
||||
/* Equirectangular coordinates; all layout measurements are in CSS pixels. */
|
||||
(() => {
|
||||
'use strict';
|
||||
function layout(canvas) {
|
||||
const pins = [...canvas.querySelectorAll('[data-lat]')];
|
||||
const width = canvas.clientWidth;
|
||||
const baseHeight = width / 2;
|
||||
const placed = [];
|
||||
const lines = canvas.querySelector('svg');
|
||||
lines.replaceChildren();
|
||||
// A fixed minimum width preserves readable labels on phones. The scroll
|
||||
// container exposes the entire map without shrinking portraits or hiding people.
|
||||
let height = baseHeight;
|
||||
pins.forEach(pin => {
|
||||
const anchor = {
|
||||
x: (Number(pin.dataset.lon) + 180) / 360 * width,
|
||||
y: (90 - Number(pin.dataset.lat)) / 180 * baseHeight,
|
||||
};
|
||||
const w = pin.offsetWidth, h = pin.offsetHeight;
|
||||
let point;
|
||||
// Deterministic expanding rings. Test complete label+portrait rectangles,
|
||||
// and grow the canvas for arbitrarily dense maps instead of accepting overlap.
|
||||
for (let ring = 0; !point; ring++) {
|
||||
const radius = ring * 12;
|
||||
const count = Math.max(1, Math.ceil(2 * Math.PI * radius / 12));
|
||||
for (let step = 0; step < count; step++) {
|
||||
const angle = step / count * 2 * Math.PI;
|
||||
const x = Math.max(w / 2 + 8, Math.min(width - w / 2 - 8, anchor.x + Math.cos(angle) * radius));
|
||||
const y = Math.max(h / 2 + 8, anchor.y + Math.sin(angle) * radius);
|
||||
if (!placed.some(p => Math.abs(p.x - x) < (p.w + w) / 2 + 8 && Math.abs(p.y - y) < (p.h + h) / 2 + 8)) {
|
||||
point = {x, y, w, h}; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
placed.push(point);
|
||||
height = Math.max(height, point.y + h / 2 + 12);
|
||||
pin.style.left = `${point.x - w / 2}px`;
|
||||
pin.style.top = `${point.y - h / 2}px`;
|
||||
pin.style.visibility = "visible";
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', anchor.x); line.setAttribute('y1', anchor.y);
|
||||
line.setAttribute('x2', point.x); line.setAttribute('y2', point.y - h / 2 + 28);
|
||||
lines.append(line);
|
||||
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
dot.setAttribute('cx', anchor.x); dot.setAttribute('cy', anchor.y); dot.setAttribute('r', 3);
|
||||
lines.append(dot);
|
||||
});
|
||||
canvas.style.height = `${height}px`;
|
||||
lines.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
}
|
||||
const maps = [...document.querySelectorAll('[data-people-map]')];
|
||||
for (const canvas of maps) {
|
||||
layout(canvas);
|
||||
let previousWidth = canvas.clientWidth;
|
||||
new ResizeObserver(() => {
|
||||
if (previousWidth !== canvas.clientWidth) {
|
||||
previousWidth = canvas.clientWidth; layout(canvas);
|
||||
}
|
||||
}).observe(canvas);
|
||||
}
|
||||
document.fonts.ready.then(() => maps.forEach(layout));
|
||||
})();
|
||||
Reference in New Issue
Block a user