Serialize creature animator. (#233)

In #232, cluster animations were added, but they weren't serialized as part of the cluster design, which meant that sharing them wouldn't copy over the animation.  This fixes that.

This also removes "(experimental)" from the animation widget label.
main
Atul Varma 2021-12-31 09:27:00 -05:00 zatwierdzone przez GitHub
rodzic 19208970cd
commit 415f902150
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
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));
}