', { 'class': classes('retake-button-wrapper') });
var $button = createButton('button', {
'html': retakeText,
'class': classes('button', 'retake-button'),
'type': 'button'
});
addButtonListener($button, function () {
quiz.trigger('personality-quiz-restart');
});
$container.append($button);
quiz.$resultWrapper = $wrapper;
$result.append($wrapper);
$result.append($container);
return $result;
}
/**
Sets the background image of the personality slide the the image
associated with the personality.
@param {jQuery} $result The result slide to set the background on
@param {Object} personality
*/
function setPersonalityBackgroundImage($result, $personality, personality) {
var path = _getPath(personality.image.file.path);
var classNames = [
prefix('background'),
prefix('center-personality-wrapper'),
];
$result.css('background-image', 'url(' + path + ')');
$result.addClass(classNames.join(' '));
$personality.addClass(prefix('center-personality'));
}
/**
Create an element only if the passed expression evaluates to 'true'.
@param {boolean} expression
@param {string} element The tag for the element to be created.
@param {Object} attributes Attributes to set on the created element.
@return {jQuery}
*/
function createIf(expression, element, attributes) {
var $element = null;
if (expression) {
$element = $(element, attributes);
}
return $element;
}
/**
Sets the height of the inline personality image.
@param {jQuery} $wrapper
@param {jQuery} $personality
@param {jQuery} $image
*/
function setInlineImageHeight ($image, $personality, $resultWrapper) {
var height;
if (!$image) {
return;
}
$image.hide();
height = $resultWrapper.outerHeight() - $personality.outerHeight(true);
$image.css({'height': 'calc(' + height + 'px - 4 * 1em)'});
$image.show();
}
/**
Appends the personality information to the result slide.
@param {PeronalityQuiz} quiz
@param {Object} personality
@param {boolean} hasTitle
@param {boolean} hasImage
@param {boolean} hasDescription
*/
function appendPersonality(quiz, personality, hasTitle, hasImage, hasDescription) {
var $personality, $title, $description, $image;
$title = createIf(hasTitle, '
', { 'html': personality.name });
if (personality.image.file) {
$image = createIf(hasImage, '
', {
'class': classes('result-image'),
'src': _getPath(personality.image.file.path),
'alt': personality.image.alt
});
}
$description = createIf(hasDescription, '
', {
'html': personality.description
});
// NOTE (Emil): We only create $personality element if it has at least
// one child element.
if (hasTitle || hasImage || hasDescription) {
$personality = $('
', { 'class': classes('personality') });
$personality.append($title);
$personality.append($image);
$personality.append($description);
}
quiz.$resultWrapper.append($personality);
setInlineImageHeight($image, $personality, quiz.$resultWrapper);
return $personality;
}
/**
The click event listener if animations are enabled.
@param {jQuery} $button
@param {Object[]} personalities The list of personalities associated with the $button
*/
function animatedButtonListener($button, personalities) {
var animationClass = prefix('button-animate');
$button.addClass(animationClass);
$button.on('animationend', function () {
$(this).removeClass(animationClass);
$(this).off('animationend');
self.trigger('personality-quiz-answer', personalities);
});
}
/**
Click event handler for disabled animation option.
@param {jQuery}
@param {Object[]} personalities The personalities associated with the $button
*/
function nonAnimatedButtonListener($button, personalities) {
self.trigger('personality-quiz-answer', personalities);
}
/**
Resize the questions with image answers.
*/
function resizeColumns($quiz) {
var rowCount, columns;
var $answers, $rows;
columns = getNumColumns();
$answers = $quiz.find(prefix('image-answers', true));
$answers.each(function () {
var $answer, $slide, $alternatives;
$answer = $(this);
$rows = $answer.children(prefix('row', true));
$alternatives = $rows.children(prefix('column', true));
// NOTE (Emil): Remove the answer-images from the DOM so we can
// calculate the new column width.
$alternatives = $alternatives.detach();
rowCount = Math.ceil($alternatives.length / columns);
checkRows($answer, $rows, rowCount);
if (!$alternatives.hasClass(prefix('columns-' + columns))) {
$alternatives.toggleClass(classes('columns-2', 'columns-3'));
}
attachRows($answer, $alternatives, columns);
// NOTE (Emil): Update the selection after changes.
$rows = $answer.children(prefix('row', true));
$slide = $answer.parent().parent();
var titleHeight = $slide.children(prefix('question-text', true)).outerHeight(true) || 0;
var imageHeight = $slide.children(prefix('question-image', true)).outerHeight(true) || 0;
var height = $slide.height() - (titleHeight + imageHeight);
setAnswerImageHeight($rows, height / $rows.length);
});
}
/**
Resize the result screen.
*/
function resizeResult($quiz) {
var $wrapper, $personality, $image;
$wrapper = self.$resultWrapper;
$personality = $quiz.find(prefix('personality'));
$image = $quiz.find(prefix('result-image'));
setInlineImageHeight($image, $personality, self.$resultWrapper);
}
/**
Resize event handler.
@param {Object} event The resize event object.
*/
function resize() {
resizeColumns(self.$wrapper);
resizeResult(self.$wrapper);
}
/**
Calculate and set the height of the slides in the quiz.
@param {Object} self The quiz object
@param {jQuery} $quiz The container for the entire quiz
*/
function setQuizHeight(self, $quiz) {
var height = 0;
self.$slides.each(function (index, element) {
var $slide, $image;
var h = 0;
$slide = $(element);
$image = $slide.children(prefix('question-image', true));
$image.hide();
if (this.clientHeight > height)
{
h = $(this).height();
if ($image) { h = h * 1.3; }
height = h;
}
$image.show();
});
height = Math.max(height, minimumHeight);
$quiz.height(height);
self.height = height;
}
/**
Set the height of all images attached to answer alternatives.
@param {jQuery} $quiz The root of the quiz
*/
function setAnswerImageHeight($rows, maxRowHeight) {
var ratio = 9.0 / 16.0;
var numColumns = getNumColumns();
$rows.each(function () {
var imageHeight, heights, width;
var $row, $columns, $buttons, $images;
$row = $(this);
$columns = $row.children();
$buttons = $columns.children(prefix('image-answer-button', true));
$images = $columns.children(prefix('image-answer-image', true));
width = (self.$wrapper.width() / numColumns);
imageHeight = Math.floor(width * ratio);
heights = $buttons.map(function (i, e) {
var $e = $(e);
$e.css('height', ''); // NOTE (Emil): Unset previous height calculations.
return $e.height();
});
$buttons.height(Math.max.apply(null, heights));
$images.height(imageHeight);
// If the size of the containing box is larger than the limit set by
// maxRowHeight we subtract the difference from the height of the image.
if (maxRowHeight && $columns.outerHeight() > maxRowHeight) {
imageHeight -= ($columns.outerHeight() - maxRowHeight);
$images.height(imageHeight);
}
});
}
/**
Internal attach function. Creates the quiz, calculates the height
the quiz needs to be and starts canvas rendering if the
wheel of fortune animation is enabled.
@param {jQuery} $container
*/
function attach($container) {
loadingImages = [];
self.reset();
var $quiz = createQuiz(self, params);
$container.append($quiz);
// NOTE (Emil): We only want to do the work for a resize event once.
// Only the resize event call that survives 100 ms is called.
$(window).resize(function () {
clearTimeout(resizeEventHandler);
resizeEventHandler = setTimeout(resize, 100);
});
// NOTE (Emil): Wait for images to load, if there are any.
// If there aren't any images to wait for this function is called immediately.
$.when.apply(null, loadingImages).done(function () {
setAnswerImageHeight($quiz.find(prefix('row', true)));
setQuizHeight(self, $quiz);
if (animation && params.resultScreen.animation === 'wheel') {
canvas.width = $container.width() * 0.8;
canvas.height = $container.height();
self.wheel = new PersonalityQuiz.WheelAnimation(
self,
self.personalities,
canvas.width,
canvas.height,
_getPath
);
}
self.trigger('resize');
});
}
/**
Required function for interacting with H5P.
@param {jQuery} $container The parent element for the entire quiz.
*/
self.attach = function ($container) {
if (self.$container === undefined) {
self.$container = $container;
attach(self.$container);
}
};
/**
Sets the result of the personality quiz. Creates the missing
elements for the result screen and sets the result on the wheel
of fortune animation if it is enabled.
@param {Object} personality
*/
self.setResult = function (personality) {
var $personality;
var backgroundImage = (personality.image.file) && self.resultImagePosition === 'background';
var inlineImage = (personality.image.file) && self.resultImagePosition === 'inline';
if (self.$canvas) {
self.wheel.attach(self.$canvas[0]);
self.wheel.setTarget(personality);
self.wheel.animate();
}
$personality = appendPersonality(
self,
personality,
self.resultTitle,
inlineImage,
self.resultDescription
);
if (backgroundImage) {
setPersonalityBackgroundImage(self.$result, $personality, personality);
}
};
/**
Searches the personalities for the one with the highest 'count'
property. In the case of a tie the first element with the shared
highest 'count' is selected.
@return {Object} The result personality of the quiz
*/
self.calculatePersonality = function () {
var max = self.personalities[0].count;
var index = 0;
self.personalities.forEach(function (personality, i) {
if (max < personality.count) {
max = personality.count;
index = i;
}
});
return self.personalities[index];
};
/**
Updates the progressbar. Moves the background gradient based
on the number of questions answered and updates the text
with the current question number and the question total.
*/
self.updateProgress = function () {
var percentage = 100 - (self.answered) * self.slidePercentage;
var text = interpolate(self.progressText, {
'question': self.answered + 1,
'total': self.numQuestions
});
self.$progressbar.css('background-position', String(percentage) + '%');
self.$progressText.html(text);
};
/**
Moves to the next slide. Toggles visiblity of slides and
triggers 'personality-quiz-completed' event upon completion.
*/
self.next = function () {
var answeredAllQuestions = (self.answered === self.numQuestions);
var $prev = self.$slides.eq(self.index);
var $curr = self.$slides.eq(self.index + 1);
$prev.hide();
$curr.show();
self.index = self.index + 1;
if ($curr.hasClass(prefix('question'))) {
self.updateProgress(self.index);
}
if (!self.completed && answeredAllQuestions) {
self.trigger('personality-quiz-completed');
}
};
/**
The click event listener used for all buttons associated with an answer
to a question in the personality quiz.
@param {Object} event
*/
self.answerListener = function (event) {
var $target, $button;
var isImage, isButton, buttonListener, personalities;
$target = $(event.target);
$button = $target;
isImage = $target.hasClass(prefix('image-answer-image'));
isButton = $target.hasClass(prefix('image-answer-button'));
buttonListener = animatedButtonListener;
$button = isImage ? $target.siblings().eq(0) : $button;
$target = (isButton || isImage) ? $target.parent() : $target;
personalities = $target.attr('data-personality');
if (personalities) {
buttonListener = animation ? animatedButtonListener : nonAnimatedButtonListener;
buttonListener($button, personalities);
$target.parent(prefix('answers')).off('click');
}
};
/**
Zeros out all personality quiz state variables.
*/
self.reset = function () {
self.personalities.map(function (e) { e.count = 0; });
self.index = 0;
self.answered = 0;
self.completed = false;
};
/**
Event handler for the personality quiz start event. Makes the
progressbar visible and goes to the next slide.
*/
self.on('personality-quiz-start', function () {
self.$progressbar.show();
self.next();
});
/**
Event handler for the personality quiz answer event. Counts
up all personalities in the answer matching the given personalities.
*/
self.on('personality-quiz-answer', function (event) {
var answers;
if (event !== undefined && event.data !== undefined) {
answers = event.data.split(', ');
answers.forEach(function (answer) {
self.personalities.forEach(function (personality) {
if (personality.name === answer) {
personality.count++;
}
});
});
self.answered += 1;
}
self.next();
});
/**
Event handler for the personality quiz completed event. Hides
the progressbar, since it is no longer needed. Sets the quiz
as completed, calculates the personality and sets the result.
*/
self.on('personality-quiz-completed', function () {
var personality = self.calculatePersonality();
self.$progressbar.hide();
self.completed = true;
self.setResult(personality);
if (animation && self.resultAnimation === 'fade-in') {
self.$result.addClass(prefix('fade-in'));
}
});
/**
Event handler for the animation end event for the wheel of
fortune animation. Sets a fade-out animation and moves
the quiz on to the next slide.
*/
self.on('wheel-animation-end', function () {
setTimeout(function () {
self.$canvas.addClass(prefix('fade-out'));
}, 500);
self.$canvas.on('animationend', self.next);
});
/**
Event handler for the quiz restart event. Empties the root
container for the quiz and rebuilds it.
*/
self.on('personality-quiz-restart', function () {
self.$container.empty();
attach(self.$container);
});
}
PersonalityQuiz.prototype = Object.create(EventDispatcher);
PersonalityQuiz.prototype.constructor = PersonalityQuiz;
return PersonalityQuiz;
})(H5P.jQuery, H5P.EventDispatcher);
;
var H5P = H5P || {};
(function ($, PersonalityQuiz) {
/**
A wheel of fortune animation.
@param {Object} quiz
@param {Object} personalities
@param {number} width
@param {number} height
@param {PathFunction} _getPath
@constructor
*/
PersonalityQuiz.WheelAnimation = function (quiz, personalities, width, height, _getPath) {
var self = this;
// TODO (Emil): Some of these variables should probably be private.
// NOTE (Emil): Choose the smallest if the dimensions vary, for simplicity.
var side = Math.min(width, height);
var min = 320;
var max = 800;
var hasImages = true;
self.width = clamp(side, min, max);
self.height = clamp(side, min, max);
self.offscreen = document.createElement('canvas');
self.offscreen.width = Math.max(self.width - 25, 500);
self.offscreen.height = Math.max(self.height - 25, 500);
self.offscreen.context = self.offscreen.getContext('2d');
self.nubArrowSize = self.width * 0.1;
self.nubRadius = self.width * 0.06;
self.segmentAngle = (Math.PI * 2) / (personalities.length * 2);
self.targetRotation = 6 * (Math.PI * 2) + ((3 * Math.PI) / 2) - (self.segmentAngle / 2);
self.rotation = 0;
self.rotationSpeed = Math.PI / 32;
self.center = { x: self.width / 2, y: self.height / 2 };
self.personalities = personalities;
self.colors = {
even: 'rgb(77, 93, 170)',
odd: 'rgb(56, 183, 85)',
text: 'rgb(233, 239, 247)',
nub: 'rgb(233, 239, 247)',
overlay: 'rgba(60, 62, 64, 0.5)',
frame: 'rgb(60, 62, 64)'
};
self.images = [];
self.personalities.forEach(function (personality) {
hasImages = (hasImages && personality.image.file);
});
// NOTE (Emil): Prerender the wheel.
if (hasImages) {
load();
}
else {
// NOTE (Emil): If there aren't any images we use a slightly different colors.
self.colors = {
even: 'rgb(233, 239, 247)',
odd: 'rgb(203, 209, 217)',
text: 'rgb(60, 62, 64)',
nub: 'rgb(77, 93, 170)',
overlay: 'rgba(60, 62, 64, 0.4)',
frame: 'rgb(255, 255, 255)'
};
drawOffscreen(self.offscreen.context, self.personalities);
}
/**
Clamp a value between low and high
@param {number} value
@param {number} low
@param {number} high
@return {number}
*/
function clamp(value, low, high) {
return Math.min(Math.max(low, value), high);
}
/**
Zero out the current rotation and set the initial targetRotation.
*/
function reset() {
self.rotation = 0;
self.targetRotation = 6 * (Math.PI * 2) + ((3 * Math.PI) / 2) - (self.segmentAngle / 2);
}
/**
Returns the image pattern associated with the personality, if there is one,
or it returns the background color for the segment.
@param {Object} personality
@param {number} index
@return {Object|string}
*/
function getPattern(personality, index) {
if (personality.image.pattern) {
return personality.image.pattern;
}
if (index % 2 === 0) {
return self.colors.even;
}
else {
return self.colors.odd;
}
}
/**
Load personality images and when all images are loaded draw the wheel.
*/
function load() {
self.loadingImages = [];
self.personalities.forEach(function (personality) {
var image = new Image();
var deferred = $.Deferred();
image.addEventListener('load', function () {
personality.image.pattern = self.offscreen.context.createPattern(this, 'no-repeat');
deferred.resolve();
});
image.src = _getPath(personality.image.file.path);
self.images.push({ name: personality.name, data: image });
self.loadingImages.push(deferred);
});
// NOTE(Emil): When all the images are loaded we can prerender the offscreen buffer.
$.when.apply(null, self.loadingImages).done(function () {
drawOffscreen(self.offscreen.context, self.personalities);
});
}
/**
Draw a single segment of the wheel. There are two segments per personality.
@param {CanvasRenderingContext2D} context
@param {Object} center
@param {number} radius
@param {number} fromAngle
@param {number} toAngle
@param {string|CanvasGradient|CanvasPattern} fillStyle
*/
function drawSegment(context, center, radius, fromAngle, toAngle, fillStyle) {
context.beginPath();
context.fillStyle = fillStyle;
context.moveTo(center.x, center.y);
context.arc(center.x, center.y, radius, fromAngle, toAngle, false);
context.lineTo(center.x, center.y);
context.fill(); // Fill the segment with the background image.
context.stroke(); // Draw the outline of the segment.
context.closePath();
}
/**
Draw the personality name if there is not image associated with the personality.
@param {CanvasRenderingContext2D} context
@param {string} text
@param {number} x
@param {number} y
@param {number} maxWidth
*/
function drawText(context, text, x, y, maxWidth) {
context.fillStyle = self.colors.text;
context.font = '24px Arial';
context.fillText(text, x, y, maxWidth);
}
/**
Draw the prerendered wheel, which is stored in the offscreen canvas.
@param {CanvasRenderingContext2D} context
@param {number} rotation
@param {HTMLElement} canvas
*/
function drawWheel(context, rotation, canvas) {
var scale = 1;
context.save();
scale = {
x: self.width / self.offscreen.width,
y: self.width / self.offscreen.height
};
context.translate(self.center.x, self.center.y);
context.rotate(rotation);
context.scale(scale.x, scale.y);
context.translate(-self.offscreen.width / 2, -self.offscreen.height / 2);
context.drawImage(canvas, 0, 0);
context.restore();
}
/**
Draw the center arrow which will point to the result personality.
@param {CanvasRenderingContext2D} context
@param {Object} center
@param {number} radius
@param {string} color
*/
function drawNub(context, center, radius, color) {
context.fillStyle = color;
context.beginPath();
context.moveTo(self.center.x - radius, self.center.y);
context.lineTo(self.center.x, self.center.y - self.nubArrowSize);
context.lineTo(self.center.x + radius, self.center.y);
context.arc(self.center.x, self.center.y, radius, 0, Math.PI * 2, false);
context.fill();
context.closePath();
}
/**
Draw the wheel in the offscreen canvas and save it for later.
@param {CanvasRenderingContext2D} context
@param {Object[]} personalities - a list of personalities from H5P.PersonalityQuiz
*/
function drawOffscreen(context, personalities) {
var center = { x: self.offscreen.width / 2, y: self.offscreen.height / 2 };
var radius = self.offscreen.width / 2 - 2;
// NOTE (Emil): We draw two segments for each personality,
// this way the number of segments is always even.
var length = personalities.length * 2;
var angle = (Math.PI * 2) / length;
var halfAngle = angle / 2;
var i, personality, pattern, open, offset;
context.textBaseline = 'middle';
context.strokeStyle = self.colors.frame;
for (i = 0; i < length; i++) {
personality = personalities[i % personalities.length];
pattern = getPattern(personality, i);
open = i * angle;
offset = { x: 0, y: 0 };
if (personality.image.file) {
offset.x = personality.image.file.width / 2;
offset.y = (personality.image.file.height - radius) / 2;
}
// NOTE (Emil): Assumes that the center of the image is the most interesting.
context.save();
// NOTE (Emil): Center the circle segment over the center of the background image.
context.translate(center.x, center.y);
context.rotate(open + halfAngle - (Math.PI / 2));
context.translate(-offset.x, -offset.y);
drawSegment(
context,
{x: offset.x, y: offset.y},
radius,
(Math.PI / 2) - halfAngle,
(Math.PI / 2) + halfAngle,
pattern
);
context.restore();
// NOTE (Emil): Draw the personality name if there are no images.
if (self.images.length < self.personalities.length) {
context.save();
context.translate(center.x, center.y);
context.rotate(open + halfAngle);
context.translate(-center.x, -center.y);
drawText(
context,
personality.name,
center.x + (self.nubRadius * 2),
center.y,
radius - (self.nubRadius * 2.5)
);
context.restore();
}
}
}
/**
Attach the wheel animation to the provided canvas element.
@param {HTMLElement} canvasElement
*/
self.attach = function (canvasElement) {
self.onscreen = canvasElement;
self.onscreen.width = self.width;
self.onscreen.height = self.height;
self.onscreen.context = self.onscreen.getContext('2d');
};
/**
Set the target personality and calculates the target rotation.
@param {Object} targetPersonality
*/
self.setTarget = function (targetPersonality) {
var deviation, round;
reset();
deviation = self.segmentAngle * 0.4;
// NOTE (Emil): Randomly choose one of the two segments associated
// with the personality.
round = Math.floor(Math.random() + 0.5);
self.personalities.forEach(function (personality, index) {
if (targetPersonality.name === personality.name) {
var angle = index * self.segmentAngle + (round * Math.PI);
var min = angle + deviation;
var max = angle - deviation;
var deviated = Math.random() * (max - min) + min;
self.targetRotation = self.targetRotation - deviated;
return;
}
});
};
/**
Draw the wheel of fortune
@param {CanvasRenderingContext2D} context
@param {number} rotation
@param {HTMLElement} canvas
*/
self.draw = function (context, rotation, canvas) {
context.clearRect(0, 0, self.onscreen.width, self.onscreen.height);
drawWheel(context, rotation, canvas);
drawNub(context, self.center, self.nubRadius, self.colors.nub);
};
/**
The main animation loop. Starts the callback loop to requestAnimationFrame.
A call to 'setTarget' is required before calling this function.
*/
self.animate = function () {
var end, start;
self.rotation = 0;
function _animate (timestamp) {
var dt, scale, rotation;
end = end ? end : timestamp;
start = timestamp;
dt = (start - end) / 1000;
scale = 1 - (self.rotation / self.targetRotation);
rotation = Math.max(scale * dt * self.rotationSpeed, 0.01);
if (self.rotation < self.targetRotation) {
// NOTE (Emil): Always move atleast a little until the targetRotation is achieved.
self.rotation += Math.min(rotation, self.targetRotation - self.rotation);
self.draw(self.onscreen.context, self.rotation, self.offscreen);
window.requestAnimationFrame(_animate);
}
else {
quiz.trigger('wheel-animation-end');
}
}
window.requestAnimationFrame(_animate);
};
};
})(H5P.jQuery, H5P.PersonalityQuiz);
;