mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-22 10:05:37 -04:00
deb802cfec
* For now, this is limited to instances inside custom objects. This will be made available in the future for all instances in scenes if this works well.
519 lines
19 KiB
TypeScript
519 lines
19 KiB
TypeScript
/*
|
|
GDevelop - Anchor Behavior Extension
|
|
Copyright (c) 2013-2016 Florian Rival (Florian.Rival@gmail.com)
|
|
*/
|
|
|
|
namespace gdjs {
|
|
const enum HorizontalAnchor {
|
|
None = 0,
|
|
WindowLeft,
|
|
WindowRight,
|
|
Proportional,
|
|
WindowCenter,
|
|
}
|
|
const enum VerticalAnchor {
|
|
None = 0,
|
|
WindowTop,
|
|
WindowBottom,
|
|
Proportional,
|
|
WindowCenter,
|
|
}
|
|
|
|
/**
|
|
* @category Behaviors > Anchor
|
|
*/
|
|
export class AnchorRuntimeBehavior extends gdjs.RuntimeBehavior {
|
|
// Configuration
|
|
|
|
_relativeToOriginalWindowSize: boolean;
|
|
_leftEdgeAnchor: HorizontalAnchor;
|
|
_rightEdgeAnchor: HorizontalAnchor;
|
|
_topEdgeAnchor: VerticalAnchor;
|
|
_bottomEdgeAnchor: VerticalAnchor;
|
|
_useLegacyBottomAndRightAnchors: boolean = false;
|
|
|
|
// State
|
|
|
|
_hasJustBeenCreated: boolean = true;
|
|
_leftEdgeDistance: float = 0;
|
|
_rightEdgeDistance: float = 0;
|
|
_topEdgeDistance: float = 0;
|
|
_bottomEdgeDistance: float = 0;
|
|
|
|
_oldDrawableX: float = 0;
|
|
_oldDrawableY: float = 0;
|
|
_oldWidth: float = 0;
|
|
_oldHeight: float = 0;
|
|
|
|
_parentOldMinX: float = 0;
|
|
_parentOldMinY: float = 0;
|
|
_parentOldMaxX: float = 0;
|
|
_parentOldMaxY: float = 0;
|
|
|
|
constructor(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
behaviorData,
|
|
owner: gdjs.RuntimeObject
|
|
) {
|
|
super(instanceContainer, behaviorData, owner);
|
|
this._relativeToOriginalWindowSize =
|
|
!!behaviorData.relativeToOriginalWindowSize;
|
|
this._leftEdgeAnchor = behaviorData.leftEdgeAnchor;
|
|
this._rightEdgeAnchor = behaviorData.rightEdgeAnchor;
|
|
this._topEdgeAnchor = behaviorData.topEdgeAnchor;
|
|
this._bottomEdgeAnchor = behaviorData.bottomEdgeAnchor;
|
|
this._useLegacyBottomAndRightAnchors =
|
|
behaviorData.useLegacyBottomAndRightAnchors === undefined
|
|
? true
|
|
: behaviorData.useLegacyBottomAndRightAnchors;
|
|
}
|
|
|
|
override applyBehaviorOverriding(behaviorData): boolean {
|
|
if (behaviorData.leftEdgeAnchor !== undefined) {
|
|
this._leftEdgeAnchor = behaviorData.leftEdgeAnchor;
|
|
}
|
|
if (behaviorData.rightEdgeAnchor !== undefined) {
|
|
this._rightEdgeAnchor = behaviorData.rightEdgeAnchor;
|
|
}
|
|
if (behaviorData.topEdgeAnchor !== undefined) {
|
|
this._topEdgeAnchor = behaviorData.topEdgeAnchor;
|
|
}
|
|
if (behaviorData.bottomEdgeAnchor !== undefined) {
|
|
this._bottomEdgeAnchor = behaviorData.bottomEdgeAnchor;
|
|
}
|
|
if (behaviorData.useLegacyTrajectory !== undefined) {
|
|
this._useLegacyBottomAndRightAnchors =
|
|
behaviorData.useLegacyBottomAndRightAnchors;
|
|
}
|
|
if (behaviorData.relativeToOriginalWindowSize !== undefined) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
override onActivate(): void {
|
|
// This only has a side effect if the camera moved while the behavior was
|
|
// deactivated.
|
|
// The new position on the viewport is where the object should stay.
|
|
this._hasJustBeenCreated = true;
|
|
}
|
|
|
|
override doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
if (this._hasJustBeenCreated) {
|
|
this._initializeAnchorDistances(instanceContainer);
|
|
this._hasJustBeenCreated = false;
|
|
|
|
this._oldDrawableX = this.owner.getDrawableX();
|
|
this._oldDrawableY = this.owner.getDrawableY();
|
|
this._oldWidth = this.owner.getWidth();
|
|
this._oldHeight = this.owner.getHeight();
|
|
}
|
|
this._updateAnchorDistances(instanceContainer);
|
|
this._followAnchor(instanceContainer);
|
|
|
|
this._oldDrawableX = this.owner.getDrawableX();
|
|
this._oldDrawableY = this.owner.getDrawableY();
|
|
this._oldWidth = this.owner.getWidth();
|
|
this._oldHeight = this.owner.getHeight();
|
|
}
|
|
|
|
/**
|
|
* Evaluate the anchor distance according to the object position on the
|
|
* screen.
|
|
*
|
|
* The camera is taken into account.
|
|
*/
|
|
private _initializeAnchorDistances(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer
|
|
) {
|
|
const workingPoint: FloatPoint = gdjs.staticArray(
|
|
gdjs.AnchorRuntimeBehavior.prototype.doStepPreEvents
|
|
) as FloatPoint;
|
|
const layer = instanceContainer.getLayer(this.owner.getLayer());
|
|
|
|
if (this._relativeToOriginalWindowSize) {
|
|
this._parentOldMinX =
|
|
instanceContainer.getInitialUnrotatedViewportMinX();
|
|
this._parentOldMinY =
|
|
instanceContainer.getInitialUnrotatedViewportMinY();
|
|
this._parentOldMaxX =
|
|
instanceContainer.getInitialUnrotatedViewportMaxX();
|
|
this._parentOldMaxY =
|
|
instanceContainer.getInitialUnrotatedViewportMaxY();
|
|
} else {
|
|
this._parentOldMinX = instanceContainer.getUnrotatedViewportMinX();
|
|
this._parentOldMinY = instanceContainer.getUnrotatedViewportMinY();
|
|
this._parentOldMaxX = instanceContainer.getUnrotatedViewportMaxX();
|
|
this._parentOldMaxY = instanceContainer.getUnrotatedViewportMaxY();
|
|
}
|
|
const parentMinX = this._parentOldMinX;
|
|
const parentMinY = this._parentOldMinY;
|
|
const parentMaxX = this._parentOldMaxX;
|
|
const parentMaxY = this._parentOldMaxY;
|
|
|
|
const parentCenterX = (parentMaxX + parentMinX) / 2;
|
|
const parentCenterY = (parentMaxY + parentMinY) / 2;
|
|
const parentWidth = parentMaxX - parentMinX;
|
|
const parentHeight = parentMaxY - parentMinY;
|
|
|
|
// Calculate the distances from the window's bounds.
|
|
const topLeftPixel = this._convertInverseCoords(
|
|
instanceContainer,
|
|
layer,
|
|
this.owner.getDrawableX(),
|
|
this.owner.getDrawableY(),
|
|
workingPoint
|
|
);
|
|
|
|
// Left edge
|
|
if (this._leftEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
|
this._leftEdgeDistance = topLeftPixel[0] - parentMinX;
|
|
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowRight) {
|
|
this._leftEdgeDistance = topLeftPixel[0] - parentMaxX;
|
|
} else if (this._leftEdgeAnchor === HorizontalAnchor.Proportional) {
|
|
this._leftEdgeDistance = (topLeftPixel[0] - parentMinX) / parentWidth;
|
|
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
|
this._leftEdgeDistance = topLeftPixel[0] - parentCenterX;
|
|
}
|
|
|
|
// Top edge
|
|
if (this._topEdgeAnchor === VerticalAnchor.WindowTop) {
|
|
this._topEdgeDistance = topLeftPixel[1] - parentMinY;
|
|
} else if (this._topEdgeAnchor === VerticalAnchor.WindowBottom) {
|
|
this._topEdgeDistance = topLeftPixel[1] - parentMaxY;
|
|
} else if (this._topEdgeAnchor === VerticalAnchor.Proportional) {
|
|
this._topEdgeDistance = (topLeftPixel[1] - parentMinY) / parentHeight;
|
|
} else if (this._topEdgeAnchor === VerticalAnchor.WindowCenter) {
|
|
this._topEdgeDistance = topLeftPixel[1] - parentCenterY;
|
|
}
|
|
|
|
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
|
|
const bottomRightPixel = this._convertInverseCoords(
|
|
instanceContainer,
|
|
layer,
|
|
this.owner.getDrawableX() + this.owner.getWidth(),
|
|
this.owner.getDrawableY() + this.owner.getHeight(),
|
|
workingPoint
|
|
);
|
|
|
|
// Right edge
|
|
if (this._rightEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
|
this._rightEdgeDistance = bottomRightPixel[0] - parentMinX;
|
|
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowRight) {
|
|
this._rightEdgeDistance = bottomRightPixel[0] - parentMaxX;
|
|
} else if (this._rightEdgeAnchor === HorizontalAnchor.Proportional) {
|
|
this._rightEdgeDistance =
|
|
(bottomRightPixel[0] - parentMinX) / parentWidth;
|
|
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
|
this._rightEdgeDistance = bottomRightPixel[0] - parentCenterX;
|
|
}
|
|
|
|
// Bottom edge
|
|
if (this._bottomEdgeAnchor === VerticalAnchor.WindowTop) {
|
|
this._bottomEdgeDistance = bottomRightPixel[1] - parentMinY;
|
|
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowBottom) {
|
|
this._bottomEdgeDistance = bottomRightPixel[1] - parentMaxY;
|
|
} else if (this._bottomEdgeAnchor === VerticalAnchor.Proportional) {
|
|
this._bottomEdgeDistance =
|
|
(bottomRightPixel[1] - parentMinY) / parentHeight;
|
|
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowCenter) {
|
|
this._bottomEdgeDistance = bottomRightPixel[1] - parentCenterY;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the anchor distances according to the object position change in
|
|
* the scene.
|
|
*
|
|
* The camera is not taken into account. Indeed, a camera scrolling should
|
|
* not shift the anchored object on screen.
|
|
*/
|
|
private _updateAnchorDistances(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer
|
|
) {
|
|
if (
|
|
this._oldDrawableX !== this.owner.getDrawableX() ||
|
|
this._oldWidth !== this.owner.getWidth()
|
|
) {
|
|
const parentOldWidth = this._parentOldMaxX - this._parentOldMinX;
|
|
|
|
// Left edge
|
|
const deltaMinX = this.owner.getDrawableX() - this._oldDrawableX;
|
|
if (this._leftEdgeAnchor === HorizontalAnchor.Proportional) {
|
|
this._leftEdgeDistance += deltaMinX / parentOldWidth;
|
|
} else {
|
|
this._leftEdgeDistance += deltaMinX;
|
|
}
|
|
|
|
// Right edge
|
|
const deltaMaxX = deltaMinX + this.owner.getWidth() - this._oldWidth;
|
|
if (this._rightEdgeAnchor === HorizontalAnchor.Proportional) {
|
|
this._rightEdgeDistance += deltaMaxX / parentOldWidth;
|
|
} else {
|
|
this._rightEdgeDistance += deltaMaxX;
|
|
}
|
|
}
|
|
if (
|
|
this._oldDrawableY !== this.owner.getDrawableY() ||
|
|
this._oldHeight !== this.owner.getHeight()
|
|
) {
|
|
const parentOldHeight = this._parentOldMaxY - this._parentOldMinY;
|
|
|
|
// Top edge
|
|
const deltaMinY = this.owner.getDrawableY() - this._oldDrawableY;
|
|
if (this._topEdgeAnchor === VerticalAnchor.Proportional) {
|
|
this._topEdgeDistance += deltaMinY / parentOldHeight;
|
|
} else {
|
|
this._topEdgeDistance += deltaMinY;
|
|
}
|
|
|
|
// Bottom edge
|
|
const deltaMaxY = deltaMinY + this.owner.getHeight() - this._oldHeight;
|
|
if (this._bottomEdgeAnchor === VerticalAnchor.Proportional) {
|
|
this._bottomEdgeDistance += deltaMaxY / parentOldHeight;
|
|
} else {
|
|
this._bottomEdgeDistance += deltaMaxY;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the object position to keep the object on screen according to the
|
|
* anchor distances.
|
|
*
|
|
* The camera is taken into account.
|
|
*/
|
|
private _followAnchor(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
let parentMinX = instanceContainer.getUnrotatedViewportMinX();
|
|
let parentMinY = instanceContainer.getUnrotatedViewportMinY();
|
|
let parentMaxX = instanceContainer.getUnrotatedViewportMaxX();
|
|
let parentMaxY = instanceContainer.getUnrotatedViewportMaxY();
|
|
|
|
if (
|
|
this._parentOldMinX === parentMinX &&
|
|
this._parentOldMinY === parentMinY &&
|
|
this._parentOldMaxX === parentMaxX &&
|
|
this._parentOldMaxY === parentMaxY
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const workingPoint: FloatPoint = gdjs.staticArray(
|
|
gdjs.AnchorRuntimeBehavior.prototype.doStepPreEvents
|
|
) as FloatPoint;
|
|
const layer = instanceContainer.getLayer(this.owner.getLayer());
|
|
|
|
const parentCenterX = (parentMaxX + parentMinX) / 2;
|
|
const parentCenterY = (parentMaxY + parentMinY) / 2;
|
|
const parentWidth = parentMaxX - parentMinX;
|
|
const parentHeight = parentMaxY - parentMinY;
|
|
|
|
//Move and resize the object if needed
|
|
let leftPixel = 0;
|
|
let topPixel = 0;
|
|
let rightPixel = 0;
|
|
let bottomPixel = 0;
|
|
|
|
// Left edge
|
|
if (this._leftEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
|
leftPixel = parentMinX + this._leftEdgeDistance;
|
|
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowRight) {
|
|
leftPixel = parentMaxX + this._leftEdgeDistance;
|
|
} else if (this._leftEdgeAnchor === HorizontalAnchor.Proportional) {
|
|
leftPixel = parentMinX + this._leftEdgeDistance * parentWidth;
|
|
} else if (this._leftEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
|
leftPixel = parentCenterX + this._leftEdgeDistance;
|
|
}
|
|
|
|
// Top edge
|
|
if (this._topEdgeAnchor === VerticalAnchor.WindowTop) {
|
|
topPixel = parentMinY + this._topEdgeDistance;
|
|
} else if (this._topEdgeAnchor === VerticalAnchor.WindowBottom) {
|
|
topPixel = parentMaxY + this._topEdgeDistance;
|
|
} else if (this._topEdgeAnchor === VerticalAnchor.Proportional) {
|
|
topPixel = parentMinY + this._topEdgeDistance * parentHeight;
|
|
} else if (this._topEdgeAnchor === VerticalAnchor.WindowCenter) {
|
|
topPixel = parentCenterY + this._topEdgeDistance;
|
|
}
|
|
|
|
// Right edge
|
|
if (this._rightEdgeAnchor === HorizontalAnchor.WindowLeft) {
|
|
rightPixel = parentMinX + this._rightEdgeDistance;
|
|
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowRight) {
|
|
rightPixel = parentMaxX + this._rightEdgeDistance;
|
|
} else if (this._rightEdgeAnchor === HorizontalAnchor.Proportional) {
|
|
rightPixel = parentMinX + this._rightEdgeDistance * parentWidth;
|
|
} else if (this._rightEdgeAnchor === HorizontalAnchor.WindowCenter) {
|
|
rightPixel = parentCenterX + this._rightEdgeDistance;
|
|
}
|
|
|
|
// Bottom edge
|
|
if (this._bottomEdgeAnchor === VerticalAnchor.WindowTop) {
|
|
bottomPixel = parentMinY + this._bottomEdgeDistance;
|
|
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowBottom) {
|
|
bottomPixel = parentMaxY + this._bottomEdgeDistance;
|
|
} else if (this._bottomEdgeAnchor === VerticalAnchor.Proportional) {
|
|
bottomPixel = parentMinY + this._bottomEdgeDistance * parentHeight;
|
|
} else if (this._bottomEdgeAnchor === VerticalAnchor.WindowCenter) {
|
|
bottomPixel = parentCenterY + this._bottomEdgeDistance;
|
|
}
|
|
|
|
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
|
|
const topLeftCoord = this._convertCoords(
|
|
instanceContainer,
|
|
layer,
|
|
leftPixel,
|
|
topPixel,
|
|
workingPoint
|
|
);
|
|
let left = topLeftCoord[0];
|
|
let top = topLeftCoord[1];
|
|
|
|
const bottomRightCoord = this._convertCoords(
|
|
instanceContainer,
|
|
layer,
|
|
rightPixel,
|
|
bottomPixel,
|
|
workingPoint
|
|
);
|
|
const right = bottomRightCoord[0];
|
|
const bottom = bottomRightCoord[1];
|
|
|
|
// Compatibility with GD <= 5.0.133
|
|
if (this._useLegacyBottomAndRightAnchors) {
|
|
//Move and resize the object according to the anchors
|
|
if (this._rightEdgeAnchor !== HorizontalAnchor.None) {
|
|
this.owner.setWidth(right - left);
|
|
}
|
|
if (this._bottomEdgeAnchor !== VerticalAnchor.None) {
|
|
this.owner.setHeight(bottom - top);
|
|
}
|
|
if (this._leftEdgeAnchor !== HorizontalAnchor.None) {
|
|
this.owner.setX(left + this.owner.getX() - this.owner.getDrawableX());
|
|
}
|
|
if (this._topEdgeAnchor !== VerticalAnchor.None) {
|
|
this.owner.setY(top + this.owner.getY() - this.owner.getDrawableY());
|
|
}
|
|
}
|
|
// End of compatibility code
|
|
else {
|
|
// Resize if right and left anchors are set
|
|
if (
|
|
this._rightEdgeAnchor !== HorizontalAnchor.None &&
|
|
this._leftEdgeAnchor !== HorizontalAnchor.None
|
|
) {
|
|
const width = right - left;
|
|
this.owner.setX(
|
|
this.owner.getX() === this.owner.getDrawableX()
|
|
? left
|
|
: // It uses the position of the origin relatively to the object
|
|
// size to apply it with the new size.
|
|
// This is the same as doing:
|
|
// lerp(left, right, (this.owner.getX() - this.owner.getDrawableX() / this.owner.getWidth())
|
|
// But, the division is done at the end to avoid rounding errors.
|
|
left +
|
|
((this.owner.getX() - this.owner.getDrawableX()) * width) /
|
|
this.owner.getWidth()
|
|
);
|
|
this.owner.setWidth(width);
|
|
} else {
|
|
if (this._leftEdgeAnchor !== HorizontalAnchor.None) {
|
|
this.owner.setX(
|
|
left + this.owner.getX() - this.owner.getDrawableX()
|
|
);
|
|
}
|
|
if (this._rightEdgeAnchor !== HorizontalAnchor.None) {
|
|
this.owner.setX(
|
|
right +
|
|
this.owner.getX() -
|
|
this.owner.getDrawableX() -
|
|
this.owner.getWidth()
|
|
);
|
|
}
|
|
}
|
|
|
|
// Resize if top and bottom anchors are set
|
|
if (
|
|
this._bottomEdgeAnchor !== VerticalAnchor.None &&
|
|
this._topEdgeAnchor !== VerticalAnchor.None
|
|
) {
|
|
const height = bottom - top;
|
|
this.owner.setY(
|
|
this.owner.getY() === this.owner.getDrawableY()
|
|
? top
|
|
: top +
|
|
((this.owner.getY() - this.owner.getDrawableY()) * height) /
|
|
this.owner.getHeight()
|
|
);
|
|
this.owner.setHeight(height);
|
|
} else {
|
|
if (this._topEdgeAnchor !== VerticalAnchor.None) {
|
|
this.owner.setY(
|
|
top + this.owner.getY() - this.owner.getDrawableY()
|
|
);
|
|
}
|
|
if (this._bottomEdgeAnchor !== VerticalAnchor.None) {
|
|
this.owner.setY(
|
|
bottom +
|
|
this.owner.getY() -
|
|
this.owner.getDrawableY() -
|
|
this.owner.getHeight()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
this._parentOldMinX = instanceContainer.getUnrotatedViewportMinX();
|
|
this._parentOldMinY = instanceContainer.getUnrotatedViewportMinY();
|
|
this._parentOldMaxX = instanceContainer.getUnrotatedViewportMaxX();
|
|
this._parentOldMaxY = instanceContainer.getUnrotatedViewportMaxY();
|
|
}
|
|
|
|
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {
|
|
// Custom objects can be resized during the events step.
|
|
// The anchor constraints must be applied on child-objects after the parent events.
|
|
const isChildObject = instanceContainer !== instanceContainer.getScene();
|
|
if (isChildObject) {
|
|
this.doStepPreEvents(instanceContainer);
|
|
}
|
|
}
|
|
|
|
private _convertCoords(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
layer: gdjs.RuntimeLayer,
|
|
x: float,
|
|
y: float,
|
|
result: FloatPoint
|
|
) {
|
|
const isParentACustomObject =
|
|
instanceContainer !== instanceContainer.getScene();
|
|
if (isParentACustomObject) {
|
|
result[0] = x;
|
|
result[1] = y;
|
|
return result;
|
|
}
|
|
return layer.convertCoords(x, y, 0, result);
|
|
}
|
|
|
|
private _convertInverseCoords(
|
|
instanceContainer: gdjs.RuntimeInstanceContainer,
|
|
layer: gdjs.RuntimeLayer,
|
|
x: float,
|
|
y: float,
|
|
result: FloatPoint
|
|
) {
|
|
const isParentACustomObject =
|
|
instanceContainer !== instanceContainer.getScene();
|
|
if (isParentACustomObject) {
|
|
result[0] = x;
|
|
result[1] = y;
|
|
return result;
|
|
}
|
|
return layer.convertInverseCoords(x, y, 0, result);
|
|
}
|
|
}
|
|
gdjs.registerBehavior(
|
|
'AnchorBehavior::AnchorBehavior',
|
|
gdjs.AnchorRuntimeBehavior
|
|
);
|
|
}
|