this repo has no description
0
fork

Configure Feed

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

update

uwx bc0105fe f9ce58ec

+1066 -1807
+291
public/exporter.js
··· 1 + /* eslint-disable indent */ 2 + /* globals choiceOptions, kinkCategories, kinkNamesById, getSelectedKinkOrDefault, ClipboardItem */ 3 + 4 + // @ts-check 5 + 6 + const IMGUR_CLIENT_ID = '9db53e5936cd02f'; 7 + 8 + const drawCallHandlers = { 9 + /** 10 + * @param {CanvasRenderingContext2D} context 11 + * @param {CanvasPattern} pattern 12 + * @param {{ data: string; x: number; y: number; }} drawCall 13 + */ 14 + // @ts-ignore 15 + simpleTitle(context, pattern, drawCall) { 16 + context.fillStyle = '#000000'; 17 + context.font = 'bold 18px Arial'; 18 + context.fillText(drawCall.data, drawCall.x, drawCall.y + 5); 19 + }, 20 + /** 21 + * @param {CanvasRenderingContext2D} context 22 + * @param {CanvasPattern} pattern 23 + * @param {{ data: { category: any; fields: any[]; }; x: number; y: number; }} drawCall 24 + */ 25 + // @ts-ignore 26 + titleSubtitle(context, pattern, drawCall) { 27 + context.fillStyle = '#000000'; 28 + context.font = 'bold 18px Arial'; 29 + context.fillText(drawCall.data.category, drawCall.x, drawCall.y + 5); 30 + 31 + const fieldsString = drawCall.data.fields.join(', '); 32 + context.font = 'italic 12px Arial'; 33 + context.fillText(fieldsString, drawCall.x, drawCall.y + 20); 34 + }, 35 + /** 36 + * @param {CanvasRenderingContext2D} context 37 + * @param {CanvasPattern} pattern 38 + * @param {{ data: { choices: string[]; text: string; }; x: number; y: number; }} drawCall 39 + */ 40 + kinkRow(context, pattern, drawCall) { 41 + context.fillStyle = '#000000'; 42 + context.font = '12px Arial'; 43 + 44 + let x = drawCall.x + 5 + (drawCall.data.choices.length * 20); 45 + let y = drawCall.y - 6; 46 + context.fillText(drawCall.data.text, x, y); 47 + // Circles 48 + for (let i = 0; i < drawCall.data.choices.length; i++) { 49 + const choice = drawCall.data.choices[i]; 50 + const color = choiceOptions.find(e => e[0] == choice)[2]; 51 + 52 + x = 10 + drawCall.x + (i * 20); 53 + y = drawCall.y - 10; 54 + 55 + context.beginPath(); 56 + context.arc(x, y, 8, 0, 2 * Math.PI, false); 57 + if (color != 'pattern') { 58 + context.fillStyle = color; 59 + } else { 60 + context.fillStyle = pattern; 61 + } 62 + context.fill(); 63 + context.strokeStyle = 'rgba(0, 0, 0, 0.5)'; 64 + context.lineWidth = 1; 65 + context.stroke(); 66 + } 67 + 68 + } 69 + }; 70 + 71 + const patternImg = new Image(); 72 + patternImg.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNScgaGVpZ2h0PScxNSc+CiAgPHJlY3Qgd2lkdGg9JzE1JyBoZWlnaHQ9JzE1JyBmaWxsPSd3aGl0ZScvPgogIDxwYXRoIGQ9J00tMSwxIGwyLC0yCiAgICAgICAgICAgTTAsNSBsMTUsLTE1CiAgICAgICAgICAgTTAsMTAgbDE1LC0xNQogICAgICAgICAgIE0wLDE1IGwxNSwtMTUKICAgICAgICAgICBNMCwyMCBsMTUsLTE1CiAgICAgICAgICAgTTAsMjUgbDE1LC0xNQogICAgICAgICAgIE0xNCwxNiBsMiwtMicgc3Ryb2tlPSdibGFjaycgc3Ryb2tlLXdpZHRoPScxJy8+Cjwvc3ZnPgo='; 73 + 74 + /** 75 + * @param {number} width 76 + * @param {number} height 77 + */ 78 + function setupCanvas(width, height) { 79 + const canvas = document.createElement('canvas'); 80 + canvas.setAttribute('id', 'mainCanvas'); 81 + canvas.width = width; 82 + canvas.height = height; 83 + 84 + const context = canvas.getContext('2d'); 85 + context.fillStyle = '#FFFFFF'; 86 + context.fillRect(0, 0, canvas.width, canvas.height); 87 + 88 + context.font = 'bold 24px Arial'; 89 + context.fillStyle = '#000000'; 90 + context.fillText('Kinklist', 5, 25); 91 + 92 + const pattern = context.createPattern(patternImg, 'repeat'); 93 + 94 + drawLegend(context, pattern); 95 + return { context, canvas, pattern }; 96 + } 97 + 98 + /** 99 + * @param {CanvasRenderingContext2D} context 100 + * @param {CanvasPattern} pattern 101 + */ 102 + function drawLegend(context, pattern) { 103 + context.font = 'bold 13px Arial'; 104 + context.fillStyle = '#000000'; 105 + 106 + const x = context.canvas.width - 15 - (120 * choiceOptions.length); 107 + for (const [i, option] of choiceOptions.entries()) { 108 + context.beginPath(); 109 + context.arc(x + (120 * i), 17, 8, 0, 2 * Math.PI, false); 110 + if (option[2] != 'pattern') { 111 + context.fillStyle = option[2]; 112 + } else { 113 + context.fillStyle = pattern; 114 + } 115 + context.fill(); 116 + context.strokeStyle = 'rgba(0, 0, 0, 0.5)'; 117 + context.lineWidth = 1; 118 + context.stroke(); 119 + 120 + context.fillStyle = '#000000'; 121 + context.fillText(option[1], x + 15 + (i * 120), 22); 122 + } 123 + } 124 + 125 + /** 126 + * @returns {HTMLCanvasElement} 127 + */ 128 + function exportImage() { 129 + // Constants 130 + const numberCols = 6; 131 + const columnWidth = 250; 132 + const simpleTitleHeight = 35; 133 + const titleSubtitleHeight = 50; 134 + const rowHeight = 25; 135 + const offsets = { 136 + left: 10, 137 + right: 10, 138 + top: 50, 139 + bottom: 10 140 + }; 141 + 142 + // Find out how many we have of everything 143 + const numberCats = kinkCategories.length; 144 + const dualCats = kinkCategories.filter(e => e.participants.length == 2).length; 145 + const simpleCats = numberCats - dualCats; 146 + const numberKinks = kinkNamesById.length; 147 + 148 + // Determine the height required for all categories and kinks 149 + const totalHeight = ( 150 + (numberKinks * rowHeight) + 151 + (dualCats * titleSubtitleHeight) + 152 + (simpleCats * simpleTitleHeight) 153 + ); 154 + 155 + // Initialize columns and drawStacks 156 + const columns = []; 157 + for (let i = 0; i < numberCols; i++) { 158 + columns.push({ height: 0, drawStack: [] }); 159 + } 160 + 161 + // Create drawcalls and place them in the drawStack 162 + // for the appropriate column 163 + const avgColHeight = totalHeight / numberCols; 164 + let columnIndex = 0; 165 + for (const category of kinkCategories) { 166 + const catName = category.name; 167 + const fields = category.participants; 168 + const catKinks = category.kinks; 169 + 170 + let catHeight = 0; 171 + catHeight += (fields.length === 1) ? simpleTitleHeight : titleSubtitleHeight; 172 + catHeight += (catKinks.length * rowHeight); 173 + 174 + // Determine which column to place this category in 175 + if ((columns[columnIndex].height + (catHeight / 2)) > avgColHeight) columnIndex++; 176 + while (columnIndex >= numberCols) columnIndex--; 177 + const column = columns[columnIndex]; 178 + 179 + // Drawcall for title 180 + const drawCall = { y: column.height }; 181 + column.drawStack.push(drawCall); 182 + if (fields.length < 2) { 183 + column.height += simpleTitleHeight; 184 + drawCall.type = 'simpleTitle'; 185 + drawCall.data = catName; 186 + } else { 187 + column.height += titleSubtitleHeight; 188 + drawCall.type = 'titleSubtitle'; 189 + drawCall.data = { 190 + category: catName, 191 + fields: fields 192 + }; 193 + } 194 + 195 + // Drawcalls for kinks 196 + for (const kink of category.kinks) { 197 + const drawCall = { 198 + y: column.height, 199 + type: 'kinkRow', 200 + data: { 201 + choices: [], 202 + text: kink 203 + } 204 + }; 205 + column.drawStack.push(drawCall); 206 + column.height += rowHeight; 207 + 208 + for (const participant of category.participants) { 209 + drawCall.data.choices.push(getSelectedKinkOrDefault(kink, participant)); 210 + } 211 + } 212 + } 213 + 214 + let tallestColumnHeight = 0; 215 + for (const column of columns) { 216 + if (tallestColumnHeight < column.height) { 217 + tallestColumnHeight = column.height; 218 + } 219 + } 220 + 221 + const canvasWidth = offsets.left + offsets.right + (columnWidth * numberCols); 222 + const canvasHeight = offsets.top + offsets.bottom + tallestColumnHeight; 223 + const {context, canvas, pattern} = setupCanvas(canvasWidth, canvasHeight); 224 + 225 + for (const [i, column] of columns.entries()) { 226 + const drawStack = column.drawStack; 227 + 228 + const drawX = offsets.left + (columnWidth * i); 229 + for (const drawCall of drawStack) { 230 + drawCall.x = drawX; 231 + drawCall.y += offsets.top; 232 + drawCallHandlers[drawCall.type](context, pattern, drawCall); 233 + } 234 + } 235 + 236 + return canvas; 237 + } 238 + 239 + const modalContainer = document.querySelector('#export-modal-container'); 240 + const modalContent = document.querySelector('#export-modal-content'); 241 + 242 + for (const e of document.querySelectorAll('.modal-close')) { 243 + e.addEventListener('click', () => { 244 + e.parentElement.classList.remove('is-active'); 245 + }); 246 + } 247 + 248 + document.querySelector('#export-image').addEventListener('click', () => { 249 + const canvas = exportImage(); 250 + 251 + // Try 1: https://stackoverflow.com/a/57546936 252 + 253 + // @ts-ignore 254 + if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write !== undefined) { 255 + canvas.toBlob(blob => { 256 + // @ts-ignore 257 + navigator.permissions.query({name: 'clipboard-write'}) 258 + .then(status => { 259 + if (status.state !== 'granted') { 260 + // FAIL SCENARIO 261 + noCopyFallback(); 262 + } 263 + 264 + // @ts-ignore 265 + const item = new ClipboardItem({ 'image/png': blob }); 266 + // @ts-ignore 267 + navigator.clipboard.write([item]); 268 + 269 + alert('Copied to clipboard!'); 270 + }) 271 + .catch(err => { 272 + // FAIL SCENARIO 273 + console.error(err); 274 + noCopyFallback(); 275 + }); 276 + }); 277 + return; 278 + } 279 + 280 + noCopyFallback(); 281 + 282 + function noCopyFallback() { 283 + const image = document.createElement('img'); 284 + image.src = canvas.toDataURL(); 285 + 286 + while (modalContent.firstChild) modalContent.firstChild.remove(); 287 + modalContent.append(image); 288 + 289 + modalContainer.classList.add('is-active'); 290 + } 291 + });
+336 -1807
public/index.html
··· 2 2 <html> 3 3 4 4 <head> 5 - <title>Kinklist</title> 6 - <meta charset="UTF-8"> 7 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 8 - <script src="//code.jquery.com/jquery-2.1.4.min.js"></script> 9 - <script src="//cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script> 10 - <link rel="icon" type="image/x-icon" 11 - href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMjEvMTV5ehY1AAAAZElEQVQ4jaWTwQ3AIAwDbcT+I9f9hjYQA/mAkO7igKAACWdFAF0A2gb0hP0uCyTATyAJSoaK5xGyEmTC9lktmORUdAQvBQ5cJshktoDk0HkmKRNUEmuE6ztYVXe7bT+jW7z9zi8qYiodCjCHKgAAAABJRU5ErkJggg=="> 12 - <style> 13 - body { 14 - font-family: 'Verdana', 'Arial'; 15 - font-size: 12px; 16 - } 17 - 18 - h2 { 19 - padding: 0; 20 - margin: 0; 21 - margin-top: 10px; 22 - margin-bottom: 5px; 23 - font-size: 18px; 24 - } 25 - 26 - table { 27 - border-collapse: collapse; 28 - margin-bottom: 10px; 29 - width: 100%; 30 - } 31 - 32 - th { 33 - border: solid #999 1px; 34 - border-right: none; 35 - margin: 0px; 36 - padding: 4px; 37 - background-color: #666; 38 - color: #FFF; 39 - } 40 - 41 - th.choicesCol { 42 - box-sizing: border-box; 43 - width: 146px; 44 - } 45 - 46 - th+th { 47 - border-left: none; 48 - } 49 - 50 - th:last-child { 51 - border-right: solid #999 1px; 52 - } 53 - 54 - td { 55 - border-left: solid #999 1px; 56 - border-bottom: solid #999 1px; 57 - border-right: solid #999 1px; 58 - margin: 0px; 59 - padding: 4px; 60 - padding-right: 2px; 61 - } 62 - 63 - @-moz-document url-prefix() { 64 - td { 65 - padding: 3.3px; 66 - } 67 - } 68 - 69 - td+td { 70 - border-left-style: none; 71 - } 72 - 73 - .choice { 74 - box-sizing: border-box; 75 - width: 15px; 76 - height: 15px; 77 - opacity: 0.35; 78 - overflow: hidden; 79 - text-indent: 100px; 80 - border: solid #000 1px; 81 - border-radius: 50%; 82 - outline-style: none !important; 83 - vertical-align: middle; 84 - display: inline-block; 85 - cursor: pointer; 86 - font-size: 0; 87 - padding: 0; 88 - } 89 - 90 - .choices .choice { 91 - transition: all 0.3s ease-in-out; 92 - } 93 - 94 - .choice+.choice { 95 - margin-left: 5px; 96 - } 97 - 98 - .choices .choice:hover { 99 - opacity: 0.75; 100 - } 101 - 102 - .choice.selected, 103 - .selected>.choice { 104 - opacity: 1; 105 - border-width: 2px; 106 - } 107 - 108 - .legend { 109 - vertical-align: middle; 110 - font-size: 14px; 111 - } 112 - 113 - .legend div { 114 - display: inline-block; 115 - } 116 - 117 - .legend .choice { 118 - opacity: 1; 119 - cursor: default; 120 - } 121 - 122 - .legend-text { 123 - vertical-align: middle; 124 - } 125 - 126 - #ExportWrapper { 127 - width: 460px; 128 - height: 36px; 129 - } 130 - 131 - #URL { 132 - display: none; 133 - position: absolute; 134 - top: 3px; 135 - box-sizing: border-box; 136 - width: 300px; 137 - height: 30px; 138 - border-radius: 4px; 139 - border: solid #CCC 1px; 140 - font-size: 16px; 141 - padding: 10px; 142 - text-align: center; 143 - color: #666; 144 - font-weight: bold; 145 - } 146 - 147 - #Export { 148 - position: absolute; 149 - left: 310px; 150 - box-sizing: border-box; 151 - color: #FFF; 152 - text-transform: uppercase; 153 - background-color: #4980ae; 154 - font-size: 18px; 155 - width: 150px; 156 - height: 36px; 157 - border-style: none; 158 - border-radius: 4px; 159 - cursor: pointer; 160 - transition: all 0.3s ease-in-out; 161 - } 5 + <meta charset="utf-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1"> 7 + <title>Hello Bulma!</title> 8 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.css"> 162 9 163 - #Export:hover { 164 - opacity: 0.85; 165 - } 166 - 167 - #Loading { 168 - display: none; 169 - overflow: visible; 170 - line-height: 26px; 171 - font-size: 16px; 172 - color: #999; 173 - font-weight: bold; 174 - position: absolute; 175 - top: 4px; 176 - left: 220px; 177 - } 178 - 179 - #Loading:before { 180 - content: ''; 181 - position: absolute; 182 - box-sizing: border-box; 183 - width: 26px; 184 - height: 26px; 185 - border-radius: 50%; 186 - border: solid #999 2px; 187 - border-top-color: transparent; 188 - border-left-color: #CCC; 189 - border-right-color: #666; 190 - animation: spin .5s infinite linear; 191 - margin-left: -40px; 192 - } 193 - 194 - @media (min-width: 1700px) { 195 - .legend { 196 - position: absolute; 197 - top: 7px; 198 - left: 160px; 199 - } 200 - 201 - .legend div { 202 - width: 130px; 203 - } 10 + <style> 11 + /* Display table vertically. Matches Bulma's threshold for stacking columns vertically. */ 12 + @media screen and (max-width: 1023px) { 13 + .kinks-table tr { display: block; float: left; width: 100%; } 14 + .kinks-table td { display: block; } /* Had th, see below for what we actually do */ 204 15 205 - #ExportWrapper { 206 - position: absolute; 207 - top: -3px; 208 - right: 46px; 16 + /* Size choice button as a square that takes up the whole space */ 17 + .choice { 18 + width: 100%; 209 19 } 210 - 211 - h1 { 212 - margin-bottom: 0; 20 + .choice:after { 21 + content: ""; 22 + display: block; 23 + padding-bottom: 100%; 213 24 } 214 - } 215 25 216 - @media (max-width: 1700px) and (min-width: 800px) { 217 - .legend div { 218 - width: 130px; 219 - padding-bottom: 10px; 26 + .kinks-legend .choice { 27 + width: 32px; 28 + height: 32px; 220 29 } 221 30 222 - #ExportWrapper { 223 - position: absolute; 224 - top: -3px; 225 - right: 46px; 31 + /* Hide the border between Self/Partner options etc */ 32 + .kinks-table td:not(:last-child) { 33 + border-bottom: none; 226 34 } 227 - } 228 35 229 - @media (max-width: 800px) and (min-width: 598px) { 230 - .legend div { 231 - width: 180px; 232 - padding-bottom: 10px; 233 - padding-left: 10px; 36 + /* Due to the display:block above the table headers will not match the vertical layout. So we put them side-by-side (and then make them look pretty) */ 37 + .kinks-table th { 38 + display: inline-block; 234 39 } 235 40 236 - #ExportWrapper { 237 - position: relative; 238 - margin-top: 10px; 239 - margin-left: 5px; 41 + /* They're side-by-side, but we still need to remove the padding to make it seem as one single element */ 42 + .kinks-table th:not(:first-child) { 43 + padding-left: 0; 240 44 } 241 - 242 - #URL { 243 - left: 155px; 244 - width: 190px; 245 - font-size: 10px; 45 + .kinks-table th:not(:last-child) { 46 + padding-right: 0; 246 47 } 247 - 248 - #Export { 249 - left: 0px; 48 + /* Then, we add a comma separating them. */ 49 + .kinks-table th:not(:last-child)::after { 50 + content: ', '; 51 + white-space: pre; 250 52 } 251 53 252 - #Loading { 253 - left: 230px; 54 + /* Now we have to add the Self/Partner text to each set of choices so the reader doesn't get confused. */ 55 + /* This is only applied when there is more than one choice. */ 56 + /* https://stackoverflow.com/a/12198561 */ 57 + .kinks-table:not([data-num-participants="1"]) td::before { /* We have to set this manually for every table. */ 58 + color: #363636; 59 + font-style: italic; 60 + content: attr(data-choice-type); /* We have to set this manually for every td. */ 254 61 } 255 62 } 256 - 257 - @media (max-width: 597px) { 258 - body { 259 - font-size: 10px; 260 - } 261 - 262 - table { 263 - min-width: 345px; 63 + @media screen and (min-width: 1024px) { 64 + .choice { 65 + width: 16px; 66 + height: 16px; 264 67 } 265 68 266 - .legend div { 267 - width: 150px; 268 - padding-bottom: 10px; 269 - padding-left: 10px; 69 + /* Make the table stretch out to the entire available width for the outer masonry element. This may be a hack. */ 70 + .kinks-table td:last-child { 71 + width: 100%; /* Stretches out the last column as far as possible, which the browser limits to the masonry's width */ 270 72 } 271 - 272 - #ExportWrapper { 273 - position: relative; 274 - margin-top: 10px; 275 - margin-left: 0px; 276 - width: 345px; 277 - } 278 - 279 - #URL { 280 - left: 155px; 281 - width: 190px; 282 - font-size: 10px; 283 - } 284 - 285 - #Export { 286 - left: 0px; 287 - } 288 - 289 - #Loading { 290 - left: 230px; 73 + .kinks-table { 74 + margin-right: 1rem; /* Adds a small margin to the table so they don't all touch each other */ 291 75 } 292 76 } 293 77 294 - @keyframes spin { 295 - 0% { 296 - transform: rotate(0deg); 297 - } 298 - 299 - 100% { 300 - transform: rotate(-360deg); 301 - } 78 + .column { 79 + margin-right: 1% !important; 302 80 } 303 81 304 - #ExportWrapper :last-child:after { 305 - content: ''; 306 - display: block; 307 - clear: both; 82 + kinks { 83 + display: none !important; 308 84 } 309 85 310 - .kinkCategory {} 311 - 312 - .col { 86 + /*.masonry { 87 + display: flex; 88 + flex-direction: column; 89 + flex-wrap: wrap; 90 + height: 100vw; 91 + max-height: 800px; 92 + }*/ 93 + .masonry > .masonry-inner { 313 94 float: left; 314 - box-sizing: border-box; 315 - margin: 0; 316 - padding: 5px; 317 - } 318 - 319 - .col.col25 { 320 - width: 25%; 321 - } 322 - 323 - .col.col33 { 324 - width: 33.33333%; 325 - } 326 - 327 - .col.col50 { 328 - width: 50%; 329 - } 330 - 331 - .col.col100 { 332 95 width: 100%; 333 - padding: 0px; 334 - } 335 - 336 - .widthWrapper { 337 - max-width: 1700px; 338 - margin-left: auto; 339 - margin-right: auto; 340 - position: relative; 341 - } 342 - 343 - #Edit { 344 - width: 18px; 345 - height: 18px; 346 - background-color: transparent; 347 - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTIvMTUsb8MLAAABaElEQVQ4jZWUvY3jMBCF390mzpQ6I1wBS5A6oDuQOxi4Ap4rUAlqwR1M7IhQBYRCR1To7G1giJBkes/7gElGg4/zK7AgVaWI0FpLAIwxlsJWwtbRdR0BrKzruvw9pUTnHEMIP4NCCC8gay2991TVnGVVVSvYCyil9AJ6Z0sY5nJCCEwp5Rc/NefcE6Sqb4OMMRQRigiNMcWSU0pPkIgUIW3b5gmqKkmybdtVzOwnSZRKMcaQJOu6zr66rklylZn3PoO+7vf7P2x0Op2w2+1wuVyybxxHNE0DALjdbgCAaZrweDyw3+/xN8aIvu9hrd3y/qthGHA+n3E4HJDH773/dWnLpmfQdnqfNHs2EeEfkpymCU3TYBiGVerGGByPRwDA9XrFOI7FElX1WZpz7ldLOO9PCCHfIeb7qqrqY9C8hEvlHi1h1lqqKr33xZPZXv4KNMOcc6sX+77/8bdSBJUUY8xZisjqLJb6Bpjss/W5PAkOAAAAAElFTkSuQmCC'); 348 - background-repeat: no-repeat; 349 - float: left; 350 - border-style: none; 351 - outline-style: none !important; 352 - margin-top: 6px; 353 - margin-right: 4px; 354 - opacity: 0.5; 355 - cursor: pointer; 356 - } 357 - 358 - #Edit:hover { 359 - opacity: 1; 360 - } 361 - 362 - .overlay { 363 - position: fixed; 364 - top: 0; 365 - left: 0; 366 - right: 0; 367 - bottom: 0; 368 - background-color: rgba(0, 0, 0, 0.8); 369 - display: none; 370 - } 371 - 372 - #EditOverlay #Kinks { 373 - box-sizing: border-box; 374 - position: absolute; 375 - top: 10px; 376 - bottom: 50px; 377 - width: 330px; 378 - left: 50%; 379 - margin-left: -165px; 380 - resize: none; 381 - padding: 10px; 382 - border-radius: 5px; 383 - font-family: monospace, 'Courier new', Courier; 96 + /*transition: .8s opacity;*/ 384 97 } 385 98 386 - #EditOverlay #KinksOK { 387 - box-sizing: border-box; 388 - position: absolute; 389 - bottom: 10px; 390 - width: 330px; 391 - height: 30px; 392 - left: 50%; 393 - margin-left: -165px; 394 - color: #FFF; 395 - text-transform: uppercase; 396 - background-color: #4980ae; 397 - font-size: 18px; 398 - border-style: none; 399 - border-radius: 5px; 400 - cursor: pointer; 99 + @media screen and (min-width: 1024px) { 100 + .masonry > .masonry-inner { 101 + width: 50%; 102 + } 401 103 } 402 104 403 - #InputOverlay { 404 - text-align: center; 405 - white-space: nowrap; 105 + @media screen and (min-width: 1530px) { 106 + .masonry > .masonry-inner { 107 + width: 33.3%; 108 + } 406 109 } 407 110 408 - #InputOverlay:before { 409 - content: ''; 410 - display: inline-block; 411 - height: 100%; 412 - vertical-align: middle; 413 - margin-right: -0.25em; 111 + /* Margin in between choices */ 112 + .choices .columns.is-gapless .column:not(:last-child) { 113 + margin-right: 3px !important; 414 114 } 415 115 416 - #InputOverlay .widthWrapper { 417 - display: inline-block; 116 + .choice { 117 + padding: 0; 118 + font-size: 0; 119 + outline: none; 120 + border: 1px solid black; 121 + border-radius: 50%; 122 + transition: opacity 0.3s ease-in-out; 418 123 vertical-align: middle; 419 - width: 400px; 420 - text-align: left; 421 - max-width: 100%; 422 - } 423 - 424 - #InputOverlay .widthWrapper #InputCurrent, 425 - #InputOverlay .widthWrapper .kink-simple { 426 - display: block; 427 - box-sizing: border-box; 428 - padding: 10px; 429 - background-color: #EEE; 430 - } 431 - 432 - #InputOverlay .widthWrapper .kink-simple { 433 - position: relative; 434 - height: 40px; 435 - line-height: 20px; 124 + opacity: 0.35; 436 125 cursor: pointer; 437 126 } 438 127 439 - #InputOverlay .widthWrapper .kink-simple .choice { 440 - margin-right: 5px; 441 - } 442 - 443 - #InputOverlay .widthWrapper .kink-simple .txt-category { 444 - position: absolute; 445 - right: 5px; 446 - top: 5px; 447 - text-transform: uppercase; 448 - font-size: 90%; 449 - font-weight: bold; 450 - opacity: 0.6; 451 - line-height: 1em; 452 - } 453 - 454 - #InputOverlay .widthWrapper .kink-simple .txt-field, 455 - #InputOverlay .widthWrapper .kink-simple .txt-kink { 456 - vertical-align: middle; 457 - } 458 - 459 - #InputOverlay .widthWrapper .kink-simple .txt-field:empty { 460 - display: none; 461 - } 462 - 463 - #InputOverlay .widthWrapper .kink-simple .txt-field:before { 464 - content: '('; 465 - } 466 - 467 - #InputOverlay .widthWrapper .kink-simple .txt-field:after { 468 - content: ') '; 469 - } 470 - 471 - #InputOverlay .widthWrapper #InputPrevious .kink-simple:first-child, 472 - #InputOverlay .widthWrapper #InputNext .kink-simple:nth-child(3) { 473 - background-color: #BBB; 474 - font-size: 10px; 475 - margin-left: 12px; 476 - margin-right: 12px; 477 - height: 33px; 478 - } 479 - 480 - #InputOverlay .widthWrapper #InputPrevious .kink-simple:nth-child(2), 481 - #InputOverlay .widthWrapper #InputNext .kink-simple:nth-child(2) { 482 - background-color: #CCC; 483 - font-size: 11px; 484 - margin-left: 6px; 485 - margin-right: 6px; 486 - height: 37px; 487 - } 488 - 489 - #InputOverlay .widthWrapper #InputPrevious .kink-simple:nth-child(3), 490 - #InputOverlay .widthWrapper #InputNext .kink-simple:first-child { 491 - background-color: #DDD; 492 - margin-left: 3px; 493 - margin-right: 3px; 494 - } 495 - 496 - #InputOverlay .widthWrapper #InputPrevious .kink-simple:first-child { 497 - padding-bottom: 4px; 498 - padding-top: 7px; 499 - } 500 - 501 - #InputOverlay .widthWrapper #InputNext .kink-simple:nth-child(3) { 502 - padding-top: 4px; 503 - } 504 - 505 - #InputOverlay .widthWrapper #InputPrevious .kink-simple:nth-child(2) { 506 - padding-bottom: 7px; 507 - padding-top: 9px; 508 - } 509 - 510 - #InputOverlay .widthWrapper #InputNext .kink-simple:nth-child(2) { 511 - padding-top: 7px; 512 - } 513 - 514 - #InputPrevious .kink-simple { 515 - border-top-left-radius: 2px; 516 - border-top-right-radius: 2px; 128 + .choice:not([disabled]):hover { 129 + opacity: 0.7; 517 130 } 518 131 519 - #InputNext .kink-simple { 520 - border-bottom-left-radius: 2px; 521 - border-bottom-right-radius: 2px; 132 + .choice[disabled] { 133 + cursor: inherit; 522 134 } 523 135 524 - #InputOverlay .widthWrapper #InputCurrent { 525 - position: relative; 526 - } 527 - 528 - #InputOverlay .widthWrapper #InputCurrent .closePopup { 529 - position: absolute; 530 - top: 0; 531 - right: 5px; 532 - border-style: none; 533 - background-color: transparent; 534 - font-size: 30px; 535 - cursor: pointer; 536 - outline-style: none !important; 537 - opacity: 0.65; 538 - } 539 - 540 - #InputOverlay .widthWrapper #InputCurrent .closePopup:hover { 136 + .choice.selected { 541 137 opacity: 1; 542 - } 543 - 544 - #InputOverlay .widthWrapper #InputCurrent h2 { 545 - text-transform: uppercase; 546 - opacity: 0.6; 547 - margin: 0; 548 - } 549 - 550 - #InputOverlay .widthWrapper #InputCurrent h3 { 551 - margin-top: 3px; 552 - margin-bottom: 0; 553 - font-size: 14px; 554 - } 555 - 556 - #InputOverlay .widthWrapper #InputCurrent #InputValues .big-choice { 557 - padding: 10px; 558 - background-color: rgba(255, 255, 255, 0.75); 559 - border-radius: 4px; 560 - margin-top: 5px; 561 - cursor: pointer; 562 - } 563 - 564 - #InputOverlay .widthWrapper #InputCurrent #InputValues .big-choice.selected { 565 - font-weight: bold; 138 + border-width: 2px; 566 139 } 567 140 568 - #InputOverlay .widthWrapper #InputCurrent #InputValues .big-choice.selected .choice { 569 - opacity: 1; 141 + .choice.not-entered { background-color: #FFFFFF; } 142 + .choice.favorite { background-color: #6DB5FE; } 143 + .choice.like { background-color: #23FD22; } 144 + .choice.okay { background-color: #FDFD6B; } 145 + .choice.maybe { background-color: #DB6C00; } 146 + .choice.no { background-color: #920000; } 147 + .choice.try { 148 + background-color: white; 149 + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNScgaGVpZ2h0PScxNSc+CiAgPHJlY3Qgd2lkdGg9JzE1JyBoZWlnaHQ9JzE1JyBmaWxsPSd3aGl0ZScvPgogIDxwYXRoIGQ9J00tMSwxIGwyLC0yCiAgICAgICAgICAgTTAsNSBsMTUsLTE1CiAgICAgICAgICAgTTAsMTAgbDE1LC0xNQogICAgICAgICAgIE0wLDE1IGwxNSwtMTUKICAgICAgICAgICBNMCwyMCBsMTUsLTE1CiAgICAgICAgICAgTTAsMjUgbDE1LC0xNQogICAgICAgICAgIE0xNCwxNiBsMiwtMicgc3Ryb2tlPSdibGFjaycgc3Ryb2tlLXdpZHRoPScxJy8+Cjwvc3ZnPgo=); 150 + background-repeat: repeat; 570 151 } 571 152 572 - #InputOverlay .widthWrapper #InputCurrent #InputValues .big-choice:hover { 573 - padding: 8px; 574 - border: solid #999 2px; 575 - background-color: rgba(255, 255, 255, 1); 153 + .kinks-subtitle { 154 + margin-top: 1.5rem; 155 + margin-bottom: 0 !important; 576 156 } 577 157 578 - #InputOverlay .widthWrapper #InputCurrent #InputValues .big-choice .btn-num-text { 579 - float: right; 580 - display: inline-block; 581 - border: solid #CCC 1px; 582 - text-align: center; 583 - width: 16px; 584 - border-radius: 3px; 158 + .kinks-section { 159 + padding-top: 0; 585 160 } 586 161 587 - #StartBtn { 588 - position: absolute; 589 - top: -3px; 590 - right: 5px; 591 - box-sizing: border-box; 592 - width: 36px; 593 - height: 36px; 594 - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAAWdEVYdENyZWF0aW9uIFRpbWUAMTAvMTUvMTWxuPuyAAACcElEQVRYhe3X30tTYRzH8fdz3FYTd1xamAuTLDbM3EwqNMnoQgxydVH0A6Ks/oG6Mvp1n1flVUJE3pjUTVPCCJxmEKTWRAuCApujkmhJoZu57ZwuwsCaOx7nbBf73D2H7/PldfjCc54j6CxXSaNI/xvwdzIgrWRAWsmAtJJ2IIOe4iLzejoqmym1lDAZ+UFEicatC8Vm6A++5Jb/Pm+nxlIHatl2keo8FwJBriGHREe8U7bjyrWz7/m51IHq1lWjqire4AAvJkf5qczGrduUvYHTRQdxyQ5dGN0gg8hCBc4MXyUQnliwbqO5kOO2/ZzyXQagONvGcO0DLrxp5m7As3ygxUZB4aTvEt1fnuGSHQgEnoleKnIdEEi8NymQ6h7555nocvJt9jsPJ3o4XFhHq/MaAkFF/xE+hD5p9kwKJLqccZ+HYjMcs9XTXnkdSUi0BToXhYEUnUM7rFvnYRqHryx6b1Ig1T2C6h4h0uDjqK0eSUi4ZAc9VbeRhMS76XFdGFjmkblkB3277yAbc3g/PY7D69bdc9lGNoexGi10fOzG7nWTb7KuLCh8YJAyy2YEgqE997AaLbQFOjnxqom1JiueXTd190xqZOZHOwFoLDrEVCxMm9/D+dfNSELiSVUrpZaSlQXNpTc4wJrHNX/W+SYr5bKdqBr/45soSxqZQWTNW/tDn+etV0umpbT93VtP8awSwZhl4EZZE/7wwgddsdmGJAQxRUktyPt1gIaCvTQU1Ca8eghAVVX6goO6QULPn2vBqnyatpylJm87RrHwu0TVKE+DQ7SMtSe8FSQNWomk3Z06A9JKBqSVDEgraQf6BTg8uT5wSV/pAAAAAElFTkSuQmCC'); 595 - border-style: none; 596 - border-radius: 4px; 597 - cursor: pointer; 162 + /* Legend: Bulma's Level does not come with margins by default. We pad the inner element instead. */ 163 + .kinks-legend { 164 + padding-left: 12px; 598 165 } 599 166 600 - #StartBtn:hover { 601 - opacity: 0.8; 167 + /* Margin between the choice button reference and the text that describes it */ 168 + .kinks-legend .choice { 169 + margin-right: 4px; 602 170 } 603 171 604 - @media (max-height: 500px) { 605 - 606 - #InputPrevious, 607 - #InputNext { 608 - display: none; 172 + /* Increases margin between elements, matching Bulma's behavior to only do anything when not displaying vertically */ 173 + @media screen and (min-width: 769px) { 174 + .kinks-legend > .level-item { 175 + margin-right: 1rem !important; 609 176 } 610 177 } 178 + </style> 611 179 612 - /* added */ 180 + <kinks> 181 + #Bodies 182 + (General) 183 + * Skinny 184 + * Chubby 185 + * Hyper 186 + * Twink 187 + * Bear 188 + * Small breasts 189 + * Large breasts 190 + * Hyper breasts 191 + * Small cocks 192 + * Large cocks 193 + * Hyper cocks 194 + * Beard 195 + * Hairy body 196 + * Shaven body 613 197 614 - @media (orientation: portrait) { 615 - .kinkGroup th:last-child { 616 - display: none; 617 - } 198 + #Clothing 199 + (Self, Partner) 200 + * Clothed sex 201 + * Skirts 202 + * Lingerie 203 + * Stockings 204 + * Heels 205 + * Leather 206 + * Latex 207 + * Uniform / costume 208 + * Zettai ryoiki 209 + * Gothic 210 + * Bodysuits 211 + * Cross-dressing 212 + * Animal ears 213 + * Piercings 214 + * Nipple piercings 618 215 619 - .choice { 620 - width: 30px; 621 - height: 30px; 622 - } 216 + #Interactions 217 + (General) 218 + * You and 1 male 219 + * You and 1 female 220 + * You and 1 enby 221 + * You and MtF trans 222 + * You and FtM trans 223 + * You and 1 male, 1 female 224 + * You and 2 males 225 + * You and 2 females 226 + * You and 2 enby 227 + * Group sex 228 + * Hate sex 229 + * ERPing 623 230 624 - .kinkRow td { 625 - text-align: center; 626 - font-size: 200%; 627 - } 231 + #General 232 + (Giving, Receiving) 233 + * Romance / Affection 234 + * Handjob / fingering 235 + * Blowjob 236 + * Deep throat 237 + * Swallowing 238 + * Facials 239 + * Cunnilingus 240 + * Face-sitting 241 + * Edging 242 + * Teasing 243 + * JOI, SI 628 244 629 - .kinkGroup { 630 - display: flex; 631 - flex-flow: row wrap; 632 - } 245 + #Butt play 246 + (Giving, Receiving) 247 + * Anal sex, pegging 248 + * Rimming 249 + * Double penetration 250 + * Anal fisting 633 251 634 - .kinkGroup thead { 635 - flex: 1 100%; 636 - display: flex; 637 - } 252 + #Restrictive 253 + (Self, Partner) 254 + * Gag 255 + * Collar 256 + * Leash 257 + * Chastity 258 + * Bondage (Light) 259 + * Bondage (Heavy) 260 + * Gas masks 261 + * Encasement 638 262 639 - .kinkGroup th { 640 - flex: 1 50%; 641 - display: inline-block; 642 - } 263 + #Toys 264 + (Self, Partner) 265 + * Dildos 266 + * Dragon dildo 267 + * Horse dildo 268 + * Butt plug 269 + * Tail plug 270 + * Anal beads 271 + * Vibrators 272 + * Sounding 643 273 644 - .kinkGroup tbody { 645 - flex: 1 100%; 646 - } 274 + #Domination 275 + (Dominant, Submissive) 276 + * Dominant / Submissive 277 + * Domestic servitude 278 + * Slavery 279 + * Pet play 280 + * Daddy/Mommy 281 + * Discipline 282 + * Begging 283 + * Forced orgasm 284 + * Orgasm control 285 + * Orgasm denial 286 + * Power exchange 287 + * Neglect play 288 + * Breathplay 289 + * Choking 290 + * Hypnoplay / Hypnosis 291 + * Droneplay 647 292 648 - .kinkRow { 649 - flex: 1 100%; 650 - display: flex; 651 - flex-flow: row wrap; 652 - } 293 + #Fantasy 294 + (Self, Partner) 295 + * Futanari 296 + * Nekomimi / Cat features 297 + * Kitsunemimi / Fox features 298 + * Kemonomimi / Animal features 299 + * Furry 300 + * Cows 301 + * Rodents 302 + * Animal Crossing characters 303 + * Clowns 304 + * Vore 305 + * Digestion 306 + * Transformation 307 + * Bimbofication 308 + * Inflation 309 + * Growth 310 + * Tentacles 311 + * Consentacles 312 + * Monster or Alien 313 + * Living clothes 314 + * Force-feeding 315 + * Hermaphrodite 316 + * Dullahan / Detachable head 653 317 654 - .kinkRow td { 655 - padding: 5px; 656 - padding-top: 5px; 657 - border: none; 658 - } 318 + #Fantasy / Noncon 319 + (Aggressor, Target) 320 + * Non-con / rape 321 + * Mindbreak 322 + * Blackmail / coercion 323 + * Kidnapping 324 + * Drugs / alcohol 325 + * Sleep play 326 + * Slavery play 659 327 660 - .kinkRow td:not(:last-child) { 661 - flex: 1 50%; 662 - } 328 + #Fantasy / Taboo 329 + (General) 330 + * Incest 331 + * Cheating 332 + * Exhibitionism 333 + * Voyeurism 663 334 664 - .kinkRow td:last-child { 665 - flex: 1 51%; 666 - border-bottom: solid #999 1px; 667 - } 335 + #Fluids 336 + (General) 337 + * Blood 338 + * Watersports 339 + * Lactation 340 + * Diapers 341 + * Cum play 342 + * Excessive cum 343 + * Gas (farts etc) 668 344 669 - /* two items */ 670 - .kinkRow td:first-child:nth-last-child(2), 671 - .kinkRow td:first-child:nth-last-child(2) ~ td { 672 - border-left: solid #999 1px; 673 - border-right: solid #999 1px; 674 - } 345 + #Degradation 346 + (Giving, Receiving) 347 + * Glory hole 348 + * Name calling 349 + * Humiliation 350 + * Cleaning up 675 351 352 + #Touch & Stimulation 353 + (Actor, Subject) 354 + * Cock/Pussy worship 355 + * Ass worship 356 + * Foot play 357 + * Tickling 358 + * Sensation play 359 + * Electro stimulation 676 360 677 - /* three items */ 678 - .kinkRow td:first-child:nth-last-child(3) { 679 - border-left: solid #999 1px; 680 - border-right: dashed #999 1px; 681 - } 682 - .kinkRow td:first-child:nth-last-child(3) ~ td { 683 - border-right: solid #999 1px; 684 - } 685 - .kinkRow td:first-child:nth-last-child(3) ~ td:last-child { 686 - border-left: solid #999 1px; 687 - } 688 - .kinkRow td:first-child:nth-last-child(3):not(:last-child), 689 - .kinkRow td:first-child:nth-last-child(3):not(:last-child) ~ td:not(:last-child) { 690 - flex: 1 35%; 691 - } 692 - } 693 - </style> 361 + #Misc. Fetish 362 + (Giving, Receiving) 363 + * Fisting 364 + * Gangbang 365 + * Impregnation 366 + * Pregnancy 367 + * Feminization 368 + * Cuckold / Cuckquean 369 + 370 + #Pain 371 + (Giving, Receiving) 372 + * Light pain 373 + * Heavy pain 374 + * Nipple clamps 375 + * Clothes pins 376 + * Caning 377 + * Flogging 378 + * Beating 379 + * Spanking 380 + * Cock/Pussy slapping 381 + * Cock/Pussy torture 382 + * Hot Wax 383 + * Scratching 384 + * Biting 385 + * Cutting 386 + </kinks> 694 387 </head> 695 388 696 389 <body> 697 - <div class="widthWrapper"> 698 - <button id="Edit"></button> 699 - <h1>Kink list</h1> 700 - <div class="legend"> 701 - <div><span data-color="#FFFFFF" class="choice notEntered"></span> <span class="legend-text">Not Entered</span></div> 702 - <div><span data-color="#6DB5FE" class="choice favorite"></span> <span class="legend-text">Favorite</span></div> 703 - <div><span data-color="#23FD22" class="choice like"></span> <span class="legend-text">Like</span></div> 704 - <div><span data-color="#FDFD6B" class="choice okay"></span> <span class="legend-text">Okay</span></div> 705 - <div><span data-color="#DB6C00" class="choice maybe"></span> <span class="legend-text">Maybe</span></div> 706 - <div><span data-color="#920000" class="choice no"></span> <span class="legend-text">No</span></div> 707 - <div><span data-color="pattern" class="choice try"></span> <span class="legend-text">Want To Try</span></div> 708 - <input type="checkbox" name="UpdateHashEnabled" id="UpdateHashEnabled"><label for="UpdateHashEnabled">Autosave</label> 709 - </div> 710 - <div id="ExportWrapper"> 711 - <input type="text" id="URL"> 712 - <button id="Export">Export</button> 713 - <div id="Loading">Loading</div> 390 + <nav class="navbar" role="navigation" aria-label="main navigation"> 391 + <div class="navbar-brand"> 392 + <div class="navbar-item"> 393 + <h1 class="title">Kink list</h1> 394 + </div> 395 + <div class="navbar-item"> 396 + <button id="export-image" class="button is-primary">Export</button> 397 + </div> 714 398 </div> 715 - <button id="StartBtn"></button> 716 - <div id="InputList"></div> 717 - </div> 718 - <div id="EditOverlay" class="overlay"> 719 - <textarea id="Kinks"> 720 - #Bodies 721 - (General) 722 - * Skinny 723 - * Chubby 724 - * Hyper 725 - * Twink 726 - * Bear 727 - * Small breasts 728 - * Large breasts 729 - * Hyper breasts 730 - * Small cocks 731 - * Large cocks 732 - * Hyper cocks 733 - * Beard 734 - * Hairy body 735 - * Shaven body 399 + </nav> 736 400 737 - #Clothing 738 - (Self, Partner) 739 - * Clothed sex 740 - * Skirts 741 - * Lingerie 742 - * Stockings 743 - * Heels 744 - * Leather 745 - * Latex 746 - * Uniform / costume 747 - * Zettai ryoiki 748 - * Gothic 749 - * Bodysuits 750 - * Cross-dressing 751 - * Animal ears 752 - * Piercings 753 - * Nipple piercings 754 - 755 - #Interactions 756 - (General) 757 - * You and 1 male 758 - * You and 1 female 759 - * You and 1 enby 760 - * You and MtF trans 761 - * You and FtM trans 762 - * You and 1 male, 1 female 763 - * You and 2 males 764 - * You and 2 females 765 - * You and 2 enby 766 - * Group sex 767 - * Hate sex 768 - * ERPing 769 - 770 - #General 771 - (Giving, Receiving) 772 - * Romance / Affection 773 - * Handjob / fingering 774 - * Blowjob 775 - * Deep throat 776 - * Swallowing 777 - * Facials 778 - * Cunnilingus 779 - * Face-sitting 780 - * Edging 781 - * Teasing 782 - * JOI, SI 401 + <nav class="level"> 402 + <div id="legend" class="kinks-legend level-left"> 403 + </div> 404 + </nav> 783 405 784 - #Butt play 785 - (Giving, Receiving) 786 - * Anal sex, pegging 787 - * Rimming 788 - * Double penetration 789 - * Anal fisting 406 + <section class="section kinks-section"> 407 + <div id="root" class="masonry"> 408 + </div> 409 + </section> 790 410 791 - #Restrictive 792 - (Self, Partner) 793 - * Gag 794 - * Collar 795 - * Leash 796 - * Chastity 797 - * Bondage (Light) 798 - * Bondage (Heavy) 799 - * Gas masks 800 - * Encasement 411 + <div id="export-modal-container" class="modal"> 412 + <div class="modal-background"></div> 413 + <div class="modal-content"> 801 414 802 - #Toys 803 - (Self, Partner) 804 - * Dildos 805 - * Dragon dildo 806 - * Horse dildo 807 - * Butt plug 808 - * Tail plug 809 - * Anal beads 810 - * Vibrators 811 - * Sounding 415 + <div class="box"> 416 + <h1 class="title">Exported! Copy the image to your clipboard or save it now.</h1> 417 + <div id="export-modal-content"></div> 418 + </div> 812 419 813 - #Domination 814 - (Dominant, Submissive) 815 - * Dominant / Submissive 816 - * Domestic servitude 817 - * Slavery 818 - * Pet play 819 - * Daddy/Mommy 820 - * Discipline 821 - * Begging 822 - * Forced orgasm 823 - * Orgasm control 824 - * Orgasm denial 825 - * Power exchange 826 - * Neglect play 827 - * Breathplay 828 - * Choking 829 - * Hypnoplay / Hypnosis 830 - * Droneplay 831 - 832 - #Fantasy 833 - (Self, Partner) 834 - * Futanari 835 - * Nekomimi / Cat features 836 - * Kitsunemimi / Fox features 837 - * Kemonomimi / Animal features 838 - * Furry 839 - * Cows 840 - * Rodents 841 - * Animal Crossing characters 842 - * Clowns 843 - * Vore 844 - * Digestion 845 - * Transformation 846 - * Bimbofication 847 - * Inflation 848 - * Growth 849 - * Tentacles 850 - * Consentacles 851 - * Monster or Alien 852 - * Living clothes 853 - * Force-feeding 854 - * Hermaphrodite 855 - * Dullahan / Detachable head 856 - 857 - #Fantasy / Noncon 858 - (Aggressor, Target) 859 - * Non-con / rape 860 - * Mindbreak 861 - * Blackmail / coercion 862 - * Kidnapping 863 - * Drugs / alcohol 864 - * Sleep play 865 - * Slavery play 866 - 867 - #Fantasy / Taboo 868 - (General) 869 - * Incest 870 - * Cheating 871 - * Exhibitionism 872 - * Voyeurism 873 - 874 - #Fluids 875 - (General) 876 - * Blood 877 - * Watersports 878 - * Lactation 879 - * Diapers 880 - * Cum play 881 - * Excessive cum 882 - * Gas (farts etc) 883 - 884 - #Degradation 885 - (Giving, Receiving) 886 - * Glory hole 887 - * Name calling 888 - * Humiliation 889 - * Cleaning up 890 - 891 - #Touch & Stimulation 892 - (Actor, Subject) 893 - * Cock/Pussy worship 894 - * Ass worship 895 - * Foot play 896 - * Tickling 897 - * Sensation play 898 - * Electro stimulation 899 - 900 - #Misc. Fetish 901 - (Giving, Receiving) 902 - * Fisting 903 - * Gangbang 904 - * Impregnation 905 - * Pregnancy 906 - * Feminization 907 - * Cuckold / Cuckquean 908 - 909 - #Pain 910 - (Giving, Receiving) 911 - * Light pain 912 - * Heavy pain 913 - * Nipple clamps 914 - * Clothes pins 915 - * Caning 916 - * Flogging 917 - * Beating 918 - * Spanking 919 - * Cock/Pussy slapping 920 - * Cock/Pussy torture 921 - * Hot Wax 922 - * Scratching 923 - * Biting 924 - * Cutting 925 - </textarea> 926 - <button id="KinksOK">Accept</button> 927 - </div> 928 - <div id="InputOverlay" class="overlay"> 929 - <div class="widthWrapper"> 930 - <div id="InputPrevious"></div> 931 - <div id="InputCurrent"> 932 - <h2 id="InputCategory"></h2> 933 - <h3 id="InputField"></h3> 934 - <button class="closePopup">&times;</button> 935 - <div id="InputValues"></div> 936 - </div> 937 - <div id="InputNext"></div> 938 420 </div> 421 + <button class="modal-close is-large" aria-label="close"></button> 939 422 </div> 940 - <script type="text/javascript"> 941 - function isMobile() { 942 - let check = false; 943 - (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); 944 - return check; 945 - } 946 423 947 - function log(val, base) { 948 - return Math.log(val) / Math.log(base); 949 - } 950 - function strToClass(str) { 951 - var className = ""; 952 - str = str.toLowerCase(); 953 - var validChars = 'abcdefghijklmnopqrstuvwxyz'; 954 - var newWord = false; 955 - for (var i = 0; i < str.length; i++) { 956 - var chr = str[i]; 957 - if (validChars.indexOf(chr) >= 0) { 958 - if (newWord) chr = chr.toUpperCase(); 959 - className += chr; 960 - newWord = false; 961 - } 962 - else { 963 - newWord = true; 964 - } 965 - } 966 - return className; 967 - } 968 - function addCssRule(selector, rules) { 969 - var sheet = document.styleSheets[0]; 970 - if ("insertRule" in sheet) { 971 - sheet.insertRule(selector + "{" + rules + "}", 0); 972 - } 973 - else if ("addRule" in sheet) { 974 - sheet.addRule(selector, rules, 0); 975 - } 976 - } 977 - 978 - var kinks = {}; 979 - var inputKinks = {}; 980 - var colors = {}; 981 - var level = {}; 982 - 983 - function hatchPattern(patCanvas, x1, y1, dx, dy, delta) { 984 - var patContext = patCanvas.getContext('2d'); 985 - patContext.rect(x1, y1, dx, dy); 986 - patContext.save(); 987 - patContext.clip(); 988 - var majorAxe = _.max([dx, dy]); 989 - patContext.strokeStyle = 'rgba(0, 0, 0, 0.5)'; 990 - 991 - _.each(_.range(-1 * (majorAxe), majorAxe, delta), function(n, i) { 992 - patContext.beginPath(); 993 - patContext.moveTo(dy - n + x1, y1); 994 - patContext.lineTo(-n + x1, y1 + dy); 995 - patContext.stroke(); 996 - }) 997 - patContext.restore(); 998 - } 999 - 1000 - $(function() { 1001 - 1002 - var imgurClientId = '9db53e5936cd02f'; 1003 - 1004 - inputKinks = { 1005 - $columns: [], 1006 - createCategory(name, fields) { 1007 - var $category = $('<div class="kinkCategory">') 1008 - .addClass('cat-' + strToClass(name)) 1009 - .data('category', name) 1010 - .append($('<h2>') 1011 - .text(name)); 1012 - 1013 - var $table = $('<table class="kinkGroup">').data('fields', fields); 1014 - var $thead = $('<thead>').appendTo($table); 1015 - for (var i = 0; i < fields.length; i++) { 1016 - $('<th>').addClass('choicesCol').text(fields[i]).appendTo($thead); 1017 - } 1018 - $('<th>').appendTo($thead); 1019 - $('<tbody>').appendTo($table); 1020 - $category.append($table); 1021 - 1022 - return $category; 1023 - }, 1024 - createChoice() { 1025 - var $container = $('<div>').addClass('choices'); 1026 - var levels = Object.keys(level); 1027 - for (var i = 0; i < levels.length; i++) { 1028 - $('<button>') 1029 - .addClass('choice') 1030 - .addClass(level[levels[i]]) 1031 - .data('level', levels[i]) 1032 - .data('levelInt', i) 1033 - .attr('title', levels[i]) 1034 - .appendTo($container) 1035 - .on('click', function() { 1036 - $container.find('button').removeClass('selected'); 1037 - $(this).addClass('selected'); 1038 - }); 1039 - } 1040 - return $container; 1041 - }, 1042 - createKink(fields, name) { 1043 - var $row = $('<tr>').data('kink', name).addClass('kinkRow'); 1044 - for (var i = 0; i < fields.length; i++) { 1045 - var $choices = inputKinks.createChoice(); 1046 - $choices.data('field', fields[i]); 1047 - $choices.addClass('choice-' + strToClass(fields[i])); 1048 - $('<td>').append($choices).appendTo($row); 1049 - } 1050 - $('<td>').text(name).appendTo($row); 1051 - $row.addClass('kink-' + strToClass(name)); 1052 - return $row; 1053 - }, 1054 - createColumns() { 1055 - var colClasses = ['100', '50', '33', '25']; 1056 - 1057 - var numCols = Math.floor((document.body.scrollWidth - 20) / 400); 1058 - if (!numCols) numCols = 1; 1059 - if (numCols > 4) numCols = 4; 1060 - var colClass = 'col' + colClasses[numCols - 1]; 1061 - 1062 - inputKinks.$columns = []; 1063 - for (var i = 0; i < numCols; i++) { 1064 - inputKinks.$columns.push($('<div>').addClass('col ' + colClass).appendTo($('#InputList'))); 1065 - } 1066 - }, 1067 - placeCategories($categories) { 1068 - var $body = $('body'); 1069 - var totalHeight = 0; 1070 - for (var i = 0; i < $categories.length; i++) { 1071 - var $clone = $categories[i].clone().appendTo($body); 1072 - var height = $clone.height();; 1073 - totalHeight += height; 1074 - $clone.remove(); 1075 - } 1076 - 1077 - var colHeight = totalHeight / (inputKinks.$columns.length); 1078 - var colIndex = 0; 1079 - for (var i = 0; i < $categories.length; i++) { 1080 - var curHeight = inputKinks.$columns[colIndex].height(); 1081 - var catHeight = $categories[i].height(); 1082 - if (curHeight + (catHeight / 2) > colHeight) colIndex++; 1083 - while (colIndex >= inputKinks.$columns.length) { 1084 - colIndex--; 1085 - } 1086 - inputKinks.$columns[colIndex].append($categories[i]); 1087 - } 1088 - }, 1089 - fillInputList() { 1090 - $('#InputList').empty(); 1091 - inputKinks.createColumns(); 1092 - 1093 - var $categories = []; 1094 - var kinkCats = Object.keys(kinks); 1095 - for (var i = 0; i < kinkCats.length; i++) { 1096 - var catName = kinkCats[i]; 1097 - var category = kinks[catName]; 1098 - var fields = category.fields; 1099 - var kinkArr = category.kinks; 1100 - 1101 - var $category = inputKinks.createCategory(catName, fields); 1102 - var $tbody = $category.find('tbody'); 1103 - for (var k = 0; k < kinkArr.length; k++) { 1104 - $tbody.append(inputKinks.createKink(fields, kinkArr[k])); 1105 - } 1106 - 1107 - $categories.push($category); 1108 - } 1109 - inputKinks.placeCategories($categories); 1110 - 1111 - // Make things update hash 1112 - $('#InputList').find('button.choice').on('click', function() { 1113 - if ($('#UpdateHashEnabled').is(':checked')) { 1114 - location.hash = inputKinks.updateHash(); 1115 - } 1116 - }); 1117 - }, 1118 - init() { 1119 - // Set up DOM 1120 - inputKinks.fillInputList(); 1121 - 1122 - // Read hash 1123 - inputKinks.parseHash(); 1124 - 1125 - var patCanvas = document.createElement('canvas'); 1126 - patCanvas.height = 30; 1127 - patCanvas.width = 30; 1128 - hatchPattern(patCanvas, 0, 0, 30, 30, 5); 1129 - patCanvas.toBlob(function (blob) { 1130 - var patternImg = document.createElement('img'); 1131 - patternImg.crossOrigin = 'Anonymous'; 1132 - var url = URL.createObjectURL(blob); 1133 - 1134 - patternImg.onload = function() { 1135 - // no longer need to read the blob so it's revoked 1136 - URL.revokeObjectURL(url); 1137 - }; 1138 - 1139 - patternImg.src = url; 1140 - patternImg.setAttribute('id', 'patternImg'); 1141 - patternImg.setAttribute('hidden', 'true'); 1142 - // var body = document.getElementsByTagName('body')[0] 1143 - document.body.appendChild(patternImg); 1144 - }, 'image/png'); 1145 - 1146 - 1147 - // Make export button work 1148 - $('#Export').on('click', inputKinks.export); 1149 - $('#URL').on('click', function() { this.select() }); 1150 - 1151 - // On resize, redo columns 1152 - (function() { 1153 - 1154 - var lastResize = 0; 1155 - $(window).on('resize', function() { 1156 - var curTime = (new Date()).getTime(); 1157 - lastResize = curTime; 1158 - setTimeout(function() { 1159 - if (lastResize === curTime) { 1160 - inputKinks.fillInputList(); 1161 - inputKinks.parseHash(); 1162 - } 1163 - }, 500); 1164 - }); 1165 - 1166 - })(); 1167 - }, 1168 - hashChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.=+*^!@", 1169 - maxPow(base, maxVal) { 1170 - var maxPow = 1; 1171 - for (var pow = 1; Math.pow(base, pow) <= maxVal; pow++) { 1172 - maxPow = pow; 1173 - } 1174 - return maxPow; 1175 - }, 1176 - prefix(input, len, char) { 1177 - while (input.length < len) { 1178 - input = char + input; 1179 - } 1180 - return input; 1181 - }, 1182 - drawLegend(context) { 1183 - context.font = "bold 13px Arial"; 1184 - context.fillStyle = '#000000'; 1185 - 1186 - var levels = Object.keys(colors); 1187 - var x = context.canvas.width - 15 - (120 * levels.length); 1188 - var patternImg = document.getElementById('patternImg'); 1189 - var pattern = context.createPattern(patternImg, 'repeat'); 1190 - for (var i = 0; i < levels.length; i++) { 1191 - context.beginPath(); 1192 - context.arc(x + (120 * i), 17, 8, 0, 2 * Math.PI, false); 1193 - if (colors[levels[i]] != 'pattern') { 1194 - context.fillStyle = colors[levels[i]]; 1195 - } 1196 - else { 1197 - context.fillStyle = pattern; 1198 - } 1199 - context.fill(); 1200 - context.strokeStyle = 'rgba(0, 0, 0, 0.5)' 1201 - context.lineWidth = 1; 1202 - context.stroke(); 1203 - 1204 - context.fillStyle = '#000000'; 1205 - context.fillText(levels[i], x + 15 + (i * 120), 22); 1206 - } 1207 - }, 1208 - setupCanvas(width, height, username) { 1209 - $('#mainCanvas').remove(); 1210 - var canvas = document.createElement('canvas'); 1211 - canvas.setAttribute('id', 'mainCanvas'); 1212 - canvas.width = width; 1213 - canvas.height = height; 1214 - 1215 - var $canvas = $('#mainCanvas'); 1216 - $canvas.css({ 1217 - width: width, 1218 - height: height 1219 - }); 1220 - // $canvas.insertBefore($('#InputList')); 1221 - 1222 - var context = canvas.getContext('2d'); 1223 - context.fillStyle = '#FFFFFF'; 1224 - context.fillRect(0, 0, canvas.width, canvas.height); 1225 - 1226 - context.font = "bold 24px Arial"; 1227 - context.fillStyle = '#000000'; 1228 - context.fillText('Kinklist ' + username, 5, 25); 1229 - 1230 - inputKinks.drawLegend(context); 1231 - return { context: context, canvas: canvas }; 1232 - }, 1233 - drawCallHandlers: { 1234 - simpleTitle(context, drawCall) { 1235 - context.fillStyle = '#000000'; 1236 - context.font = "bold 18px Arial"; 1237 - context.fillText(drawCall.data, drawCall.x, drawCall.y + 5); 1238 - }, 1239 - titleSubtitle(context, drawCall) { 1240 - context.fillStyle = '#000000'; 1241 - context.font = "bold 18px Arial"; 1242 - context.fillText(drawCall.data.category, drawCall.x, drawCall.y + 5); 1243 - 1244 - var fieldsStr = drawCall.data.fields.join(', '); 1245 - context.font = "italic 12px Arial"; 1246 - context.fillText(fieldsStr, drawCall.x, drawCall.y + 20); 1247 - }, 1248 - kinkRow(context, drawCall) { 1249 - context.fillStyle = '#000000'; 1250 - context.font = "12px Arial"; 1251 - 1252 - var x = drawCall.x + 5 + (drawCall.data.choices.length * 20); 1253 - var y = drawCall.y - 6; 1254 - context.fillText(drawCall.data.text, x, y); 1255 - var patternImg = document.getElementById('patternImg'); 1256 - var pattern = context.createPattern(patternImg, 'repeat'); 1257 - // Circles 1258 - for (var i = 0; i < drawCall.data.choices.length; i++) { 1259 - var choice = drawCall.data.choices[i]; 1260 - var color = colors[choice]; 1261 - 1262 - var x = 10 + drawCall.x + (i * 20); 1263 - var y = drawCall.y - 10; 1264 - 1265 - context.beginPath(); 1266 - context.arc(x, y, 8, 0, 2 * Math.PI, false); 1267 - if (color != 'pattern') { 1268 - context.fillStyle = color; 1269 - } else { 1270 - context.fillStyle = pattern; 1271 - } 1272 - context.fill(); 1273 - context.strokeStyle = 'rgba(0, 0, 0, 0.5)' 1274 - context.lineWidth = 1; 1275 - context.stroke(); 1276 - } 1277 - 1278 - } 1279 - }, 1280 - export() { 1281 - var username = prompt("Please enter your name"); 1282 - if (typeof username !== 'string') return; 1283 - else if (username.length) username = '(' + username + ')'; 1284 - 1285 - $('#Loading').fadeIn(); 1286 - $('#URL').fadeOut(); 1287 - 1288 - // Constants 1289 - var numCols = 6; 1290 - var columnWidth = 250; 1291 - var simpleTitleHeight = 35; 1292 - var titleSubtitleHeight = 50; 1293 - var rowHeight = 25; 1294 - var offsets = { 1295 - left: 10, 1296 - right: 10, 1297 - top: 50, 1298 - bottom: 10 1299 - }; 1300 - 1301 - // Find out how many we have of everything 1302 - var numCats = $('.kinkCategory').length; 1303 - var dualCats = $('.kinkCategory th + th + th').length; 1304 - var simpleCats = numCats - dualCats; 1305 - var numKinks = $('.kinkRow').length; 1306 - 1307 - // Determine the height required for all categories and kinks 1308 - var totalHeight = ( 1309 - (numKinks * rowHeight) + 1310 - (dualCats * titleSubtitleHeight) + 1311 - (simpleCats * simpleTitleHeight) 1312 - ); 1313 - 1314 - // Initialize columns and drawStacks 1315 - var columns = []; 1316 - for (var i = 0; i < numCols; i++) { 1317 - columns.push({ height: 0, drawStack: [] }); 1318 - } 1319 - 1320 - // Create drawcalls and place them in the drawStack 1321 - // for the appropriate column 1322 - var avgColHeight = totalHeight / numCols; 1323 - var columnIndex = 0; 1324 - $('.kinkCategory').each(function() { 1325 - var $cat = $(this); 1326 - var catName = $cat.data('category'); 1327 - var category = kinks[catName]; 1328 - var fields = category.fields; 1329 - var catKinks = category.kinks; 1330 - 1331 - var catHeight = 0; 1332 - catHeight += (fields.length === 1) ? simpleTitleHeight : titleSubtitleHeight; 1333 - catHeight += (catKinks.length * rowHeight); 1334 - 1335 - // Determine which column to place this category in 1336 - if ((columns[columnIndex].height + (catHeight / 2)) > avgColHeight) columnIndex++; 1337 - while (columnIndex >= numCols) columnIndex--; 1338 - var column = columns[columnIndex]; 1339 - 1340 - // Drawcall for title 1341 - var drawCall = { y: column.height }; 1342 - column.drawStack.push(drawCall); 1343 - if (fields.length < 2) { 1344 - column.height += simpleTitleHeight; 1345 - drawCall.type = 'simpleTitle'; 1346 - drawCall.data = catName; 1347 - } 1348 - else { 1349 - column.height += titleSubtitleHeight; 1350 - drawCall.type = 'titleSubtitle'; 1351 - drawCall.data = { 1352 - category: catName, 1353 - fields: fields 1354 - }; 1355 - } 1356 - 1357 - // Drawcalls for kinks 1358 - $cat.find('.kinkRow').each(function() { 1359 - var $kinkRow = $(this); 1360 - var drawCall = { 1361 - y: column.height, type: 'kinkRow', data: { 1362 - choices: [], 1363 - text: $kinkRow.data('kink') 1364 - } 1365 - }; 1366 - column.drawStack.push(drawCall); 1367 - column.height += rowHeight; 1368 - 1369 - // Add choices 1370 - $kinkRow.find('.choices').each(function() { 1371 - var $selection = $(this).find('.choice.selected'); 1372 - var selection = ($selection.length > 0) 1373 - ? $selection.data('level') 1374 - : Object.keys(level)[0]; 1375 - 1376 - drawCall.data.choices.push(selection); 1377 - }); 1378 - }); 1379 - }); 1380 - 1381 - var tallestColumnHeight = 0; 1382 - for (var i = 0; i < columns.length; i++) { 1383 - if (tallestColumnHeight < columns[i].height) { 1384 - tallestColumnHeight = columns[i].height; 1385 - } 1386 - } 1387 - 1388 - var canvasWidth = offsets.left + offsets.right + (columnWidth * numCols); 1389 - var canvasHeight = offsets.top + offsets.bottom + tallestColumnHeight; 1390 - var setup = inputKinks.setupCanvas(canvasWidth, canvasHeight, username); 1391 - var context = setup.context; 1392 - var canvas = setup.canvas; 1393 - 1394 - for (var i = 0; i < columns.length; i++) { 1395 - var column = columns[i]; 1396 - var drawStack = column.drawStack; 1397 - 1398 - var drawX = offsets.left + (columnWidth * i); 1399 - for (var j = 0; j < drawStack.length; j++) { 1400 - var drawCall = drawStack[j]; 1401 - drawCall.x = drawX; 1402 - drawCall.y += offsets.top; 1403 - inputKinks.drawCallHandlers[drawCall.type](context, drawCall); 1404 - } 1405 - } 1406 - 1407 - // return $(canvas).insertBefore($('#InputList')); 1408 - 1409 - // Send canvas to imgur 1410 - $.ajax({ 1411 - url: 'https://api.imgur.com/3/image', 1412 - type: 'POST', 1413 - headers: { 1414 - // Your application gets an imgurClientId from Imgur 1415 - Authorization: 'Client-ID ' + imgurClientId, 1416 - Accept: 'application/json' 1417 - }, 1418 - data: { 1419 - // convert the image data to base64 1420 - image: canvas.toDataURL().split(',')[1], 1421 - type: 'base64' 1422 - }, 1423 - success(result) { 1424 - $('#Loading').hide(); 1425 - var url = 'https://i.imgur.com/' + result.data.id + '.png'; 1426 - $('#URL').val(url).fadeIn(); 1427 - }, 1428 - fail() { 1429 - $('#Loading').hide(); 1430 - alert('Failed to upload to imgur, could not connect'); 1431 - } 1432 - }); 1433 - }, 1434 - encode(base, input) { 1435 - var hashBase = inputKinks.hashChars.length; 1436 - var outputPow = inputKinks.maxPow(hashBase, Number.MAX_SAFE_INTEGER); 1437 - var inputPow = inputKinks.maxPow(base, Math.pow(hashBase, outputPow)); 1438 - 1439 - var output = ""; 1440 - var numChunks = Math.ceil(input.length / inputPow); 1441 - var inputIndex = 0; 1442 - for (var chunkId = 0; chunkId < numChunks; chunkId++) { 1443 - var inputIntValue = 0; 1444 - for (var pow = 0; pow < inputPow; pow++) { 1445 - var inputVal = input[inputIndex++]; 1446 - if (typeof inputVal === "undefined") break; 1447 - var val = inputVal * Math.pow(base, pow); 1448 - inputIntValue += val; 1449 - } 1450 - 1451 - var outputCharValue = ""; 1452 - while (inputIntValue > 0) { 1453 - var maxPow = Math.floor(log(inputIntValue, hashBase)); 1454 - var powVal = Math.pow(hashBase, maxPow); 1455 - var charInt = Math.floor(inputIntValue / powVal); 1456 - var subtract = charInt * powVal; 1457 - var char = inputKinks.hashChars[charInt]; 1458 - outputCharValue += char; 1459 - inputIntValue -= subtract; 1460 - } 1461 - var chunk = inputKinks.prefix(outputCharValue, outputPow, inputKinks.hashChars[0]); 1462 - output += chunk; 1463 - } 1464 - return output; 1465 - }, 1466 - decode: function (base, output) { 1467 - var hashBase = inputKinks.hashChars.length; 1468 - var outputPow = inputKinks.maxPow(hashBase, Number.MAX_SAFE_INTEGER); 1469 - 1470 - var values = []; 1471 - var numChunks = Math.max(output.length / outputPow) 1472 - for (var i = 0; i < numChunks; i++) { 1473 - var chunk = output.substring(i * outputPow, (i + 1) * outputPow); 1474 - var chunkValues = inputKinks.decodeChunk(base, chunk); 1475 - for (var j = 0; j < chunkValues.length; j++) { 1476 - values.push(chunkValues[j]); 1477 - } 1478 - } 1479 - return values; 1480 - }, 1481 - decodeChunk: function (base, chunk) { 1482 - var hashBase = inputKinks.hashChars.length; 1483 - var outputPow = inputKinks.maxPow(hashBase, Number.MAX_SAFE_INTEGER); 1484 - var inputPow = inputKinks.maxPow(base, Math.pow(hashBase, outputPow)); 1485 - 1486 - var chunkInt = 0; 1487 - for (var i = 0; i < chunk.length; i++) { 1488 - var char = chunk[i]; 1489 - var charInt = inputKinks.hashChars.indexOf(char); 1490 - var pow = chunk.length - 1 - i; 1491 - var intVal = Math.pow(hashBase, pow) * charInt; 1492 - chunkInt += intVal; 1493 - } 1494 - var chunkIntCopy = chunkInt; 1495 - 1496 - var output = []; 1497 - for (var pow = inputPow - 1; pow >= 0; pow--) { 1498 - var posBase = Math.floor(Math.pow(base, pow)); 1499 - var posVal = Math.floor(chunkInt / posBase); 1500 - var subtract = posBase * posVal; 1501 - output.push(posVal); 1502 - chunkInt -= subtract; 1503 - } 1504 - output.reverse(); 1505 - return output; 1506 - }, 1507 - updateHash() { 1508 - var hashValues = []; 1509 - $('#InputList .choices').each(function() { 1510 - var $this = $(this); 1511 - var lvlInt = $this.find('.selected').data('levelInt'); 1512 - if (!lvlInt) lvlInt = 0; 1513 - hashValues.push(lvlInt); 1514 - }); 1515 - return inputKinks.encode(Object.keys(colors).length, hashValues); 1516 - }, 1517 - parseHash() { 1518 - var hash = location.hash.substring(1); 1519 - if (hash.length < 10) return; 1520 - 1521 - var values = inputKinks.decode(Object.keys(colors).length, hash); 1522 - var valueIndex = 0; 1523 - $('#InputList .choices').each(function() { 1524 - var $this = $(this); 1525 - var value = values[valueIndex++]; 1526 - $this.children().eq(value).addClass('selected'); 1527 - }); 1528 - }, 1529 - saveSelection() { 1530 - var selection = []; 1531 - $('.choice.selected').each(function() { 1532 - // .choice selector 1533 - var selector = '.' + this.className.replace(/ /g, '.'); 1534 - // .choices selector 1535 - selector = '.' + $(this).closest('.choices')[0].className.replace(/ /g, '.') + ' ' + selector; 1536 - // .kinkRow selector 1537 - selector = '.' + $(this).closest('tr.kinkRow')[0].className.replace(/ /g, '.') + ' ' + selector; 1538 - // .kinkCategory selector 1539 - selector = '.' + $(this).closest('.kinkCategory')[0].className.replace(/ /g, '.') + ' ' + selector; 1540 - selector = selector.replace('.selected', ''); 1541 - selection.push(selector); 1542 - }); 1543 - return selection; 1544 - }, 1545 - inputListToText() { 1546 - var KinksText = ""; 1547 - var kinkCats = Object.keys(kinks); 1548 - for (var i = 0; i < kinkCats.length; i++) { 1549 - var catName = kinkCats[i]; 1550 - var catFields = kinks[catName].fields; 1551 - var catKinks = kinks[catName].kinks; 1552 - KinksText += '#' + catName + "\r\n"; 1553 - KinksText += '(' + catFields.join(', ') + ")\r\n"; 1554 - for (var j = 0; j < catKinks.length; j++) { 1555 - KinksText += '* ' + catKinks[j] + "\r\n"; 1556 - } 1557 - KinksText += "\r\n"; 1558 - } 1559 - return KinksText; 1560 - }, 1561 - restoreSavedSelection(selection) { 1562 - setTimeout(function() { 1563 - for (var i = 0; i < selection.length; i++) { 1564 - var selector = selection[i]; 1565 - $(selector).addClass('selected'); 1566 - } 1567 - location.hash = inputKinks.updateHash(); 1568 - }, 300); 1569 - }, 1570 - parseKinksText(kinksText) { 1571 - var newKinks = {}; 1572 - var lines = kinksText.replace(/\r/g, '').split("\n"); 1573 - 1574 - var cat = null; 1575 - var catName = null; 1576 - for (var i = 0; i < lines.length; i++) { 1577 - var line = lines[i]; 1578 - if (!line.length) continue; 1579 - 1580 - if (line[0] === '#') { 1581 - if (catName) { 1582 - if (!(cat.fields instanceof Array) || cat.fields.length < 1) { 1583 - alert(catName + ' does not have any fields defined!'); 1584 - return; 1585 - } 1586 - if (!(cat.kinks instanceof Array) || cat.kinks.length < 1) { 1587 - alert(catName + ' does not have any kinks listed!'); 1588 - return; 1589 - } 1590 - newKinks[catName] = cat; 1591 - } 1592 - catName = line.substring(1).trim(); 1593 - cat = { kinks: [] }; 1594 - } 1595 - if (!catName) continue; 1596 - if (line[0] === '(') { 1597 - cat.fields = line.substring(1, line.length - 1).trim().split(','); 1598 - for (var j = 0; j < cat.fields.length; j++) { 1599 - cat.fields[j] = cat.fields[j].trim(); 1600 - } 1601 - } 1602 - if (line[0] === '*') { 1603 - var kink = line.substring(1).trim(); 1604 - cat.kinks.push(kink); 1605 - } 1606 - } 1607 - if (catName && !newKinks[catName]) { 1608 - if (!(cat.fields instanceof Array) || cat.fields.length < 1) { 1609 - alert(catName + ' does not have any fields defined!'); 1610 - return; 1611 - } 1612 - if (!(cat.kinks instanceof Array) || cat.kinks.length < 1) { 1613 - alert(catName + ' does not have any kinks listed!'); 1614 - return; 1615 - } 1616 - newKinks[catName] = cat; 1617 - } 1618 - return newKinks; 1619 - } 1620 - }; 1621 - 1622 - $('#Edit').on('click', function() { 1623 - var KinksText = inputKinks.inputListToText(); 1624 - $('#Kinks').val(KinksText.trim()); 1625 - $('#EditOverlay').fadeIn(); 1626 - }); 1627 - $('#EditOverlay').on('click', function() { 1628 - $(this).fadeOut(); 1629 - }); 1630 - $('#KinksOK').on('click', function() { 1631 - var selection = inputKinks.saveSelection(); 1632 - try { 1633 - var kinksText = $('#Kinks').val(); 1634 - kinks = inputKinks.parseKinksText(kinksText); 1635 - inputKinks.fillInputList(); 1636 - } 1637 - catch (e) { 1638 - alert('An error occured trying to parse the text entered, please correct it and try again'); 1639 - return; 1640 - } 1641 - inputKinks.restoreSavedSelection(selection); 1642 - $('#EditOverlay').fadeOut(); 1643 - }); 1644 - $('.overlay > *').on('click', e => { 1645 - e.stopPropagation(); 1646 - }); 1647 - 1648 - var stylesheet = document.styleSheets[0]; 1649 - $('.legend .choice').each(function() { 1650 - var $choice = $(this); 1651 - var $parent = $choice.parent(); 1652 - var text = $parent.text().trim(); 1653 - var color = $choice.data('color'); 1654 - var cssClass = this.className.replace('choice ', '').trim(); 1655 - 1656 - if (color != 'pattern') { 1657 - addCssRule('.choice.' + cssClass, 'background-color: ' + color + ';'); 1658 - colors[text] = color; 1659 - } 1660 - else { 1661 - addCssRule('.choice.' + cssClass, 'background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNScgaGVpZ2h0PScxNSc+CiAgPHJlY3Qgd2lkdGg9JzE1JyBoZWlnaHQ9JzE1JyBmaWxsPSd3aGl0ZScvPgogIDxwYXRoIGQ9J00tMSwxIGwyLC0yCiAgICAgICAgICAgTTAsNSBsMTUsLTE1CiAgICAgICAgICAgTTAsMTAgbDE1LC0xNQogICAgICAgICAgIE0wLDE1IGwxNSwtMTUKICAgICAgICAgICBNMCwyMCBsMTUsLTE1CiAgICAgICAgICAgTTAsMjUgbDE1LC0xNQogICAgICAgICAgIE0xNCwxNiBsMiwtMicgc3Ryb2tlPSdibGFjaycgc3Ryb2tlLXdpZHRoPScxJy8+Cjwvc3ZnPgo="); background-repeat: repeat'); 1662 - 1663 - colors[text] = 'pattern'; 1664 - } 1665 - level[text] = cssClass; 1666 - }); 1667 - 1668 - kinks = inputKinks.parseKinksText($('#Kinks').text().trim()); 1669 - inputKinks.init(); 1670 - 1671 - (function() { 1672 - var $popup = $('#InputOverlay'); 1673 - var $previous = $('#InputPrevious'); 1674 - var $next = $('#InputNext'); 1675 - 1676 - // current 1677 - var $category = $('#InputCategory'); 1678 - var $field = $('#InputField'); 1679 - var $options = $('#InputValues'); 1680 - 1681 - function getChoiceValue($choices) { 1682 - var $selected = $choices.find('.choice.selected'); 1683 - return $selected.data('level'); 1684 - } 1685 - 1686 - function getChoicesElement(category, kink, field) { 1687 - var selector = '.cat-' + strToClass(category); 1688 - selector += ' .kink-' + strToClass(kink); 1689 - selector += ' .choice-' + strToClass(field); 1690 - 1691 - var $choices = $(selector); 1692 - return $choices; 1693 - } 1694 - 1695 - inputKinks.getAllKinks = function() { 1696 - var list = []; 1697 - 1698 - var categories = Object.keys(kinks); 1699 - for (var i = 0; i < categories.length; i++) { 1700 - var category = categories[i]; 1701 - var fields = kinks[category].fields; 1702 - var kinkArr = kinks[category].kinks; 1703 - 1704 - for (var j = 0; j < fields.length; j++) { 1705 - var field = fields[j]; 1706 - for (var k = 0; k < kinkArr.length; k++) { 1707 - var kink = kinkArr[k]; 1708 - var $choices = getChoicesElement(category, kink, field); 1709 - var value = getChoiceValue($choices); 1710 - var obj = { category: category, kink: kink, field: field, value: value, $choices: $choices, showField: (fields.length >= 2) }; 1711 - list.push(obj); 1712 - } 1713 - } 1714 - 1715 - } 1716 - return list; 1717 - }; 1718 - 1719 - inputKinks.inputPopup = { 1720 - numPrev: 3, 1721 - numNext: 3, 1722 - allKinks: [], 1723 - kinkByIndex(i) { 1724 - var numKinks = inputKinks.inputPopup.allKinks.length; 1725 - i = (numKinks + i) % numKinks; 1726 - return inputKinks.inputPopup.allKinks[i]; 1727 - }, 1728 - generatePrimary(kink) { 1729 - var $container = $('<div>'); 1730 - var btnIndex = 0; 1731 - $('.legend > div').each(function() { 1732 - var $btn = $(this).clone(); 1733 - $btn.addClass('big-choice'); 1734 - $btn.appendTo($container); 1735 - 1736 - $('<span>') 1737 - .addClass('btn-num-text') 1738 - .text(btnIndex++) 1739 - .appendTo($btn) 1740 - 1741 - var text = $btn.text().trim().replace(/[0-9]/g, ''); 1742 - if (kink.value === text) { 1743 - $btn.addClass('selected'); 1744 - } 1745 - 1746 - $btn.on('click', function() { 1747 - $container.find('.big-choice').removeClass('selected'); 1748 - $btn.addClass('selected'); 1749 - kink.value = text; 1750 - $options.fadeOut(200, function() { 1751 - $options.show(); 1752 - inputKinks.inputPopup.showNext(); 1753 - }); 1754 - var choiceClass = strToClass(text); 1755 - kink.$choices.find('.' + choiceClass).click(); 1756 - }); 1757 - }); 1758 - return $container; 1759 - }, 1760 - generateSecondary(kink) { 1761 - var $container = $('<div class="kink-simple">'); 1762 - $('<span class="choice">').addClass(level[kink.value]).appendTo($container); 1763 - $('<span class="txt-category">').text(kink.category).appendTo($container); 1764 - if (kink.showField) { 1765 - $('<span class="txt-field">').text(kink.field).appendTo($container); 1766 - } 1767 - $('<span class="txt-kink">').text(kink.kink).appendTo($container); 1768 - return $container; 1769 - }, 1770 - showIndex(index) { 1771 - $previous.html(''); 1772 - $next.html(''); 1773 - $options.html(''); 1774 - $popup.data('index', index); 1775 - 1776 - // Current 1777 - var currentKink = inputKinks.inputPopup.kinkByIndex(index); 1778 - var $currentKink = inputKinks.inputPopup.generatePrimary(currentKink); 1779 - $options.append($currentKink); 1780 - $category.text(currentKink.category); 1781 - $field.text((currentKink.showField ? '(' + currentKink.field + ') ' : '') + currentKink.kink); 1782 - $options.append($currentKink); 1783 - 1784 - // Prev 1785 - for (var i = inputKinks.inputPopup.numPrev; i > 0; i--) { 1786 - var prevKink = inputKinks.inputPopup.kinkByIndex(index - i); 1787 - var $prevKink = inputKinks.inputPopup.generateSecondary(prevKink); 1788 - $previous.append($prevKink); 1789 - (skip => { 1790 - $prevKink.on('click', function() { 1791 - inputKinks.inputPopup.showPrev(skip); 1792 - }); 1793 - })(i); 1794 - } 1795 - // Next 1796 - for (var i = 1; i <= inputKinks.inputPopup.numNext; i++) { 1797 - var nextKink = inputKinks.inputPopup.kinkByIndex(index + i); 1798 - var $nextKink = inputKinks.inputPopup.generateSecondary(nextKink); 1799 - $next.append($nextKink); 1800 - (skip => { 1801 - $nextKink.on('click', function() { 1802 - inputKinks.inputPopup.showNext(skip); 1803 - }); 1804 - })(i); 1805 - } 1806 - }, 1807 - showPrev(skip) { 1808 - if (typeof skip !== "number") skip = 1; 1809 - var index = $popup.data('index') - skip; 1810 - var numKinks = inputKinks.inputPopup.allKinks.length; 1811 - index = (numKinks + index) % numKinks; 1812 - inputKinks.inputPopup.showIndex(index); 1813 - }, 1814 - showNext(skip) { 1815 - if (typeof skip !== "number") skip = 1; 1816 - var index = $popup.data('index') + skip; 1817 - var numKinks = inputKinks.inputPopup.allKinks.length; 1818 - index = (numKinks + index) % numKinks; 1819 - inputKinks.inputPopup.showIndex(index); 1820 - }, 1821 - show() { 1822 - inputKinks.inputPopup.allKinks = inputKinks.getAllKinks(); 1823 - inputKinks.inputPopup.showIndex(0); 1824 - $popup.fadeIn(); 1825 - } 1826 - }; 1827 - 1828 - $(window).on('keydown', function (e) { 1829 - if (e.altKey || e.shiftKey || e.ctrlKey) return; 1830 - if (!$popup.is(':visible')) return; 1831 - 1832 - let btn = -1; 1833 - 1834 - switch (e.code) { 1835 - case 'ArrowUp': 1836 - inputKinks.inputPopup.showPrev(); 1837 - e.preventDefault(); 1838 - e.stopPropagation(); 1839 - return; 1840 - case 'ArrowDown': 1841 - inputKinks.inputPopup.showNext(); 1842 - e.preventDefault(); 1843 - e.stopPropagation(); 1844 - return; 1845 - case 'Numpad0': 1846 - case 'Digit0': 1847 - btn = 0; 1848 - break; 1849 - case 'Numpad1': 1850 - case 'Digit1': 1851 - btn = 1; 1852 - break; 1853 - case 'Numpad2': 1854 - case 'Digit2': 1855 - btn = 2; 1856 - break; 1857 - case 'Numpad3': 1858 - case 'Digit3': 1859 - btn = 3; 1860 - break; 1861 - case 'Numpad4': 1862 - case 'Digit4': 1863 - btn = 4; 1864 - break; 1865 - case 'Numpad5': 1866 - case 'Digit5': 1867 - btn = 5; 1868 - break; 1869 - case 'Numpad6': 1870 - case 'Digit6': 1871 - btn = 6; 1872 - break; 1873 - case 'Numpad7': 1874 - case 'Digit7': 1875 - btn = 7; 1876 - break; 1877 - case 'Numpad8': 1878 - case 'Digit8': 1879 - btn = 8; 1880 - break; 1881 - case 'Numpad9': 1882 - case 'Digit9': 1883 - btn = 9; 1884 - break; 1885 - default: 1886 - return; 1887 - } 1888 - 1889 - var $btn = $options.find('.big-choice').eq(btn); 1890 - $btn.click(); 1891 - }); 1892 - $('#StartBtn').on('click', inputKinks.inputPopup.show); 1893 - $('#InputCurrent .closePopup, #InputOverlay').on('click', function() { 1894 - $popup.fadeOut(); 1895 - }); 1896 - })(); 1897 - }); 1898 - </script> 424 + <script src="https://gistcdn.githack.com/uwx/da1b8582cc5300b6b3d42540d738df9e/raw/7b29235ca1dd327c138317031327b60a9f1782a1/dom-tools.js"></script> 425 + <script src="https://unpkg.com/masonry-layout@4.2.2/dist/masonry.pkgd.min.js"></script> 426 + <script src="kinklist.js"></script> 427 + <script src="exporter.js"></script> 1899 428 </body> 1900 429 1901 430 </html>
+9
public/jsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "module": "commonjs", 4 + "target": "es6", 5 + "lib": ["es2019", "dom", "dom.iterable"] 6 + }, 7 + "exclude": ["node_modules", "**/node_modules/*"], 8 + "include": ["*"] 9 + }
+430
public/kinklist.js
··· 1 + /* eslint-disable unicorn/no-useless-undefined */ 2 + /* eslint-disable unicorn/prevent-abbreviations */ 3 + /* eslint-disable indent */ 4 + 5 + // @ts-check 6 + 7 + // @ts-ignore 8 + const $ = window.$d; 9 + 10 + const root = $('#root'); 11 + const legend = $('#legend'); 12 + 13 + const kinkCode = [...document.querySelectorAll('kinks')] 14 + .flatMap(e => e.textContent.split('\n')) 15 + .map(e => e.trim()) 16 + .filter(e => e); 17 + 18 + /** 19 + * @typedef {object} KinkCategory 20 + * @property {string} name 21 + * @property {string[]} kinks 22 + * @property {string[]} participants 23 + */ 24 + 25 + /** @type {KinkCategory[]} */ 26 + const kinkCategories = []; 27 + 28 + /** @type {string[]} */ 29 + const kinkNamesById = []; 30 + 31 + /** @type {[string, string, string][]} */ 32 + const choiceOptions = [ 33 + ['not-entered', 'Not Entered', '#FFFFFF'], 34 + ['favorite', 'Favorite', '#6DB5FE'], 35 + ['like', 'Like', '#23FD22'], 36 + ['okay', 'Okay', '#FDFD6B'], 37 + ['maybe', 'Maybe', '#DB6C00'], 38 + ['no', 'No', '#920000'], 39 + ['try', 'Want To Try', 'pattern'], 40 + ]; 41 + const choiceOptionIndices = Object.fromEntries(choiceOptions.map(([id, _name], i) => [id, i])); 42 + 43 + /** 44 + * Maps kink name -> participant -> choice (id string) 45 + * Entries may be undefined! 46 + * @type {Record<string, Record<string, string>>} 47 + */ 48 + const kinkSelections = {}; 49 + 50 + /** 51 + * Maps kink name -> participant -> choice option -> button element 52 + * Entries may be undefined! 53 + * @type {Record<string, Record<string, Record<string, any>>>} 54 + */ 55 + const kinkButtons = {}; 56 + 57 + /** 58 + * @param {string} name 59 + * @returns {KinkCategory | undefined} 60 + */ 61 + function findKinkCategory(name) { 62 + for (const category of kinkCategories) { 63 + const index = category.kinks.indexOf(name); 64 + if (index !== -1) { 65 + return category; 66 + } 67 + } 68 + return undefined; 69 + } 70 + 71 + for (const [type, typeDescription] of choiceOptions) { 72 + legend.append( 73 + $('<div>') 74 + .addClass('level-item') 75 + .append( 76 + $('<button>') 77 + .addClass('choice', type) 78 + .attr('title', typeDescription) 79 + .attr('disabled', '') 80 + ) 81 + .append( 82 + $('<span>') 83 + .appendText(typeDescription) 84 + ) 85 + ); 86 + } 87 + 88 + { 89 + /** 90 + * @type {{name: string, kinks: string[], participants: string[] | undefined} | undefined} 91 + */ 92 + let lastKinkCategory; 93 + let lastKinkId = 0; 94 + for (const line of kinkCode) { 95 + if (line.startsWith('#')) { 96 + if (lastKinkCategory !== undefined) { 97 + kinkCategories.push(lastKinkCategory); 98 + } 99 + lastKinkCategory = { 100 + name: line.slice(1), 101 + kinks: [], 102 + participants: undefined 103 + }; 104 + } else if (line.startsWith('(') && line.endsWith(')')) { 105 + if (lastKinkCategory === undefined) { 106 + throw new Error('Encountered a participant definition before a kink type declaration'); 107 + } 108 + lastKinkCategory.participants = line.slice(1, -1).split(',').map(e => e.trim()); 109 + } else if (line.startsWith('*')) { 110 + if (lastKinkCategory === undefined) { 111 + throw new Error('Encountered a kink definition before a kink type declaration'); 112 + } 113 + const kink = line.slice(1).trim(); 114 + lastKinkCategory.kinks.push(kink); 115 + kinkNamesById[lastKinkId++] = kink; 116 + } 117 + } 118 + 119 + if (lastKinkCategory !== undefined) { 120 + kinkCategories.push(lastKinkCategory); 121 + } 122 + } 123 + 124 + const base64Alphabet = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/']; // base64 alphabet 125 + 126 + /** 127 + * @param {number} n 128 + * @returns {string} 129 + */ 130 + const toBinary = n => n.toString(2).padStart(8, '0'); // convert num to 8-bit binary string 131 + 132 + /** 133 + * https://stackoverflow.com/a/62362724 134 + * @param {number[]} arr 135 + * @returns {string} 136 + */ 137 + function bytesArrToBase64(arr) { 138 + const l = arr.length; 139 + let result = ''; 140 + 141 + for (let i=0; i<=(l-1)/3; i++) { 142 + const c1 = i*3+1>=l; // case when "=" is on end 143 + const c2 = i*3+2>=l; // case when "=" is on end 144 + const chunk = toBinary(arr[3*i]) + toBinary(c1? 0:arr[3*i+1]) + toBinary(c2? 0:arr[3*i+2]); 145 + const r = chunk.match(/.{1,6}/g).map((x,j)=> j==3&&c2 ? '=' :(j==2&&c1 ? '=':base64Alphabet[+('0b'+x)])); 146 + result += r.join(''); 147 + } 148 + 149 + return result; 150 + } 151 + /** 152 + * https://stackoverflow.com/a/62364519 153 + * @param {string} str 154 + * @returns {number[]} 155 + */ 156 + function base64ToBytesArr(str) { 157 + const result = []; 158 + 159 + for (let i=0; i<str.length/4; i++) { 160 + const chunk = [...str.slice(4*i,4*i+4)]; 161 + const bin = chunk.map(x=> base64Alphabet.indexOf(x).toString(2).padStart(6, '0')).join(''); 162 + const bytes = bin.match(/.{1,8}/g).map(x=> +('0b'+x)); 163 + result.push(...bytes.slice(0,3 - (str[4*i+2]=='='?1:0) - (str[4*i+3]=='='?1:0))); 164 + } 165 + return result; 166 + } 167 + 168 + /** 169 + * @param {string} kink 170 + * @param {string} participant 171 + * @returns {string} 172 + */ 173 + function getSelectedKinkOrDefault(kink, participant) { 174 + return kinkSelections[kink] && kinkSelections[kink][participant] || 'not-entered'; 175 + } 176 + 177 + /** 178 + * 4-bit padding reference 179 + */ 180 + const PADDING_MARKER = 0xF; 181 + /** 182 + * @returns {string} 183 + */ 184 + function serializeChoices() { 185 + const bytes = []; 186 + 187 + let i = -1; 188 + function pushNumber(num) { 189 + if (i !== -1) { 190 + i |= num << 4; 191 + bytes.push(i); 192 + i = -1; 193 + } else { 194 + i = num; 195 + } 196 + } 197 + 198 + for (const kink of kinkNamesById) { 199 + const category = findKinkCategory(kink); 200 + for (const participant of category.participants) { 201 + pushNumber(choiceOptionIndices[getSelectedKinkOrDefault(kink, participant)]); 202 + } 203 + } 204 + 205 + if (i !== -1) { 206 + pushNumber(PADDING_MARKER); 207 + } 208 + 209 + return bytesArrToBase64(bytes); 210 + } 211 + 212 + /** 213 + * @param {string} base64 214 + */ 215 + function deserializeChoices(base64) { 216 + const bytes = base64ToBytesArr(base64); 217 + 218 + let isUpperHalf = false; 219 + let index = 0; 220 + for (const kink of kinkNamesById) { 221 + const category = findKinkCategory(kink); 222 + for (const participant of category.participants) { 223 + const byte = bytes[index]; 224 + let choice; 225 + if (isUpperHalf) { 226 + choice = byte >> 4 & 0xF; 227 + 228 + if (choice === PADDING_MARKER) { 229 + console.warn('Reached the end of data early.'); 230 + break; 231 + } 232 + 233 + isUpperHalf = false; 234 + index++; 235 + } else { 236 + choice = byte & 0xF; 237 + 238 + isUpperHalf = true; 239 + } 240 + if (!kinkSelections[kink]) kinkSelections[kink] = {}; 241 + 242 + const choiceId = choiceOptions[choice][0]; 243 + kinkSelections[kink][participant] = choiceId; 244 + 245 + for (const [thatChoiceId, thatButton] of Object.entries(kinkButtons[kink][participant])) { 246 + if (thatChoiceId === choiceId) { 247 + thatButton.addClass('selected'); 248 + } else { 249 + thatButton.removeClass('selected'); 250 + } 251 + } 252 + } 253 + } 254 + 255 + window.location.hash = serializeChoices(); 256 + } 257 + 258 + /** 259 + * @param {string} choiceId 260 + * @param {string} choiceDescription 261 + */ 262 + function createKinkChoiceButton(choiceId, choiceDescription) { 263 + // <div class="column"><button class="choice notEntered" title="Not Entered"></button></div> 264 + 265 + const button = $('<button>') 266 + .addClass('choice', choiceId) 267 + .attr('title', choiceDescription); 268 + 269 + const column = $('<div>') 270 + .addClass('column') 271 + .append(button); 272 + 273 + return {container: column, button}; 274 + } 275 + 276 + /** 277 + * @param {KinkCategory} kinkCategory 278 + * @param {string} kinkName 279 + */ 280 + function createKink(kinkCategory, kinkName) { 281 + /* 282 + <tr class="kinkRow kink-skinny"> 283 + <td> 284 + <div class="choices choice-general"> 285 + <div class="columns is-mobile is-gapless"> 286 + <div class="column"><button class="choice not-entered" title="Not Entered"></button></div> 287 + <div class="column"><button class="choice favorite" title="Favorite"></button></div> 288 + <div class="column"><button class="choice like" title="Like"></button></div> 289 + <div class="column"><button class="choice okay" title="Okay"></button></div> 290 + <div class="column"><button class="choice maybe" title="Maybe"></button></div> 291 + <div class="column"><button class="choice no" title="No"></button></div> 292 + <div class="column"><button class="choice try" title="Want To Try"></button></div> 293 + </div> 294 + </div> 295 + </td> 296 + <td>Skinny</td> 297 + </tr> 298 + */ 299 + 300 + kinkButtons[kinkName] = {}; 301 + const choices = []; 302 + 303 + for (const participant of kinkCategory.participants) { 304 + kinkButtons[kinkName][participant] = {}; 305 + const choiceButtons = []; 306 + 307 + for (const [choiceId, choiceDescription] of choiceOptions) { 308 + const {container, button} = createKinkChoiceButton(choiceId, choiceDescription); 309 + choiceButtons.push(container); 310 + 311 + kinkButtons[kinkName][participant][choiceId] = button; 312 + 313 + button.on('click', () => { 314 + if (!kinkSelections[kinkName]) kinkSelections[kinkName] = {}; 315 + kinkSelections[kinkName][participant] = choiceId; 316 + 317 + for (const [thatChoiceId, thatButton] of Object.entries(kinkButtons[kinkName][participant])) { 318 + if (thatChoiceId === choiceId) { 319 + thatButton.addClass('selected'); 320 + } else { 321 + thatButton.removeClass('selected'); 322 + } 323 + } 324 + 325 + window.location.hash = serializeChoices(); 326 + }); 327 + } 328 + 329 + choices.push($('<td>') 330 + .attr('data-choice-type', participant) 331 + .append( 332 + $('<div>') 333 + .addClass('choices choice-general') 334 + .append( 335 + $('<div>') 336 + .addClass('columns is-mobile is-gapless') 337 + .append(choiceButtons) 338 + ) 339 + ) 340 + ); 341 + } 342 + 343 + const kinkElement = $('<tr>') 344 + .addClass('kinks-row') 345 + .append(choices) 346 + .append( 347 + $('<td>') 348 + .appendText(kinkName) 349 + ); 350 + 351 + return {kinkElement, choices}; 352 + } 353 + 354 + /** 355 + * @param {KinkCategory} kinkCategory 356 + */ 357 + function createKinkCategory(kinkCategory) { 358 + /* 359 + <div class="column is-narrow"> 360 + <!--<p class="notification is-primary"> 361 + <code class="html">is-narrow</code><br> 362 + First Column 363 + </p>--> 364 + <table class="table is-striped is-narrow is-hoverable"> 365 + <thead> 366 + <th class="choicesCol">General</th> 367 + </thead> 368 + <tbody> 369 + <tr class="kinkRow kink-skinny"> 370 + <td> 371 + <div class="choices choice-general"> 372 + <div class="columns is-mobile is-gapless"> 373 + <div class="column"><button class="choice notEntered" title="Not Entered"></button></div> 374 + <div class="column"><button class="choice favorite" title="Favorite"></button></div> 375 + <div class="column"><button class="choice like" title="Like"></button></div> 376 + <div class="column"><button class="choice okay" title="Okay"></button></div> 377 + <div class="column"><button class="choice maybe" title="Maybe"></button></div> 378 + <div class="column"><button class="choice no" title="No"></button></div> 379 + <div class="column"><button class="choice try" title="Want To Try"></button></div> 380 + </div> 381 + </div> 382 + </td> 383 + <td>Skinny</td> 384 + </tr> 385 + */ 386 + 387 + const headers = kinkCategory.participants.map(e => $('<th>').addClass('kinks-header').appendText(e)); 388 + const kinks = kinkCategory.kinks.map(e => createKink(kinkCategory, e)); 389 + 390 + const kinkCategoryContainer = $('<div>') 391 + .addClass('masonry-inner') 392 + .attr('data-num-participants', kinkCategory.participants.length) 393 + .append( 394 + $('<h1>') 395 + .addClass('subtitle kinks-subtitle') 396 + .appendText(kinkCategory.name) 397 + ) 398 + .append( 399 + $('<table>') 400 + .addClass('table kinks-table is-striped is-narrow is-hoverable') 401 + .append( 402 + $('<thead>') 403 + .append(headers) 404 + ) 405 + .append( 406 + $('<tbody>') 407 + .append(kinks.map(e => e.kinkElement)) 408 + ) 409 + ); 410 + 411 + return kinkCategoryContainer; 412 + } 413 + 414 + for (const kinkCategory of kinkCategories) { 415 + root.append(createKinkCategory(kinkCategory)); 416 + } 417 + 418 + if (window.location.hash) { 419 + try { 420 + deserializeChoices(window.location.hash.slice(1)); 421 + } catch (err) { 422 + console.error('Failed to load saved kinks:', err); 423 + } 424 + } 425 + 426 + const masonry = new Masonry('.masonry', { 427 + itemSelector: '.masonry-inner', 428 + columnWidth: '.masonry-inner:first-child', 429 + percentPosition: true 430 + });