import { SvgVocabulary } from "../../svg-vocabulary"; import { SvgCompositionContext } from "../../svg-composition-context"; import MandalaAvsc from "./mandala-design.avsc.json"; import type { AvroCircle, AvroMandalaDesign, AvroSvgCompositionContext, } from "./mandala-design.avsc"; import * as avro from "avro-js"; import { clampedByteToHex } from "../../random-colors"; import { MANDALA_DESIGN_DEFAULTS, ExtendedMandalaCircleParams, MandalaDesign, } from "./core"; import { fromBase64, toBase64 } from "../../base64"; const avroMandalaDesign = avro.parse(MandalaAvsc); /** * A generic interface for "packing" one type to a different representation * for the purposes of serialization, and "unpacking" the packed type * back to its original representation (for deserialization). */ interface Packer { pack(value: UnpackedType): PackedType; unpack(value: PackedType): UnpackedType; } const CirclePacker: Packer = { pack: ({ data, ...circle }) => ({ ...circle, symbol: data.name, }), unpack: ({ symbol, ...circle }) => ({ ...circle, data: SvgVocabulary.get(symbol), }), }; const SvgCompositionContextPacker: Packer< SvgCompositionContext, AvroSvgCompositionContext > = { pack: (ctx) => ({ ...ctx, fill: ColorPacker.pack(ctx.fill), stroke: ColorPacker.pack(ctx.stroke), background: ColorPacker.pack(ctx.background), uniformStrokeWidth: ctx.uniformStrokeWidth || 1, }), unpack: (ctx) => ({ ...ctx, fill: ColorPacker.unpack(ctx.fill), stroke: ColorPacker.unpack(ctx.stroke), background: ColorPacker.unpack(ctx.background), showSpecs: false, }), }; export const ColorPacker: Packer = { pack: (string) => { const red = parseInt(string.substring(1, 3), 16); const green = parseInt(string.substring(3, 5), 16); const blue = parseInt(string.substring(5, 7), 16); return (red << 16) + (green << 8) + blue; }, unpack: (number) => { const red = (number >> 16) & 0xff; const green = (number >> 8) & 0xff; const blue = number & 0xff; return "#" + [red, green, blue].map(clampedByteToHex).join(""); }, }; const DesignConfigPacker: Packer = { pack: (value) => { const circles: AvroCircle[] = [CirclePacker.pack(value.circle1)]; if (value.useTwoCircles) { circles.push(CirclePacker.pack(value.circle2)); } return { ...value, circles, baseCompCtx: SvgCompositionContextPacker.pack(value.baseCompCtx), }; }, unpack: ({ circles, ...value }) => { if (circles.length === 0) { throw new Error(`Circles must have at least one item!`); } const useTwoCircles = circles.length > 1; const circle1 = CirclePacker.unpack(circles[0]); const circle2 = useTwoCircles ? CirclePacker.unpack(circles[1]) : MANDALA_DESIGN_DEFAULTS.circle2; return { ...value, baseCompCtx: SvgCompositionContextPacker.unpack(value.baseCompCtx), circle1, circle2, useTwoCircles, }; }, }; export function serializeMandalaDesign(value: MandalaDesign): string { const buf = avroMandalaDesign.toBuffer(DesignConfigPacker.pack(value)); return toBase64(buf); } export function deserializeMandalaDesign(value: string): MandalaDesign { const buf = fromBase64(value); return DesignConfigPacker.unpack(avroMandalaDesign.fromBuffer(buf)); }