1292 строки
50 KiB
TypeScript
1292 строки
50 KiB
TypeScript
|
///<reference path='refs.ts'/>
|
|||
|
|
|||
|
module TDev.RT {
|
|||
|
export enum SpriteType
|
|||
|
{
|
|||
|
Ellipse,
|
|||
|
Rectangle,
|
|||
|
Text,
|
|||
|
Picture,
|
|||
|
Anchor,
|
|||
|
}
|
|||
|
|
|||
|
class SpriteContent
|
|||
|
extends RTValue
|
|||
|
{
|
|||
|
constructor() {
|
|||
|
super()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? A sprite
|
|||
|
//@ icon("sprite") ctx(general,gckey)
|
|||
|
export class Sprite
|
|||
|
extends RTValue
|
|||
|
{
|
|||
|
public _parent : Board = undefined;
|
|||
|
public _sheet : SpriteSheet = undefined;
|
|||
|
private _friction : number = Number.NaN;
|
|||
|
private _angular_speed : number = 0;
|
|||
|
public _height : number = undefined;
|
|||
|
public _location : Location_ = undefined;
|
|||
|
private _angle : number = 0;
|
|||
|
public _elasticity : number = 1;
|
|||
|
public _scale : number = 1;
|
|||
|
constructor() {
|
|||
|
super()
|
|||
|
}
|
|||
|
private _speed : Vector2 = Vector2.mk(0,0);
|
|||
|
private _acceleration : Vector2 = Vector2.mk(0,0);
|
|||
|
public _width : number = undefined;
|
|||
|
private _mass : number = Number.NaN;
|
|||
|
|
|||
|
public _position : Vector2 = Vector2.mk(0,0);
|
|||
|
private _color : Color = Colors.light_gray();
|
|||
|
private _text : string = undefined;
|
|||
|
private _textBaseline : string = undefined;
|
|||
|
public _hidden : boolean = false;
|
|||
|
private _opacity : number = 1;
|
|||
|
private _clip: number[] = undefined;
|
|||
|
|
|||
|
public spriteType : SpriteType;
|
|||
|
public fontSize : number;
|
|||
|
private _picture : Picture;
|
|||
|
private _animations : SpriteAnimation[];
|
|||
|
private shapeDirty = true;
|
|||
|
private hasChanged = true;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Produced at each step by the wall collision
|
|||
|
/// Used in the next step to clip forces (reactive forces)
|
|||
|
/// </summary>
|
|||
|
public normalTouchPoints : Vector2[] = [];
|
|||
|
|
|||
|
private _lastPosition : Vector2 ; // used in redraw, no serialization
|
|||
|
|
|||
|
private _springs : Spring[] = [];
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// newPosition, newSpeed, and midSpeed, newRotation are only used during an update step and need not be serialized
|
|||
|
/// </summary>
|
|||
|
public newPosition : Vector2;
|
|||
|
public newSpeed : Vector2;
|
|||
|
public midSpeed : Vector2;
|
|||
|
private newRotation : number;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Recomputed on demand
|
|||
|
/// </summary>
|
|||
|
public boundingMinX : number;
|
|||
|
public boundingMaxX : number;
|
|||
|
public boundingMinY : number;
|
|||
|
public boundingMaxY : number;
|
|||
|
|
|||
|
/// events
|
|||
|
public onTap: Event_ = new Event_();
|
|||
|
public onSwipe: Event_ = new Event_();
|
|||
|
public onDrag: Event_ = new Event_();
|
|||
|
public onTouchDown: Event_ = new Event_();
|
|||
|
public onTouchUp: Event_ = new Event_();
|
|||
|
public onEveryFrame: Event_ = new Event_();
|
|||
|
|
|||
|
static mk(tp:SpriteType, x:number, y:number, w:number, h:number)
|
|||
|
{
|
|||
|
var s = new Sprite();
|
|||
|
s.spriteType = tp;
|
|||
|
s._width = w;
|
|||
|
s._height = h;
|
|||
|
s._position = new Vector2(x, y);
|
|||
|
s.computeBoundingBox();
|
|||
|
return s;
|
|||
|
}
|
|||
|
|
|||
|
//? Gets the fraction of speed loss between 0 and 1
|
|||
|
//@ readsMutable
|
|||
|
public friction() : number
|
|||
|
{
|
|||
|
if (! this._parent) return NaN;
|
|||
|
if (isNaN(this._friction)) return this._parent._worldFriction;
|
|||
|
return this._friction;
|
|||
|
}
|
|||
|
|
|||
|
//? Sets the friction to a fraction of speed loss between 0 and 1
|
|||
|
//@ writesMutable
|
|||
|
public set_friction(friction:number) : void { this._friction = Math.min(1, Math.abs(friction)); }
|
|||
|
|
|||
|
//? Gets the scaling applied when rendering the sprite. This scaling does not influence the bounding box.
|
|||
|
//@ readsMutable
|
|||
|
public scale() : number { return this._scale; }
|
|||
|
|
|||
|
//? Sets the scaling applied to the sprite on rendering. This scaling does not influence the bounding box.
|
|||
|
//@ writesMutable [value].defl(1)
|
|||
|
public set_scale(value : number) { this._scale = value; }
|
|||
|
|
|||
|
//? Gets the rotation speed in degrees/sec
|
|||
|
//@ readsMutable
|
|||
|
public angular_speed() : number { return this._angular_speed; }
|
|||
|
|
|||
|
//? Sets the rotation speed in degrees/sec
|
|||
|
//@ writesMutable
|
|||
|
public set_angular_speed(speed:number) : void { this._angular_speed = speed; }
|
|||
|
|
|||
|
//? Gets the height in pixels
|
|||
|
public height() : number { return this._height; }
|
|||
|
|
|||
|
//? Gets the geo location assigned to the sprite
|
|||
|
//@ readsMutable
|
|||
|
public location() : Location_ { return this._location; }
|
|||
|
|
|||
|
//? Sets the geo location of the sprite
|
|||
|
//@ cap(motion) flow(SourceGeoLocation)
|
|||
|
//@ writesMutable
|
|||
|
public set_location(location:Location_) : void { this._location = location; }
|
|||
|
|
|||
|
//? Gets the angle of the sprite in degrees
|
|||
|
//@ readsMutable
|
|||
|
public angle() : number { return this._angle; }
|
|||
|
|
|||
|
//? Sets the angle of the sprite in degrees
|
|||
|
//@ writesMutable
|
|||
|
public set_angle(angle:number) : void {
|
|||
|
if(this._angle != angle) {
|
|||
|
this._angle = angle;
|
|||
|
this.computeBoundingBox();
|
|||
|
this.contentChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Gets the sprite elasticity as a fraction of speed preservation per bounce (0-1)
|
|||
|
//@ readsMutable
|
|||
|
public elasticity() : number { return this._elasticity; }
|
|||
|
|
|||
|
//? Sets the sprite elasticity as a fraction of speed preservation per bounce (0-1)
|
|||
|
//@ writesMutable
|
|||
|
public set_elasticity(elasticity:number) : void { this._elasticity = Math.abs(elasticity); }
|
|||
|
|
|||
|
//? Gets the speed along x in pixels/sec
|
|||
|
//@ readsMutable
|
|||
|
public speed_x() : number { return this._speed.x(); }
|
|||
|
|
|||
|
//? Sets the x speed in pixels/sec
|
|||
|
//@ writesMutable
|
|||
|
public set_speed_x(vx:number) : void { this._speed = new Vector2(vx, this._speed.y()); }
|
|||
|
|
|||
|
//? Gets the speed along y in pixels/sec
|
|||
|
//@ readsMutable
|
|||
|
public speed_y() : number { return this._speed.y(); }
|
|||
|
|
|||
|
//? Sets the y speed in pixels/sec
|
|||
|
//@ writesMutable
|
|||
|
public set_speed_y(vy:number) : void { this._speed = new Vector2(this._speed.x(), vy); }
|
|||
|
|
|||
|
//? Gets the width in pixels
|
|||
|
public width() : number { return this._width; }
|
|||
|
|
|||
|
//? Sets the height in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_height(height:number) : void
|
|||
|
{
|
|||
|
height = Math.max(1, height);
|
|||
|
if (height != this._height) {
|
|||
|
this._height = height;
|
|||
|
if (this._picture)
|
|||
|
this._width = this._picture.widthSync() / Math.max(1, this._picture.heightSync()) * this._height;
|
|||
|
this.computeBoundingBox();
|
|||
|
this.contentChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Sets the width in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_width(width:number) : void {
|
|||
|
width = Math.max(1, width);
|
|||
|
if(this._width != width) {
|
|||
|
this._frame = null;
|
|||
|
this._width = width;
|
|||
|
if (this._picture)
|
|||
|
this._height = this._picture.heightSync() / Math.max(1, this._picture.widthSync()) * this._width;
|
|||
|
this.computeBoundingBox();
|
|||
|
this.contentChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Gets the top position in pixels
|
|||
|
//@ readsMutable
|
|||
|
public top(): number { return this._position.y() - this._height / 2; }
|
|||
|
|
|||
|
//? Sets the top position in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_top(y: number): void { this._position = new Vector2(this._position.x(), y + this._height / 2); }
|
|||
|
|
|||
|
//? Gets the bottom position in pixels
|
|||
|
//@ readsMutable
|
|||
|
public bottom(): number { return this._position.y() + this._height / 2; }
|
|||
|
|
|||
|
//? Sets the bottom position in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_bottom(y: number): void { this._position = new Vector2(this._position.x(), y - this._height / 2); }
|
|||
|
|
|||
|
//? Gets the right position in pixels
|
|||
|
//@ readsMutable
|
|||
|
public right(): number { return this._position.x() + this._width / 2; }
|
|||
|
|
|||
|
//? Sets the right position in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_right(x: number): void { this._position = new Vector2(x - this._width / 2, this._position.y()); }
|
|||
|
|
|||
|
//? Gets the left position in pixels
|
|||
|
//@ readsMutable
|
|||
|
public left(): number { return this._position.x() - this._width / 2; }
|
|||
|
|
|||
|
//? Sets the left position in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_left(x: number): void { this._position = new Vector2(x + this._width / 2, this._position.y()); }
|
|||
|
|
|||
|
//? Gets the center horizontal position of in pixels
|
|||
|
//@ readsMutable
|
|||
|
public x() : number { return this._position.x(); }
|
|||
|
|
|||
|
//? Sets the center horizontal position in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_x(x:number) : void { this._position = new Vector2(x, this._position.y()); }
|
|||
|
|
|||
|
//? Gets the y position in pixels
|
|||
|
//@ readsMutable
|
|||
|
public y() : number { return this._position.y(); }
|
|||
|
|
|||
|
//? Sets the y position in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_y(y:number) : void { this._position = new Vector2(this._position.x(), y); }
|
|||
|
|
|||
|
//? Returns the sprite color.
|
|||
|
//@ readsMutable
|
|||
|
public color() : Color { return this._color; }
|
|||
|
|
|||
|
//? Sets the sprite color.
|
|||
|
//@ writesMutable
|
|||
|
//@ [color].deflExpr('colors->random')
|
|||
|
public set_color(color:Color) : void { this._color = color; this.contentChanged(); }
|
|||
|
|
|||
|
//? Gets the opacity (between 0 transparent and 1 opaque)
|
|||
|
public opacity() : number { return this._opacity; }
|
|||
|
|
|||
|
//? Sets the sprite opacity (between 0 transparent and 1 opaque).
|
|||
|
//@ writesMutable
|
|||
|
public set_opacity(opacity:number) : void { this._opacity = Math.min(1,Math.max(0,opacity)); this.contentChanged(); }
|
|||
|
|
|||
|
//? Gets the associated sprite sheet
|
|||
|
public sheet() : SpriteSheet { return this._sheet; }
|
|||
|
|
|||
|
public setSheet(sheet : SpriteSheet) {
|
|||
|
this._sheet = sheet;
|
|||
|
this.setPictureInternal(this._sheet._picture);
|
|||
|
this._width = 0;
|
|||
|
this._height = 0;
|
|||
|
}
|
|||
|
|
|||
|
//? Sets the font size in pixels of the sprite (for text sprites)
|
|||
|
//@ writesMutable [size].defl(20)
|
|||
|
public set_font_size(size : number, s : IStackFrame) {
|
|||
|
var size = Math.round(size);
|
|||
|
if (this.fontSize != size) {
|
|||
|
this.fontSize = size;
|
|||
|
this.changed();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Gets the font size in pixels (for text sprites)
|
|||
|
//@ readsMutable
|
|||
|
public font_size() : number {
|
|||
|
return this.fontSize || 0;
|
|||
|
}
|
|||
|
|
|||
|
//? Sets the current text baseline used when drawing text (for text sprites)
|
|||
|
//@ [pos].deflStrings("top", "alphabetic", "hanging", "middle", "ideographic", "bottom") writesMutable
|
|||
|
public set_text_baseline(pos : string, s : IStackFrame) {
|
|||
|
pos = pos.trim().toLowerCase();
|
|||
|
if (!/^(alphabetic|top|hanging|middle|ideographic|bottom)$/.test(pos))
|
|||
|
Util.userError(lf("invalid text baseline value"), s.pc);
|
|||
|
this._textBaseline = pos;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
//? Gets the current text baseline (for text sprites)
|
|||
|
//@ readsMutable
|
|||
|
public text_baseline() : string {
|
|||
|
return this._textBaseline;
|
|||
|
}
|
|||
|
|
|||
|
//? Fits the bounding box to the size of the text
|
|||
|
//@ writesMutable
|
|||
|
public fit_text() {
|
|||
|
var ctx;
|
|||
|
if (this._text && this._parent && (ctx = this._parent.renderingContext())) {
|
|||
|
ctx.save();
|
|||
|
ctx.font = this.font(this.fontSize);
|
|||
|
var lines = this._text.split('\n');
|
|||
|
var w = 0, h = this.fontSize *((lines.length - 1) * 1.25 + 1);
|
|||
|
lines.forEach(line => w = Math.max(w, ctx.measureText(line).width));
|
|||
|
ctx.restore();
|
|||
|
this.set_width(w);
|
|||
|
this.set_height(h);
|
|||
|
this._textBaseline = "middle";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? The text on a text sprite (if it is a text sprite)
|
|||
|
//@ readsMutable
|
|||
|
public text() : string { return this._text; }
|
|||
|
|
|||
|
//? Updates text on a text sprite (if it is a text sprite)
|
|||
|
//@ writesMutable
|
|||
|
public set_text(text: string): void {
|
|||
|
if (this.spriteType == SpriteType.Text) {
|
|||
|
this._text = text; this.contentChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Gets the mass
|
|||
|
//@ readsMutable
|
|||
|
public mass() : number {
|
|||
|
if (isNaN(this._mass)) {
|
|||
|
return Math.max(1e-6, this.width() * this.height());
|
|||
|
}
|
|||
|
return this._mass;
|
|||
|
}
|
|||
|
|
|||
|
//? Sets the sprite mass.
|
|||
|
//@ writesMutable
|
|||
|
public set_mass(mass:number) : void {
|
|||
|
if (isNaN(mass) || isFinite(mass) && mass > 0) {
|
|||
|
this._mass = mass;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Gets the acceleration along x in pixels/sec^2
|
|||
|
//@ readsMutable
|
|||
|
public acceleration_x():number { return this._acceleration._x; }
|
|||
|
|
|||
|
//? Gets the acceleration along y in pixels/sec^2
|
|||
|
//@ readsMutable
|
|||
|
public acceleration_y():number { return this._acceleration._y; }
|
|||
|
|
|||
|
//? Sets the x acceleration in pixels/sec^2
|
|||
|
//@ writesMutable
|
|||
|
public set_acceleration_x(x:number) { this._acceleration = new Vector2(x, this._acceleration._y); }
|
|||
|
|
|||
|
//? Sets the y acceleration in pixels/sec^2
|
|||
|
//@ writesMutable
|
|||
|
public set_acceleration_y(y:number) { this._acceleration = new Vector2(this._acceleration._x, y); }
|
|||
|
|
|||
|
//? Sets the acceleration in pixels/sec^2
|
|||
|
//@ writesMutable
|
|||
|
public set_acceleration(x:number, y:number) { this._acceleration = new Vector2(x, y); }
|
|||
|
|
|||
|
//? Set the handler invoked when the sprite is tapped
|
|||
|
//@ ignoreReturnValue
|
|||
|
public on_tap(tapped: PositionAction) : EventBinding {
|
|||
|
return this.onTap.addHandler(tapped);
|
|||
|
}
|
|||
|
|
|||
|
//? Set the handler invoked when the sprite is swiped
|
|||
|
//@ ignoreReturnValue
|
|||
|
public on_swipe(swiped: VectorAction) : EventBinding {
|
|||
|
return this.onSwipe.addHandler(swiped);
|
|||
|
}
|
|||
|
|
|||
|
//? Set the handler invoked when the sprite is dragged
|
|||
|
//@ ignoreReturnValue
|
|||
|
public on_drag(dragged: VectorAction) : EventBinding {
|
|||
|
return this.onDrag.addHandler(dragged);
|
|||
|
}
|
|||
|
|
|||
|
//? Set the handler invoked when the sprite is touched initially
|
|||
|
//@ ignoreReturnValue
|
|||
|
public on_touch_down(touch_down: PositionAction) : EventBinding {
|
|||
|
return this.onTouchDown.addHandler(touch_down);
|
|||
|
}
|
|||
|
|
|||
|
//? Set the handler invoked when the sprite touch is released
|
|||
|
//@ ignoreReturnValue
|
|||
|
public on_touch_up(touch_up: PositionAction) : EventBinding {
|
|||
|
return this.onTouchUp.addHandler(touch_up);
|
|||
|
}
|
|||
|
|
|||
|
//? Add an action that fires for every display frame
|
|||
|
//@ ignoreReturnValue
|
|||
|
public on_every_frame(perform: Action, s: IStackFrame): EventBinding {
|
|||
|
if (this._parent)
|
|||
|
this._parent.enableEveryFrameOnSprite(s);
|
|||
|
return this.onEveryFrame.addHandler(perform)
|
|||
|
}
|
|||
|
|
|||
|
public changed(): void
|
|||
|
{
|
|||
|
this.hasChanged = true;
|
|||
|
}
|
|||
|
|
|||
|
private contentChanged(): void
|
|||
|
{
|
|||
|
this.changed();
|
|||
|
this.shapeDirty = true;
|
|||
|
}
|
|||
|
|
|||
|
public redraw(ctx : CanvasRenderingContext2D, debug: boolean)
|
|||
|
{
|
|||
|
if (!debug && (this._hidden || this._opacity == 0 || this._width <= 0 || this._height <= 0 || this._scale == 0)) return; // don't render hidden sprites
|
|||
|
|
|||
|
//if (!hasChanged) return;
|
|||
|
//hasChanged = false;
|
|||
|
this.drawShape(ctx, debug);
|
|||
|
//self.canvas.style.left = (self.x() - self.width()/2 ) + "px";
|
|||
|
//self.canvas.style.top = (self.y() - self.height()/2 ) + "px";
|
|||
|
// self.canvas.style.transform = "rotate(30deg)";
|
|||
|
}
|
|||
|
|
|||
|
private font(size : number) : string {
|
|||
|
return size + "px " + '"Segoe UI", "Segoe WP", "Helvetica Neue", Sans-Serif';
|
|||
|
}
|
|||
|
|
|||
|
private drawShape(ctx : CanvasRenderingContext2D, debug : boolean)
|
|||
|
{
|
|||
|
//if (!shapeDirty) return;
|
|||
|
//shapeDirty = false;
|
|||
|
|
|||
|
//if (!canvasIsEmpty)
|
|||
|
// ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|||
|
//canvasIsEmpty = false;
|
|||
|
|
|||
|
//canvas.width = _width;
|
|||
|
//canvas.height = _height;
|
|||
|
|
|||
|
var fcolor = this.color().toHtml();
|
|||
|
var dcolor = () => this.color().make_transparent(1).toHtml(); // debug color not tranparent
|
|||
|
var scaledWidth = this._width * this._scale;
|
|||
|
var scaledHeight = this._height * this._scale;
|
|||
|
var scaledFontSize = Math_.round_with_precision(this.fontSize * this._scale, 1);
|
|||
|
ctx.save();
|
|||
|
ctx.translate(this.x(), this.y());
|
|||
|
var ag = this._angle / 180 * Math.PI;
|
|||
|
if (this._frame && this._frame.rotated) ag -= 90;
|
|||
|
ctx.rotate(ag);
|
|||
|
switch (this.spriteType) {
|
|||
|
case SpriteType.Rectangle:
|
|||
|
ctx.translate(-scaledWidth/2, -scaledHeight/2);
|
|||
|
if (debug) {
|
|||
|
ctx.strokeStyle = dcolor();
|
|||
|
ctx.strokeRect(0, 0, scaledWidth, scaledHeight);
|
|||
|
}
|
|||
|
// set opacity only after debugging done
|
|||
|
ctx.globalAlpha = this._opacity;
|
|||
|
ctx.fillStyle = fcolor;
|
|||
|
if (!this._hidden) {
|
|||
|
ctx.fillRect(0, 0, scaledWidth, scaledHeight);
|
|||
|
}
|
|||
|
break;
|
|||
|
|
|||
|
case SpriteType.Ellipse:
|
|||
|
// TODO need to play with createRadialGradient()
|
|||
|
ctx.scale(scaledWidth/scaledHeight, 1);
|
|||
|
ctx.translate(-scaledHeight/2, -scaledHeight/2);
|
|||
|
|
|||
|
// debug rectangle around ellipse
|
|||
|
if (debug) {
|
|||
|
ctx.strokeStyle = dcolor();
|
|||
|
ctx.strokeRect(0, 0, scaledHeight, scaledHeight);
|
|||
|
}
|
|||
|
|
|||
|
// set opacity only after debugging done
|
|||
|
ctx.globalAlpha = this._opacity;
|
|||
|
if (!this._hidden) {
|
|||
|
ctx.beginPath();
|
|||
|
ctx.arc(scaledHeight/2, scaledHeight/2, scaledHeight/2, 0, 2*Math.PI);
|
|||
|
if (Browser.brokenGradient) {
|
|||
|
ctx.fillStyle = fcolor;
|
|||
|
}
|
|||
|
else {
|
|||
|
try {
|
|||
|
var radgrad = ctx.createRadialGradient(scaledHeight * 0.75, scaledHeight * 0.25, 1, scaledHeight / 2, scaledHeight / 2, scaledHeight / 2);
|
|||
|
radgrad.addColorStop(0, '#FFFFFF');
|
|||
|
radgrad.addColorStop(1, fcolor);
|
|||
|
ctx.fillStyle = radgrad;
|
|||
|
} catch (e) {
|
|||
|
Util.log("draw shape crash, color: " + fcolor);
|
|||
|
throw e;
|
|||
|
}
|
|||
|
}
|
|||
|
ctx.closePath();
|
|||
|
ctx.fill();
|
|||
|
}
|
|||
|
break;
|
|||
|
|
|||
|
case SpriteType.Picture:
|
|||
|
ctx.translate(- scaledWidth/2, -scaledHeight/2);
|
|||
|
// debug rectangle around ellipse
|
|||
|
if (debug) {
|
|||
|
ctx.strokeStyle = dcolor();
|
|||
|
ctx.strokeRect(0, 0, scaledWidth, scaledHeight);
|
|||
|
}
|
|||
|
// this may be called by screen resize before _picture is actually set
|
|||
|
if (this._opacity > 0 && this._picture) {
|
|||
|
// set opacity only after debugging done
|
|||
|
ctx.globalAlpha = this._opacity;
|
|||
|
if (!this._hidden) {
|
|||
|
if (this._clip) {
|
|||
|
if(this._clip[2] > 0 && this._clip[3] > 0)
|
|||
|
ctx.drawImage(
|
|||
|
this._picture.getCanvas(),
|
|||
|
this._clip[0], this._clip[1], this._clip[2], this._clip[3],
|
|||
|
0, 0, scaledWidth, scaledHeight);
|
|||
|
} else {
|
|||
|
ctx.drawImage(
|
|||
|
this._picture.getCanvas(),
|
|||
|
0, 0, this._picture.widthSync(), this._picture.heightSync(),
|
|||
|
0, 0, scaledWidth, scaledHeight);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
|
|||
|
case SpriteType.Text:
|
|||
|
ctx.translate(-scaledWidth/2, -scaledHeight/2);
|
|||
|
// debug rectangle around ellipse
|
|||
|
ctx.fillStyle = fcolor;
|
|||
|
if (debug) {
|
|||
|
ctx.strokeStyle = dcolor();
|
|||
|
ctx.strokeRect(0, 0, scaledWidth, scaledHeight);
|
|||
|
}
|
|||
|
if (!this._hidden) {
|
|||
|
// set opacity only after debugging done
|
|||
|
ctx.globalAlpha = this._opacity;
|
|||
|
ctx.font = this.font(scaledFontSize);
|
|||
|
var lines = this._text.split("\n");
|
|||
|
ctx.textBaseline = this._textBaseline || "top";
|
|||
|
// adjust y to match phone layout
|
|||
|
if (!this._textBaseline)
|
|||
|
ctx.translate(0, scaledFontSize * 0.2);
|
|||
|
else ctx.translate(0, scaledHeight /2);
|
|||
|
//ctx.translate(offset, this.fontSize);
|
|||
|
for (var line = 0; line < lines.length; line++) {
|
|||
|
var msr = ctx.measureText(lines[line]);
|
|||
|
ctx.save();
|
|||
|
var offset = scaledWidth - msr.width;
|
|||
|
if (offset > 0) {
|
|||
|
offset = offset / 2;
|
|||
|
}
|
|||
|
else {
|
|||
|
offset = 0;
|
|||
|
}
|
|||
|
ctx.translate(offset, 0);
|
|||
|
ctx.fillText(lines[line], 0, 0);
|
|||
|
ctx.restore();
|
|||
|
ctx.translate(0, scaledFontSize * 1.25);
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
|
|||
|
case SpriteType.Anchor:
|
|||
|
ctx.translate(- scaledWidth/2, - scaledHeight/2);
|
|||
|
// debug rectangle around ellipse
|
|||
|
if (debug) {
|
|||
|
ctx.strokeStyle = dcolor();
|
|||
|
ctx.strokeRect(0, 0, scaledWidth, scaledHeight);
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
if (debug) {
|
|||
|
ctx.restore();
|
|||
|
ctx.save();
|
|||
|
ctx.translate(this.x() + this.boundingMaxX + 2, this.y()+this.boundingMinY);
|
|||
|
ctx.font = "10px sans-serif";
|
|||
|
ctx.fillStyle = dcolor();
|
|||
|
ctx.fillText("x:" + this.x().toFixed(1), 0, 0);
|
|||
|
ctx.translate(0, 10);
|
|||
|
ctx.fillText("y:" + this.y().toFixed(1), 0, 0);
|
|||
|
if (this.speed_x() != 0 || this.speed_y() != 0) {
|
|||
|
ctx.translate(0, 10);
|
|||
|
ctx.fillText("vx:" + this.speed_x().toFixed(1), 0, 0);
|
|||
|
ctx.translate(0, 10);
|
|||
|
ctx.fillText("vy:" + this.speed_y().toFixed(1), 0, 0);
|
|||
|
}
|
|||
|
|
|||
|
// draw capsule
|
|||
|
ctx.restore();
|
|||
|
ctx.save();
|
|||
|
ctx.strokeStyle = "green";
|
|||
|
ctx.beginPath();
|
|||
|
var cap = this.capsule();
|
|||
|
ctx.moveTo(cap.x(), cap.y());
|
|||
|
ctx.lineWidth = 5;
|
|||
|
ctx.lineTo(cap.x() + cap.z(), cap.y() + cap.w());
|
|||
|
|
|||
|
ctx.lineWidth = 1;
|
|||
|
ctx.moveTo(this.x() + this.boundingMinX, this.y() + this.boundingMinY);
|
|||
|
ctx.lineTo(this.x() + this.boundingMaxX, this.y() + this.boundingMinY);
|
|||
|
ctx.lineTo(this.x() + this.boundingMaxX, this.y() + this.boundingMaxY);
|
|||
|
ctx.lineTo(this.x() + this.boundingMinX, this.y() + this.boundingMaxY);
|
|||
|
ctx.lineTo(this.x() + this.boundingMinX, this.y() + this.boundingMinY);
|
|||
|
|
|||
|
// draw center
|
|||
|
ctx.moveTo(this.x() - 3, this.y());
|
|||
|
ctx.lineTo(this.x() + 3, this.y());
|
|||
|
ctx.moveTo(this.x(), this.y() - 3);
|
|||
|
ctx.lineTo(this.x(), this.y() + 3);
|
|||
|
|
|||
|
ctx.stroke();
|
|||
|
}
|
|||
|
ctx.restore();
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
public computeBoundingBox() : void
|
|||
|
{
|
|||
|
var rx = this.radiusX();
|
|||
|
var ry = this.radiusY();
|
|||
|
if (this._angle == 0 || this._angle == 180) {
|
|||
|
this.boundingMinX = -rx;
|
|||
|
this.boundingMaxX = rx;
|
|||
|
this.boundingMinY = -ry;
|
|||
|
this.boundingMaxY = ry;
|
|||
|
return;
|
|||
|
} if (this._angle == 90 || this._angle == 270 || this._angle == -90) {
|
|||
|
this.boundingMinX = -ry;
|
|||
|
this.boundingMaxX = ry;
|
|||
|
this.boundingMinY = -rx;
|
|||
|
this.boundingMaxY = rx;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// rotate the 4 corners according to the rotation and figure out min/max values
|
|||
|
this.boundingMinX = Number.MAX_VALUE;
|
|||
|
this.boundingMaxX = Number.MIN_VALUE;
|
|||
|
this.boundingMinY = Number.MAX_VALUE;
|
|||
|
this.boundingMaxY = Number.MIN_VALUE;
|
|||
|
|
|||
|
var sine = Math.sin(Math.PI * this._angle / 180);
|
|||
|
var cosine = Math.cos(Math.PI * this._angle / 180);
|
|||
|
|
|||
|
// upper right corner
|
|||
|
this.updateBoundingX(this.rotateX(rx, -ry, sine, cosine));
|
|||
|
this.updateBoundingY(this.rotateY(rx, -ry, sine, cosine));
|
|||
|
|
|||
|
// lower right corner
|
|||
|
this.updateBoundingX(this.rotateX(rx, ry, sine, cosine));
|
|||
|
this.updateBoundingY(this.rotateY(rx, ry, sine, cosine));
|
|||
|
|
|||
|
// lower left corner
|
|||
|
this.updateBoundingX(this.rotateX(-rx, ry, sine, cosine));
|
|||
|
this.updateBoundingY(this.rotateY(-rx, ry, sine, cosine));
|
|||
|
|
|||
|
// upper left corner
|
|||
|
this.updateBoundingX(this.rotateX(-rx, -ry, sine, cosine));
|
|||
|
this.updateBoundingY(this.rotateY(-rx, -ry, sine, cosine));
|
|||
|
}
|
|||
|
|
|||
|
private rotateX(x : number, y : number, sine :number, cosine:number) : number
|
|||
|
{
|
|||
|
return x * cosine - y * sine;
|
|||
|
}
|
|||
|
private rotateY(x : number, y : number, sine : number, cosine : number) : number
|
|||
|
{
|
|||
|
return x * sine + y * cosine;
|
|||
|
}
|
|||
|
|
|||
|
private rotate(v: Vector2): Vector2 {
|
|||
|
var sine = Math.sin(Math.PI * this._angle / 180);
|
|||
|
var cosine = Math.cos(Math.PI * this._angle / 180);
|
|||
|
|
|||
|
var x = this.rotateX(v.x(), v.y(), sine, cosine);
|
|||
|
var y = this.rotateY(v.x(), v.y(), sine, cosine);
|
|||
|
return new Vector2(x, y);
|
|||
|
}
|
|||
|
|
|||
|
///
|
|||
|
/// Project p0-p1 onto d0 and subtract from p0-p1 to get vector
|
|||
|
/// returns the closest vector rooted at x1,y1 to the segment
|
|||
|
private minPointSegment(x0:number, y0:number, dx0:number, dy0:number, x1:number, y1:number) : Vector2 {
|
|||
|
var wx = x0 - x1;
|
|||
|
var wy = y0 - y1;
|
|||
|
var d = dx0 * dx0 + dy0 * dy0;
|
|||
|
if (Math.abs(d) < 0.00001) { // zero
|
|||
|
// point to point
|
|||
|
var result = new Vector2(wx, wy);
|
|||
|
(<any>result).from = 0;
|
|||
|
return result;
|
|||
|
}
|
|||
|
var from = 1;
|
|||
|
var t = -(wx * dx0 + wy * dy0) / d;
|
|||
|
if (t < 0) {
|
|||
|
t = 0;
|
|||
|
from = 2;
|
|||
|
}
|
|||
|
else if (t > 1) {
|
|||
|
t = 1;
|
|||
|
from = 3;
|
|||
|
}
|
|||
|
var result = new Vector2(wx + t * dx0, wy + t * dy0);
|
|||
|
(<any>result).from = from;
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Consider segments o0 + d0*s and o1 + d1*t, parametrized by s and t in [0,1]
|
|||
|
/// Either the segments overlap, i.e., we can find both s and t in the range [0,1], in which case, the
|
|||
|
/// closest segment is 0 length.
|
|||
|
///
|
|||
|
/// Otherwise the closest distance will be rooted at one of the four endpoints of these segments.
|
|||
|
/// Thus, compute 4 point/segment distances and pick the smallest one.
|
|||
|
/// </summary>
|
|||
|
private minConnectingSegment(x0: number, y0: number, dx0: number, dy0: number, x1: number, y1: number, dx1: number, dy1: number) : Vector4 {
|
|||
|
var b = dx0 * dy1 - dy0 * dx1;
|
|||
|
if (b != 0) {
|
|||
|
var wx = x0 - x1;
|
|||
|
var wy = y0 - y1;
|
|||
|
var d = dx0 * wy - dy0 * wx;
|
|||
|
var e = dx1 * wy - dy1 * wx;
|
|||
|
var t = d / b;
|
|||
|
var s = e / b;
|
|||
|
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
|
|||
|
var result = new Vector4(x0 + s * dx0, y0 + s * dy0, 0, 0);
|
|||
|
(<any>result).from = 32;
|
|||
|
return result;
|
|||
|
}
|
|||
|
}
|
|||
|
var v1 = this.minPointSegment(x0, y0, dx0, dy0, x1, y1);
|
|||
|
var v2 = this.minPointSegment(x0, y0, dx0, dy0, x1 + dx1, y1 + dy1);
|
|||
|
var v3 = this.minPointSegment(x1, y1, dx1, dy1, x0, y0);
|
|||
|
var v4 = this.minPointSegment(x1, y1, dx1, dy1, x0 + dx0, y0 + dy0);
|
|||
|
|
|||
|
var d1 = v1.length();
|
|||
|
var d2 = v2.length();
|
|||
|
var d3 = v3.length();
|
|||
|
var d4 = v4.length();
|
|||
|
|
|||
|
var m = Math.min(d1, d2, d3, d4);
|
|||
|
|
|||
|
if (m == d1) {
|
|||
|
var result = new Vector4(x1, y1, v1.x(), v1.y());
|
|||
|
(<any>result).from = (<any>v1).from + 4;
|
|||
|
return result;
|
|||
|
}
|
|||
|
if (m == d2) {
|
|||
|
var result = new Vector4(x1 + dx1, y1 + dy1, v2.x(), v2.y());
|
|||
|
(<any>result).from = (<any>v1).from + 8;
|
|||
|
return result;
|
|||
|
}
|
|||
|
if (m == d3) {
|
|||
|
var result = new Vector4(x0, y0, v3.x(), v3.y());
|
|||
|
(<any>result).from = (<any>v1).from + 12;
|
|||
|
return result;
|
|||
|
}
|
|||
|
//if (m == d4) {
|
|||
|
var result = new Vector4(x0 + dx0, y0 + dy0, v4.x(), v4.y());
|
|||
|
(<any>result).from = (<any>v1).from + 16;
|
|||
|
return result;
|
|||
|
//}
|
|||
|
}
|
|||
|
|
|||
|
private updateBoundingX(newX : number) : void
|
|||
|
{
|
|||
|
this.boundingMaxX = Math.max(this.boundingMaxX, newX);
|
|||
|
this.boundingMinX = Math.min(this.boundingMinX, newX);
|
|||
|
}
|
|||
|
|
|||
|
private updateBoundingY(newY : number) : void
|
|||
|
{
|
|||
|
this.boundingMaxY = Math.max(this.boundingMaxY, newY);
|
|||
|
this.boundingMinY = Math.min(this.boundingMinY, newY);
|
|||
|
}
|
|||
|
|
|||
|
private bbRadius(unitNormal : Vector2) : number
|
|||
|
{
|
|||
|
// rotate unitNormal into rotation of box of this sprite by adding -angle to it
|
|||
|
var angle = Math.atan(unitNormal.y()/unitNormal.x());
|
|||
|
angle = angle - Math.PI * this._angle / 180;
|
|||
|
|
|||
|
// find intersect with horizontals of bb
|
|||
|
var tan = Math.tan(angle);
|
|||
|
var horiz = Math.abs(this.radiusY()/tan);
|
|||
|
if (horiz <= this.radiusX()) {
|
|||
|
// dist to bb
|
|||
|
return Math.abs(this.radiusY()/Math.sin(angle));
|
|||
|
}
|
|||
|
// find intersect with verticals of bb
|
|||
|
var vert = Math.abs(this.radiusX() * tan);
|
|||
|
if (vert > this.radiusY()) {
|
|||
|
debugger; // something is wrong
|
|||
|
}
|
|||
|
return Math.abs(this.radiusX()/Math.cos(angle));
|
|||
|
}
|
|||
|
|
|||
|
public radius(unitNormal : Vector2) : number
|
|||
|
{
|
|||
|
// TODO: Explain where this funky dot product came from...
|
|||
|
// return Math.abs(self.boundingMaxX * unitNormal.x() + this.boundingMaxY * unitNormal.y());
|
|||
|
|
|||
|
// Approximate as ellipse
|
|||
|
var angle = Math.atan(unitNormal.y() / unitNormal.x());
|
|||
|
angle = angle - Math.PI * this._angle / 180;
|
|||
|
|
|||
|
var sin = Math.sin(angle);
|
|||
|
var cos = Math.cos(angle);
|
|||
|
|
|||
|
// Detect ellipse axis rotation
|
|||
|
var majorAxis:number, majorAxisMult:number;
|
|||
|
var minorAxis:number, minorAxisMult:number;
|
|||
|
if (this.width >= this.height) {
|
|||
|
majorAxis = this.radiusX();
|
|||
|
minorAxis = this.radiusY();
|
|||
|
majorAxisMult = sin;
|
|||
|
minorAxisMult = cos;
|
|||
|
} else {
|
|||
|
majorAxis = this.radiusY();
|
|||
|
minorAxis = this.radiusX();
|
|||
|
majorAxisMult = cos;
|
|||
|
minorAxisMult = -sin;
|
|||
|
}
|
|||
|
|
|||
|
var rad = (majorAxis * minorAxis) / (Math.sqrt(majorAxis * majorAxis * majorAxisMult * majorAxisMult + minorAxis * minorAxis * minorAxisMult * minorAxisMult));
|
|||
|
|
|||
|
return rad;
|
|||
|
}
|
|||
|
|
|||
|
private radiusX() : number { return this.width() / 2; }
|
|||
|
private radiusY() : number { return this.height() / 2; }
|
|||
|
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Used during a time step to determine collisions etc.
|
|||
|
/// </summary>
|
|||
|
public stepDisplacement() : Vector2
|
|||
|
{
|
|||
|
return this.newPosition.subtract(this._position);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Apply gravity and user applied force and also repulsive forces due to touching of walls/other objects
|
|||
|
/// </summary>
|
|||
|
private computeForces(positionSpeed : Vector4) : Vector2
|
|||
|
{
|
|||
|
var force = (this._parent.gravity().add(this._acceleration)).scale(this.mass());
|
|||
|
|
|||
|
for (var i = 0; i < this._springs.length; i++)
|
|||
|
{
|
|||
|
var spring = this._springs[i];
|
|||
|
force = force.add(spring.forceOn(this));
|
|||
|
}
|
|||
|
|
|||
|
// compute repulsive forces from walls
|
|||
|
for (var i = 0; i < this.normalTouchPoints.length; i++)
|
|||
|
{
|
|||
|
var unitNormal = this.normalTouchPoints[i];
|
|||
|
if (Vector2.dot(unitNormal, force) > 0) continue; // not pointing into the wall
|
|||
|
if (Vector2.dot(unitNormal, new Vector2(positionSpeed.z(), positionSpeed.w())) > 0) continue; // speeding away from wall.
|
|||
|
var unitParallel = unitNormal.rotate90Left();
|
|||
|
var proj = unitParallel.scale(Vector2.dot(force, unitParallel));
|
|||
|
force = proj;
|
|||
|
}
|
|||
|
if (Math.abs(force.x()) < 0.1) force = new Vector2(0, force.y());
|
|||
|
if (Math.abs(force.y()) < 0.1) force = new Vector2(force.x(), 0);
|
|||
|
return force;
|
|||
|
}
|
|||
|
|
|||
|
private isEqualToEpsilon(x:number, p:number) : boolean
|
|||
|
{
|
|||
|
return (Math.round((x - p) / 2) == 0.0);
|
|||
|
}
|
|||
|
|
|||
|
private derivativePosAndSpeed(dT:number, positionSpeed:Vector4) : Vector4
|
|||
|
{
|
|||
|
var accel = this.computeForces(positionSpeed).scale(1 / this.mass());
|
|||
|
|
|||
|
// apply friction directly (instead of as a force)
|
|||
|
|
|||
|
return new Vector4((positionSpeed.z() + dT * accel.x()),
|
|||
|
(positionSpeed.w() + dT * accel.y()),
|
|||
|
accel.x(), accel.y());
|
|||
|
}
|
|||
|
|
|||
|
private actualFriction() : number
|
|||
|
{
|
|||
|
if (isNaN(this._friction)) return this._parent._worldFriction;
|
|||
|
return this._friction;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
private RungaKutta(dT:number): Vector4
|
|||
|
{
|
|||
|
var yi = Vector4.fromV2V2(this._position, this._speed);
|
|||
|
var u1 = this.derivativePosAndSpeed(0, yi).scale(dT);
|
|||
|
var u2 = this.derivativePosAndSpeed(dT / 2, yi.add(u1.scale(.5))).scale(dT);
|
|||
|
var u3 = this.derivativePosAndSpeed(dT / 2, yi.add(u2.scale(.5))).scale(dT);
|
|||
|
var u4 = this.derivativePosAndSpeed(dT, yi.add(u3)).scale(dT);
|
|||
|
|
|||
|
var avg = (u1.add(u2.scale(2)).add(u3.scale(2)).add(u4)).scale(1/6);
|
|||
|
|
|||
|
// clean accel
|
|||
|
var nz = avg.z();
|
|||
|
if (avg.z() < 0.1 && avg.z() > -0.1) nz = 0;
|
|||
|
var nw = avg.w();
|
|||
|
if (avg.w() < 0.1 && avg.w() > -0.1) nw = 0;
|
|||
|
|
|||
|
var nx = avg.x() * (1 - this.actualFriction());
|
|||
|
var ny = avg.y() * (1 - this.actualFriction());
|
|||
|
avg = new Vector4(nx,ny,nz,nw);
|
|||
|
|
|||
|
this.midSpeed = new Vector2(avg.x() / dT, avg.y() / dT);
|
|||
|
|
|||
|
var yip1 = yi.add(avg);
|
|||
|
|
|||
|
// apply friction directly (instead of as a force)
|
|||
|
yip1 = yip1.withW(yip1.w() * (1 - this.actualFriction()))
|
|||
|
yip1 = yip1.withZ(yip1.z() * (1 - this.actualFriction()));
|
|||
|
return yip1;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Make a time step.
|
|||
|
/// - Uses speed, position to create newSpeed and newPosition
|
|||
|
///
|
|||
|
/// CommitUpdate moves the newPosition, newSpeed into position, speed, thereby finalizing it.
|
|||
|
///
|
|||
|
/// Use Euler midpoint method.
|
|||
|
///
|
|||
|
/// This method must be IDEMPOTENT so we can call it a few times during a step with different partial time steps.
|
|||
|
/// </summary>
|
|||
|
public update(dT:number):void
|
|||
|
{
|
|||
|
TDev.Contract.Requires(dT >= 0);
|
|||
|
|
|||
|
if (!this._parent) return;
|
|||
|
var yip1 = this.RungaKutta(dT);
|
|||
|
// compute final displacement (position is updated after collision detection)
|
|||
|
this.newPosition = new Vector2(yip1.x(), yip1.y());
|
|||
|
this.newSpeed = new Vector2(yip1.z(), yip1.w());
|
|||
|
this.newRotation = this._angle + this._angular_speed * dT;
|
|||
|
}
|
|||
|
F
|
|||
|
public commitUpdate(rt : Runtime, dT : number): void
|
|||
|
{
|
|||
|
if (!this._lastPosition || !this._lastPosition.equals(this.newPosition) || this._angle != this.newRotation)
|
|||
|
this.changed();
|
|||
|
this._position = this.newPosition;
|
|||
|
this._angle = this.newRotation;
|
|||
|
this._speed = this.newSpeed;
|
|||
|
if (this._animations) {
|
|||
|
var anyDone = false;
|
|||
|
this._animations.forEach(anim => anyDone = !anim.evolve(rt, dT) || anyDone);
|
|||
|
// cleanup on demand
|
|||
|
if (anyDone) {
|
|||
|
this._animations = this._animations.filter(anim => anim.isActive);
|
|||
|
if (this._animations.length == 0) this._animations = undefined;
|
|||
|
}
|
|||
|
}
|
|||
|
this.computeBoundingBox();
|
|||
|
}
|
|||
|
|
|||
|
//? Hide sprite.
|
|||
|
//@ writesMutable
|
|||
|
public hide() : void {
|
|||
|
this._hidden = true;
|
|||
|
}
|
|||
|
|
|||
|
//? Returns true if sprite is not hidden
|
|||
|
//@ readsMutable
|
|||
|
public is_visible() : boolean {
|
|||
|
return !this._hidden;
|
|||
|
}
|
|||
|
|
|||
|
//? Moves sprite.
|
|||
|
//@ writesMutable
|
|||
|
public move(delta_x:number, delta_y:number) : void {
|
|||
|
this._position = new Vector2(this._position._x + delta_x, this._position._y + delta_y);
|
|||
|
}
|
|||
|
|
|||
|
//? Moves sprite towards other sprite.
|
|||
|
//@ writesMutable [other].readsMutable
|
|||
|
//@ [fraction].defl(1)
|
|||
|
public move_towards(other:Sprite, fraction:number) : void
|
|||
|
{
|
|||
|
var center1 = this._position;
|
|||
|
var center2 = other._position;
|
|||
|
var dir = center2.subtract(center1).scale(fraction);
|
|||
|
this.move(dir._x, dir._y);
|
|||
|
}
|
|||
|
|
|||
|
public capsule(): Vector4 {
|
|||
|
var s0x, s0y, s1x, s1y, d0x, d0y, d1x, d1y;
|
|||
|
if (this._width > this._height) {
|
|||
|
d0x = (this._width - this._height);
|
|||
|
d0y = 0;
|
|||
|
s0x = -d0x / 2;
|
|||
|
s0y = 0;
|
|||
|
}
|
|||
|
else {
|
|||
|
d0x = 0;
|
|||
|
d0y = (this._height - this._width);
|
|||
|
s0x = 0;
|
|||
|
s0y = -d0y / 2;
|
|||
|
}
|
|||
|
var d0 = this.rotate(new Vector2(d0x, d0y));
|
|||
|
var s0 = this.rotate(new Vector2(s0x, s0y));
|
|||
|
return new Vector4(this.x() + s0.x(), this.y() + s0.y(), d0.x(), d0.y());
|
|||
|
}
|
|||
|
|
|||
|
public capsuleRadius(): number {
|
|||
|
if (this._width < this._height) {
|
|||
|
return this._width / 2;
|
|||
|
}
|
|||
|
return this._height / 2;
|
|||
|
}
|
|||
|
|
|||
|
//? Do the sprites overlap
|
|||
|
//@ readsMutable [other].readsMutable
|
|||
|
public overlaps_with(other:Sprite) : boolean {
|
|||
|
if (!this._parent) return false;
|
|||
|
if (!other._parent) return false;
|
|||
|
|
|||
|
if (isNaN(this.x()) || isNaN(this.y()) || isNaN(other.x()) || isNaN(other.y())) return false;
|
|||
|
if (this.x() + this.boundingMaxX <= other.x() + other.boundingMinX) return false;
|
|||
|
if (this.x() + this.boundingMinX >= other.x() + other.boundingMaxX) return false;
|
|||
|
if (this.y() + this.boundingMaxY <= other.y() + other.boundingMinY) return false;
|
|||
|
if (this.y() + this.boundingMinY >= other.y() + other.boundingMaxY) return false;
|
|||
|
|
|||
|
// capsule-capsule intersection
|
|||
|
var cap1 = this.capsule();
|
|||
|
var cap2 = other.capsule();
|
|||
|
|
|||
|
//var dist = this.segmentSegmentDistanceSquared(cap1.x(), cap1.y(), cap1.z(), cap1.w(), cap2.x(), cap2.y(), cap2.z(), cap2.w());
|
|||
|
var seg = this.minConnectingSegment(cap1.x(), cap1.y(), cap1.z(), cap1.w(), cap2.x(), cap2.y(), cap2.z(), cap2.w());
|
|||
|
if (this._parent) {
|
|||
|
this._parent._minSegments.push(seg);
|
|||
|
}
|
|||
|
var radi = this.capsuleRadius() + other.capsuleRadius();
|
|||
|
var dist = seg.z() * seg.z() + seg.w() * seg.w();
|
|||
|
if (dist >= radi*radi) {
|
|||
|
(<any>seg).overlap = false;
|
|||
|
return false;
|
|||
|
}
|
|||
|
(<any>seg).overlap = true;
|
|||
|
return true;
|
|||
|
|
|||
|
var center1 = this._position;
|
|||
|
var center2 = other._position;
|
|||
|
var distVec = center2.subtract(center1);
|
|||
|
var dist = distVec.length();
|
|||
|
if (dist == 0) return true;
|
|||
|
var norm = distVec.normalize();
|
|||
|
|
|||
|
var radius1 = this.radius(norm);
|
|||
|
var radius2 = other.radius(norm);
|
|||
|
if (radius1 + radius2 >= dist) return true;
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
//? Returns the subset of sprites in the given set that overlap with sprite.
|
|||
|
//@ readsMutable [sprites].readsMutable
|
|||
|
public overlap_with(sprites:SpriteSet) : SpriteSet {
|
|||
|
if (!this._parent) return new SpriteSet();
|
|||
|
|
|||
|
return this._parent.overlapWithAny(this, sprites);
|
|||
|
}
|
|||
|
|
|||
|
//? Are these the same sprite
|
|||
|
//@ readsMutable [other].readsMutable
|
|||
|
public equals(other:Sprite) : boolean {
|
|||
|
return this === other;
|
|||
|
}
|
|||
|
|
|||
|
//? Updates picture on a picture sprite (if it is a picture sprite)
|
|||
|
//@ writesMutable picAsync
|
|||
|
public set_picture(pic:Picture, r:ResumeCtx) : void
|
|||
|
{
|
|||
|
if (this.spriteType != SpriteType.Picture) r.resume();
|
|||
|
else
|
|||
|
pic.loadFirst(r, () => {
|
|||
|
this.setPictureInternal(pic);
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
public setPictureInternal(pic:Picture):void
|
|||
|
{
|
|||
|
this._picture = pic;
|
|||
|
this._width = pic.widthSync();
|
|||
|
this._height = pic.heightSync();
|
|||
|
this.computeBoundingBox();
|
|||
|
this.contentChanged();
|
|||
|
}
|
|||
|
|
|||
|
//? The picture on a picture sprite (if it is a picture sprite)
|
|||
|
//@ readsMutable
|
|||
|
public picture(): Picture { return this._picture; }
|
|||
|
|
|||
|
//? Sets the position in pixels
|
|||
|
//@ writesMutable
|
|||
|
public set_pos(x:number, y:number) : void { this._position = new Vector2(x,y); }
|
|||
|
|
|||
|
//? Sets the speed in pixels/sec
|
|||
|
//@ writesMutable
|
|||
|
public set_speed(vx:number, vy:number) : void { this._speed = new Vector2(vx,vy); }
|
|||
|
|
|||
|
//? Show sprite.
|
|||
|
//@ writesMutable
|
|||
|
public show() : void {
|
|||
|
this._hidden = false;
|
|||
|
}
|
|||
|
|
|||
|
//? Sets sprite speed direction towards other sprite with given magnitude.
|
|||
|
//@ writesMutable [other].readsMutable
|
|||
|
public speed_towards(other:Sprite, magnitude:number) : void {
|
|||
|
var center1 = this._position;
|
|||
|
var center2 = other._position;
|
|||
|
var speed = center2.subtract(center1);
|
|||
|
speed = speed.normalize();
|
|||
|
speed = speed.scale(magnitude);
|
|||
|
|
|||
|
this._speed = speed;
|
|||
|
}
|
|||
|
|
|||
|
//? Sets the clipping area for an image sprite (if it is an image sprite)
|
|||
|
//@ writesMutable
|
|||
|
//@ [width].defl(48) [height].defl(48)
|
|||
|
public set_clip(left: number, top: number, width: number, height: number): void
|
|||
|
{
|
|||
|
if (this._picture
|
|||
|
&& isFinite(left) && isFinite(top) && isFinite(width) && isFinite(height))
|
|||
|
{
|
|||
|
this._frame = undefined;
|
|||
|
this._width = width;
|
|||
|
this._height = height;
|
|||
|
this._clip = [left, top, width, height];
|
|||
|
this.computeBoundingBox();
|
|||
|
this.contentChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private _frame : SpriteFrame;
|
|||
|
public setFrame(frame : SpriteFrame) {
|
|||
|
if (this._frame != frame) {
|
|||
|
this._frame = frame;
|
|||
|
if (this._width <= 0) this._width = frame.width;
|
|||
|
this._height = frame.width <= 0 ? frame.height : this._width / frame.width * frame.height;
|
|||
|
this._clip = [frame.x, frame.y, frame.width, frame.height];
|
|||
|
// this._angle = frame.rotated ? -90 : 0;
|
|||
|
this.computeBoundingBox();
|
|||
|
this.contentChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Use `Sprite Sheet` instead.
|
|||
|
//@ writesMutable obsolete
|
|||
|
//@ [x].defl(48)
|
|||
|
public move_clip(x:number, y:number) : void
|
|||
|
{
|
|||
|
if (this._clip && this._picture) {
|
|||
|
var left = (this._clip[0] + x) % this._picture.widthSync();
|
|||
|
if (left < 0) left += this._clip[2];
|
|||
|
else if (left + this._clip[2] > this._picture.widthSync()) left = 0;
|
|||
|
var top = (this._clip[1] + y) % this._picture.heightSync();
|
|||
|
if (top < 0) top += this._clip[3];
|
|||
|
else if (top + this._clip[3] > this._picture.heightSync()) top = 0;
|
|||
|
this._clip = [left, top, this._clip[2], this._clip[3]];
|
|||
|
this.computeBoundingBox();
|
|||
|
this.contentChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//? Delete sprite.
|
|||
|
//@ writesMutable
|
|||
|
public delete_() : void
|
|||
|
{
|
|||
|
if (! this._parent) {
|
|||
|
return;
|
|||
|
}
|
|||
|
this._parent.deleteSprite(this);
|
|||
|
this._parent = null;
|
|||
|
}
|
|||
|
|
|||
|
//? True if sprite is deleted.
|
|||
|
//@ readsMutable
|
|||
|
public is_deleted(): boolean {
|
|||
|
return !this._parent;
|
|||
|
}
|
|||
|
|
|||
|
// return true if x, y is within the sprite extent (rotated bounding box)
|
|||
|
public contains(x:number, y:number) : boolean {
|
|||
|
var diff = Vector2.mk(x, y).subtract(this._position);
|
|||
|
var norm = diff.normalize();
|
|||
|
var rad = this.bbRadius(norm);
|
|||
|
if (diff.length() <= rad) return true;
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
public addSpring(sp:Spring) : void
|
|||
|
{
|
|||
|
this._springs.push(sp);
|
|||
|
}
|
|||
|
|
|||
|
public removeSpring(sp:Spring) : void
|
|||
|
{
|
|||
|
var idx = this._springs.indexOf(sp);
|
|||
|
if (idx > -1)
|
|||
|
this._springs.splice(idx, 1);
|
|||
|
}
|
|||
|
|
|||
|
private _z_index : number = undefined;
|
|||
|
|
|||
|
//? Gets the z-index of the sprite
|
|||
|
//@ readsMutable
|
|||
|
public z_index() : number { return this._z_index; }
|
|||
|
|
|||
|
//? Sets the z-index of the sprite
|
|||
|
//@ writesMutable
|
|||
|
public set_z_index(zindex: number): void {
|
|||
|
if (this._z_index != zindex) {
|
|||
|
this._z_index = zindex;
|
|||
|
if (this._parent)
|
|||
|
this._parent.spritesChanged();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public createAnimation() : SpriteAnimation {
|
|||
|
var anim = new SpriteAnimation(this);
|
|||
|
return anim;
|
|||
|
}
|
|||
|
|
|||
|
public startAnimation(anim : SpriteAnimation) {
|
|||
|
Util.assert(anim._sprite == this);
|
|||
|
if(!this._animations) this._animations = [];
|
|||
|
this._animations.push(anim);
|
|||
|
}
|
|||
|
|
|||
|
//? Starts a new tween animation.
|
|||
|
public create_animation() : SpriteAnimation {
|
|||
|
var anim = this.createAnimation();
|
|||
|
this.startAnimation(anim);
|
|||
|
return anim;
|
|||
|
}
|
|||
|
|
|||
|
public debuggerChildren() {
|
|||
|
return {
|
|||
|
'Z-index': this.z_index(),
|
|||
|
Friction: this.friction(),
|
|||
|
'Angular speed': this.angular_speed(),
|
|||
|
Angle: this.angle(),
|
|||
|
Elasticity: this.elasticity(),
|
|||
|
'Speed X': this.speed_x(),
|
|||
|
'Speed Y': this.speed_y(),
|
|||
|
X: this.x(),
|
|||
|
Y: this.y(),
|
|||
|
Color: this.color(),
|
|||
|
Opacity: this.opacity(),
|
|||
|
Text: this.text(),
|
|||
|
Picture: this.picture(),
|
|||
|
Mass: this.mass(),
|
|||
|
'Acceleration X': this.acceleration_x(),
|
|||
|
'Acceleration Y': this.acceleration_y(),
|
|||
|
Visible: this.is_visible(),
|
|||
|
};
|
|||
|
}
|
|||
|
}
|
|||
|
}
|