Skip to content

Instantly share code, notes, and snippets.

@enjalot
Last active April 29, 2016 10:15
Show Gist options
  • Save enjalot/7b226e90c7338c69606b to your computer and use it in GitHub Desktop.
Save enjalot/7b226e90c7338c69606b to your computer and use it in GitHub Desktop.
matrix: rotations

Matrix: rotations

Extending http://ncase.me/matrix/ with SVG and d3.js, with a focus on 2D Rotation matrices.

I want to illustrate the relationship between angles and the matrix representation of a rotational transformation.

I'm still working on porting all of the original interactions, but I'm quite pleased with the work in progress.

Additional inspiration for Matrix UI (namely the coloring) comes from Max Goldstein's excellent Invitation to Another Dimension

forked from enjalot's block: matrix: reboot

[
{"x":-1,"y":-1},
{"x":-1,"y":-0.75},
{"x":-1,"y":-0.50},
{"x":-1,"y":-0.25},
{"x":-1,"y":0},
{"x":-1,"y":0.25},
{"x":-1,"y":0.50},
{"x":-1,"y":0.75},
{"x":-1,"y":1},
{"x":-0.83,"y":0.83},
{"x":-0.66,"y":0.66},
{"x":-0.50,"y":0.50},
{"x":-0.33,"y":0.33},
{"x":-0.16,"y":0.16},
{"x":0,"y":0},
{"x":1,"y":-1},
{"x":1,"y":-0.75},
{"x":1,"y":-0.50},
{"x":1,"y":-0.25},
{"x":1,"y":0},
{"x":1,"y":0.25},
{"x":1,"y":0.50},
{"x":1,"y":0.75},
{"x":1,"y":1},
{"x":0.83,"y":0.83},
{"x":0.66,"y":0.66},
{"x":0.50,"y":0.50},
{"x":0.33,"y":0.33},
{"x":0.16,"y":0.16}
]
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="scrubbing.js"></script>
<style>
.sin {
background-color: #bdeae8 !important;
}
.cos {
background-color: #bbb9de !important;
}
</style>
</head>
<body>
<svg width=960 height=250></svg>
<div id="math">
<div class="angle-container unselectable">
<div>Angle:</div> <input class="angle" value="0"/>
<span class="unit sin">sin(<span class="angle-in">0</span>) = <span class="sin-out">0</span></span>
<span class="unit cos">cos(<span class="angle-in">0</span>) = <span class="cos-out">1</span></span>
<!-- add dial to directly rotate -->
</div>
<div id="mtx_transform" class="matrix unselectable" style="width:180px">
<div class="transforms cos"></div><div class="transforms sin"></div> <input value="0.0"/>
<div class="transforms sin"></div><div class="transforms cos"/></div> <input value="0.0"/>
<div plain style="position: absolute; top: 120px;">0</div>
<div plain style="position: absolute; top: 120px; left:70px">0</div>
<div plain style="position: absolute; top: 120px; left:130px">1</div>
<div class="label">
the transformation matrix
<br>
<span>(adjust the numbers!)</span>
</div>
</div>
<div id="mtx_input" class="matrix unselectable" style="width:60px">
<div plain style="position: absolute; top: 0px;">x</div>
<div plain style="position: absolute; top: 60px;">y</div>
<div plain style="position: absolute; top: 120px;">1</div>
<div class="label">
a vector
<br>
<span>(hover over the dots)</span>
</div>
</div>
<div class="equals"></div>
<div id="mtx_output" class="matrix" style="width:60px">
<div>x'</div>
<div>y'</div>
<div>1</div>
<div class="label">
new vector
<br>
<span>(hover over the dots)</span>
</div>
</div>
</div>
<script>
var transform = {}; // global transform
var t = transform; // convenience
var bullets = []; // global data
var mtx_inputs = document.querySelectorAll("#mtx_input div");
var mtx_outputs = document.querySelectorAll("#mtx_output div");
var mtx_transforms = document.querySelectorAll("#mtx_transform input");
var transforms = d3.selectAll("div.transforms");
var angle = d3.select("input.angle").node();
var angleIn = d3.selectAll("span.angle-in");
var sinOut = d3.select("span.sin-out")
var cosOut = d3.select("span.cos-out")
function calculate(x,y){
x = x || 0;
y = y || 0;
var x2 = t.a*x + t.b*y + t.tx;
var y2 = t.c*x + t.d*y + t.ty;
return {x:x2, y:y2};
}
function render() {
var xscale = d3.scale.linear()
.domain([-1, 1])
.range([350, 510]);
var yscale = d3.scale.linear()
.domain([-1, 1])
.range([200, 50])
var transformed = bullets.map(function(d) {
return calculate(d.x, d.y)
})
var svg = d3.select("svg");
function hover(d,i) {
mtx_inputs[0].innerHTML = bullets[i].x.toFixed(1);
mtx_inputs[1].innerHTML = bullets[i].y.toFixed(1);
d3.select(mtx_inputs[0]).style("border", "3px solid red");
d3.select(mtx_inputs[1]).style("border", "3px solid red");
function filter(f,j) { return j === i }
d3.selectAll("line")
.filter(filter).style("stroke", "red")
d3.selectAll("circle.bullet")
.filter(filter).style("stroke", "red")
d3.selectAll("circle.transformed")
.filter(filter).style({stroke:"red", fill:"red"})
}
function mouseout(d,i) {
mtx_inputs[0].innerHTML = "x";
mtx_inputs[1].innerHTML = "y";
d3.select(mtx_inputs[0]).style("border", "3px solid #eee");
d3.select(mtx_inputs[1]).style("border", "3px solid #eee");
d3.selectAll("line").style("stroke", "#111");
d3.selectAll("circle.bullet").style("stroke", "#111")
d3.selectAll("circle.transformed").style({stroke:"#111", fill:"#111"})
}
var lines = svg.selectAll("line")
.data(bullets)
lines.enter().append("line")
.on("mouseover", hover)
.on("mouseout", mouseout)
lines
.transition()
.duration(170)
.ease("linear")
.attr({
x1: function(d,i) { return xscale(d.x) },
y1: function(d,i) { return yscale(d.y) },
x2: function(d,i) { return xscale(transformed[i].x)},
y2: function(d,i) { return yscale(transformed[i].y)},
stroke: "#111"
})
var circlesB = svg.selectAll("circle.bullet")
.data(bullets)
circlesB.enter().append("circle").classed("bullet", true)
.on("mouseover", hover)
.on("mouseout", mouseout)
circlesB.attr({
r: 4,
fill: "none",
stroke: "#111"
}).attr({
cx: function(d) { return xscale(d.x) },
cy: function(d) { return yscale(d.y) },
})
var circlesT = svg.selectAll("circle.transformed")
.data(transformed)
circlesT.enter().append("circle").classed("transformed", true)
.on("mouseover", hover)
.on("mouseout", mouseout)
circlesT.attr({
r: 8,
fill: "#111",
stroke: "#111"
})
.transition()
.duration(170)
.ease("linear")
.attr({
cx: function(d) { return xscale(d.x) },
cy: function(d) { return yscale(d.y) },
})
}
function updateMatrixLeft() {
var theta = angle.value;
//https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
var sin = Math.sin(theta*Math.PI/180).toFixed(2);
var cos = Math.cos(theta*Math.PI/180).toFixed(2);
transform.a = cos;
transform.b = -sin;
transform.c = sin;
transform.d = cos;
transform.tx = parseFloat(mtx_transforms[0].value) || 0;
transform.ty = parseFloat(mtx_transforms[1].value) || 0;
/*
mtx_transforms[0].value = cos;
mtx_transforms[1].value = -sin;
mtx_transforms[3].value = sin;
mtx_transforms[4].value = cos;
transform.a = parseFloat(mtx_transforms[0].value) || 0;
transform.b = parseFloat(mtx_transforms[1].value) || 0;
transform.tx = parseFloat(mtx_transforms[2].value) || 0;
transform.c = parseFloat(mtx_transforms[3].value) || 0;
transform.d = parseFloat(mtx_transforms[4].value) || 0;
transform.ty = parseFloat(mtx_transforms[5].value) || 0;
*/
var tvalues = [
transform.a, transform.b, transform.c, transform.d
]
transforms.text(function(d,i) { return tvalues[i] })
angleIn.text(theta);
sinOut.text(sin);
cosOut.text(cos);
render();
}
setupScrubbing(updateMatrixLeft);
for(var i=0;i<mtx_transforms.length;i++){
var input = mtx_transforms[i];
input.onchange = updateMatrixLeft;
makeScrubbable(input);
}
makeScrubbable(angle, 5);
// get the data and trigger the initial rendering
d3.json("bullets.json", function(err, data) {
bullets = data.map(function(d) {
return {x: d.x, y: d.y};
});
updateMatrixLeft();
})
</script>
</body>
// Make inputs scrubbable
var Mouse = {};
var scrubInput = null;
var scrubPosition = {x:0, y:0};
var scrubStartValue = 0;
var scrubId;
var stepSize;
function makeScrubbable(input, step){
if(!step) {
input.stepSize = 0.1;
} else {
input.stepSize = step;
}
input.onmousedown = function(e){
scrubInput = e.target;
scrubPosition.x = e.clientX;
scrubPosition.y = e.clientY;
scrubStartValue = parseFloat(input.value);
}
input.onclick = function(e){
e.target.select();
}
}
function setupScrubbing(cb) {
window.onmousemove = function(e){
// Mouse
Mouse.x = e.clientX;
Mouse.y = e.clientY;
// Scrubbing
if(!scrubInput) return;
scrubInput.blur();
var deltaX = e.clientX - scrubPosition.x;
deltaX = Math.round(deltaX/10)*scrubInput.stepSize; // 0.1 for every 10px
var val = scrubStartValue + deltaX;
scrubInput.value = (Math.round(val*10)/10).toFixed(1);
scrubId = null;
cb();
}
window.onmouseup = function(){
scrubInput = null;
}
}
body { margin: 0; overflow-x: none; }
svg {
background-color: #cccccc;
}
#math{
width: 960px;
height: 220px;
margin: 0px auto;
margin-top: -5px;
font-family: monospace;
/** HACK **/
-webkit-transform: scale(0.9);
-moz-transform: scale(0.9);
-ms-transform: scale(0.9);
transform: scale(0.9);
}
.matrix, .equals{
position: relative;
height:180px;
margin:10px;
margin-right:0;
float: left;
}
.matrix{
padding: 0 10px;
}
.matrix > input, .matrix > div{
float:left; margin:5px; position: relative;
width:50px; height:44px;
font-size: 15px; line-height: 44px; text-align: center;
background: #eee
}
.matrix > input{
border: 2px solid #bbb;
display: block;
width:44px; height:44px;
font-size: 15px;
font-family: monospace; cursor: col-resize;
}
.matrix[expanded]{
width:300px;
}
.matrix[expanded] > div{
position: relative;
width:80px; margin:5px 10px;
font-size: 12px; cursor: pointer;
}
.matrix[expanded] > div[plus]:before{
content: '+';
position: absolute; left: -16px;
font-size: 20px; text-align: center;
width:0px; height:0px;
color: #000;
}
.matrix:before, .matrix:after{
content:'';
position:absolute;
width:20px; height:190px;
border: 5px solid #000;
top:-10px;
}
.matrix[highlight=yes]:before, .matrix[highlight=yes]:after{
border-color: #DD3838;
}
.matrix:before{
left:0;
border-right: none;
}
.matrix:after{
right:0;
border-left: none;
}
.equals{
width:60px;
}
.equals:after{
content: '';
width:40px;height:20px;
position: absolute; margin:auto;
top:0; bottom:0; left:0; right:0;
border: 5px solid #000;border-left: none;border-right: none;
}
.matrix > .label, .matrix[expanded] > .label{
font-size: 15px;float: none;background: none;
width: 100%;
position: absolute;margin: 0;
top: 195px;left: 0px;
line-height: 20px;font-family: Helvetica, Arial, sans-serif;
}
.matrix > .label > span{
color: #888;
}
.matrix > div[plain]{
border: 3px solid #eee;
width: 44px; height: 44px;
}
.unselectable{
-webkit-user-select: none; /* Chrome all / Safari all */
-moz-user-select: none; /* Firefox all */
-ms-user-select: none; /* IE 10+ */
/* No support for these yet, use at own risk */
-o-user-select: none;
user-select: none;
}
.angle-container{
position: relative;
width: 200px;
height:180px;
margin:10px;
margin-right:0;
float: left;
padding: 0 0px;
}
.angle-container > input, .angle-container > div{
float:left; margin:5px; position: relative;
width:50px; height:44px;
font-size: 15px; line-height: 44px; text-align: center;
}
.angle-container > input{
border: 2px solid #bbb;
display: block;
width:54px; height:44px;
font-size: 15px;
font-family: monospace; cursor: col-resize;
}
.unit {
float:left;
clear:left;
margin:10px 10px;
display: block;
font-size: 15px; line-height: 44px;
}
/* http://codepen.io/NobodyRocks/pen/qzfoc */
input.angle {
background-color:gray;
-webkit-animation: neon1 1.5s ease-in-out infinite alternate;
-moz-animation: neon1 1.5s ease-in-out infinite alternate;
animation: neon1 1.5s ease-in-out infinite alternate;
}
@-webkit-keyframes neon1 {
from {
text-shadow: 0 0 10px #fff,
0 0 20px #fff,
0 0 30px #fff,
0 0 40px #FF1177,
0 0 70px #FF1177,
0 0 80px #FF1177,
0 0 100px #FF1177,
0 0 150px #FF1177;
}
to {
text-shadow: 0 0 5px #fff,
0 0 10px #fff,
0 0 15px #fff,
0 0 20px #FF1177,
0 0 35px #FF1177,
0 0 40px #FF1177,
0 0 50px #FF1177,
0 0 75px #FF1177;
}
}
@-moz-keyframes neon1 {
from {
text-shadow: 0 0 10px #fff,
0 0 20px #fff,
0 0 30px #fff,
0 0 40px #FF1177,
0 0 70px #FF1177,
0 0 80px #FF1177,
0 0 100px #FF1177,
0 0 150px #FF1177;
}
to {
text-shadow: 0 0 5px #fff,
0 0 10px #fff,
0 0 15px #fff,
0 0 20px #FF1177,
0 0 35px #FF1177,
0 0 40px #FF1177,
0 0 50px #FF1177,
0 0 75px #FF1177;
}
}
@keyframes neon1 {
from {
text-shadow: 0 0 10px #fff,
0 0 20px #fff,
0 0 30px #fff,
0 0 40px #FF1177,
0 0 70px #FF1177,
0 0 80px #FF1177,
0 0 100px #FF1177,
0 0 150px #FF1177;
}
to {
text-shadow: 0 0 5px #fff,
0 0 10px #fff,
0 0 15px #fff,
0 0 20px #FF1177,
0 0 35px #FF1177,
0 0 40px #FF1177,
0 0 50px #FF1177,
0 0 75px #FF1177;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment