nodegame-widgets
Version:
Collections of useful and reusable javascript / HTML snippets for nodeGame
662 lines (555 loc) • 18.5 kB
JavaScript
/**
* # ChernoffFacesSimple
* Copyright(c) 2017 Stefano Balietti <ste@nodegame.org>
* MIT Licensed
*
* Displays multidimensional data in the shape of a Chernoff Face.
*
* www.nodegame.org
*/
(function(node) {
"use strict";
var Table = W.Table;
node.widgets.register('ChernoffFacesSimple', ChernoffFaces);
// ## Defaults
ChernoffFaces.defaults = {};
ChernoffFaces.defaults.id = 'ChernoffFaces';
ChernoffFaces.defaults.canvas = {};
ChernoffFaces.defaults.canvas.width = 100;
ChernoffFaces.defaults.canvas.heigth = 100;
// ## Meta-data
ChernoffFaces.version = '0.4';
ChernoffFaces.description =
'Display parametric data in the form of a Chernoff Face.';
// ## Dependencies
ChernoffFaces.dependencies = {
JSUS: {},
Table: {},
Canvas: {},
'Controls.Slider': {}
};
ChernoffFaces.FaceVector = FaceVector;
ChernoffFaces.FacePainter = FacePainter;
function ChernoffFaces (options) {
this.options = options;
this.id = options.id;
this.table = new Table({id: 'cf_table'});
this.root = options.root || document.createElement('div');
this.root.id = this.id;
this.sc = node.widgets.get('Controls.Slider'); // Slider Controls
this.fp = null; // Face Painter
this.canvas = null;
this.dims = null; // width and height of the canvas
this.change = 'CF_CHANGE';
var that = this;
this.changeFunc = function() {
that.draw(that.sc.getAllValues());
};
this.features = null;
this.controls = null;
}
ChernoffFaces.prototype.init = function(options) {
this.id = options.id || this.id;
var PREF = this.id + '_';
this.features = options.features || this.features ||
FaceVector.random();
this.controls = ('undefined' !== typeof options.controls) ?
options.controls : true;
var idCanvas = (options.idCanvas) ? options.idCanvas : PREF + 'canvas';
this.dims = {
width: options.width ?
options.width : ChernoffFaces.defaults.canvas.width,
height: options.height ?
options.height : ChernoffFaces.defaults.canvas.heigth
};
this.canvas = W.getCanvas(idCanvas, this.dims);
this.fp = new FacePainter(this.canvas);
this.fp.draw(new FaceVector(this.features));
var sc_options = {
id: 'cf_controls',
features:
J.mergeOnKey(FaceVector.defaults, this.features, 'value'),
change: this.change,
fieldset: {id: this.id + '_controls_fieldest',
legend: this.controls.legend || 'Controls'
},
submit: 'Send'
};
this.sc = node.widgets.get('Controls.Slider', sc_options);
// Controls are always there, but may not be visible
if (this.controls) {
this.table.add(this.sc);
}
// Dealing with the onchange event
if ('undefined' === typeof options.change) {
node.on(this.change, this.changeFunc);
} else {
if (options.change) {
node.on(options.change, this.changeFunc);
}
else {
node.removeListener(this.change, this.changeFunc);
}
this.change = options.change;
}
this.table.add(this.canvas);
this.table.parse();
this.root.appendChild(this.table.table);
};
ChernoffFaces.prototype.getRoot = function() {
return this.root;
};
ChernoffFaces.prototype.getCanvas = function() {
return this.canvas;
};
ChernoffFaces.prototype.append = function(root) {
root.appendChild(this.root);
this.table.parse();
return this.root;
};
ChernoffFaces.prototype.listeners = function() {};
ChernoffFaces.prototype.draw = function(features) {
if (!features) return;
var fv = new FaceVector(features);
this.fp.redraw(fv);
// Without merging wrong values are passed as attributes
this.sc.init({
features: J.mergeOnKey(FaceVector.defaults, features, 'value')
});
this.sc.refresh();
};
ChernoffFaces.prototype.getAllValues = function() {
//if (this.sc) return this.sc.getAllValues();
return this.fp.face;
};
ChernoffFaces.prototype.randomize = function() {
var fv = FaceVector.random();
this.fp.redraw(fv);
var sc_options = {
features: J.mergeOnKey(FaceVector.defaults, fv, 'value'),
change: this.change
};
this.sc.init(sc_options);
this.sc.refresh();
return true;
};
// FacePainter
// The class that actually draws the faces on the Canvas
function FacePainter(canvas, settings) {
this.canvas = new W.Canvas(canvas);
this.scaleX = canvas.width / ChernoffFaces.defaults.canvas.width;
this.scaleY = canvas.height / ChernoffFaces.defaults.canvas.heigth;
}
// Draws a Chernoff face.
FacePainter.prototype.draw = function(face, x, y) {
if (!face) return;
this.face = face;
this.fit2Canvas(face);
this.canvas.scale(face.scaleX, face.scaleY);
//console.log('Face Scale ' + face.scaleY + ' ' + face.scaleX );
x = x || this.canvas.centerX;
y = y || this.canvas.centerY;
this.drawHead(face, x, y);
this.drawEyes(face, x, y);
this.drawPupils(face, x, y);
this.drawEyebrow(face, x, y);
this.drawNose(face, x, y);
this.drawMouth(face, x, y);
};
FacePainter.prototype.redraw = function(face, x, y) {
this.canvas.clear();
this.draw(face,x,y);
};
FacePainter.prototype.scale = function(x, y) {
this.canvas.scale(this.scaleX, this.scaleY);
};
// TODO: Improve. It eats a bit of the margins
FacePainter.prototype.fit2Canvas = function(face) {
var ratio;
if (!this.canvas) {
console.log('No canvas found');
return;
}
if (this.canvas.width > this.canvas.height) {
ratio = this.canvas.width / face.head_radius * face.head_scale_x;
}
else {
ratio = this.canvas.height / face.head_radius * face.head_scale_y;
}
face.scaleX = ratio / 2;
face.scaleY = ratio / 2;
};
FacePainter.prototype.drawHead = function(face, x, y) {
var radius = face.head_radius;
this.canvas.drawOval({
x: x,
y: y,
radius: radius,
scale_x: face.head_scale_x,
scale_y: face.head_scale_y,
color: face.color,
lineWidth: face.lineWidth
});
};
FacePainter.prototype.drawEyes = function(face, x, y) {
var height = FacePainter.computeFaceOffset(face, face.eye_height, y);
var spacing = face.eye_spacing;
var radius = face.eye_radius;
//console.log(face);
this.canvas.drawOval({
x: x - spacing,
y: height,
radius: radius,
scale_x: face.eye_scale_x,
scale_y: face.eye_scale_y,
color: face.color,
lineWidth: face.lineWidth
});
//console.log(face);
this.canvas.drawOval({
x: x + spacing,
y: height,
radius: radius,
scale_x: face.eye_scale_x,
scale_y: face.eye_scale_y,
color: face.color,
lineWidth: face.lineWidth
});
};
FacePainter.prototype.drawPupils = function(face, x, y) {
var radius = face.pupil_radius;
var spacing = face.eye_spacing;
var height = FacePainter.computeFaceOffset(face, face.eye_height, y);
this.canvas.drawOval({
x: x - spacing,
y: height,
radius: radius,
scale_x: face.pupil_scale_x,
scale_y: face.pupil_scale_y,
color: face.color,
lineWidth: face.lineWidth
});
this.canvas.drawOval({
x: x + spacing,
y: height,
radius: radius,
scale_x: face.pupil_scale_x,
scale_y: face.pupil_scale_y,
color: face.color,
lineWidth: face.lineWidth
});
};
FacePainter.prototype.drawEyebrow = function(face, x, y) {
var height = FacePainter.computeEyebrowOffset(face,y);
var spacing = face.eyebrow_spacing;
var length = face.eyebrow_length;
var angle = face.eyebrow_angle;
this.canvas.drawLine({
x: x - spacing,
y: height,
length: length,
angle: angle,
color: face.color,
lineWidth: face.lineWidth
});
this.canvas.drawLine({
x: x + spacing,
y: height,
length: 0-length,
angle: -angle,
color: face.color,
lineWidth: face.lineWidth
});
};
FacePainter.prototype.drawNose = function(face, x, y) {
var height = FacePainter.computeFaceOffset(face, face.nose_height, y);
var nastril_r_x = x + face.nose_width / 2;
var nastril_r_y = height + face.nose_length;
var nastril_l_x = nastril_r_x - face.nose_width;
var nastril_l_y = nastril_r_y;
this.canvas.ctx.lineWidth = face.lineWidth;
this.canvas.ctx.strokeStyle = face.color;
this.canvas.ctx.save();
this.canvas.ctx.beginPath();
this.canvas.ctx.moveTo(x,height);
this.canvas.ctx.lineTo(nastril_r_x,nastril_r_y);
this.canvas.ctx.lineTo(nastril_l_x,nastril_l_y);
//this.canvas.ctx.closePath();
this.canvas.ctx.stroke();
this.canvas.ctx.restore();
};
FacePainter.prototype.drawMouth = function(face, x, y) {
var height = FacePainter.computeFaceOffset(face, face.mouth_height, y);
var startX = x - face.mouth_width / 2;
var endX = x + face.mouth_width / 2;
var top_y = height - face.mouth_top_y;
var bottom_y = height + face.mouth_bottom_y;
// Upper Lip
this.canvas.ctx.moveTo(startX,height);
this.canvas.ctx.quadraticCurveTo(x, top_y, endX, height);
this.canvas.ctx.stroke();
//Lower Lip
this.canvas.ctx.moveTo(startX,height);
this.canvas.ctx.quadraticCurveTo(x, bottom_y, endX, height);
this.canvas.ctx.stroke();
};
//TODO Scaling ?
FacePainter.computeFaceOffset = function(face, offset, y) {
y = y || 0;
//var pos = y - face.head_radius * face.scaleY +
// face.head_radius * face.scaleY * 2 * offset;
var pos = y - face.head_radius + face.head_radius * 2 * offset;
//console.log('POS: ' + pos);
return pos;
};
FacePainter.computeEyebrowOffset = function(face, y) {
y = y || 0;
var eyemindistance = 2;
return FacePainter.computeFaceOffset(face, face.eye_height, y) -
eyemindistance - face.eyebrow_eyedistance;
};
/*!
*
* A description of a Chernoff Face.
*
* This class packages the 11-dimensional vector of numbers from 0 through
* 1 that completely describe a Chernoff face.
*
*/
FaceVector.defaults = {
// Head
head_radius: {
// id can be specified otherwise is taken head_radius
min: 10,
max: 100,
step: 0.01,
value: 30,
label: 'Face radius'
},
head_scale_x: {
min: 0.2,
max: 2,
step: 0.01,
value: 0.5,
label: 'Scale head horizontally'
},
head_scale_y: {
min: 0.2,
max: 2,
step: 0.01,
value: 1,
label: 'Scale head vertically'
},
// Eye
eye_height: {
min: 0.1,
max: 0.9,
step: 0.01,
value: 0.4,
label: 'Eye height'
},
eye_radius: {
min: 2,
max: 30,
step: 0.01,
value: 5,
label: 'Eye radius'
},
eye_spacing: {
min: 0,
max: 50,
step: 0.01,
value: 10,
label: 'Eye spacing'
},
eye_scale_x: {
min: 0.2,
max: 2,
step: 0.01,
value: 1,
label: 'Scale eyes horizontally'
},
eye_scale_y: {
min: 0.2,
max: 2,
step: 0.01,
value: 1,
label: 'Scale eyes vertically'
},
// Pupil
pupil_radius: {
min: 1,
max: 9,
step: 0.01,
value: 1, //this.eye_radius;
label: 'Pupil radius'
},
pupil_scale_x: {
min: 0.2,
max: 2,
step: 0.01,
value: 1,
label: 'Scale pupils horizontally'
},
pupil_scale_y: {
min: 0.2,
max: 2,
step: 0.01,
value: 1,
label: 'Scale pupils vertically'
},
// Eyebrow
eyebrow_length: {
min: 1,
max: 30,
step: 0.01,
value: 10,
label: 'Eyebrow length'
},
eyebrow_eyedistance: {
min: 0.3,
max: 10,
step: 0.01,
value: 3, // From the top of the eye
label: 'Eyebrow from eye'
},
eyebrow_angle: {
min: -2,
max: 2,
step: 0.01,
value: -0.5,
label: 'Eyebrow angle'
},
eyebrow_spacing: {
min: 0,
max: 20,
step: 0.01,
value: 5,
label: 'Eyebrow spacing'
},
// Nose
nose_height: {
min: 0.4,
max: 1,
step: 0.01,
value: 0.4,
label: 'Nose height'
},
nose_length: {
min: 0.2,
max: 30,
step: 0.01,
value: 15,
label: 'Nose length'
},
nose_width: {
min: 0,
max: 30,
step: 0.01,
value: 10,
label: 'Nose width'
},
// Mouth
mouth_height: {
min: 0.2,
max: 2,
step: 0.01,
value: 0.75,
label: 'Mouth height'
},
mouth_width: {
min: 2,
max: 100,
step: 0.01,
value: 20,
label: 'Mouth width'
},
mouth_top_y: {
min: -10,
max: 30,
step: 0.01,
value: -2,
label: 'Upper lip'
},
mouth_bottom_y: {
min: -10,
max: 30,
step: 0.01,
value: 20,
label: 'Lower lip'
}
};
//Constructs a random face vector.
FaceVector.random = function() {
var out = {};
for (var key in FaceVector.defaults) {
if (FaceVector.defaults.hasOwnProperty(key)) {
if (!J.inArray(key,
['color', 'lineWidth', 'scaleX', 'scaleY'])) {
out[key] = FaceVector.defaults[key].min +
Math.random() * FaceVector.defaults[key].max;
}
}
}
out.scaleX = 1;
out.scaleY = 1;
out.color = 'green';
out.lineWidth = 1;
return new FaceVector(out);
};
function FaceVector(faceVector) {
faceVector = faceVector || {};
this.scaleX = faceVector.scaleX || 1;
this.scaleY = faceVector.scaleY || 1;
this.color = faceVector.color || 'green';
this.lineWidth = faceVector.lineWidth || 1;
// Merge on key
for (var key in FaceVector.defaults) {
if (FaceVector.defaults.hasOwnProperty(key)){
if (faceVector.hasOwnProperty(key)){
this[key] = faceVector[key];
}
else {
this[key] = FaceVector.defaults[key].value;
}
}
}
}
//Constructs a random face vector.
FaceVector.prototype.shuffle = function() {
for (var key in this) {
if (this.hasOwnProperty(key)) {
if (FaceVector.defaults.hasOwnProperty(key)) {
if (key !== 'color') {
this[key] = FaceVector.defaults[key].min +
Math.random() * FaceVector.defaults[key].max;
}
}
}
}
};
//Computes the Euclidean distance between two FaceVectors.
FaceVector.prototype.distance = function(face) {
return FaceVector.distance(this,face);
};
FaceVector.distance = function(face1, face2) {
var sum = 0.0;
var diff;
for (var key in face1) {
if (face1.hasOwnProperty(key)) {
diff = face1[key] - face2[key];
sum = sum + diff * diff;
}
}
return Math.sqrt(sum);
};
FaceVector.prototype.toString = function() {
var out = 'Face: ';
for (var key in this) {
if (this.hasOwnProperty(key)) {
out += key + ' ' + this[key];
}
}
return out;
};
})(node);