SVGで、CTM(Current Transformation Matrix)を取得し座標変換を行う。
Last active
May 25, 2018 19:50
-
-
Save sounisi5011/b434f72570a7c72003b0728120527e60 to your computer and use it in GitHub Desktop.
SVG CTM 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 CTM 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 CTM test</h1> | |
<svg id="root_svg" viewBox="0 0 100 100" preserveAspectRatio="none"> | |
<g transform="translate(15 5) scale(.5) rotate(30)"> | |
<rect x="0" y="0" width="100%" height="100%" fill="red" fill-opacity=".5" /> | |
<circle id="red_point" r="3" fill="none" stroke="red" stroke-width="1" display="none" /> | |
</g> | |
<g transform="translate(15 80) scale(.8) rotate(-71)"> | |
<rect x="0" y="0" width="100%" height="100%" fill="blue" fill-opacity=".5" /> | |
<circle id="blue_point" r="4" fill="none" stroke="blue" stroke-width="1" display="none" /> | |
</g> | |
<svg viewBox="-30 0 50 25" x="0" y="60" width="50" height="50" preserveAspectRatio="none"> | |
<rect x="0" y="0" width="100%" height="100%" fill="green" fill-opacity=".5" /> | |
<circle id="green_point" r="6" fill="none" stroke="green" stroke-width="1" /> | |
</svg> | |
<circle id="black_point" r=".5" fill="black" display="none" /> | |
</svg> | |
<h2>Gist</h2> | |
<p><a href=https://gist.github.com/sounisi5011/b434f72570a7c72003b0728120527e60>gist.github.com<wbr>/sounisi5011<wbr>/b434f72570a7c72003b0728120527e60</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
#root_svg { | |
display: block; | |
max-width: 600px; | |
width: 100vw; | |
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 {number} max | |
* @param {number} mim | |
* @return {number} | |
*/ | |
function randInt(max, min = 0) { | |
return Math.floor(Math.random() * (max - min) + min); | |
} | |
/** | |
* 第一引数に指定された要素の座標を、 | |
* 第二引数に指定された要素の座標へと変換するための | |
* アフィン行列を取得する。 | |
* @param {!SVGGraphicsElement|!SVGLocatable} fromElem | |
* @param {!SVGGraphicsElement|!SVGLocatable} toElem | |
* @return {!SVGMatrix} | |
*/ | |
function getTransformMatrix(fromElem, toElem) { | |
/** | |
* @see https://github.com/dagrejs/dagre-d3/issues/202#issuecomment-173653044 | |
* @see https://greensock.com/forums/topic/13671-gettransformtoelement-removed-in-chrome-48/ | |
* @see https://www.jointjs.com/blog/announcement-gettransformtoelement-polyfill | |
* | |
* Note: getCTMメソッドを使用すると、親要素のsvg要素が異なる場合に正しく座標変換されない。 | |
* getCTMメソッドはビューポート座標系への変換行列を返すため、 | |
* viewBox属性でビューポートを定義するsvg要素が異なる場合は変換に失敗する。 | |
* 一方、getScreenCTMメソッドはディスプレイへの変換行列を返すため、 | |
* 同一ドキュメント内の要素であれば正しく座標変換できる。 | |
*/ | |
return toElem.getScreenCTM() | |
.inverse() | |
.multiply(fromElem.getScreenCTM()); | |
} | |
/** | |
* SVGPointオブジェクトを作成する。 | |
* @param {!SVGSVGElement|!SVGElement} svgElem | |
* @param {!SVGPoint|!{x: number, y: number}} point | |
* @return {!SVGPoint|null} | |
*/ | |
function createSVGPoint(svgElem, {x = 0, y = 0} = {}) { | |
while ((typeof svgElem.createSVGPoint !== 'function') && svgElem.ownerSVGElement) { | |
svgElem = svgElem.ownerSVGElement; | |
} | |
if (!svgElem) { | |
return null; | |
} | |
const point = svgElem.createSVGPoint(); | |
point.x = x; | |
point.y = y; | |
return point; | |
} | |
/** | |
* 第一引数に指定した要素の座標系における第二引数の座標を、 | |
* 第三引数に指定した要素の座標系へと変換する。 | |
* @param {!SVGGraphicsElement|!SVGEllipseElement|!SVGSVGElement|!SVGDefsElement|!SVGPolygonElement|!SVGPathElement|!SVGCircleElement|!SVGUseElement|!SVGPolylineElement|!SVGGElement|!SVGSwitchElement|!SVGImageElement|!SVGRectElement|!SVGAElement|!SVGClipPathElement|!SVGTextElement|!SVGLineElement|!SVGForeignObjectElement} fromElem | |
* @param {!SVGPoint|!{x: number, y: number}} fromPoint | |
* @param {!SVGGraphicsElement|!SVGLocatable} toElem | |
* @return {!SVGPoint} | |
*/ | |
function transformCoordinate(fromElem, fromPoint, toElem) { | |
const point = createSVGPoint(fromElem, fromPoint); | |
const outPoint1 = point | |
.matrixTransform(fromElem.getScreenCTM()) | |
.matrixTransform(toElem.getScreenCTM().inverse()); | |
const outPoint2 = point | |
.matrixTransform( | |
getTransformMatrix(fromElem, toElem) | |
); | |
if (outPoint1.x !== outPoint2.x || outPoint1.y !== outPoint2.y) { | |
console.group('transformCoordinate()'); | |
console.warn('Point difference!'); | |
if (outPoint1.x !== outPoint2.x) { | |
console.warn(`outPoint1.x (${outPoint1.x}) != outPoint2.x (${outPoint2.x})`); | |
} | |
if (outPoint1.y !== outPoint2.y) { | |
console.warn(`outPoint1.y (${outPoint1.y}) != outPoint2.y (${outPoint2.y})`); | |
} | |
console.log('outPoint1', outPoint1); | |
console.log('outPoint2', outPoint2); | |
console.groupEnd(); | |
} | |
return outPoint2; | |
} | |
/** | |
* circle要素の中心座標を設定する | |
* @param {!SVGCircleElement} circleElem | |
* @param {!SVGPoint|!{x: number, y: number}} centerPoint | |
*/ | |
function setPoint2circleElem(circleElem, centerPoint) { | |
circleElem.setAttributeNS(null, 'cx', centerPoint.x); | |
circleElem.setAttributeNS(null, 'cy', centerPoint.y); | |
} | |
// ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- // | |
const rootSvgElem = document.getElementById('root_svg'); | |
const blackPointElem = document.getElementById('black_point'); | |
const pointElemList = [ | |
document.getElementById('red_point'), | |
document.getElementById('blue_point'), | |
document.getElementById('green_point') | |
]; | |
/** | |
* 指定された座標へ、黒点を移動する。 | |
* SVGPointに対し、CTMを直接適用して座標変換する。 | |
* @param {!SVGPoint|!{x: number, y: number}} coordinate | |
*/ | |
const movePointPos1 = ({x: coordinateX = randInt(100), y: coordinateY = randInt(100)} = {}) => { | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.groupCollapsed(`[${coordinateX}, ${coordinateY}]`); | |
/* | |
* 黒点の座標を示すSVGPointを作成 | |
*/ | |
const blackPoint = createSVGPoint(rootSvgElem, { | |
x: coordinateX, | |
y: coordinateY, | |
}); | |
/* | |
* 黒点の位置を更新 | |
*/ | |
setPoint2circleElem(blackPointElem, blackPoint); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.log('blackPoint', blackPoint); | |
/* | |
* 黒点の座標を、ルート要素の座標系へ変換する | |
*/ | |
const rootPoint = blackPoint | |
.matrixTransform(blackPointElem.getScreenCTM()); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.log('rootPoint', rootPoint); | |
for (const pointElem of pointElemList) { | |
/* | |
* ルート要素の座標系における黒点の座標を、対象の点の座標系へ変換する | |
*/ | |
const point = rootPoint | |
.matrixTransform(pointElem.getScreenCTM().inverse()); | |
/* | |
* 点の位置を更新 | |
*/ | |
setPoint2circleElem(pointElem, point); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.group(pointElem.id); | |
console.log('point', point); | |
console.groupEnd(); | |
} | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.groupEnd(); | |
}; | |
/** | |
* 指定された座標へ、黒点を移動する。 | |
* CTMから変換用のアフィン行列を作成し、SVGPointに適用して座標変換する。 | |
* @param {!SVGPoint|!{x: number, y: number}} coordinate | |
*/ | |
const movePointPos2 = ({x: coordinateX = randInt(100), y: coordinateY = randInt(100)} = {}) => { | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.groupCollapsed(`[${coordinateX}, ${coordinateY}]`); | |
/* | |
* 黒点の座標を示すSVGPointを作成 | |
*/ | |
const blackPoint = createSVGPoint(rootSvgElem, { | |
x: coordinateX, | |
y: coordinateY, | |
}); | |
/* | |
* 黒点の位置を更新 | |
*/ | |
setPoint2circleElem(blackPointElem, blackPoint); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.log('blackPoint', blackPoint); | |
/* | |
* 黒点の座標をルート要素の座標系へ変換するアフィン行列を作成 | |
*/ | |
const blackPointCTM = blackPointElem.getScreenCTM(); | |
const blackPointInversedCTM = blackPointCTM.inverse(); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.log('blackPointCTM', blackPointCTM); | |
for (const pointElem of pointElemList) { | |
/* | |
* 黒点の座標系を対象の点の座標系に変換するアフィン行列を作成 | |
*/ | |
const black2targetPointMatrix = blackPointInversedCTM | |
.multiply(pointElem.getScreenCTM()) | |
.inverse(); | |
/* | |
* 黒点の位置を対象の点の座標系へ変換 | |
*/ | |
const point = blackPoint.matrixTransform(black2targetPointMatrix); | |
/* | |
* 対象の点の位置を更新 | |
*/ | |
setPoint2circleElem(pointElem, point); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.group(pointElem.id); | |
console.log('point', point); | |
console.log('black2targetPointMatrix', black2targetPointMatrix); | |
console.groupEnd(); | |
} | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.groupEnd(); | |
}; | |
/** | |
* 指定された座標へ、黒点を移動する。 | |
* CTMから変換用のアフィン行列を作成し、SVGPointに適用して座標変換する。 | |
* @param {!SVGPoint|!{x: number, y: number}} coordinate | |
*/ | |
const movePointPos3 = ({x: coordinateX = randInt(100), y: coordinateY = randInt(100)} = {}) => { | |
const point = {x: coordinateX, y: coordinateY}; | |
/* | |
* 黒点の位置を更新 | |
*/ | |
setPoint2circleElem(blackPointElem, point); | |
for (const pointElem of pointElemList) { | |
/* | |
* 黒点の位置を対象の点の座標系へ変換 | |
*/ | |
const targetPoint = transformCoordinate(blackPointElem, point, pointElem); | |
/* | |
* 対象の点の位置を更新 | |
*/ | |
setPoint2circleElem(pointElem, targetPoint); | |
} | |
}; | |
/* | |
* display属性を削除 | |
*/ | |
blackPointElem.removeAttributeNS(null, 'display'); | |
for (const pointElem of pointElemList) { | |
pointElem.removeAttributeNS(null, 'display'); | |
} | |
/* | |
* 点を設定 | |
*/ | |
movePointPos3({ x: 31, y: 65 }); | |
/* | |
* イベントを設定 | |
*/ | |
function movePointListener(evt) { | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.groupCollapsed('movePointListener()'); | |
const pointerPoint = createSVGPoint(rootSvgElem, { | |
x: evt.clientX, | |
y: evt.clientY, | |
}); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.log('pointerPoint', pointerPoint); | |
const svgPoint = pointerPoint | |
.matrixTransform(rootSvgElem.getScreenCTM().inverse()); | |
/* | |
* デバッグ用の情報を出力 | |
*/ | |
console.log('svgPoint', svgPoint); | |
console.groupEnd(); | |
movePointPos3(svgPoint); | |
} | |
rootSvgElem.addEventListener('click', movePointListener); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment