├── .gitattributes ├── LICENSE.txt ├── README.md ├── .gitignore ├── MarchingSquaresOpt.js ├── marchingSquaresTest.html ├── MarchingSquaresOld.js └── MarchingSquares.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sakri Rosenstrom 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Optimized implementation of the [Marching Squares](http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html) algorithm in Javascript 2 | 3 | ![canvas](https://cloud.githubusercontent.com/assets/8630763/18348811/e2d33b10-75cd-11e6-8131-9d57140d5508.png) 4 | 5 | Computation times for 20 iterations: 6 | 7 | | | old | new | optimized | 8 | |------------|------|-----|-----| 9 | | time in ms | 3876 | 443 | 20 | 10 | | time in % | 873 | 100 | **4** | 11 | 12 | The optimized version is approximately 217 times faster than the "old version" and **24 times faster** than the "new version" implemented by [sakri](https://github.com/sakri). 13 | 14 | [ES2015](http://www.ecma-international.org/ecma-262/6.0/) [features](https://kangax.github.io/compat-table/es6/) like TypedArrays, const and local scoping are used to gain this speed up. 15 | 16 | Test it yourself with this online [benchmark / demo](http://htmlpreview.github.io/?https://github.com/mamrehn/MarchingSquaresJS/blob/master/marchingSquaresTest.html). 17 | 18 | ## Original README 19 | 20 | Marching squares outlines blobs of non-transparent pixels inside a bitmap. 21 | 22 | This is a straight forward port from this excellent implementation by Phill Spiess: 23 | 24 | http://devblog.phillipspiess.com/2010/02/23/better-know-an-algorithm-1-marching-squares/ 25 | 26 | This is a Javascript implementation designed to be used with Html5 Canvas. 27 | 28 | Check out the code in marchingSquaresTest.html for usage example. 29 | 30 | http://htmlpreview.github.io/?https://github.com/sakri/MarchingSquaresJS/blob/master/marchingSquaresTest.html 31 | 32 | 33 | Here's a few codepen demos using it: 34 | 35 | A visualization of the algo as it executes 36 | http://codepen.io/sakri/full/aIirl 37 | 38 | Text Edge Flare 39 | http://codepen.io/sakri/full/vIKJp 40 | 41 | Ghost text 42 | http://codepen.io/sakri/full/zbqti 43 | 44 | He-Man effect 45 | http://codepen.io/sakri/full/wsiLC 46 | 47 | 48 | Here's a demo that tackles "blobs with holes" as in getting the outline for characters such as "O" "8" "&" etc. 49 | http://codepen.io/sakri/pen/LCrDe 50 | 51 | If there is interest, I can include the required code in this repo (floodFill, basic threshold etc.) 52 | 53 | 54 | 55 | Have a good day! 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Webstorm 3 | ################# 4 | 5 | .idea/ 6 | .idea/workspace.xml 7 | 8 | ################# 9 | ## Eclipse 10 | ################# 11 | 12 | *.pydevproject 13 | .project 14 | .metadata 15 | bin/ 16 | tmp/ 17 | *.tmp 18 | *.bak 19 | *.swp 20 | *~.nib 21 | local.properties 22 | .classpath 23 | .settings/ 24 | .loadpath 25 | 26 | # External tool builders 27 | .externalToolBuilders/ 28 | 29 | # Locally stored "Eclipse launch configurations" 30 | *.launch 31 | 32 | # CDT-specific 33 | .cproject 34 | 35 | # PDT-specific 36 | .buildpath 37 | 38 | 39 | ################# 40 | ## Visual Studio 41 | ################# 42 | 43 | ## Ignore Visual Studio temporary files, build results, and 44 | ## files generated by popular Visual Studio add-ons. 45 | 46 | # User-specific files 47 | *.suo 48 | *.user 49 | *.sln.docstates 50 | 51 | # Build results 52 | 53 | [Dd]ebug/ 54 | [Rr]elease/ 55 | x64/ 56 | build/ 57 | [Bb]in/ 58 | [Oo]bj/ 59 | 60 | # MSTest test Results 61 | [Tt]est[Rr]esult*/ 62 | [Bb]uild[Ll]og.* 63 | 64 | *_i.c 65 | *_p.c 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.pch 70 | *.pdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.log 86 | *.scc 87 | 88 | # Visual C++ cache files 89 | ipch/ 90 | *.aps 91 | *.ncb 92 | *.opensdf 93 | *.sdf 94 | *.cachefile 95 | 96 | # Visual Studio profiler 97 | *.psess 98 | *.vsp 99 | *.vspx 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | *.ncrunch* 116 | .*crunch*.local.xml 117 | 118 | # Installshield output folder 119 | [Ee]xpress/ 120 | 121 | # DocProject is a documentation generator add-in 122 | DocProject/buildhelp/ 123 | DocProject/Help/*.HxT 124 | DocProject/Help/*.HxC 125 | DocProject/Help/*.hhc 126 | DocProject/Help/*.hhk 127 | DocProject/Help/*.hhp 128 | DocProject/Help/Html2 129 | DocProject/Help/html 130 | 131 | # Click-Once directory 132 | publish/ 133 | 134 | # Publish Web Output 135 | *.Publish.xml 136 | *.pubxml 137 | 138 | # NuGet Packages Directory 139 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 140 | #packages/ 141 | 142 | # Windows Azure Build Output 143 | csx 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.[Pp]ublish.xml 158 | *.pfx 159 | *.publishsettings 160 | 161 | # RIA/Silverlight projects 162 | Generated_Code/ 163 | 164 | # Backup & report files from converting an old project file to a newer 165 | # Visual Studio version. Backup files are not needed, because we have git ;-) 166 | _UpgradeReport_Files/ 167 | Backup*/ 168 | UpgradeLog*.XML 169 | UpgradeLog*.htm 170 | 171 | # SQL Server files 172 | App_Data/*.mdf 173 | App_Data/*.ldf 174 | 175 | ############# 176 | ## Windows detritus 177 | ############# 178 | 179 | # Windows image file caches 180 | Thumbs.db 181 | ehthumbs.db 182 | 183 | # Folder config file 184 | Desktop.ini 185 | 186 | # Recycle Bin used on file shares 187 | $RECYCLE.BIN/ 188 | 189 | # Mac crap 190 | .DS_Store 191 | 192 | 193 | ############# 194 | ## Python 195 | ############# 196 | 197 | *.py[co] 198 | 199 | # Packages 200 | *.egg 201 | *.egg-info 202 | dist/ 203 | build/ 204 | eggs/ 205 | parts/ 206 | var/ 207 | sdist/ 208 | develop-eggs/ 209 | .installed.cfg 210 | 211 | # Installer logs 212 | pip-log.txt 213 | 214 | # Unit test / coverage reports 215 | .coverage 216 | .tox 217 | 218 | #Translations 219 | *.mo 220 | 221 | #Mr Developer 222 | .mr.developer.cfg 223 | -------------------------------------------------------------------------------- /MarchingSquaresOpt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by @sakri on 25-3-14. 3 | * Edited and optimized by @mamrehn on 08-09-16 4 | * 5 | * Javascript port of : 6 | * http://devblog.phillipspiess.com/2010/02/23/better-know-an-algorithm-1-marching-squares/ 7 | * returns an Array of x and y positions defining the perimeter of a blob of non-transparent pixels on a canvas 8 | * 9 | */ 10 | (function (window){ 11 | 12 | const MarchingSquaresOpt = {}; 13 | 14 | MarchingSquaresOpt.getBlobOutlinePoints = function(source_array, width, height=0){ 15 | // Note: object should not be on the border of the array, since there is 16 | // no padding of 1 pixel to handle points which touch edges 17 | 18 | if (source_array instanceof HTMLCanvasElement){ 19 | width = source_array.width; 20 | height = source_array.height; 21 | const data4 = source_array.getContext('2d').getImageData(0, 0, width, height).data, // Uint8ClampedArray 22 | len = width * height, 23 | data = new Uint8Array(len); 24 | for (let i = 0; i < len; ++i){ 25 | data[i] = data4[i << 2]; 26 | } 27 | source_array = data; 28 | } else if (0 == height){ 29 | height = (source_array.length / width)|0; 30 | } 31 | 32 | // find the starting point 33 | const startingPoint = MarchingSquaresOpt.getFirstNonTransparentPixelTopDown(source_array, width, height); 34 | if (null === startingPoint){ 35 | console.log('[Warning] Marching Squares could not find an object in the given array'); 36 | return []; 37 | } 38 | 39 | // return list of w and h positions 40 | return MarchingSquaresOpt.walkPerimeter(source_array, width, height, startingPoint.w, startingPoint.h); 41 | }; 42 | 43 | MarchingSquaresOpt.getFirstNonTransparentPixelTopDown = function(source_array, width, height){ 44 | let idx; 45 | for(let h = 0|0; h < height; ++h){ 46 | idx = (h * width)|0; 47 | for(let w = 0|0; w < width; ++w){ 48 | if(source_array[idx] > 0){ 49 | return {w : w, h : h}; 50 | } 51 | ++idx; 52 | } 53 | } 54 | return null; 55 | }; 56 | 57 | MarchingSquaresOpt.walkPerimeter = function(source_array, width, height, start_w, start_h){ 58 | 59 | width = width|0; 60 | height = height|0; 61 | 62 | // Set up our return list 63 | const point_list = [], 64 | up = 1|0, left = 2|0, down = 3|0, right = 4|0, 65 | step_func = MarchingSquaresOpt.step; 66 | 67 | let idx = 0|0, // Note: initialize it with an integer, so the JS interpreter optimizes for this type. 68 | 69 | // our current x and y positions, initialized 70 | // to the init values passed in 71 | w = start_w, 72 | h = start_h, 73 | 74 | // the main while loop, continues stepping until 75 | // we return to our initial points 76 | next_step; 77 | do { 78 | // evaluate our state, and set up our next direction 79 | idx = (h - 1) * width + (w - 1); 80 | next_step = step_func(idx, source_array, width); 81 | 82 | // if our current point is within our image 83 | // add it to the list of points 84 | if (w >= 0 && w < width && h >= 0 && h < height){ 85 | point_list.push(w - 1, h); 86 | } 87 | 88 | switch (next_step){ 89 | case up: --h; break; 90 | case left: --w; break; 91 | case down: ++h; break; 92 | case right: ++w; break; 93 | default: 94 | break; 95 | } 96 | 97 | } while (w != start_w || h != start_h); 98 | 99 | point_list.push(w, h); 100 | 101 | return point_list; 102 | }; 103 | 104 | // determines and sets the state of the 4 pixels that 105 | // represent our current state, and sets our current and 106 | // previous directions 107 | 108 | MarchingSquaresOpt.step = function(idx, source_array, width){ 109 | //console.log('Sakri.MarchingSquaresOpt.step()'); 110 | // Scan our 4 pixel area 111 | //Sakri.imageData = Sakri.MarchingSquaresOpt.sourceContext.getImageData(x-1, y-1, 2, 2).data; 112 | 113 | const up_left = 0 < source_array[idx + 1], 114 | up_right = 0 < source_array[idx + 2], 115 | down_left = 0 < source_array[idx + width + 1], 116 | down_right = 0 < source_array[idx + width + 2], 117 | none = 0|0, up = 1|0, left = 2|0, down = 3|0, right = 4|0; 118 | 119 | // Determine which state we are in 120 | let state = 0|0; 121 | 122 | if (up_left){state |= 1;} 123 | if (up_right){state |= 2;} 124 | if (down_left){state |= 4;} 125 | if (down_right){state |= 8;} 126 | 127 | // State now contains a number between 0 and 15 128 | // representing our state. 129 | // In binary, it looks like 0000-1111 (in binary) 130 | 131 | // An example. Let's say the top two pixels are filled, 132 | // and the bottom two are empty. 133 | // Stepping through the if statements above with a state 134 | // of 0b0000 initially produces: 135 | // Upper Left == true ==> 0b0001 136 | // Upper Right == true ==> 0b0011 137 | // The others are false, so 0b0011 is our state 138 | // (That's 3 in decimal.) 139 | 140 | // Looking at the chart above, we see that state 141 | // corresponds to a move right, so in our switch statement 142 | // below, we add a case for 3, and assign Right as the 143 | // direction of the next step. We repeat this process 144 | // for all 16 states. 145 | 146 | // So we can use a switch statement to determine our 147 | // next direction based on 148 | switch (state){ 149 | case 1: MarchingSquaresOpt.next_step = up; break; 150 | case 2: MarchingSquaresOpt.next_step = right; break; 151 | case 3: MarchingSquaresOpt.next_step = right; break; 152 | case 4: MarchingSquaresOpt.next_step = left; break; 153 | case 5: MarchingSquaresOpt.next_step = up; break; 154 | case 6: 155 | if (MarchingSquaresOpt.next_step == up){ // info from previous_step 156 | MarchingSquaresOpt.next_step = left; 157 | } else { 158 | MarchingSquaresOpt.next_step = right; 159 | } 160 | break; 161 | case 7: MarchingSquaresOpt.next_step = right; break; 162 | case 8: MarchingSquaresOpt.next_step = down; break; 163 | case 9: 164 | if (MarchingSquaresOpt.next_step == right){ // info from previous_step 165 | MarchingSquaresOpt.next_step = up; 166 | } else { 167 | MarchingSquaresOpt.next_step = down; 168 | } 169 | break; 170 | case 10: MarchingSquaresOpt.next_step = down; break; 171 | case 11: MarchingSquaresOpt.next_step = down; break; 172 | case 12: MarchingSquaresOpt.next_step = left; break; 173 | case 13: MarchingSquaresOpt.next_step = up; break; 174 | case 14: MarchingSquaresOpt.next_step = left; break; 175 | default: 176 | MarchingSquaresOpt.next_step = none; // this should never happen 177 | break; 178 | } 179 | return MarchingSquaresOpt.next_step; 180 | }; 181 | 182 | window.MarchingSquaresOpt = MarchingSquaresOpt; 183 | 184 | }(window)); 185 | -------------------------------------------------------------------------------- /marchingSquaresTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Marching Squares Test 5 | 6 | 7 | 8 | 9 | 10 | 156 | 157 | 181 | 182 | 183 | 184 |
185 |
186 | 187 | 188 | 189 | 190 | 191 |

192 | 193 |

194 |

195 |
196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /MarchingSquaresOld.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by @sakri on 25-3-14. 3 | * 4 | * Javascript port of : 5 | * http://devblog.phillipspiess.com/2010/02/23/better-know-an-algorithm-1-marching-squares/ 6 | * returns an Array of x and y positions defining the perimeter of a blob of non-transparent pixels on a canvas 7 | * 8 | */ 9 | (function (window){ 10 | 11 | var MarchingSquaresOld = {}; 12 | 13 | MarchingSquaresOld.NONE = 0; 14 | MarchingSquaresOld.UP = 1; 15 | MarchingSquaresOld.LEFT = 2; 16 | MarchingSquaresOld.DOWN = 3; 17 | MarchingSquaresOld.RIGHT = 4; 18 | 19 | // Takes a canvas and returns a list of pixels that 20 | // define the perimeter of the upper-left most 21 | // object in that texture, using pixel alpha>0 to define 22 | // the boundary. 23 | MarchingSquaresOld.getBlobOutlinePoints = function(sourceCanvas){ 24 | 25 | //Add a padding of 1 pixel to handle points which touch edges 26 | MarchingSquaresOld.sourceCanvas = document.createElement("canvas"); 27 | MarchingSquaresOld.sourceCanvas.width = sourceCanvas.width + 2; 28 | MarchingSquaresOld.sourceCanvas.height = sourceCanvas.height + 2; 29 | MarchingSquaresOld.sourceContext = MarchingSquaresOld.sourceCanvas.getContext("2d"); 30 | MarchingSquaresOld.sourceContext.drawImage(sourceCanvas,1,1); 31 | 32 | // Find the starting point 33 | var startingPoint = MarchingSquaresOld.getFirstNonTransparentPixelTopDown(MarchingSquaresOld.sourceCanvas); 34 | 35 | // Return list of x and y positions 36 | return MarchingSquaresOld.walkPerimeter(startingPoint.x, startingPoint.y); 37 | }; 38 | 39 | MarchingSquaresOld.getFirstNonTransparentPixelTopDown = function(canvas){ 40 | var context = canvas.getContext("2d"); 41 | var y, i, rowData; 42 | for(y = 0; y < canvas.height; y++){ 43 | rowData = context.getImageData(0, y, canvas.width, 1).data; 44 | for(i=0; i 0){ 46 | return {x : i/4, y : y}; 47 | } 48 | } 49 | } 50 | return null; 51 | }; 52 | 53 | MarchingSquaresOld.walkPerimeter = function(startX, startY){ 54 | // Do some sanity checking, so we aren't 55 | // walking outside the image 56 | // technically this should never happen 57 | if (startX< 0){ 58 | startX = 0; 59 | } 60 | if (startX> MarchingSquaresOld.sourceCanvas.width){ 61 | startX= MarchingSquaresOld.sourceCanvas.width; 62 | } 63 | if (startY< 0){ 64 | startY= 0; 65 | } 66 | if (startY> MarchingSquaresOld.sourceCanvas.height){ 67 | startY= MarchingSquaresOld.sourceCanvas.height; 68 | } 69 | 70 | // Set up our return list 71 | var pointList =[]; 72 | 73 | // Our current x and y positions, initialized 74 | // to the init values passed in 75 | var x = startX; 76 | var y = startY; 77 | 78 | // The main while loop, continues stepping until 79 | // we return to our initial points 80 | do{ 81 | // Evaluate our state, and set up our next direction 82 | MarchingSquaresOld.step(x, y); 83 | 84 | // If our current point is within our image 85 | // add it to the list of points 86 | if (x >= 0 && 87 | x < MarchingSquaresOld.sourceCanvas.width && 88 | y >= 0 && 89 | y < MarchingSquaresOld.sourceCanvas.height){ 90 | pointList.push(x - 1, y - 1);//offset of 1 due to the 1 pixel padding added to sourceCanvas 91 | } 92 | 93 | switch (MarchingSquaresOld.nextStep){ 94 | case MarchingSquaresOld.UP: y--; break; 95 | case MarchingSquaresOld.LEFT: x--; break; 96 | case MarchingSquaresOld.DOWN: y++; break; 97 | case MarchingSquaresOld.RIGHT: x++; break; 98 | default: 99 | break; 100 | } 101 | 102 | } while (x != startX || y != startY); 103 | 104 | return pointList; 105 | }; 106 | 107 | // Determines and sets the state of the 4 pixels that 108 | // represent our current state, and sets our current and 109 | // previous directions 110 | 111 | MarchingSquaresOld.step = function(x, y){ 112 | //console.log("MarchingSquaresOld.step()"); 113 | // Scan our 4 pixel area 114 | MarchingSquaresOld.sampleData = MarchingSquaresOld.sourceContext.getImageData(x-1, y-1, 2, 2).data; 115 | 116 | MarchingSquaresOld.upLeft = MarchingSquaresOld.sampleData[3] > 0; 117 | MarchingSquaresOld.upRight = MarchingSquaresOld.sampleData[7] > 0; 118 | MarchingSquaresOld.downLeft = MarchingSquaresOld.sampleData[11] > 0; 119 | MarchingSquaresOld.downRight = MarchingSquaresOld.sampleData[15] > 0; 120 | 121 | // Store our previous step 122 | MarchingSquaresOld.previousStep = MarchingSquaresOld.nextStep; 123 | 124 | // Determine which state we are in 125 | MarchingSquaresOld.state = 0; 126 | 127 | if (MarchingSquaresOld.upLeft){ 128 | MarchingSquaresOld.state |= 1; 129 | } 130 | if (MarchingSquaresOld.upRight){ 131 | MarchingSquaresOld.state |= 2; 132 | } 133 | if (MarchingSquaresOld.downLeft){ 134 | MarchingSquaresOld.state |= 4; 135 | } 136 | if (MarchingSquaresOld.downRight){ 137 | MarchingSquaresOld.state |= 8; 138 | } 139 | 140 | // State now contains a number between 0 and 15 141 | // representing our state. 142 | // In binary, it looks like 0000-1111 (in binary) 143 | 144 | // An example. Let's say the top two pixels are filled, 145 | // and the bottom two are empty. 146 | // Stepping through the if statements above with a state 147 | // of 0b0000 initially produces: 148 | // Upper Left == true ==> 0b0001 149 | // Upper Right == true ==> 0b0011 150 | // The others are false, so 0b0011 is our state 151 | // (That's 3 in decimal.) 152 | 153 | // Looking at the chart above, we see that state 154 | // corresponds to a move right, so in our switch statement 155 | // below, we add a case for 3, and assign Right as the 156 | // direction of the next step. We repeat this process 157 | // for all 16 states. 158 | 159 | // So we can use a switch statement to determine our 160 | // next direction based on 161 | switch (MarchingSquaresOld.state ){ 162 | case 1: MarchingSquaresOld.nextStep = MarchingSquaresOld.UP; break; 163 | case 2: MarchingSquaresOld.nextStep = MarchingSquaresOld.RIGHT; break; 164 | case 3: MarchingSquaresOld.nextStep = MarchingSquaresOld.RIGHT; break; 165 | case 4: MarchingSquaresOld.nextStep = MarchingSquaresOld.LEFT; break; 166 | case 5: MarchingSquaresOld.nextStep = MarchingSquaresOld.UP; break; 167 | case 6: 168 | if (MarchingSquaresOld.previousStep == MarchingSquaresOld.UP){ 169 | MarchingSquaresOld.nextStep = MarchingSquaresOld.LEFT; 170 | }else{ 171 | MarchingSquaresOld.nextStep = MarchingSquaresOld.RIGHT; 172 | } 173 | break; 174 | case 7: MarchingSquaresOld.nextStep = MarchingSquaresOld.RIGHT; break; 175 | case 8: MarchingSquaresOld.nextStep = MarchingSquaresOld.DOWN; break; 176 | case 9: 177 | if (MarchingSquaresOld.previousStep == MarchingSquaresOld.RIGHT){ 178 | MarchingSquaresOld.nextStep = MarchingSquaresOld.UP; 179 | }else{ 180 | MarchingSquaresOld.nextStep = MarchingSquaresOld.DOWN; 181 | } 182 | break; 183 | case 10: MarchingSquaresOld.nextStep = MarchingSquaresOld.DOWN; break; 184 | case 11: MarchingSquaresOld.nextStep = MarchingSquaresOld.DOWN; break; 185 | case 12: MarchingSquaresOld.nextStep = MarchingSquaresOld.LEFT; break; 186 | case 13: MarchingSquaresOld.nextStep = MarchingSquaresOld.UP; break; 187 | case 14: MarchingSquaresOld.nextStep = MarchingSquaresOld.LEFT; break; 188 | default: 189 | MarchingSquaresOld.nextStep = MarchingSquaresOld.NONE;//this should never happen 190 | break; 191 | } 192 | }; 193 | 194 | window.MarchingSquaresOld = MarchingSquaresOld; 195 | 196 | }(window)); -------------------------------------------------------------------------------- /MarchingSquares.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by @sakri on 25-3-14. 3 | * 4 | * Javascript port of : 5 | * http://devblog.phillipspiess.com/2010/02/23/better-know-an-algorithm-1-marching-squares/ 6 | * returns an Array of x and y positions defining the perimeter of a blob of non-transparent pixels on a canvas 7 | * 8 | */ 9 | (function (window){ 10 | 11 | var MarchingSquares = {}; 12 | 13 | MarchingSquares.NONE = 0; 14 | MarchingSquares.UP = 1; 15 | MarchingSquares.LEFT = 2; 16 | MarchingSquares.DOWN = 3; 17 | MarchingSquares.RIGHT = 4; 18 | 19 | // Takes a canvas and returns a list of pixels that 20 | // define the perimeter of the upper-left most 21 | // object in that texture, using pixel alpha>0 to define 22 | // the boundary. 23 | MarchingSquares.getBlobOutlinePoints = function(sourceCanvas){ 24 | 25 | //Add a padding of 1 pixel to handle points which touch edges 26 | MarchingSquares.sourceCanvas = document.createElement("canvas"); 27 | MarchingSquares.sourceCanvas.width = sourceCanvas.width + 2; 28 | MarchingSquares.sourceCanvas.height = sourceCanvas.height + 2; 29 | MarchingSquares.sourceContext = MarchingSquares.sourceCanvas.getContext("2d"); 30 | MarchingSquares.sourceContext.drawImage(sourceCanvas,1,1); 31 | 32 | // Find the starting point 33 | var startingPoint = MarchingSquares.getFirstNonTransparentPixelTopDown(MarchingSquares.sourceCanvas); 34 | 35 | // Return list of x and y positions 36 | return MarchingSquares.walkPerimeter(startingPoint.x, startingPoint.y); 37 | }; 38 | 39 | MarchingSquares.getFirstNonTransparentPixelTopDown = function(canvas){ 40 | var context = canvas.getContext("2d"); 41 | var y, i, rowData; 42 | for(y = 0; y < canvas.height; y++){ 43 | rowData = context.getImageData(0, y, canvas.width, 1).data; 44 | for(i=0; i 0){ 46 | return {x : i/4, y : y}; 47 | } 48 | } 49 | } 50 | return null; 51 | }; 52 | 53 | MarchingSquares.walkPerimeter = function(startX, startY){ 54 | // Do some sanity checking, so we aren't 55 | // walking outside the image 56 | // technically this should never happen 57 | if (startX < 0){ 58 | startX = 0; 59 | } 60 | if (startX > MarchingSquares.sourceCanvas.width){ 61 | startX = MarchingSquares.sourceCanvas.width; 62 | } 63 | if (startY < 0){ 64 | startY = 0; 65 | } 66 | if (startY > MarchingSquares.sourceCanvas.height){ 67 | startY = MarchingSquares.sourceCanvas.height; 68 | } 69 | 70 | // Set up our return list 71 | var pointList =[]; 72 | 73 | // Our current x and y positions, initialized 74 | // to the init values passed in 75 | var x = startX; 76 | var y = startY; 77 | 78 | var imageData = MarchingSquares.sourceContext.getImageData(0,0, MarchingSquares.sourceCanvas.width, MarchingSquares.sourceCanvas.height); 79 | var index, width4 = imageData.width * 4; 80 | 81 | // The main while loop, continues stepping until 82 | // we return to our initial points 83 | do{ 84 | // Evaluate our state, and set up our next direction 85 | //index = (y-1) * width4 + (x-1) * 4; 86 | index = (y-1) * width4 + (x-1) * 4; 87 | MarchingSquares.step(index, imageData.data, width4); 88 | 89 | // If our current point is within our image 90 | // add it to the list of points 91 | if (x >= 0 && 92 | x < MarchingSquares.sourceCanvas.width && 93 | y >= 0 && 94 | y < MarchingSquares.sourceCanvas.height){ 95 | pointList.push(x - 2, y - 1);//offset of 1 due to the 1 pixel padding added to sourceCanvas 96 | } 97 | 98 | switch (MarchingSquares.nextStep){ 99 | case MarchingSquares.UP: y--; break; 100 | case MarchingSquares.LEFT: x--; break; 101 | case MarchingSquares.DOWN: y++; break; 102 | case MarchingSquares.RIGHT: x++; break; 103 | default: 104 | break; 105 | } 106 | 107 | } while (x != startX || y != startY); 108 | 109 | pointList.push(x - 1, y - 1); 110 | 111 | return pointList; 112 | }; 113 | 114 | // Determines and sets the state of the 4 pixels that 115 | // represent our current state, and sets our current and 116 | // previous directions 117 | 118 | MarchingSquares.step = function(index, data, width4){ 119 | //console.log("Sakri.MarchingSquares.step()"); 120 | // Scan our 4 pixel area 121 | //Sakri.imageData = Sakri.MarchingSquares.sourceContext.getImageData(x-1, y-1, 2, 2).data; 122 | 123 | MarchingSquares.upLeft = data[index + 3] > 0; 124 | MarchingSquares.upRight = data[index + 7] > 0; 125 | MarchingSquares.downLeft = data[index + width4 + 3] > 0; 126 | MarchingSquares.downRight = data[index + width4 + 7] > 0; 127 | 128 | // Store our previous step 129 | MarchingSquares.previousStep = MarchingSquares.nextStep; 130 | 131 | // Determine which state we are in 132 | MarchingSquares.state = 0; 133 | 134 | if (MarchingSquares.upLeft){ 135 | MarchingSquares.state |= 1; 136 | } 137 | if (MarchingSquares.upRight){ 138 | MarchingSquares.state |= 2; 139 | } 140 | if (MarchingSquares.downLeft){ 141 | MarchingSquares.state |= 4; 142 | } 143 | if (MarchingSquares.downRight){ 144 | MarchingSquares.state |= 8; 145 | } 146 | 147 | // State now contains a number between 0 and 15 148 | // representing our state. 149 | // In binary, it looks like 0000-1111 (in binary) 150 | 151 | // An example. Let's say the top two pixels are filled, 152 | // and the bottom two are empty. 153 | // Stepping through the if statements above with a state 154 | // of 0b0000 initially produces: 155 | // Upper Left == true ==> 0b0001 156 | // Upper Right == true ==> 0b0011 157 | // The others are false, so 0b0011 is our state 158 | // (That's 3 in decimal.) 159 | 160 | // Looking at the chart above, we see that state 161 | // corresponds to a move right, so in our switch statement 162 | // below, we add a case for 3, and assign Right as the 163 | // direction of the next step. We repeat this process 164 | // for all 16 states. 165 | 166 | // So we can use a switch statement to determine our 167 | // next direction based on 168 | switch (MarchingSquares.state ){ 169 | case 1: MarchingSquares.nextStep = MarchingSquares.UP; break; 170 | case 2: MarchingSquares.nextStep = MarchingSquares.RIGHT; break; 171 | case 3: MarchingSquares.nextStep = MarchingSquares.RIGHT; break; 172 | case 4: MarchingSquares.nextStep = MarchingSquares.LEFT; break; 173 | case 5: MarchingSquares.nextStep = MarchingSquares.UP; break; 174 | case 6: 175 | if (MarchingSquares.previousStep == MarchingSquares.UP){ 176 | MarchingSquares.nextStep = MarchingSquares.LEFT; 177 | }else{ 178 | MarchingSquares.nextStep = MarchingSquares.RIGHT; 179 | } 180 | break; 181 | case 7: MarchingSquares.nextStep = MarchingSquares.RIGHT; break; 182 | case 8: MarchingSquares.nextStep = MarchingSquares.DOWN; break; 183 | case 9: 184 | if (MarchingSquares.previousStep == MarchingSquares.RIGHT){ 185 | MarchingSquares.nextStep = MarchingSquares.UP; 186 | }else{ 187 | MarchingSquares.nextStep = MarchingSquares.DOWN; 188 | } 189 | break; 190 | case 10: MarchingSquares.nextStep = MarchingSquares.DOWN; break; 191 | case 11: MarchingSquares.nextStep = MarchingSquares.DOWN; break; 192 | case 12: MarchingSquares.nextStep = MarchingSquares.LEFT; break; 193 | case 13: MarchingSquares.nextStep = MarchingSquares.UP; break; 194 | case 14: MarchingSquares.nextStep = MarchingSquares.LEFT; break; 195 | default: 196 | MarchingSquares.nextStep = MarchingSquares.NONE;//this should never happen 197 | break; 198 | } 199 | }; 200 | 201 | window.MarchingSquares = MarchingSquares; 202 | 203 | }(window)); --------------------------------------------------------------------------------