mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 17:45:25 -04:00
9934457b39
Only show in developer changelog
553 lines
19 KiB
JavaScript
553 lines
19 KiB
JavaScript
// @ts-check
|
||
/// <reference path="../JsExtensionTypes.d.ts" />
|
||
/**
|
||
* This is a declaration of an extension for GDevelop 5.
|
||
*
|
||
* ℹ️ Changes in this file are watched and automatically imported if the editor
|
||
* is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts).
|
||
*
|
||
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
|
||
* ⚠️ If you make a change and the extension is not loaded, open the developer console
|
||
* and search for any errors.
|
||
*
|
||
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
|
||
*/
|
||
|
||
/** @type {ExtensionModule} */
|
||
module.exports = {
|
||
createExtension: function (_, gd) {
|
||
const extension = new gd.PlatformExtension();
|
||
|
||
extension
|
||
.setExtensionInformation(
|
||
'SpineObject',
|
||
_('Spine (experimental)'),
|
||
_('Displays a Spine animation.'),
|
||
'Vladyslav Pohorielov',
|
||
'Open source (MIT License)'
|
||
)
|
||
.setShortDescription(
|
||
'Spine skeletal animation object. Play animations, mix, set skins, track events.'
|
||
)
|
||
.setDimension('2D')
|
||
.setExtensionHelpPath('/objects/spine')
|
||
.setCategory('Advanced');
|
||
|
||
extension
|
||
.addInstructionOrExpressionGroupMetadata(_('Spine'))
|
||
.setIcon('JsPlatform/Extensions/spine.svg');
|
||
|
||
const object = extension
|
||
.addObject(
|
||
'SpineObject',
|
||
_('Spine (experimental)'),
|
||
_(
|
||
'Display and smoothly animate a 2D object with skeletal animations made with Spine. Use files exported from Spine (json, atlas and image).'
|
||
),
|
||
'JsPlatform/Extensions/spine.svg',
|
||
new gd.SpineObjectConfiguration()
|
||
)
|
||
.addDefaultBehavior('EffectCapability::EffectBehavior')
|
||
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
|
||
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
|
||
.addDefaultBehavior('FlippableCapability::FlippableBehavior')
|
||
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
|
||
.addDefaultBehavior('AnimatableCapability::AnimatableBehavior')
|
||
.setIncludeFile('Extensions/Spine/spineruntimeobject.js')
|
||
.addIncludeFile('Extensions/Spine/spineruntimeobject-pixi-renderer.js')
|
||
.addIncludeFile('Extensions/Spine/pixi-spine/pixi-spine.js')
|
||
.addIncludeFile('Extensions/Spine/managers/pixi-spine-atlas-manager.js')
|
||
.addIncludeFile('Extensions/Spine/managers/pixi-spine-manager.js')
|
||
.setCategory('Advanced')
|
||
.setOpenFullEditorLabel(_('Edit animations'));
|
||
|
||
object
|
||
.addExpressionAndConditionAndAction(
|
||
'number',
|
||
'Animation',
|
||
_('Animation mixing duration'),
|
||
_(
|
||
'the duration of the smooth transition between 2 animations (in second)'
|
||
),
|
||
_('the animation mixing duration'),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('setAnimationMixingDuration')
|
||
.setGetter('getAnimationMixingDuration');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentX',
|
||
_('Point attachment X position'),
|
||
_('x position of spine point attachment'),
|
||
_('x position of spine _PARAM1_ point attachment for _PARAM2_ slot'),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentX');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentY',
|
||
_('Point attachment Y position'),
|
||
_('y position of spine point attachment'),
|
||
_('y position of spine _PARAM1_ point attachment for _PARAM2_ slot'),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentY');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentScaleXWorld',
|
||
_('Point attachment scale world X position'),
|
||
_('world x position of spine point attachment scale'),
|
||
_(
|
||
'world x position of spine _PARAM1_ point attachment for _PARAM2_ slot'
|
||
),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentScaleXWorld');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentScaleXLocal',
|
||
_('Point attachment scale local X position'),
|
||
_('local x position of spine point attachment scale'),
|
||
_(
|
||
'local x position of spine _PARAM1_ point attachment for _PARAM2_ slot'
|
||
),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentScaleXLocal');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentScaleYWorld',
|
||
_('Point attachment scale world Y position'),
|
||
_('world y position of spine point attachment scale'),
|
||
_(
|
||
'world y position of spine _PARAM1_ point attachment for _PARAM2_ slot'
|
||
),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentScaleYWorld');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentScaleYLocal',
|
||
_('Point attachment scale local Y position'),
|
||
_('local y position of spine point attachment scale'),
|
||
_(
|
||
'local y position of spine _PARAM1_ point attachment for _PARAM2_ slot'
|
||
),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentScaleYLocal');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentRotationWorld',
|
||
_('Point attachment world rotation'),
|
||
_('world rotation of spine point attachment'),
|
||
_(
|
||
'world rotation of spine _PARAM1_ point attachment for _PARAM2_ slot'
|
||
),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentRotationWorld');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'number',
|
||
'PointAttachmentRotationLocal',
|
||
_('Point attachment local rotation'),
|
||
_('local rotation of spine point attachment'),
|
||
_(
|
||
'local rotation of spine _PARAM1_ point attachment for _PARAM2_ slot'
|
||
),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('string', _('Attachment name'))
|
||
.addParameter('string', _('Slot name (use "" if names are the same)'))
|
||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||
.setFunctionName('getPointAttachmentRotationLocal');
|
||
|
||
object
|
||
.addExpressionAndCondition(
|
||
'string',
|
||
'getSkin',
|
||
_('Get skin name'),
|
||
_('the skin of the object'),
|
||
_('the skin'),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.useStandardParameters(
|
||
'string',
|
||
gd.ParameterOptions.makeNewOptions().setDescription(_('Skin name'))
|
||
)
|
||
.setFunctionName('getSkin');
|
||
|
||
object
|
||
.addAction(
|
||
'SetSkin',
|
||
_('Set skin'),
|
||
_('Set the skin of a Spine object.'),
|
||
_('Set the skin of _PARAM0_ to _PARAM1_'),
|
||
_('Animations and images'),
|
||
'JsPlatform/Extensions/spine.svg',
|
||
'JsPlatform/Extensions/spine.svg'
|
||
)
|
||
.addParameter('object', _('Spine'), 'SpineObject')
|
||
.addParameter('objectSkinName', _('Skin name'))
|
||
.setFunctionName('setSkin');
|
||
|
||
return extension;
|
||
},
|
||
|
||
/**
|
||
* You can optionally add sanity tests that will check the basic working
|
||
* of your extension behaviors/objects by instantiating behaviors/objects
|
||
* and setting the property to a given value.
|
||
*
|
||
* If you don't have any tests, you can simply return an empty array.
|
||
*
|
||
* But it is recommended to create tests for the behaviors/objects properties you created
|
||
* to avoid mistakes.
|
||
*/
|
||
runExtensionSanityTests: function (gd, extension) {
|
||
return [];
|
||
},
|
||
/**
|
||
* Register editors for objects.
|
||
*
|
||
* ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
|
||
*/
|
||
registerEditorConfigurations: function (objectsEditorService) {},
|
||
/**
|
||
* Register renderers for instance of objects on the scene editor.
|
||
*
|
||
* ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
|
||
*/
|
||
registerInstanceRenderers: function (objectsRenderingService) {
|
||
const { PIXI, RenderedInstance, gd } = objectsRenderingService;
|
||
|
||
class RenderedSpineInstance extends RenderedInstance {
|
||
/** @type {pixi_spine.Spine | null} */
|
||
_spine = null;
|
||
/** @type {PIXI.Sprite} */
|
||
_placeholder;
|
||
_initialWidth = 256;
|
||
_initialHeight = 256;
|
||
_animationIndex = -1;
|
||
_spineOriginOffsetX = 0;
|
||
_spineOriginOffsetY = 0;
|
||
/** @type {string} */
|
||
_skinName = RenderedSpineInstance.getDefaultSkinName();
|
||
|
||
constructor(
|
||
project,
|
||
instance,
|
||
associatedObjectConfiguration,
|
||
pixiContainer,
|
||
pixiResourcesLoader
|
||
) {
|
||
super(
|
||
project,
|
||
instance,
|
||
associatedObjectConfiguration,
|
||
pixiContainer,
|
||
pixiResourcesLoader
|
||
);
|
||
|
||
// there is issue with spine selection. mouse events are not triggering during interaction.
|
||
// create the invisible background rectangle to fill spine range.
|
||
this._placeholder = new PIXI.Sprite(
|
||
this._pixiResourcesLoader.getInvalidPIXITexture()
|
||
);
|
||
this._placeholder.alpha = 0;
|
||
this._pixiObject = new PIXI.Container();
|
||
this._pixiObject.addChild(this._placeholder);
|
||
this._pixiContainer.addChild(this._pixiObject);
|
||
|
||
this._loadSpine();
|
||
}
|
||
|
||
static getDefaultSkinName() {
|
||
return 'default';
|
||
}
|
||
|
||
static getThumbnail(project, resourcesLoader, objectConfiguration) {
|
||
return 'JsPlatform/Extensions/spine.svg';
|
||
}
|
||
|
||
update() {
|
||
const object = gd.castObject(
|
||
this._associatedObjectConfiguration,
|
||
gd.SpineObjectConfiguration
|
||
);
|
||
|
||
const spineResourceName = object.getSpineResourceName();
|
||
if (this._spineResourceName !== spineResourceName) {
|
||
this._spineResourceName = spineResourceName;
|
||
this._loadSpine();
|
||
}
|
||
|
||
// Apply skin if it changed.
|
||
const skinName = object.getSkinName();
|
||
if (this._skinName !== skinName && this._spine) {
|
||
this._skinName = skinName;
|
||
try {
|
||
this._spine.skeleton.setSkinByName(
|
||
skinName || RenderedSpineInstance.getDefaultSkinName()
|
||
);
|
||
this._spine.skeleton.setSlotsToSetupPose();
|
||
this._spine.update(0);
|
||
} catch (skinError) {
|
||
console.warn('Unable to set skin "' + skinName + '":', skinError);
|
||
}
|
||
}
|
||
|
||
this._pixiObject.position.set(
|
||
this._instance.getX(),
|
||
this._instance.getY()
|
||
);
|
||
this._pixiObject.rotation = RenderedInstance.toRad(
|
||
this._instance.getAngle()
|
||
);
|
||
|
||
// Do not hide completely an object so it can still be manipulated
|
||
const alphaForDisplay = Math.max(
|
||
this._instance.getOpacity() / 255,
|
||
0.5
|
||
);
|
||
this._pixiObject.alpha = alphaForDisplay;
|
||
// Scale is already handled below, so we just apply the flip here.
|
||
this._pixiObject.scale.x =
|
||
Math.abs(this._pixiObject.scale.x) *
|
||
(this._instance.isFlippedX() ? -1 : 1);
|
||
this._pixiObject.scale.y =
|
||
Math.abs(this._pixiObject.scale.y) *
|
||
(this._instance.isFlippedY() ? -1 : 1);
|
||
|
||
this.setAnimation(this._instance.getRawDoubleProperty('animation'));
|
||
|
||
const scale = object.getScale() || 1;
|
||
|
||
const spine = this._spine;
|
||
if (spine) {
|
||
const localBounds = spine.getLocalBounds(undefined, true);
|
||
this._initialWidth = localBounds.width * scale;
|
||
this._initialHeight = localBounds.height * scale;
|
||
} else {
|
||
this._initialWidth = 256;
|
||
this._initialHeight = 256;
|
||
}
|
||
|
||
const width = this.getWidth();
|
||
const height = this.getHeight();
|
||
if (spine) {
|
||
spine.width = width;
|
||
spine.height = height;
|
||
const localBounds = spine.getLocalBounds(undefined, true);
|
||
|
||
this._spineOriginOffsetX = localBounds.x * spine.scale.x;
|
||
this._spineOriginOffsetY = localBounds.y * spine.scale.y;
|
||
} else {
|
||
this._spineOriginOffsetX = -width / 2;
|
||
this._spineOriginOffsetY = -height / 2;
|
||
}
|
||
this._placeholder.position.set(
|
||
this._spineOriginOffsetX,
|
||
this._spineOriginOffsetY
|
||
);
|
||
const frame = this._placeholder.texture.frame;
|
||
this._placeholder.scale.x = width / frame.width;
|
||
this._placeholder.scale.y = height / frame.height;
|
||
}
|
||
|
||
/**
|
||
* @returns x coordinate of this spine origin offset
|
||
*/
|
||
getOriginX() {
|
||
return -this._spineOriginOffsetX;
|
||
}
|
||
|
||
/**
|
||
* @returns y coordinate of this spine origin offset
|
||
*/
|
||
getOriginY() {
|
||
return -this._spineOriginOffsetY;
|
||
}
|
||
|
||
getCenterX() {
|
||
return this.getOriginX();
|
||
}
|
||
|
||
getCenterY() {
|
||
return this.getOriginY();
|
||
}
|
||
|
||
/**
|
||
* @param {number} index - animation index
|
||
*/
|
||
setAnimation(index) {
|
||
const { _spine: spine } = this;
|
||
const configuration = gd.asSpineConfiguration(
|
||
this._associatedObjectConfiguration
|
||
);
|
||
|
||
if (
|
||
!spine ||
|
||
configuration.hasNoAnimations() ||
|
||
index === this._animationIndex
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (!Number.isInteger(index) || index < 0) {
|
||
index = 0;
|
||
} else if (configuration.getAnimationsCount() <= index) {
|
||
index = configuration.getAnimationsCount() - 1;
|
||
}
|
||
|
||
this._animationIndex = index;
|
||
const animation = configuration.getAnimation(index);
|
||
const source = animation.getSource();
|
||
const shouldLoop = animation.shouldLoop();
|
||
|
||
// reset scale to track new animation range
|
||
// if custom size is set it will be reinitialized in update method
|
||
spine.scale.set(1, 1);
|
||
spine.state.setAnimation(0, source, shouldLoop);
|
||
spine.state.tracks[0].trackTime = 0;
|
||
spine.update(0);
|
||
spine.autoUpdate = false;
|
||
}
|
||
|
||
/**
|
||
* @returns {number} default width
|
||
*/
|
||
getDefaultWidth() {
|
||
return this._initialWidth;
|
||
}
|
||
|
||
/**
|
||
* @returns {number} default height
|
||
*/
|
||
getDefaultHeight() {
|
||
return this._initialHeight;
|
||
}
|
||
|
||
onRemovedFromScene() {
|
||
super.onRemovedFromScene();
|
||
this._pixiObject.destroy({ children: true });
|
||
}
|
||
|
||
_loadSpine() {
|
||
const object = gd.castObject(
|
||
this._associatedObjectConfiguration,
|
||
gd.SpineObjectConfiguration
|
||
);
|
||
this._pixiResourcesLoader
|
||
.getSpineData(this._project, object.getSpineResourceName())
|
||
.then((spineDataOrLoadingError) => {
|
||
if (this._wasDestroyed) return;
|
||
if (this._spine) this._pixiObject.removeChild(this._spine);
|
||
|
||
if (!spineDataOrLoadingError.skeleton) {
|
||
console.error(
|
||
'Unable to load Spine (' +
|
||
(spineDataOrLoadingError.loadingErrorReason ||
|
||
'Unknown reason') +
|
||
')',
|
||
spineDataOrLoadingError.loadingError
|
||
);
|
||
this._spine = null;
|
||
this._placeholder.alpha = 255;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
this._spine = new PIXI.Spine(spineDataOrLoadingError.skeleton);
|
||
|
||
// Apply the default skin if configured.
|
||
const skinName = object.getSkinName();
|
||
if (skinName && this._spine) {
|
||
try {
|
||
this._spine.skeleton.setSkinByName(skinName);
|
||
this._spine.skeleton.setSlotsToSetupPose();
|
||
} catch (skinError) {
|
||
console.warn(
|
||
'Unable to set skin "' + skinName + '":',
|
||
skinError
|
||
);
|
||
}
|
||
}
|
||
|
||
this._pixiObject.addChild(this._spine);
|
||
this._placeholder.alpha = 0;
|
||
} catch (error) {
|
||
console.error('Exception while loading Spine.', error);
|
||
this._spine = null;
|
||
this._placeholder.alpha = 255;
|
||
return;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
objectsRenderingService.registerInstanceRenderer(
|
||
'SpineObject::SpineObject',
|
||
RenderedSpineInstance
|
||
);
|
||
},
|
||
};
|