@melonjs/spine-plugin 1.4.0 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melonjs/spine-plugin",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "melonJS Spine plugin",
5
5
  "homepage": "https://github.com/melonjs/spine-plugin#readme",
6
6
  "type": "module",
@@ -46,12 +46,12 @@
46
46
  "CHANGELOG.md"
47
47
  ],
48
48
  "peerDependencies": {
49
- "melonjs": "^15.10.0"
49
+ "melonjs": "^15.12.0"
50
50
  },
51
51
  "dependencies": {
52
- "@esotericsoftware/spine-canvas": "^4.2.19",
53
- "@esotericsoftware/spine-core": "^4.2.19",
54
- "@esotericsoftware/spine-webgl": "^4.2.19"
52
+ "@esotericsoftware/spine-canvas": "^4.2.23",
53
+ "@esotericsoftware/spine-core": "^4.2.23",
54
+ "@esotericsoftware/spine-webgl": "^4.2.23"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@babel/eslint-parser": "^7.22.15",
@@ -61,9 +61,9 @@
61
61
  "@rollup/plugin-node-resolve": "^15.2.1",
62
62
  "@rollup/plugin-replace": "^5.0.2",
63
63
  "del-cli": "^5.1.0",
64
- "eslint": "^8.48.0",
65
- "eslint-plugin-jsdoc": "^46.5.1",
66
- "rollup": "^3.28.1",
64
+ "eslint": "^8.50.0",
65
+ "eslint-plugin-jsdoc": "^46.8.2",
66
+ "rollup": "^3.29.2",
67
67
  "rollup-plugin-bundle-size": "^1.0.3",
68
68
  "typescript": "^5.2.2"
69
69
  },
@@ -1,4 +1,4 @@
1
- import { event, video } from "melonjs";
1
+ import { utils, loader } from "melonjs";
2
2
  import * as spineWebGL from "@esotericsoftware/spine-webgl";
3
3
  import * as spineCanvas from "@esotericsoftware/spine-canvas";
4
4
 
@@ -11,18 +11,46 @@ export default class AssetManager {
11
11
  pathPrefix;
12
12
 
13
13
  /**
14
+ * @param {CanvasRenderer|WebGLRenderer} renderer - a melonJS renderer instance
14
15
  * @param {string} [pathPrefix=""] - a default path prefix for assets location
15
16
  */
16
- constructor(pathPrefix = "") {
17
- event.once(event.VIDEO_INIT, () => {
18
- this.pathPrefix = pathPrefix;
19
- if (video.renderer.WebGLVersion >= 1) {
20
- this.asset_manager = new spineWebGL.AssetManager(video.renderer.getContext(), this.pathPrefix);
21
- } else {
22
- // canvas renderer
23
- this.asset_manager = new spineCanvas.AssetManager(this.pathPrefix);
17
+ constructor(renderer, pathPrefix = "") {
18
+ this.pathPrefix = pathPrefix;
19
+ if (renderer.WebGLVersion >= 1) {
20
+ this.asset_manager = new spineWebGL.AssetManager(renderer.getContext(), this.pathPrefix);
21
+ } else {
22
+ // canvas renderer
23
+ this.asset_manager = new spineCanvas.AssetManager(this.pathPrefix);
24
+ }
25
+
26
+ // set the spine custom parser
27
+ loader.setParser("spine", (data, onload, onerror) => {
28
+ // decompose data.src for the spine loader
29
+ const ext = utils.file.getExtension(data.src);
30
+ const basename = utils.file.getBasename(data.src);
31
+ const path = utils.file.getPath(data.src);
32
+ const filename = basename + "." + ext;
33
+
34
+ this.setPrefix(path);
35
+
36
+ // load asset
37
+ switch (ext) {
38
+ case "atlas":
39
+ this.loadTextureAtlas(filename, onload, onerror);
40
+ break;
41
+ case "json":
42
+ this.loadText(filename, onload, onerror);
43
+ break;
44
+ case "skel":
45
+ this.loadBinary(filename, onload, onerror);
46
+ break;
47
+ default:
48
+ throw "Spine plugin: unknown extension when preloading spine assets";
24
49
  }
50
+
51
+ return 1;
25
52
  });
53
+
26
54
  }
27
55
 
28
56
  /**
@@ -41,23 +69,48 @@ export default class AssetManager {
41
69
  * @param {string} atlas
42
70
  * @param {string} skel
43
71
  * @example
44
- * // load spine assets
72
+ * // "manually" load spine assets
45
73
  * Spine.assetManager.setPrefix("data/spine/");
46
74
  * Spine.assetManager.loadAsset("alien.atlas", "alien-ess.json");
47
75
  * await Spine.assetManager.loadAll();
48
76
  */
49
77
  loadAsset(atlas, skel) {
50
78
  if (atlas) {
51
- this.asset_manager.loadTextureAtlas(atlas);
79
+ this.loadTextureAtlas(atlas);
52
80
  }
53
81
 
54
82
  if (skel.endsWith(".skel")) {
55
- this.asset_manager.loadBinary(skel);
83
+ this.loadBinary(skel);
56
84
  } else {
57
- this.asset_manager.loadText(skel);
85
+ this.loadText(skel);
58
86
  }
59
87
  }
60
88
 
89
+ /**
90
+ * load the given texture atlas
91
+ * @param {string} atlas
92
+ */
93
+ loadTextureAtlas(atlas, onload, onerror) {
94
+ return this.asset_manager.loadTextureAtlas(atlas, onload, onerror);
95
+ }
96
+
97
+
98
+ /**
99
+ * load the given skeleton .skel file
100
+ * @param {string} skel
101
+ */
102
+ loadBinary(skel, onload, onerror) {
103
+ return this.asset_manager.loadBinary(skel, onload, onerror);
104
+ }
105
+
106
+ /**
107
+ * load the given skeleton binary file
108
+ * @param {string} skel
109
+ */
110
+ loadText(skel, onload, onerror) {
111
+ return this.asset_manager.loadText(skel, onload, onerror);
112
+ }
113
+
61
114
  /**
62
115
  * load all defined spine assets
63
116
  * @see loadAsset
@@ -65,4 +118,12 @@ export default class AssetManager {
65
118
  loadAll() {
66
119
  return this.asset_manager.loadAll();
67
120
  }
121
+
122
+ /**
123
+ * get the loaded skeleton data
124
+ * @param {string} path
125
+ */
126
+ require(path) {
127
+ return this.asset_manager.require(path);
128
+ }
68
129
  }
@@ -0,0 +1,24 @@
1
+ import { plugin } from "melonjs";
2
+ import { name, version, dependencies, homepage, peerDependencies } from "../package.json";
3
+ import AssetManager from "./AssetManager";
4
+
5
+ /**
6
+ * @classdesc
7
+ * a Spine 4.x plugin implementation for melonJS
8
+ * @augments plugin.BasePlugin
9
+ */
10
+ export class SpinePlugin extends plugin.BasePlugin {
11
+ constructor() {
12
+ // call the super constructor
13
+ super();
14
+
15
+ // minimum melonJS version expected to run this plugin
16
+ this.version = peerDependencies["melonjs"];
17
+
18
+ // hello world
19
+ console.log(`${name} ${version} - spine runtime ${dependencies["@esotericsoftware/spine-core"]} | ${homepage}`);
20
+
21
+ // instantiate the asset manager
22
+ this.assetManager = new AssetManager(this.app.renderer);
23
+ }
24
+ }
package/src/index.js CHANGED
@@ -1,67 +1,55 @@
1
- import { Math, Renderable, Vector2d, video, loader, utils, event } from "melonjs";
1
+ import { Math, Renderable, Vector2d, plugin } from "melonjs";
2
2
  import * as spineWebGL from "@esotericsoftware/spine-webgl";
3
3
  import * as spineCanvas from "@esotericsoftware/spine-canvas";
4
4
  import { Vector2 } from "@esotericsoftware/spine-core";
5
5
 
6
- import AssetManager from "./AssetManager.js";
7
6
  import SkeletonRenderer from "./SkeletonRenderer.js";
7
+ import { SpinePlugin } from "./SpinePlugin.js";
8
8
 
9
- import { name, version, dependencies, homepage } from "../package.json";
10
-
11
- export let assetManager = new AssetManager();
12
-
13
- // a custom Spine parser for melonJS preloader
14
- function spineParser(data, onload, onerror) {
15
-
16
- // decompose data.src for the spine loader
17
- const ext = utils.file.getExtension(data.src);
18
- const basename = utils.file.getBasename(data.src);
19
- const path = utils.file.getPath(data.src);
20
- const filename = basename + "." + ext;
21
-
22
- // set url prefix
23
- assetManager.setPrefix(path);
24
-
25
- // load asset
26
- switch (ext) {
27
- case "atlas":
28
- assetManager.asset_manager.loadTextureAtlas(filename, onload, onerror);
29
- break;
30
- case "json":
31
- assetManager.asset_manager.loadText(filename, onload, onerror);
32
- break;
33
- case "skel":
34
- assetManager.asset_manager.loadBinary(filename, onload, onerror);
35
- break;
36
- default:
37
- throw "Spine plugin: unknown extension when preloading spine assets";
38
- }
39
-
40
- return 1;
41
- }
9
+ export { SpinePlugin } from "./SpinePlugin.js";
42
10
 
43
- // set the spine custom parser
44
- loader.setParser("spine", spineParser);
45
-
46
- // hello world
47
- event.once(event.VIDEO_INIT, () => {
48
- console.log(`${name} ${version} - spine runtime ${dependencies["@esotericsoftware/spine-core"]} | ${homepage}`);
49
- });
11
+ // a temporary array used for skeleton.getBounds();
12
+ let tempArray = [];
50
13
 
51
14
  /**
52
15
  * @classdesc
53
- * An object to display a Spine animated skeleton on screen.
16
+ * An renderable object to render Spine animated skeleton.
54
17
  * @augments Renderable
55
18
  */
56
19
  export default class Spine extends Renderable {
57
20
  runtime;
58
21
  skeleton;
22
+ plugin;
23
+ renderer;
59
24
  animationState;
60
25
  skeletonRenderer;
61
- assetManager;
62
26
  root;
63
27
  boneOffset;
64
28
  boneSize;
29
+ isSpineFlipped = {
30
+ x : false,
31
+ y : false
32
+ };
33
+
34
+ /**
35
+ * Stores settings and other state for the playback of the current animation (if any).
36
+ * @type {TrackEntry}
37
+ * @see http://en.esotericsoftware.com/spine-api-reference#TrackEntry
38
+ * @see setAnimation
39
+ * @default undefined
40
+ * @example
41
+ * // set a default animation to "run"
42
+ * this.setAnimation(0, "run", true);
43
+ * ...
44
+ * ...
45
+ * // pause the animation
46
+ * this.currentTrack.timeScale = 0;
47
+ * ...
48
+ * ...
49
+ * // resume the animation
50
+ * this.currentTrack.timeScale = 1;
51
+ */
52
+ currentTrack;
65
53
 
66
54
  /**
67
55
  * @param {number} x - the x coordinates of the Spine object
@@ -100,13 +88,19 @@ export default class Spine extends Renderable {
100
88
  constructor(x, y, settings) {
101
89
  super(x, y, settings.width, settings.height);
102
90
 
103
- if (video.renderer.WebGLVersion >= 1) {
91
+ // ensure plugin was properly registered
92
+ this.plugin = plugin.get(SpinePlugin);
93
+ if (typeof this.plugin === "undefined") {
94
+ throw "Spine plugin: plugin needs to be registered first using plugin.register";
95
+ }
96
+ this.renderer = this.plugin.app.renderer;
97
+
98
+ if (this.renderer.WebGLVersion >= 1) {
104
99
  this.runtime = spineWebGL;
105
100
  } else {
106
101
  this.runtime = spineCanvas;
107
102
  }
108
103
 
109
- this.assetManager = assetManager.asset_manager;
110
104
  this.skeletonRenderer = new SkeletonRenderer(this.runtime);
111
105
 
112
106
  // force anchorPoint to 0,0
@@ -165,15 +159,15 @@ export default class Spine extends Renderable {
165
159
  */
166
160
  setSkeleton(atlasFile, jsonFile) {
167
161
  // Create the texture atlas and skeleton data.
168
- let atlas = this.assetManager.require(atlasFile);
162
+ let atlas = this.plugin.assetManager.require(atlasFile);
169
163
  let atlasLoader = new this.runtime.AtlasAttachmentLoader(atlas);
170
164
  let skeletonJson = new this.runtime.SkeletonJson(atlasLoader);
171
- let skeletonData = skeletonJson.readSkeletonData(this.assetManager.require(jsonFile));
165
+ let skeletonData = skeletonJson.readSkeletonData(this.plugin.assetManager.require(jsonFile));
172
166
 
173
167
  // Instantiate a new skeleton based on the atlas and skeleton data.
174
168
  this.skeleton = new this.runtime.Skeleton(skeletonData);
175
- this.skeleton.setToSetupPose();
176
- this.skeleton.updateWorldTransform();
169
+
170
+ this.setToSetupPose();
177
171
 
178
172
  // Setup an animation state with a default mix of 0.2 seconds.
179
173
  var animationStateData = new this.runtime.AnimationStateData(this.skeleton.data);
@@ -182,11 +176,34 @@ export default class Spine extends Renderable {
182
176
 
183
177
  // get a reference to the root bone
184
178
  this.root = this.skeleton.getRootBone();
185
- // Spine uses Y-up, melonJS uses Y-down
186
- this.root.scaleY *= -1;
179
+ }
187
180
 
188
- // mark the object as dirty
189
- this.isDirty = true;
181
+ /**
182
+ * flip the Spine skeleton on the horizontal axis (around its center)
183
+ * @param {boolean} [flip=true] - `true` to flip this Spine object.
184
+ * @returns {Spine} Reference to this object for method chaining
185
+ */
186
+ flipX(flip = true) {
187
+ if (this.isSpineFlipped.x !== flip) {
188
+ this.isSpineFlipped.x = flip;
189
+ this.root.scaleX *= -1;
190
+ this.isDirty = true;
191
+ }
192
+ return this;
193
+ }
194
+
195
+ /**
196
+ * flip the Spine skeleton on the vertical axis (around its center)
197
+ * @param {boolean} [flip=true] - `true` to flip this Spine object.
198
+ * @returns {Spine} Reference to this object for method chaining
199
+ */
200
+ flipY(flip = true) {
201
+ if (this.isSpineFlipped.y !== flip) {
202
+ this.isSpineFlipped.y = flip;
203
+ this.root.scaleY *= -1;
204
+ this.isDirty = true;
205
+ }
206
+ return this;
190
207
  }
191
208
 
192
209
  /**
@@ -235,13 +252,16 @@ export default class Spine extends Renderable {
235
252
  let boneOffset = this.boneOffset;
236
253
  let boneSize = this.boneSize;
237
254
 
238
- this.skeleton.getBounds(boneOffset, boneSize);
255
+ this.skeleton.getBounds(boneOffset, boneSize, tempArray);
256
+
257
+ let minX = boneOffset.x - rootBone.x,
258
+ minY = boneOffset.y - rootBone.y;
239
259
 
240
260
  bounds.addFrame(
241
- boneOffset.x - rootBone.x,
242
- boneOffset.y - rootBone.y,
243
- boneSize.x + boneOffset.x - rootBone.x,
244
- boneSize.y + boneOffset.y - rootBone.y,
261
+ minX,
262
+ minY,
263
+ minX + boneSize.x,
264
+ minY + boneSize.y,
245
265
  !isIdentity ? this.currentTransform : undefined
246
266
  );
247
267
  } else {
@@ -256,6 +276,7 @@ export default class Spine extends Renderable {
256
276
 
257
277
  if (absolute === true) {
258
278
  var absPos = this.getAbsolutePosition();
279
+ //bounds.translate(absPos.x, absPos.y);
259
280
  bounds.centerOn(absPos.x + bounds.centerX, absPos.y + bounds.centerY);
260
281
  }
261
282
  return bounds;
@@ -275,22 +296,24 @@ export default class Spine extends Renderable {
275
296
  update(dt) {
276
297
  if (typeof this.skeleton !== "undefined") {
277
298
  let rootBone = this.skeleton.getRootBone();
278
-
279
- // update the root bone position
280
- if (rootBone.x !== this.pos.x) {
281
- rootBone.x = this.pos.x;
282
- }
283
- if (rootBone.y !== this.pos.y) {
284
- rootBone.y = this.pos.y;
285
- }
299
+ //let height = this.renderer.getHeight();
286
300
 
287
301
  // Update and apply the animation state, update the skeleton's
288
- // world transforms and render the skeleton.
289
302
  this.animationState.update(dt / 1000);
290
303
  this.animationState.apply(this.skeleton);
304
+
305
+ // update the root bone position
306
+ rootBone.x = this.pos.x;
307
+ rootBone.y = this.pos.y;
308
+
309
+ // world transforms
291
310
  this.skeleton.updateWorldTransform();
292
311
 
312
+ // update Bounds
293
313
  this.updateBounds();
314
+
315
+ // world transforms
316
+ //this.skeleton.updateWorldTransform();
294
317
  }
295
318
  return true;
296
319
  }
@@ -310,13 +333,13 @@ export default class Spine extends Renderable {
310
333
  * @param {number} [track_index] - If the formerly current track entry was never applied to a skeleton, it is replaced (not mixed from). In either case trackEnd determines when the track is cleared.
311
334
  * @param {number} [index] - the animation index
312
335
  * @param {boolean} [loop= false] - If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its duration.
313
- * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept after the dispose event occurs.
336
+ * @returns {TrackEntry} A track entry to allow further customization of animation playback. References to the track entry must not be kept after the dispose event occurs.
314
337
  */
315
338
  setAnimationByIndex(track_index, index, loop = false) {
316
339
  if (index < 0 || index >= this.skeleton.data.animations.length) {
317
340
  return (console.log("Animation Index not found"));
318
341
  } else {
319
- this.animationState.setAnimation(track_index, this.skeleton.data.animations[index].name, loop);
342
+ return this.setAnimation(track_index, this.skeleton.data.animations[index].name, loop);
320
343
  }
321
344
  }
322
345
 
@@ -325,25 +348,40 @@ export default class Spine extends Renderable {
325
348
  * @param {number} [track_index] - If the formerly current track entry was never applied to a skeleton, it is replaced (not mixed from). In either case trackEnd determines when the track is cleared.
326
349
  * @param {string} [name] - the animation name
327
350
  * @param {boolean} [loop= false] - If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its duration.
328
- * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept after the dispose event occurs.
351
+ * @returns {TrackEntry} A track entry to allow further customization of animation playback. References to the track entry must not be kept after the dispose event occurs.
329
352
  * @example
330
353
  * // set the current animation
331
354
  * spineAlien.setAnimation(0, "death", true);
332
355
  */
333
356
  setAnimation(track_index, name, loop = false) {
334
- this.animationState.setAnimation(track_index, name, loop);
357
+ this.currentTrack = this.animationState.setAnimation(track_index, name, loop);
358
+ return this.currentTrack;
359
+ }
360
+
361
+ /**
362
+ * return true if the given animation name is the current running animation for the current track.
363
+ * @name isCurrentAnimation
364
+ * @param {string} name - animation name
365
+ * @returns {boolean}
366
+ * @example
367
+ * if (!this.isCurrentAnimation("death")) {
368
+ * // do something funny...
369
+ * }
370
+ */
371
+ isCurrentAnimation(name) {
372
+ return typeof this.currentTrack !== "undefined" && this.currentTrack.animation.name === name;
335
373
  }
336
374
 
337
375
  /**
338
376
  * Adds an animation to be played after the current or last queued animation for a track, and sets the track entry's mixDuration.
339
377
  * @param {number} [delay=0] - If > 0, sets delay. If <= 0, the delay set is the duration of the previous track entry minus any mix duration plus the specified `delay` (ie the mix ends at (`delay` = 0) or before (`delay` < 0) the previous track entry duration). If the previous entry is looping, its next loop completion is used instead of its duration.
340
- * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept after the dispose} event occurs.
378
+ * @return {TrackEntry} A track entry to allow further customization of animation playback. References to the track entry must not be kept after the dispose} event occurs.
341
379
  */
342
380
  addAnimationByIndex(track_index, index, loop = false, delay = 0) {
343
381
  if (index < 0 || index >= this.skeleton.data.animations.length) {
344
382
  return (console.log("Animation Index not found"));
345
383
  } else {
346
- this.animationState.addAnimation(track_index, this.skeleton.data.animations[index].name, loop, delay);
384
+ return this.addAnimation(track_index, this.skeleton.data.animations[index].name, loop, delay);
347
385
  }
348
386
  }
349
387
 
@@ -403,9 +441,19 @@ export default class Spine extends Renderable {
403
441
  }
404
442
 
405
443
  /**
406
- * Sets this slot to the setup pose.
444
+ * Reset this slot to the setup pose.
407
445
  */
408
446
  setToSetupPose() {
409
447
  this.skeleton.setToSetupPose();
448
+ // Spine uses Y-up, melonJS uses Y-down
449
+ this.skeleton.getRootBone().scaleY *= -1;
450
+ this.skeleton.updateWorldTransform();
451
+ // reset flip flags
452
+ this.isSpineFlipped.y = false;
453
+ this.isSpineFlipped.x = false;
454
+ // reset reference to current track entry
455
+ this.currentTrack = undefined;
456
+ // mark the object as dirty
457
+ this.isDirty = true;
410
458
  }
411
459
  }