minimact-spatial
Spatial Computing for the Web
Query the viewport as a 2D database. Turn spatial regions into reactive data sources.
Overview
minimact-spatial transforms the browser viewport into a queryable 2D spatial database. Instead of tracking individual elements, track spatial regions of the viewport and query what's inside them.
Revolutionary Concept
Traditional: Track individual elements
const element = document.querySelector('#sidebar');
const rect = element.getBoundingClientRect();Spatial Computing: Track spatial regions as reactive data sources
const sidebar = useArea('#sidebar');
console.log(sidebar.elementsCount); // 5
console.log(sidebar.coverage); // 0.85 (85% covered)
console.log(sidebar.isEmpty); // falseInstallation
npm install minimact-spatialQuick Start
import { useArea } from 'minimact-spatial';
function Dashboard() {
// Define spatial areas
const header = useArea({ top: 0, height: 80 });
const main = useArea('#main-content');
const footer = useArea({ bottom: 0, height: 60 });
return (
<div>
<p>Header has {header.elementsCount} elements</p>
<p>Main content coverage: {(main.coverage * 100).toFixed(0)}%</p>
<p>Footer is {footer.isEmpty ? 'empty' : 'filled'}</p>
</div>
);
}Defining Areas
1. By Selector
useArea('#sidebar')
useArea('.card')2. By Keywords
useArea('viewport') // Current viewport
useArea('window') // Same as viewport
useArea('document') // Entire document3. By Bounds
useArea({
top: 0,
left: 0,
width: 250,
height: '100vh'
})4. By Element
const el = document.querySelector('#box');
useArea(el)Configuration Options
useArea(definition, {
elementFilter?: (el: Element) => boolean, // Filter tracked elements
minElementSize?: number, // Ignore small elements (px²)
trackScroll?: boolean, // Update on scroll (default: true)
trackResize?: boolean, // Update on resize (default: true)
trackMutations?: boolean, // Update on DOM changes
throttle?: number, // Throttle updates (ms)
debugLogging?: boolean // Enable debug logs
})AreaState Properties
Geometry
area.bounds // { top, left, width, height, right, bottom }
area.width // Width in pixels
area.height // Height in pixels
area.area // Total area (px²)
area.center // { x, y } center pointElement Queries
area.elementsFullyEnclosed // Element[] - fully inside
area.elementsPartiallyEnclosed // Element[] - partially overlapping
area.elementsAll // Element[] - all (fully + partial)
area.elementsCount // Total count
area.elementsFullyCount // Fully enclosed count
area.elementsPartiallyCount // Partially enclosed countCoverage Analysis
area.coverage // 0.0-1.0 (0% to 100%)
area.totalPixelsCovered // Pixels covered by elements
area.emptySpace // Empty pixels
area.emptyRatio // 0.0-1.0 (empty ratio)Element Statistics
area.elementDensity // Elements per 1000px²
area.averageElementSize // Average size (px²)
area.largestElement // Largest element in area
area.smallestElement // Smallest element in areaState Flags
area.isEmpty // No elements in area
area.isFull // High coverage (> 80%)
area.isSparse // Low density (< 5 elements per 1000px²)Spatial Methods
Intersection & Overlap
area.intersects(otherArea) // boolean - do they overlap?
area.intersectionRatio(otherArea) // 0.0-1.0 - how much overlap
area.intersectionArea(otherArea) // px² - overlap area
area.contains(element) // boolean - element fully inside?
area.overlaps(element) // boolean - element partially inside?Distance Calculations
area.distance(otherArea) // pixels (edge-to-edge)
area.centerDistance(otherArea) // pixels (center-to-center)Element Queries
area.getElementsByTag('div')
area.getElementsByClass('card')
area.querySelector('.active')
area.querySelectorAll('.item')Viewport Queries
area.isInViewport // Is area currently visible?
area.visibleRatio // 0.0-1.0 (how much is visible)
area.visiblePixels // Visible pixelsReal-World Examples
Example 1: Collision Detection
function DragAndDrop() {
const dropZone = useArea('#drop-zone');
const draggedItem = useArea('#dragged-item');
const isOverDropZone = dropZone.intersects(draggedItem);
return (
<div>
<div
id="drop-zone"
className={isOverDropZone ? 'highlight' : ''}
style={{
background: isOverDropZone ? '#90EE90' : '#f0f0f0'
}}
>
Drop Here
</div>
<div id="dragged-item" draggable>
Drag Me
</div>
{isOverDropZone && (
<p>Ready to drop!</p>
)}
</div>
);
}Example 2: Viewport Sections
function LazyLoader() {
const aboveFold = useArea({ top: 0, height: '100vh' });
const belowFold = useArea({ top: '100vh', height: '100vh' });
return (
<div>
<div className="analytics">
<p>Immediately visible: {aboveFold.elementsCount} elements</p>
<p>Coming into view: {belowFold.elementsPartiallyCount} elements</p>
</div>
{aboveFold.coverage < 0.5 && (
<div className="warning">
⚠️ Low content above fold! Add more engaging content.
</div>
)}
{aboveFold.isSparse && (
<div className="suggestion">
💡 Consider adding more elements to fill the space
</div>
)}
</div>
);
}Example 3: Heat Map Grid
function HeatMap() {
// Create 4x4 grid
const grid = Array.from({ length: 16 }, (_, i) => {
const row = Math.floor(i / 4);
const col = i % 4;
return useArea({
top: `${row * 25}%`,
left: `${col * 25}%`,
width: '25%',
height: '25%'
});
});
// Find hottest region
const hottest = grid.reduce((max, area) =>
area.coverage > max.coverage ? area : max
);
return (
<div className="heat-map">
{grid.map((area, i) => (
<div
key={i}
className="grid-cell"
style={{
background: `rgba(255, 0, 0, ${area.coverage})`,
border: area === hottest ? '3px solid red' : '1px solid #ccc'
}}
>
<span className="coverage">
{(area.coverage * 100).toFixed(0)}%
</span>
<span className="elements">
{area.elementsCount} elements
</span>
</div>
))}
<div className="hottest-indicator">
Hottest region: Cell {grid.indexOf(hottest) + 1}
</div>
</div>
);
}Example 4: Scroll Progress
function ScrollProgress() {
const article = useArea('#article');
const viewport = useArea('viewport');
const scrollProgress = article.intersectionRatio(viewport);
return (
<div className="scroll-indicator">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${scrollProgress * 100}%` }}
/>
</div>
<span className="progress-text">
{(scrollProgress * 100).toFixed(0)}% read
</span>
{scrollProgress >= 1.0 && (
<div className="completion-badge">
🎉 Article complete!
</div>
)}
</div>
);
}Example 5: Dynamic Layout Analysis
function LayoutAnalyzer() {
const header = useArea({ top: 0, height: 80 });
const sidebar = useArea({ left: 0, width: 250, top: 80, bottom: 60 });
const main = useArea({
left: 250,
right: 0,
top: 80,
bottom: 60
});
const footer = useArea({ bottom: 0, height: 60 });
return (
<div className="analyzer">
<h3>Layout Analysis</h3>
<ul>
<li>
Header: {header.elementsCount} elements, {(header.coverage * 100).toFixed(0)}% coverage
{header.isFull && <span className="badge">Full</span>}
</li>
<li>
Sidebar: {sidebar.elementsCount} elements, {sidebar.elementDensity.toFixed(1)} density
{sidebar.isSparse && <span className="badge warning">Sparse</span>}
</li>
<li>
Main: {main.elementsCount} elements
{main.isEmpty && <span className="badge error">EMPTY</span>}
</li>
<li>
Footer: {footer.elementsCount} elements
{footer.isFull ? (
<span className="badge warning">Crowded</span>
) : (
<span className="badge success">Spacious</span>
)}
</li>
</ul>
<div className="recommendations">
{header.coverage < 0.3 && (
<p>💡 Header has low coverage - consider adding logo or navigation</p>
)}
{main.isEmpty && (
<p>⚠️ Main content area is empty!</p>
)}
{sidebar.isSparse && (
<p>💡 Sidebar could use more widgets or navigation items</p>
)}
</div>
</div>
);
}Example 6: Responsive Regions
function ResponsiveContent() {
const contentArea = useArea('#content');
// Adapt based on spatial properties
const mode = contentArea.width < 600 ? 'compact'
: contentArea.width < 1200 ? 'normal'
: 'spacious';
return (
<div id="content" className={mode}>
<h2>Content (Mode: {mode})</h2>
{contentArea.isSparse && (
<div className="suggestion">
Lots of space available! Consider expanding content.
</div>
)}
{contentArea.isFull && (
<div className="warning">
Content is dense here. Consider pagination or collapsing.
</div>
)}
{mode === 'compact' && (
<MobileNavigation />
)}
{mode === 'spacious' && (
<ExpandedGallery />
)}
</div>
);
}Example 7: Canvas Quadrants
function CanvasAnalyzer() {
const canvas = useArea('#canvas');
const topLeft = useArea({
top: canvas.bounds.top,
left: canvas.bounds.left,
width: canvas.width / 2,
height: canvas.height / 2
});
const topRight = useArea({
top: canvas.bounds.top,
left: canvas.bounds.left + canvas.width / 2,
width: canvas.width / 2,
height: canvas.height / 2
});
const bottomLeft = useArea({
top: canvas.bounds.top + canvas.height / 2,
left: canvas.bounds.left,
width: canvas.width / 2,
height: canvas.height / 2
});
const bottomRight = useArea({
top: canvas.bounds.top + canvas.height / 2,
left: canvas.bounds.left + canvas.width / 2,
width: canvas.width / 2,
height: canvas.height / 2
});
return (
<div className="quadrant-analysis">
<h3>Canvas Quadrant Analysis</h3>
<div className="quadrants">
<div>Top-left: {topLeft.elementsCount} shapes</div>
<div>Top-right: {topRight.elementsCount} shapes</div>
<div>Bottom-left: {bottomLeft.elementsCount} shapes</div>
<div>Bottom-right: {bottomRight.elementsCount} shapes</div>
</div>
{topRight.elementsCount > topLeft.elementsCount * 2 && (
<div className="insight">
📊 User is focused on the right side!
</div>
)}
{bottomLeft.isEmpty && bottomRight.isEmpty && (
<div className="insight">
📊 Bottom half is empty - consider showing suggestions
</div>
)}
</div>
);
}Use Cases
- ✅ Collision detection - Drag & drop, game physics
- ✅ Heat mapping - Where users focus content
- ✅ Scroll analytics - Reading progress, engagement
- ✅ Layout optimization - Balance content distribution
- ✅ Viewport analysis - Above/below fold metrics
- ✅ Responsive design - Adapt based on spatial constraints
- ✅ Game development - Spatial queries for game objects
- ✅ Data visualization - Analyze element distribution
- ✅ A/B testing - Compare layout effectiveness
- ✅ Accessibility - Ensure balanced content placement
Performance
| Metric | Value |
|---|---|
| Bounds calculation | < 1ms |
| Element query (1000 elements) | 5-10ms |
| Coverage analysis | 2-5ms |
| Updates (throttled) | 100ms default |
| Memory per area | ~2KB |
Optimization
Highly optimized with:
- RequestAnimationFrame batching
- Throttled updates (configurable)
- Memoized calculations
- Efficient spatial algorithms
Optimization Tips:
// Use throttling for real-time tracking
useArea('#container', { throttle: 100 })
// Disable unnecessary tracking
useArea('#static', {
trackScroll: false,
trackResize: false
})
// Filter elements to reduce processing
useArea('viewport', {
elementFilter: (el) => el.classList.contains('tracked'),
minElementSize: 100 // Ignore tiny elements
})Integration with Minimact
minimact-spatial follows the standard Minimact extension pattern:
Client-Side
// Area state is reactive
const header = useArea({ top: 0, height: 80 });
// Automatically syncs to server
context.signalR.updateSpatialState(
componentId,
areaKey,
{
elementsCount: header.elementsCount,
coverage: header.coverage,
isEmpty: header.isEmpty
}
);Server-Side
// Server receives spatial state
protected override VNode Render()
{
var headerState = State["spatial_area_0"];
// Render based on spatial properties
return new VNode("div",
headerState.IsFull
? new VNode("button", "Collapse Header")
: new VNode("span", "Header has space")
);
}Philosophy
"The viewport isn't just a window. It's a queryable 2D database."
You're not building layouts. You're architecting spatial topologies. 📐
Traditional frameworks treat the viewport as a static container. minimact-spatial treats it as a dynamic, queryable spatial database where regions have properties, relationships, and behaviors.
Browser Support
- ✅ Chrome 90+
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ Edge 90+
Uses standard APIs:
getBoundingClientRect()IntersectionObserver(optional, for viewport tracking)ResizeObserver(optional, for resize tracking)
Next Steps
- minimact-punch (DOM State)
- minimact-query (SQL for DOM)
- minimact-quantum (DOM Entanglement)
- Core Hooks API
Part of the Minimact Quantum Stack 🌵📐✨
