Last active
August 29, 2015 14:07
-
-
Save ldematte/3612241baa7c2a080dd2 to your computer and use it in GitHub Desktop.
Hats! :) With local caching in storage, click to select, drag to adjust it over your avatar and.. a couple of "face detection" ideas: HUE-based image partitioning and feature detection through edge filters (Sobel). HTML and CSS are non-existent, just the bare minimum to make it work, and the Controller is ... fake!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html xmlns="http://www.w3.org/1999/xhtml"> | |
<head> | |
<title></title> | |
<script src='Scripts/jquery-1.10.2.min.js'></script> | |
<script src='Scripts/jquery-ui.min-1.11.1.js'></script> | |
<script src='Scripts/hats.js'></script> | |
</head> | |
<body> | |
<div id="statusarea"> | |
</div> | |
<div id="hatpicker"> | |
</div> | |
<div id="useravatar"> | |
<div id="chosenhat-container" ></div> | |
<div style="position: relative; top: 0px;" > | |
<img id="theavatar" src="Content/avatar.png" /> | |
</div> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* hat picker html | |
<div id="hatpicker"> | |
</div> | |
*/ | |
/* user avatar | |
<div id="useravatar"> | |
</div> | |
*/ | |
var SE = (function() { | |
var that = {}; | |
that.parseCookies = function() { | |
var result = {}; | |
var tokens = document.cookie.split(";"); | |
for(var t in tokens) { | |
var pair = t.split("="); | |
result[pair[0]] = pair[1]; | |
} | |
return result; | |
}; | |
// Generate the DOM nodes to represent a hat. | |
// Currently, it's only an img tag, but we can assume it hold more info (description, points, etc) | |
// so we wrap it in a div | |
that.generateHat = function (hat, id) { | |
// Create one hat | |
var hatDiv = $('<div>'); | |
hatDiv.addClass("hat-container"); | |
var hatImg = $('<img>'); | |
hatImg.attr('src', hat.imageUrl); | |
hatImg.click(function() { | |
// Set this hat as "chosen".. temporarly! | |
// The idea is: we select it, we give immediate feedback (a grey contour or highlight) ... | |
hatImg.addClass('temp-selected'); | |
// Notify the backend of the newly selected hat... | |
$.ajax({ | |
type: "POST", | |
url: 'users/' + id + '/selecthat/', | |
data: hat.name | |
}).done( function() { | |
// Clear the highlight for the selected hat | |
$('.hat-container').removeClass('hat-selected'); | |
// Remove the "temporary selection" class .. | |
hatImg.removeClass('temp-selected'); | |
// .. and replace it with the definitive "chosen" accent (a grey contour or highlight) | |
hatImg.addClass('hat-selected'); | |
var hatInAvatar = $('#chosenhat-container'); | |
// Add a new node inside our avatar to show the hat | |
hatInAvatar.empty(); | |
hatInAvatar.append(hatDiv.clone()); | |
// Move the hat down "a little bit" (ideally: this would be relative to the avatar size!) | |
hatInAvatar.css({ top: '25px', position: 'relative', "z-index": 1 }); | |
// Make it draggable, so that we can drag it around and adjust it | |
hatInAvatar.draggable({ | |
stop: function (event, ui) { | |
// TODO: inform the backend of the new hat position | |
} | |
}); | |
}).fail(function (jqxhr, textStatus, error) { | |
// On failure, we remove the "temprarly selected" visual feedback too | |
hatImg.removeClass('temp-selected'); | |
// notify the user | |
$('#statusarea').text(textStatus); | |
setTimeout(function() { | |
$('#statusarea').text(''); | |
}, 5000); | |
}); | |
}); | |
hatImg.appendTo(hatDiv); | |
return hatDiv; | |
}; | |
that.generateHats = function(hats, id) { | |
var hatpicker = $('#hatpicker'); | |
for(var i = 0; i < hats.length; ++i) { | |
hatpicker.append(that.generateHat(hats[i], id)); | |
} | |
}; | |
// Helper function to create a new image data on which we can "draw" our pixels | |
that.createImageData = function (w, h) { | |
var tmpCanvas = document.createElement('canvas'); | |
var tmpCtx = tmpCanvas.getContext('2d'); | |
return tmpCtx.createImageData(w, h); | |
}; | |
// Perform a 2D convolution of an image with a kernel | |
// http://www.html5rocks.com/en/tutorials/canvas/imagefilters/ | |
that.convolute = function (pixels, weights, opaque) { | |
var side = Math.round(Math.sqrt(weights.length)); | |
var halfSide = Math.floor(side / 2); | |
var src = pixels.data; | |
var sw = pixels.width; | |
var sh = pixels.height; | |
// pad output by the convolution matrix | |
var w = sw; | |
var h = sh; | |
var output = that.createImageData(w, h); | |
var dst = output.data; | |
// go through the destination image pixels | |
var alphaFac = opaque ? 1 : 0; | |
for (var y = 0; y < h; y++) { | |
for (var x = 0; x < w; x++) { | |
var sy = y; | |
var sx = x; | |
var dstOff = (y * w + x) * 4; | |
// calculate the weighed sum of the source image pixels that | |
// fall under the convolution matrix | |
var r = 0, g = 0, b = 0, a = 0; | |
for (var cy = 0; cy < side; cy++) { | |
for (var cx = 0; cx < side; cx++) { | |
var scy = sy + cy - halfSide; | |
var scx = sx + cx - halfSide; | |
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { | |
var srcOff = (scy * sw + scx) * 4; | |
var wt = weights[cy * side + cx]; | |
r += src[srcOff] * wt; | |
g += src[srcOff + 1] * wt; | |
b += src[srcOff + 2] * wt; | |
a += src[srcOff + 3] * wt; | |
} | |
} | |
} | |
dst[dstOff] = r; | |
dst[dstOff + 1] = g; | |
dst[dstOff + 2] = b; | |
dst[dstOff + 3] = a + alphaFac * (255 - a); | |
} | |
} | |
return output; | |
}; | |
that.analyseAvatar = function() { | |
var img = $('#theavatar')[0]; | |
var canvas = $('<canvas/>')[0]; | |
canvas.width = img.width; | |
canvas.height = img.height; | |
var context = canvas.getContext('2d'); | |
var x = 0; | |
var y = 0; | |
context.drawImage(img, x, y); | |
var imageData = context.getImageData(x, y, img.width, img.height); | |
var data = imageData.data; | |
for (var i = 0; i < data.length; i += 4) { | |
var brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]; | |
// set red, green and blue to the same value (B) -> gray scale | |
data[i] = brightness; | |
data[i + 1] = brightness; | |
data[i + 2] = brightness; | |
// leave alpha as it is! | |
} | |
// Perfor edge detection using the Sobel filter | |
var vertical = that.convolute(imageData, [-1, 0, 1, -2, 0, 2, -1, 0, 1], 1); | |
var horizontal = that.convolute(imageData, [-1, -2, -1, 0, 0, 0, 1, 2, 1], 1); | |
// Horizontally, vertically (H and V edges), and then conbine them | |
for (var i = 0; i < imageData.data.length; i += 4) { | |
// make the vertical gradient red | |
var v = Math.abs(vertical.data[i]); | |
imageData.data[i] = v; | |
// make the horizontal gradient green | |
var h = Math.abs(horizontal.data[i]); | |
imageData.data[i + 1] = h; | |
// and mix in some blue for aesthetics | |
imageData.data[i + 2] = (v + h) / 4; | |
imageData.data[i + 3] = 255; // opaque alpha | |
} | |
// overwrite original image | |
context.putImageData(imageData, x, y); | |
img.parentElement.appendChild(canvas); | |
} | |
// private function to convert color space | |
var rgbToHsl = function(r, g, b){ | |
r /= 255, g /= 255, b /= 255; | |
var max = Math.max(r, g, b), min = Math.min(r, g, b); | |
var h, s, l = (max + min) / 2; | |
if(max == min){ | |
h = s = 0; // achromatic | |
}else{ | |
var d = max - min; | |
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | |
switch(max){ | |
case r: h = (g - b) / d + (g < b ? 6 : 0); break; | |
case g: h = (b - r) / d + 2; break; | |
case b: h = (r - g) / d + 4; break; | |
} | |
h /= 6; | |
} | |
return [h, s, l]; | |
} | |
// Analyse the avatar by producing a "on/off" image based on the | |
// principal hues found in the center of the image. | |
// The idea is: | |
// - take the hue(s) values that are most present in the center of the image | |
// - these are the "colors" of the face: filter all the others out | |
// - obtain a binary image: "face color - not face color". | |
// - center the face, and measure its extension, based on the boundaries of the "on" pixels. | |
that.analyseAvatar2 = function() { | |
var img = $('#theavatar')[0]; | |
var canvas = $('<canvas/>')[0]; | |
canvas.width = img.width; | |
canvas.height = img.height; | |
var context = canvas.getContext('2d'); | |
context.drawImage(img, 0, 0); | |
var imageData = context.getImageData(0, 0, img.width, img.height); | |
var data = imageData.data; | |
var hues = []; | |
var halfHeight = img.height / 2; | |
var halfWidth = img.width / 2; | |
// Build a H(sl) histogram... | |
for (var i = 0; i < data.length; i += 4) { | |
var x = i / img.width; | |
var y = i % img.height; | |
// ... but only with the central values! | |
if (x < halfWidth + 10 && x > halfWidth - 10 && | |
y < halfHeight + 10 && x > halfHeight - 10) { | |
var red = data[i]; | |
var green = data[i + 1]; | |
var blue = data[i + 2]; | |
var hsl = rgbToHsl(red, green, blue); | |
var hue = Math.floor(hsl[0] * 255); | |
if (hues[hue]) { | |
hues[hue] = hues[hue] + 1; | |
} | |
else { | |
hues[hue] = 1; | |
} | |
} | |
} | |
// Set the pixels to on/off (binary image) for displaying the "face". | |
for (var i = 0; i < data.length; i += 4) { | |
var red = data[i]; | |
var green = data[i + 1]; | |
var blue = data[i + 2]; | |
var hsl = rgbToHsl(red, green, blue); | |
var hue = Math.floor(hsl[0] * 255); | |
if (typeof (hues[hue]) === 'undefined' ||hue === 0 || hues[hue] < 5) { | |
data[i] = 0; | |
data[i + 1] = 0; | |
data[i + 2] = 0; | |
} | |
else { | |
data[i] = 255; | |
data[i + 1] = 255; | |
data[i + 2] = 255; | |
} | |
} | |
context.putImageData(imageData, 0, 0); | |
img.parentElement.appendChild(canvas); | |
} | |
return that; | |
})(); | |
/* | |
Assume that the backend serves "hats" as an encapsulated array of objects like: | |
{ d: [ | |
{ name : "", | |
imageUrl : "", | |
description: "" | |
} | |
] ]; | |
*/ | |
$(window).load(function () { | |
// document.cookie, returns all cookies in one string | |
var id = SE.parseCookies().UserId || 0; | |
SE.analyseAvatar2(); | |
/// hit the local storage | |
// render the hats I have there | |
var hats = []; | |
if (window.localStorage) { | |
hats = JSON.parse(localStorage.getItem('hats')) || []; | |
var lastHat = localStorage.getItem('lastHat'); | |
if (hats) { | |
SE.generateHats(hats, id); | |
} | |
} | |
var hasHats = lastHat && hats && hats.length | |
$.ajax({ | |
type: "GET", | |
url: 'users/' + id + '/hats' + (hasHats ? '?since=' + lastHat : '') | |
}).success(function(data) { | |
var newHats = data.d; | |
if (newHats) { | |
SE.generateHats(newHats, id); | |
if (window.localStorage) { | |
for (var j = 0; j < newHats.length; ++j) { | |
hats.push(newHats[j]); | |
} | |
localStorage.setItem('hats', JSON.stringify(hats)); | |
localStorage.setItem('lastHat', new Date().toGMTString()); | |
} | |
} | |
}).fail( function(jqxhr, textStatus, error) { | |
// notify the user | |
$('#statusarea').text(textStatus); | |
setTimeout(function() { | |
$('#statusarea').text(''); | |
}, 5000); | |
}); | |
}); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Web; | |
using System.Web.Mvc; | |
namespace SimpleHats.Controllers { | |
public class HomeController : Controller { | |
public ActionResult Index() { | |
return View(); | |
} | |
[HttpPost] | |
public JsonResult SelectHat(int id) { | |
return Json(Request.Form.Get(0)); | |
} | |
[HttpGet] | |
public JsonResult Hats(int id) { | |
var since = Request.QueryString["since"]; | |
if (since != null) { | |
// Already data there | |
return Json( | |
new { | |
d = new Object[] { } | |
}, JsonRequestBehavior.AllowGet); | |
}else { | |
return Json( | |
new { | |
d = new[] { | |
new { | |
name = "Hat1", | |
imageUrl = "Content/hat1.png", | |
description= "First Hat" | |
}, | |
new { | |
name = "Hat2", | |
imageUrl = "Content/hat2.png", | |
description= "Blue Hat" | |
}, | |
new { | |
name = "Hat3", | |
imageUrl = "Content/hat3.png", | |
description= "Third Hat" | |
} | |
} | |
}, JsonRequestBehavior.AllowGet); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment