p1、p2、p3
和 p3、p2、p1
看起来相同,但方向相反。y
值。
const { cos, sin, atan2 } = Math;
function map(v, s1,e1, s2,e2) {
return s2 + (v-s1) * (e2-s2)/(e1-s1);
}
const path = original.getAttribute(`d`);
const terms = path.replace(/[A-Z]/g, ``).split(/\s+/).map(v => parseFloat(v));
const points = [];
for(let i=0, e=terms.length; i<e; i=i+2) points[i/2] = terms.slice(i,i+2);
// Let's do the thing, which we don't actually need to do because for
// quadratic curves it'll turn out we already know at which "t" value
// the axis-aligned extremum can be found. But let's discover that anyway:
(function findExtremum([p1, p2, p3]) {
const [sx, sy] = p1;
const [cx, cy] = p2;
const [ex, ey] = p3;
// In order to realign, we only need to recompute three points.
// The other three are all zero.
const a = atan2(ey-sy, ex-sx);
const newcx = (cx-sx) * cos(-a) - (cy-sy) * sin(-a);
const newcy = (cx-sx) * sin(-a) + (cy-sy) * cos(-a);
const newex = (ex-sx) * cos(-a) - (ey-sy) * sin(-a);
aligned.setAttribute(`d`, `M0 0 Q${newcx} ${newcy} ${newex} 0`);
// If we work out the derivative, we discover we don't need it
// thanks to our translation/rotation step:
//
// const d = [
// [2 * (newcx - 0), 2 * (newcy - 0)],
// [2 * (newex - newcx), 2 * (0 - newcy)],
// ];
//
// Which, when we omit the zeroes, is:
//
// const d = [
// [2 * newcx, 2 * newcy],
// [2 * (newex - newcx), 2 * -newcy],
// ];
//
// We see two y values that are the same, except for the sign,
// and so the zero crossing lies at the midpoint, i.e. at the
// bezier control value t=0.5, and with that knowledge we can
// find the original point:
const t = 0.5;
// This is all we needed, and all the code we've written so far
// turns out to have been irrelevant: for quadratic curves, the
// axis-aligned extremum is *always* at t=0.5, so let's plug
// that into our curves to see the result:
const x = 2 * newcx * (1-t) * t + newex * t**2;
const y = 2 * newcy * (1-t) * t;
extremum.setAttribute(`cx`, x);
extremum.setAttribute(`cy`, y);
// and for our original curve:
const ox = sx * (1-t)**2 + 2 * cx * (1-t) * t + ex * t**2;
const oy = sy * (1-t)**2 + 2 * cy * (1-t) * t + ey * t**2;
point.setAttribute(`cx`, ox);
point.setAttribute(`cy`, oy);
})(points);
svg { border: 1px solid grey; }
p { display: inline-block; height: 120px; vertical-align: 40px; }
<svg width="120" height="100" viewBox="0 0 120 100">
<path id="original" fill="none" stroke="black" d="M20 50 Q50 10 100 80"/>
</svg>
<p>→</p>
<svg width="120" height="100" viewBox="0 0 120 100">
<g transform="translate(20,80)">
<path fill="none" stroke="grey" d="M0 -100L0 200M-100 0L200 0"/>
<path id="aligned" fill="none" stroke="black" d=""/>
<circle id="extremum" cx="0" cy="0" r="2"/>
</g>
</svg>
<p>→</p>
<svg width="120" height="100" viewBox="0 0 120 100">
<path fill="none" stroke="black" d="M20 50 Q50 10 100 80"/>
<circle id="point" cx="0" cy="0" r="2"/>
</svg>
const path = original.getAttribute(`d`);
const terms = path.replace(/[A-Z]/g, ``).split(/\s+/).map(v => parseFloat(v));
const points = [];
for(let i=0, e=terms.length; i<e; i=i+2) points[i/2] = terms.slice(i,i+2);
// If t=0.5 then (1-t)^2, (1-t)t, and t^2 are all 0.25, so
// we can drastically simplify things even more:
const x = (points[0][0] + 2*points[1][0] + points[2][0])/4;
const y = (points[0][1] + 2*points[1][1] + points[2][1])/4;
point.setAttribute(`cx`, x);
point.setAttribute(`cy`, y);
svg { border: 1px solid grey; }
p { display: inline-block; height: 120px; vertical-align: 40px; }
<svg width="120" height="100" viewBox="0 0 120 100">
<path id="original" fill="none" stroke="black" d="M20 50 Q50 10 100 80"/>
</svg>
<p>→</p>
<svg width="120" height="100" viewBox="0 0 120 100">
<path fill="none" stroke="black" d="M20 50 Q50 10 100 80"/>
<circle id="point" cx="0" cy="0" r="2"/>
</svg>
完成。
暴力破解:
对于一个SVG中的多条路径:
请注意粉色路径中的逻辑错误您需要修复
<peak-point>
<svg>
<path d="M20 50 Q50 10, 100 80"/>
<path d="M 10 49 Q 13 111 74 97" fill="pink"/>
</svg>
</peak-point>
<script>
customElements.define('peak-point', class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // make sure innerHTML SVG is parsed
this.querySelectorAll("path").forEach(path => {
const len = path.getTotalLength();
let point;
let pos = 0;
let pointAt = (at) => (point = path.getPointAtLength(at));
let previous_y = pointAt(0).y;
while (previous_y >= pointAt(pos).y) {
previous_y = point.y;
pos++;
}
path.insertAdjacentHTML("afterend",
`<circle cx="${point.x}" cy="${point.y}" r=5 fill="red"/>`);
})
})
}
});
</script>