cvs
Version:
A basic canvas framework for 2d drawing.
198 lines (163 loc) • 4.97 kB
JavaScript
// https://github.com/colinmeinke/svg-arc-to-cubic-bezier
//
// Convert an arc to a sequence of cubic bézier curves
//
/* eslint-disable */
const TAU = Math.PI * 2
/* eslint-disable space-infix-ops */
// Calculate an angle between two unit vectors
//
// Since we measure angle between radii of circular arcs,
// we can use simplified math (without length normalization)
//
function unit_vector_angle (ux, uy, vx, vy) {
const sign = ux * vy - uy * vx < 0 ? -1 : 1
let dot = ux * vx + uy * vy
// Add this to work with arbitrary vectors:
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
// rounding errors, e.g. -1.0000000000000002 can screw up this
if (dot > 1.0) {
dot = 1.0
}
if (dot < -1.0) {
dot = -1.0
}
return sign * Math.acos(dot)
}
// Convert from endpoint to center parameterization,
// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
//
// Return [cx, cy, theta1, delta_theta]
//
function get_arc_center (x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi) {
// Step 1.
//
// Moving an ellipse so origin will be the middlepoint between our two
// points. After that, rotate it to line up ellipse axes with coordinate
// axes.
//
const x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2
const y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2
const rx_sq = rx * rx
const ry_sq = ry * ry
const x1p_sq = x1p * x1p
const y1p_sq = y1p * y1p
// Step 2.
//
// Compute coordinates of the centre of this ellipse (cx', cy')
// in the new coordinate system.
//
let radicant = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq
if (radicant < 0) {
// due to rounding errors it might be e.g. -1.3877787807814457e-17
radicant = 0
}
radicant /= rx_sq * y1p_sq + ry_sq * x1p_sq
radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1)
const cxp = ((radicant * rx) / ry) * y1p
const cyp = ((radicant * -ry) / rx) * x1p
// Step 3.
//
// Transform back to get centre coordinates (cx, cy) in the original
// coordinate system.
//
const cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2
const cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2
// Step 4.
//
// Compute angles (theta1, delta_theta).
//
const v1x = (x1p - cxp) / rx
const v1y = (y1p - cyp) / ry
const v2x = (-x1p - cxp) / rx
const v2y = (-y1p - cyp) / ry
const theta1 = unit_vector_angle(1, 0, v1x, v1y)
let delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y)
if (fs === 0 && delta_theta > 0) {
delta_theta -= TAU
}
if (fs === 1 && delta_theta < 0) {
delta_theta += TAU
}
return [cx, cy, theta1, delta_theta]
}
//
// Approximate one unit arc segment with bézier curves,
// see http://math.stackexchange.com/questions/873224
//
function approximate_unit_arc (theta1, delta_theta) {
const alpha = (4 / 3) * Math.tan(delta_theta / 4)
const x1 = Math.cos(theta1)
const y1 = Math.sin(theta1)
const x2 = Math.cos(theta1 + delta_theta)
const y2 = Math.sin(theta1 + delta_theta)
return [
x1,
y1,
x1 - y1 * alpha,
y1 + x1 * alpha,
x2 + y2 * alpha,
y2 - x2 * alpha,
x2,
y2
]
}
export default function a2c (x1, y1, x2, y2, fa, fs, rx, ry, phi) {
const sin_phi = Math.sin((phi * TAU) / 360)
const cos_phi = Math.cos((phi * TAU) / 360)
// Make sure radii are valid
//
const x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2
const y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2
if (x1p === 0 && y1p === 0) {
// we're asked to draw line to itself
return []
}
if (rx === 0 || ry === 0) {
// one of the radii is zero
return []
}
// Compensate out-of-range radii
//
rx = Math.abs(rx)
ry = Math.abs(ry)
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry)
if (lambda > 1) {
rx *= Math.sqrt(lambda)
ry *= Math.sqrt(lambda)
}
// Get center parameters (cx, cy, theta1, delta_theta)
//
const cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi)
const result = []
let theta1 = cc[2]
let delta_theta = cc[3]
// Split an arc to multiple segments, so each segment
// will be less than τ/4 (= 90°)
//
const segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1)
delta_theta /= segments
for (let i = 0; i < segments; i++) {
result.push(approximate_unit_arc(theta1, delta_theta))
theta1 += delta_theta
}
// We have a bezier approximation of a unit circle,
// now need to transform back to the original ellipse
//
return result.map(curve => {
for (let i = 0; i < curve.length; i += 2) {
let x = curve[i + 0]
let y = curve[i + 1]
// scale
x *= rx
y *= ry
// rotate
const xp = cos_phi * x - sin_phi * y
const yp = sin_phi * x + cos_phi * y
// translate
curve[i + 0] = xp + cc[0]
curve[i + 1] = yp + cc[1]
}
return curve
})
}