var film2d = use('film2d') var math = use('math/radians') var line_proto = { type: 'mesh2d', set_pos: function(x, y) { this.pos.x = x this.pos.y = y this._dirty = true return this }, set_points: function(points) { this.points = points this._dirty = true this._rebuild() return this }, set_point: function(i, x, y) { if (i >= 0 && i < length(this.points)) { this.points[i].x = x this.points[i].y = y this._dirty = true this._rebuild() } return this }, set_width: function(w) { this.width = w this._dirty = true this._rebuild() return this }, set_widths: function(widths) { this.widths = widths this._dirty = true this._rebuild() return this }, _rebuild: function() { var result = build_polyline_mesh(this) this.verts = result.verts this.indices = result.indices this._cumulative_lengths = result.cumulative_lengths this._total_length = result.total_length }, destroy: function() { film2d.unregister(this._id) } } function build_polyline_mesh(line) { var points = line.points || [] if (length(points) < 2) return {verts: [], indices: [], cumulative_lengths: [], total_length: 0} var width = line.width || 10 var widths = line.widths var closed = line.closed || false var join = line.join || 'miter' var cap = line.cap || 'butt' var miter_limit = line.miter_limit || 4 var uv_mode = line.uv ? (line.uv.mode || 'repeat') : 'repeat' var u_per_unit = line.uv ? (line.uv.u_per_unit || (1 / 16)) : (1 / 16) var u_offset = line.uv ? (line.uv.u_offset || 0) : 0 var v_scale = line.uv ? (line.uv.v_scale || 1) : 1 var v_offset = line.uv ? (line.uv.v_offset || 0) : 0 var pos = line.pos || {x: 0, y: 0} var points_space = line.points_space || 'world' // Transform points if in local space var pts = [] var i = 0 var p = null var dx = 0 var dy = 0 for (i = 0; i < length(points); i++) { p = points[i] if (points_space == 'local') { push(pts, {x: p.x + pos.x, y: p.y + pos.y}) } else { push(pts, {x: p.x, y: p.y}) } } // Calculate cumulative distances var cumulative = [0] for (i = 1; i < length(pts); i++) { dx = pts[i].x - pts[i-1].x dy = pts[i].y - pts[i-1].y push(cumulative, cumulative[i-1] + math.sqrt(dx*dx + dy*dy)) } var total_length = cumulative[length(cumulative) - 1] // Build triangle strip mesh var verts = [] var indices = [] // Get width at point i function get_width(idx) { if (widths && length(widths) > idx) return widths[idx] return width } // Get U coordinate at point i var seg_idx = 0 var seg_len = 0 function get_u(idx) { if (uv_mode == 'stretch') { return total_length > 0 ? cumulative[idx] / total_length : 0 } else if (uv_mode == 'per_segment') { if (idx == 0) return 0 seg_idx = idx - 1 seg_len = cumulative[idx] - cumulative[idx-1] return 1 // Each segment ends at u=1 } else { // repeat (default) return cumulative[idx] * u_per_unit + u_offset } } // Calculate normals at each point var normals = [] var prev = null var curr = null var next = null var n = null var d1x = 0 var d1y = 0 var d2x = 0 var d2y = 0 var len1 = 0 var len2 = 0 var n1x = 0 var n1y = 0 var n2x = 0 var n2y = 0 var nlen = 0 var dot = 0 var miter_scale = 0 var len = 0 for (i = 0; i < length(pts); i++) { prev = i > 0 ? pts[i-1] : (closed ? pts[length(pts)-1] : null) curr = pts[i] next = i < length(pts)-1 ? pts[i+1] : (closed ? pts[0] : null) n = {x: 0, y: 0} if (prev && next) { // Middle point - average normals d1x = curr.x - prev.x d1y = curr.y - prev.y d2x = next.x - curr.x d2y = next.y - curr.y len1 = math.sqrt(d1x*d1x + d1y*d1y) len2 = math.sqrt(d2x*d2x + d2y*d2y) if (len1 > 0.0001) { d1x /= len1; d1y /= len1 } if (len2 > 0.0001) { d2x /= len2; d2y /= len2 } // Normals (perpendicular) n1x = -d1y n1y = d1x n2x = -d2y n2y = d2x // Average n.x = n1x + n2x n.y = n1y + n2y nlen = math.sqrt(n.x*n.x + n.y*n.y) if (nlen > 0.0001) { n.x /= nlen; n.y /= nlen } // Miter correction dot = n1x * n.x + n1y * n.y if (dot > 0.0001) { miter_scale = 1 / dot if (miter_scale > miter_limit) miter_scale = miter_limit n.x *= miter_scale n.y *= miter_scale } } else if (next) { // Start point dx = next.x - curr.x dy = next.y - curr.y len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } n.x = -dy n.y = dx } else if (prev) { // End point dx = curr.x - prev.x dy = curr.y - prev.y len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } n.x = -dy n.y = dx } push(normals, n) } // Generate vertices (2 per point - left and right of line) var w = 0 var u = 0 for (i = 0; i < length(pts); i++) { p = pts[i] n = normals[i] w = get_width(i) * 0.5 u = get_u(i) // Left vertex (v=0) push(verts, { x: p.x + n.x * w, y: p.y + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1 }) // Right vertex (v=1) push(verts, { x: p.x - n.x * w, y: p.y - n.y * w, u: u, v: v_scale + v_offset, r: 1, g: 1, b: 1, a: 1 }) } // Generate indices (triangle strip as triangles) var base = 0 for (i = 0; i < length(pts) - 1; i++) { base = i * 2 // First triangle push(indices, base + 0) push(indices, base + 1) push(indices, base + 2) // Second triangle push(indices, base + 1) push(indices, base + 3) push(indices, base + 2) } // Handle closed path var last = 0 if (closed && length(pts) > 2) { last = (length(pts) - 1) * 2 push(indices, last + 0) push(indices, last + 1) push(indices, 0) push(indices, last + 1) push(indices, 1) push(indices, 0) } // Add round caps if requested if (!closed && cap == 'round') { add_round_cap({verts: verts, indices: indices, p: pts[0], n: normals[0], width: get_width(0), u: get_u(0), v_offset: v_offset, v_scale: v_scale, is_start: true}) add_round_cap({verts: verts, indices: indices, p: pts[length(pts)-1], n: normals[length(pts)-1], width: get_width(length(pts)-1), u: get_u(length(pts)-1), v_offset: v_offset, v_scale: v_scale, is_start: false}) } else if (!closed && cap == 'square') { add_square_cap({verts: verts, indices: indices, p: pts[0], n: normals[0], width: get_width(0), u: get_u(0), v_offset: v_offset, v_scale: v_scale, is_start: true, adjacent: pts[1]}) add_square_cap({verts: verts, indices: indices, p: pts[length(pts)-1], n: normals[length(pts)-1], width: get_width(length(pts)-1), u: get_u(length(pts)-1), v_offset: v_offset, v_scale: v_scale, is_start: false, adjacent: pts[length(pts)-2]}) } return { verts: verts, indices: indices, cumulative_lengths: cumulative, total_length: total_length } } function add_round_cap(opts) { var verts = opts.verts var indices = opts.indices var p = opts.p var n = opts.n var rc_width = opts.width var u = opts.u var v_offset = opts.v_offset var v_scale = opts.v_scale var is_start = opts.is_start var w = rc_width * 0.5 var segments = 8 var base_idx = length(verts) // Direction along the line var dx = is_start ? -n.y : n.y var dy = is_start ? n.x : -n.x // Center vertex push(verts, { x: p.x, y: p.y, u: u, v: 0.5 * v_scale + v_offset, r: 1, g: 1, b: 1, a: 1 }) // Arc vertices var start_angle = is_start ? math.arc_tangent(n.y, n.x) : math.arc_tangent(-n.y, -n.x) var i = 0 var angle = 0 var cx = 0 var cy = 0 for (i = 0; i <= segments; i++) { angle = start_angle + (i / segments) * 3.14159 cx = math.cosine(angle) cy = math.sine(angle) push(verts, { x: p.x + cx * w, y: p.y + cy * w, u: u, v: (0.5 + cy * 0.5) * v_scale + v_offset, r: 1, g: 1, b: 1, a: 1 }) } // Fan triangles for (i = 0; i < segments; i++) { push(indices, base_idx) push(indices, base_idx + 1 + i) push(indices, base_idx + 2 + i) } } function add_square_cap(opts) { var verts = opts.verts var indices = opts.indices var p = opts.p var n = opts.n var sc_width = opts.width var u = opts.u var v_offset = opts.v_offset var v_scale = opts.v_scale var is_start = opts.is_start var adjacent = opts.adjacent var w = sc_width * 0.5 var base_idx = length(verts) // Direction along the line (away from adjacent point) var dx = p.x - adjacent.x var dy = p.y - adjacent.y var len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } // Extend by half width var ext = w var ex = p.x + dx * ext var ey = p.y + dy * ext // Four corners of the cap push(verts, {x: p.x + n.x * w, y: p.y + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1}) push(verts, {x: p.x - n.x * w, y: p.y - n.y * w, u: u, v: v_scale + v_offset, r: 1, g: 1, b: 1, a: 1}) push(verts, {x: ex + n.x * w, y: ey + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1}) push(verts, {x: ex - n.x * w, y: ey - n.y * w, u: u, v: v_scale + v_offset, r: 1, g: 1, b: 1, a: 1}) push(indices, base_idx + 0) push(indices, base_idx + 1) push(indices, base_idx + 2) push(indices, base_idx + 1) push(indices, base_idx + 3) push(indices, base_idx + 2) } var defaults = { type: 'mesh2d', // routing plane: 'default', layer: 0, groups: [], visible: true, // transform pos: {x: 0, y: 0}, points_space: 'world', // geometry points: [], closed: false, // thickness width: 10, widths: null, // join/cap join: 'miter', cap: 'butt', miter_limit: 4, // material image: null, tint: {r: 1, g: 1, b: 1, a: 1}, opacity: 1, blend: 'alpha', filter: 'linear', // UV behavior uv: { space: 'world', mode: 'repeat', u_per_unit: 1 / 16, u_offset: 0, v_scale: 1, v_offset: 0, rotate: 0 }, // dashes dash_len: 0, gap_len: 0, dash_offset: 0 } function make_line(props) { var data = object(defaults, props) // Ensure groups is array if (!data.groups) data.groups = [] if (is_text(data.groups)) data.groups = [data.groups] var line = meme(line_proto, data) line._rebuild() film2d.register(line) return line } var line2d = { polyline: function(props) { return make_line(props) }, line: function(from, to, props) { var p = props || {} p.points = [{x: from.x, y: from.y}, {x: to.x, y: to.y}] return make_line(p) } } return line2d