Skip to content

Instantly share code, notes, and snippets.

@sounisi5011
Last active May 25, 2018 19:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sounisi5011/b434f72570a7c72003b0728120527e60 to your computer and use it in GitHub Desktop.
Save sounisi5011/b434f72570a7c72003b0728120527e60 to your computer and use it in GitHub Desktop.
SVG CTM 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 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>
#root_svg {
display: block;
max-width: 600px;
width: 100vw;
height: 300px;
margin: 0 auto;
border: solid 1px #ccc;
}
/**
* 指定された範囲内の乱数を整数で生成する。
* @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