Skip to content

Instantly share code, notes, and snippets.

@dwskau
Created July 14, 2012 06:07
Show Gist options
  • Save dwskau/3109641 to your computer and use it in GitHub Desktop.
Save dwskau/3109641 to your computer and use it in GitHub Desktop.
just another inlet to tributary
//inspired by http://bl.ocks.org/3014589
//and http://vis4.net/blog/posts/avoid-equidistant-hsv-colors/
//set to true for debugging
//tributary.trace = true;
trib.number_bars = 100;
trib_options.number_bars = {min: 1, max: 400};
var Handle = function() {
var data = new Backbone.Model({
//the x,y coordinates which are the actual location
x: 0,
y: -17,
//the x,y coordinates which represent the target or moving location
mx: 0,
my: 0,
color: "#ff0000",
sl: 0,
sr: 0,
height: 20
});
//map 0,1 to screen space
var x_scale = d3.scale.linear();
//map screen space to color range
var color_scale = d3.scale.linear();
var x_max = 1;
var x_min = 0;
//the handle's containing group
var group;
//reference to the svg gradient element
var gradient;
var handle = function(g) {
//represent the handle
//a handle is made up of a top part, with a color rect and a little arrow
gradient = g;
group = gradient.append("g")
.classed("handle", true)
.attr("transform", "translate(" + [data.get("x"), data.get("height")/2] + ")");
/*
group.append("polygon")
.attr("points", "0,-1 6,-1 6,5 3,8 0,5 ")
.attr("fill", "#e3e3e3")
//TODO: make this positioning properly calculated (center the control on the gradient point)
.attr("transform", "translate(" + [-5,0] +")scale(" + 2.0 + ")");
*/
group.append("rect")
.attr("width", 20)
.attr("height", 20)
.attr("transform", "translate(" + [-10, -10] + ")")
.style("fill", data.get("color"))
.style("stroke", "#ffffff")
.style("stroke-width", 3)
;
group.append("circle")
.classed("sl", true)
.attr("r", 5)
.style("stroke", "#ffffff")
.style("stroke-width", 1)
;
group.append("circle")
.classed("sr", true)
.attr("r", 5)
.style("stroke", "#ffffff")
.style("stroke-width", 1)
;
group.append("text")
.attr("x", 1)
.attr("y", -12)
.attr("font-size", 12)
.attr("text-anchor", "middle")
.attr("fill", "white")
.text(data.get("x"))
;
group.append("text")
.attr("x", 0)
.attr("y", -13)
.attr("font-size", 12)
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(data.get("x"))
;
//calculate the changes and update the data
data.on("change:mx", handle.update_x);
data.on("change:my", handle.update_y);
//update the representation
data.on("move", handle.move);
data.on("move", handle.move_picker);
//update the stop handles
data.on("change:sl", handle.update_sl);
data.on("change:sr", handle.update_sr);
handle.update_sl();
handle.update_sr();
data.on("change:color", handle.update_color);
handle.update_color();
//set the drag behavior
group.call(handle.drag);
//deal with the color picker
group.on("mousedown", handle.update_picker);
handle.update_picker();
data.on("remove", function() {
group.remove();
});
};
handle.move = function() {
//update the representation of the handle when moving
group.attr("transform", "translate(" + [data.get("x"), data.get("y")] + ")");
return handle;
};
handle.update_sl = function() {
group.select("circle.sl")
.attr("transform", "translate(" + [data.get("sl"), 0] + ")");
};
handle.update_sr = function() {
group.select("circle.sr")
.attr("transform", "translate(" + [data.get("sr"), 0] + ")");
};
handle.update_color = function() {
group.select("rect").style("fill", data.get("color"));
group.selectAll("circle").style("fill", data.get("color"));
};
handle.update_x = function() {
//this behavior constrains the x movement from the mouse/touch
var x = data.get("mx");
//limit movement to inside the gradient
//this will also limit handle to stay between two other handles
//(when a new handle is added, it becomes the max of the handle to it's left
//and the min of the handle to it's right
if(x > x_max) { x = x_max; }
if(x < x_min) { x = x_min; }
data.set({x: x});
};
//capture dragging behavior on the handle
handle.drag = d3.behavior.drag()
.on("drag", function(d,i) {
var dx = d3.event.dx;
var dy = d3.event.dy;
var mx = data.get("mx");
var my = data.get("my");
mx += dx;
my += dy;
var sl = -my; //down makes it go left
var sr = my; //up makes it go right
var x = data.get("x");
if(sl > 0) { sl = 0; }
if(x + sl < x_min) { sl = x_min - x; }
if(sr < 0) { sr = 0; }
if(x + sr > x_max) { sr = x_max - x; }
data.set({
mx:mx,
my:my,
dx:dx,
dy:dy,
sl:sl,
sr:sr
});
data.trigger("move");
})
.on("dragend", function(d,i) {
//d.first = false;
});
handle.update_picker = function() {
picker.toggle(true);
//grab just the hex #s and not the hashtag
var c = data.get("color").slice(1, 7);
handle.move_picker();
delete picker.callback;
picker.callback = function(rgba, state, type) {
//console.log("rgba", rgba);
var newcolor = Color.Space(rgba, "RGB>STRING");
data.set({color: "#" + newcolor});
};
picker.update(c);
};
handle.move_picker = function() {
//NOTE: this is least elegant part, due to global nature of colorpicker
//figure out the absolute position of the handle
//first get svg element's offset
var svg = group.node().nearestViewportElement;
var x = svg.offsetLeft;
var y = svg.offsetTop;
//get position of handle relative to svg
var rent = group.node().parentElement;
var p = svg.createSVGPoint();
var matrix = rent.getTransformToElement(svg);
p.x = 0;
p.y = 0;
var sp = p.matrixTransform(matrix);
//Update the location of the color picker
//don't like that i'm using globals here
var hx = sp.x + x + data.get("x") - colorpicker_size/2 - 20;
var hy = sp.y + y + data.get("y") - 2*colorpicker_size;
$('#colorpicker_gradient').css("top", hy);
$('#colorpicker_gradient').css("left", hx);
};
handle.data = function(value) {
if (!arguments.length) { return data; }
data.set(value);
return handle;
};
handle.x_min = function(value) {
if (!arguments.length) { return x_min; }
x_min = value;
return handle;
};
handle.x_max = function(value) {
if (!arguments.length) { return x_max; }
x_max = value;
return handle;
};
handle.color_scale = function(value) {
if (!arguments.length) { return color_scale; }
color_scale = value;
return handle;
};
return handle;
};
var Gradient = function() {
//layout properties
var x = 0;
var y = 0;
var width = 800;
var height = 100;
var color_scale;
var interpolator = d3.cie.interpolateLch;
var nbars = 800;
var bar_width;
var x_scale = d3.scale.linear().range([0, width]);
//the gradient's containing group
var group;
//defs element to keep reference to the svg gradient
var defs;
//reference to the svg gradient element
var svggrad;
//id of the gradient
var gradid = "gradient0";
//list of handles
var handles = [];
//allow other things to listen for change in the color_scale
var grad = new Backbone.Model();
var gradient = function(g) {
//create initial frame
group = g.append("g")
.attr("transform", "translate(" + [x, y] + ")");
var bars = d3.range(nbars);
bar_width = width/nbars;
var grad = group.selectAll("rect.gbar")
.data(bars)
.enter()
.append("rect")
.classed("gbar", true)
.attr("stroke", "none")
.attr("width", bar_width)
.attr("height", height)
.attr("transform", function(d,i) {
//return "translate(" + [i*bar_width, height/2 - bar_width] + ")";
return "translate(" + [i*bar_width, 0] + ")";
})
var rect = group.append("rect")
.classed("gradient", true)
.attr("width", width)
.attr("height", height)
//.attr("stroke", "#000000")
.attr("stroke", "none")
//.attr("fill", "url(#" + gradid + ")");
.attr("fill", "#ffffff")
.attr("fill-opacity", 0)
.attr("pointer-events", "all");
rect.on("click", gradient.add_handle);
//add 2 defualt handles at each end
gradient.add_handle({
x: 0,
mx: 0,
color: "#ff0000"
});
gradient.add_handle({
x: width,
mx: width,
color: "#0000ff"
});
};
gradient.add_handle = function(options) {
var hd = new Handle();
var x;
if(!options) {
//set the initial position to where the mouse was clicked
x = d3.mouse(this)[0];
} else if(options.x !== undefined) {
x = options.x;
} else {
//TODO: shouldn't need to repeat this line...
x = d3.mouse(this)[0];
}
if(!options) {
options = {
//id: handles.length,
x: x,
mx: x//TODO: shouldn't have to set this from outside...
};
}
options.height = height;
options.y = height/2;
hd.data(options);
//set the handle's bounds
//find the closest handle to the left of the click
//we take advantage of the fact that handles will always be in order
//(due to their behavior)
var i, ind = -1, hi, hix, insert;
for(i = handles.length; i >=0; i--) {
hi = handles[i];
if(hi) {
hix = hi.data().get("x");
if(hix < x) {
ind = i;
//update the left handles max to be the current x
hi.x_max(x);
break;
}
}
}
if(ind >= 0) {
//update the min we found
hd.x_min(hix);
//save the index we want to insert at
insert = ind+1;
} else {
//if none set min to 0;
hd.x_min(0);
insert = 0;
}
//find the closest handle to the right of the click
ind = -1;
for(i = 0; i < handles.length; i++) {
hi = handles[i];
if(hi) {
hix = hi.data().get("x");
if(hix > x) {
ind = i;
//update the right handles min to be current x
hi.x_min(x);
break;
}
}
}
if(ind >= 0) {
hd.x_max(hix);
} else {
//if none set min to width;
hd.x_max(width);
}
//add hd to list of handles
handles.splice(insert, 0, hd);
//listen on changes to x, update min and max of other handles
var hd_data = hd.data();
hd_data.on("change:x", function() {
var ind = handles.indexOf(hd);
if(ind > 0) {
//update x_max of left handle when moving
handles[ind-1].x_max(hd_data.get("x"));
}
if(ind < handles.length-1) {
//update x_max of left handle when moving
handles[ind+1].x_min(hd_data.get("x"));
}
});
//make the color an interpolation of the two colors this stop is between
//var color = ["#0000ff", "#ff0000", "#00ff00"];
//hd_data.set({color: color[parseInt(Math.random() * 3)]});
ind = handles.indexOf(hd);
var left, right, left_color, right_color, lx, rx, t;
if(ind > 0 && ind < handles.length-1) {
left = handles[ind-1].data();
right = handles[ind+1].data();
left_color = left.get("color");
right_color = right.get("color");
lx = left.get("x");
rx = right.get("x");
t = (x - lx) / (rx - lx);
//console.log(t, left_color, right_color, lx, x, rx)
//options.color = d3.interpolateRgb(left_color, right_color)(t);
options.color = interpolator(left_color, right_color)(t);
} else if(!options.color) {
options.color = "#ff0000";
}
hd_data.set({color: options.color});
update_colors();
//listen on the handle's change in sl and sr to update stop position
//listen for change in color to update the stops
//listen on the handle's movement to update the stop positions
hd_data.on("change:color change:sl change:sr move", function() {
var ind = handles.indexOf(hd);
//update_stop(ind);
update_colors();
});
function update_colors() {
var i,handle;
var domain = [];
var colors = [];
for(i = 0; i < handles.length; i++) {
handle = handles[i].data();
domain.push((handle.get("x") + handle.get("sl"))/width);
domain.push((handle.get("x") + handle.get("sr"))/width);
colors.push(handle.get("color"));
colors.push(handle.get("color"));
}
color_scale = d3.scale.linear()
.domain(domain)
.interpolate(interpolator)
.range(colors);
group.selectAll("rect.gbar")
.style("fill", function(d,i) {
return color_scale(i/nbars);
})
.style("stroke", function(d,i) {
return color_scale(i/nbars);
});
grad.trigger("update:color_scale", color_scale);
}
//remove a handle (by yanking off)
hd_data.on("change:dy", function() {
var dy = hd_data.get("dy");
if(dy < -30) {
var ind = handles.indexOf(hd);
//remove the svg representations
hd_data.trigger("remove");
/*
svggrad.select("#lstop" + (handles.length-1)).remove()
svggrad.select("#rstop" + (handles.length-1)).remove()
*/
//set the appropriate min and max
var left, right;
if(ind > 0 && ind < handles.length-1) {
left = handles[ind-1];
right = handles[ind+1];
left.x_max(right.data().get("x"));
right.x_min(left.data().get("x"));
} else if(ind === 0) {
right = handles[1];
right.x_min(0);
} else if(ind === handles.length-1) {
left = handles[ind-1];
left.x_max(width);
}
//remove the handle from the array
handles.splice(ind, 1);
hd_data.unbind();
picker.toggle("false");
//reupdate all stops
/*
for(i = 0; i < handles.length; i++) {
update_stop(i);
}
*/
update_colors();
}
});
//draw the handle
hd(group);
};
gradient.x = function(value) {
if (!arguments.length) { return x; }
x = value;
return gradient;
};
gradient.y = function(value) {
if (!arguments.length) { return y; }
y = value;
return gradient;
};
gradient.width = function(value) {
if (!arguments.length) { return width; }
width = value;
//update the scale when the width changes
x_scale.range([0, width]);
bar_width = width/nbars;
return gradient;
};
gradient.height = function(value) {
if (!arguments.length) { return height; }
height = value;
return gradient;
};
gradient.nbars = function(value) {
if (!arguments.length) { return nbars; }
nbars = value;
bar_width = width/nbars;
return gradient;
};
gradient.interpolator = function(value) {
if (!arguments.length) { return interpolator; }
interpolator = value;
return gradient;
};
gradient.grad = function(value) {
if (!arguments.length) { return grad; }
grad = value;
return gradient;
};
return gradient;
};
var svg = d3.select('svg');
var bg_rect = svg.append("rect").attr("width", "100%").attr("height", "100%")
.attr("fill-opacity", 0.1)
//.attr("fill", "#ffffff");
.attr("fill", "#d3d3d3");
var colorpicker_size = 110;
//remove colorpicker from the dom (for tributary)
$("#colorpicker_gradient").remove();
var picker = new Color.Picker({
id:"colorpicker_gradient",
color: "#643263",// accepts rgba(), or #hex
display: false,
size: colorpicker_size,
callback: function(rgba, state, type) {
}
});
$("#colorpicker_gradient").css("position", "absolute");
bg_rect.on("click", function() {
picker.toggle(false);
});
var w = tributary.sw-20;
var h = 100;
var n = parseInt(trib.number_bars);
var hi = 100;
var gradRGB = new Gradient();
svg.append("text")
.text("RGB")
.attr("x", 20)
.attr("y", hi-10);
gradRGB
.x(20)
.y(hi)
.width(w)
.height(h)
.interpolator(d3.interpolateRgb)
.nbars(n);
//make the shadow gradients
function makeBars(g) {
var bars = d3.range(n);
var bar_width = w/n;
var grad = g.selectAll("rect.gbar")
.data(bars)
.enter()
.append("rect")
.classed("gbar", true)
.attr("stroke", "none")
.attr("width", bar_width)
.attr("height", h)
.attr("transform", function(d,i) {
//return "translate(" + [i*bar_width, height/2 - bar_width] + ")";
return "translate(" + [i*bar_width, 0] + ")";
});
}
var gradEvents = gradRGB.grad();
gradEvents.on("update:color_scale", function(cs) {
var hsl_cs = cs.interpolate(d3.interpolateHsl);
svg.selectAll("g.hsl").selectAll("rect.gbar")
.style("fill", function(d,i) {
return hsl_cs(i/n);
})
.style("stroke", function(d,i) {
return hsl_cs(i/n);
});
var lch_cs = cs.interpolate(d3.cie.interpolateLch);
svg.selectAll("g.lch").selectAll("rect.gbar")
.style("fill", function(d,i) {
return lch_cs(i/n);
})
.style("stroke", function(d,i) {
return lch_cs(i/n);
});
var lab_cs = cs.interpolate(d3.cie.interpolateLab);
svg.selectAll("g.lab").selectAll("rect.gbar")
.style("fill", function(d,i) {
return lab_cs(i/n);
})
.style("stroke", function(d,i) {
return lab_cs(i/n);
});
});
var hsl = svg.append("g")
.classed("hsl", true)
.attr("transform", "translate(" + [20, hi+140] + ")")
hsl.append("text")
.text("HSL")
.attr("y", -10);
makeBars(hsl);
var lch = svg.append("g")
.classed("lch", true)
.attr("transform", "translate(" + [20, hi+280] + ")")
lch.append("text")
.text("LCh")
.attr("y", -10);
makeBars(lch);
var lab = svg.append("g")
.classed("lab", true)
.attr("transform", "translate(" + [20, hi+420] + ")")
lab.append("text")
.text("L*a*b")
.attr("y", -10);
makeBars(lab);
svg.append("text")
.text("click on the RGB gradient to mess with all of the color spaces.")
.attr("x", 20)
.attr("y", 30);
svg.append("text")
.text("look how nice and uniform LCh is! (3rd one down)")
.attr("x", 20)
.attr("y", 50);
gradRGB(svg);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment