diff --git a/CHANGELOG.md b/CHANGELOG.md
index d19d4bdac..d4a45c911 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -75,6 +75,29 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added
+- Added a new `ex.NineSlice` `Graphic` for creating arbitrarily resizable rectangular regions, useful for creating UI, backgrounds, and other resizable elements.
+ ```typescript
+ var nineSlice = new ex.NineSlice({
+ width: 300,
+ height: 100,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 7,
+ bottomMargin: 5,
+ rightMargin: 7
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: ex.NineSliceStretch.Stretch,
+ verticalStretch: ex.NineSliceStretch.Stretch
+ }
+ });
+
+ actor.graphics.add(nineSlice);
+ ```
- Added a method to force graphics on screen `ex.GraphicsComponent.forceOnScreen`
- Added new `ex.Slide` scene transition, which can slide a screen shot of the current screen: `up`, `down`, `left`, or `right`. Optionally you can add an `ex.EasingFunction`, by default `ex.EasingFunctions.Linear`
```typescript
diff --git a/karma.conf.js b/karma.conf.js
index b11f56e8e..0b53e2105 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -8,40 +8,39 @@ process.env.CHROME_BIN = require('puppeteer').executablePath();
console.log('Chromium', process.env.CHROMIUM_BIN);
const isAppveyor = process.env.APPVEYOR_BUILD_NUMBER ? true : false;
-const KarmaJasmineSeedReporter = function(baseReporterDecorator) {
+const KarmaJasmineSeedReporter = function (baseReporterDecorator) {
baseReporterDecorator(this);
- this.onBrowserComplete = function(browser, result) {
+ this.onBrowserComplete = function (browser, result) {
if (result.order && result.order.random && result.order.seed) {
this.write('\n%s: Randomized with seed %s\n', browser.name, result.order.seed);
}
};
- this.onRunComplete = function() {
- }
+ this.onRunComplete = function () {};
};
-const seedReporter = {
- 'reporter:jasmine-seed': ['type', KarmaJasmineSeedReporter], // 1. 'jasmine-seed' is a name that can be referenced in karma.conf.js
+const seedReporter = {
+ 'reporter:jasmine-seed': ['type', KarmaJasmineSeedReporter] // 1. 'jasmine-seed' is a name that can be referenced in karma.conf.js
};
-const SlowSpecsReporter = function(baseReporterDecorator) {
+const SlowSpecsReporter = function (baseReporterDecorator) {
baseReporterDecorator(this);
let slowSpecs = [];
this.specSuccess = this.specFailure = function (browser, result) {
- const seconds = (result.time) / 1000;
+ const seconds = result.time / 1000;
slowSpecs.push({
time: result.time,
name: result.fullName,
- message:`Spec ${result.fullName} took ${seconds} seconds\n`
+ message: `Spec ${result.fullName} took ${seconds} seconds\n`
});
};
- this.onBrowserComplete = function(browser, result) {
- this.write('\n')
+ this.onBrowserComplete = function (browser, result) {
+ this.write('\n');
slowSpecs.sort((a, b) => {
return b.time - a.time;
- })
+ });
for (const spec of slowSpecs.slice(0, 20)) {
let color = '\u001b[32m'; // green
let timeSeconds = spec.time/1000;
@@ -57,8 +56,8 @@ const SlowSpecsReporter = function(baseReporterDecorator) {
};
};
const timingReporter = {
- 'reporter:jasmine-slow': ['type', SlowSpecsReporter], // 1.
-}
+ 'reporter:jasmine-slow': ['type', SlowSpecsReporter] // 1.
+};
const TimeoutSpecsReporter = function(baseReporterDecorator, logger, emitter) {
baseReporterDecorator(this);
@@ -78,6 +77,10 @@ const timeoutReporter = {
module.exports = (config) => {
config.set({
+ browserConsoleLogOptions: {
+ terminal: true,
+ level: ''
+ },
singleRun: true,
frameworks: ['jasmine', 'webpack'],
plugins: [
@@ -101,20 +104,20 @@ module.exports = (config) => {
},
proxies: {
// smooths over loading files because karma prepends '/base/' to everything
- '/src/' : '/base/src/'
+ '/src/': '/base/src/'
},
- files: [
- '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 },
- { pattern: 'src/spec/images/**/*.css', included: false, served: true },
- { pattern: 'src/spec/images/**/*.woff2', included: false, served: true },
- { pattern: 'src/spec/fonts/**/*.ttf', included: false, served: true },
- ],
+ files: [
+ '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 },
+ { pattern: 'src/spec/images/**/*.css', included: false, served: true },
+ { pattern: 'src/spec/images/**/*.woff2', included: false, served: true },
+ { pattern: 'src/spec/fonts/**/*.ttf', included: false, served: true }
+ ],
mime: { 'text/x-typescript': ['ts', 'tsx'] },
preprocessors: {
'./src/spec/_boot.ts': ['webpack']
@@ -125,14 +128,14 @@ module.exports = (config) => {
resolve: {
extensions: ['.ts', '.js'],
alias: {
- "@excalibur": path.resolve(__dirname, './src/engine/')
+ '@excalibur': path.resolve(__dirname, './src/engine/')
}
},
plugins: [
new webpack.DefinePlugin({
- 'process.env.__EX_VERSION': '\'test-runner\'',
+ 'process.env.__EX_VERSION': "'test-runner'",
'process.env.NODE_ENV': JSON.stringify('test')
- }),
+ })
],
module: {
rules: [
@@ -176,16 +179,13 @@ module.exports = (config) => {
}
},
webpackMiddleware: {
- // webpack-dev-middleware configuration
- // i. e.
- stats: 'normal'
+ // webpack-dev-middleware configuration
+ // i. e.
+ stats: 'normal'
},
reporters: ['jasmine-order', 'progress', /*'spec'*/, 'coverage-istanbul','jasmine-seed', 'jasmine-slow', 'jasmine-timeout'],
coverageReporter: {
- reporters: [
- { type: 'html', dir: 'coverage/' },
- { type: 'lcovonly', dir: 'coverage/', file: 'lcov.info' },
- { type: 'text-summary' }]
+ reporters: [{ type: 'html', dir: 'coverage/' }, { type: 'lcovonly', dir: 'coverage/', file: 'lcov.info' }, { type: 'text-summary' }]
},
coverageIstanbulReporter: {
// reports can be any that are listed here: https://github.com/istanbuljs/istanbuljs/tree/aae256fb8b9a3d19414dcf069c592e88712c32c6/packages/istanbul-reports/lib
@@ -195,24 +195,24 @@ module.exports = (config) => {
dir: path.join(__dirname, 'coverage')
},
browsers: ['ChromiumHeadless_with_audio'],
- browserDisconnectTolerance : 1,
+ browserDisconnectTolerance: 1,
browserDisconnectTimeout: 60000, // appveyor is slow :(
browserNoActivityTimeout: 60000, // appveyor is slow :(
customLaunchers: {
ChromeHeadless_with_audio: {
- base: 'ChromeHeadless',
- flags: ['--autoplay-policy=no-user-gesture-required', '--mute-audio', '--disable-gpu', '--no-sandbox']
+ base: 'ChromeHeadless',
+ flags: ['--autoplay-policy=no-user-gesture-required', '--mute-audio', '--disable-gpu', '--no-sandbox']
},
ChromiumHeadless_with_audio: {
- base: 'ChromiumHeadless',
- flags: [
- '--autoplay-policy=no-user-gesture-required',
- '--mute-audio',
- '--disable-gpu',
- '--no-sandbox',
- '--enable-precise-memory-info',
- '--js-flags="--max_old_space_size=8192"'
- ]
+ base: 'ChromiumHeadless',
+ flags: [
+ '--autoplay-policy=no-user-gesture-required',
+ '--mute-audio',
+ '--disable-gpu',
+ '--no-sandbox',
+ '--enable-precise-memory-info',
+ '--js-flags="--max_old_space_size=8192"'
+ ]
},
ChromiumHeadless_with_debug: {
base: 'ChromiumHeadless',
@@ -220,7 +220,13 @@ module.exports = (config) => {
},
Chromium_with_debug: {
base: 'Chromium',
- flags: ['--remote-debugging-address=0.0.0.0', '--remote-debugging-port=9222', '--disable-web-security', '--mute-audio', '--no-sandbox']
+ flags: [
+ '--remote-debugging-address=0.0.0.0',
+ '--remote-debugging-port=9222',
+ '--disable-web-security',
+ '--mute-audio',
+ '--no-sandbox'
+ ]
}
}
});
diff --git a/sandbox/tests/9-slice/InputTile.png b/sandbox/tests/9-slice/InputTile.png
new file mode 100644
index 000000000..9df9c61cc
Binary files /dev/null and b/sandbox/tests/9-slice/InputTile.png differ
diff --git a/sandbox/tests/9-slice/index.html b/sandbox/tests/9-slice/index.html
new file mode 100644
index 000000000..d061a112f
--- /dev/null
+++ b/sandbox/tests/9-slice/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 9-Slice
+
+
+
+
+
+
diff --git a/sandbox/tests/9-slice/index.ts b/sandbox/tests/9-slice/index.ts
new file mode 100644
index 000000000..7c66a62d4
--- /dev/null
+++ b/sandbox/tests/9-slice/index.ts
@@ -0,0 +1,35 @@
+var game = new ex.Engine({
+ width: 600,
+ height: 400,
+ displayMode: ex.DisplayMode.FitScreenAndFill,
+ pixelArt: true
+});
+
+var inputTile = new ex.ImageSource('./InputTile.png');
+
+var actor = new ex.Actor({
+ pos: ex.vec(200, 200)
+});
+game.add(actor);
+var nineSlice = new ex.NineSlice({
+ width: 300,
+ height: 100,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 7,
+ bottomMargin: 5,
+ rightMargin: 7
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: ex.NineSliceStretch.Stretch,
+ verticalStretch: ex.NineSliceStretch.Stretch
+ }
+});
+
+actor.graphics.add(nineSlice);
+var loader = new ex.Loader([inputTile]);
+game.start(loader).then(() => {});
diff --git a/site/docs/04-graphics/04.6-nineslice.mdx b/site/docs/04-graphics/04.6-nineslice.mdx
new file mode 100644
index 000000000..490b0ba5d
--- /dev/null
+++ b/site/docs/04-graphics/04.6-nineslice.mdx
@@ -0,0 +1,147 @@
+---
+title: Nine-Slice
+slug: /nineslice
+section: Graphics
+---
+
+## Overview
+
+The NineSlice is a Graphic which uses a 2D technique allowing for reuse of an image at various sizes without needing to prepare multiple assets. The process includes taking an input sprite texture, and splitting it into 9 sections, which then can be repainted in a new sprite texture without scaling or distorting the texture. This is very useful in game development for UI panels or buttons or textured platforms.
+
+For example: let's say this is your source texture sprite.
+
+
+
+The nine slice Graphic will break that texture up into 9 separate parts.
+
+
+
+Then based on your output parameters specified it will 'redraw' this texture to whatever specified output graphic you want.
+
+You can set parameters under `sourceConfig` properties to control the margins. This lets you dial in where the module 'slices' your texture up. For example, with the example 64 pixel x 64 pixel sprite, we can set the top/bottom margins to 5 pixels, and the left/right margins to 6 pixels to get this slicing.
+
+
+
+## Configuration
+
+```typescript
+export type NineSliceConfig = GraphicOptions & {
+ /*
+ overwrites the GraphicOptions width/height param, as it is required for this module
+ */
+ width: number;
+ height: number;
+
+ /*
+ source is the loaded ImageSource, which you can load manually or through a loader
+ */
+ source: ImageSource;
+
+ /*
+ sourceConfig has all the parameters necessary to cut up the input texture properly, width and height are the overall pixel dimensions of the texture and the margins define the overall cutting boundaries in pixels
+ */
+
+ sourceConfig: {
+ width: number;
+ height: number;
+ topMargin: number;
+ leftMargin: number;
+ bottomMargin: number;
+ rightMargin: number;
+ };
+
+ /*
+ destination configuration specifies if you want center piece drawn, the '5' frame, and how you want the algorithm to manipulate the frame textures horizontally and vertically
+ */
+ destinationConfig: {
+ drawCenter: boolean;
+ horizontalStretch: NineSliceStretch; //exported enum
+ verticalStretch: NineSliceStretch; //exported enum
+ };
+};
+```
+
+## Usage
+
+This is a Graphic object so it can be used on either an Actor or ScreenElement entity as a primary graphic, or as a part of a graphic group.
+
+```ts
+export class Player extends Actor {
+ private myNineSlice: NineSlice;
+ constructor() {
+ super({ name: 'player', width: 300, height: 128, pos: new Vector(100, 100) });
+ const myNineSliceConfig: NineSliceConfig = {
+ width: 300,
+ height: 128,
+ source: Resources.myImage,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 6,
+ bottomMargin: 5,
+ rightMargin: 6
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: NineSliceStretch.TileFit,
+ verticalStretch: NineSliceStretch.TileFit
+ }
+ };
+
+ this.myNineSlice = new NineSlice(myNineSliceConfig);
+ this.graphics.use(this.myNineSlice);
+ }
+}
+```
+
+## Stretch Options
+
+There are two parameters in the configuration that need to be set to dictate the behavior of the drawing destination graphic, `horizontalStretch` and `verticalStretch`. They are of type `NineSliceStretch`. The `horizontalStretch` parameter will dictate the behavior of tile slices 2,5, and 8, as these frames can stretch horizontally. The `verticalStretch` parameter dictates how tile slices 4,5,6 behave, as they can stretch vertically.
+
+The class includes some exported enumerated options:
+
+```ts
+export enum NineSliceStretch {
+ Stretch,
+ Tile,
+ TileFit
+}
+```
+
+### Stretch
+
+
+
+The stretch setting takes input slice and paints in across the entirety of the output dimensions, causing a stretching of the texture as its redrawn, but this is guaranteed to fit the designated area, just with some potential image distortion.
+
+### Tile
+
+
+
+The tile setting takes input slice and paints in across the entirety of the output dimensions in a repeating texture fashion that will not distort the source image at all, but may not fit properly in the destination space, leaving a potentially partial drawing of the texture remaining.
+
+### TileFit
+
+
+
+The tile fit setting takes the input slice and paints it across the entirety of the output dimensions in a repeating texture, but it 'resizes' the output tiling to create a 'best fit' of the texture, which is calculated to fit properly, but may have some squished and distorted texture.
+
+## Draw Center
+
+```ts
+destinationConfig: {
+ drawCenter: boolean; //<------------ this property
+ ...
+ };
+```
+
+The boolean flag `drawCenter` under `destinationConfig` tells the module if the section 5, middle section should be drawn or not, if this is set to `false` then it gets skipped in its draw routine, and remains 'transparent' so that the NineSlice is more of a border or frame Graphic.
+
+
+
+Look, Ma! No Center!
+
+## Changing the Graphic
+
+At this point, if you modify the graphic details, it is recommended to recreate a new instance of the graphic and update your Actor or Screen Element if it needs changed. The existing instance will not dynamically update itself.
diff --git a/site/docs/04-graphics/9slice.png b/site/docs/04-graphics/9slice.png
new file mode 100644
index 000000000..a397c5c7a
Binary files /dev/null and b/site/docs/04-graphics/9slice.png differ
diff --git a/site/docs/04-graphics/InputTile.png b/site/docs/04-graphics/InputTile.png
new file mode 100644
index 000000000..9df9c61cc
Binary files /dev/null and b/site/docs/04-graphics/InputTile.png differ
diff --git a/site/docs/04-graphics/noCenter.png b/site/docs/04-graphics/noCenter.png
new file mode 100644
index 000000000..2d181e169
Binary files /dev/null and b/site/docs/04-graphics/noCenter.png differ
diff --git a/site/docs/04-graphics/sliced.png b/site/docs/04-graphics/sliced.png
new file mode 100644
index 000000000..17b5961ab
Binary files /dev/null and b/site/docs/04-graphics/sliced.png differ
diff --git a/site/docs/04-graphics/stretched.png b/site/docs/04-graphics/stretched.png
new file mode 100644
index 000000000..6fc0a7086
Binary files /dev/null and b/site/docs/04-graphics/stretched.png differ
diff --git a/site/docs/04-graphics/tiled.png b/site/docs/04-graphics/tiled.png
new file mode 100644
index 000000000..2213c2c77
Binary files /dev/null and b/site/docs/04-graphics/tiled.png differ
diff --git a/site/docs/04-graphics/tiledfit.png b/site/docs/04-graphics/tiledfit.png
new file mode 100644
index 000000000..f51c35297
Binary files /dev/null and b/site/docs/04-graphics/tiledfit.png differ
diff --git a/src/engine/Graphics/NineSlice.ts b/src/engine/Graphics/NineSlice.ts
new file mode 100644
index 000000000..d88acbee4
--- /dev/null
+++ b/src/engine/Graphics/NineSlice.ts
@@ -0,0 +1,600 @@
+import { Graphic, GraphicOptions } from './Graphic';
+import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
+import { ImageSource } from './ImageSource';
+import { Logger } from '../Util/Log';
+import { Vector } from '../Math/vector';
+
+/**
+ * Nine slice stretch mode
+ */
+export enum NineSliceStretch {
+ /**
+ * Stretch the image across a dimension
+ */
+ Stretch = 'stretch',
+ /**
+ * Tile the image across a dimension
+ */
+ Tile = 'tile',
+ /**
+ * Tile the image across a dimension but only by whole image amounts
+ */
+ TileFit = 'tile-fit'
+}
+
+export type NineSliceConfig = GraphicOptions & {
+ /**
+ * Final width of the nine slice graphic
+ */
+ width: number;
+ /**
+ * Final height of the nine slice graphic
+ */
+ height: number;
+ /**
+ * Image source that's loaded from a Loader or individually
+ *
+ */
+ source: ImageSource;
+
+ /**
+ * Configuration for the source
+ *
+ * Details for the source image, including:
+ *
+ * width and height as numbers of the source image
+ *
+ * and the 9 slice margins
+ */
+ sourceConfig: {
+ width: number;
+ height: number;
+ topMargin: number;
+ leftMargin: number;
+ bottomMargin: number;
+ rightMargin: number;
+ };
+
+ /**
+ * Configuration for the destination
+ *
+ * Details for the destination image, including:
+ *
+ * stretching strategies for horizontal and vertical stretching
+ *
+ * and flag for drawing the center tile if desired
+ */
+ destinationConfig: {
+ /**
+ * Draw the center part of the nine slice, if false it's a completely transparent gap
+ */
+ drawCenter: boolean;
+ /**
+ * Horizontal stretch configuration
+ */
+ horizontalStretch: NineSliceStretch;
+ /**
+ * Vertical stretch configuration
+ */
+ verticalStretch: NineSliceStretch;
+ };
+};
+
+export class NineSlice extends Graphic {
+ private _imgSource: ImageSource;
+ private _sourceSprite?: HTMLImageElement;
+ private _canvasA: HTMLCanvasElement;
+ private _canvasB: HTMLCanvasElement;
+ private _canvasC: HTMLCanvasElement;
+ private _canvasD: HTMLCanvasElement;
+ private _canvasE: HTMLCanvasElement;
+ private _canvasF: HTMLCanvasElement;
+ private _canvasG: HTMLCanvasElement;
+ private _canvasH: HTMLCanvasElement;
+ private _canvasI: HTMLCanvasElement;
+
+ private _logger = Logger.getInstance();
+
+ private _config: NineSliceConfig;
+ constructor(config: NineSliceConfig) {
+ super(config);
+ this._config = config;
+ this._imgSource = config.source;
+
+ this._canvasA = document.createElement('canvas');
+ this._canvasB = document.createElement('canvas');
+ this._canvasC = document.createElement('canvas');
+ this._canvasD = document.createElement('canvas');
+ this._canvasE = document.createElement('canvas');
+ this._canvasF = document.createElement('canvas');
+ this._canvasG = document.createElement('canvas');
+ this._canvasH = document.createElement('canvas');
+ this._canvasI = document.createElement('canvas');
+
+ this._initialize();
+
+ this._imgSource.ready.then(() => {
+ this._initialize();
+ });
+ }
+
+ /**
+ * Sets the target width of the 9 slice (pixels), and recalculates the 9 slice if desired (auto)
+ * @param newWidth
+ * @param auto
+ */
+ setTargetWidth(newWidth: number, auto: boolean = false) {
+ this._config.width = newWidth;
+ if (auto) {
+ this._initialize();
+ }
+ }
+
+ /**
+ * Sets the target height of the 9 slice (pixels), and recalculates the 9 slice if desired (auto)
+ * @param newHeight
+ * @param auto
+ */
+ setTargetHeight(newHeight: number, auto: boolean = false) {
+ this._config.height = newHeight;
+ if (auto) {
+ this._initialize();
+ }
+ }
+
+ /**
+ * Sets the 9 slice margins (pixels), and recalculates the 9 slice if desired (auto)
+ */
+ setMargins(left: number, top: number, right: number, bottom: number, auto: boolean = false) {
+ this._config.sourceConfig.leftMargin = left;
+ this._config.sourceConfig.topMargin = top;
+ this._config.sourceConfig.rightMargin = right;
+ this._config.sourceConfig.bottomMargin = bottom;
+ if (auto) {
+ this._initialize();
+ }
+ }
+
+ /**
+ * Sets the stretching strategy for the 9 slice, and recalculates the 9 slice if desired (auto)
+ *
+ */
+ setStretch(type: 'horizontal' | 'vertical' | 'both', stretch: NineSliceStretch, auto: boolean = false) {
+ if (type === 'horizontal') {
+ this._config.destinationConfig.horizontalStretch = stretch;
+ } else if (type === 'vertical') {
+ this._config.destinationConfig.verticalStretch = stretch;
+ } else {
+ this._config.destinationConfig.horizontalStretch = stretch;
+ this._config.destinationConfig.verticalStretch = stretch;
+ }
+ if (auto) {
+ this._initialize();
+ }
+ }
+
+ /**
+ * Returns the config of the 9 slice
+ */
+ getConfig(): NineSliceConfig {
+ return this._config;
+ }
+
+ /**
+ * Draws 1 of the 9 tiles based on parameters passed in
+ * context is the ExcaliburGraphicsContext from the _drawImage function
+ * destinationSize is the size of the destination image as a vector (width,height)
+ * targetCanvas is the canvas to draw to
+ * horizontalStretch and verticalStretch are the horizontal and vertical stretching strategies
+ * marginW and marginH are optional margins for the 9 slice for positioning
+ * @param context
+ * @param targetCanvas
+ * @param destinationSize
+ * @param horizontalStretch
+ * @param verticalStretch
+ * @param marginWidth
+ * @param marginHeight
+ */
+ protected _drawTile(
+ context: ExcaliburGraphicsContext,
+ targetCanvas: HTMLCanvasElement,
+ destinationSize: Vector,
+ horizontalStretch: NineSliceStretch,
+ verticalStretch: NineSliceStretch,
+ marginWidth?: number,
+ marginHeight?: number
+ ) {
+ const tempMarginW = marginWidth || 0;
+ const tempMarginH = marginHeight || 0;
+ let tempSizeX: number, tempPositionX: number, tempSizeY: number, tempPositionY: number;
+ const numTilesX = this._getNumberOfTiles(targetCanvas.width, destinationSize.x, horizontalStretch);
+ const numTilesY = this._getNumberOfTiles(targetCanvas.height, destinationSize.y, verticalStretch);
+
+ for (let i = 0; i < numTilesX; i++) {
+ for (let j = 0; j < numTilesY; j++) {
+ let { tempSize, tempPosition } = this._calculateParams(
+ i,
+ numTilesX,
+ targetCanvas.width,
+ destinationSize.x,
+ this._config.destinationConfig.horizontalStretch
+ );
+ tempSizeX = tempSize;
+ tempPositionX = tempPosition;
+
+ ({ tempSize, tempPosition } = this._calculateParams(
+ j,
+ numTilesY,
+ targetCanvas.height,
+ destinationSize.y,
+ this._config.destinationConfig.verticalStretch
+ ));
+ tempSizeY = tempSize;
+ tempPositionY = tempPosition;
+
+ context.drawImage(
+ targetCanvas,
+ 0,
+ 0,
+ targetCanvas.width,
+ targetCanvas.height,
+ tempMarginW + tempPositionX,
+ tempMarginH + tempPositionY,
+ tempSizeX,
+ tempSizeY
+ );
+ }
+ }
+ }
+
+ /**
+ * Draws the 9 slices to the canvas
+ */
+ protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number): void {
+ if (this._imgSource.isLoaded()) {
+ // Top left, no stretching
+
+ this._drawTile(
+ ex,
+ this._canvasA,
+
+ new Vector(this._config.sourceConfig.leftMargin, this._config.sourceConfig.topMargin),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch
+ );
+
+ // Top, middle, horizontal stretching
+ this._drawTile(
+ ex,
+ this._canvasB,
+
+ new Vector(
+ this._config.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin,
+ this._config.sourceConfig.topMargin
+ ),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+ this._config.sourceConfig.leftMargin,
+ 0
+ );
+
+ // Top right, no stretching
+
+ this._drawTile(
+ ex,
+ this._canvasC,
+
+ new Vector(this._config.sourceConfig.rightMargin, this._config.sourceConfig.topMargin),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+
+ this._config.width - this._config.sourceConfig.rightMargin,
+ 0
+ );
+
+ // middle, left, vertical stretching
+
+ this._drawTile(
+ ex,
+ this._canvasD,
+ new Vector(
+ this._config.sourceConfig.leftMargin,
+
+ this._config.height - this._config.sourceConfig.bottomMargin - this._config.sourceConfig.topMargin
+ ),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+ 0,
+ this._config.sourceConfig.topMargin
+ );
+
+ // center, both stretching
+ if (this._config.destinationConfig.drawCenter) {
+ this._drawTile(
+ ex,
+ this._canvasE,
+ new Vector(
+ this._config.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin,
+
+ this._config.height - this._config.sourceConfig.bottomMargin - this._config.sourceConfig.topMargin
+ ),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+ this._config.sourceConfig.leftMargin,
+ this._config.sourceConfig.topMargin
+ );
+ }
+ // middle, right, vertical stretching
+ this._drawTile(
+ ex,
+ this._canvasF,
+
+ new Vector(
+ this._config.sourceConfig.rightMargin,
+
+ this._config.height - this._config.sourceConfig.bottomMargin - this._config.sourceConfig.topMargin
+ ),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+
+ this._config.width - this._config.sourceConfig.rightMargin,
+ this._config.sourceConfig.topMargin
+ );
+
+ // bottom left, no stretching
+ this._drawTile(
+ ex,
+ this._canvasG,
+ new Vector(this._config.sourceConfig.leftMargin, this._config.sourceConfig.bottomMargin),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+ 0,
+
+ this._config.height - this._config.sourceConfig.bottomMargin
+ );
+
+ // bottom middle, horizontal stretching
+ this._drawTile(
+ ex,
+ this._canvasH,
+
+ new Vector(
+ this._config.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin,
+ this._config.sourceConfig.bottomMargin
+ ),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+ this._config.sourceConfig.leftMargin,
+
+ this._config.height - this._config.sourceConfig.bottomMargin
+ );
+
+ // bottom right, no stretching
+ this._drawTile(
+ ex,
+ this._canvasI,
+ new Vector(this._config.sourceConfig.rightMargin, this._config.sourceConfig.bottomMargin),
+ this._config.destinationConfig.horizontalStretch,
+ this._config.destinationConfig.verticalStretch,
+
+ this._config.width - this._config.sourceConfig.rightMargin,
+
+ this._config.height - this._config.sourceConfig.bottomMargin
+ );
+ } else {
+ this._logger.warnOnce(
+ `NineSlice ImageSource ${this._imgSource.path}` +
+ ` is not yet loaded and won't be drawn. Please call .load() or include in a Loader.\n\n` +
+ `Read https://excaliburjs.com/docs/imagesource for more information.`
+ );
+ }
+ }
+
+ /**
+ * Slices the source sprite into the 9 slice canvases internally
+ */
+ protected _initialize() {
+ this._sourceSprite = this._imgSource.image;
+
+ // top left slice
+ this._canvasA.width = this._config.sourceConfig.leftMargin;
+ this._canvasA.height = this._config.sourceConfig.topMargin;
+ const aCtx = this._canvasA.getContext('2d');
+
+ aCtx?.drawImage(this._sourceSprite, 0, 0, this._canvasA.width, this._canvasA.height, 0, 0, this._canvasA.width, this._canvasA.height);
+
+ // top slice
+
+ this._canvasB.width = this._config.sourceConfig.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin;
+ this._canvasB.height = this._config.sourceConfig.topMargin;
+
+ const bCtx = this._canvasB.getContext('2d');
+ bCtx?.drawImage(
+ this._sourceSprite,
+ this._config.sourceConfig.leftMargin,
+ 0,
+ this._canvasB.width,
+ this._canvasB.height,
+ 0,
+ 0,
+ this._canvasB.width,
+ this._canvasB.height
+ );
+
+ // top right slice
+ this._canvasC.width = this._config.sourceConfig.rightMargin;
+ this._canvasC.height = this._config.sourceConfig.topMargin;
+ const cCtx = this._canvasC.getContext('2d');
+ cCtx?.drawImage(
+ this._sourceSprite,
+ this._sourceSprite.width - this._config.sourceConfig.rightMargin,
+ 0,
+ this._canvasC.width,
+ this._canvasC.height,
+ 0,
+ 0,
+ this._canvasC.width,
+ this._canvasC.height
+ );
+
+ // middle left slice
+ this._canvasD.width = this._config.sourceConfig.leftMargin;
+ this._canvasD.height = this._config.sourceConfig.height - this._config.sourceConfig.topMargin - this._config.sourceConfig.bottomMargin;
+ const dCtx = this._canvasD.getContext('2d');
+ dCtx?.drawImage(
+ this._sourceSprite,
+ 0,
+ this._config.sourceConfig.topMargin,
+ this._canvasD.width,
+ this._canvasD.height,
+ 0,
+ 0,
+ this._canvasD.width,
+ this._canvasD.height
+ );
+
+ // middle slice
+ this._canvasE.width = this._config.sourceConfig.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin;
+ this._canvasE.height = this._config.sourceConfig.height - this._config.sourceConfig.topMargin - this._config.sourceConfig.bottomMargin;
+ const eCtx = this._canvasE.getContext('2d');
+ eCtx?.drawImage(
+ this._sourceSprite,
+ this._config.sourceConfig.leftMargin,
+ this._config.sourceConfig.topMargin,
+ this._canvasE.width,
+ this._canvasE.height,
+ 0,
+ 0,
+ this._canvasE.width,
+ this._canvasE.height
+ );
+
+ // middle right slice
+ this._canvasF.width = this._config.sourceConfig.rightMargin;
+ this._canvasF.height = this._config.sourceConfig.height - this._config.sourceConfig.topMargin - this._config.sourceConfig.bottomMargin;
+ const fCtx = this._canvasF.getContext('2d');
+ fCtx?.drawImage(
+ this._sourceSprite,
+
+ this._config.sourceConfig.width - this._config.sourceConfig.rightMargin,
+ this._config.sourceConfig.topMargin,
+ this._canvasF.width,
+ this._canvasF.height,
+ 0,
+ 0,
+ this._canvasF.width,
+ this._canvasF.height
+ );
+
+ // bottom left slice
+ this._canvasG.width = this._config.sourceConfig.leftMargin;
+ this._canvasG.height = this._config.sourceConfig.bottomMargin;
+ const gCtx = this._canvasG.getContext('2d');
+ gCtx?.drawImage(
+ this._sourceSprite,
+ 0,
+ this._config.sourceConfig.height - this._config.sourceConfig.bottomMargin,
+ this._canvasG.width,
+ this._canvasG.height,
+ 0,
+ 0,
+ this._canvasG.width,
+ this._canvasG.height
+ );
+
+ // bottom slice
+ this._canvasH.width = this._config.sourceConfig.width - this._config.sourceConfig.leftMargin - this._config.sourceConfig.rightMargin;
+ this._canvasH.height = this._config.sourceConfig.bottomMargin;
+ const hCtx = this._canvasH.getContext('2d');
+ hCtx?.drawImage(
+ this._sourceSprite,
+ this._config.sourceConfig.leftMargin,
+ this._config.sourceConfig.height - this._config.sourceConfig.bottomMargin,
+ this._canvasH.width,
+ this._canvasH.height,
+ 0,
+ 0,
+ this._canvasH.width,
+ this._canvasH.height
+ );
+
+ // bottom right slice
+ this._canvasI.width = this._config.sourceConfig.rightMargin;
+ this._canvasI.height = this._config.sourceConfig.bottomMargin;
+ const iCtx = this._canvasI.getContext('2d');
+ iCtx?.drawImage(
+ this._sourceSprite,
+ this._sourceSprite.width - this._config.sourceConfig.rightMargin,
+ this._config.sourceConfig.height - this._config.sourceConfig.bottomMargin,
+ this._canvasI.width,
+ this._canvasI.height,
+ 0,
+ 0,
+ this._canvasI.width,
+ this._canvasI.height
+ );
+ }
+
+ /**
+ * Clones the 9 slice
+ */
+
+ clone(): NineSlice {
+ return new NineSlice(this._config);
+ }
+
+ /**
+ * Returns the number of tiles
+ */
+ protected _getNumberOfTiles(tileSize: number, destinationSize: number, strategy: NineSliceStretch): number {
+ switch (strategy) {
+ case NineSliceStretch.Stretch:
+ return 1;
+ case NineSliceStretch.Tile:
+ return Math.ceil(destinationSize / tileSize);
+ case NineSliceStretch.TileFit:
+ return Math.ceil(destinationSize / tileSize);
+ }
+ }
+
+ /**
+ * Returns the position and size of the tile
+ */
+ protected _calculateParams(
+ tileNum: number,
+ numTiles: number,
+ tileSize: number,
+ destinationSize: number,
+ strategy: NineSliceStretch
+ ): { tempPosition: number; tempSize: number } {
+ switch (strategy) {
+ case NineSliceStretch.Stretch:
+ return {
+ tempPosition: 0,
+ tempSize: destinationSize
+ };
+ case NineSliceStretch.Tile:
+ // if last tile, adjust size
+ if (tileNum === numTiles - 1) {
+ //last tile
+ return {
+ tempPosition: tileNum * tileSize,
+ tempSize: tileSize - (numTiles * tileSize - destinationSize)
+ };
+ } else {
+ return {
+ tempPosition: tileNum * tileSize,
+ tempSize: tileSize
+ };
+ }
+
+ case NineSliceStretch.TileFit:
+ const reducedTileSize = destinationSize / numTiles;
+ const position = tileNum * reducedTileSize;
+ return {
+ tempPosition: position,
+ tempSize: reducedTileSize
+ };
+ }
+ }
+}
diff --git a/src/engine/Graphics/index.ts b/src/engine/Graphics/index.ts
index 6ab82bae7..8c4055e54 100644
--- a/src/engine/Graphics/index.ts
+++ b/src/engine/Graphics/index.ts
@@ -25,6 +25,7 @@ export * from './Font';
export * from './FontCache';
export * from './SpriteFont';
export * from './Canvas';
+export * from './NineSlice';
export * from './Context/ExcaliburGraphicsContext';
export * from './Context/ExcaliburGraphicsContext2DCanvas';
diff --git a/src/spec/NineSliceSpec.ts b/src/spec/NineSliceSpec.ts
new file mode 100644
index 000000000..13053b7dc
--- /dev/null
+++ b/src/spec/NineSliceSpec.ts
@@ -0,0 +1,213 @@
+import * as ex from '@excalibur';
+import { ExcaliburAsyncMatchers, ExcaliburMatchers } from 'excalibur-jasmine';
+
+const inputTile = new ex.ImageSource('src/spec/images/GraphicsNineSliceSpec/InputTile.png');
+
+describe('A NineSlice', () => {
+ let canvasElement: HTMLCanvasElement;
+
+ let ctx: ex.ExcaliburGraphicsContext;
+
+ let testGraphicConfigSmall: ex.NineSliceConfig;
+ let testGraphicConfigStretch: ex.NineSliceConfig;
+ let testGraphicConfigTile: ex.NineSliceConfig;
+ let testGraphicConfigTileFit: ex.NineSliceConfig;
+ let testGraphicConfigNoCenter: ex.NineSliceConfig;
+
+ beforeAll(async () => {
+ await inputTile.load();
+ testGraphicConfigSmall = {
+ width: 64,
+ height: 64,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 6,
+ bottomMargin: 5,
+ rightMargin: 6
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: ex.NineSliceStretch.Stretch,
+ verticalStretch: ex.NineSliceStretch.Stretch
+ }
+ };
+ testGraphicConfigStretch = {
+ width: 128,
+ height: 128,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 6,
+ bottomMargin: 5,
+ rightMargin: 6
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: ex.NineSliceStretch.Stretch,
+ verticalStretch: ex.NineSliceStretch.Stretch
+ }
+ };
+ testGraphicConfigTile = {
+ width: 128,
+ height: 128,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 6,
+ bottomMargin: 5,
+ rightMargin: 6
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: ex.NineSliceStretch.Tile,
+ verticalStretch: ex.NineSliceStretch.Tile
+ }
+ };
+ testGraphicConfigTileFit = {
+ width: 128,
+ height: 128,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 6,
+ bottomMargin: 5,
+ rightMargin: 6
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: ex.NineSliceStretch.TileFit,
+ verticalStretch: ex.NineSliceStretch.TileFit
+ }
+ };
+ testGraphicConfigNoCenter = {
+ width: 64,
+ height: 64,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 6,
+ bottomMargin: 5,
+ rightMargin: 6
+ },
+ destinationConfig: {
+ drawCenter: false,
+ horizontalStretch: ex.NineSliceStretch.Stretch,
+ verticalStretch: ex.NineSliceStretch.Stretch
+ }
+ };
+ });
+ beforeEach(() => {
+ jasmine.addMatchers(ExcaliburMatchers);
+ jasmine.addAsyncMatchers(ExcaliburAsyncMatchers);
+ canvasElement = document.createElement('canvas');
+ canvasElement.width = 64;
+ canvasElement.height = 64;
+ ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement });
+ });
+
+ it('can exist', () => {
+ expect(ex.NineSlice).toBeDefined();
+ });
+
+ it('can have its config modified', () => {
+ const dummyConfig = {
+ width: 64,
+ height: 64,
+ source: inputTile,
+ sourceConfig: {
+ width: 64,
+ height: 64,
+ topMargin: 5,
+ leftMargin: 6,
+ bottomMargin: 6,
+ rightMargin: 5
+ },
+ destinationConfig: {
+ drawCenter: true,
+ horizontalStretch: ex.NineSliceStretch.Stretch,
+ verticalStretch: ex.NineSliceStretch.Stretch
+ }
+ };
+
+ const sut = new ex.NineSlice(dummyConfig);
+
+ sut.setMargins(1, 2, 3, 4);
+ expect(sut.getConfig().sourceConfig.leftMargin).toBe(1);
+ expect(sut.getConfig().sourceConfig.topMargin).toBe(2);
+ expect(sut.getConfig().sourceConfig.rightMargin).toBe(3);
+ expect(sut.getConfig().sourceConfig.bottomMargin).toBe(4);
+
+ sut.setTargetWidth(256);
+ expect(sut.getConfig().width).toBe(256);
+ sut.setTargetHeight(256);
+ expect(sut.getConfig().height).toBe(256);
+
+ sut.setStretch('horizontal', ex.NineSliceStretch.Tile);
+ sut.setStretch('vertical', ex.NineSliceStretch.TileFit);
+ expect(sut.getConfig().destinationConfig.horizontalStretch).toBe(ex.NineSliceStretch.Tile);
+ expect(sut.getConfig().destinationConfig.verticalStretch).toBe(ex.NineSliceStretch.TileFit);
+ });
+
+ it('can clone the Graphic', () => {
+ const sut = new ex.NineSlice(testGraphicConfigSmall);
+ const newSut = sut.clone();
+ expect(newSut).not.toBe(sut);
+ expect(newSut).toBeInstanceOf(ex.NineSlice);
+ });
+
+ it('can draw a copy of input tile', async () => {
+ canvasElement.width = 64;
+ canvasElement.height = 64;
+ ctx.clear();
+ const sut = new ex.NineSlice(testGraphicConfigSmall);
+ sut.draw(ctx, 0, 0);
+ await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsNineSliceSpec/InputTile.png');
+ });
+
+ it('can draw a tiled copy of input tile', async () => {
+ canvasElement.width = 128;
+ canvasElement.height = 128;
+ ctx.clear();
+ const sut = new ex.NineSlice(testGraphicConfigTile);
+ sut.draw(ctx, 0, 0);
+ await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.png');
+ });
+
+ it('can draw a fitted tile copy of input tile', async () => {
+ canvasElement.width = 128;
+ canvasElement.height = 128;
+ ctx.clear();
+ const sut = new ex.NineSlice(testGraphicConfigTileFit);
+ sut.draw(ctx, 0, 0);
+ await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.png');
+ });
+
+ it('can draw a stretched copy of input tile', async () => {
+ canvasElement.width = 128;
+ canvasElement.height = 128;
+ ctx.clear();
+ const sut = new ex.NineSlice(testGraphicConfigStretch);
+ sut.draw(ctx, 0, 0);
+ await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.png');
+ });
+
+ it('can draw the frame of a tile with no center', async () => {
+ canvasElement.width = 64;
+ canvasElement.height = 64;
+ ctx.clear();
+ const sut = new ex.NineSlice(testGraphicConfigNoCenter);
+ sut.draw(ctx, 0, 0);
+ await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsNineSliceSpec/resultImage_64_64_noCenter.png');
+ });
+});
diff --git a/src/spec/images/GraphicsNineSliceSpec/InputTile.png b/src/spec/images/GraphicsNineSliceSpec/InputTile.png
new file mode 100644
index 000000000..9df9c61cc
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/InputTile.png differ
diff --git a/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.aseprite b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.aseprite
new file mode 100644
index 000000000..397375f16
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.aseprite differ
diff --git a/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.png b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.png
new file mode 100644
index 000000000..a0e91d193
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_stretch_h_stretch.png differ
diff --git a/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.aseprite b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.aseprite
new file mode 100644
index 000000000..9e682a983
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.aseprite differ
diff --git a/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.png b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.png
new file mode 100644
index 000000000..fd315ecbe
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tile_h_tile.png differ
diff --git a/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.aseprite b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.aseprite
new file mode 100644
index 000000000..b5a7fbce3
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.aseprite differ
diff --git a/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.png b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.png
new file mode 100644
index 000000000..a658d3ccb
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/resultImage_128_128_v_tilefit_h_tilefit.png differ
diff --git a/src/spec/images/GraphicsNineSliceSpec/resultImage_64_64_noCenter.png b/src/spec/images/GraphicsNineSliceSpec/resultImage_64_64_noCenter.png
new file mode 100644
index 000000000..99cd1e1f4
Binary files /dev/null and b/src/spec/images/GraphicsNineSliceSpec/resultImage_64_64_noCenter.png differ