diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65c591fe5..5a89c2616 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,7 +65,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added
-- The `ex.Engine` had a new `enableCanvasContextMenu` arg that can be used to enable the right click context menu, by default the context menu is disabled which is what most games seem to want.
+- Added inline SVG image support `ex.ImageSource.fromSvgString('')`, note images produced this way still must be loaded.
+- Added ability to optionally specify sprite options in the `.toSprite(options:? SpriteOptions)`
+- The `ex.Engine` constructor had a new `enableCanvasContextMenu` arg that can be used to enable the right click context menu, by default the context menu is disabled which is what most games seem to want.
- Child `ex.Actor` inherits opacity of parents
- `ex.Engine.timeScale` values of 0 are now supported
- `ex.Trigger` now supports all valid actor constructor parameters from `ex.ActorArgs` in addition to `ex.TriggerOptions`
diff --git a/karma.conf.js b/karma.conf.js
index 716ecf677..8efc0267c 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -82,6 +82,7 @@ module.exports = (config) => {
'src/spec/_boot.ts',
{ pattern: 'src/spec/images/**/*.mp3', included: false, served: true },
{ pattern: 'src/spec/images/**/*.ogg', included: false, served: true },
+ { pattern: 'src/spec/images/**/*.svg', included: false, served: true },
{ pattern: 'src/spec/images/**/*.png', included: false, served: true },
{ pattern: 'src/spec/images/**/*.gif', included: false, served: true },
{ pattern: 'src/spec/images/**/*.txt', included: false, served: true },
diff --git a/sandbox/images/arrows.svg b/sandbox/images/arrows.svg
new file mode 100644
index 000000000..849d71fd7
--- /dev/null
+++ b/sandbox/images/arrows.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts
index 31aba0d9e..14c8e7901 100644
--- a/sandbox/src/game.ts
+++ b/sandbox/src/game.ts
@@ -177,12 +177,53 @@ cards2.draw(game.graphicsContext, 0, 0);
jump.volume = 0.3;
+var svgExternal = new ex.ImageSource('../images/arrows.svg');
+var svg = (tags: TemplateStringsArray) => tags[0];
+
+var svgImage = ex.ImageSource.fromSvgString(svg`
+
+`);
+
+var svgActor = new ex.Actor({
+ name: 'svg',
+ pos: ex.vec(200, 200)
+});
+svgActor.graphics.add(
+ svgImage.toSprite({
+ destSize: {
+ width: 100,
+ height: 100
+ },
+ sourceView: {
+ x: 400,
+ y: 0,
+ width: 400,
+ height: 400
+ }
+ })
+);
+// svgActor.graphics.add(svgExternal.toSprite());
+game.add(svgActor);
+
var boot = new ex.Loader();
// var boot = new ex.Loader({
// fullscreenAfterLoad: true,
// fullscreenContainer: document.getElementById('container')
// });
// boot.suppressPlayButton = true;
+boot.addResource(svgExternal);
+boot.addResource(svgImage);
boot.addResource(heartImageSource);
boot.addResource(heartTex);
boot.addResource(imageRun);
diff --git a/src/engine/Graphics/ImageSource.ts b/src/engine/Graphics/ImageSource.ts
index b0f803057..09a1375b0 100644
--- a/src/engine/Graphics/ImageSource.ts
+++ b/src/engine/Graphics/ImageSource.ts
@@ -1,11 +1,12 @@
import { Resource } from '../Resources/Resource';
-import { Sprite } from './Sprite';
+import { Sprite, SpriteOptions } from './Sprite';
import { Loadable } from '../Interfaces/Index';
import { Logger } from '../Util/Log';
import { ImageFiltering } from './Filtering';
import { Future } from '../Util/Future';
import { TextureLoader } from '../Graphics/Context/texture-loader';
import { ImageWrapping } from './Wrapping';
+import { GraphicOptions } from './Graphic';
export interface ImageSourceOptions {
filtering?: ImageFiltering;
@@ -75,19 +76,19 @@ export class ImageSource implements Loadable {
/**
* The path to the image, can also be a data url like 'data:image/'
- * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute
+ * @param pathOrBase64 {string} Path to the image resource relative from the HTML document hosting the game, or absolute
* @param options
*/
- constructor(path: string, options?: ImageSourceOptions);
+ constructor(pathOrBase64: string, options?: ImageSourceOptions);
/**
* The path to the image, can also be a data url like 'data:image/'
- * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute
+ * @param pathOrBase64 {string} Path to the image resource relative from the HTML document hosting the game, or absolute
* @param bustCache {boolean} Should excalibur add a cache busting querystring?
* @param filtering {ImageFiltering} Optionally override the image filtering set by {@apilink EngineOptions.antialiasing}
*/
- constructor(path: string, bustCache: boolean, filtering?: ImageFiltering);
- constructor(path: string, bustCacheOrOptions: boolean | ImageSourceOptions | undefined, filtering?: ImageFiltering) {
- this.path = path;
+ constructor(pathOrBase64: string, bustCache: boolean, filtering?: ImageFiltering);
+ constructor(pathOrBase64: string, bustCacheOrOptions: boolean | ImageSourceOptions | undefined, filtering?: ImageFiltering) {
+ this.path = pathOrBase64;
let bustCache: boolean | undefined = false;
let wrapping: ImageWrapConfiguration | ImageWrapping | undefined;
if (typeof bustCacheOrOptions === 'boolean') {
@@ -95,7 +96,7 @@ export class ImageSource implements Loadable {
} else {
({ filtering, wrapping, bustCache } = { ...bustCacheOrOptions });
}
- this._resource = new Resource(path, 'blob', bustCache);
+ this._resource = new Resource(pathOrBase64, 'blob', bustCache);
this.filtering = filtering ?? this.filtering;
if (typeof wrapping === 'string') {
this.wrapping = {
@@ -105,8 +106,10 @@ export class ImageSource implements Loadable {
} else {
this.wrapping = wrapping ?? this.wrapping;
}
- if (path.endsWith('.svg') || path.endsWith('.gif')) {
- this._logger.warn(`Image type is not fully supported, you may have mixed results ${path}. Fully supported: jpg, bmp, and png`);
+ if (pathOrBase64.endsWith('.gif')) {
+ this._logger.warn(
+ `Use the ex.Gif type to load gifs, you may have mixed results with ${pathOrBase64} in ex.ImageSource. Fully supported: svg, jpg, bmp, and png`
+ );
}
}
@@ -151,6 +154,12 @@ export class ImageSource implements Loadable {
return imageSource;
}
+ static fromSvgString(svgSource: string, options?: ImageSourceOptions) {
+ const blob = new Blob([svgSource], { type: 'image/svg+xml' });
+ const url = URL.createObjectURL(blob);
+ return new ImageSource(url, options);
+ }
+
/**
* Should excalibur add a cache busting querystring? By default false.
* Must be set before loading
@@ -216,8 +225,8 @@ export class ImageSource implements Loadable {
/**
* Build a sprite from this ImageSource
*/
- public toSprite(): Sprite {
- return Sprite.from(this);
+ public toSprite(options?: Omit): Sprite {
+ return Sprite.from(this, options);
}
/**
diff --git a/src/engine/Graphics/Sprite.ts b/src/engine/Graphics/Sprite.ts
index 7a0c512c7..6d70dab7a 100644
--- a/src/engine/Graphics/Sprite.ts
+++ b/src/engine/Graphics/Sprite.ts
@@ -28,9 +28,10 @@ export class Sprite extends Graphic {
public destSize: DestinationSize;
private _dirty = true;
- public static from(image: ImageSource): Sprite {
+ public static from(image: ImageSource, options?: Omit): Sprite {
return new Sprite({
- image: image
+ image,
+ ...options
});
}
diff --git a/src/spec/ImageSourceSpec.ts b/src/spec/ImageSourceSpec.ts
index 74b25ca84..0662230f0 100644
--- a/src/spec/ImageSourceSpec.ts
+++ b/src/spec/ImageSourceSpec.ts
@@ -15,9 +15,9 @@ describe('A ImageSource', () => {
const logger = ex.Logger.getInstance();
spyOn(logger, 'warn');
const image1 = new ex.ImageSource('base/404/img.svg');
- expect(logger.warn).toHaveBeenCalledTimes(1);
+ expect(logger.warn).toHaveBeenCalledTimes(0);
const image2 = new ex.ImageSource('base/404/img.gif');
- expect(logger.warn).toHaveBeenCalledTimes(2);
+ expect(logger.warn).toHaveBeenCalledTimes(1);
});
it('can load images', async () => {
@@ -30,6 +30,40 @@ describe('A ImageSource', () => {
expect(whenLoaded).toHaveBeenCalledTimes(1);
});
+ it('can load svg image strings', async () => {
+ const svgImage = ex.ImageSource.fromSvgString(`
+
+ `);
+ const whenLoaded = jasmine.createSpy('whenLoaded');
+ await svgImage.load();
+ await svgImage.ready.then(whenLoaded);
+ expect(svgImage.image.src).not.toBeNull();
+ expect(whenLoaded).toHaveBeenCalledTimes(1);
+ });
+
+ it('can load svg images', async () => {
+ const svgImage = new ex.ImageSource('src/spec/images/GraphicsImageSourceSpec/arrows.svg');
+ const whenLoaded = jasmine.createSpy('whenLoaded');
+ await svgImage.load();
+ await svgImage.ready.then(whenLoaded);
+ expect(svgImage.image.src).not.toBeNull();
+ expect(whenLoaded).toHaveBeenCalledTimes(1);
+
+ expect(svgImage.width).toBe(800);
+ expect(svgImage.height).toBe(800);
+ });
+
it('will log a warning if images are too large for mobile', async () => {
const canvasElement = document.createElement('canvas');
canvasElement.width = 100;
@@ -269,6 +303,31 @@ describe('A ImageSource', () => {
expect(sprite.height).toBe(image.height);
});
+ it('can convert to a Sprite with options', async () => {
+ const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png');
+ const sprite = spriteFontImage.toSprite({
+ sourceView: {
+ x: 16,
+ y: 16,
+ width: 16,
+ height: 16
+ },
+ destSize: {
+ width: 32,
+ height: 32
+ }
+ });
+
+ // Sprites have no width/height until the underlying image is loaded
+ expect(sprite.width).toBe(32);
+ expect(sprite.height).toBe(32);
+
+ const image = await spriteFontImage.load();
+ await spriteFontImage.ready;
+ expect(sprite.width).toBe(32);
+ expect(sprite.height).toBe(32);
+ });
+
it('can unload from memory', async () => {
const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png');
await spriteFontImage.load();
diff --git a/src/spec/images/GraphicsImageSourceSpec/arrows.svg b/src/spec/images/GraphicsImageSourceSpec/arrows.svg
new file mode 100644
index 000000000..849d71fd7
--- /dev/null
+++ b/src/spec/images/GraphicsImageSourceSpec/arrows.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/wallaby.js b/wallaby.js
index 10f77ce9e..a41d5519d 100644
--- a/wallaby.js
+++ b/wallaby.js
@@ -10,6 +10,7 @@ module.exports = function (wallaby) {
{ pattern: 'src/engine/**/*.glsl', load: false },
{ pattern: 'src/spec/images/**/*.mp3' },
{ pattern: 'src/spec/images/**/*.ogg' },
+ { pattern: 'src/spec/images/**/*.svg' },
{ pattern: 'src/spec/images/**/*.png' },
{ pattern: 'src/spec/images/**/*.gif' },
{ pattern: 'src/spec/images/**/*.txt' },