UNPKG

nodegame-widgets

Version:

Collections of useful and reusable javascript / HTML snippets for nodeGame

1,011 lines (893 loc) 29.4 kB
/** * # ChernoffFaces * Copyright(c) 2017 Stefano Balietti * 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('ChernoffFaces', ChernoffFaces); // ## Meta-data ChernoffFaces.version = '0.6.2'; ChernoffFaces.description = 'Display parametric data in the form of a Chernoff Face.'; ChernoffFaces.title = 'ChernoffFaces'; ChernoffFaces.className = 'chernofffaces'; // ## Dependencies ChernoffFaces.dependencies = { JSUS: {}, Table: {}, Canvas: {}, SliderControls: {} }; ChernoffFaces.FaceVector = FaceVector; ChernoffFaces.FacePainter = FacePainter; ChernoffFaces.width = 100; ChernoffFaces.height = 100; ChernoffFaces.onChange = 'CF_CHANGE'; /** * ## ChernoffFaces constructor * * Creates a new instance of ChernoffFaces * * @see Canvas constructor */ function ChernoffFaces(options) { var that = this; // ## Public Properties // ### ChernoffFaces.options // Configuration options this.options = null; // ### ChernoffFaces.table // The table containing everything this.table = null; // ### ChernoffFaces.sc // The slider controls of the interface // Can be set manually via options.controls. // @see SliderControls this.sc = null; // ### ChernoffFaces.fp // The object generating the Chernoff faces // @see FacePainter this.fp = null; // ### ChernoffFaces.canvas // The HTMLElement canvas where the faces are created this.canvas = null; // ### ChernoffFaces.changes // History all the changes (if options.trackChanges is TRUE). // Each time the `draw` method is called, the input parameters // and a time measurement will be added here. this.changes = []; // ### ChernoffFaces.onChange // Name of the event to emit to update the canvas (falsy disabled) this.onChange = null; // ### ChernoffFaces.onChangeCb // Updates the canvas when the onChange event is emitted // // @param {object} f Optional. The list of features to change. // Can be a complete set or subset of all the features. If // not specified, it will try to get the features from the // the controls object, and if not found, a random vector // will be created. // @param {boolean} updateControls Optional. If TRUE, controls // are updated with the new values. Default, FALSE. // // @see ChernoffFaces.draw this.onChangeCb = function(f, updateControls) { if ('undefined' === typeof updateControls) updateControls = false; if (!f) { if (that.sc) f = that.sc.getValues(); else f = FaceVector.random(); } that.draw(f, updateControls); }; /** * ### ChoiceTable.timeFrom * * Name of the event to measure time from for each change * * Default event is a new step is loaded (user can interact with * the screen). Set it to FALSE, to have absolute time. * * @see node.timer.getTimeSince */ this.timeFrom = 'step'; // ### ChernoffFaces.features // The object containing all the features to draw Chernoff faces this.features = null; } /** * ### ChernoffFaces.init * * Inits the widget * * Stores the reference to options, most of the operations are done * by the `append` method. * * @param {object} options Configuration options. Accepted options: * * - canvas {object} containing all options for canvas * * - width {number} width of the canvas (read only if canvas is not set) * * - height {number} height of the canvas (read only if canvas is not set) * * - features {FaceVector} vector of face-features. Default: random * * - onChange {string|boolean} The name of the event that will trigger * redrawing the canvas, or null/false to disable event listener * * - controls {object|false} the controls (usually a set of sliders) * offering the user the ability to manipulate the canvas. If equal * to false no controls will be created. Default: SlidersControls. * Any custom implementation must provide the following methods: * * - getValues: returns the current features vector * - refresh: redraws the current feature vector * - init: accepts a configuration object containing a * features and onChange as specified above. * */ ChernoffFaces.prototype.init = function(options) { this.options = options; // Face Painter. if (options.features) { this.features = new FaceVector(options.features); } else if (!this.features) { this.features = FaceVector.random(); } // Draw features, if facepainter was already created. if (this.fp) this.fp.draw(this.features); // onChange event. if (options.onChange === false || options.onChange === null) { if (this.onChange) { node.off(this.onChange, this.onChangeCb); this.onChange = null; } } else { this.onChange = 'undefined' === typeof options.onChange ? ChernoffFaces.onChange : options.onChange; node.on(this.onChange, this.onChangeCb); } }; /** * ## ChernoffFaces.getCanvas * * Returns the reference to current wrapper Canvas object * * To get to the HTML Canvas element use `canvas.canvas`. * * @return {Canvas} Canvas object * * @see Canvas */ ChernoffFaces.prototype.getCanvas = function() { return this.canvas; }; /** * ## ChernoffFaces.buildHTML * * Builds HTML objects, but does not append them * * Creates the table, canvas, draw the current image, and * eventually adds the controls. * * If the table was already built, it returns immediately. */ ChernoffFaces.prototype.buildHTML = function() { var controlsOptions, f; var tblOptions, options; if (this.table) return; options = this.options; // Table. tblOptions = {}; if (this.id) tblOptions.id = this.id; if ('string' === typeof options.className) { tblOptions.className = options.className; } else if (options.className !== false) { tblOptions.className = 'cf_table'; } this.table = new Table(tblOptions); // Canvas. if (!this.canvas) this.buildCanvas(); // Controls. if ('undefined' === typeof options.controls || options.controls) { // Sc options. f = J.mergeOnKey(FaceVector.defaults, this.features, 'value'); controlsOptions = { id: 'cf_controls', features: f, onChange: this.onChange, submit: 'Send' }; // Create them. if ('object' === typeof options.controls) { this.sc = options.controls; } else { this.sc = node.widgets.get('SliderControls', controlsOptions); } } // Table. if (this.sc) { this.table.addRow( [{ content: this.sc, id: this.id + '_td_controls' },{ content: this.canvas, id: this.id + '_td_cf' }] ); } else { this.table.add({ content: this.canvas, id: this.id + '_td_cf' }); } // Create and append table. this.table.parse(); }; /** * ## ChernoffFaces.buildCanvas * * Builds the canvas object and face painter * * All the necessary to draw faces * * If the canvas was already built, it simply returns it. * * @return {canvas} */ ChernoffFaces.prototype.buildCanvas = function() { var options; if (!this.canvas) { options = this.options; if (!options.canvas) { options.canvas = {}; if ('undefined' !== typeof options.height) { options.canvas.height = options.height; } if ('undefined' !== typeof options.width) { options.canvas.width = options.width; } } this.canvas = W.get('canvas', options.canvas); this.canvas.id = 'ChernoffFaces_canvas'; // Face Painter. this.fp = new FacePainter(this.canvas); this.fp.draw(this.features); } }; /** * ## ChernoffFaces.append * * Appends the widget * * Creates table, canvas, face painter (fp) and controls (sc), according * to current options. * * @see ChernoffFaces.buildHTML * @see ChernoffFaces.fp * @see ChernoffFaces.sc * @see ChernoffFaces.table * @see Table * @see Canvas * @see SliderControls * @see FacePainter * @see FaceVector */ ChernoffFaces.prototype.append = function() { if (!this.table) this.buildHTML(); this.bodyDiv.appendChild(this.table.table); }; /** * ### ChernoffFaces.draw * * Draw a face on canvas and optionally updates the controls * * Stores the current value of the drawn image under `.features`. * * @param {object} features The features to draw (If not a complete * set of features, they will be merged with current values) * @param {boolean} updateControls Optional. If equal to false, * controls are not updated. Default: true * * @see ChernoffFaces.sc * @see ChernoffFaces.features */ ChernoffFaces.prototype.draw = function(features, updateControls) { var time; if ('object' !== typeof features) { throw new TypeError('ChernoffFaces.draw: features must be object.'); } if (this.options.trackChanges) { // Relative time. if ('string' === typeof this.timeFrom) { time = node.timer.getTimeSince(this.timeFrom); } // Absolute time. else { time = Date.now ? Date.now() : new Date().getTime(); } this.changes.push({ time: time, change: features }); } // Create a new FaceVector, if features is not one, mixing-in // new features and old ones. this.features = (features instanceof FaceVector) ? features : new FaceVector(features, this.features); this.fp.redraw(this.features); if (this.sc && (updateControls !== false)) { // Without merging wrong values are passed as attributes. this.sc.init({ features: J.mergeOnKey(FaceVector.defaults, features, 'value') }); this.sc.refresh(); } }; ChernoffFaces.prototype.getValues = function(options) { if (options && options.changes) { return { changes: this.changes, cf: this.features }; } else { return this.fp.face; } }; /** * ### ChernoffFaces.randomize * * Draws a random image and updates controls accordingly (if found) * * @see ChernoffFaces.sc */ ChernoffFaces.prototype.randomize = function() { var fv; fv = FaceVector.random(); this.fp.redraw(fv); // If controls are visible, updates them. if (this.sc) { this.sc.init({ features: J.mergeOnValue(FaceVector.defaults, fv), onChange: this.onChange }); this.sc.refresh(); } return true; }; /** * # FacePainter * * Draws faces on a Canvas * * @param {HTMLCanvas} canvas The canvas * @param {object} settings Optional. Settings (not used). */ function FacePainter(canvas, settings) { /** * ### FacePainter.canvas * * The wrapper element for the HTML canvas * * @see Canvas */ this.canvas = new W.Canvas(canvas); /** * ### FacePainter.scaleX * * Scales images along the X-axis of this proportion */ this.scaleX = canvas.width / ChernoffFaces.width; /** * ### FacePainter.scaleX * * Scales images along the X-axis of this proportion */ this.scaleY = canvas.height / ChernoffFaces.heigth; /** * ### FacePainter.face * * The last drawn face */ this.face = null; } // ## Methods /** * ### FacePainter.draw * * Draws a face into the canvas and stores it as reference * * @param {object} face Multidimensional vector of features * @param {number} x Optional. The x-coordinate to center the image. * Default: the center of the canvas * @param {number} y Optional. The y-coordinate to center the image. * Default: the center of the canvas * * @see Canvas * @see Canvas.centerX * @see Canvas.centerY */ 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; }; /** * FaceVector.defaults * * Numerical description of all the components of a standard 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' }, scaleX: { min: 0, max: 20, step: 0.01, value: 0.2, label: 'Scale X' }, scaleY: { min: 0, max: 20, step: 0.01, value: 0.2, label: 'Scale Y' }, color: { min: 0, max: 20, step: 0.01, value: 0.2, label: 'color' }, lineWidth: { min: 0, max: 20, step: 0.01, value: 0.2, label: 'lineWidth' } }; // Compute range for each feature. (function(defaults) { var key; for (key in defaults) { if (defaults.hasOwnProperty(key)) { defaults[key].range = defaults[key].max - defaults[key].min; } } })(FaceVector.defaults); // Constructs a random face vector. FaceVector.random = function() { console.log('*** FaceVector.random is deprecated. ' + 'Use new FaceVector() instead.'); return new FaceVector(); }; function FaceVector(faceVector, defaults) { var key; // Make random vector. if ('undefined' === typeof faceVector) { for (key in FaceVector.defaults) { if (FaceVector.defaults.hasOwnProperty(key)) { if (key === 'color') { this.color = 'red'; } else if (key === 'lineWidth') { this.lineWidth = 1; } else if (key === 'scaleX') { this.scaleX = 1; } else if (key === 'scaleY') { this.scaleY = 1; } else { this[key] = FaceVector.defaults[key].min + Math.random() * FaceVector.defaults[key].range; } } } } // Mixin values. else if ('object' === typeof faceVector) { this.scaleX = faceVector.scaleX || 1; this.scaleY = faceVector.scaleY || 1; this.color = faceVector.color || 'green'; this.lineWidth = faceVector.lineWidth || 1; defaults = defaults || FaceVector.defaults; // Merge on key. for (key in defaults) { if (defaults.hasOwnProperty(key)){ if (faceVector.hasOwnProperty(key)) { this[key] = faceVector[key]; } else { this[key] = defaults ? defaults[key] : FaceVector.defaults[key].value; } } } } else { throw new TypeError('FaceVector constructor: faceVector must be ' + 'object or undefined.'); } } // //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);