Skip to content

Commit

Permalink
feat: [#150] Implement SVG support in ImageSource with static builder (
Browse files Browse the repository at this point in the history
…#3244)

Closes #150 

## Changes:

- Implement svg source string support 
- Allow optional sprite arguments `.toSprite(options:? SpriteOptions)`
  • Loading branch information
eonarheim authored Oct 15, 2024
1 parent 39c2d3e commit 0624e76
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 17 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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('<svg>...</svg>')`, 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`
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
12 changes: 12 additions & 0 deletions sandbox/images/arrows.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions sandbox/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<svg version="1.1"
id="svg2"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="resize-full.svg" inkscape:version="0.48.4 r9939"
xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px"
viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve">
<path id="path18934" fill="#000000ff" inkscape:connector-curvature="0" d="M670.312,0l177.246,177.295L606.348,418.506l175.146,175.146
l241.211-241.211L1200,529.688V0H670.312z M418.506,606.348L177.295,847.559L0,670.312V1200h529.688l-177.246-177.295
l241.211-241.211L418.506,606.348z"/>
</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);
Expand Down
33 changes: 21 additions & 12 deletions src/engine/Graphics/ImageSource.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -75,27 +76,27 @@ export class ImageSource implements Loadable<HTMLImageElement> {

/**
* 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') {
bustCache = bustCacheOrOptions;
} 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 = {
Expand All @@ -105,8 +106,10 @@ export class ImageSource implements Loadable<HTMLImageElement> {
} 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`
);
}
}

Expand Down Expand Up @@ -151,6 +154,12 @@ export class ImageSource implements Loadable<HTMLImageElement> {
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
Expand Down Expand Up @@ -216,8 +225,8 @@ export class ImageSource implements Loadable<HTMLImageElement> {
/**
* Build a sprite from this ImageSource
*/
public toSprite(): Sprite {
return Sprite.from(this);
public toSprite(options?: Omit<GraphicOptions & SpriteOptions, 'image'>): Sprite {
return Sprite.from(this, options);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/engine/Graphics/Sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GraphicOptions & SpriteOptions, 'image'>): Sprite {
return new Sprite({
image: image
image,
...options
});
}

Expand Down
63 changes: 61 additions & 2 deletions src/spec/ImageSourceSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -30,6 +30,40 @@ describe('A ImageSource', () => {
expect(whenLoaded).toHaveBeenCalledTimes(1);
});

it('can load svg image strings', async () => {
const svgImage = ex.ImageSource.fromSvgString(`
<svg version="1.1"
id="svg2"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="resize-full.svg" inkscape:version="0.48.4 r9939"
xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px"
viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve">
<path id="path18934" fill="#000000ff" inkscape:connector-curvature="0" d="M670.312,0l177.246,177.295L606.348,418.506l175.146,175.146
l241.211-241.211L1200,529.688V0H670.312z M418.506,606.348L177.295,847.559L0,670.312V1200h529.688l-177.246-177.295
l241.211-241.211L418.506,606.348z"/>
</svg>
`);
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;
Expand Down Expand Up @@ -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();
Expand Down
12 changes: 12 additions & 0 deletions src/spec/images/GraphicsImageSourceSpec/arrows.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions wallaby.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down

0 comments on commit 0624e76

Please sign in to comment.