├── 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 | --------------------------------------------------------------------------------