Skip to content

Instantly share code, notes, and snippets.

@sounisi5011
Last active June 2, 2018 20:43
Show Gist options
  • Save sounisi5011/c377120cb758cc29264bf11863db6b1c to your computer and use it in GitHub Desktop.
Save sounisi5011/c377120cb758cc29264bf11863db6b1c to your computer and use it in GitHub Desktop.
SVG getPoint angle test
# See https://bl.ocks.org/-/about
license: mit
<!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>
svg {
display: block;
width: 300px;
height: 300px;
margin: 0 auto;
border: solid 1px #ccc;
}
/**
* @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