A sorter site for Idolm@ster Characters
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Displays correctly! Now for the actual sorting...

execfera 92d55a75 8528b91c

+242 -81
+5 -2
index.html
··· 39 39 40 40 <div class="starting start button">Touhou Project Character Sorter<br><br>Click to Start!</div> 41 41 <div class="starting load button">Load Progress</div> 42 + 43 + <div class="loading button"><div></div><span>Loading...</span></div> 44 + 42 45 <div class="sorting tie button">Tie</div> 43 46 <div class="sorting undo button">Undo</div> 44 47 <div class="sorting save button">Save Progress</div> 45 48 46 49 <img src="src/assets/defaultR.jpg" class="right sort image"> 47 50 48 - <div class="left sort text"></div> 49 - <div class="right sort text"></div> 51 + <div class="left sort text"><p></p></div> 52 + <div class="right sort text"><p></p></div> 50 53 </div> 51 54 52 55 <div class="options"></div>
+46 -6
src/css/styles.css
··· 34 34 .progressfill { 35 35 height: 20px; 36 36 background-color: lightgreen; 37 - width: 79%; 37 + width: 0%; 38 38 } 39 39 40 40 .sorter { ··· 45 45 width: 420px; 46 46 } 47 47 48 - .sorter > * { 49 - cursor: pointer; 50 - } 51 - 52 48 .button { 53 49 border: 1px solid black; 54 50 text-align: center; 55 51 padding: 10%; 56 52 grid-column: 2 / 3; 53 + cursor: pointer; 57 54 } 58 55 59 56 .starting.button { ··· 65 62 display: none; 66 63 } 67 64 65 + .loading.button { 66 + grid-row: span 6; 67 + display: none; 68 + } 69 + 70 + .loading.button > div { 71 + width: 25px; 72 + height: 25px; 73 + margin: 50px auto; 74 + background-color: #333; 75 + 76 + border-radius: 100%; 77 + -webkit-animation: sk-scaleout 1.0s infinite ease-in-out; 78 + animation: sk-scaleout 1.0s infinite ease-in-out; 79 + } 80 + 81 + /* Animation taken from: http://tobiasahlin.com/spinkit/ */ 82 + 83 + .loading.button > span { 84 + margin: auto auto 20%; 85 + font-size: 0.7em; 86 + } 87 + 88 + @-webkit-keyframes sk-scaleout { 89 + 0% { -webkit-transform: scale(0) } 90 + 100% { 91 + -webkit-transform: scale(1.0); 92 + opacity: 0; 93 + } 94 + } 95 + 96 + @keyframes sk-scaleout { 97 + 0% { 98 + -webkit-transform: scale(0); 99 + transform: scale(0); 100 + } 100% { 101 + -webkit-transform: scale(1.0); 102 + transform: scale(1.0); 103 + opacity: 0; 104 + } 105 + } 106 + 68 107 .sorter > .image { 69 108 width: 120px; 70 109 height: 180px; 71 110 margin: auto; 72 111 grid-row: 1 / 7; 112 + cursor: pointer; 73 113 } 74 114 75 115 .sorter > .text { ··· 79 119 } 80 120 81 121 .sorter > .text > p { 82 - margin: 5px 5px; 122 + margin: 0.5em 5px 0px; 83 123 width: calc(100%-10px); 84 124 text-align: center; 85 125 font-size: 0.8em;
+2 -2
src/js/data.js
··· 1 1 /** 2 2 * @typedef {{name: string, key: string, tooltip?: string, checked?: boolean, sub?: {name: string, tooltip?: string, checked?: string}[]}[]} Options 3 - * @typedef {{name: string, img: string, opts: Object<string, boolean|number[]}[]} CharacterData 3 + * @typedef {{name: string, img: string, opts: Object<string, boolean|number[]}[]} CharData 4 4 */ 5 5 6 6 /** 7 7 * Data set. Characters will be removed from the sorting array based on selected options, working down the array. 8 8 * 9 - * @type {Object.<string, {options: Options, characterData: CharacterData}>} 9 + * @type {Object.<string, {options: Options, characterData: CharData}>} 10 10 */ 11 11 const dataSet = {}; 12 12
+189 -57
src/js/main.js
··· 1 - /** @type {CharacterData} */ 1 + /** @type {CharData} */ 2 2 let characterData = []; 3 - /** @type {CharacterData} */ 3 + /** @type {CharData} */ 4 4 let characterDataToSort = []; 5 5 6 6 /** @type {Options} */ 7 7 let options = []; 8 8 9 + let currentVersion = ''; 10 + 9 11 /** @type {(boolean|boolean[])[]} */ 10 12 let optTaken = []; 11 13 12 - let prngSeed = ''; 14 + let timeError = false; 15 + let timestamp = 0; 13 16 let timeTaken = 0; 17 + let choices = ''; 18 + let optStr = ''; 19 + let suboptStr = ''; 20 + 21 + let leftList = []; 22 + let rightList = []; 23 + let sortedIndexList = []; 24 + let recordDataList = []; 25 + let parentIndexList = []; 26 + 14 27 let leftIndex = 0; 28 + let leftInnerIndex = 0; 15 29 let rightIndex = 0; 16 - let choices = ''; 17 - 30 + let rightInnerIndex = 0; 31 + let battleNo = 1; 32 + let sortedNo = 0; 33 + let totalBattles = 0; 34 + let pointer = 0; 18 35 19 36 /** Initialize script. */ 20 37 function init() { ··· 22 39 document.querySelector('.starting.start.button').addEventListener('click', start); 23 40 document.querySelector('.starting.load.button').addEventListener('click', loadProgress); 24 41 25 - document.querySelector('.left.sort.image').addEventListener('click', pick('left')); 26 - document.querySelector('.right.sort.image').addEventListener('click', pick('right')); 42 + document.querySelector('.left.sort.image').addEventListener('click', () => pick('left')); 43 + document.querySelector('.right.sort.image').addEventListener('click', () => pick('right')); 27 44 28 - document.querySelector('.sorting.tie.button').addEventListener('click', pick('tie')); 45 + document.querySelector('.sorting.tie.button').addEventListener('click', () => pick('tie')); 29 46 document.querySelector('.sorting.undo.button').addEventListener('click', undo); 30 47 document.querySelector('.sorting.save.button').addEventListener('click', saveProgress); 31 48 32 49 setLatestDataset(); 33 50 34 51 /** Decode query string if available. */ 35 - decodeQuery(window.location.search.slice(1)); 52 + if (window.location.search.slice(1) !== '') decodeQuery(); 36 53 } 37 54 38 55 /** Begin sorting. */ 39 56 function start() { 40 - document.querySelectorAll('input[type=checkbox]').forEach(cb => cb.disabled = true); 41 - 42 - prngSeed = prngSeed || new Date().toISOString().split('T')[0]; 43 - Math.seedrandom(prngSeed); 44 - 45 57 /** Copy data into sorting array to filter. */ 46 58 characterDataToSort = characterData.slice(0); 47 59 48 - /** Check selected options. */ 60 + /** Check selected options and convert to boolean array form. */ 49 61 options.forEach(opt => { 50 62 if ('sub' in opt) { 51 63 if (!document.getElementById(`cbgroup-${opt.key}`).checked) optTaken.push(false); 52 64 else { 53 - let i = 0, suboptArray = []; 54 - while (i < opt.sub.length) { suboptArray.push(document.getElementById(`cb-${opt.key}-${i++}`).checked); } 65 + const suboptArray = opt.sub.reduce((arr, val, idx) => { 66 + arr.push(document.getElementById(`cb-${opt.key}-${idx}`).checked); 67 + return arr; 68 + }, []); 55 69 optTaken.push(suboptArray); 56 70 } 57 71 } else { optTaken.push(document.getElementById(`cb-${opt.key}`).checked); } 58 72 }); 59 73 60 - /** Filter out deselected criteria. */ 74 + /** Convert boolean array form to string form. */ 75 + optStr = ''; 76 + suboptStr = ''; 77 + 78 + optStr = optTaken 79 + .map(val => !!val) 80 + .reduce((str, val) => { 81 + str += val ? '1' : '0'; 82 + return str; 83 + }, optStr); 84 + optTaken.forEach(val => { 85 + if (Array.isArray(val)) { 86 + suboptStr += '|'; 87 + suboptStr += val.reduce((str, val) => { 88 + str += val ? '1' : '0'; 89 + return str; 90 + }, ''); 91 + } 92 + }); 93 + 94 + /** Filter out deselected nested criteria and remove selected criteria. */ 61 95 options.forEach((opt, index) => { 62 96 if ('sub' in opt) { 63 97 if (optTaken[index]) { ··· 73 107 characterDataToSort = characterDataToSort.filter(char => !char.opts[opt.key]); 74 108 } 75 109 }); 110 + 111 + /** Shuffle character array with timestamp seed. */ 112 + timestamp = timestamp || new Date().getTime(); 113 + if (new Date(timestamp) < new Date(currentVersion)) { timeError = true; } 114 + Math.seedrandom(timestamp); 115 + 116 + characterDataToSort = characterDataToSort 117 + .map(a => [Math.random(), a]) 118 + .sort((a,b) => a[0] - b[0]) 119 + .map(a => a[1]); 120 + 121 + /** 122 + * tiedDataList will keep a record of indexes on which characters are equal (i.e. tied) 123 + * to another one. 124 + */ 125 + 126 + recordDataList = characterDataToSort.map(() => 0); 127 + tiedDataList = characterDataToSort.map(() => -1); 128 + 129 + /** 130 + * Put a list of indexes that we'll be sorting into sortedIndexList. These will refer back 131 + * to characterDataToSort. 132 + * 133 + * Begin splitting each element into little arrays and spread them out over sortedIndexList 134 + * increasing its length until it become arrays of length 1 and you can't split it anymore. 135 + * 136 + * parentIndexList indicates each element's parent (i.e. where it was split from), except 137 + * for the first element, which has no parent. 138 + */ 139 + 140 + sortedIndexList[0] = characterDataToSort.map((val, idx) => idx); 141 + parentIndexList[0] = -1; 142 + 143 + let midpoint = 0; // Indicates where to split the array. 144 + let marker = 1; // Indicates where to place our newly split array. 145 + 146 + for (let i = 0; i < sortedIndexList.length; i++) { 147 + if (sortedIndexList[i].length > 1) { 148 + let parent = sortedIndexList[i]; 149 + midpoint = Math.ceil(parent.length / 2); 150 + 151 + sortedIndexList[marker] = parent.slice(0, midpoint); // Split the array in half, and put the left half into the marked index. 152 + totalBattles += sortedIndexList[marker].length; // The result's length will add to our total number of comparisons. 153 + parentIndexList[marker] = i; // Record where it came from. 154 + 155 + marker++; // Increment the marker to put the right half into. 156 + 157 + sortedIndexList[marker] = parent.slice(midpoint, parent.length); // Put the right half next to its left half. 158 + totalBattles += sortedIndexList[marker].length; // The result's length will add to our total number of comparisons. 159 + parentIndexList[marker] = i; // Record where it came from. 160 + 161 + marker++; // Rinse and repeat, until we get arrays of length 1. 162 + } 163 + } 164 + 165 + leftIndex = sortedIndexList.length - 2; // Start with the second last value and... 166 + rightIndex = sortedIndexList.length - 1; // the last value in the sorted list and work our way down to index 0. 167 + 168 + leftInnerIndex = 0; // Inner indexes, because we'll be comparing the left array 169 + rightInnerIndex = 0; // to the right array, in order to merge them into one sorted array. 170 + 171 + /** Disable all checkboxes and hide/show appropriate parts. */ 172 + document.querySelectorAll('input[type=checkbox]').forEach(cb => cb.disabled = true); 173 + document.querySelectorAll('.starting.button').forEach(el => el.style.display = 'none'); 174 + document.querySelector('.loading.button').style.display = 'block'; 175 + 176 + preloadImages().then(() => { 177 + document.querySelector('.progress').style.display = 'block'; 178 + document.querySelector('.loading.button').style.display = 'none'; 179 + document.querySelectorAll('.sorting.button').forEach(el => el.style.display = 'block'); 180 + document.querySelectorAll('.sort.text').forEach(el => el.style.display = 'block'); 181 + display(); 182 + }); 183 + } 184 + 185 + /** Displays the current state of the sorter. */ 186 + function display() { 187 + const percent = Math.floor(sortedNo * 100 / totalBattles) + '%'; 188 + const leftCharIndex = sortedIndexList[leftIndex][leftInnerIndex]; 189 + const rightCharIndex = sortedIndexList[rightIndex][rightInnerIndex]; 190 + const leftChar = characterDataToSort[leftCharIndex]; 191 + const rightChar = characterDataToSort[rightCharIndex]; 192 + 193 + document.querySelector('.progressbattle').innerHTML = `Battle No. ${battleNo}`; 194 + document.querySelector('.progressfill').style.width = percent; 195 + document.querySelector('.progresstext').innerHTML = percent; 196 + 197 + document.querySelector('.left.sort.image').src = imageRoot + leftChar.img; 198 + document.querySelector('.right.sort.image').src = imageRoot + rightChar.img; 199 + 200 + document.querySelector('.left.sort.text > p').innerHTML = leftChar.name; 201 + document.querySelector('.right.sort.text > p').innerHTML = rightChar.name; 76 202 } 77 203 78 204 /** ··· 81 207 * @param {'left'|'right'|'tie'} sortType 82 208 */ 83 209 function pick(sortType) { 84 - 210 + if (!timestamp) { return start(); } // Start the sort if it hasn't yet. 211 + else if (timeTaken) { return; } // Don't do anything if the sort finished. 85 212 } 86 213 87 214 /** Undo previous choice. */ 88 215 function undo() {} 89 - 90 - function display() {} 91 216 92 217 function result() {} 93 218 ··· 105 230 // LZString.compressToEncodedURIComponent() 106 231 } 107 232 233 + /** Retrieve latest character data and options from dataset. */ 108 234 function setLatestDataset() { 109 - /** Use latest character data and options. */ 235 + /** Set some defaults. */ 236 + timestamp = 0; 237 + timeTaken = 0; 238 + choices = ''; 239 + 110 240 const latestDateIndex = Object.keys(dataSet) 111 - .map(date => new Date(date)) 112 - .reduce((latestDateIndex, currentDate, currentIndex, array) => { 113 - return currentDate > array[latestDateIndex] ? currentIndex : latestDateIndex; 114 - }, 0); 115 - const latestDate = Object.keys(dataSet)[latestDateIndex]; 241 + .map(date => new Date(date)) 242 + .reduce((latestDateIndex, currentDate, currentIndex, array) => { 243 + return currentDate > array[latestDateIndex] ? currentIndex : latestDateIndex; 244 + }, 0); 245 + currentVersion = Object.keys(dataSet)[latestDateIndex]; 116 246 117 - characterData = dataSet[latestDate].characterData; 118 - options = dataSet[latestDate].options; 247 + characterData = dataSet[currentVersion].characterData; 248 + options = dataSet[currentVersion].options; 119 249 120 - /** Set some defaults. */ 121 - prngSeed = ''; 122 - timeTaken = 0; 123 - choices = ''; 124 - 125 - /** Insert data from new options. */ 126 250 populateOptions(); 127 251 } 128 252 ··· 151 275 const groupbox = document.getElementById(`cbgroup-${opt.key}`); 152 276 153 277 groupbox.parentElement.addEventListener('click', () => { 154 - let i = 0; 155 - while (i < opt.sub.length) { 156 - document.getElementById(`cb-${opt.key}-${i}`).disabled = !groupbox.checked; 157 - if (groupbox.checked) { document.getElementById(`cb-${opt.key}-${i}`).checked = true; } 158 - i++; 159 - } 278 + opt.sub.forEach((subopt, subindex) => { 279 + document.getElementById(`cb-${opt.key}-${subindex}`).disabled = !groupbox.checked; 280 + if (groupbox.checked) { document.getElementById(`cb-${opt.key}-${subindex}`).checked = true; } 281 + }); 160 282 }); 161 283 } else { 162 284 optList.insertAdjacentHTML('beforeend', optInsert(opt.name, opt.key, opt.tooltip, opt.checked)); ··· 168 290 * Decodes compressed shareable link query string. 169 291 * @param {string} queryString 170 292 */ 171 - function decodeQuery(queryString) { 172 - if (!queryString) return; 173 - 293 + function decodeQuery(queryString = window.location.search.slice(1)) { 174 294 let successfulLoad; 175 295 176 296 try { ··· 179 299 * @type {string[]} 180 300 */ 181 301 const decoded = LZString.decompressFromEncodedURIComponent(queryString).split('|'); 302 + if (!decoded[0]) { 303 + decoded.splice(0, 1); 304 + timeError = true; 305 + } 182 306 183 - prngSeed = decoded[0]; 307 + timestamp = Number(decoded[0]); 184 308 timeTaken = Number(decoded[1]); 185 309 choices = decoded[3]; 186 310 ··· 188 312 const suboptDecoded = decoded.slice(0); 189 313 190 314 /** 191 - * Get latest data set version from before the seed's date. 192 - * If seed date is before any of the datasets, get the earliest one. 315 + * Get latest data set version from before the timestamp. 316 + * If timestamp is before or after any of the datasets, get the closest one. 317 + * If timestamp is between any of the datasets, get the one in the past, but if timeError is set, get the one in the future. 193 318 */ 194 - const seedDate = { str: prngSeed, val: new Date(prngSeed) }; 319 + const seedDate = { str: timestamp, val: new Date(timestamp) }; 195 320 const dateMap = Object.keys(dataSet) 196 321 .map(date => { 197 322 return { str: date, val: new Date(date) }; 198 323 }) 199 - const dateVersion = dateMap 200 - .sort((a, b) => a.val - b.val) 201 - .reduce((prevDate, currDate) => { 202 - return currDate.val > prevDate.val && currDate.val > seedDate.val ? prevDate : currDate; 203 - }, seedDate); 204 - const targetVersion = dateVersion.val < dateMap[0].val ? dateMap[0].str : dateVersion.str; 324 + const beforeDateIndex = dateMap 325 + .reduce((prevIndex, currDate, currIndex) => { 326 + return currDate.val < seedDate.val ? currIndex : prevIndex; 327 + }, -1); 328 + const afterDateIndex = dateMap.findIndex(date => date.val > seedDate.val); 329 + 330 + if (beforeDateIndex === -1) { 331 + currentVersion = dateMap[afterDateIndex].str; 332 + } else if (afterDateIndex === -1) { 333 + currentVersion = dateMap[beforeDateIndex].str; 334 + } else { 335 + currentVersion = dateMap[timeError ? afterDateIndex : beforeDateIndex].str; 336 + } 205 337 206 - options = dataSet[targetVersion].options; 207 - characterData = dataSet[targetVersion].characterData; 338 + options = dataSet[currentVersion].options; 339 + characterData = dataSet[currentVersion].characterData; 208 340 209 341 /** Populate option list and decode options selected. */ 210 342 populateOptions(); ··· 241 373 img.src = src; 242 374 }); 243 375 }; 244 - const promises = characterData.map(char => loadImage(imageRoot + char.img)); 376 + const promises = characterDataToSort.map(char => loadImage(imageRoot + char.img)); 245 377 return Promise.all(promises); 246 378 } 247 379
-14
test.js
··· 1 - const prngSeed = '2000-01-01' 2 - const seedDate = { str: prngSeed, val: new Date(prngSeed) }; 3 - const dateMap = ['2018-01-01', '2017-01-01'] 4 - .map(date => { 5 - return { str: date, val: new Date(date) }; 6 - }) 7 - .sort((a, b) => a.val - b.val); 8 - let dateVersion = dateMap 9 - .reduce((prevDate, currDate) => { 10 - return currDate.val > prevDate.val && currDate.val > seedDate.val ? prevDate : currDate; 11 - }, seedDate).str; 12 - dateVersion = new Date(dateVersion) < dateMap[0].val ? dateMap[0].str : dateVersion; 13 - 14 - console.log(dateVersion);