1095 строки
41 KiB
TypeScript
1095 строки
41 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
|
|
module TDev { export module RT {
|
|
//? A board to build 2D games
|
|
//@ icon("gameboard") ctx(general,gckey,enumerable)
|
|
export class Board
|
|
extends RTValue
|
|
{
|
|
private _landscape: boolean;
|
|
public _width:number;
|
|
public _height:number;
|
|
public _full: boolean;
|
|
public scaleFactor = 1.0;
|
|
private container:HTMLElement;
|
|
private canvas:HTMLCanvasElement;
|
|
private ctx : CanvasRenderingContext2D;
|
|
private sprites: Sprite[] = [];
|
|
private _orderedSprites: Sprite[];
|
|
public backgroundColor:Color = null;
|
|
private backgroundPicture:Picture = null;
|
|
private backgroundCamera: Camera = null;
|
|
private _boundaryDistance : number = NaN;
|
|
private _everyFrameTimer : Timer = undefined;
|
|
|
|
constructor() {
|
|
super()
|
|
}
|
|
private _gravity : Vector2 = new Vector2(0,0);
|
|
public _worldFriction : number = 0;
|
|
private _debugMode : boolean = false;
|
|
private _lastUpdateMS : number = 0; // no serialization, relative time since board initialized.
|
|
private _lastTimeDelta : number = 0; // no serialization
|
|
public _startTime : number = 0; // no serialization
|
|
private _touched : boolean = false; // no serialization
|
|
private _touchStart : Vector3 = Vector3.mk(0,0,0); // no serialization
|
|
private _touchCurrent : Vector3 = Vector3.mk(0,0,0); // no serialization
|
|
private _touchEnd : Vector3 = Vector3.mk(0,0,0); // no serialization
|
|
private _touchVelocity : Vector3 = Vector3.mk(0,0,0); // no serialization
|
|
private _touchedSpriteStack: Sprite[];
|
|
private _touchLast: Vector3;
|
|
private _runtime:Runtime;
|
|
|
|
/// <summary>
|
|
/// for debugging only
|
|
/// </summary>
|
|
public _minSegments: Vector4[] = [];
|
|
|
|
/// <summary>
|
|
/// Constructed on demand from obstacles and in the constructor. No serialization
|
|
/// </summary>
|
|
private _walls : WallSegment[] = []; // not serialized
|
|
private _obstacles : Obstacle[] = [];
|
|
private _springs: Spring[] = [];
|
|
private _backgroundScene: BoardBackgroundScene = undefined;
|
|
|
|
static mk(rt:Runtime, landscape : boolean, w:number, h:number, full:boolean)
|
|
{
|
|
var b = new Board();
|
|
b._landscape = landscape;
|
|
b._width = w;
|
|
b._height = h;
|
|
b._full = full;
|
|
b._startTime = rt.currentTime();
|
|
b.backgroundColor = Colors.transparent();
|
|
b.init(rt);
|
|
return b;
|
|
}
|
|
|
|
public init(rt:Runtime)
|
|
{
|
|
this._runtime = rt;
|
|
this.canvas = <HTMLCanvasElement> document.createElement("canvas");
|
|
this.canvas.className = "boardCanvas";
|
|
this.updateScaleFactor();
|
|
this.container = div("boardContainer", this.canvas);
|
|
this.ctx = this.canvas.getContext("2d");
|
|
this.container.setChildren([this.canvas]);
|
|
(<any>this.container).updateSizes = () => {
|
|
this.updateScaleFactor();
|
|
this.redrawBoardAndContents();
|
|
};
|
|
|
|
var handler = new TouchHandler(this.canvas, (e,x,y) => { this.touchHandler(e, x, y); });
|
|
|
|
}
|
|
|
|
private updateScaleFactor()
|
|
{
|
|
Util.assert(!!this._runtime);
|
|
if (!this._runtime) return;
|
|
|
|
var s0:number;
|
|
var s1:number;
|
|
|
|
if (this._full) {
|
|
s0 = this._runtime.host.fullWallWidth() / this._width;
|
|
s1 = this._runtime.host.fullWallHeight() / this._height;
|
|
} else {
|
|
var w = this._runtime.host.wallWidth;
|
|
if (this.container && this.container.offsetWidth)
|
|
w = this.container.offsetWidth;
|
|
s0 = w / this._width;
|
|
s1 = this._runtime.host.wallHeight / this._height;
|
|
}
|
|
|
|
if (s0 > s1) s0 = s1;
|
|
|
|
var ww = this._width * s0;
|
|
var hh = this._height * s0;
|
|
|
|
this.scaleFactor = s0;
|
|
this.canvas.width = ww * SizeMgr.devicePixelRatio;
|
|
this.canvas.height = hh * SizeMgr.devicePixelRatio;
|
|
this.canvas.style.width = ww + "px";
|
|
this.canvas.style.height = hh + "px";
|
|
|
|
if (this._full) {
|
|
var topMargin = (this._runtime.host.fullWallHeight() - hh) / 2;
|
|
this.canvas.style.marginTop = topMargin + "px";
|
|
}
|
|
}
|
|
|
|
private swipeThreshold = 10;
|
|
private dragThreshold = 5;
|
|
|
|
private onTap: Event_ = new Event_();
|
|
private onTouchDown: Event_ = new Event_();
|
|
private onTouchUp: Event_ = new Event_();
|
|
private onSwipe: Event_ = new Event_();
|
|
|
|
private _prevTouchTime : number;
|
|
private _touchDeltaTime : number;
|
|
private _touchPrevious : Vector3 = Vector3.mk(0,0,0); // no serialization
|
|
private _touchDirection : Vector3 = Vector3.mk(0,0,0); // no serialization
|
|
|
|
private touchHandler(e:string, x:number, y:number) : void {
|
|
Util.assert(!!this._runtime);
|
|
if (!this._runtime) return;
|
|
|
|
x = Math.round(x / this.scaleFactor);
|
|
y = Math.round(y / this.scaleFactor);
|
|
switch (e) {
|
|
case "down":
|
|
this._touched = true;
|
|
this._touchPrevious = this._touchCurrent = this._touchLast = this._touchStart = Vector3.mk(x, y, 0);
|
|
this._touchedSpriteStack = this.findTouchedSprites(x, y);
|
|
this._prevTouchTime = this._runtime.currentTime();
|
|
this._touchDeltaTime = 0;
|
|
this._touchDirection = Vector3.mk(0, 0, 0);
|
|
this.queueTouchDown(this._touchedSpriteStack, [x, y]);
|
|
this._runtime.queueBoardEvent(["touch down: "], [this], [x, y]);
|
|
if (!!this._touchedSpriteStack) {
|
|
this._runtime.queueBoardEvent(["touch over "], this._touchedSpriteStack, [x, y], true, true);
|
|
}
|
|
break;
|
|
case "move":
|
|
this._touchCurrent = Vector3.mk(x, y, 0);
|
|
var now = this._runtime.currentTime();
|
|
var deltaMove = this._touchCurrent.subtract(this._touchPrevious);
|
|
var deltaTime = now - this._prevTouchTime;
|
|
if (deltaTime > 50 || deltaMove.length() > 20) {
|
|
this._touchDirection = deltaMove;
|
|
this._touchDeltaTime = deltaTime;
|
|
}
|
|
if (!!this._touchedSpriteStack) {
|
|
var dist = this._touchCurrent.subtract(this._touchLast);
|
|
if (dist.length() > this.dragThreshold) {
|
|
this._touchLast = this._touchCurrent;
|
|
this.queueDrag(this._touchedSpriteStack, [x, y, dist._x, dist._y]);
|
|
this._runtime.queueBoardEvent(["drag sprite in ", "drag sprite: "], this._touchedSpriteStack, [x, y, dist._x, dist._y]);
|
|
}
|
|
}
|
|
var currentStack = this.findTouchedSprites(x, y);
|
|
if (!!currentStack) {
|
|
this._runtime.queueBoardEvent(["touch over "], currentStack, [x, y], true, true);
|
|
}
|
|
break;
|
|
case "up":
|
|
var currentPoint = Vector3.mk(x, y, 0);
|
|
this._touchEnd = this._touchCurrent = currentPoint;
|
|
this._touched = false;
|
|
if (this._touchDeltaTime > 0) {
|
|
this._touchVelocity = this._touchDirection.scale(1000 / this._touchDeltaTime);
|
|
}
|
|
else {
|
|
this._touchVelocity = Vector3.mk(0, 0, 0);
|
|
}
|
|
var dist = this._touchEnd.subtract(this._touchStart);
|
|
var stack: any[] = this._touchedSpriteStack;
|
|
if (!stack) { stack = []; }
|
|
stack.push(this); // add board
|
|
if (dist.length() > this.swipeThreshold) {
|
|
this.queueSwipe(this._touchedSpriteStack, [this._touchStart._x, this._touchStart._y, dist._x, dist._y]);
|
|
this._runtime.queueBoardEvent(["swipe sprite in ", "swipe sprite: ", "swipe board: "], stack,
|
|
[this._touchStart._x, this._touchStart._y, dist._x, dist._y]);
|
|
}
|
|
else {
|
|
this.queueTap(this._touchedSpriteStack, [x, y]);
|
|
this._runtime.queueBoardEvent(["tap sprite in ", "tap sprite: ", "tap board: "], stack, [x, y]);
|
|
}
|
|
this.queueTouchUp(this._touchedSpriteStack, [x, y]);
|
|
this._runtime.queueBoardEvent(["touch up: "], [this], [x, y]);
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
private queueDrag(stack: Sprite[], args): boolean {
|
|
if (!stack) return false;
|
|
for (var i = 0; i < stack.length; i++) {
|
|
var sprite = stack[i];
|
|
if (sprite instanceof Board) continue;
|
|
if (sprite.onDrag.handlers) {
|
|
this._runtime.queueLocalEvent(sprite.onDrag, args);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private queueTouchDown(stack: Sprite[], args): boolean {
|
|
if (stack && stack.length > 0) {
|
|
for (var i = 0; i < stack.length; i++) {
|
|
var sprite = stack[i];
|
|
if (sprite instanceof Board) continue;
|
|
if (sprite.onTouchDown.handlers) {
|
|
this._runtime.queueLocalEvent(sprite.onTouchDown, args);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (this.onTouchDown.handlers) {
|
|
this._runtime.queueLocalEvent(this.onTouchDown, args);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private queueTouchUp(stack: Sprite[], args): boolean {
|
|
if (stack && stack.length > 0) {
|
|
for (var i = 0; i < stack.length; i++) {
|
|
var sprite = stack[i];
|
|
if (sprite instanceof Board) continue;
|
|
if (sprite.onTouchUp.handlers) {
|
|
this._runtime.queueLocalEvent(sprite.onTouchUp, args);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (this.onTouchUp.handlers) {
|
|
this._runtime.queueLocalEvent(this.onTouchUp, args);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private queueTap(stack: Sprite[], args): boolean {
|
|
if (stack && stack.length > 0) {
|
|
for (var i = 0; i < stack.length; i++) {
|
|
var sprite = stack[i];
|
|
if (sprite instanceof Board) continue;
|
|
if (sprite.onTap.handlers) {
|
|
this._runtime.queueLocalEvent(sprite.onTap, args);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
if (this.onTap.handlers) {
|
|
this._runtime.queueLocalEvent(this.onTap, args);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private queueSwipe(stack: Sprite[], args): boolean {
|
|
if (stack && stack.length > 0) {
|
|
for (var i = 0; i < stack.length; i++) {
|
|
var sprite = stack[i];
|
|
if (sprite instanceof Board) continue;
|
|
if (sprite.onSwipe.handlers) {
|
|
this._runtime.queueLocalEvent(sprite.onSwipe, args);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (this.onSwipe.handlers) {
|
|
this._runtime.queueLocalEvent(this.onSwipe, args);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private findTouchedSprites(x:number, y:number) : Sprite[] {
|
|
var candidates = this.orderedSprites()
|
|
.filter(sp => !sp._hidden && sp.contains(x, y))
|
|
.reverse();
|
|
if (candidates.length == 0)
|
|
return undefined;
|
|
return candidates;
|
|
}
|
|
|
|
private applyBackground()
|
|
{
|
|
this.ctx.save();
|
|
this.ctx.clearRect(0, 0, this._width, this._height);
|
|
if (!!this.backgroundCamera) {
|
|
// TODO: display video element in div to start streaming
|
|
}
|
|
// it may not have a canvas when the picture is still loading and an resize event occurs
|
|
else if (!!this.backgroundPicture && this.backgroundPicture.hasCanvas()) {
|
|
this.ctx.drawImage(this.backgroundPicture.getCanvas(), 0, 0,
|
|
this.backgroundPicture.widthSync(), this.backgroundPicture.heightSync(), 0, 0, this._width, this._height);
|
|
} else if (!!this.backgroundColor) {
|
|
this.ctx.fillStyle = this.backgroundColor.toHtml();
|
|
this.ctx.fillRect(0, 0, this._width, this._height);
|
|
}
|
|
if (this._backgroundScene) this._backgroundScene.render(this._width, this._height, this.ctx);
|
|
this.ctx.restore();
|
|
}
|
|
|
|
//? Gets the height in pixels
|
|
public height() : number { return this._height; }
|
|
|
|
//? Gets the sprite count
|
|
//@ readsMutable
|
|
public count(): number { return this.sprites.length; }
|
|
|
|
public get_enumerator() { return this.sprites.slice(0); }
|
|
|
|
//? Gets the width in pixels
|
|
public width() : number { return this._width; }
|
|
|
|
//? True if board is touched
|
|
//@ tandre
|
|
public touched() : boolean {
|
|
return this._touched;
|
|
}
|
|
|
|
//? Last touch start point
|
|
//@ tandre
|
|
public touch_start() : Vector3 {
|
|
return this._touchStart;
|
|
}
|
|
|
|
//? Current touch point
|
|
//@ tandre
|
|
public touch_current() : Vector3 {
|
|
return this._touchCurrent;
|
|
}
|
|
|
|
//? Last touch end point
|
|
//@ tandre
|
|
public touch_end() : Vector3 {
|
|
return this._touchEnd;
|
|
}
|
|
|
|
//? Final touch velocity after touch ended
|
|
//@ tandre
|
|
public touch_velocity() : Vector3 {
|
|
return this._touchVelocity;
|
|
}
|
|
|
|
//? Create walls around the board at the given distance.
|
|
//@ writesMutable
|
|
public create_boundary(distance:number) : void
|
|
{
|
|
if (!isNaN(this._boundaryDistance)) return;
|
|
this._boundaryDistance = distance;
|
|
this.initializeCanvasBoundaries(distance);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call only after canvasHeight has been determined (in deserialize)
|
|
/// </summary>
|
|
private initializeCanvasBoundaries(distance:number):void
|
|
{
|
|
if (isNaN(distance)) return;
|
|
|
|
// add surrounding walls (orient counter-clock-wise)
|
|
this._walls.push(WallSegment.mk(-distance, -distance, this.width() + 2 * distance, 0, 1, 0));
|
|
this._walls.push(WallSegment.mk(this.width() + distance, -distance, 0, this.height() + 2 * distance, 1, 0));
|
|
this._walls.push(WallSegment.mk(this.width() + distance, this.height() + distance, -(this.width() + 2 * distance), 0, 1, 0));
|
|
this._walls.push(WallSegment.mk(-distance, this.height() + distance, 0, -(this.height() + 2 * distance), 1, 0));
|
|
}
|
|
|
|
private addObstacle(o : Obstacle): void
|
|
{
|
|
this._walls.push(WallSegment.mk(o.x, o.y, o.xextent, o.yextent, o.elasticity, o.friction, o));
|
|
this._walls.push(WallSegment.mk(o.x + o.xextent, o.y + o.yextent, -o.xextent, -o.yextent, o.elasticity, o.friction, o));
|
|
this._obstacles.push(o);
|
|
}
|
|
|
|
//? Create a new collection for sprites.
|
|
public create_sprite_set() : SpriteSet {
|
|
return new SpriteSet();
|
|
}
|
|
|
|
public deleteSprite(sprite : Sprite) : void {
|
|
var idx = this.sprites.indexOf(sprite);
|
|
if (idx < 0) return;
|
|
this.sprites.splice(idx, 1);
|
|
this.spritesChanged();
|
|
}
|
|
|
|
public spritesChanged() {
|
|
this._orderedSprites = undefined;
|
|
}
|
|
|
|
//? gets the timer that fires for every display frame.
|
|
public frame_timer(s : IStackFrame): Timer {
|
|
if(!this._everyFrameTimer) this._everyFrameTimer = new Timer(s.rt, 0.02, false);
|
|
return this._everyFrameTimer;
|
|
}
|
|
|
|
//? add an action that fires for every display frame.
|
|
//@ ignoreReturnValue
|
|
public add_on_every_frame(perform: Action, s: IStackFrame): EventBinding {
|
|
return this.on_every_frame(perform, s);
|
|
}
|
|
|
|
//? add an action that fires for every display frame.
|
|
//@ ignoreReturnValue
|
|
public on_every_frame(perform: Action, s: IStackFrame): EventBinding {
|
|
return this.frame_timer(s).on_trigger(perform);
|
|
}
|
|
|
|
//? Stops and clears all the `every frame` timers
|
|
public clear_every_frame_timers() {
|
|
if (this._everyFrameTimer) {
|
|
this._everyFrameTimer.clear();
|
|
this._everyFrameTimer = undefined;
|
|
this._everyFrameOnSprite = false;
|
|
}
|
|
}
|
|
|
|
//? set the handler that is invoked when the board is tapped
|
|
//@ ignoreReturnValue
|
|
//@ writesMutable
|
|
public on_tap(tapped: PositionAction) : EventBinding {
|
|
return this.onTap.addHandler(tapped);
|
|
}
|
|
|
|
//? set the handler that is invoked when the board is swiped
|
|
//@ ignoreReturnValue
|
|
//@ writesMutable
|
|
public on_swipe(swiped: VectorAction) : EventBinding {
|
|
return this.onSwipe.addHandler(swiped);
|
|
}
|
|
|
|
//? set the handler that is invoked when the board is touched
|
|
//@ writesMutable
|
|
//@ ignoreReturnValue
|
|
public on_touch_down(touch_down: PositionAction) : EventBinding {
|
|
return this.onTouchDown.addHandler(touch_down);
|
|
}
|
|
|
|
//? set the handler that is invoked when the board touch is released
|
|
//@ writesMutable
|
|
//@ ignoreReturnValue
|
|
public on_touch_up(touch_up: PositionAction) : EventBinding {
|
|
return this.onTouchUp.addHandler(touch_up);
|
|
}
|
|
|
|
public tick: number = 0;
|
|
//? Update positions of sprites on board.
|
|
//@ timestamp
|
|
//@ writesMutable
|
|
public evolve() : void
|
|
{
|
|
Util.assert(!!this._runtime);
|
|
if (!this._runtime) return;
|
|
|
|
this.tick++; if (isNaN(this.tick)) this.tick = 0;
|
|
var now = this._runtime.currentTime();
|
|
var newDelta = this._lastTimeDelta = (now - this._startTime) - this._lastUpdateMS;
|
|
//if (newDelta === undefined || newDelta < 0) {
|
|
// throw new Error("negative dt");
|
|
//}
|
|
this._lastUpdateMS += newDelta;
|
|
var dT = Math_.clamp(0, 0.2, newDelta / 1000);
|
|
this.sprites.forEach(sprite => sprite.update(dT));
|
|
this.detectCollisions(dT);
|
|
this.sprites.forEach(sprite => sprite.commitUpdate(this._runtime, dT));
|
|
}
|
|
|
|
private detectCollisions(dT:number):void
|
|
{
|
|
// detect wall collisions
|
|
for (var i = 0; i < this.sprites.length; i++)
|
|
{
|
|
var s = this.sprites[i];
|
|
if (!!s._location) continue;
|
|
this.detectWallCollision(s, dT);
|
|
}
|
|
}
|
|
|
|
private detectWallCollision(sprite:Sprite, dT:number):void
|
|
{
|
|
sprite.normalTouchPoints.clear(); // this means clear the array!
|
|
|
|
for (var i = 0; i < this._walls.length; i++)
|
|
{
|
|
var wall = this._walls[i];
|
|
if(wall.processPotentialCollision(sprite, dT) && wall._obstacle)
|
|
wall._obstacle.raiseCollision(this._runtime, sprite);
|
|
}
|
|
// do it twice to get corners right
|
|
for (var i = 0; i < this._walls.length; i++)
|
|
{
|
|
var wall = this._walls[i];
|
|
if(wall.processPotentialCollision(sprite, dT) && wall._obstacle)
|
|
wall._obstacle.raiseCollision(this._runtime, sprite);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public updateViewCore(s: IStackFrame, b: BoxBase) {
|
|
|
|
if (b instanceof WallBox)
|
|
(<WallBox> b).fullScreen = this._full;
|
|
this.redrawBoardAndContents();
|
|
}
|
|
|
|
public getViewCore(s:IStackFrame, b:BoxBase) : HTMLElement
|
|
{
|
|
// called when board gets posted
|
|
this._touched = false; // clear any past touches that were not lifted
|
|
|
|
return this.container;
|
|
}
|
|
|
|
//? Checks if the board is the same instance as the other board.
|
|
public equals(other_board: Board): boolean {
|
|
return this == other_board;
|
|
}
|
|
|
|
//? Gets the background scene
|
|
//@ readsMutable
|
|
public background_scene(): BoardBackgroundScene {
|
|
if (!this._backgroundScene)
|
|
this._backgroundScene = new BoardBackgroundScene(this);
|
|
return this._backgroundScene;
|
|
}
|
|
|
|
//? Sets the background color
|
|
//@ writesMutable
|
|
//@ [color].deflExpr('colors->random')
|
|
public set_background(color:Color) : void
|
|
{
|
|
this.backgroundCamera = null;
|
|
this.backgroundColor = color;
|
|
this.backgroundPicture = null;
|
|
}
|
|
|
|
//? Sets the background camera
|
|
//@ writesMutable
|
|
//@ cap(camera)
|
|
//@ [camera].deflExpr('senses->camera')
|
|
public set_background_camera(camera: Camera): void
|
|
{
|
|
this.backgroundCamera = camera;
|
|
this.backgroundColor = null;
|
|
this.backgroundPicture = null;
|
|
}
|
|
|
|
//? Sets the background picture
|
|
//@ writesMutable picAsync
|
|
//@ embedsLink("Board", "Picture")
|
|
public set_background_picture(picture:Picture, r:ResumeCtx) : void
|
|
{
|
|
this.backgroundCamera = null;
|
|
this.backgroundColor = null;
|
|
this.backgroundPicture = picture;
|
|
picture.loadFirst(r, null);
|
|
}
|
|
|
|
//? In debug mode, board displays speed and other info of sprites
|
|
//@ [debug].defl(true)
|
|
public set_debug_mode(debug:boolean) : void {
|
|
this._debugMode = debug;
|
|
}
|
|
|
|
//? Sets the default friction for sprites to a fraction of speed loss between 0 and 1
|
|
//@ writesMutable
|
|
//@ [friction].defl(0.01)
|
|
public set_friction(friction:number) : void
|
|
{
|
|
this._worldFriction = friction;
|
|
}
|
|
|
|
//? Gets a value indicating if the board is designed to be viewed in landscape mode
|
|
public is_landscape(): boolean
|
|
{
|
|
return this._landscape;
|
|
}
|
|
|
|
//? Sets the uniform acceleration vector for objects on the board to pixels/sec^2
|
|
//@ writesMutable [y].defl(200)
|
|
public set_gravity(x:number, y:number) : void
|
|
{
|
|
this._gravity = new Vector2(x, y);
|
|
}
|
|
|
|
public gravity() : Vector2 { return this._gravity; }
|
|
|
|
//? Gets the sprite indexed by i
|
|
//@ readsMutable
|
|
public at(i:number) : Sprite { return this.sprites[i]; }
|
|
|
|
private initialX() : number { return this.width() / 2; }
|
|
private initialY() : number { return this.height() / 2; }
|
|
|
|
private _everyFrameOnSprite = false;
|
|
public enableEveryFrameOnSprite(s:IStackFrame)
|
|
{
|
|
if (this._everyFrameOnSprite) return;
|
|
this._everyFrameOnSprite = true;
|
|
var handler:any = (bot, prev) => {
|
|
var q = this._runtime.eventQ
|
|
var args = []
|
|
this.sprites.forEach(s => {
|
|
if (s.onEveryFrame.pendinghandlers == 0)
|
|
q.addLocalEvent(s.onEveryFrame, args)
|
|
})
|
|
bot.entryAddr = prev
|
|
return bot
|
|
};
|
|
this.on_every_frame(handler, s);
|
|
}
|
|
|
|
//? Make updates visible.
|
|
//@ writesMutable
|
|
public update_on_wall() : void
|
|
{
|
|
this.redrawBoardAndContents();
|
|
}
|
|
|
|
private orderedSprites() : Sprite[] {
|
|
if (!this._orderedSprites) {
|
|
this._orderedSprites = this.sprites.slice(0);
|
|
this._orderedSprites.stableSort((a, b) => a.z_index() - b.z_index());
|
|
}
|
|
return this._orderedSprites;
|
|
}
|
|
|
|
private redrawBoardAndContents() : void
|
|
{
|
|
var isDebugMode = this._debugMode && (this._runtime && !this._runtime.currentScriptId);
|
|
|
|
this.ctx.save();
|
|
var scale = this.scaleFactor * SizeMgr.devicePixelRatio;
|
|
this.ctx.scale(scale, scale);
|
|
|
|
this.applyBackground();
|
|
this.orderedSprites().forEach(s => s.redraw(this.ctx, isDebugMode));
|
|
this.renderObstacles();
|
|
if (isDebugMode) {
|
|
this.debugGrid();
|
|
this.debugSprings();
|
|
this.debugSegments();
|
|
}
|
|
this.ctx.restore();
|
|
}
|
|
|
|
public renderingContext() { return this.ctx; }
|
|
|
|
private debugGrid() {
|
|
this.ctx.save();
|
|
this.ctx.beginPath();
|
|
var w = this.width();
|
|
var h = this.height();
|
|
this.ctx.strokeStyle = "rgba(90, 90, 90, 0.7)";
|
|
this.ctx.fillStyle = "rgba(90, 90, 90, 0.7)";
|
|
this.ctx.lineWidth = 1;
|
|
this.ctx.font = "12px sans-serif";
|
|
// this.ctx.globalAlpha = 0.8;
|
|
for(var y = 0; y <= h; y += 100) {
|
|
this.ctx.moveTo(0, y);
|
|
this.ctx.lineTo(w, y);
|
|
if (y > 0 && y % 100 == 0)
|
|
this.ctx.fillText(y.toString(), 2, y - 5);
|
|
this.ctx.stroke();
|
|
}
|
|
for(var x = 0; x <= w; x += 100) {
|
|
this.ctx.moveTo(x, 0);
|
|
this.ctx.lineTo(x, h);
|
|
if (x > 0 && x % 100 == 0)
|
|
this.ctx.fillText(x.toString(), x - 15, 10);
|
|
this.ctx.stroke();
|
|
}
|
|
this.ctx.restore();
|
|
}
|
|
|
|
private debugSegments(): void {
|
|
this.ctx.save();
|
|
this.ctx.beginPath();
|
|
|
|
for (var i = 0; i < this._minSegments.length; ++i) {
|
|
var seg = this._minSegments[i];
|
|
|
|
if ((<any>seg).overlap) {
|
|
this.ctx.fillStyle = "green";
|
|
this.ctx.strokeStyle = "green";
|
|
}
|
|
else {
|
|
this.ctx.fillStyle = "red";
|
|
this.ctx.strokeStyle = "red";
|
|
}
|
|
this.ctx.font = "20px sans-serif";
|
|
|
|
this.ctx.lineWidth = 4;
|
|
this.ctx.moveTo(seg.x(), seg.y());
|
|
this.ctx.lineTo(seg.x() + seg.z(), seg.y() + seg.w());
|
|
this.ctx.fillText((<any>seg).from + "", seg.x() + seg.z(), seg.y() + seg.w());
|
|
}
|
|
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
if (this._minSegments.length > 0) {
|
|
debugger;
|
|
this._minSegments = [];
|
|
}
|
|
}
|
|
|
|
private debugSprings(): void {
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = "gray";
|
|
this.ctx.beginPath();
|
|
for (var i = 0; i < this._springs.length; i++) {
|
|
var o = this._springs[i];
|
|
|
|
this.ctx.moveTo(o.sprite1.x(), o.sprite1.y());
|
|
this.ctx.lineTo(o.sprite2.x(), o.sprite2.y());
|
|
}
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
|
|
}
|
|
|
|
private renderObstacles() : void
|
|
{
|
|
this.ctx.save();
|
|
for (var i = 0; i < this._obstacles.length; i++) {
|
|
var o = this._obstacles[i];
|
|
if (!o.isValid()) continue;
|
|
|
|
this.ctx.beginPath();
|
|
this.ctx.lineWidth = o._thickness;
|
|
this.ctx.strokeStyle = o._color.toHtml();
|
|
this.ctx.moveTo(o.x, o.y);
|
|
this.ctx.lineTo(o.x+o.xextent, o.y+o.yextent);
|
|
this.ctx.stroke();
|
|
}
|
|
this.ctx.restore();
|
|
}
|
|
|
|
public mkSprite(tp:SpriteType, w:number, h:number)
|
|
{
|
|
var s = Sprite.mk(tp, this.initialX(), this.initialY(), w, h);
|
|
this.addSprite(s);
|
|
return s;
|
|
}
|
|
|
|
private addSprite(s:Sprite)
|
|
{
|
|
s._parent = this;
|
|
s.set_z_index(0);
|
|
this.sprites.push(s);
|
|
s.changed();
|
|
this.spritesChanged();
|
|
}
|
|
|
|
//? Create a new ellipse sprite.
|
|
//@ readsMutable [result].writesMutable
|
|
//@ [width].defl(20) [height].defl(20)
|
|
//@ embedsLink("Board", "Sprite")
|
|
public create_ellipse(width:number, height:number) : Sprite { return this.mkSprite(SpriteType.Ellipse, width, height); }
|
|
|
|
//? Create a new rectangle sprite.
|
|
//@ readsMutable [result].writesMutable
|
|
//@ [width].defl(20) [height].defl(20)
|
|
//@ embedsLink("Board", "Sprite")
|
|
public create_rectangle(width:number, height:number) : Sprite { return this.mkSprite(SpriteType.Rectangle, width, height); }
|
|
|
|
//? Create a new text sprite.
|
|
//@ readsMutable [result].writesMutable
|
|
//@ [width].defl(100) [height].defl(40) [fontSize].defl(20)
|
|
//@ embedsLink("Board", "Sprite")
|
|
public create_text(width:number, height:number, fontSize:number, text:string) : Sprite
|
|
{
|
|
var s = this.mkSprite(SpriteType.Text, width, height);
|
|
s.fontSize = fontSize;
|
|
s.set_text(text);
|
|
return s;
|
|
}
|
|
|
|
//? Create a new picture sprite.
|
|
//@ readsMutable [result].writesMutable picAsync
|
|
//@ embedsLink("Board", "Sprite"), embedsLink("Sprite", "Picture")
|
|
//@ returns(Sprite)
|
|
public create_picture(picture:Picture, r:ResumeCtx)
|
|
{
|
|
var s = this.mkSprite(SpriteType.Picture, 1, 1);
|
|
picture.loadFirst(r, () => {
|
|
s.setPictureInternal(picture);
|
|
return s;
|
|
});
|
|
}
|
|
|
|
//? Create a new sprite sheet.
|
|
//@ readsMutable [result].writesMutable picAsync
|
|
//@ embedsLink("Board", "SpriteSheet"), embedsLink("SpriteSheet", "Picture")
|
|
//@ returns(SpriteSheet)
|
|
public create_sprite_sheet(picture:Picture, r:ResumeCtx)
|
|
{
|
|
picture.loadFirst(r, () => {
|
|
Util.log('board: new sprite sheet - ' + picture.widthSync() + 'x' + picture.heightSync());
|
|
var sheet = new SpriteSheet(this, picture);
|
|
return sheet;
|
|
});
|
|
}
|
|
|
|
//? Create an anchor sprite.
|
|
//@ readsMutable [result].writesMutable
|
|
//@ [width].defl(20) [height].defl(20)
|
|
//@ embedsLink("Board", "Sprite")
|
|
public create_anchor(width:number, height:number) : Sprite {
|
|
var anchor = this.mkSprite(SpriteType.Anchor, width, height);
|
|
anchor.set_friction(1); // don't move
|
|
anchor.hide();
|
|
return anchor;
|
|
}
|
|
|
|
//? Create a line obstacle with given start point, and given width and height. Elasticity is 0 for sticky, 1 for complete bounce.
|
|
//@ writesMutable ignoreReturnValue
|
|
//@ [elasticity].defl(1)
|
|
public create_obstacle(x:number, y:number, width:number, height:number, elasticity:number) : Obstacle {
|
|
if (width == 0 && height == 0) return; // avoid singularities
|
|
|
|
var o = new Obstacle(this, x, y, width, height, elasticity, 1 - elasticity);
|
|
this.addObstacle(o);
|
|
return o;
|
|
}
|
|
|
|
public deleteObstacle(obstacle : Obstacle)
|
|
{
|
|
var idx = this._obstacles.indexOf(obstacle);
|
|
if (idx > -1) {
|
|
this._obstacles.splice(idx, 1);
|
|
this._walls = this._walls.filter(wall => wall._obstacle != obstacle);
|
|
}
|
|
}
|
|
|
|
//? Create a spring between the two sprites.
|
|
//@ writesMutable ignoreReturnValue
|
|
//@ [stiffness].defl(100)
|
|
public create_spring(sprite1:Sprite, sprite2:Sprite, stiffness:number) : Spring
|
|
{
|
|
// TODO: check for invalid parents
|
|
var spring = new Spring(this, sprite1, sprite2, stiffness);
|
|
this._springs.push(spring);
|
|
sprite1.addSpring(spring);
|
|
sprite2.addSpring(spring);
|
|
return spring;
|
|
}
|
|
|
|
public deleteSpring(spring : Spring) {
|
|
spring.sprite1.removeSpring(spring);
|
|
spring.sprite2.removeSpring(spring);
|
|
var idx = this._springs.indexOf(spring);
|
|
if (idx > -1)
|
|
this._springs.splice(idx, 1);
|
|
}
|
|
|
|
//? Clear all queued events related to this board
|
|
public clear_events() : void
|
|
{
|
|
}
|
|
|
|
public overlapWithAny(sprite:Sprite, sprites:SpriteSet):SpriteSet {
|
|
var result = new SpriteSet();
|
|
for (var i = 0; i < sprites.count(); i++)
|
|
{
|
|
var other = sprites.at(i);
|
|
if (sprite === other) continue;
|
|
if (sprite.overlaps_with(other))
|
|
{
|
|
result.add(other);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//? Clears the background camera
|
|
//@ cap(camera)
|
|
public clear_background_camera(): void
|
|
{
|
|
this.backgroundCamera = null;
|
|
}
|
|
|
|
//? Clear the background picture
|
|
public clear_background_picture(): void
|
|
{
|
|
this.backgroundPicture = null;
|
|
}
|
|
|
|
//? Shows the board on the wall.
|
|
public post_to_wall(s:IStackFrame) : void
|
|
{
|
|
super.post_to_wall(s)
|
|
if (this._full) {
|
|
if (this._landscape) Runtime.lockOrientation(false, true, false);
|
|
else Runtime.lockOrientation(true, false, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
//? An obstacle on a board
|
|
//@ ctx(general,gckey)
|
|
export class Obstacle
|
|
extends RTValue
|
|
{
|
|
public _color : Color = Colors.gray();
|
|
public _thickness : number = 3;
|
|
public _onCollision : Event_;
|
|
constructor(public board : Board, public x:number, public y:number, public xextent:number, public yextent:number, public elasticity:number, public friction:number) {
|
|
super()
|
|
}
|
|
|
|
//? Attaches a handler where a sprite bounces on the obstacle
|
|
//@ ignoreReturnValue
|
|
public on_collision(bounce : SpriteAction) : EventBinding {
|
|
if (!this._onCollision) this._onCollision = new Event_();
|
|
return this._onCollision.addHandler(bounce);
|
|
}
|
|
public raiseCollision(rt : Runtime, sprite : Sprite) {
|
|
if (this._onCollision && this._onCollision.handlers)
|
|
rt.queueLocalEvent(this._onCollision, [sprite], false);
|
|
}
|
|
|
|
//? Sets the obstacle color
|
|
//@ [color].deflExpr('colors->random')
|
|
public set_color(color : Color) {
|
|
this._color = color;
|
|
}
|
|
|
|
//? Sets the obstacle thickness
|
|
//@ [thickness].defl(3)
|
|
public set_thickness(thickness : number) {
|
|
this._thickness = Math.max(1, thickness);
|
|
}
|
|
|
|
//? Delete the obstacle
|
|
public delete_() {
|
|
this.board.deleteObstacle(this);
|
|
}
|
|
|
|
public isValid() : boolean {
|
|
if (!this.IsFinite(this.x)) return false;
|
|
if (!this.IsFinite(this.y)) return false;
|
|
if (!this.IsFinite(this.xextent)) return false;
|
|
if (!this.IsFinite(this.yextent)) return false;
|
|
return true;
|
|
}
|
|
|
|
private IsFinite(x:number) : boolean {
|
|
if (isNaN(x)) return false;
|
|
if (isFinite(x)) return true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export class WallSegment {
|
|
public _position:Vector2;
|
|
public _unitExtent:Vector2;
|
|
public _length:number;
|
|
public _elasticity:number;
|
|
public _friction:number;
|
|
public _obstacle : Obstacle;
|
|
|
|
static mk(x:number, y:number, xextent:number, yextent:number, elasticity:number, friction:number, obstacle : Obstacle = undefined)
|
|
{
|
|
var w = new WallSegment();
|
|
w._position = new Vector2(x,y);
|
|
var segment = new Vector2(xextent, yextent);
|
|
w._length = segment.length();
|
|
w._unitExtent = segment.normalize();
|
|
w._elasticity = elasticity;
|
|
w._friction = friction;
|
|
w._obstacle = obstacle;
|
|
return w;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find two points, p1 along wall and p2 along sprite path, such that their distance is the radius of the sprite.
|
|
///
|
|
/// s = 0..length. P1(s) = pos + unitExtent * s
|
|
/// t = 0..1 P2(t) = lastPosition + t*(newPosition - lastPosition);
|
|
///
|
|
/// For now, we simplify this to just compute the time t at which the sprite is distance r from the wall. To avoid
|
|
/// missing collisions on the end of the segment, we pretend that the segment extends by object radius on both sides.
|
|
///
|
|
/// </summary>
|
|
public processPotentialCollision(sprite:Sprite, dT:number):boolean
|
|
{
|
|
var unitNormal = this._unitExtent.rotate90Left();
|
|
var normalSpeedMag = -Vector2.dot(unitNormal, sprite.stepDisplacement());
|
|
if (normalSpeedMag <= 0)
|
|
{
|
|
// moving away
|
|
return false;
|
|
}
|
|
var pq = sprite._position.subtract(this._position);
|
|
var distance = Vector2.dot(pq, unitNormal);
|
|
var normalRadius = sprite.radius(unitNormal);
|
|
if (distance < normalRadius / 2)
|
|
{
|
|
// inside or behind the wall
|
|
// check how much
|
|
if (distance <= -normalRadius / 2)
|
|
{
|
|
// completely clear of other side
|
|
return false;
|
|
}
|
|
|
|
// inside the wall. Check if we are overlapping it
|
|
var unitRadius = sprite.radius(this._unitExtent);
|
|
var segmentProj = Vector2.dot(sprite._position.subtract(this._position), this._unitExtent);
|
|
if (segmentProj < -unitRadius / 2 || segmentProj > this._length + unitRadius / 2)
|
|
{
|
|
// outside wall segment
|
|
return false;
|
|
}
|
|
|
|
//move it back.
|
|
if (distance < 0)
|
|
{
|
|
return false;
|
|
//sprite.newPosition = sprite.position + unitNormal * (sprite.RadiusX - distance);
|
|
}
|
|
else
|
|
{
|
|
sprite.newPosition = sprite._position.add(unitNormal.scale(normalRadius - distance));
|
|
}
|
|
// reverse newSpeed
|
|
normalSpeedMag = Math.abs(Vector2.dot(unitNormal, sprite.midSpeed));
|
|
var normalSpeed = unitNormal.scale(normalSpeedMag);
|
|
var parallelSpeed = this._unitExtent.scale(Vector2.dot(this._unitExtent, sprite.midSpeed));
|
|
|
|
sprite.midSpeed = sprite.newSpeed = (normalSpeed.scale(sprite._elasticity * this._elasticity).add( parallelSpeed.scale(1 - this._friction)));
|
|
|
|
sprite.normalTouchPoints.push(unitNormal);
|
|
return true;
|
|
}
|
|
|
|
var t = (distance - normalRadius) / normalSpeedMag; // approximation of radius
|
|
|
|
if (t > 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var impactPos = sprite._position.add(sprite.stepDisplacement().scale(((t < 0) ? (t * 1.01) : (t * 0.99))));
|
|
// check if impact Pos projected onto segment is within bounds
|
|
var unitRadius2 = sprite.radius(this._unitExtent);
|
|
var segmentIndex = Vector2.dot(impactPos.subtract(this._position), this._unitExtent);
|
|
if (segmentIndex < -unitRadius2 / 2 || segmentIndex > this._length + unitRadius2 / 2)
|
|
{
|
|
// outside wall segment
|
|
return false;
|
|
}
|
|
{
|
|
// fixup position
|
|
normalSpeedMag = Math.abs(Vector2.dot(unitNormal, sprite.midSpeed));
|
|
var normalSpeed = unitNormal.scale(normalSpeedMag);
|
|
var stepParallelSpeed = this._unitExtent.scale(Vector2.dot(this._unitExtent, sprite.stepDisplacement()));
|
|
sprite.newPosition = impactPos.add(stepParallelSpeed.scale(1 - t));
|
|
|
|
var parallelSpeed = this._unitExtent.scale(Vector2.dot(this._unitExtent, sprite.newSpeed));
|
|
sprite.midSpeed = sprite.newSpeed = (normalSpeed.scale(sprite._elasticity * this._elasticity).add(parallelSpeed.scale(1 - this._friction)));
|
|
sprite.normalTouchPoints.push(unitNormal);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
}
|
|
} }
|