Skip to content

Instantly share code, notes, and snippets.

@ldematte
Last active August 29, 2015 14:07
Show Gist options
  • Save ldematte/3612241baa7c2a080dd2 to your computer and use it in GitHub Desktop.
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!
<!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>
/* 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);
});
});
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