Skip to content

Instantly share code, notes, and snippets.

@enjalot
Last active June 3, 2016 14:41
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save enjalot/65ae9c0fc95337107448 to your computer and use it in GitHub Desktop.
Save enjalot/65ae9c0fc95337107448 to your computer and use it in GitHub Desktop.
matrix: reboot

Matrix: reboot

Remaking http://ncase.me/matrix/ with SVG and d3.js

This technical experiment currently lacks the finesse and wistfulness of the original. I'm most inspired by the genius (yet obvious in retrospect) technique of showing both the original point and the transformed point connected by a line. Even the idea to use a letter is brilliant to me: it is familiar and intuitive, it allows for discrete sampling without introducing other concepts like pixels.

My hope is that by going through the exercise of making it more data-driven I can expand on the concept to introduce things like rotation. Perhaps using d3 will also make the code more concise, but that's not certain as the original is relatively short.

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

building-blocks

I wrote all this code inside building-blocks as a way to "dogfood" it and do an initial usability test. Working with multiple files has certainly exposed some gaps, but it was overall quite a pleasant experience.

[
{"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>
</head>
<body>
<svg width=960 height=250></svg>
<div id="math">
<div id="mtx_transform" class="matrix unselectable" style="width:180px">
<input value="1.5"/><input value="0.0"/><input value="0.0"/>
<input value="0.0"/><input value="1.0"/><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_expanded" class="matrix unselectable" expanded>
<div><span class="left">1</span>*<span class="right">1</span></div>
<div plus><span class="left">1</span>*<span class="right">1</span></div>
<div plus><span class="left">1</span>*<span class="right">1</span></div>
<div><span class="left">0</span>*<span class="right">0</span></div>
<div plus><span class="left">0</span>*<span class="right">0</span></div>
<div plus><span class="left">0</span>*<span class="right">0</span></div>
<div><span class="left">0</span>*<span class="right">x</span></div>
<div plus><span class="left">0</span>*<span class="right">y</span></div>
<div plus><span class="left">1</span>*<span class="right">1</span></div>
<div class="label">
how to multiply transformation matrix &amp; vector
<br>
<span>(hover over each cell)</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_expanded_left = document.querySelectorAll("#mtx_expanded span.left");
var mtx_expanded_right = document.querySelectorAll("#mtx_expanded span.right");
var mtx_expanded = document.querySelectorAll("#mtx_expanded")[0];
var mtx_transforms = document.querySelectorAll("#mtx_transform input");
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([250, 610]);
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() {
for(var i=0;i<6;i++){
var m = mtx_expanded_left[i];
var t = mtx_transforms[i];
m.innerHTML = t.value;
}
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;
render();
}
setupScrubbing(updateMatrixLeft);
for(var i=0;i<mtx_transforms.length;i++){
var input = mtx_transforms[i];
input.onchange = updateMatrixLeft;
makeScrubbable(input);
}
// 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;
function makeScrubbable(input){
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)*0.1; // 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:50px;
font-size: 15px; line-height: 50px; 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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment