diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | desktop.css | 3 | ||||
-rw-r--r-- | die.mjs | 64 | ||||
-rw-r--r-- | error.mjs | 55 | ||||
-rw-r--r-- | favicon.png | bin | 0 -> 61638 bytes | |||
-rw-r--r-- | genome-list.mjs | 24 | ||||
-rw-r--r-- | genome.mjs | 80 | ||||
-rw-r--r-- | index.html | 92 | ||||
-rw-r--r-- | main.mjs | 16 | ||||
-rw-r--r-- | mobile.css | 123 | ||||
-rw-r--r-- | nucleotide-selector.mjs | 71 | ||||
-rw-r--r-- | nucleotide.mjs | 79 | ||||
-rw-r--r-- | print.css | 29 | ||||
-rw-r--r-- | rules.mjs | 361 | ||||
-rw-r--r-- | style.css | 57 | ||||
-rw-r--r-- | tablet.css | 23 | ||||
-rw-r--r-- | utils.mjs | 48 |
17 files changed, 1129 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..02db249 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: serve + +serve: + python -m http.server diff --git a/desktop.css b/desktop.css new file mode 100644 index 0000000..3372515 --- /dev/null +++ b/desktop.css @@ -0,0 +1,3 @@ +#instructions { + flex-basis: 33% +} @@ -0,0 +1,64 @@ +class Die { + constructor(elt) { + this.elt = elt + + this.value = '--' + this._boundRollHandler = this.rollHandler.bind(this) + this.disable() + } + + get valueElt() { + if (this._valueElt === undefined) { + this._valueElt = this.elt.querySelector('.value') + } + return this._valueElt + } + + get value() { + return this.valueElt.innerText + } + + set value(val) { + this.valueElt.innerText = val + } + + get button() { + if (this._button === undefined) { + this._button = this.elt.querySelector('button') + } + return this._button + } + + enable() { + this.elt.classList.add('enabled') + this.elt.classList.remove('disabled') + this.button.disabled = false + this.button.addEventListener('click', this._boundRollHandler) + } + + disable() { + this.elt.classList.add('disabled') + this.elt.classList.remove('enabled') + this.button.disabled = true + this.button.removeEventListener('click', this._boundRollHandler) + } + + get onChanged() { + if (this._onChanged !== undefined) { + return this._onChanged + } + return () => {} + } + + set onChanged(fn) { + this._onChanged = fn + } + + rollHandler() { + this.value = Math.floor(Math.random() * Die.size) + 1 + this.onChanged(this.value) + } +} +Die.size = 20 + +export default Die diff --git a/error.mjs b/error.mjs new file mode 100644 index 0000000..0c12f0e --- /dev/null +++ b/error.mjs @@ -0,0 +1,55 @@ +class Error { + constructor(elt) { + this.elt = elt + + this._boundClickHandler = this.clickHandler.bind(this) + this.button.addEventListener('click', this._boundClickHandler) + } + + get errorElt() { + if (this._errorElt === undefined) { + this._errorElt = this.elt.querySelector('p') + } + return this._errorElt + } + + get button() { + if (this._button === undefined) { + this._button = this.elt.querySelector('button') + } + return this._button + } + + get innerHTML() { + return this.errorElt.tinnerHTML + } + + set innerHTML(html) { + this.errorElt.innerHTML = html + } + + get onClick() { + if (this._onClick !== undefined) { + return this._onClick + } + return () => {} + } + + set onClick(fn) { + this._onClick = fn + } + + show() { + this.elt.classList.remove('hidden') + } + + hide() { + this.elt.classList.add('hidden') + } + + clickHandler() { + this.onClick() + } +} + +export default Error diff --git a/favicon.png b/favicon.png Binary files differnew file mode 100644 index 0000000..73c27ee --- /dev/null +++ b/favicon.png diff --git a/genome-list.mjs b/genome-list.mjs new file mode 100644 index 0000000..0008ce8 --- /dev/null +++ b/genome-list.mjs @@ -0,0 +1,24 @@ +class GenomeList { + constructor(elt) { + this.genomes = []; + this.elt = elt; + } + + push(genome) { + this.genomes.push(genome) + this.elt.appendChild(genome.elt) + window.genome = genome + console.log('scrolling into vuew', genome.elt) + genome.elt.scrollIntoView(false) + } + + get last() { + if (this.genomes.length > 0) { + return this.genomes[this.genomes.length - 1] + } else { + return undefined + } + } +} + +export default GenomeList diff --git a/genome.mjs b/genome.mjs new file mode 100644 index 0000000..fcab4ff --- /dev/null +++ b/genome.mjs @@ -0,0 +1,80 @@ +import Nucleotide from './nucleotide.mjs' +import Die from './die.mjs' +import { randomItem } from './utils.mjs' + +class Genome { + static *randomBase() { + for (const i of [...Array(Genome.size)]) { + yield randomItem(Nucleotide.bases) + } + } + + constructor(gen) { + const nucleotideList = document.createElement('ol') + this._boundNucleotideClickedHandler = + this.nucleotideClickedHandler.bind(this) + this.nucleotides = [...gen].map(base => { + const n = new Nucleotide(base) + n.onClick = this._boundNucleotideClickedHandler + nucleotideList.appendChild(n.elt) + return n + }) + this.elt.appendChild(nucleotideList) + this.lock() + } + + get elt() { + if (this._elt === undefined) { + this._elt = document.createElement('li') + this._elt.classList.add('genome') + } + return this._elt + } + + get onSelectionChanged() { + if (this._onSelectionChanged !== undefined) { + return this._onSelectionChanged + } + return () => {} + } + + set onSelectionChanged(fn) { + this._onSelectionChanged = fn + } + + lock() { + this.elt.classList.add('locked') + this.nucleotides.forEach(n => n.lock()) + } + + unlock() { + this.elt.classList.remove('locked') + this.nucleotides.forEach(n => n.unlock()) + } + + clone() { + return new Genome(this.nucleotides.map(n => n.value)) + } + + get selectedNucleotide() { + return this._selectedNucleotide + } + + set selectedNucleotide(nucleotide) { + if (this.selectedNucleotide !== undefined) { + this.selectedNucleotide.deselect() + } + this._selectedNucleotide = nucleotide + this._selectedNucleotide.select() + + const i = this.nucleotides.indexOf(this._selectedNucleotide) + this.onSelectionChanged(this._selectedNucleotide, i) + } + + nucleotideClickedHandler(nucleotide) { + this.selectedNucleotide = nucleotide + } +} +Genome.size = 18 + +export default Genome diff --git a/index.html b/index.html new file mode 100644 index 0000000..5ab898a --- /dev/null +++ b/index.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<html lang='en'> + <head> + <meta charset='utf-8'> + <meta name='viewport' + content='width=device-width, initial-scale=1.0'> + <title>Molecular Evolution Simulator</title> + <link rel='shortcut icon' href='favicon.png' type='image/png'> + <link rel='stylesheet' href='style.css'> + <link rel='stylesheet' media='only screen' + href='mobile.css'> + <link rel='stylesheet' media='only screen and (min-width: 768px)' + href='tablet.css'> + <link rel='stylesheet' media='only screen and (min-width: 1024px)' + href='desktop.css'> + <link rel='stylesheet' media='only print' + href='print.css'> + </head> + + <body> + <ol id='genome-history' start='0'> + </ol> + + <div id='instructions'> + <div id='die'> + <div class='value'>--</div> + <button class='roll' disabled=''>Roll</button> + </div> + + <span id='remaining-iterations'>--</span> more times: + <ol> + <li id='clone-nucleotide' class='step'> + <button id='clone' disabled=''>Clone</button> the genome to + start mutating it. + </li> + + <li id='roll-for-nucleotide' class='step'> + Roll to find the nucleotide to mutate. + <ul> + <li>If the roll is between 1 through 18, inclusive, select + that nucleotide.</li> + <li>Otherwise, skip mutation and clone the genome + again.</li> + </ul> + </li> + + <li id='nucleotide-select' class='step'> + Select the <span id='select-number'>rolled</span> + nucleotide in the sequence. + </li> + + <li id='roll-for-mutation' class='step'> + Roll to see what kind of mutation to perform. + </li> + + <li id='perform-mutation' class='step'> + Depending on the roll: + + <ul> + <li>If the roll is between 1 through 14, inclusive, perform + a <em>transition</em> on the selected nucleotide.</li> + + <li>If the roll is between 15 through 17, inclusive, + perform a <em>complementing transversion</em> on the selected + nucleotide to the base it pairs with.</li> + + <li>Otherwise, perform the <em>other transversion</em> on + the selected nucleotide.</li> + </ul> + </li> + </ol> + + <p id='print-results' class='step'> + <button id='print' disabled=''>Print</button> results. + </p> + </div> + + <div id='errors' class='hidden'> + <p></p> + <button>OK</button> + </div> + + <ul id='nucleotide-selector' class='hidden'> + <li>A</li> + <li>G</li> + <li>C</li> + <li>T</li> + </ul> + + <script src='main.mjs' type='module'></script> + </body> +</html> diff --git a/main.mjs b/main.mjs new file mode 100644 index 0000000..aed3c1a --- /dev/null +++ b/main.mjs @@ -0,0 +1,16 @@ +import Rules from './rules.mjs' + +function init() { + const genomeList = document.querySelector('#genome-history') + const die = document.querySelector('#die') + const nucleotideSelector = document.querySelector('#nucleotide-selector') + const instructions = document.querySelector('#instructions') + const cloneButton = document.querySelector('#clone') + const remainingIterations = document.querySelector('#remaining-iterations') + const printButton = document.querySelector('#print') + const errors = document.querySelector('#errors') + + const rules = new Rules(die, instructions, genomeList, nucleotideSelector, cloneButton, remainingIterations, printButton, errors) +} + +init() diff --git a/mobile.css b/mobile.css new file mode 100644 index 0000000..3d721de --- /dev/null +++ b/mobile.css @@ -0,0 +1,123 @@ +body { + display: flex; + flex-direction: column; + align-items: stretch; + margin: 0; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +#errors { + position: fixed; + display: block; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background-color: #f99; + border: 1px solid #922; +} + +#errors p { + padding: 1ex 1em; + margin: 0; +} + +#errors button { + display: block; + margin-left: auto; + margin-right: 5px; + margin-bottom: 5px; +} + +#genome-history { + margin: 0; + overflow: scroll; + align-self: stretch; + flex-basis: 50%; +} + +#instructions { + padding: 1ex; + margin: 0; + overflow: scroll; + flex-basis: 50%; + border-style: solid; + border-top-width: 1px; + border-left-width: 0; + border-right-width: 0; + border-bottom-width: 0; +} + +#instructions .step { + padding: 5px; +} + +#instructions .step { + display: none; +} + +#instructions .step.current { + display: list-item; +} + +#print-results.step.current { + display: block; +} + +#die { + display: block; + float: right; + padding: 2ex; +} + +#die .value { + padding-bottom: 1ex; +} + +#nucleotide-selector { + position: relative; + white-space: nowrap; + padding: 0; + word-spacing: -6px; /* TODO: 0 doesn't work. And for some reason whitespace is being renderded between <li> elements? */ + width: 8em; /* TODO: this shouldn't be hard coded. */ +} + +#nucleotide-selector li { + display: inline-block; + width: 2em; + flex-basis: 2em; + text-align: center; + padding-top: 1ex; + padding-bottom: 1ex; +} + +.genome { + margin: 0.5ex; +} + +.genome>ol { + display: inline-flex; + flex-wrap: wrap; + padding: 0; +} + +.genome .nucleotide { + display: inline-block; + height: 32px; + width: 32px; + font-size: 18px; + text-align: center; + flex-wrap: wrap; + justify-content: space-around; + align-items: center; +} + +.genome .nucleotide span { + display: inline-block; + padding-top: 5px; + padding-bottom: 5px; +} diff --git a/nucleotide-selector.mjs b/nucleotide-selector.mjs new file mode 100644 index 0000000..69a94ab --- /dev/null +++ b/nucleotide-selector.mjs @@ -0,0 +1,71 @@ +class NucleotideSelector { + constructor(elt) { + this.elt = elt + for (const elt of this.elt.querySelectorAll('li')) { + elt.addEventListener('click', this.select.bind(this)) + } + } + + get onItemSelected() { + if (this._onItemSelected !== undefined) { + return this._onItemSelected + } + return () => {} + } + + set onItemSelected(fn) { + this._onItemSelected = fn + this._boundWindowResizeHandler = + this.windowResizeHandler.bind(this) + } + + attach(nucleotide) { + this.nucleotide = nucleotide + + this.nucleotide.elt.appendChild(this.elt) + this.elt.classList.remove('hidden') + + this.adjustPosition() + window.addEventListener('resize', this._boundWindowResizeHandler) + } + + detach() { + this.elt.classList.add('hidden') + window.removeEventListener('resize', + this._boundWindowResizeHandler) + } + + windowResizeHandler() { + this.adjustPosition() + } + + adjustPosition() { + const top = + this.nucleotide.elt.offsetTop + + this.nucleotide.elt.offsetHeight + const myWidth = + this.elt.getBoundingClientRect().width + const eltLeft = + this.nucleotide.elt.offsetLeft + const parentWidth = + this.nucleotide.elt.offsetParent.offsetWidth +/* + this.elt.style.top = `${top}px` + if (eltLeft + myWidth > parentWidth) { + this.elt.style.left = 'auto' + this.elt.style.right = '0' + } else { + this.elt.style.left = `${eltLeft}px` + this.elt.style.right = 'auto' + } +*/ + + this.elt.scrollIntoView(false) + } + + select(evt) { + this.onItemSelected(evt.currentTarget.innerText) + } +} + +export default NucleotideSelector diff --git a/nucleotide.mjs b/nucleotide.mjs new file mode 100644 index 0000000..d599fe1 --- /dev/null +++ b/nucleotide.mjs @@ -0,0 +1,79 @@ +import NucleotideSelector from './nucleotide-selector.mjs' + +class Nucleotide { + constructor(base) { + this.value = base + this._boundClickHandler = this.clickHandler.bind(this) + } + + get elt() { + if (this._elt === undefined) { + this._elt = document.createElement('li') + this._elt.classList.add('nucleotide') + } + return this._elt + } + + get valueElt() { + if (this._valueElt === undefined) { + this._valueElt = document.createElement('span') + this.elt.appendChild(this._valueElt) + } + return this._valueElt + } + + get value() { + return this.valueElt.innerText + } + + set value(val) { + this.valueElt.innerText = val + } + + get onClick() { + if (this._onClick !== undefined) { + return this._onClick + } + return () => {} + } + + set onClick(fn) { + this._onClick = fn + } + + lock() { + this.elt.removeEventListener('click', this._boundClickHandler) + } + + unlock() { + this.elt.addEventListener('click', this._boundClickHandler) + } + + select() { + this._elt.classList.add('selected') + } + + deselect() { + this._elt.classList.remove('selected') + } + + clickHandler(evt) { + this.onClick(this) + } +} +Nucleotide.translation = {'A': 'G', + 'C': 'T', + 'G': 'A', + 'T': 'C'} +Nucleotide.complementingTransversion = {'A': 'T', + 'C': 'G', + 'G': 'C', + 'T': 'A'} +Nucleotide.defaultTransversion = {'A': 'C', + 'C': 'A', + 'G': 'T', + 'T': 'G'} +Nucleotide.bases = Object.keys(Nucleotide.translation) + + +export default Nucleotide diff --git a/print.css b/print.css new file mode 100644 index 0000000..cbbfff0 --- /dev/null +++ b/print.css @@ -0,0 +1,29 @@ +body { + background-color: white; +} + +#instructions { + display: none; +} + +#die { + display: none; +} + +#errors { + display: none; +} + +#nucleotide-selector { + display: none; +} + +#genome-history { + top: 0; + left: 0; + width: 100%; +} + +.genome .nucleotide.selected { + border: 2px solid black; +} diff --git a/rules.mjs b/rules.mjs new file mode 100644 index 0000000..cc1363b --- /dev/null +++ b/rules.mjs @@ -0,0 +1,361 @@ +import { randomItem, ordinalSuffix } from './utils.mjs' +import Genome from './genome.mjs' +import Nucleotide from './nucleotide.mjs' +import Die from './die.mjs' +import GenomeList from './genome-list.mjs' +import NucleotideSelector from './nucleotide-selector.mjs' +import Error from './error.mjs' + +class CloneNucleotide { + id = 'clone-nucleotide' + + constructor(rules) { + this.rules = rules + + this._boundClickHandler = this.clickHandler.bind(this) + } + + enter() { + this.rules.cloneButton.addEventListener('click', + this._boundClickHandler) + this.rules.cloneButton.disabled = false + } + + exit() { + this.rules.cloneButton.removeEventListener('click', + this._boundClickHandler) + this.rules.cloneButton.disabled = true + } + + clickHandler(evt) { + const genome = this.rules.currentGenome.clone() + this.rules.genomeList.push(genome) + this.rules.next(new RollForNucleotide(this.rules)) + } +} + +class RollForNucleotide { + id = 'roll-for-nucleotide' + + constructor(rules) { + this.rules = rules + } + + enter() { + this.rules.die.value = '--' + this.rules.die.onChanged = this.handleDieRoll.bind(this) + this.rules.die.enable() + } + + exit() { + this.rules.die.disable() + this.rules.die.onChanged = undefined + } + + handleDieRoll() { + if (this.rules.die.value > Genome.size) { + this.rules.iterations-- + if (this.rules.isLastIteration) { + this.rules.next(new DoNothing(this.rules)) + } else { + this.rules.next(new CloneNucleotide(this.rules)) + } + } else { + this.rules.next(new NucleotideSelect(this.rules)) + } + } +} + +class NucleotideSelect { + id = 'nucleotide-select' + + constructor(rules) { + this.rules = rules + } + + enter() { + this.want = this.rules.die.value + this.rules.instructions.querySelector('#select-number').innerHTML = + `${this.want}<sup>${ordinalSuffix(this.want)}</sup>` + + this.rules.currentGenome.onSelectionChanged = + this.handleSelectionChanged.bind(this) + this.rules.currentGenome.unlock() + } + + exit() { + this.rules.currentGenome.lock() + this.rules.currentGenome.onSelectionChanged = undefined; + } + + handleSelectionChanged(nucleotide, i) { + i++; + if (i != this.rules.die.value) { + this.rules.error.innerHTML = + `You selected the ${i}<sup>${ordinalSuffix(i)}</sup> nucleotide. Please select the ${this.want}<sup>${ordinalSuffix(this.want)}</sup> one.` + this.rules.next(new ShowError(this.rules, this)) + return + } + this.rules.next(new RollForMutation(this.rules)) + } +} + +class RollForMutation { + id = 'roll-for-mutation' + + constructor(rules) { + this.rules = rules + } + + enter() { + this.rules.die.value = '--' + this.rules.die.onChanged = this.handleDieRoll.bind(this) + this.rules.die.enable() + } + + exit() { + this.rules.die.disable() + this.rules.die.onChanged = undefined + } + + handleDieRoll() { + this.rules.next(new PerformMutation(this.rules)) + } +} + +class PerformMutation { + id = 'perform-mutation' + + constructor(rules) { + this.rules = rules + } + + enter() { + const selector = this.rules.nucleotideSelector + selector.onItemSelected = this.handleItemSelected.bind(this) + selector.attach(this.selectedNucleotide) + } + + exit() { + this.rules.nucleotideSelector.detach() + } + + validMutation(from, to) { + return to == this.expectedMutation[from] + } + + get expectedMutation() { + if (this.rules.die.value <= 14) { + return Nucleotide.translation + } else if (this.rules.die.value <= 17) { + return Nucleotide.complementingTransversion + } else { + return Nucleotide.defaultTransversion + } + } + + get selectedNucleotide() { + return this.rules.currentGenome.selectedNucleotide + } + + get errorTranslationHTML() { + return `Select the base that corresponds to a <em>translation</em> of ${this.selectedNucleotide.value}.` + } + get errorComplementingTransversionHTML() { + return `Select the base that corresponds to a <em>complementing transversion</em> of ${this.selectedNucleotide.value}.` + } + get errorDefaultTransversionHTML() { + return `Select the base that corresponds to the <em>other transversion</em> of ${this.selectedNucleotide.value}.` + } + + get errorHTML() { + if (this.expectedMutation == Nucleotide.translation) { + return this.errorTranslationHTML + } else if (this.expectedMutation == Nucleotide.complementingTransversion) { + return this.errorComplementingTransversionHTML + } else { + return this.errorDefaultTransversionHTML + } + } + + handleItemSelected(base) { + if (!this.validMutation(this.selectedNucleotide.value, base)) { + this.rules.error.innerHTML = this.errorHTML + this.rules.next(new ShowError(this.rules, this)) + return + } + + this.selectedNucleotide.value = base + this.rules.iterations-- + if (this.rules.isLastIteration) { + this.rules.next(new DoNothing(this.rules)) + } else { + this.rules.next(new CloneNucleotide(this.rules)) + } + } +} + +class DoNothing { + id = 'print-results' + + constructor(rules) { + this.rules = rules + + this._boundClickHandler = this.clickHandler.bind(this) + } + + enter() { + this.rules.printButton.addEventListener('click', this._boundClickHandler) + this.rules.printButton.disabled = false + } + + exit() { + this.rules.printButton.disabled = true + this.rules.printButton.removeEventListener('click', this._boundClickHandler) + } + + clickHandler() { + window.print() + } +} + +class ShowError { + constructor(rules, nextState) { + this.rules = rules + this.nextState = nextState + + this._boundClickHandler = this.clickHandler.bind(this) + } + + enter() { + this.rules.error.onClick = this._boundClickHandler + this.rules.error.show() + } + + exit() { + this.rules.error.hide() + this.rules.error.onClick = undefined + } + + clickHandler() { + this.rules.next(this.nextState) + } +} + +class Rules { + constructor(die, instructions, genomeList, nucleotideSelector, cloneButton, remainingIterations, printButton, errors) { + this.die = new Die(die) + this.instructions = instructions + this.genomeList = new GenomeList(genomeList) + this.nucleotideSelector = new NucleotideSelector(nucleotideSelector) + this.cloneButton = cloneButton + this.remainingIterations = remainingIterations + this.printButton = printButton + this.error = new Error(errors) + + this.iterations = Rules.maxIterations + this.cloneButton.disabled = true + this.genomeList.push(new Genome(Rules.initialGenomeBases)) + + if (false) { + this._debugStartAtRollForMutation() + } else if (false) { + this._debugStartAtPerformMutation(4) + } else if (false) { + this._debugStartWithError() + } else { + this.currentState = new CloneNucleotide(this) + } + this.enterState() + } + + get iterations() { + return Number(this.remainingIterations.innerText) + } + + set iterations(val) { + this.remainingIterations.innerText = val + } + + get isLastIteration() { + return this.iterations == 0 + } + + _debugStartAtRollForMutation() { + this.currentState = new RollForMutation(this) + const nucleotide = this.currentGenome.nucleotides[2] + this.currentGenome.selectedNucleotide = nucleotide + } + + _debugStartAtPerformMutation(n) { + [...Array(n)].forEach(i => { + const n = randomItem(this.currentGenome.nucleotides) + n.value = randomItem(Nucleotide.bases) + this.currentGenome.selectedNucleotide = n + const g = this.currentGenome.clone() + window.g = g + console.log(g) + this.genomeList.push(g) + }) + + this.currentState = new PerformMutation(this) + this.die.value = 15 + const nucleotide = this.currentGenome.nucleotides[15] + this.currentGenome.selectedNucleotide = nucleotide + } + + _debugStartWithError() { + this.currentState = new ShowError(this, new CloneNucleotide(this)) + this.error.innerHTML = 'test an error' + } + + get currentGenome() { + return this.genomeList.last + } + + showCurrent() { + if (this.currentState.id === undefined) { + return + } + const elt = + this.instructions.querySelector(`#${this.currentState.id}`) + elt.classList.add('current') + elt.scrollIntoView(true) + } + + hideCurrent() { + if (this.currentState.id === undefined) { + return + } + const elt = + this.instructions.querySelector(`#${this.currentState.id}`) + elt.classList.remove('current') + } + + enterState() { + this.currentState.enter() + this.showCurrent() + } + + exitState() { + this.hideCurrent() + this.currentState.exit() + } + + next(nextState) { + this.exitState() + this.currentState = nextState + this.enterState() + } +} +Rules.maxIterations = 10 +Rules.initialGenomeBases = [ + 'G', 'C', 'A', + 'C', 'T', 'C', + 'G', 'G', 'A', + 'T', 'C', 'G', + 'A', 'A', 'T', + 'T', 'C', 'T' +] + +export default Rules diff --git a/style.css b/style.css new file mode 100644 index 0000000..1d4f71c --- /dev/null +++ b/style.css @@ -0,0 +1,57 @@ +* { + box-sizing: border-box; +} + +.hidden { + display: none !important; +} + +body { + background-color: #eee; +} + +#instructions { + background-color: #ddd; +} + +#instructions .current { + background-color: yellow; +} + +#die { + background-color: #fde; + border: 1px solid #aaa; +} + +#die .value { + text-align: center; +} + +#nucleotide-selector { + background-color: ivory; + border: 1px solid black; +} + +.genome.locked { + cursor: text +} + +.genome>ol { + border: 1px solid black; + background-color: white; +} + +#nucleotide-selector, +.genome:not(.locked) { + cursor: pointer; +} + +#nucleotide-selector li:hover, +.genome:not(.locked) .nucleotide:hover { + background-color: orange; +} + +.genome .nucleotide.selected, +.genome:not(.locked) .nucleotide.selected:hover { + background-color: red; +} diff --git a/tablet.css b/tablet.css new file mode 100644 index 0000000..3774e6c --- /dev/null +++ b/tablet.css @@ -0,0 +1,23 @@ +body { + flex-direction: row-reverse; +} + +#instructions { + border-top-width: 0; + border-bottom-width: 0; + border-left-width: 0; + border-right-width: 1px; + flex-basis: 40% +} + +#instructions .step { + display: list-item; +} + +#print-results.step { + display: block; +} + +#genome-history { + flex-grow: 4; +} diff --git a/utils.mjs b/utils.mjs new file mode 100644 index 0000000..61e61b1 --- /dev/null +++ b/utils.mjs @@ -0,0 +1,48 @@ +function randomItem(array) { + return array[Math.floor(Math.random() * array.length)] +} + +function ordinalSuffix(v) { + let n = Number(v) + if (n > 20) { + n = n % 10 + } + + switch (n) { + case 1: + return 'st' + case 2: + return 'nd' + case 3: + return 'rd' + default: + return 'th' + } +} + +function testOrdinalSuffix() { + const tests = {1: 'st', + 2: 'nd', + 3: 'rd', + 4: 'th', + 10: 'th', + 11: 'th', + 12: 'th', + 13: 'th', + 14: 'th', + 20: 'th', + 21: 'st', + 32: 'nd', + 43: 'rd', + 44: 'th', + 100: 'th', + 101: 'st'} + for (const t of Object.keys(tests)) { + const got = ordinalSuffix(t) + const want = tests[t] + console.assert(got === want, + `bad suffix for ${t}: ${got} !== ${want}`) + } +} + +export { ordinalSuffix, testOrdinalSuffix, randomItem } |