A little memory game to help learn big ideas.
xxxxxxxxxx
<meta charset="utf-8">
<title>Memorizinator</title>
<link rel="stylesheet" href="skinny_skel.css" type="text/css" media="screen" />
<style>
#container {
position: relative;
display: inline-block;
}
#gameboard {
border: 1px solid #840000;
position: relative;
width: 100%;
height: 100%;
}
.gnode > circle:hover {
stroke: #e0e0e2;
stroke-width: 2px;
}
.text { /* centers text in node */
text-anchor: middle;
alignment-baseline: central;
pointer-events: none;
transform: translateY(1%); /* Firefox */
}
.next { /* for boldIt hint */
font-weight: bold;
}
.link {
stroke: lightgray;
stroke-width: 1px;
}
#selectQuote {
position: absolute;
top: 3px;
right: 2px;
}
#message {
position: absolute;
bottom: 2px;
left: 3px;
}
#restart {
bottom: 2px;
right: 4px;
}
#settings {
bottom: 2px;
right: 28px;
}
#setsPop {
left: 15%;
top: 20%;
width: 66%;
height: 40%;
}
#timerBar {
fill: #840000;
opacity: 0.7;
}
#timerBarBox {
fill: #e0e0e2;
}
</style>
<body>
<div id="container">
<svg id="gameboard">
<rect width="0" height="10" x="3" y="3" id="timerBarBox"></rect>
<rect width="0" height="10" x="3" y="3" id="timerBar"></rect>
</svg>
<select id="selectQuote"></select>
<div id="message"><b>Memorizinator</b> (A little game to help you learn big ideas.)</div>
<svg id="settings" class="iconButton" viewBox="0 0 20 20" xml:space="preserve"><path d="M5,1.6C5,1.047,4.552,1,4,1C3.447,1,3,1.047,3,1.6V10h2V1.6z M3,18.4C3,18.951,3.447,19,4,19c0.552,0,1-0.049,1-0.6V15H3 V18.4z M6.399,11H1.599C1.046,11,1,11.448,1,12v1c0,0.553,0.046,1,0.599,1h4.801C6.95,14,7,13.553,7,13v-1 C7,11.448,6.95,11,6.399,11z M18.399,12h-4.801C13.046,12,13,12.448,13,13v1c0,0.553,0.046,1,0.599,1h4.801 C18.95,15,19,14.553,19,14v-1C19,12.448,18.95,12,18.399,12z M13,7c0-0.552-0.05-1-0.601-1H7.599C7.046,6,7,6.448,7,7v1 c0,0.553,0.046,1,0.599,1h4.801C12.95,9,13,8.553,13,8V7z M11,1.6C11,1.047,10.552,1,10,1C9.447,1,9,1.047,9,1.6V5h2V1.6z M9,18.4 c0,0.551,0.447,0.6,1,0.6c0.552,0,1-0.049,1-0.6V10H9V18.4z M17,1.6C17,1.047,16.552,1,16,1c-0.553,0-1,0.047-1,0.6V11h2V1.6z M15,18.4c0,0.551,0.447,0.6,1,0.6c0.552,0,1-0.049,1-0.6V16h-2V18.4z"><title>Show Settings</title></path></svg>
<svg id="restart" class="iconButton" viewBox="0 0 20 20" xml:space="preserve"><path d="M5.516,14.224c-2.262-2.432-2.222-6.244,0.128-8.611c0.962-0.969,2.164-1.547,3.414-1.736L8.989,1.8 C7.234,2.013,5.537,2.796,4.192,4.151c-3.149,3.17-3.187,8.289-0.123,11.531l-1.741,1.752l5.51,0.301l-0.015-5.834L5.516,14.224z M12.163,2.265l0.015,5.834l2.307-2.322c2.262,2.434,2.222,6.246-0.128,8.611c-0.961,0.969-2.164,1.547-3.414,1.736l0.069,2.076 c1.755-0.213,3.452-0.996,4.798-2.35c3.148-3.172,3.186-8.291,0.122-11.531l1.741-1.754L12.163,2.265z"><title>Restart game</title></path></svg>
<div id="setsPop" class="popup" tabindex="-1">
<div class="row">
<div class="eleven columns"><h5>Settings</h5></div>
<div class="one columns"><svg class="close iconButton" viewBox="0 0 20 20" xml:space="preserve"><path d="M14.348,14.849c-0.469,0.469-1.229,0.469-1.697,0L10,11.819l-2.651,3.029c-0.469,0.469-1.229,0.469-1.697,0 c-0.469-0.469-0.469-1.229,0-1.697l2.758-3.15L5.651,6.849c-0.469-0.469-0.469-1.228,0-1.697c0.469-0.469,1.228-0.469,1.697,0 L10,8.183l2.651-3.031c0.469-0.469,1.228-0.469,1.697,0c0.469,0.469,0.469,1.229,0,1.697l-2.758,3.152l2.758,3.15 C14.817,13.62,14.817,14.38,14.348,14.849z"><title>Close popup</title></path></svg></div>
</div>
<hr />
<div class="row">
<div class="button three columns" id="sayIt" title="Reads the quote to you. Click again to silence.">Say It</div>
<div class="button three columns" id="showIt" title="Show the quote. Click again to hide.">Show It</div>
<div class="button three columns" id="boldIt" title="Bold the next word you need to click. Click again to turn off.">Bold It</div>
<div class="button three columns" id="punctuateIt" title="Turn on punctuation and capitalization. Click again to turn off.">Punctuate It</div>
</div>
<div class="row">
<div id="setsMessage" class="twelve columns"></div>
</div>
</div>
</div>
</body>
<script src="//d3js.org/d3.v4.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="colors.js"></script>
<script>
$(function () {
// ********** EVENTS **********
$("#selectQuote").change(function () {
citation = $("#selectQuote").val();
gameManager('restart');
});
$("#settings").click(function () { $('#setsPop').show(); $('#setsPop').focus(); }); // focus allows close
$("#restart").click(function () { gameManager('restart'); });
$("#sayIt").click(function () { gameManager('sayIt'); });
$("#showIt").click(function () { gameManager('showIt'); });
$("#boldIt").click(function () { gameManager('boldIt'); });
$("#punctuateIt").click(function () { gameManager('punctuateIt'); });
$(".close").click(function () { $('.popup:visible').hide(); });
$(".popup").on('blur', function () { $('.popup:visible').hide(); }); // close when clicking outside popup
// ********** D3 HAPPENS HERE **********
// General variablees
var wordData = [], // All words (data) from text from json
wordLinks = [], // Links (data) in the simulation
svgWidth = 700, // Width of the svg palette
svgHeight = 400, // Height of the svg palette
initCount = 7, // Number of words to show at beginning of game
foci = [{ x: svgWidth * 0.25, y: svgHeight * 0.45 }, { x: svgWidth * 0.75, y: svgHeight * 0.45 }], // Sets 2 foci on page
citation = "Williams Shedd",
quote, // The quote to be memorized
boldIt = false, // Is the boltIt hint be active?
punctuateIt = false; // Is the punctuateIt on?
// Get svg handle, set up color and radius scale (use word length to set size)
$("#container").css("width", svgWidth).css("height", svgHeight);
var svg = d3.select("#gameboard");
var radScale = d3.scaleLinear().domain([1, 15]).range([10, 50]);
// Set forces in the simulation
var simulation = d3.forceSimulation()
//.force("link", d3.forceLink().id(function (d) { return d.id; }))
.force("link", d3.forceLink().distance(20).strength(0.6))
.force("charge", d3.forceManyBody().strength(-50))
.force("collide", d3.forceCollide(function (d) { return d.rad; }));
// Get quote from json, then start simulation
function build() {
d3.json("quotes.json", function (error, quotesData) {
// Populate dropdown if it's empty
if ($('#selectQuote').children('option').length == 0) {
$.each(quotesData.nodes, function (key, value) {
option = "<option value='" + value.cit + "' " + (value.cit == citation ? "selected" : "") + ">" + value.cit + "</option>";
$('#selectQuote').append(option);
});
}
// Get the selected quote (filter returns an array, length = 1); then split.
quote = quotesData.nodes.filter(function (obj) { return (obj.cit == citation ? true : false); })[0].text;
quote = punctuateIt ? quote : quote.toLowerCase().replace(/([^a-z ])/g, '');
var quoteSplit = quote.replace("-", "- ").split(" ");
var colors = d3.scaleOrdinal().domain(0, 4).range(colorPalette(quote.length)); // function in colors.js
// Format data
for (i = 0; i < quoteSplit.length; i++) {
wordData.push({
word: quoteSplit[i], // the actual word
id: i, // the original index
x: 0,
y: ~~(Math.random() * svgHeight),
rad: radScale(quoteSplit[i].length), // the radius
clickOrder: 0, // what order was it clicked on by user?
focus: (i < initCount ? 0 : -1), // is is part of the answer set; controls focii
color: colors(i)
})
}
gameManager("start");
start();
});
}
build();
// Build the force simulation
function start() {
// Create links
var glinks = svg.selectAll(".link")
.data(wordLinks, function (d) { return d.source.id + "-" + d.target.id; })
.enter().insert("line", ".gnode").attr("class", "link");
var glinks = svg.selectAll(".link")
.data(wordLinks, function (d) { return d.source.id + "-" + d.target.id; })
.exit().remove()
// Create g-elements (for nodes) based on a subset of wordData
var gnodes = svg.selectAll("g")
.data(words("showing", -1), function id(d, i) { return d.id; })
.enter().append("g").attr("class", function (d) { return "gnode g" + d.id; })
.classed("next", function (d) { return (boldIt && d.id == 0) ? true : false ; });
var circles = gnodes.append("circle").attr("class", function (d) { return "c" + d.id; })
.attr("r", function (d) { return d.rad; }).style("fill", function (d, i) { return d.color; })
.attr("opacity", 0.7);
var texts = gnodes.append("text")
.attr("class", function (d) { return "text w" + d.id; }) // Tie into CSS
.text(function (d) { return (d.word); });
var actions = gnodes.on("click", nodeClicked);
// Link data to simulation and set it in motion.
simulation.nodes(words("showing", -1)).force("link").links(wordLinks);
simulation.on("tick", ticked).alpha(0.5).restart();
}
// Manage node & link movement
function ticked(e) {
var k = .2 * simulation.alpha();
svg.selectAll(".gnode").attr("transform", function (d) {
// Set node location, multi-foci
d.y += (foci[d.focus].y - d.y) * k;
d.x += (foci[d.focus].x - d.x) * k;
// But be sure that nodes don't go out-of-bounds
d.y = Math.max(d.rad, Math.min(svgHeight - d.rad, d.y));
d.x = Math.max(d.rad, Math.min(svgWidth - d.rad, d.x));
return 'translate(' + [d.x, d.y] + ')';
});
// Set link locations
svg.selectAll(".link")
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
}
// User clicked a node
function nodeClicked(d) {
if (d.focus == 0) { // A node from the unused side
d.focus = 1; // Move gnode to other focii.
var answerCount = words("focus", 1).length; // answerCount includes just-clicked node; equals clickOrder
var showCount = words("showing", -1).length; //
d.clickOrder = answerCount; // Set clickOrder
if (wordData.length > showCount && words("focus", 0).length < initCount) { wordData[showCount].focus = 0; } // Show another node
if (answerCount > 1) { wordLinks.push({ "source": words("clickOrder", d.clickOrder - 1)[0].id, "target": d.id }); } // Add link
start();
svg.selectAll(".text").classed("next", function (d) { return (boldIt && d.index == answerCount) ? true : false; }); // Bold next word
// Check accuracy
var accurate = true;
for (i = 0; i < answerCount; i++) { // cycle thru wordData by clickOrder (base 1); compare with wordData (correct answers)
if (words("clickOrder", i + 1)[0].word != wordData[i].word) {
accurate = false;
$('.c' + words("clickOrder", i + 1)[0].id).css("fill", "red");
}
}
if (accurate && timerLength >= 0 && wordData.length == answerCount) { gameManager("win"); }
} else { // d.focus == 1 (a node from the focus)
d.focus = 0;
$('.c' + d.id).css("fill", d.color); // reset color
d.clickOrder = 0;
removeLinks(d.id)
start();
}
}
// Return portion of wordData needed.
function words(key, val) {
switch (key) {
case "showing": // returns an array of the shown [focus > -1]
return wordData.filter(function (value, index) { return value.focus > -1 ? true : false; });
break;
case "focus": // returns an array of the focus nodes [val = 1] or un-focus [val = 0]
return wordData.filter(function (value, index) { return value.focus == val ? true : false; });
break;
case "clickOrder": // returns an array with the single requested clickOrder item
return wordData.filter(function (value, index) { return value.clickOrder == val ? true : false; });
break;
}
}
// Remove links that attach to specific node id's
function removeLinks(id) {
for (var i = wordLinks.length - 1; i >= 0; i--) { // reverse order since splicing changes indexes
if (id == wordLinks[i].source.id || id == wordLinks[i].target.id) {
wordLinks.splice(i, 1);
}
}
}
// ********** GAME MECHANICS / MANAGEMENT **********
var timer, // Allows timer to be managed from multiple places
timerLength = 20; // Allows clean management of timer length
function gameManager(event) {
switch (event) {
case "timerBar":
$("#timerBar").attr("width", timerLength > 0 ? timerLength * 2 : 0).attr("opacity", (timerLength > 10 ? 0.5 : 1));
break;
case "start":
timerLength = ~~(wordData.length * 1.5);
clearInterval(timer);
timer = setInterval(gameTimer, 1000);
$("#timerBar").attr("width", timerLength * 2);
$("#timerBarBox").attr("width", timerLength * 2);
$('#message').html("<b>Memorizinator</b>. Click the words in the correct order.");
break;
case "win":
$('#message').html("<b>You won!</b> Score = " + timerLength);
$("#timerBar").attr("width", 0);
clearInterval(timer);
break;
case "loss":
$('#message').text("I'm sorry for your loss.");
clearInterval(timer);
break;
case "restart":
wordData = []; // All words (data) from quote from json
wordLinks = []; // Links (data) in the simulation
svg.selectAll("g, .link").remove();
build();
break;
case "sayIt":
$('#setsMessage').text("'Say It' only works in Chrome/Safari. Click again to silence.");
if (window.speechSynthesis.speaking) {
window.speechSynthesis.cancel();
} else {
window.speechSynthesis.speak(new SpeechSynthesisUtterance(quote));
}
break;
case "boldIt":
boldIt = !boldIt;
$('#setsMessage').text("Bolding: " + (boldIt ? "On" : "Off"));
gameManager("restart");
break;
case "punctuateIt":
punctuateIt = !punctuateIt;
$('#setsMessage').text("Punctuation: " + (punctuateIt ? "On" : "Off"));
gameManager("restart");
break;
case "showIt":
$('#setsMessage').text("Click again to hide.");
$('#message').text($('#message').text() == quote ? "" : quote);
break; // t.replace(/(\B[a-z])/g, "-")
}
}
function gameTimer() {
if (--timerLength >= 0) {
gameManager("timerBar");
} else {
gameManager("loss")
clearInterval(timer);
}
}
});
</script>
https://d3js.org/d3.v4.js
https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js