Serialize creature animator.

pull/233/head
Atul Varma 2021-12-31 09:22:47 -05:00
rodzic 19208970cd
commit dc7986aa9d
6 zmienionych plików z 138 dodań i 14 usunięć

Wyświetl plik

@ -75,10 +75,26 @@ const spinAnimator: CreatureAnimator = {
getChildAnimator: () => spinAnimator,
};
/**
* Names of all the animators.
*
* Note that this list should never be re-ordered, as the index of
* each animator corresponds to its animator id.
*/
export const CREATURE_ANIMATOR_NAMES = ["none", "breathe", "spin"] as const;
export type CreatureAnimatorName = typeof CREATURE_ANIMATOR_NAMES[number];
export function creatureAnimatorNameToId(name: CreatureAnimatorName): number {
return CREATURE_ANIMATOR_NAMES.indexOf(name);
}
export function creatureAnimatorIdToName(
id: number
): CreatureAnimatorName | undefined {
return CREATURE_ANIMATOR_NAMES[id];
}
export const CreatureAnimators: {
[k in CreatureAnimatorName]: CreatureAnimator;
} = {

Wyświetl plik

@ -256,11 +256,13 @@ function repeatUntilSymbolIsIncluded(
}
export type CreatureDesign = {
animatorName: CreatureAnimatorName;
compCtx: SvgCompositionContext;
creature: CreatureSymbol;
};
export const CREATURE_DESIGN_DEFAULTS: CreatureDesign = {
animatorName: "none",
compCtx: createSvgCompositionContext(),
creature: {
data: ROOT_SYMBOLS[0],
@ -279,7 +281,7 @@ const AnimationWidget: React.FC<AnimationWidgetProps> = (props) => {
const id = "animationName";
return (
<div className="flex-widget thingy">
<label htmlFor={id}>Animation (experimental):</label>
<label htmlFor={id}>Animation:</label>
<select
id={id}
onChange={(e) => props.onChange(e.target.value as CreatureAnimatorName)}
@ -299,11 +301,7 @@ export const CreaturePageWithDefaults: React.FC<
ComponentWithShareableStateProps<CreatureDesign>
> = ({ defaults, onChange }) => {
const svgRef = useRef<SVGSVGElement>(null);
const [animatorName, setAnimatorName] =
useRememberedState<CreatureAnimatorName>(
"creature-page:animatorName",
"none"
);
const [animatorName, setAnimatorName] = useState(defaults.animatorName);
const isAnimated = animatorName !== "none";
const [randomlyInvert, setRandomlyInvert] = useRememberedState(
"creature-page:randomlyInvert",
@ -339,10 +337,11 @@ export const CreaturePageWithDefaults: React.FC<
);
const design: CreatureDesign = useMemo(
() => ({
animatorName,
creature,
compCtx,
}),
[creature, compCtx]
[creature, compCtx, animatorName]
);
useDebouncedEffect(

Wyświetl plik

@ -2,6 +2,11 @@
"type": "record",
"name": "AvroCreatureDesign",
"fields": [
{
"name": "animatorId",
"type": "int",
"default": 0
},
{
"name": "compCtx",
"type": {

Wyświetl plik

@ -0,0 +1,60 @@
{
"type": "record",
"name": "AvroCreatureDesign",
"fields": [
{
"name": "compCtx",
"type": {
"name": "AvroSvgCompositionContext",
"type": "record",
"fields": [
{ "name": "stroke", "type": "int" },
{ "name": "fill", "type": "int" },
{ "name": "background", "type": "int" },
{ "name": "uniformStrokeWidth", "type": "float" },
{ "name": "disableGradients", "type": "boolean", "default": true }
]
}
},
{
"name": "creature",
"type": {
"name": "AvroCreatureSymbol",
"type": "record",
"fields": [
{ "name": "symbol", "type": "string" },
{ "name": "invertColors", "type": "boolean" },
{
"name": "attachments",
"type": {
"type": "array",
"items": {
"name": "AvroAttachedCreatureSymbol",
"type": "record",
"fields": [
{ "name": "base", "type": "AvroCreatureSymbol" },
{ "name": "attachTo", "type": "int" },
{ "name": "indices", "type": "bytes" }
]
}
}
},
{
"name": "nests",
"type": {
"type": "array",
"items": {
"name": "AvroNestedCreatureSymbol",
"type": "record",
"fields": [
{ "name": "base", "type": "AvroCreatureSymbol" },
{ "name": "indices", "type": "bytes" }
]
}
}
}
]
}
}
]
}

Wyświetl plik

@ -0,0 +1,23 @@
import {
serializeCreatureDesign,
deserializeCreatureDesign,
} from "./serialization";
import { CREATURE_DESIGN_DEFAULTS } from "./core";
describe("Mandala design serialization/desrialization", () => {
// Helper to make it easy for us to copy/paste from URLs.
const decodeAndDeserialize = (s: string) =>
deserializeCreatureDesign(decodeURIComponent(s));
it("deserializes from v2", () => {
const design = decodeAndDeserialize(
"v2.gIiJA%2BqfB4bA0wwAAIA%2FABpleWVfc3RhcmJ1cnN0AAQKY2xvY2sAAAAIBAABEGluZmluaXR5AQAAAgIAAAIUc3Blcm1fdGFpbAEAAAIAAA%3D%3D"
);
expect(design.animatorName).toBe("none");
});
it("works", () => {
const s = serializeCreatureDesign(CREATURE_DESIGN_DEFAULTS);
expect(deserializeCreatureDesign(s)).toEqual(CREATURE_DESIGN_DEFAULTS);
});
});

Wyświetl plik

@ -8,6 +8,7 @@ import {
} from "./creature-design.avsc";
import { fromBase64, toBase64 } from "../../base64";
import CreatureAvsc from "./creature-design.avsc.json";
import CreatureAvscV2 from "./creature-design.v2.avsc.json";
import { Packer, SvgCompositionContextPacker } from "../../serialization";
import {
AttachedCreatureSymbol,
@ -16,8 +17,12 @@ import {
} from "../../creature-symbol";
import { SvgVocabulary } from "../../svg-vocabulary";
import { ATTACHMENT_POINT_TYPES } from "../../specs";
import {
creatureAnimatorIdToName,
creatureAnimatorNameToId,
} from "../../creature-animator";
const LATEST_VERSION = "v2";
const LATEST_VERSION = "v3";
const avroCreatureDesign = avro.parse<AvroCreatureDesign>(CreatureAvsc);
@ -76,7 +81,7 @@ const AttachedCreatureSymbolPacker: Packer<
const CreatureSymbolPacker: Packer<CreatureSymbol, AvroCreatureSymbol> = {
pack: (value) => {
return {
...value,
invertColors: value.invertColors,
symbol: value.data.name,
attachments: value.attachments.map(AttachedCreatureSymbolPacker.pack),
nests: value.nests.map(NestedCreatureSymbolPacker.pack),
@ -84,7 +89,7 @@ const CreatureSymbolPacker: Packer<CreatureSymbol, AvroCreatureSymbol> = {
},
unpack: (value) => {
return {
...value,
invertColors: value.invertColors,
data: SvgVocabulary.get(value.symbol),
attachments: value.attachments.map(AttachedCreatureSymbolPacker.unpack),
nests: value.nests.map(NestedCreatureSymbolPacker.unpack),
@ -95,18 +100,37 @@ const CreatureSymbolPacker: Packer<CreatureSymbol, AvroCreatureSymbol> = {
const DesignConfigPacker: Packer<CreatureDesign, AvroCreatureDesign> = {
pack: (value) => {
return {
animatorId: creatureAnimatorNameToId(value.animatorName),
creature: CreatureSymbolPacker.pack(value.creature),
compCtx: SvgCompositionContextPacker.pack(value.compCtx),
};
},
unpack: (value) => {
return {
animatorName: creatureAnimatorIdToName(value.animatorId) ?? "none",
creature: CreatureSymbolPacker.unpack(value.creature),
compCtx: SvgCompositionContextPacker.unpack(value.compCtx),
};
},
};
function loadSchemaVersion(version: string, buf: Buffer): AvroCreatureDesign {
switch (version) {
case "v1":
throw new Error(`Sorry, we no longer support loading v1 creatures!`);
case "v2":
const res = avroCreatureDesign.createResolver(avro.parse(CreatureAvscV2));
return avroCreatureDesign.fromBuffer(buf, res);
case LATEST_VERSION:
return avroCreatureDesign.fromBuffer(buf);
default:
throw new Error(`Don't know how to load schema version ${version}`);
}
}
export function serializeCreatureDesign(value: CreatureDesign): string {
const buf = avroCreatureDesign.toBuffer(DesignConfigPacker.pack(value));
return `${LATEST_VERSION}.${toBase64(buf)}`;
@ -114,9 +138,6 @@ export function serializeCreatureDesign(value: CreatureDesign): string {
export function deserializeCreatureDesign(value: string): CreatureDesign {
const [version, serialized] = value.split(".", 2);
if (version === "v1") {
throw new Error(`Sorry, we no longer support loading v1 creatures!`);
}
const buf = fromBase64(serialized);
return DesignConfigPacker.unpack(avroCreatureDesign.fromBuffer(buf));
return DesignConfigPacker.unpack(loadSchemaVersion(version, buf));
}