diff --git a/client/glyph.js b/client/glyph.js index 2aec9408..0ce07968 100644 --- a/client/glyph.js +++ b/client/glyph.js @@ -8,7 +8,7 @@ var COLORS = ['#0074D9', '#2ECC40', '#FFDC00', '#FF4136', '#7FDBFF', function change_glyph(method, glyph) { glyph = glyph || Session.get('glyph.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.bridges_added = data.manual.bridges_added || []; @@ -178,7 +178,7 @@ Template.glyph.helpers({ return Session.get('glyph.show_strokes') ? 'black' : 'gray'; }, d: function() { - return Session.get('glyph.data').d; + return Session.get('glyph.data').render.d; }, show_strokes: function() { return !!Session.get('glyph.show_strokes'); @@ -194,6 +194,7 @@ Template.glyph.helpers({ return result; }, bridges: function() { + return []; var glyph = Session.get('glyph.data'); var removed = {}; for (var i = 0; i < glyph.manual.bridges_removed.length; i++) { @@ -216,15 +217,12 @@ Template.glyph.helpers({ }, points: function() { 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 = []; - for (var i = 0; i < glyph.extractor.points.length; i++) { - var point = to_point(glyph.extractor.points[i]); - point.color = corners[point.coordinates] ? 'red' : 'black'; - point.z_index = corners[point.coordinates] ? 1 : 0; + for (var i = 0; i < glyph.render.endpoints.length; i++) { + var endpoint = glyph.render.endpoints[i]; + var point = to_point(endpoint.point); + point.color = endpoint.corner ? 'red' : 'black'; + point.z_index = endpoint.corner ? 1 : 0; if (point.coordinates === Session.get('glyph.selected_point')) { point.color = 'purple'; } diff --git a/lib/glyphs.js b/lib/glyphs.js index 22268ac8..558d12dd 100644 --- a/lib/glyphs.js +++ b/lib/glyphs.js @@ -14,112 +14,3 @@ Glyphs.get_svg_path = function(glyph) { } 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)); -} diff --git a/lib/stroke_extractor.js b/lib/stroke_extractor.js new file mode 100644 index 00000000..2919e9e2 --- /dev/null +++ b/lib/stroke_extractor.js @@ -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}; +}