@flighthq/particles-formats 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detect.d.ts +18 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +75 -0
- package/dist/detect.js.map +1 -0
- package/dist/formatRegistry.d.ts +60 -0
- package/dist/formatRegistry.d.ts.map +1 -0
- package/dist/formatRegistry.js +75 -0
- package/dist/formatRegistry.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/libgdxParse.d.ts +27 -0
- package/dist/libgdxParse.d.ts.map +1 -0
- package/dist/libgdxParse.js +316 -0
- package/dist/libgdxParse.js.map +1 -0
- package/dist/libgdxSchema.d.ts +68 -0
- package/dist/libgdxSchema.d.ts.map +1 -0
- package/dist/libgdxSchema.js +12 -0
- package/dist/libgdxSchema.js.map +1 -0
- package/dist/libgdxSerialize.d.ts +17 -0
- package/dist/libgdxSerialize.d.ts.map +1 -0
- package/dist/libgdxSerialize.js +295 -0
- package/dist/libgdxSerialize.js.map +1 -0
- package/dist/parseParticleConfig.d.ts +34 -0
- package/dist/parseParticleConfig.d.ts.map +1 -0
- package/dist/parseParticleConfig.js +96 -0
- package/dist/parseParticleConfig.js.map +1 -0
- package/dist/particleDesignerParse.d.ts +23 -0
- package/dist/particleDesignerParse.d.ts.map +1 -0
- package/dist/particleDesignerParse.js +214 -0
- package/dist/particleDesignerParse.js.map +1 -0
- package/dist/particleDesignerSchema.d.ts +51 -0
- package/dist/particleDesignerSchema.d.ts.map +1 -0
- package/dist/particleDesignerSchema.js +5 -0
- package/dist/particleDesignerSchema.js.map +1 -0
- package/dist/particleDesignerSerialize.d.ts +22 -0
- package/dist/particleDesignerSerialize.d.ts.map +1 -0
- package/dist/particleDesignerSerialize.js +209 -0
- package/dist/particleDesignerSerialize.js.map +1 -0
- package/dist/pixiParse.d.ts +18 -0
- package/dist/pixiParse.d.ts.map +1 -0
- package/dist/pixiParse.js +173 -0
- package/dist/pixiParse.js.map +1 -0
- package/dist/serializeResult.d.ts +15 -0
- package/dist/serializeResult.d.ts.map +1 -0
- package/dist/serializeResult.js +2 -0
- package/dist/serializeResult.js.map +1 -0
- package/dist/spineParse.d.ts +18 -0
- package/dist/spineParse.d.ts.map +1 -0
- package/dist/spineParse.js +263 -0
- package/dist/spineParse.js.map +1 -0
- package/dist/spineSchema.d.ts +41 -0
- package/dist/spineSchema.d.ts.map +1 -0
- package/dist/spineSchema.js +8 -0
- package/dist/spineSchema.js.map +1 -0
- package/dist/spineSerialize.d.ts +16 -0
- package/dist/spineSerialize.d.ts.map +1 -0
- package/dist/spineSerialize.js +108 -0
- package/dist/spineSerialize.js.map +1 -0
- package/dist/starlingPexParse.d.ts +29 -0
- package/dist/starlingPexParse.d.ts.map +1 -0
- package/dist/starlingPexParse.js +254 -0
- package/dist/starlingPexParse.js.map +1 -0
- package/dist/starlingPexSchema.d.ts +49 -0
- package/dist/starlingPexSchema.d.ts.map +1 -0
- package/dist/starlingPexSchema.js +15 -0
- package/dist/starlingPexSchema.js.map +1 -0
- package/dist/starlingPexSerialize.d.ts +20 -0
- package/dist/starlingPexSerialize.d.ts.map +1 -0
- package/dist/starlingPexSerialize.js +184 -0
- package/dist/starlingPexSerialize.js.map +1 -0
- package/dist/unityParse.d.ts +23 -0
- package/dist/unityParse.d.ts.map +1 -0
- package/dist/unityParse.js +347 -0
- package/dist/unityParse.js.map +1 -0
- package/dist/unitySchema.d.ts +109 -0
- package/dist/unitySchema.d.ts.map +1 -0
- package/dist/unitySchema.js +9 -0
- package/dist/unitySchema.js.map +1 -0
- package/dist/unitySerialize.d.ts +20 -0
- package/dist/unitySerialize.d.ts.map +1 -0
- package/dist/unitySerialize.js +148 -0
- package/dist/unitySerialize.js.map +1 -0
- package/package.json +38 -0
- package/src/detect.test.ts +99 -0
- package/src/formatRegistry.test.ts +118 -0
- package/src/libgdxParse.test.ts +309 -0
- package/src/libgdxSerialize.test.ts +279 -0
- package/src/parseParticleConfig.test.ts +447 -0
- package/src/particleDesignerParse.test.ts +279 -0
- package/src/particleDesignerSerialize.test.ts +113 -0
- package/src/pixiParse.test.ts +131 -0
- package/src/spineParse.test.ts +289 -0
- package/src/spineSerialize.test.ts +81 -0
- package/src/starlingPexParse.test.ts +154 -0
- package/src/starlingPexSerialize.test.ts +115 -0
- package/src/unityParse.test.ts +328 -0
- package/src/unitySerialize.test.ts +77 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type UnityParticleShapeType = 'Sphere' | 'Hemisphere' | 'Cone' | 'Box' | 'Circle' | 'Edge' | 'Rectangle' | 'Donut';
|
|
2
|
+
export interface UnityColor {
|
|
3
|
+
r: number;
|
|
4
|
+
g: number;
|
|
5
|
+
b: number;
|
|
6
|
+
a: number;
|
|
7
|
+
}
|
|
8
|
+
/** A two-value range used for min/max curves with constant mode. */
|
|
9
|
+
export interface UnityMinMaxValue {
|
|
10
|
+
mode: 'constant' | 'twoConstants' | 'curve' | 'twoCurves';
|
|
11
|
+
constant?: number;
|
|
12
|
+
constantMin?: number;
|
|
13
|
+
constantMax?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface UnityBurst {
|
|
16
|
+
time: number;
|
|
17
|
+
count: number;
|
|
18
|
+
cycleCount: number;
|
|
19
|
+
repeatInterval: number;
|
|
20
|
+
}
|
|
21
|
+
export interface UnityEmission {
|
|
22
|
+
rateOverTime: UnityMinMaxValue;
|
|
23
|
+
bursts: UnityBurst[];
|
|
24
|
+
}
|
|
25
|
+
export interface UnityShape {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
shapeType: UnityParticleShapeType;
|
|
28
|
+
/** Sphere / Hemisphere / Circle / Donut outer radius */
|
|
29
|
+
radius: number;
|
|
30
|
+
/** Cone angle (degrees) */
|
|
31
|
+
angle: number;
|
|
32
|
+
/** Box / Rectangle dimensions */
|
|
33
|
+
scale: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
z: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** A Unity gradient key (RGB stop at a normalised lifetime position). */
|
|
40
|
+
export interface UnityGradientColorKey {
|
|
41
|
+
time: number;
|
|
42
|
+
color: {
|
|
43
|
+
r: number;
|
|
44
|
+
g: number;
|
|
45
|
+
b: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** A Unity gradient alpha key. */
|
|
49
|
+
export interface UnityGradientAlphaKey {
|
|
50
|
+
time: number;
|
|
51
|
+
alpha: number;
|
|
52
|
+
}
|
|
53
|
+
/** Multi-stop gradient (full MinMaxGradient fidelity). */
|
|
54
|
+
export interface UnityGradient {
|
|
55
|
+
colorKeys: UnityGradientColorKey[];
|
|
56
|
+
alphaKeys: UnityGradientAlphaKey[];
|
|
57
|
+
}
|
|
58
|
+
/** A Unity AnimationCurve key. */
|
|
59
|
+
export interface UnityCurveKey {
|
|
60
|
+
time: number;
|
|
61
|
+
value: number;
|
|
62
|
+
}
|
|
63
|
+
/** Multi-key AnimationCurve. */
|
|
64
|
+
export interface UnityAnimationCurve {
|
|
65
|
+
keys: UnityCurveKey[];
|
|
66
|
+
}
|
|
67
|
+
export interface UnityColorOverLifetime {
|
|
68
|
+
enabled: boolean;
|
|
69
|
+
/** Color at start and end of lifetime */
|
|
70
|
+
colorStart: UnityColor;
|
|
71
|
+
colorEnd: UnityColor;
|
|
72
|
+
/** Full multi-stop gradient; when present it carries the complete color/alpha
|
|
73
|
+
* timeline that colorStart/colorEnd only approximate. */
|
|
74
|
+
gradient?: UnityGradient;
|
|
75
|
+
}
|
|
76
|
+
export interface UnitySizeOverLifetime {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
/** Size multiplier at start and end of lifetime */
|
|
79
|
+
sizeStart: number;
|
|
80
|
+
sizeEnd: number;
|
|
81
|
+
/** Full AnimationCurve; when present it carries the complete size-over-lifetime shape. */
|
|
82
|
+
curve?: UnityAnimationCurve;
|
|
83
|
+
}
|
|
84
|
+
export interface UnityRotationOverLifetime {
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
/** Angular velocity in degrees/sec */
|
|
87
|
+
angularVelocity: UnityMinMaxValue;
|
|
88
|
+
}
|
|
89
|
+
/** Full Unity Shuriken particle system JSON document. */
|
|
90
|
+
export interface UnityParticleDocument {
|
|
91
|
+
name: string;
|
|
92
|
+
duration: number;
|
|
93
|
+
looping: boolean;
|
|
94
|
+
prewarm: boolean;
|
|
95
|
+
maxParticles: number;
|
|
96
|
+
startLifetime: UnityMinMaxValue;
|
|
97
|
+
startSpeed: UnityMinMaxValue;
|
|
98
|
+
startSize: UnityMinMaxValue;
|
|
99
|
+
startRotation: UnityMinMaxValue;
|
|
100
|
+
startColor: UnityColor;
|
|
101
|
+
gravityModifier: number;
|
|
102
|
+
physicsGravity: number;
|
|
103
|
+
emission: UnityEmission;
|
|
104
|
+
shape: UnityShape;
|
|
105
|
+
colorOverLifetime: UnityColorOverLifetime;
|
|
106
|
+
sizeOverLifetime: UnitySizeOverLifetime;
|
|
107
|
+
rotationOverLifetime: UnityRotationOverLifetime;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=unitySchema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unitySchema.d.ts","sourceRoot":"","sources":["../src/unitySchema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Unity Shuriken particle system JSON schema.
|
|
2
|
+
// Based on the Unity Particle System component's serialized field names as exported
|
|
3
|
+
// by Unity's JsonUtility and common third-party particle-system exporters.
|
|
4
|
+
// Targets Unity 2021 LTS and later.
|
|
5
|
+
//
|
|
6
|
+
// Curves (MinMaxCurve) are simplified to constant or two-keyframe linear values.
|
|
7
|
+
// Gradients (MinMaxGradient) are simplified to start/end color stops.
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=unitySchema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unitySchema.js","sourceRoot":"","sources":["../src/unitySchema.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,oFAAoF;AACpF,2EAA2E;AAC3E,oCAAoC;AACpC,EAAE;AACF,iFAAiF;AACjF,sEAAsE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ParticleEmitterConfig } from '@flighthq/types';
|
|
2
|
+
import type { ParticleSerializeResult } from './serializeResult';
|
|
3
|
+
import type { UnityParticleDocument } from './unitySchema';
|
|
4
|
+
export interface UnitySerializeOptions {
|
|
5
|
+
/** Pixels-per-unit — reverses the conversion applied during parsing. Defaults to 100. */
|
|
6
|
+
pixelsPerUnit?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Serialise a ParticleEmitterConfig to a Unity Shuriken particle system JSON string.
|
|
9
|
+
*
|
|
10
|
+
* Pass the `document` returned by `parseUnityParticle` to preserve fields that
|
|
11
|
+
* don't round-trip through the config (name, duration, looping, prewarm, etc.). */
|
|
12
|
+
export declare function serializeUnityParticle(config: Readonly<ParticleEmitterConfig>, existing?: Partial<UnityParticleDocument>, options?: UnitySerializeOptions): string;
|
|
13
|
+
/** Serialise a ParticleEmitterConfig to a Unity Shuriken particle system JSON
|
|
14
|
+
* string, returning both the serialized text and any warnings for features the
|
|
15
|
+
* format cannot represent.
|
|
16
|
+
*
|
|
17
|
+
* Pass the `document` returned by `parseUnityParticleDocument` to preserve
|
|
18
|
+
* fields that don't round-trip through the config. */
|
|
19
|
+
export declare function serializeUnityParticleDocument(config: Readonly<ParticleEmitterConfig>, existing?: Partial<UnityParticleDocument>, options?: UnitySerializeOptions): ParticleSerializeResult;
|
|
20
|
+
//# sourceMappingURL=unitySerialize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unitySerialize.d.ts","sourceRoot":"","sources":["../src/unitySerialize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAKV,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,qBAAqB;IACpC,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA2HD;;;oFAGoF;AACpF,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,QAAQ,CAAC,qBAAqB,CAAC,EACvC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,EACzC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,MAAM,CAIR;AAED;;;;;uDAKuD;AACvD,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,QAAQ,CAAC,qBAAqB,CAAC,EACvC,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,EACzC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,uBAAuB,CAIzB"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { particleColorCurveToKeyframes, particleCurveToKeyframes } from '@flighthq/particles';
|
|
2
|
+
const RAD2DEG = 180 / Math.PI;
|
|
3
|
+
const DEFAULT_PPU = 100;
|
|
4
|
+
function color(r, g, b, a) {
|
|
5
|
+
return { r, g, b, a };
|
|
6
|
+
}
|
|
7
|
+
function constant(v) {
|
|
8
|
+
return { mode: 'constant', constant: v };
|
|
9
|
+
}
|
|
10
|
+
function twoConst(low, high) {
|
|
11
|
+
if (low === high)
|
|
12
|
+
return constant(low);
|
|
13
|
+
return { mode: 'twoConstants', constantMin: low, constantMax: high };
|
|
14
|
+
}
|
|
15
|
+
function configToDocument(config, existing, ppu) {
|
|
16
|
+
const rotSpeedDegLow = config.rotationSpeedMin * RAD2DEG;
|
|
17
|
+
const rotSpeedDegHigh = config.rotationSpeedMax * RAD2DEG;
|
|
18
|
+
const hasRotation = config.rotationSpeedMin !== 0 || config.rotationSpeedMax !== 0;
|
|
19
|
+
const hasBurst = config.burstCount > 0;
|
|
20
|
+
// Gravity: pixel/s² → Unity world units / s²
|
|
21
|
+
const gravWorldUnit = config.gravityY / ppu;
|
|
22
|
+
const physicsGravity = existing.physicsGravity ?? 9.81;
|
|
23
|
+
const gravModifier = physicsGravity !== 0 ? gravWorldUnit / physicsGravity : 0;
|
|
24
|
+
// Shape
|
|
25
|
+
let shapeType = 'Cone';
|
|
26
|
+
let radius = 0;
|
|
27
|
+
let angle = config.spread * RAD2DEG;
|
|
28
|
+
const scaleXY = 1;
|
|
29
|
+
if (config.emitterShape === 'circle') {
|
|
30
|
+
shapeType = 'Circle';
|
|
31
|
+
radius = config.emitterRadius / ppu;
|
|
32
|
+
angle = 0;
|
|
33
|
+
}
|
|
34
|
+
else if (config.emitterShape === 'rect') {
|
|
35
|
+
shapeType = 'Box';
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
name: existing.name ?? '',
|
|
39
|
+
duration: config.duration > 0 ? config.duration : (existing.duration ?? 5),
|
|
40
|
+
looping: config.loop,
|
|
41
|
+
prewarm: existing.prewarm ?? false,
|
|
42
|
+
maxParticles: config.maxParticles,
|
|
43
|
+
startLifetime: twoConst(config.lifetimeMin, config.lifetimeMax),
|
|
44
|
+
startSpeed: twoConst(config.speedMin / ppu, config.speedMax / ppu),
|
|
45
|
+
startSize: twoConst(config.scaleMin, config.scaleMax),
|
|
46
|
+
startRotation: constant(0),
|
|
47
|
+
startColor: color(config.colorStartR, config.colorStartG, config.colorStartB, config.alphaStart),
|
|
48
|
+
gravityModifier: gravModifier,
|
|
49
|
+
physicsGravity,
|
|
50
|
+
emission: {
|
|
51
|
+
rateOverTime: constant(config.spawnRate),
|
|
52
|
+
bursts: hasBurst
|
|
53
|
+
? [
|
|
54
|
+
{
|
|
55
|
+
time: 0,
|
|
56
|
+
count: config.burstCount,
|
|
57
|
+
cycleCount: config.burstInterval > 0 ? 0 : 1,
|
|
58
|
+
repeatInterval: config.burstInterval,
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
: [],
|
|
62
|
+
},
|
|
63
|
+
shape: {
|
|
64
|
+
enabled: config.emitterShape !== 'point',
|
|
65
|
+
shapeType,
|
|
66
|
+
radius,
|
|
67
|
+
angle,
|
|
68
|
+
scale: {
|
|
69
|
+
x: config.emitterShape === 'rect' ? config.emitterWidth / ppu : scaleXY,
|
|
70
|
+
y: config.emitterShape === 'rect' ? config.emitterHeight / ppu : scaleXY,
|
|
71
|
+
z: 1,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
colorOverLifetime: {
|
|
75
|
+
enabled: true,
|
|
76
|
+
colorStart: color(config.colorStartR, config.colorStartG, config.colorStartB, config.alphaStart),
|
|
77
|
+
colorEnd: color(config.colorEndR, config.colorEndG, config.colorEndB, config.alphaEnd),
|
|
78
|
+
// Emit a full gradient when the config carries baked color/alpha curves so
|
|
79
|
+
// the complete timeline round-trips back out (not just the endpoints).
|
|
80
|
+
...(config.colorCurve || config.alphaCurve ? { gradient: buildGradient(config) } : {}),
|
|
81
|
+
},
|
|
82
|
+
sizeOverLifetime: {
|
|
83
|
+
enabled: config.scaleEnd !== 1 || config.scaleCurve != null,
|
|
84
|
+
sizeStart: 1,
|
|
85
|
+
sizeEnd: config.scaleEnd,
|
|
86
|
+
...(config.scaleCurve ? { curve: buildSizeCurve(config.scaleCurve) } : {}),
|
|
87
|
+
},
|
|
88
|
+
rotationOverLifetime: {
|
|
89
|
+
enabled: hasRotation,
|
|
90
|
+
angularVelocity: hasRotation ? twoConst(rotSpeedDegLow, rotSpeedDegHigh) : constant(0),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function buildGradient(config) {
|
|
95
|
+
const colorKeys = config.colorCurve
|
|
96
|
+
? particleColorCurveToKeyframes(config.colorCurve).map((k) => ({ time: k.time, color: { r: k.r, g: k.g, b: k.b } }))
|
|
97
|
+
: [
|
|
98
|
+
{ time: 0, color: { r: config.colorStartR, g: config.colorStartG, b: config.colorStartB } },
|
|
99
|
+
{ time: 1, color: { r: config.colorEndR, g: config.colorEndG, b: config.colorEndB } },
|
|
100
|
+
];
|
|
101
|
+
const alphaKeys = config.alphaCurve
|
|
102
|
+
? particleCurveToKeyframes(config.alphaCurve).map((k) => ({ time: k.time, alpha: k.value }))
|
|
103
|
+
: [
|
|
104
|
+
{ time: 0, alpha: config.alphaStart },
|
|
105
|
+
{ time: 1, alpha: config.alphaEnd },
|
|
106
|
+
];
|
|
107
|
+
return { colorKeys, alphaKeys };
|
|
108
|
+
}
|
|
109
|
+
function buildSizeCurve(scaleCurve) {
|
|
110
|
+
return { keys: particleCurveToKeyframes(scaleCurve).map((k) => ({ time: k.time, value: k.value })) };
|
|
111
|
+
}
|
|
112
|
+
/** Serialise a ParticleEmitterConfig to a Unity Shuriken particle system JSON string.
|
|
113
|
+
*
|
|
114
|
+
* Pass the `document` returned by `parseUnityParticle` to preserve fields that
|
|
115
|
+
* don't round-trip through the config (name, duration, looping, prewarm, etc.). */
|
|
116
|
+
export function serializeUnityParticle(config, existing, options) {
|
|
117
|
+
const ppu = options?.pixelsPerUnit ?? DEFAULT_PPU;
|
|
118
|
+
const doc = configToDocument(config, existing ?? {}, ppu);
|
|
119
|
+
return JSON.stringify(doc, null, 2);
|
|
120
|
+
}
|
|
121
|
+
/** Serialise a ParticleEmitterConfig to a Unity Shuriken particle system JSON
|
|
122
|
+
* string, returning both the serialized text and any warnings for features the
|
|
123
|
+
* format cannot represent.
|
|
124
|
+
*
|
|
125
|
+
* Pass the `document` returned by `parseUnityParticleDocument` to preserve
|
|
126
|
+
* fields that don't round-trip through the config. */
|
|
127
|
+
export function serializeUnityParticleDocument(config, existing, options) {
|
|
128
|
+
const text = serializeUnityParticle(config, existing, options);
|
|
129
|
+
const warnings = collectUnitySerializeWarnings(config);
|
|
130
|
+
return { text, warnings };
|
|
131
|
+
}
|
|
132
|
+
function collectUnitySerializeWarnings(config) {
|
|
133
|
+
const warnings = [];
|
|
134
|
+
if (config.colorEndVarianceR !== 0 || config.colorEndVarianceG !== 0 || config.colorEndVarianceB !== 0) {
|
|
135
|
+
warnings.push('colorEndVariance is not supported by the Unity format and was ignored');
|
|
136
|
+
}
|
|
137
|
+
if (config.colorStartVarianceR !== 0 || config.colorStartVarianceG !== 0 || config.colorStartVarianceB !== 0) {
|
|
138
|
+
warnings.push('colorStartVariance is not supported by the Unity format and was ignored');
|
|
139
|
+
}
|
|
140
|
+
if (config.velocityInheritance !== 0) {
|
|
141
|
+
warnings.push('velocityInheritance is not supported by the Unity format and was ignored');
|
|
142
|
+
}
|
|
143
|
+
if (config.gravityX !== 0) {
|
|
144
|
+
warnings.push('gravityX (horizontal gravity) is not supported by the Unity format and was ignored');
|
|
145
|
+
}
|
|
146
|
+
return warnings;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=unitySerialize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unitySerialize.js","sourceRoot":"","sources":["../src/unitySerialize.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/particles-formats",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/particles": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "Import/export particle emitter configs from industry-standard formats (Particle Designer, Spine, Unity)",
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LibgdxParticleFormatKind,
|
|
3
|
+
ParticleDesignerFormatKind,
|
|
4
|
+
PixiParticleFormatKind,
|
|
5
|
+
SpineParticleFormatKind,
|
|
6
|
+
StarlingPexFormatKind,
|
|
7
|
+
UnityParticleFormatKind,
|
|
8
|
+
} from '@flighthq/types';
|
|
9
|
+
|
|
10
|
+
import { detectParticleFormat } from './detect';
|
|
11
|
+
|
|
12
|
+
const PLIST_SNIPPET = `<?xml version="1.0" encoding="utf-8"?>
|
|
13
|
+
<plist version="1.0">
|
|
14
|
+
<dict>
|
|
15
|
+
<key>maxParticles</key><integer>200</integer>
|
|
16
|
+
</dict>
|
|
17
|
+
</plist>`;
|
|
18
|
+
const SPINE_JSON = JSON.stringify({
|
|
19
|
+
name: 'spark',
|
|
20
|
+
continuous: true,
|
|
21
|
+
emission: { low: 10, high: 30 },
|
|
22
|
+
life: { low: 500, high: 1500 },
|
|
23
|
+
});
|
|
24
|
+
const UNITY_JSON = JSON.stringify({
|
|
25
|
+
name: 'smoke',
|
|
26
|
+
looping: true,
|
|
27
|
+
startLifetime: { mode: 'twoConstants', constantMin: 1.0, constantMax: 2.5 },
|
|
28
|
+
gravityModifier: 0.1,
|
|
29
|
+
});
|
|
30
|
+
const LIBGDX_SNIPPET = `Particle Effect
|
|
31
|
+
- Spark -
|
|
32
|
+
maxParticleCount: 50
|
|
33
|
+
additive: true
|
|
34
|
+
`;
|
|
35
|
+
const STARLING_PEX_SNIPPET = `<?xml version="1.0" encoding="utf-8"?>
|
|
36
|
+
<particleEmitterConfig>
|
|
37
|
+
<attribute name="maxParticles" value="200"/>
|
|
38
|
+
</particleEmitterConfig>`;
|
|
39
|
+
const PIXI_JSON = JSON.stringify({
|
|
40
|
+
pos: { x: 0, y: 0 },
|
|
41
|
+
alpha: { start: 1, end: 0 },
|
|
42
|
+
speed: { start: 100, end: 50 },
|
|
43
|
+
lifetime: { min: 0.5, max: 1.5 },
|
|
44
|
+
maxParticles: 500,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('detectParticleFormat', () => {
|
|
48
|
+
it('detects libGDX .p by "Particle Effect" first line', () => {
|
|
49
|
+
expect(detectParticleFormat(LIBGDX_SNIPPET)).toBe(LibgdxParticleFormatKind);
|
|
50
|
+
});
|
|
51
|
+
it('detects libGDX .p with leading whitespace before Particle Effect', () => {
|
|
52
|
+
expect(detectParticleFormat(' \nParticle Effect\n- Emitter -\n')).toBe(LibgdxParticleFormatKind);
|
|
53
|
+
});
|
|
54
|
+
it('detects Particle Designer plist by <plist tag', () => {
|
|
55
|
+
expect(detectParticleFormat(PLIST_SNIPPET)).toBe(ParticleDesignerFormatKind);
|
|
56
|
+
});
|
|
57
|
+
it('detects Particle Designer plist with leading whitespace', () => {
|
|
58
|
+
expect(detectParticleFormat(' \n' + PLIST_SNIPPET)).toBe(ParticleDesignerFormatKind);
|
|
59
|
+
});
|
|
60
|
+
it('detects Pixi particle JSON by pos + alpha.start/end fields', () => {
|
|
61
|
+
expect(detectParticleFormat(PIXI_JSON)).toBe(PixiParticleFormatKind);
|
|
62
|
+
});
|
|
63
|
+
it('detects Spine JSON by continuous boolean', () => {
|
|
64
|
+
expect(detectParticleFormat(SPINE_JSON)).toBe(SpineParticleFormatKind);
|
|
65
|
+
});
|
|
66
|
+
it('detects Spine JSON by emission range object', () => {
|
|
67
|
+
const json = JSON.stringify({ emission: { low: 10, high: 30 }, life: { low: 500, high: 1500 } });
|
|
68
|
+
expect(detectParticleFormat(json)).toBe(SpineParticleFormatKind);
|
|
69
|
+
});
|
|
70
|
+
it('detects Starling PEX by <particleEmitterConfig root element', () => {
|
|
71
|
+
expect(detectParticleFormat(STARLING_PEX_SNIPPET)).toBe(StarlingPexFormatKind);
|
|
72
|
+
});
|
|
73
|
+
it('detects Unity JSON by startLifetime mode field', () => {
|
|
74
|
+
expect(detectParticleFormat(UNITY_JSON)).toBe(UnityParticleFormatKind);
|
|
75
|
+
});
|
|
76
|
+
it('detects Unity JSON by gravityModifier', () => {
|
|
77
|
+
const json = JSON.stringify({ gravityModifier: 0.5 });
|
|
78
|
+
expect(detectParticleFormat(json)).toBe(UnityParticleFormatKind);
|
|
79
|
+
});
|
|
80
|
+
it('detects Unity JSON by looping + startLifetime', () => {
|
|
81
|
+
const json = JSON.stringify({ looping: true, startLifetime: { mode: 'constant', constant: 1 } });
|
|
82
|
+
expect(detectParticleFormat(json)).toBe(UnityParticleFormatKind);
|
|
83
|
+
});
|
|
84
|
+
it('returns null for garbage input', () => {
|
|
85
|
+
expect(detectParticleFormat('not a particle file at all')).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
it('returns null for empty string', () => {
|
|
88
|
+
expect(detectParticleFormat('')).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
it('returns null for valid JSON that matches no format', () => {
|
|
91
|
+
expect(detectParticleFormat(JSON.stringify({ foo: 'bar', baz: 42 }))).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
it('returns null for a JSON array', () => {
|
|
94
|
+
expect(detectParticleFormat(JSON.stringify([1, 2, 3]))).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it('returns null for XML that is not a plist or PEX', () => {
|
|
97
|
+
expect(detectParticleFormat('<svg width="100"><circle/></svg>')).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createParticleEmitterConfig } from '@flighthq/particles';
|
|
2
|
+
|
|
3
|
+
import type { ParticleFormatCodec } from './formatRegistry';
|
|
4
|
+
import {
|
|
5
|
+
detectRegisteredParticleFormat,
|
|
6
|
+
getParticleFormatCodec,
|
|
7
|
+
getRegisteredParticleFormats,
|
|
8
|
+
parseRegisteredParticleFormat,
|
|
9
|
+
registerParticleFormat,
|
|
10
|
+
unregisterParticleFormat,
|
|
11
|
+
} from './formatRegistry';
|
|
12
|
+
|
|
13
|
+
// A minimal test codec for use across all tests
|
|
14
|
+
const TEST_KIND = 'test.MyFormat';
|
|
15
|
+
const testCodec: ParticleFormatCodec = {
|
|
16
|
+
detect: (text) => text.startsWith('MY_FORMAT:'),
|
|
17
|
+
parseToConfig: (text) => createParticleEmitterConfig({ maxParticles: parseInt(text.slice(10), 10) || 50 }),
|
|
18
|
+
parseToDocument: (text) => ({
|
|
19
|
+
config: createParticleEmitterConfig({ maxParticles: parseInt(text.slice(10), 10) || 50 }),
|
|
20
|
+
warnings: [],
|
|
21
|
+
}),
|
|
22
|
+
serialize: () => ({ text: 'MY_FORMAT:50', warnings: [] }),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('detectRegisteredParticleFormat', () => {
|
|
26
|
+
it('returns the kind when a codec detects the input', () => {
|
|
27
|
+
registerParticleFormat(TEST_KIND, testCodec);
|
|
28
|
+
expect(detectRegisteredParticleFormat('MY_FORMAT:100')).toBe(TEST_KIND);
|
|
29
|
+
unregisterParticleFormat(TEST_KIND);
|
|
30
|
+
});
|
|
31
|
+
it('returns null when no codec matches', () => {
|
|
32
|
+
expect(detectRegisteredParticleFormat('totally unknown')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
it('ignores codecs that throw in detect()', () => {
|
|
35
|
+
const throwingCodec: ParticleFormatCodec = {
|
|
36
|
+
...testCodec,
|
|
37
|
+
detect: () => {
|
|
38
|
+
throw new Error('boom');
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
registerParticleFormat(TEST_KIND, throwingCodec);
|
|
42
|
+
expect(() => detectRegisteredParticleFormat('anything')).not.toThrow();
|
|
43
|
+
unregisterParticleFormat(TEST_KIND);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getParticleFormatCodec', () => {
|
|
48
|
+
it('returns null when no codec is registered for the kind', () => {
|
|
49
|
+
expect(getParticleFormatCodec('nope.Missing')).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
it('returns the registered codec', () => {
|
|
52
|
+
registerParticleFormat(TEST_KIND, testCodec);
|
|
53
|
+
expect(getParticleFormatCodec(TEST_KIND)).toBe(testCodec);
|
|
54
|
+
unregisterParticleFormat(TEST_KIND);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('getRegisteredParticleFormats', () => {
|
|
59
|
+
it('returns an array including newly registered kinds', () => {
|
|
60
|
+
registerParticleFormat(TEST_KIND, testCodec);
|
|
61
|
+
expect(getRegisteredParticleFormats()).toContain(TEST_KIND);
|
|
62
|
+
unregisterParticleFormat(TEST_KIND);
|
|
63
|
+
});
|
|
64
|
+
it('does not include unregistered kinds', () => {
|
|
65
|
+
registerParticleFormat(TEST_KIND, testCodec);
|
|
66
|
+
unregisterParticleFormat(TEST_KIND);
|
|
67
|
+
expect(getRegisteredParticleFormats()).not.toContain(TEST_KIND);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('parseRegisteredParticleFormat', () => {
|
|
72
|
+
it('parses using the registered codec and returns config and format', () => {
|
|
73
|
+
registerParticleFormat(TEST_KIND, testCodec);
|
|
74
|
+
const result = parseRegisteredParticleFormat('MY_FORMAT:75', TEST_KIND);
|
|
75
|
+
expect(result.format).toBe(TEST_KIND);
|
|
76
|
+
expect(result.config.maxParticles).toBe(75);
|
|
77
|
+
expect(result.warnings).toEqual([]);
|
|
78
|
+
unregisterParticleFormat(TEST_KIND);
|
|
79
|
+
});
|
|
80
|
+
it('returns a default config with unknown-format warning when no codec is registered', () => {
|
|
81
|
+
const result = parseRegisteredParticleFormat('whatever', 'nope.Missing');
|
|
82
|
+
expect(result.config).toBeDefined();
|
|
83
|
+
expect(result.warnings.some((w) => w.includes('unknown-format'))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('catches codec parse errors and returns them as parse-error warnings', () => {
|
|
86
|
+
const errCodec: ParticleFormatCodec = {
|
|
87
|
+
...testCodec,
|
|
88
|
+
parseToDocument: () => {
|
|
89
|
+
throw new Error('bad input');
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
registerParticleFormat(TEST_KIND, errCodec);
|
|
93
|
+
const result = parseRegisteredParticleFormat('MY_FORMAT:bad', TEST_KIND);
|
|
94
|
+
expect(result.warnings.some((w) => w.includes('parse-error'))).toBe(true);
|
|
95
|
+
unregisterParticleFormat(TEST_KIND);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('registerParticleFormat', () => {
|
|
100
|
+
it('is last-write-wins: second registration replaces the first', () => {
|
|
101
|
+
const codec1: ParticleFormatCodec = { ...testCodec, detect: () => true };
|
|
102
|
+
const codec2: ParticleFormatCodec = { ...testCodec, detect: () => false };
|
|
103
|
+
registerParticleFormat(TEST_KIND, codec1);
|
|
104
|
+
registerParticleFormat(TEST_KIND, codec2);
|
|
105
|
+
expect(getParticleFormatCodec(TEST_KIND)).toBe(codec2);
|
|
106
|
+
unregisterParticleFormat(TEST_KIND);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('unregisterParticleFormat', () => {
|
|
111
|
+
it('returns true when a codec is found and removed', () => {
|
|
112
|
+
registerParticleFormat(TEST_KIND, testCodec);
|
|
113
|
+
expect(unregisterParticleFormat(TEST_KIND)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
it('returns false when no codec was registered for the kind', () => {
|
|
116
|
+
expect(unregisterParticleFormat('nope.NotRegistered')).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|