block by renecnielsen dcea5360f4447c123399

matrix: rotations

Full Screen

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

forked from enjalot‘s block: matrix: rotations

index.html

<!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>

bullets.json

[
  {"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}
]

scrubbing.js

// 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;
  }
}

style.css

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;
  }
}