mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 02:09:39 +02:00
Implement SpiralDB - a novel data storage format that encodes structured data into PNG images using an 8-color palette. Data grows in a spiral pattern from the center outward, enabling infinite expansion. Features: - 8-color palette (3-bit per pixel) for compression robustness - Spiral coordinate system with ring-based organization - Schema-based serialization (int, bool, string, timestamp, arrays) - Record management with CRUD operations and status tracking - PNG export/import with pako compression - Browser support (Canvas, Blob, DataURL) - Todo schema as reference implementation Storage structure: - Ring 0: Magic byte (validation) - Ring 1: Database header (version, flags, counts) - Ring 2: Schema definition - Ring 3+: Master index (spans multiple rings if needed) - Ring 4+: Record data
189 lines
5.8 KiB
TypeScript
189 lines
5.8 KiB
TypeScript
/**
|
|
* Spiral Coordinate System
|
|
* Converts between linear indices and 2D spiral coordinates
|
|
*/
|
|
|
|
import type { Point, RingInfo } from './types.js';
|
|
|
|
/**
|
|
* Calculate which ring a given pixel index belongs to
|
|
* Ring 0: index 0 (1 pixel)
|
|
* Ring 1: indices 1-8 (8 pixels)
|
|
* Ring 2: indices 9-24 (16 pixels)
|
|
* Ring n: starts at (2n-1)², has 8n pixels
|
|
*/
|
|
export function getRingForIndex(index: number): number {
|
|
if (index === 0) return 0;
|
|
// Ring n contains indices from (2n-1)² to (2n+1)² - 1
|
|
// Solving: (2n-1)² <= index gives n <= (sqrt(index) + 1) / 2
|
|
return Math.floor((Math.sqrt(index) + 1) / 2);
|
|
}
|
|
|
|
/**
|
|
* Get info about a specific ring
|
|
*/
|
|
export function getRingInfo(ring: number): RingInfo {
|
|
if (ring === 0) {
|
|
return { ring: 0, startIndex: 0, endIndex: 0, pixelCount: 1 };
|
|
}
|
|
const startIndex = (2 * ring - 1) ** 2;
|
|
const pixelCount = 8 * ring;
|
|
const endIndex = startIndex + pixelCount - 1;
|
|
return { ring, startIndex, endIndex, pixelCount };
|
|
}
|
|
|
|
/**
|
|
* Get the image size needed to contain a given ring
|
|
*/
|
|
export function getImageSizeForRing(ring: number): number {
|
|
return 2 * ring + 1;
|
|
}
|
|
|
|
/**
|
|
* Get the total number of pixels up to and including a ring
|
|
*/
|
|
export function getTotalPixelsForRing(ring: number): number {
|
|
return (2 * ring + 1) ** 2;
|
|
}
|
|
|
|
/**
|
|
* Convert a linear spiral index to 2D coordinates
|
|
* The spiral starts at center and goes: right → up → left → down → right...
|
|
*
|
|
* For a 5x5 image (center=2), ring 1 looks like:
|
|
* x: 0 1 2 3 4
|
|
* y:0 . 6 5 4 .
|
|
* y:1 7 . . . 3
|
|
* y:2 8 . 0 . 2
|
|
* y:3 9 . . . 1
|
|
* y:4 . A B C . (A=10, B=11, C=12 would be ring 2)
|
|
*/
|
|
export function spiralToXY(index: number, imageSize: number): Point {
|
|
const center = Math.floor(imageSize / 2);
|
|
|
|
if (index === 0) {
|
|
return { x: center, y: center };
|
|
}
|
|
|
|
// Find which ring this index belongs to
|
|
const ring = getRingForIndex(index);
|
|
const ringInfo = getRingInfo(ring);
|
|
const posInRing = index - ringInfo.startIndex;
|
|
|
|
// Each ring has 4 sides, each side has 2*ring pixels
|
|
const sideLength = 2 * ring;
|
|
const side = Math.floor(posInRing / sideLength);
|
|
const offset = posInRing % sideLength;
|
|
|
|
// Ring starts at (center+ring, center+ring-1) and goes:
|
|
// Side 0: up along right edge (y decreases)
|
|
// Side 1: left along top edge (x decreases)
|
|
// Side 2: down along left edge (y increases)
|
|
// Side 3: right along bottom edge (x increases)
|
|
switch (side) {
|
|
case 0: // Right side, going up: x=center+ring, y from center+ring-1 to center-ring
|
|
return { x: center + ring, y: center + ring - 1 - offset };
|
|
case 1: // Top side, going left: y=center-ring, x from center+ring-1 to center-ring
|
|
return { x: center + ring - 1 - offset, y: center - ring };
|
|
case 2: // Left side, going down: x=center-ring, y from center-ring+1 to center+ring
|
|
return { x: center - ring, y: center - ring + 1 + offset };
|
|
case 3: // Bottom side, going right: y=center+ring, x from center-ring+1 to center+ring-1
|
|
return { x: center - ring + 1 + offset, y: center + ring };
|
|
default:
|
|
throw new Error(
|
|
`Invalid side: ${side} for index ${index} (ring=${ring}, posInRing=${posInRing})`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert 2D coordinates to a linear spiral index
|
|
* Spiral pattern: center → right → up → left → down → right...
|
|
*/
|
|
export function xyToSpiral(x: number, y: number, imageSize: number): number {
|
|
const center = Math.floor(imageSize / 2);
|
|
const dx = x - center;
|
|
const dy = y - center;
|
|
|
|
// Ring 0 (center)
|
|
if (dx === 0 && dy === 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Determine which ring based on Chebyshev distance
|
|
const ring = Math.max(Math.abs(dx), Math.abs(dy));
|
|
const ringStart = (2 * ring - 1) ** 2;
|
|
const sideLength = 2 * ring;
|
|
|
|
// The spiral goes: right side up, top side left, left side down, bottom side right
|
|
// Side 0: x = ring, y from ring-1 down to -ring (right side, going up)
|
|
// Side 1: y = -ring, x from ring-1 down to -ring (top side, going left)
|
|
// Side 2: x = -ring, y from -ring+1 up to ring (left side, going down)
|
|
// Side 3: y = ring, x from -ring+1 up to ring-1 (bottom side, going right)
|
|
|
|
if (dx === ring && dy <= ring - 1 && dy >= -ring) {
|
|
// Right side (going up): y goes from ring-1 to -ring
|
|
const offset = ring - 1 - dy;
|
|
return ringStart + offset;
|
|
} else if (dy === -ring && dx <= ring - 1 && dx >= -ring) {
|
|
// Top side (going left): x goes from ring-1 to -ring
|
|
const offset = ring - 1 - dx;
|
|
return ringStart + sideLength + offset;
|
|
} else if (dx === -ring && dy >= -ring + 1 && dy <= ring) {
|
|
// Left side (going down): y goes from -ring+1 to ring
|
|
const offset = dy - (-ring + 1);
|
|
return ringStart + 2 * sideLength + offset;
|
|
} else {
|
|
// Bottom side (going right): x goes from -ring+1 to ring-1
|
|
const offset = dx - (-ring + 1);
|
|
return ringStart + 3 * sideLength + offset;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all pixel indices in a specific ring
|
|
*/
|
|
export function getRingPixels(ring: number): number[] {
|
|
const info = getRingInfo(ring);
|
|
const pixels: number[] = [];
|
|
for (let i = info.startIndex; i <= info.endIndex; i++) {
|
|
pixels.push(i);
|
|
}
|
|
return pixels;
|
|
}
|
|
|
|
/**
|
|
* Find the next available ring that can fit a record of given length
|
|
*/
|
|
export function findSpaceForRecord(
|
|
currentRing: number,
|
|
currentOffset: number,
|
|
recordLength: number
|
|
): { ring: number; offset: number; needsExpansion: boolean } {
|
|
let ring = currentRing;
|
|
let offset = currentOffset;
|
|
|
|
while (true) {
|
|
const ringInfo = getRingInfo(ring);
|
|
const availableInRing = ringInfo.pixelCount - offset;
|
|
|
|
if (availableInRing >= recordLength) {
|
|
return { ring, offset, needsExpansion: ring > currentRing };
|
|
}
|
|
|
|
// Move to next ring
|
|
ring++;
|
|
offset = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate coordinates for all pixels in a range
|
|
*/
|
|
export function getSpiralRange(startIndex: number, length: number, imageSize: number): Point[] {
|
|
const points: Point[] = [];
|
|
for (let i = 0; i < length; i++) {
|
|
points.push(spiralToXY(startIndex + i, imageSize));
|
|
}
|
|
return points;
|
|
}
|