getPointAtLength()
メソッドを使用して、パス上の線の方向を取得する試み。
Last active
June 2, 2018 20:43
-
-
Save sounisi5011/c377120cb758cc29264bf11863db6b1c to your computer and use it in GitHub Desktop.
SVG getPoint angle test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# See https://bl.ocks.org/-/about | |
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang=ja> | |
<meta charset=utf-8> | |
<meta name=viewport content="width=device-width,initial-scale=1"> | |
<meta name=format-detection content="telephone=no,email=no,address=no"> | |
<title>SVG getPoint angle test</title> | |
<link rel=preload href=./main.css as=style> | |
<link rel=preload href=./main.js as=script> | |
<link href=./main.css rel=stylesheet> | |
<body> | |
<h1>SVG getPoint angle test</h1> | |
<svg viewBox="0 0 100 100"> | |
<path id="stroke" fill="none" stroke="darkgrey" stroke-width=".5" | |
d="M 5 5 | |
L 30 30 | |
A 10 10 0 0 0 60 30 | |
L 95 15 | |
75 50 | |
85 90 | |
75 70 | |
65 95 | |
65 50 | |
Q 45 45 40 65 | |
T 30 80 | |
Q 25 130 10 20 | |
L 5 60 | |
Z" | |
/> | |
<circle id="point" fill="red" r=".5" /> | |
<circle id="front_point" fill="aqua" r=".5" /> | |
<circle id="back_point" fill="aqua" r=".5" /> | |
<path id="arrow" d="M -3 5 0 -5 3 5" fill="none" stroke="black" stroke-width=".5" /> | |
<g transform="translate(50 10)"> | |
<g transform="translate(-21 0)"> | |
<path id="front_angle_arrow" d="M -2 2 0 -2 2 2" fill="none" stroke="black" stroke-width=".5" /> | |
<text id="front_angle_text" x="0" y="-3.5" font-size="4" text-anchor="middle">0°</text> | |
</g> | |
<g transform="translate(0 0)"> | |
<path id="current_angle_arrow" d="M -2 2 0 -2 2 2" fill="none" stroke="black" stroke-width=".5" /> | |
<text id="current_angle_text" x="0" y="-3.5" font-size="4" text-anchor="middle">0°</text> | |
</g> | |
<g transform="translate(21 0)"> | |
<path id="back_angle_arrow" d="M -2 2 0 -2 2 2" fill="none" stroke="black" stroke-width=".5" /> | |
<text id="back_angle_text" x="0" y="-3.5" font-size="4" text-anchor="middle">0°</text> | |
</g> | |
</g> | |
</svg> | |
<fieldset> | |
<legend>Animation Controller</legend> | |
<p> | |
<input type=button id=play_button value=Play> | |
<input type=button id=stop_button value=Stop> | |
</p> | |
<p> | |
<label> | |
<span id=start_distance>0</span> | |
<input type=range id=current_distance_input> | |
<span id=stop_distance>500</span> | |
</label> | |
</p> | |
</fieldset> | |
<h2>Gist</h2> | |
<p><a href=https://gist.github.com/sounisi5011/c377120cb758cc29264bf11863db6b1c>gist.github.com<wbr>/sounisi5011<wbr>/c377120cb758cc29264bf11863db6b1c</a></p> | |
<script src=./main.js></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
svg { | |
display: block; | |
width: 300px; | |
height: 300px; | |
margin: 0 auto; | |
border: solid 1px #ccc; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @param {!SVGGeometryElement|!SVGPathElement} svgElem | |
* @param {number} length | |
* @return {number} | |
*/ | |
const normalizedPathLength = (svgElem, length) => { | |
const totalLength = svgElem.getTotalLength(); | |
const startPoint = svgElem.getPointAtLength(0); | |
const endPoint = svgElem.getPointAtLength(totalLength); | |
const isClosePath = | |
startPoint.x === endPoint.x && startPoint.y === endPoint.y; | |
if (length < 0) { | |
if (isClosePath) { | |
return length + totalLength; | |
} else { | |
return 0; | |
} | |
} else if (totalLength < length) { | |
if (isClosePath) { | |
return length - totalLength; | |
} else { | |
return totalLength; | |
} | |
} else { | |
return length; | |
} | |
}; | |
/** | |
* @param {number} degreeNumber | |
* @return {number} =>0 && <360 | |
*/ | |
const normalizedDegree = degreeNumber => { | |
degreeNumber %= 360; | |
if (degreeNumber < 0) { | |
degreeNumber += 360; | |
} | |
return degreeNumber; | |
}; | |
// SVG | |
const strokeElem = document.getElementById('stroke'); | |
const pointElem = document.getElementById('point'); | |
const frontPointElem = document.getElementById('front_point'); | |
const backPointElem = document.getElementById('back_point'); | |
const arrowElem = document.getElementById('arrow'); | |
const frontAngleArrowElem = document.getElementById('front_angle_arrow'); | |
const frontAngleTextElem = document.getElementById('front_angle_text'); | |
const currentAngleArrowElem = document.getElementById('current_angle_arrow'); | |
const currentAngleTextElem = document.getElementById('current_angle_text'); | |
const backAngleArrowElem = document.getElementById('back_angle_arrow'); | |
const backAngleTextElem = document.getElementById('back_angle_text'); | |
// HTML Input | |
const playButtonElem = document.getElementById('play_button'); | |
const stopButtonElem = document.getElementById('stop_button'); | |
const currentDistanceInputElem = document.getElementById( | |
'current_distance_input', | |
); | |
const stopDistanceElem = document.getElementById('stop_distance'); | |
const MOVE_DISTANCE = 0.25; | |
const DEFAULT_DISTANCE = 0; | |
const FRONT_POINT_DISTANCE = 1; | |
const BACK_POINT_DISTANCE = 1; | |
let currentDistance = DEFAULT_DISTANCE; | |
let isPointMove = true; | |
const inputCurrentDistanceListener = () => { | |
const newDistance = parseFloat(currentDistanceInputElem.value); | |
if (!Number.isNaN(newDistance)) { | |
currentDistance = newDistance; | |
} | |
}; | |
currentDistanceInputElem.step = MOVE_DISTANCE; | |
currentDistanceInputElem.addEventListener( | |
'input', | |
inputCurrentDistanceListener, | |
); | |
currentDistanceInputElem.addEventListener( | |
'change', | |
inputCurrentDistanceListener, | |
); | |
playButtonElem.addEventListener('click', () => { | |
isPointMove = !isPointMove; | |
}); | |
stopButtonElem.addEventListener('click', () => { | |
isPointMove = false; | |
currentDistance = DEFAULT_DISTANCE; | |
}); | |
{ | |
let _toCenterAngle = (frontAngle, backAngle) => { | |
/* | |
* 角度を正規化 | |
*/ | |
frontAngle = normalizedDegree(frontAngle); | |
backAngle = normalizedDegree(backAngle); | |
/* | |
* 角度の差分を求める | |
*/ | |
const diff = frontAngle - backAngle; | |
/* | |
* 差分の中心角度を求める | |
*/ | |
const centerAngle = backAngle + diff / 2; | |
if (Math.abs(diff) <= 180) { | |
/* | |
* 角度の差分が半分以下(180度以内)の場合は、 | |
* 中心座標を返す。 | |
*/ | |
return normalizedDegree(centerAngle); | |
} else { | |
/* | |
* 角度の差分が半分より大きい(180度より大きい)場合は、 | |
* 中心座標を反転させて返す。 | |
*/ | |
return normalizedDegree(centerAngle + 180); | |
} | |
}; | |
Object.defineProperty(window, 'toCenterAngle', { | |
get() { | |
return _toCenterAngle; | |
}, | |
set(value) { | |
if (typeof value === 'function') { | |
_toCenterAngle = value; | |
} | |
}, | |
enumerable: true, | |
}); | |
} | |
(function animate() { | |
const point = strokeElem.getPointAtLength(currentDistance); | |
pointElem.setAttributeNS(null, 'cx', point.x); | |
pointElem.setAttributeNS(null, 'cy', point.y); | |
const totalLength = strokeElem.getTotalLength(); | |
currentDistanceInputElem.value = currentDistance; | |
currentDistanceInputElem.max = totalLength; | |
stopDistanceElem.textContent = totalLength; | |
const frontDistance = normalizedPathLength( | |
strokeElem, | |
currentDistance + FRONT_POINT_DISTANCE, | |
); | |
const frontPoint = strokeElem.getPointAtLength(frontDistance); | |
frontPointElem.setAttributeNS(null, 'cx', frontPoint.x); | |
frontPointElem.setAttributeNS(null, 'cy', frontPoint.y); | |
const backDistance = normalizedPathLength( | |
strokeElem, | |
currentDistance - BACK_POINT_DISTANCE, | |
); | |
const backPoint = strokeElem.getPointAtLength(backDistance); | |
backPointElem.setAttributeNS(null, 'cx', backPoint.x); | |
backPointElem.setAttributeNS(null, 'cy', backPoint.y); | |
/** | |
* @see https://stackoverflow.com/a/32793413/4907315 | |
*/ | |
const frontAngle = normalizedDegree( | |
Math.atan2(frontPoint.y - point.y, frontPoint.x - point.x) * | |
(180 / Math.PI) + | |
90, | |
); | |
frontAngleArrowElem.setAttributeNS( | |
null, | |
'transform', | |
`rotate(${frontAngle})`, | |
); | |
frontAngleTextElem.textContent = `${Math.round(frontAngle * 1000) / 1000}°`; | |
const backAngle = normalizedDegree( | |
Math.atan2(point.y - backPoint.y, point.x - backPoint.x) * (180 / Math.PI) + | |
90, | |
); | |
backAngleArrowElem.setAttributeNS(null, 'transform', `rotate(${backAngle})`); | |
backAngleTextElem.textContent = `${Math.round(backAngle * 1000) / 1000}°`; | |
const angle = window.toCenterAngle(frontAngle, backAngle); | |
currentAngleArrowElem.setAttributeNS(null, 'transform', `rotate(${angle})`); | |
currentAngleTextElem.textContent = `${Math.round(angle * 1000) / 1000}°`; | |
arrowElem.setAttributeNS( | |
null, | |
'transform', | |
`translate(${point.x} ${point.y}) rotate(${angle})`, | |
); | |
if (isPointMove) { | |
playButtonElem.value = 'Pause'; | |
currentDistance += MOVE_DISTANCE; | |
if (totalLength < currentDistance) { | |
currentDistance -= totalLength; | |
} | |
} else { | |
playButtonElem.value = 'Play'; | |
} | |
requestAnimationFrame(animate); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment