Serialize creature animator.
rodzic
19208970cd
commit
dc7986aa9d
|
@ -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;
|
||||
} = {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
"type": "record",
|
||||
"name": "AvroCreatureDesign",
|
||||
"fields": [
|
||||
{
|
||||
"name": "animatorId",
|
||||
"type": "int",
|
||||
"default": 0
|
||||
},
|
||||
{
|
||||
"name": "compCtx",
|
||||
"type": {
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue