light.css: tools/stripcss base.css
$(call cmdsave,$^)
-word: word/put.js data/wordlist.en.json data/wordlist.nl.json data/wordlist.ru.json
+word: word/put.js data/wordlist.en.json data/wordlist.nl.json data/wordlist.ru.json data/wordpairs.json
word/put.js: $(download)
tools/wget-ifmodified https://github.com/kriszyp/put-selector/raw/master/put.js $@
$(call cmdsave,$<)
data/wordlist.%.inc.pl: tools/mkwordlist data/wordlist.version.txt
$(call cmdsave,$< $*)
-data/wordlist.%.json: data/wordlist.%.inc.pl
+data/word%.json: data/word%.inc.pl
$(call cmdsave,perl -MJSON=encode_json -E "print encode_json(do \$$ARGV[0])" ./$<)
+data/wordpairs.inc.pl: data/wordlist.version.txt
+ @perl -I. -MShiar_Sheet::DB -MData::Dump=pp -E 'say pp(Shiar_Sheet::DB->connect->select("word w JOIN word a ON w.id=a.ref" => "w.id, a.id", {"a.lang"=>undef})->map or exit 1)' >$@
+
.SECONDARY: data/font/%.ttf
data/font/%.ttf:
find /usr/share/fonts/truetype/ ~/.fonts/ -iname "$(@F)" | head -1 | xargs -i ln -sf {} $@
--- /dev/null
+class WordMemory {
+ turn(click) {
+ let target = click.currentTarget;
+ if (!target.classList.contains('turn')) {
+ // show an open card
+ this.turned.push(target);
+ put(target, '.turn');
+ }
+ else if (this.turned.length < 2) {
+ return; // keep open
+ }
+
+ if (this.turned.length <= 1) {
+ return; // first choice
+ }
+
+ // compare two cards
+ let match = this.pairs[this.turned[0].id] == this.turned[1].id
+ || this.pairs[this.turned[1].id] == this.turned[0].id;
+ if (!match && !this.turned[0].classList.contains('bad')) {
+ put(this.turned[0], '.bad'); // indicate failure on first card
+ return;
+ }
+
+ if (match) {
+ // lock both as correct
+ this.turned.forEach(card => put(card, '.good![onclick]'));
+ this.turned = [];
+ return;
+ }
+
+ // fold back earlier cards
+ this.turned.splice(0, 2)
+ .forEach(card => put(card, '!.turn!.bad'));
+ }
+
+ constructor() {
+ this.dataurl = '/data/wordpairs.json';
+ fetch(this.dataurl).then(res => res.json()).then(pairs => {
+ this.turned = [];
+ this.pairs = pairs;
+ this.form = document.getElementById('quiz');
+ this.cards = Object.entries(pairs).flat()
+ .map(e => e.toString())
+ .sort(() => {return .5 - Math.random()}) // shuffle
+ this.cards.forEach(word => {
+ put(this.form,
+ 'figure>img[src=$]<', `/data/word/en/${word}.jpg`,
+ {onclick: e => this.turn(e), id: word}
+ );
+ });
+ });
+ }
+};
+
+new WordMemory();
--- /dev/null
+<(../common.inc.plp)><:
+
+Html({
+ raw => <<'EOT',
+<script src="/word/put.js"></script>
+<script src="/word/memory.js"></script>
+<style>
+/* cards */
+figure {
+ display: inline-block;
+ background: #224;
+ border: 1px solid #888;
+ margin: 2px;
+ perspective: 100em;
+ height: 300px;
+ position: relative;
+}
+figure:not(.turn):hover {
+ cursor: pointer;
+}
+figure, img {
+ transition: all .5s ease-in;
+}
+
+/* card faces */
+figure img {
+ height: 100%;
+ width: auto;
+ backface-visibility: hidden;
+ transform: rotateY(180deg); /* back */
+ transform-style: preserve-3d;
+ float: left; /* ff workaround to prevent click selection */
+}
+
+/* turn results */
+figure.turn img {
+ transform: rotateY(0deg);
+}
+figure.bad img {
+ filter: sepia(.5) hue-rotate(-45deg) saturate(2); /* red tint */
+}
+figure.good {
+ opacity: .8;
+}
+</style>
+EOT
+});
+say '<h1>memory</h1><p id="quiz"></p>';