├── test_normal.9.png
├── index.html
├── index.js
├── LICENSE
├── README.md
└── NinePatch.js
/test_normal.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/completejavascript/nine-patch-js/HEAD/test_normal.9.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Nine Patch Test
5 |
6 |
7 | Show original nine-patch Image:
8 |
9 |
10 | Show image after scaling with handling nine-patch Image:
11 |
12 |
13 | Show image after scaling without handling nine-patch image:
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const srcImg = 'test_normal.9.png';
2 | const WIDTH = 150;
3 | const HEIGHT = 150;
4 |
5 | document.addEventListener("DOMContentLoaded", event => {
6 | let $ = document.querySelector.bind(document);
7 |
8 | new NinePatch().getSize(srcImg)
9 | .then(result => setImage($('#ninePatchImg'), result.url, result.width, result.height))
10 | .catch(error => console.log(error));
11 |
12 | new NinePatch().scaleImage(srcImg, WIDTH, HEIGHT)
13 | .then(result => setImage($('#normalImg'), result, WIDTH, HEIGHT))
14 | .catch(error => console.log(error));
15 |
16 | new NinePatch().getSize(srcImg)
17 | .then(result => setImage($('#testImg'), result.url, result.width + 50, result.height + 100))
18 | .catch(error => console.log(error));
19 | });
20 |
21 | function setImage(divElement, srcURL, width, height) {
22 | divElement.style.width = width + 'px';
23 | divElement.style.height = height + 'px';
24 | divElement.style.backgroundSize = '' + width + 'px ' + height + 'px';
25 | divElement.style.backgroundImage = "url('" + srcURL + "')";
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Lam Pham
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## NinePatch.js
2 | Scale nine-patch image using JavaScript's canvas.
3 |
4 | ## Usage
5 | * APIs:
6 | + getSize(srcImg) => {url, width, height}: 'url' is the original image's url
7 | + scaleImage(srcImg, newWidth, newHeight) => url: 'url' is the new image's url, which is scaled.
8 |
9 | ## Example
10 |
11 | ```js
12 | const srcImg = 'test_normal.9.png';
13 | const WIDTH = 150;
14 | const HEIGHT = 150;
15 |
16 | document.addEventListener("DOMContentLoaded", event => {
17 | let $ = document.querySelector.bind(document);
18 |
19 | new NinePatch().getSize(srcImg)
20 | .then(result => setImage($('#ninePatchImg'), result.url, result.width, result.height))
21 | .catch(error => console.log(error));
22 |
23 | new NinePatch().scaleImage(srcImg, WIDTH, HEIGHT)
24 | .then(result => setImage($('#normalImg'), result, WIDTH, HEIGHT))
25 | .catch(error => console.log(error));
26 |
27 | new NinePatch().getSize(srcImg)
28 | .then(result => setImage($('#testImg'), result.url, result.width + 50, result.height + 100))
29 | .catch(error => console.log(error));
30 | });
31 |
32 | function setImage(divElement, srcURL, width, height) {
33 | divElement.style.width = width + 'px';
34 | divElement.style.height = height + 'px';
35 | divElement.style.backgroundSize = '' + width + 'px ' + height + 'px';
36 | divElement.style.backgroundImage = "url('" + srcURL + "')";
37 | }
38 |
39 | ```
40 | ## References
41 |
42 | * [Scale Nine-patch Image using NinePatch.js](https://codepen.io/completejavascript/pen/opOvaP)
43 |
44 | ## Visit me
45 |
46 | * [Complete JavaScript](https://completejavascript.com)
47 |
48 |
49 |
--------------------------------------------------------------------------------
/NinePatch.js:
--------------------------------------------------------------------------------
1 | class NinePatch {
2 | // New version
3 | scaleImage(srcImg, newWidth, newHeight) {
4 | return new Promise((resolve, reject) => {
5 | // Load 9patch from background-image
6 | this.bgImage = new Image();
7 | this.bgImage.crossOrigin = "Anonymous";
8 | this.bgImage.src = srcImg;
9 | this.bgImage.onload = () => {
10 | let srcWidth = this.bgImage.width - 2;
11 | let srcHeight = this.bgImage.height - 2;
12 |
13 | // Handle scale down
14 | let ratio = 1;
15 | if (newWidth < srcWidth || newHeight < srcHeight) {
16 | ratio = Math.max(srcWidth / newWidth, srcHeight / newHeight);
17 | }
18 |
19 | let destWidth = (newWidth * ratio).toFixed();
20 | let destHeight = (newHeight * ratio).toFixed();
21 |
22 | // Create a temporary canvas to get the 9Patch index data.
23 | let cvs, ctx;
24 | cvs = document.createElement('canvas');
25 | ctx = cvs.getContext('2d');
26 | ctx.drawImage(this.bgImage, 0, 0);
27 |
28 | // Loop over each horizontal pixel and get piece
29 | let data = ctx.getImageData(0, 0, this.bgImage.width, 1).data;
30 |
31 | // Use the upper-left corner to get staticColor, use the upper-right corner
32 | // to get the repeatColor.
33 | let tmpLen = data.length - 4;
34 | let staticColor = this._getColorPattern(data[0], data[1], data[2], data[3]);
35 | let repeatColor = this._getColorPattern(data[tmpLen], data[tmpLen + 1], data[tmpLen + 2], data[tmpLen + 3]);
36 |
37 | this.horizontalPieces = this._getPieces(data, staticColor, repeatColor);
38 |
39 | // Loop over each vertical pixel and get piece
40 | data = ctx.getImageData(0, 0, 1, this.bgImage.height).data;
41 | this.verticalPieces = this._getPieces(data, staticColor, repeatColor);
42 |
43 | resolve(this._draw(destWidth, destHeight));
44 | }
45 | this.bgImage.onerror = error => reject(error);
46 | });
47 | }
48 |
49 | sliceBorder(srcImg, newWidth, newHeight) {
50 | return new Promise((resolve, reject) => {
51 | // Load 9patch from background-image
52 | this.bgImage = new Image();
53 | this.bgImage.crossOrigin = "Anonymous";
54 | this.bgImage.src = srcImg;
55 | this.bgImage.onload = () => {
56 | let srcWidth = this.bgImage.width - 2;
57 | let srcHeight = this.bgImage.height - 2;
58 |
59 | // Handle scale down
60 | let ratio = 1;
61 | if (newWidth < srcWidth || newHeight < srcHeight) {
62 | ratio = Math.max(srcWidth / newWidth, srcHeight / newHeight);
63 | }
64 |
65 | let dCtx, dCanvas;
66 | dCanvas = document.createElement('canvas');
67 | dCtx = dCanvas.getContext('2d');
68 | dCanvas.width = (srcWidth * ratio).toFixed();
69 | dCanvas.height = (srcHeight * ratio).toFixed();
70 |
71 | dCtx.drawImage(
72 | this.bgImage,
73 | 1, 1,
74 | srcWidth, srcHeight,
75 | 0, 0,
76 | dCanvas.width, dCanvas.height);
77 |
78 | resolve(dCanvas.toDataURL("image/png"));
79 | }
80 | this.bgImage.onerror = error => reject(error);
81 | });
82 | }
83 |
84 | /**
85 | * s: static, r: repeat, d: dynamic
86 | */
87 | _getType(tempColor, staticColor, repeatColor) {
88 | return (tempColor == staticColor ? 's' : (tempColor == repeatColor ? 'r' : 'd'));
89 | }
90 |
91 | _getColorPattern() {
92 | return Array.from(arguments).join(',');
93 | }
94 |
95 | _getPieces(data, staticColor, repeatColor) {
96 | let curType, tempPosition, tempWidth, tempColor, tempType;
97 | let tempArray = [];
98 |
99 | tempColor = this._getColorPattern(data[4], data[5], data[6], data[7]);
100 | curType = this._getType(tempColor, staticColor, repeatColor);
101 | tempPosition = 1;
102 |
103 | for (var i = 4, n = data.length - 4; i < n; i += 4) {
104 | tempColor = this._getColorPattern(data[i], data[i + 1], data[i + 2], data[i + 3]);
105 | tempType = this._getType(tempColor, staticColor, repeatColor);
106 | if (curType != tempType) {
107 | // box changed colors
108 | tempWidth = (i / 4) - tempPosition;
109 | tempArray.push([curType, tempPosition, tempWidth]);
110 |
111 | curType = tempType;
112 | tempPosition = i / 4;
113 | tempWidth = 1;
114 | }
115 | }
116 |
117 | // push end
118 | tempWidth = (i / 4) - tempPosition;
119 | tempArray.push([curType, tempPosition, tempWidth]);
120 |
121 | return tempArray;
122 | }
123 |
124 | _draw(dWidth, dHeight) {
125 | let dCanvas = document.createElement('canvas');
126 | let dCtx = dCanvas.getContext('2d');
127 | dCanvas.width = dWidth;
128 | dCanvas.height = dHeight;
129 |
130 | // Determine the width for the static and dynamic pieces
131 | let tempStaticWidth = 0;
132 | let tempDynamicCount = 0;
133 |
134 | for (let i = 0; i < this.horizontalPieces.length; i++) {
135 | if (this.horizontalPieces[i][0] == 's') {
136 | tempStaticWidth += this.horizontalPieces[i][2];
137 | } else {
138 | tempDynamicCount++;
139 | }
140 | }
141 |
142 | let totalDynamicWidth = (dWidth - tempStaticWidth) / tempDynamicCount;
143 |
144 | // Determine the height for the static and dynamic pieces
145 | var tempStaticHeight = 0;
146 | tempDynamicCount = 0;
147 | for (let i = 0; i < this.verticalPieces.length; i++) {
148 | if (this.verticalPieces[i][0] == 's') {
149 | tempStaticHeight += this.verticalPieces[i][2];
150 | } else {
151 | tempDynamicCount++;
152 | }
153 | }
154 |
155 | let totalDynamicHeight = (dHeight - tempStaticHeight) / tempDynamicCount;
156 |
157 | // Loop through each of the vertical/horizontal pieces and draw on
158 | // the canvas
159 | for (let i = 0; i < this.verticalPieces.length; i++) {
160 | for (let j = 0; j < this.horizontalPieces.length; j++) {
161 | let tempFillWidth = (this.horizontalPieces[j][0] == 'd') ? totalDynamicWidth : this.horizontalPieces[j][2];
162 | let tempFillHeight = (this.verticalPieces[i][0] == 'd') ? totalDynamicHeight : this.verticalPieces[i][2];
163 |
164 | // Stretching :
165 | let tempCanvas = document.createElement('canvas');
166 | tempCanvas.width = this.horizontalPieces[j][2];
167 | tempCanvas.height = this.verticalPieces[i][2];
168 |
169 | let tempCtx = tempCanvas.getContext('2d');
170 | tempCtx.drawImage(this.bgImage,
171 | this.horizontalPieces[j][1], this.verticalPieces[i][1],
172 | this.horizontalPieces[j][2], this.verticalPieces[i][2],
173 | 0, 0,
174 | this.horizontalPieces[j][2], this.verticalPieces[i][2]);
175 |
176 | let tempPattern = dCtx.createPattern(tempCanvas, 'repeat');
177 | dCtx.fillStyle = tempPattern;
178 | dCtx.fillRect(
179 | 0, 0,
180 | tempFillWidth, tempFillHeight);
181 |
182 | // Shift to next x position
183 | dCtx.translate(tempFillWidth, 0);
184 | }
185 |
186 | // shift back to 0 x and down to the next line
187 | dCtx.translate(-dWidth, (this.verticalPieces[i][0] == 's' ? this.verticalPieces[i][2] : totalDynamicHeight));
188 | }
189 |
190 | // store the canvas as the div's background
191 | return dCanvas.toDataURL("image/png");
192 | }
193 |
194 | // Old Version
195 | _scaleImage(srcImg, newWidth, newHeight) {
196 | return new Promise((resolve, reject) => {
197 | let canvas = document.createElement('canvas');
198 | let newCanvas = document.createElement('canvas');
199 | let context = canvas.getContext('2d');
200 | let newContext = newCanvas.getContext('2d');
201 | let image = new Image();
202 | image.crossOrigin = "Anonymous";
203 | image.onload = () => {
204 | canvas.width = image.width;
205 | canvas.height = image.height;
206 |
207 | // Handle scale down
208 | let ratio = 1;
209 | if(newWidth < image.width || newHeight < image.height) {
210 | ratio = Math.max(image.width / newWidth, image.height / newHeight);
211 | }
212 | newCanvas.width = newWidth * ratio;
213 | newCanvas.height = newHeight * ratio;
214 |
215 | context.drawImage(image, 0, 0);
216 |
217 | let offset = this._getOffsetFromCanvas(canvas, context);
218 | this._scale(canvas, context, newCanvas, newContext, offset);
219 | resolve(newCanvas.toDataURL());
220 | };
221 | image.onerror = error => reject(error);
222 | image.src = srcImg;
223 | });
224 | }
225 |
226 | _isTransparent(rgbArray) {
227 | return (rgbArray[0] == 0 && rgbArray[1] == 0 && rgbArray[2] == 0 && rgbArray[3] == 0);
228 | }
229 |
230 | _getOffsetFromCanvas(canvas, context) {
231 | let offset = {
232 | top: 0,
233 | bottom: 0,
234 | left: 0,
235 | right: 0
236 | }
237 | // Get top offset
238 | for (let y = 0; y < canvas.height; y++) {
239 | let p = context.getImageData(0, y, 1, 1).data;
240 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.top++;
241 | else break;
242 | }
243 | // Get bottom offset
244 | for (let y = canvas.height - 1; y >= 0; y--) {
245 | let p = context.getImageData(0, y, 1, 1).data;
246 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.bottom++;
247 | else break;
248 | }
249 | // Get left offset
250 | for (let x = 0; x < canvas.width; x++) {
251 | let p = context.getImageData(x, 0, 1, 1).data;
252 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.left++;
253 | else break;
254 | }
255 | // Get right offset
256 | for (let x = canvas.width - 1; x >= 0; x--) {
257 | let p = context.getImageData(x, 0, 1, 1).data;
258 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.right++;
259 | else break;
260 | }
261 | return offset;
262 | }
263 |
264 | getSize(srcImg) {
265 | return new Promise((resolve, reject) => {
266 | let image = new Image();
267 | image.crossOrigin = "Anonymous";
268 | image.onload = () => resolve({ width: image.width, height: image.height, url: srcImg });
269 | image.onerror = error => reject(error);
270 | image.src = srcImg;
271 | });
272 | }
273 |
274 | _scale(canvas, context, newCanvas, newContext, offset) {
275 | // copy top-left corner, ignore 1px from the left
276 | let rootX = 1, rootY = 1;
277 | let newX = 0, newY = 0;
278 | let imageData;
279 | if (offset.left - 1 > 0 && offset.top - 1 > 0) {
280 | imageData = context.getImageData(rootX, rootY, offset.left - 1, offset.top - 1);
281 | newContext.putImageData(imageData, newX, newY);
282 | }
283 |
284 | // copy top-right corner, ignore 1px from the right
285 | rootX = canvas.width - offset.right; rootY = 1;
286 | newX = newCanvas.width - offset.right; newY = 0;
287 | if (offset.right - 1 > 0 && offset.top - 1 > 0) {
288 | imageData = context.getImageData(rootX, rootY, offset.right - 1, offset.top - 1);
289 | newContext.putImageData(imageData, newX, newY);
290 | }
291 |
292 | // copy bottom-right corner
293 | rootX = canvas.width - offset.right; rootY = canvas.height - offset.bottom;
294 | newX = newCanvas.width - offset.right; newY = newCanvas.height - offset.bottom;
295 | imageData = context.getImageData(rootX, rootY, offset.right - 1, offset.bottom - 1);
296 | newContext.putImageData(imageData, newX, newY);
297 |
298 | // copy bottom-left corner
299 | rootX = 1; rootY = canvas.height - offset.bottom;
300 | newX = 0; newY = newCanvas.height - offset.bottom;
301 | if (offset.left - 1 > 0 && offset.bottom - 1 > 0) {
302 | imageData = context.getImageData(rootX, rootY, offset.left - 1, offset.bottom - 1);
303 | newContext.putImageData(imageData, newX, newY);
304 | }
305 |
306 | // scale middle top
307 | rootX = offset.left; rootY = 1;
308 | if (offset.top - 1 > 0) {
309 | imageData = context.getImageData(rootX, rootY, 1, offset.top - 1);
310 | for (let x = offset.left - 1; x <= newCanvas.width - offset.right; x++) {
311 | newContext.putImageData(imageData, x, 0);
312 | }
313 | }
314 |
315 | // scale middle bottom
316 | rootX = offset.left; rootY = canvas.height - offset.bottom;
317 | if (offset.bottom - 1 > 0) {
318 | imageData = context.getImageData(rootX, rootY, 1, offset.bottom - 1);
319 | for (let x = offset.left - 1; x <= newCanvas.width - offset.right; x++) {
320 | newContext.putImageData(imageData, x, newCanvas.height - offset.bottom);
321 | }
322 | }
323 |
324 | // scale middle left
325 | rootX = 1; rootY = offset.top;
326 | if (offset.left - 1 > 0) {
327 | imageData = context.getImageData(rootX, rootY, offset.left - 1, 1);
328 | for (let y = offset.top - 1; y <= newCanvas.height - offset.top; y++) {
329 | newContext.putImageData(imageData, 0, y);
330 | }
331 | }
332 |
333 | // scale middle right
334 | rootX = canvas.width - offset.right; rootY = offset.top;
335 | if (offset.right - 1 > 0) {
336 | imageData = context.getImageData(rootX, rootY, offset.right - 1, 1);
337 | for (let y = offset.top - 1; y <= newCanvas.height - offset.top; y++) {
338 | newContext.putImageData(imageData, newCanvas.width - offset.right, y);
339 | }
340 | }
341 |
342 | // scale center
343 | rootX = offset.left; rootY = offset.top;
344 | imageData = context.getImageData(rootX, rootY, 1, 1);
345 | for (let y = offset.top - 1; y <= newCanvas.height - offset.bottom; y++) {
346 | newContext.putImageData(imageData, offset.left - 1, y);
347 | }
348 | let centerHeight = newCanvas.height - offset.bottom - offset.top;
349 | if (centerHeight > 0) {
350 | imageData = newContext.getImageData(offset.left - 1, offset.top - 1, 1, newCanvas.height - offset.bottom - offset.top);
351 | for (let x = offset.left; x <= newCanvas.width - offset.right; x++) {
352 | newContext.putImageData(imageData, x, offset.top - 1);
353 | }
354 | }
355 | }
356 |
357 | _getOffset(srcImg) {
358 | return new Promise((resolve, reject) => {
359 | let offset = { top: 0, right: 0, bottom: 0, left: 0 }
360 | let canvas = document.createElement('canvas');
361 | let context = canvas.getContext('2d');
362 | let image = new Image();
363 | image.crossOrigin = "Anonymous";
364 | image.onload = () => {
365 | canvas.width = image.width;
366 | canvas.height = image.height;
367 | context.drawImage(image, 0, 0, image.width, image.height);
368 |
369 | // Get top offset
370 | for (let y = 0; y < canvas.height; y++) {
371 | let p = context.getImageData(0, y, 1, 1).data;
372 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.top++;
373 | else break;
374 | }
375 | // Get bottom offset
376 | for (let y = canvas.height - 1; y >= 0; y--) {
377 | let p = context.getImageData(0, y, 1, 1).data;
378 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.bottom++;
379 | else break;
380 | }
381 | // Get left offset
382 | for (let x = 0; x < canvas.width; x++) {
383 | let p = context.getImageData(x, 0, 1, 1).data;
384 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.left++;
385 | else break;
386 | }
387 | // Get right offset
388 | for (let x = canvas.width - 1; x >= 0; x--) {
389 | let p = context.getImageData(x, 0, 1, 1).data;
390 | if (this._isTransparent([p[0], p[1], p[2], p[3]])) offset.right++;
391 | else break;
392 | }
393 | resolve(offset);
394 | };
395 | image.onerror = error => reject(error);
396 | image.src = srcImg;
397 | });
398 | }
399 | }
400 |
401 | /*
402 | MIT License
403 |
404 | Copyright (c) 2018 Lam Pham
405 |
406 | Permission is hereby granted, free of charge, to any person obtaining a copy
407 | of this software and associated documentation files (the "Software"), to deal
408 | in the Software without restriction, including without limitation the rights
409 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
410 | copies of the Software, and to permit persons to whom the Software is
411 | furnished to do so, subject to the following conditions:
412 |
413 | The above copyright notice and this permission notice shall be included in all
414 | copies or substantial portions of the Software.
415 |
416 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
417 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
418 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
419 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
420 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
421 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
422 | SOFTWARE.
423 | */
424 |
--------------------------------------------------------------------------------