Load an image into this stack snippet and move your mouse over it. A black curve that follows the hue angle, starting at your cursor point, will be drawn:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><style>canvas{border:1px solid black;}</style>Load an image: <input type='file' onchange='load(this)'><br><br>Max length <input id='length' type='text' value='300'><br><br><div id='coords'></div><br><canvas id='c' width='100' height='100'>Your browser doesn't support the HTML5 canvas tag.</canvas><script>function load(t){if(t.files&&t.files[0]){var e=new FileReader;e.onload=setupImage,e.readAsDataURL(t.files[0])}}function setupImage(t){function e(t){t.attr("width",img.width),t.attr("height",img.height);var e=t[0].getContext("2d");return e.drawImage(img,0,0),e}img=$("<img>").attr("src",t.target.result)[0],ctx=e($("#c")),ctxRead=e($("<canvas>"))}function findPos(t){var e=0,a=0;if(t.offsetParent){do e+=t.offsetLeft,a+=t.offsetTop;while(t=t.offsetParent);return{x:e,y:a}}return void 0}$("#c").mousemove(function(t){function e(t,e){var a=ctxRead.getImageData(t,e,1,1).data,i=a[0]/255,r=a[1]/255,o=a[2]/255;return Math.atan2(Math.sqrt(3)*(r-o),2*i-r-o)}if("undefined"!=typeof img){var a=findPos(this),i=t.pageX-a.x,r=t.pageY-a.y;$("#coords").html("x = "+i.toString()+", y = "+r.toString());var o=parseInt($("#length").val());if(isNaN(o))return void alert("Bad max length!");for(var n=[i],f=[r],h=0;n[h]>=0&&n[h]<this.width&&f[h]>=0&&f[h]<this.height&&o>h;)n.push(n[h]+Math.cos(e(n[h],f[h]))),f.push(f[h]-Math.sin(e(n[h],f[h]))),h++;ctx.clearRect(0,0,this.width,this.height),ctx.drawImage(img,0,0);for(var h=0;h<n.length;h++)ctx.fillRect(Math.floor(n[h]),Math.floor(f[h]),1,1)}});</script>
I've only tested this snippet in Google Chrome.
For example, when the cursor is above red, the curve has a 0° slope, but when it's above yellow, it has a 60° slope. The curve continues on for the specified length, continuously changing its slope to match the hue.
Load up this image and when you pan the cursor across it, the line just around the cursor should do a complete counter-clockwise turn:
This and this are other neat images to try. (You'll need to save them and then load them with the snippet. They can't be directly linked due to cross-origin constraints.)
Here is a non-minified version of the snippet:
function load(i) { //thanks http://jsfiddle.net/vacidesign/ja0tyj0f/
if (i.files && i.files[0]) {
var r = new FileReader()
r.onload = setupImage
r.readAsDataURL(i.files[0])
}
}
function setupImage(e) {
img = $('<img>').attr('src', e.target.result)[0]
function setup(c) {
c.attr('width', img.width)
c.attr('height', img.height)
var ctx = c[0].getContext('2d')
ctx.drawImage(img, 0, 0)
return ctx
}
ctx = setup($('#c'))
ctxRead = setup($('<canvas>'))
}
function findPos(obj) {
var curleft = 0, curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return { x: curleft, y: curtop };
}
return undefined;
}
$('#c').mousemove(function(e) { //thanks http://stackoverflow.com/a/6736135/3823976
if (typeof img === 'undefined') return
var pos = findPos(this)
var x = e.pageX - pos.x
var y = e.pageY - pos.y
$('#coords').html('x = ' + x.toString() + ', y = ' + y.toString())
var maxLength = parseInt($('#length').val())
if (isNaN(maxLength)) {
alert('Bad max length!')
return
}
function hue(x, y) {
var rgb = ctxRead.getImageData(x, y, 1, 1).data
var r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255
return Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b)
}
//gather points
var xs = [x], ys = [y]
var i = 0
while (xs[i] >= 0 && xs[i] < this.width && ys[i] >= 0 && ys[i] < this.height && i < maxLength) {
xs.push(xs[i] + Math.cos(hue(xs[i], ys[i])))
ys.push(ys[i] - Math.sin(hue(xs[i], ys[i])))
i++
}
//draw stuff
ctx.clearRect(0, 0, this.width, this.height)
ctx.drawImage(img, 0, 0)
for (var i = 0; i < xs.length; i++)
ctx.fillRect(Math.floor(xs[i]), Math.floor(ys[i]), 1, 1) //not using strokes because they may be anti-aliased
})
canvas {
border:1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Load an image: <input type='file' onchange='load(this)'>
<br><br>
Max length <input id='length' type='text' value='300'>
<br><br>
<div id='coords'></div>
<br>
<canvas id='c' width='100' height='100'>Your browser doesn't support the HTML5 canvas tag.</canvas>
Challenge
Write a program that does what the snippet is doing, just not interactively. Take an image and an (x, y) coordinate in the bounds of image, and a maximum curve length. Output the same image with the added black curve that follows the hue angles starting at (x, y) and ends when it reached the maximum length or hits an image boundary.
Specifically, start the curve at (x, y) and measure the hue angle there. Go one unit (one pixel's width) in that direction, noting that your new position is most likely not an integer coordinate. Mark another point on the curve and move again, using the hue from the closest pixel (using something like floor
or round
, I won't be checking this precisely). Continue on like this until the curve goes out of bounds or it exceeds the maximum length. To finish, plot all the curve points as single black pixels (again, use nearest pixels) overlayed on the image, and output this new image.
The "hue angle" is just the hue:
hue = atan2(sqrt(3) * (G - B), 2 * R - G - B)
Note that for grayscale values that don't technically have a hue, this returns 0, but that's fine.
(This formula uses atan2
which most built-in math libraries have. R, G, B are from 0 to 1, not 0 to 255.)
- You can use any common lossless image file format as well as any image libraries.
- Take input from stdin or the command line, or write a function with arguments for the image file name, x and y, and the max length.
- The max length and x and y are always nonnegative integers. You can assume x and y are in range.
- Save the output image with a name of your choice or simply display it.
- Your implementation does not have to exactly match the snippet. A few pixels in slightly different places due to a slightly different rounding/calculation method is fine. (In chaotic cases this could lead to curves that end up majorly different, but as long as they look correct visually, it's fine.)
Scoring
The smallest submission in bytes wins.