UNPKG

nodegame-widgets

Version:

Collections of useful and reusable javascript / HTML snippets for nodeGame

662 lines (555 loc) 18.5 kB
/** * # 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);