diff --git a/sandbox/tests/input-mapper/index.html b/sandbox/tests/input-mapper/index.html
new file mode 100644
index 000000000..39752728b
--- /dev/null
+++ b/sandbox/tests/input-mapper/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Input Mapping
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/tests/input-mapper/main.ts b/sandbox/tests/input-mapper/main.ts
new file mode 100644
index 000000000..da6baf1fe
--- /dev/null
+++ b/sandbox/tests/input-mapper/main.ts
@@ -0,0 +1,64 @@
+
+var game = new ex.Engine({
+ width: 600,
+ height: 400,
+ displayMode: ex.DisplayMode.FitScreenAndFill
+});
+
+var actor = new ex.Actor({
+ pos: ex.vec(100, 100),
+ width: 100,
+ height: 100,
+ color: ex.Color.Red
+});
+var moveRight = (amount: number) => {
+ actor.vel.x = 100 * amount;
+}
+var moveLeft = (amount: number) => {
+ actor.vel.x = -100 * amount;
+}
+var moveUp = (amount: number) => {
+ actor.vel.y = -100 * amount;
+}
+var moveDown = (amount: number) => {
+ actor.vel.y = 100 * amount;
+}
+var getLeftStickX = (gamepad: ex.Gamepad) => {
+ return gamepad.getAxes(ex.Axes.LeftStickX)
+}
+var getLeftStickY = (gamepad: ex.Gamepad) => {
+ return gamepad.getAxes(ex.Axes.LeftStickY)
+}
+actor.onInitialize = (engine) => {
+ const mapper = engine.inputMapper;
+ ex.Gamepads.MinAxisMoveThreshold = .10;
+ // Move right
+ mapper.on(({keyboard}) => keyboard.isHeld(ex.Keys.Right) ? 1 : 0, moveRight);
+ mapper.on(({gamepads}) => gamepads.at(0).getButton(ex.Buttons.DpadRight) ? 1 : 0, moveRight);
+ mapper.on(({gamepads}) => getLeftStickX(gamepads.at(0)) > 0 ? Math.abs(getLeftStickX(gamepads.at(0))) : 0, moveRight);
+ mapper.on(({keyboard, pointers}) => keyboard.isHeld(ex.Keys.Enter) && pointers.primary.lastWorldPos.x > actor.pos.x ? 1 : 0, moveRight);
+
+ // Move left
+ mapper.on(({keyboard}) => keyboard.isHeld(ex.Keys.Left) ? 1 : 0, moveLeft);
+ mapper.on(({gamepads}) => gamepads.at(0).getButton(ex.Buttons.DpadLeft) ? 1 : 0, moveLeft);
+ mapper.on(({gamepads}) => getLeftStickX(gamepads.at(0)) < 0 ? Math.abs(getLeftStickX(gamepads.at(0))): 0, moveLeft);
+ mapper.on(({keyboard, pointers}) => keyboard.isHeld(ex.Keys.Enter) && pointers.primary.lastWorldPos.x < actor.pos.x ? 1 : 0, moveLeft);
+
+ // Move up
+ mapper.on(({keyboard}) => keyboard.isHeld(ex.Keys.Up) ? 1 : 0, moveUp);
+ mapper.on(({gamepads}) => gamepads.at(0).getButton(ex.Buttons.DpadUp) ? 1 : 0, moveUp);
+ mapper.on(({gamepads}) => getLeftStickY(gamepads.at(0)) < 0 ? Math.abs(getLeftStickY(gamepads.at(0))) : 0, moveUp);
+ mapper.on(({keyboard, pointers}) => keyboard.isHeld(ex.Keys.Enter) && pointers.primary.lastWorldPos.y < actor.pos.y ? 1 : 0, moveUp);
+
+ // Move down
+ mapper.on(({keyboard}) => keyboard.isHeld(ex.Keys.Down) ? 1 : 0, moveDown);
+ mapper.on(({gamepads}) => gamepads.at(0).getButton(ex.Buttons.DpadDown) ? 1 : 0, moveDown);
+ mapper.on(({gamepads}) => getLeftStickY(gamepads.at(0)) > 0 ? Math.abs(getLeftStickY(gamepads.at(0))) : 0, moveDown);
+ mapper.on(({keyboard, pointers}) => keyboard.isHeld(ex.Keys.Enter) && pointers.primary.lastWorldPos.y > actor.pos.y ? 1 : 0, moveDown);
+}
+game.onPostUpdate = () => {
+ actor.vel = ex.vec(0, 0);
+}
+game.currentScene.add(actor);
+
+game.start();
\ No newline at end of file
diff --git a/sandbox/tests/postprocessor/index.html b/sandbox/tests/postprocessor/index.html
new file mode 100644
index 000000000..308c6b82f
--- /dev/null
+++ b/sandbox/tests/postprocessor/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Post-processing
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/tests/postprocessor/main.ts b/sandbox/tests/postprocessor/main.ts
new file mode 100644
index 000000000..5068aaa67
--- /dev/null
+++ b/sandbox/tests/postprocessor/main.ts
@@ -0,0 +1,115 @@
+var crtFragmentSource = `#version 300 es
+precision mediump float;
+// Sourced from https://www.shadertoy.com/view/Ms23DR
+// Loosely based on postprocessing shader by inigo quilez, License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
+uniform sampler2D u_image;
+
+uniform vec2 u_resolution;
+
+uniform float u_time_ms;
+
+uniform float u_elapsed_ms;
+
+out vec4 fragColor;
+
+vec2 curve(vec2 uv)
+{
+ uv = (uv - 0.5) * 2.0;
+ uv *= 1.1;
+ uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0);
+ uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0);
+ uv = (uv / 2.0) + 0.5;
+ uv = uv *0.92 + 0.04;
+ return uv;
+}
+void main()
+{
+ vec2 q = gl_FragCoord.xy / u_resolution;
+ vec2 uv = q;
+ uv = curve( uv );
+ // vec3 oricol = texture( iChannel0, vec2(q.x,q.y) ).xyz;
+ // vec4 o = texture(u_image, v_texcoord).xyz;
+ vec3 col;
+ float iTime = u_time_ms / 1000.0;
+ float x = sin(0.3*iTime+uv.y*21.0)*sin(0.7*iTime+uv.y*29.0)*sin(0.3+0.33*iTime+uv.y*31.0)*0.0017;
+
+ col.r = texture(u_image,vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05;
+ col.g = texture(u_image,vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05;
+ col.b = texture(u_image,vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05;
+ col.r += 0.08*texture(u_image,0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x;
+ col.g += 0.05*texture(u_image,0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y;
+ col.b += 0.08*texture(u_image,0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z;
+
+ col = clamp(col*0.6+0.4*col*col*1.0,0.0,1.0);
+
+ float vig = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y));
+ col *= vec3(pow(vig,0.3));
+
+ col *= vec3(0.95,1.05,0.95);
+ col *= 2.8;
+
+ float scans = clamp( 0.35+0.35*sin(3.5*iTime+uv.y*u_resolution.y*1.5), 0.0, 1.0);
+
+ float s = pow(scans,1.7);
+ col = col*vec3( 0.4+0.7*s) ;
+
+ col *= 1.0+0.01*sin(110.0*iTime);
+ if (uv.x < 0.0 || uv.x > 1.0)
+ col *= 0.0;
+ if (uv.y < 0.0 || uv.y > 1.0)
+ col *= 0.0;
+
+ col*=1.0-0.65*vec3(clamp((mod(gl_FragCoord.x, 2.0)-1.0)*2.0,0.0,1.0));
+
+ float comp = smoothstep( 0.1, 0.9, sin(iTime) );
+
+ // Remove the next line to stop cross-fade between original and postprocess
+// col = mix( col, oricol, comp );
+
+ fragColor = vec4(col,1.0);
+}`
+
+class CRTPostProcessors implements ex.PostProcessor {
+ private _shader: ex.ScreenShader;
+ constructor() {}
+ initialize(gl: WebGL2RenderingContext): void {
+ this._shader = new ex.ScreenShader(gl, crtFragmentSource);
+ }
+ getShader(): ex.Shader {
+ return this._shader.getShader();
+ }
+ getLayout(): ex.VertexLayout {
+ return this._shader.getLayout();
+ }
+ onUpdate(delta: number): void {
+ // set your own uniforms here
+ const shader = this.getShader();
+ shader.trySetUniformBoolean('u_some_uniform', true);
+ }
+}
+
+var game = new ex.Engine({
+ width: 600,
+ height: 400,
+ displayMode: ex.DisplayMode.FitScreenAndFill
+});
+
+var actor = new ex.Actor({
+ width: 100,
+ height: 100,
+ color: ex.Color.Red
+});
+actor.angularVelocity = .2;
+actor.actions
+ .repeatForever((ctx) => {
+ ctx.moveTo(ex.vec(600, 0), 200);
+ ctx.moveTo(ex.vec(0, 400), 200);
+ ctx.moveTo(ex.vec(600, 400), 200);
+ ctx.moveTo(ex.vec(0, 0), 200);
+ });
+game.currentScene.add(actor);
+
+var ctx = game.graphicsContext as ex.ExcaliburGraphicsContextWebGL;
+ctx.addPostProcessor(new CRTPostProcessors());
+
+game.start();
\ No newline at end of file
diff --git a/sandbox/tests/raycast/index.html b/sandbox/tests/raycast/index.html
new file mode 100644
index 000000000..453486d40
--- /dev/null
+++ b/sandbox/tests/raycast/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Scene Raycasting
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/tests/raycast/main.ts b/sandbox/tests/raycast/main.ts
new file mode 100644
index 000000000..ddf2644aa
--- /dev/null
+++ b/sandbox/tests/raycast/main.ts
@@ -0,0 +1,74 @@
+
+var game = new ex.Engine({
+ width: 600,
+ height: 400,
+ displayMode: ex.DisplayMode.FitScreenAndFill
+});
+var random = new ex.Random(1337);
+var blockGroup = ex.CollisionGroupManager.create('blockGroup');
+
+var player = new ex.Actor({
+ name: 'player',
+ pos: ex.vec(100, 100),
+ width: 40,
+ height: 40,
+ color: ex.Color.Red,
+ z: 10
+});
+var playerDir = ex.Vector.Right;
+var playerSightDistance = 100;
+var playerSpeed = 100;
+var rotationAmount = Math.PI / 32;
+player.onPostUpdate = (engine) => {
+ player.vel = ex.Vector.Zero;
+ const keyboard = engine.input.keyboard;
+ if (keyboard.isHeld(ex.Keys.ArrowLeft)) {
+ playerDir = playerDir.rotate(-rotationAmount)
+ player.rotation -= rotationAmount;
+ }
+ if (keyboard.isHeld(ex.Keys.ArrowRight)){
+ playerDir = playerDir.rotate(+rotationAmount)
+ player.rotation += rotationAmount;
+ }
+ if (keyboard.isHeld(ex.Keys.ArrowUp)) {
+ player.vel = playerDir.scale(100);
+ }
+
+ // raycast
+ var hits = engine.currentScene.physics.rayCast(
+ new ex.Ray(player.pos, playerDir),
+ {
+ maxDistance: playerSightDistance,
+ collisionGroup: blockGroup,
+ searchAllColliders: false
+ });
+
+ for (let hit of hits) {
+ const hitActor = hit.collider.owner as ex.Actor;
+ hitActor.graphics.use(new ex.Rectangle({
+ color: ex.Color.Violet,
+ width: 20,
+ height: 20
+ }));
+ }
+}
+player.graphics.onPostDraw = (ctx) => {
+ ctx.drawLine(ex.Vector.Zero, ex.Vector.Right.scale(playerSightDistance), ex.Color.Red, 2);
+};
+game.currentScene.add(player);
+
+// environment
+
+for (let i = 0; i < 10; i++) {
+ var block = new ex.Actor({
+ pos: ex.vec(random.floating(0, 600), random.floating(0, 400)),
+ width: 20,
+ height: 20,
+ color: ex.Color.Black,
+ collisionType: ex.CollisionType.Fixed,
+ collisionGroup: blockGroup
+ });
+ game.currentScene.add(block);
+}
+
+game.start();
\ No newline at end of file
diff --git a/sandbox/tests/spritesampling/index.html b/sandbox/tests/spritesampling/index.html
new file mode 100644
index 000000000..200d01b7b
--- /dev/null
+++ b/sandbox/tests/spritesampling/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Sprite Sampling
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/tests/spritesampling/main.ts b/sandbox/tests/spritesampling/main.ts
new file mode 100644
index 000000000..f3bef837e
--- /dev/null
+++ b/sandbox/tests/spritesampling/main.ts
@@ -0,0 +1,62 @@
+var game = new ex.Engine({
+ displayMode: ex.DisplayMode.Fixed,
+ resolution: {
+ height: 16 * 20,
+ width: 16 * 20,
+ },
+ viewport: {
+ width: 16 * 20,
+ height: 16 * 20
+ },
+ antialiasing: false,
+ snapToPixel: true
+})
+
+var heroSpriteImage = new ex.ImageSource('../../images/Hero 01.png', false, ex.ImageFiltering.Pixel);
+
+var loader = new ex.Loader([
+ heroSpriteImage
+]);
+
+
+class PlayerSample extends ex.Actor {
+ constructor(pos: ex.Vector) {
+ super({
+ pos,
+ width: 16,
+ height: 16,
+ collisionType: ex.CollisionType.Active
+ })
+ }
+
+ onInitialize(engine: ex.Engine): void {
+ const playerSpriteSheet = ex.SpriteSheet.fromImageSource({
+ image: heroSpriteImage,
+ grid: {
+ spriteWidth: 16,
+ spriteHeight: 16,
+ rows: 8,
+ columns: 8
+ }
+ });
+
+ const downIdle = new ex.Animation({
+ frames: [
+ { graphic: playerSpriteSheet.getSprite(0, 0) as ex.Sprite, duration: 200 },
+ { graphic: playerSpriteSheet.getSprite(1, 0) as ex.Sprite, duration: 200 },
+ { graphic: playerSpriteSheet.getSprite(2, 0) as ex.Sprite, duration: 200 },
+ { graphic: playerSpriteSheet.getSprite(3, 0) as ex.Sprite, duration: 200 },
+ ]
+ })
+ this.graphics.add('down-idle', downIdle);
+
+ this.graphics.use('down-idle');
+ }
+}
+
+
+
+game.start(loader).then(() => {
+ game.currentScene.camera.pos = ex.vec(16, 16);
+ game.add(new PlayerSample(ex.vec(16, 16)));
+});
diff --git a/sandbox/tests/text-wrapping/index.html b/sandbox/tests/text-wrapping/index.html
new file mode 100644
index 000000000..9c816f0e4
--- /dev/null
+++ b/sandbox/tests/text-wrapping/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Text Wrapping
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sandbox/tests/text-wrapping/main.ts b/sandbox/tests/text-wrapping/main.ts
new file mode 100644
index 000000000..edf9ee14c
--- /dev/null
+++ b/sandbox/tests/text-wrapping/main.ts
@@ -0,0 +1,39 @@
+var game = new ex.Engine({
+ width: 600,
+ height: 400,
+ displayMode: ex.DisplayMode.FitScreenAndFill
+});
+
+var text = new ex.Text({
+ text: '😻',
+ maxWidth: 200,
+ font: new ex.Font({
+ family: 'Consolas',
+ size: 30,
+ unit: ex.FontUnit.Px,
+ color: ex.Color.Black
+ })
+});
+
+var textActor = new ex.Actor({
+ pos: game.screen.center,
+});
+textActor.graphics.use(text);
+game.currentScene.add(textActor);
+
+game.input.keyboard.on('release', ev => {
+ if (ev.key !== ex.Keys.ShiftLeft &&
+ ev.key !== ex.Keys.ShiftRight &&
+ ev.key !== ex.Keys.Enter &&
+ ev.key !== ex.Keys.Backspace) {
+ text.text += ev.value;
+ }
+ if (ev.key === ex.Keys.Enter) {
+ text.text += '\n';
+ }
+ if (ev.key === ex.Keys.Backspace) {
+ text.text = text.text.slice(0, text.text.length - 1);
+ }
+});
+
+game.start();
\ No newline at end of file