mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 08:19:40 +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
231 lines
6.1 KiB
TypeScript
231 lines
6.1 KiB
TypeScript
/**
|
|
* Image handling for SpiralDB
|
|
* Converts between SpiralImage and raw pixel data
|
|
*/
|
|
|
|
import type { ColorIndex, SpiralImage } from './types.js';
|
|
import { colorToRGB, rgbToColor } from './encoding.js';
|
|
import { spiralToXY, xyToSpiral, getImageSizeForRing } from './spiral.js';
|
|
|
|
/**
|
|
* Create an empty spiral image
|
|
*/
|
|
export function createImage(size: number): SpiralImage {
|
|
if (size % 2 === 0) {
|
|
throw new Error('Image size must be odd');
|
|
}
|
|
const pixels = new Uint8Array(size * size * 3);
|
|
// Initialize all pixels to black (0, 0, 0)
|
|
return { width: size, height: size, pixels };
|
|
}
|
|
|
|
/**
|
|
* Create an image that can hold a specific ring
|
|
*/
|
|
export function createImageForRing(ring: number): SpiralImage {
|
|
const size = getImageSizeForRing(ring);
|
|
return createImage(size);
|
|
}
|
|
|
|
/**
|
|
* Get a pixel's color at a specific spiral index
|
|
*/
|
|
export function getPixelByIndex(image: SpiralImage, index: number): ColorIndex {
|
|
const point = spiralToXY(index, image.width);
|
|
return getPixelByXY(image, point.x, point.y);
|
|
}
|
|
|
|
/**
|
|
* Set a pixel's color at a specific spiral index
|
|
*/
|
|
export function setPixelByIndex(image: SpiralImage, index: number, color: ColorIndex): void {
|
|
const point = spiralToXY(index, image.width);
|
|
setPixelByXY(image, point.x, point.y, color);
|
|
}
|
|
|
|
/**
|
|
* Get a pixel's color at XY coordinates
|
|
*/
|
|
export function getPixelByXY(image: SpiralImage, x: number, y: number): ColorIndex {
|
|
if (x < 0 || x >= image.width || y < 0 || y >= image.height) {
|
|
throw new Error(`Coordinates out of bounds: (${x}, ${y})`);
|
|
}
|
|
const offset = (y * image.width + x) * 3;
|
|
const r = image.pixels[offset];
|
|
const g = image.pixels[offset + 1];
|
|
const b = image.pixels[offset + 2];
|
|
return rgbToColor(r, g, b);
|
|
}
|
|
|
|
/**
|
|
* Set a pixel's color at XY coordinates
|
|
*/
|
|
export function setPixelByXY(image: SpiralImage, x: number, y: number, color: ColorIndex): void {
|
|
if (x < 0 || x >= image.width || y < 0 || y >= image.height) {
|
|
throw new Error(`Coordinates out of bounds: (${x}, ${y})`);
|
|
}
|
|
const rgb = colorToRGB(color);
|
|
const offset = (y * image.width + x) * 3;
|
|
image.pixels[offset] = rgb.r;
|
|
image.pixels[offset + 1] = rgb.g;
|
|
image.pixels[offset + 2] = rgb.b;
|
|
}
|
|
|
|
/**
|
|
* Read a range of pixels starting at a spiral index
|
|
*/
|
|
export function readPixelRange(
|
|
image: SpiralImage,
|
|
startIndex: number,
|
|
length: number
|
|
): ColorIndex[] {
|
|
const colors: ColorIndex[] = [];
|
|
for (let i = 0; i < length; i++) {
|
|
colors.push(getPixelByIndex(image, startIndex + i));
|
|
}
|
|
return colors;
|
|
}
|
|
|
|
/**
|
|
* Write a range of pixels starting at a spiral index
|
|
*/
|
|
export function writePixelRange(
|
|
image: SpiralImage,
|
|
startIndex: number,
|
|
colors: ColorIndex[]
|
|
): void {
|
|
for (let i = 0; i < colors.length; i++) {
|
|
setPixelByIndex(image, startIndex + i, colors[i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expand an image to accommodate more rings
|
|
* Centers the existing data in the new larger image
|
|
*/
|
|
export function expandImage(image: SpiralImage, newRing: number): SpiralImage {
|
|
const newSize = getImageSizeForRing(newRing);
|
|
if (newSize <= image.width) {
|
|
return image; // No expansion needed
|
|
}
|
|
|
|
const newImage = createImage(newSize);
|
|
|
|
// Copy existing pixels to center of new image
|
|
const offset = Math.floor((newSize - image.width) / 2);
|
|
|
|
for (let y = 0; y < image.height; y++) {
|
|
for (let x = 0; x < image.width; x++) {
|
|
const oldOffset = (y * image.width + x) * 3;
|
|
const newOffset = ((y + offset) * newSize + (x + offset)) * 3;
|
|
|
|
newImage.pixels[newOffset] = image.pixels[oldOffset];
|
|
newImage.pixels[newOffset + 1] = image.pixels[oldOffset + 1];
|
|
newImage.pixels[newOffset + 2] = image.pixels[oldOffset + 2];
|
|
}
|
|
}
|
|
|
|
return newImage;
|
|
}
|
|
|
|
/**
|
|
* Get the current ring count based on image size
|
|
*/
|
|
export function getMaxRingForImage(image: SpiralImage): number {
|
|
return Math.floor(image.width / 2);
|
|
}
|
|
|
|
/**
|
|
* Convert SpiralImage to raw RGBA buffer (for canvas/web)
|
|
*/
|
|
export function imageToRGBA(image: SpiralImage): Uint8Array {
|
|
const rgba = new Uint8Array(image.width * image.height * 4);
|
|
for (let i = 0; i < image.width * image.height; i++) {
|
|
rgba[i * 4] = image.pixels[i * 3]; // R
|
|
rgba[i * 4 + 1] = image.pixels[i * 3 + 1]; // G
|
|
rgba[i * 4 + 2] = image.pixels[i * 3 + 2]; // B
|
|
rgba[i * 4 + 3] = 255; // A (fully opaque)
|
|
}
|
|
return rgba;
|
|
}
|
|
|
|
/**
|
|
* Create SpiralImage from raw RGBA buffer
|
|
*/
|
|
export function rgbaToImage(rgba: Uint8Array, width: number, height: number): SpiralImage {
|
|
if (width !== height) {
|
|
throw new Error('Image must be square');
|
|
}
|
|
if (width % 2 === 0) {
|
|
throw new Error('Image size must be odd');
|
|
}
|
|
|
|
const pixels = new Uint8Array(width * height * 3);
|
|
for (let i = 0; i < width * height; i++) {
|
|
pixels[i * 3] = rgba[i * 4]; // R
|
|
pixels[i * 3 + 1] = rgba[i * 4 + 1]; // G
|
|
pixels[i * 3 + 2] = rgba[i * 4 + 2]; // B
|
|
// Ignore alpha
|
|
}
|
|
|
|
return { width, height, pixels };
|
|
}
|
|
|
|
/**
|
|
* Convert SpiralImage to a 2D array of color indices (for visualization)
|
|
*/
|
|
export function imageToColorGrid(image: SpiralImage): ColorIndex[][] {
|
|
const grid: ColorIndex[][] = [];
|
|
for (let y = 0; y < image.height; y++) {
|
|
const row: ColorIndex[] = [];
|
|
for (let x = 0; x < image.width; x++) {
|
|
row.push(getPixelByXY(image, x, y));
|
|
}
|
|
grid.push(row);
|
|
}
|
|
return grid;
|
|
}
|
|
|
|
/**
|
|
* Create a visual representation of the spiral order
|
|
* Returns a string showing the spiral indices
|
|
*/
|
|
export function visualizeSpiralOrder(size: number): string {
|
|
const grid: string[][] = [];
|
|
for (let y = 0; y < size; y++) {
|
|
const row: string[] = [];
|
|
for (let x = 0; x < size; x++) {
|
|
const index = xyToSpiral(x, y, size);
|
|
row.push(index.toString().padStart(3, ' '));
|
|
}
|
|
grid.push(row);
|
|
}
|
|
return grid.map((row) => row.join(' ')).join('\n');
|
|
}
|
|
|
|
/**
|
|
* Create a visual representation using emoji colors
|
|
*/
|
|
export function visualizeImageEmoji(image: SpiralImage): string {
|
|
const emoji: Record<ColorIndex, string> = {
|
|
0: '⬛',
|
|
1: '🟦',
|
|
2: '🟩',
|
|
3: '🔷',
|
|
4: '🟥',
|
|
5: '🟪',
|
|
6: '🟨',
|
|
7: '⬜',
|
|
};
|
|
|
|
const lines: string[] = [];
|
|
for (let y = 0; y < image.height; y++) {
|
|
let line = '';
|
|
for (let x = 0; x < image.width; x++) {
|
|
const color = getPixelByXY(image, x, y);
|
|
line += emoji[color];
|
|
}
|
|
lines.push(line);
|
|
}
|
|
return lines.join('\n');
|
|
}
|