mirror of
				https://github.com/skishore/makemeahanzi.git
				synced 2025-10-31 10:56:39 +08:00 
			
		
		
		
	Port corner detection to Javascript
This commit is contained in:
		| @ -8,7 +8,7 @@ var COLORS = ['#0074D9', '#2ECC40', '#FFDC00', '#FF4136', '#7FDBFF', | |||||||
| function change_glyph(method, glyph) { | function change_glyph(method, glyph) { | ||||||
|   glyph = glyph || Session.get('glyph.data'); |   glyph = glyph || Session.get('glyph.data'); | ||||||
|   Meteor.call(method, glyph, function(error, data) { |   Meteor.call(method, glyph, function(error, data) { | ||||||
|     data.d = Glyphs.get_svg_path(data); |     data.render = get_glyph_render_data(data); | ||||||
|  |  | ||||||
|     data.manual = data.manual || {}; |     data.manual = data.manual || {}; | ||||||
|     data.manual.bridges_added = data.manual.bridges_added || []; |     data.manual.bridges_added = data.manual.bridges_added || []; | ||||||
| @ -178,7 +178,7 @@ Template.glyph.helpers({ | |||||||
|     return Session.get('glyph.show_strokes') ? 'black' : 'gray'; |     return Session.get('glyph.show_strokes') ? 'black' : 'gray'; | ||||||
|   }, |   }, | ||||||
|   d: function() { |   d: function() { | ||||||
|     return Session.get('glyph.data').d; |     return Session.get('glyph.data').render.d; | ||||||
|   }, |   }, | ||||||
|   show_strokes: function() { |   show_strokes: function() { | ||||||
|     return !!Session.get('glyph.show_strokes'); |     return !!Session.get('glyph.show_strokes'); | ||||||
| @ -194,6 +194,7 @@ Template.glyph.helpers({ | |||||||
|     return result; |     return result; | ||||||
|   }, |   }, | ||||||
|   bridges: function() { |   bridges: function() { | ||||||
|  |     return []; | ||||||
|     var glyph = Session.get('glyph.data'); |     var glyph = Session.get('glyph.data'); | ||||||
|     var removed = {}; |     var removed = {}; | ||||||
|     for (var i = 0; i < glyph.manual.bridges_removed.length; i++) { |     for (var i = 0; i < glyph.manual.bridges_removed.length; i++) { | ||||||
| @ -216,15 +217,12 @@ Template.glyph.helpers({ | |||||||
|   }, |   }, | ||||||
|   points: function() { |   points: function() { | ||||||
|     var glyph = Session.get('glyph.data'); |     var glyph = Session.get('glyph.data'); | ||||||
|     var corners = {}; |  | ||||||
|     for (var i = 0; i < glyph.extractor.corners.length; i++) { |  | ||||||
|       corners[to_point(glyph.extractor.corners[i]).coordinates] = true; |  | ||||||
|     } |  | ||||||
|     var result = []; |     var result = []; | ||||||
|     for (var i = 0; i < glyph.extractor.points.length; i++) { |     for (var i = 0; i < glyph.render.endpoints.length; i++) { | ||||||
|       var point = to_point(glyph.extractor.points[i]); |       var endpoint = glyph.render.endpoints[i]; | ||||||
|       point.color = corners[point.coordinates] ? 'red' : 'black'; |       var point = to_point(endpoint.point); | ||||||
|       point.z_index = corners[point.coordinates] ? 1 : 0; |       point.color = endpoint.corner ? 'red' : 'black'; | ||||||
|  |       point.z_index = endpoint.corner ? 1 : 0; | ||||||
|       if (point.coordinates === Session.get('glyph.selected_point')) { |       if (point.coordinates === Session.get('glyph.selected_point')) { | ||||||
|         point.color = 'purple'; |         point.color = 'purple'; | ||||||
|       } |       } | ||||||
|  | |||||||
							
								
								
									
										109
									
								
								lib/glyphs.js
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								lib/glyphs.js
									
									
									
									
									
								
							| @ -14,112 +14,3 @@ Glyphs.get_svg_path = function(glyph) { | |||||||
|   } |   } | ||||||
|   return terms.join(' '); |   return terms.join(' '); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Error out if the condition does not hold. |  | ||||||
| function assert(condition, message) { |  | ||||||
|   if (!condition) { |  | ||||||
|     console.error(message); |  | ||||||
|     throw new Error; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function clone(point) { |  | ||||||
|   return [point[0], point[1]]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function equal(point1, point2) { |  | ||||||
|   return point1[0] === point2[0] && point1[1] === point2[1]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function valid(point) { |  | ||||||
|   return point[0] !== undefined && point[1] !== undefined; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Takes a non-empty list of SVG commands that may contain multiple contours. |  | ||||||
| // Returns a list of lists of path segment objects that each form one contour. |  | ||||||
| // Each path segment has three keys: start, end, and control. |  | ||||||
| function split_path(path) { |  | ||||||
|   assert(path.length >= 2); |  | ||||||
|   assert(path[0].type === 'M', 'Path did not start with M!'); |  | ||||||
|   assert(path[path.length - 1].type === 'Z', 'Path did not end with Z!'); |  | ||||||
|  |  | ||||||
|   var result = [[]]; |  | ||||||
|   var start = [path[0].x, path[0].y]; |  | ||||||
|   var current = clone(start); |  | ||||||
|   assert(valid(current)); |  | ||||||
|  |  | ||||||
|   for (var i = 1; i < path.length; i++) { |  | ||||||
|     var command = path[i]; |  | ||||||
|     if (command.type === 'M' || command.type === 'Z') { |  | ||||||
|       assert(start.x === current.x && start.y === current.y, 'Open contour!'); |  | ||||||
|       assert(result[result.length -1].length > 0, 'Empty contour!'); |  | ||||||
|       if (command.type === 'Z') { |  | ||||||
|         assert(i === path.length - 1, 'Path ended early!'); |  | ||||||
|         return result; |  | ||||||
|       } |  | ||||||
|       var start = [command.x, command.y]; |  | ||||||
|       var current = clone(start); |  | ||||||
|       assert(valid(current)); |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|     assert(command.type === 'Q' || command.type === 'L', |  | ||||||
|            'Got unexpected TTF command: ' + command.type); |  | ||||||
|     var segment = { |  | ||||||
|       'start': clone(current), |  | ||||||
|       'end': [command.x, command.y], |  | ||||||
|       'control': [command.x1, command.y1], |  | ||||||
|     }; |  | ||||||
|     assert(valid(segment.end)); |  | ||||||
|     if (equal(segment.start, segment.end)) { |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|     if (!valid(segment.control) || |  | ||||||
|         equal(segment.start, segment.control) || |  | ||||||
|         equal(segment.end, segment.control)) { |  | ||||||
|       delete segment.control; |  | ||||||
|     } |  | ||||||
|     result[result.length - 1].push(segment); |  | ||||||
|     current = clone(segment.end); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Takes a list of paths. Returns them oriented the way a TTF glyph should be: |  | ||||||
| // exterior contours counter-clockwise and interior contours clockwise. |  | ||||||
| function orient_paths(paths) { |  | ||||||
|   var max_area = 0; |  | ||||||
|   for (var i = 0; i < paths.length; i++) { |  | ||||||
|     var area = get_2x_area(paths[i]); |  | ||||||
|     if (Math.abs(area) > max_area) { |  | ||||||
|       max_area = area; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   if (max_area < 0) { |  | ||||||
|     // The paths are reversed. Flip each one. |  | ||||||
|     var result = []; |  | ||||||
|     for (var i = 0; i < paths.length; i++) { |  | ||||||
|       var path = paths[i]; |  | ||||||
|       for (var j = 0; j < paths.length; j++) { |  | ||||||
|         var ref = [path[j].start, path[j].end]; |  | ||||||
|         path[j].start = ref[1]; |  | ||||||
|         path[j].end = ref[0]; |  | ||||||
|       } |  | ||||||
|       path[j].reverse(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return paths; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Returns twice the area contained in the path. The result is positive iff the |  | ||||||
| // path winds in the counter-clockwise direction. |  | ||||||
| function get_2x_area(path) { |  | ||||||
|   var area = 0; |  | ||||||
|   for (var i = 0; i < path.length; i++) { |  | ||||||
|     var segment = path[i]; |  | ||||||
|     area += (segment.end.x - segment.start.x)*(segment.end.y + segment.start.y); |  | ||||||
|   } |  | ||||||
|   return area; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| split_and_orient_path = function(path) { |  | ||||||
|   return orient_paths(split_path(path)); |  | ||||||
| } |  | ||||||
|  | |||||||
							
								
								
									
										173
									
								
								lib/stroke_extractor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								lib/stroke_extractor.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | |||||||
|  | var MIN_CORNER_ANGLE = 0.1*Math.PI; | ||||||
|  | var MIN_CORNER_TANGENT_DISTANCE = 4; | ||||||
|  |  | ||||||
|  | // Error out if the condition does not hold. | ||||||
|  | function assert(condition, message) { | ||||||
|  |   if (!condition) { | ||||||
|  |     console.error(message); | ||||||
|  |     throw new Error; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Helper methods for use with "points", which are just pairs of integers. | ||||||
|  | var Point = { | ||||||
|  |   angle: function(point) { | ||||||
|  |     return Math.atan2(point[1], point[0]); | ||||||
|  |   }, | ||||||
|  |   clone: function(point) { | ||||||
|  |     return [point[0], point[1]]; | ||||||
|  |   }, | ||||||
|  |   distance2: function(point1, point2) { | ||||||
|  |     var diff = Point.subtract(point1, point2); | ||||||
|  |     return Math.pow(diff[0], 2) + Math.pow(diff[1], 2); | ||||||
|  |   }, | ||||||
|  |   equal: function(point1, point2) { | ||||||
|  |     return point1[0] === point2[0] && point1[1] === point2[1]; | ||||||
|  |   }, | ||||||
|  |   subtract: function(point1, point2) { | ||||||
|  |     return [point1[0] - point2[0], point1[1] - point2[1]]; | ||||||
|  |   }, | ||||||
|  |   valid: function(point) { | ||||||
|  |     return point[0] !== undefined && point[1] !== undefined; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Helper methods for use with angles, which are floats in [-pi, pi). | ||||||
|  | var Angle = { | ||||||
|  |   subtract: function(angle1, angle2) { | ||||||
|  |     var result = angle1 - angle2; | ||||||
|  |     if (result < -Math.PI) { | ||||||
|  |       result += 2*Math.PI; | ||||||
|  |     } | ||||||
|  |     if (result >= Math.PI) { | ||||||
|  |       result -= 2*Math.PI; | ||||||
|  |     } | ||||||
|  |     return result; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Takes a non-empty list of SVG commands that may contain multiple contours. | ||||||
|  | // Returns a list of lists of path segment objects that each form one contour. | ||||||
|  | // Each path segment has three keys: start, end, and control. | ||||||
|  | function split_path(path) { | ||||||
|  |   assert(path.length >= 2); | ||||||
|  |   assert(path[0].type === 'M', 'Path did not start with M!'); | ||||||
|  |   assert(path[path.length - 1].type === 'Z', 'Path did not end with Z!'); | ||||||
|  |  | ||||||
|  |   var result = [[]]; | ||||||
|  |   var start = [path[0].x, path[0].y]; | ||||||
|  |   var current = Point.clone(start); | ||||||
|  |   assert(Point.valid(current)); | ||||||
|  |  | ||||||
|  |   for (var i = 1; i < path.length; i++) { | ||||||
|  |     var command = path[i]; | ||||||
|  |     if (command.type === 'M' || command.type === 'Z') { | ||||||
|  |       assert(start.x === current.x && start.y === current.y, 'Open contour!'); | ||||||
|  |       assert(result[result.length -1].length > 0, 'Empty contour!'); | ||||||
|  |       if (command.type === 'Z') { | ||||||
|  |         assert(i === path.length - 1, 'Path ended early!'); | ||||||
|  |         return result; | ||||||
|  |       } | ||||||
|  |       result.push([]); | ||||||
|  |       var start = [command.x, command.y]; | ||||||
|  |       var current = Point.clone(start); | ||||||
|  |       assert(Point.valid(current)); | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     assert(command.type === 'Q' || command.type === 'L', | ||||||
|  |            'Got unexpected TTF command: ' + command.type); | ||||||
|  |     var segment = { | ||||||
|  |       'start': Point.clone(current), | ||||||
|  |       'end': [command.x, command.y], | ||||||
|  |       'control': [command.x1, command.y1], | ||||||
|  |     }; | ||||||
|  |     assert(Point.valid(segment.end)); | ||||||
|  |     if (Point.equal(segment.start, segment.end)) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     if (!Point.valid(segment.control) || | ||||||
|  |         Point.equal(segment.start, segment.control) || | ||||||
|  |         Point.equal(segment.end, segment.control)) { | ||||||
|  |       delete segment.control; | ||||||
|  |     } | ||||||
|  |     result[result.length - 1].push(segment); | ||||||
|  |     current = Point.clone(segment.end); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Takes a list of paths. Returns them oriented the way a TTF glyph should be: | ||||||
|  | // exterior contours counter-clockwise and interior contours clockwise. | ||||||
|  | function orient_paths(paths) { | ||||||
|  |   var max_area = 0; | ||||||
|  |   for (var i = 0; i < paths.length; i++) { | ||||||
|  |     var area = get_2x_area(paths[i]); | ||||||
|  |     if (Math.abs(area) > max_area) { | ||||||
|  |       max_area = area; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (max_area < 0) { | ||||||
|  |     // The paths are reversed. Flip each one. | ||||||
|  |     var result = []; | ||||||
|  |     for (var i = 0; i < paths.length; i++) { | ||||||
|  |       var path = paths[i]; | ||||||
|  |       for (var j = 0; j < paths.length; j++) { | ||||||
|  |         var ref = [path[j].start, path[j].end]; | ||||||
|  |         path[j].start = ref[1]; | ||||||
|  |         path[j].end = ref[0]; | ||||||
|  |       } | ||||||
|  |       path[j].reverse(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return paths; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Returns twice the area contained in the path. The result is positive iff the | ||||||
|  | // path winds in the counter-clockwise direction. | ||||||
|  | function get_2x_area(path) { | ||||||
|  |   var area = 0; | ||||||
|  |   for (var i = 0; i < path.length; i++) { | ||||||
|  |     var segment = path[i]; | ||||||
|  |     area += (segment.end.x - segment.start.x)*(segment.end.y + segment.start.y); | ||||||
|  |   } | ||||||
|  |   return area; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function Endpoint(paths, index) { | ||||||
|  |   this.index = index; | ||||||
|  |   var path = paths[index[0]]; | ||||||
|  |   var n = path.length; | ||||||
|  |   this.segments = [path[(index[1] + n - 1) % n], path[index[1]]]; | ||||||
|  |   this.point = this.segments[0].end; | ||||||
|  |   assert(Point.valid(this.point), this.point); | ||||||
|  |   assert(Point.equal(this.point, this.segments[1].start), path); | ||||||
|  |   this.tangents = [ | ||||||
|  |     Point.subtract(this.segments[0].end, this.segments[0].start), | ||||||
|  |     Point.subtract(this.segments[1].end, this.segments[1].start), | ||||||
|  |   ]; | ||||||
|  |   var threshold = Math.pow(MIN_CORNER_TANGENT_DISTANCE, 2); | ||||||
|  |   if (this.segments[0].control !== undefined && | ||||||
|  |       Point.distance2(this.point, this.segments[0].control) > threshold) { | ||||||
|  |     this.tangents[0] = Point.subtract(this.point, this.segments[0].control); | ||||||
|  |   } | ||||||
|  |   if (this.segments[1].control !== undefined && | ||||||
|  |       Point.distance2(this.point, this.segments[1].control) > threshold) { | ||||||
|  |     this.tangents[1] = Point.subtract(this.segments[1].control, this.point); | ||||||
|  |   } | ||||||
|  |   this.angles = this.tangents.map(Point.angle); | ||||||
|  |   var diff = Angle.subtract(this.angles[1], this.angles[0]); | ||||||
|  |   this.corner = diff < -MIN_CORNER_ANGLE; | ||||||
|  |   return this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Exports go below this fold. | ||||||
|  |  | ||||||
|  | this.get_glyph_render_data = function(glyph) { | ||||||
|  |   var paths = orient_paths(split_path(glyph.path)); | ||||||
|  |   var endpoints = []; | ||||||
|  |   for (var i = 0; i < paths.length; i++) { | ||||||
|  |     for (var j = 0; j < paths[i].length; j++) { | ||||||
|  |       endpoints.push(new Endpoint(paths, [i, j])); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return {d: Glyphs.get_svg_path(glyph), endpoints: endpoints}; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Shaunak Kishore
					Shaunak Kishore