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, 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 const CREATURE_ANIMATOR_NAMES = ["none", "breathe", "spin"] as const;
export type CreatureAnimatorName = typeof CREATURE_ANIMATOR_NAMES[number]; 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: { export const CreatureAnimators: {
[k in CreatureAnimatorName]: CreatureAnimator; [k in CreatureAnimatorName]: CreatureAnimator;
} = { } = {

Wyświetl plik

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

Wyświetl plik

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