// ==UserScript==
// @name Jigidi Bingo Solver
// @namespace to.soon.userjs.jigidi
// @match https://www.jigidi.com/solve*
// @match https://www.jigidi.com/s/*
// @grant GM_getValue
// @grant GM_setValue
// @version 1.4
// @author Fox <https://github.com/f-o>
// @description Script to help solve Jigidi puzzles, by rendering columns in a colourful grid gradient, and marking each piece with numbers.
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Custom Gradients array:
var gradientsArray = {
"Rainbow": ['#FF0000', '#FFFF00', '#00FF00', '#0000FF'],
"Distinct": ['#191970', '#006400', '#ff0000', '#00ff00', '#00ffff', '#ff00ff', '#ffb6c1'],
"Rastafari": ['#1E9600', "#FFF200", "#FF0000"],
"Sublime Vivid": ['#FC466B', "#3F5EFB"],
"DanQ": ['#FF0000', '#EE82EE'],
"Instagram": ['#833ab4', "#fd1d1d", "#fcb045"],
"Hacker": ['#ff0000', "#000000", "#00ff11", "#000000", "#0077ff"]
}
const $verbose = false;
function generateSpectrum(rows, colors) {
if (!Array.isArray(colors) || colors.length < 2) {
throw new Error('Colors array must have at least two colors.');
}
const spectrum = [];
const numColors = colors.length - 1; // Number of gradients between colors
const increment = numColors / (rows - 1); // Increment between adjacent rows
for (let i = 0; i < rows; i++) {
const colorIndex = Math.floor(i * increment); // Index of the color gradient
const startColor = colors[colorIndex]; // Start color of the gradient
const endColor = colors[Math.min(colorIndex + 1, colors.length - 1)]; // End color of the gradient
const t = (i * increment) % 1; // Interpolation parameter
spectrum.push(interpolateColors(startColor, endColor, t)); // Interpolate color
}
return spectrum;
}
function interpolateColors(color1, color2, t) {
// Extract RGB components of the colors
const [r1, g1, b1] = color1.match(/\w\w/g).map(hex => parseInt(hex, 16));
const [r2, g2, b2] = color2.match(/\w\w/g).map(hex => parseInt(hex, 16));
// Interpolate RGB components
const r = Math.round(r1 + (r2 - r1) * t);
const g = Math.round(g1 + (g2 - g1) * t);
const b = Math.round(b1 + (b2 - b1) * t);
// Convert interpolated RGB components to hex format
return '#' + [r, g, b].map(component => component.toString(16).padStart(2, '0')).join('');
}
// Wait for page to load
window.addEventListener('load', function () {
// If URL ends with "solve.php", get the puzzle ID from a tag under h1.puzzle-title and redirect
if (window.location.href.endsWith('solve.php')) {
const puzzleId = document.querySelector('h1.puzzle-title').querySelector('a').href;
window.location.href = puzzleId.replace('https://www.jigidi.com/jigsaw-puzzle/', 'https://www.jigidi.com/solve/');
}
// If URL contains "/s/", get the puzzle ID from share-url and redirect
else if (window.location.href.includes('/s/')) {
// Loop through all script tags and find the one containing "ShareEmbed.url"
for (const script of document.querySelectorAll('script')) {
if (script.innerText.includes('ShareEmbed.url')) {
const puzzleId = script.innerText.match(/ShareEmbed.url = "(.+)";/)[1];
window.location.href = puzzleId;
break;
}
}
}
// Prepare Bingo Solver UI
const JigidiBingoSolver = document.createElement('div');
JigidiBingoSolver.id = 'jigidi-bingo-solver';
// Inject Bingo Solver UI after the tool info panel
const creatorElem = document.getElementById('tool-info-panel');
creatorElem.after(JigidiBingoSolver);
// Cleanup the page
const elementWithAds = document.querySelector('.show-ad');
if (elementWithAds) {
elementWithAds.classList.remove('show-ad');
if ($verbose) {
console.log('Removed element with class "show-ad".');
}
}
// Get the canvas element
const canvas = document.querySelector('canvas');
if (!canvas) {
if ($verbose) {
console.log('Canvas not found.');
}
// Sleep for 2 seconds and try again
setTimeout(() => { window.location.reload(); }, 2000);
return;
}
// Global settings
const bingoSolverSettingsGlobal = GM_getValue('bingoSolverSettingsGlobal', {
showNumbers: true,
showColours: true,
showColoursBy: 'length',
gradient: 'Rainbow',
fontSize: 26
});
// Unique jigsaw ID settings
const jigsawId = window.location.href.match(/solve\/(\w+)\//)[1];
const bingoSolverSettings = GM_getValue(`bingoSolverSettings_${jigsawId}`, { col: 1 });
// Log settings
if ($verbose) {
console.log(`Bingo Solver: jigsawId=${jigsawId}`);
console.log(`Bingo Solver: bingoSolverSettingsGlobal=${JSON.stringify(bingoSolverSettingsGlobal)}`);
console.log(`Bingo Solver: bingoSolverSettings=${JSON.stringify(bingoSolverSettings)}`);
}
const jDimensions = creatorElem.innerText.match(/(\d+)×(\d+)/);
const jCols = parseInt(jDimensions[1]);
const jRows = parseInt(jDimensions[2]);
if ($verbose) {
console.log(`Bingo Solver: jCols=${jCols} jRows=${jRows}`);
}
// Initialize an empty string to store the HTML options
let optionsHTML = '';
// Iterate over the keys of the gradientsArray object
for (const gradientName in gradientsArray) {
// Check if the current property is a direct property of the object and not inherited
if (gradientsArray.hasOwnProperty(gradientName)) {
// Create an option element with the gradient name as the value and label
optionsHTML += `<option value="${gradientName}" ${bingoSolverSettingsGlobal.gradient === gradientName ? 'selected' : ''}>${gradientName}</option>`;
}
}
JigidiBingoSolver.innerHTML = `
<hr>
<div class="hide-complete" style="margin-bottom:2rem;">
<strong>Bingo Solver</strong><br>
<p>Help with column? (0 to disable)</p>
<div class="panel-tool " style="display: flex; justify-content: space-between; gap: 1rem;" id="animated-border">
<input type="number" id="magicStripesCol" value="${bingoSolverSettings.col}" min="0" max="${jCols}" style="width: 25%; \
text-align: center !important; \
padding: 0.5rem; \
font-size: 2rem; \
font-weight: bold; \
border-radius: 0.5rem; \
background: white; \
color: black; \
border: none;">
<button title="Go!" class="btn em" id="magicStripesGo" style="width: 25%;"><span style="font-size: 2rem; font-weight: bold; cursor: pointer;">Go!</span></button>
<div title="Go +1!" class="btn em" id="magicStripesPlusOne" style="width: 50%;"><span style="font-size: 2rem; font-weight: bold; cursor: pointer;">Go +1!</span></div>
</div>
<div id="tool-settings-panel" class="panel-tool">
<label class="checkbox icon-plus">Show numbers on pieces <input type="checkbox" id="show-numbers" ${bingoSolverSettingsGlobal.showNumbers ? 'checked' : ''}><i style="${bingoSolverSettingsGlobal.showNumbers ? 'background: green;' : 'background: firebrick;'}"></i></label>
<label for="font-size" class="checkbox icon-plus">Font size: <select name="font-size" id="font-size" style="padding: 0.5rem; font-weight: bold; border-radius: 0.5rem; background: white; color: black; border: none; float: right;">
<option value="12" ${bingoSolverSettingsGlobal.fontSize === 12 ? 'selected' : ''}>12</option>
<option value="16" ${bingoSolverSettingsGlobal.fontSize === 16 ? 'selected' : ''}>16</option>
<option value="22" ${bingoSolverSettingsGlobal.fontSize === 22 ? 'selected' : ''}>22</option>
<option value="26" ${bingoSolverSettingsGlobal.fontSize === 26 ? 'selected' : ''}>26</option>
<option value="30" ${bingoSolverSettingsGlobal.fontSize === 30 ? 'selected' : ''}>30</option>
<option value="36" ${bingoSolverSettingsGlobal.fontSize === 36 ? 'selected' : ''}>36</option>
<option value="40" ${bingoSolverSettingsGlobal.fontSize === 40 ? 'selected' : ''}>40</option>
</select> </label>
<label for="gradients" class="checkbox icon-plus">Gradient: <select name="gradients" id="gradients" style="padding: 0.5rem; font-weight: bold; border-radius: 0.5rem; background: white; color: black; border: none; float: right;">
${optionsHTML}
</select> </label>
</div>
</div>
<hr>
`;
const magicStripesCol = document.getElementById('magicStripesCol');
const magicStripesGo = document.getElementById('magicStripesGo');
magicStripesGo.addEventListener('click', () => {
bingoSolverSettings.col = parseInt(magicStripesCol.value);
GM_setValue(`bingoSolverSettings_${jigsawId}`, bingoSolverSettings);
window.location.reload();
});
document.getElementById('magicStripesPlusOne').addEventListener('click', () => {
magicStripesCol.value = (parseInt(magicStripesCol.value) + 1) % (jCols + 1);
magicStripesGo.dispatchEvent(new Event('click'));
});
var fontSize = GM_getValue('bingoSolverSettingsGlobal', bingoSolverSettingsGlobal).fontSize;
// Check whether to show numbers
const showNumbers = document.getElementById('show-numbers');
showNumbers.addEventListener('change', () => {
if ($verbose) {
console.log(`Bingo Solver: showNumbers=${showNumbers.checked}`);
}
showNumbers.parentElement.querySelector('i').style.background = showNumbers.checked ? 'green' : 'firebrick';
// Store the new value
bingoSolverSettingsGlobal.showNumbers = showNumbers.checked;
GM_setValue('bingoSolverSettingsGlobal', bingoSolverSettingsGlobal);
window.location.reload();
})
// Check which gradient to use
const gradient = document.getElementById('gradients');
gradient.addEventListener('change', () => {
if ($verbose) {
console.log(`Bingo Solver: gradient=${gradient.value}`);
}
// Store the new value
bingoSolverSettingsGlobal.gradient = gradient.value;
GM_setValue('bingoSolverSettingsGlobal', bingoSolverSettingsGlobal);
window.location.reload();
})
// Check which font size to use
const fontSizeSelect = document.getElementById('font-size');
fontSizeSelect.addEventListener('change', () => {
if ($verbose) {
console.log(`Bingo Solver: fontSize=${fontSizeSelect.value}`);
}
// Store the new value
bingoSolverSettingsGlobal.fontSize = parseInt(fontSizeSelect.value);
bingoSolverSettingsGlobal.showNumbers = true;
GM_setValue('bingoSolverSettingsGlobal', bingoSolverSettingsGlobal);
window.location.reload();
})
// Check whether to show numbers
if (!bingoSolverSettingsGlobal.showNumbers) {
fontSize = 0;
}
// Generate spectrum to use
if ($verbose) {
console.log(`Bingo Solver: colors=${JSON.stringify(gradientsArray[bingoSolverSettingsGlobal.gradient])}`);
}
const spectrum = generateSpectrum(jRows, gradientsArray[bingoSolverSettingsGlobal.gradient]);
if ($verbose) {
console.log(spectrum);
}
const jColors = spectrum.map(color => `${color}`);
let jC = 0;
const targetCol = parseInt(bingoSolverSettings.col);
if (targetCol > 0) {
// Override putImageData with a manipulated version for THIS page load
CanvasRenderingContext2D.prototype.putImageData = function (imageData, dx, dy) {
const targetCol = parseInt(bingoSolverSettings.col);
const col = jC % jCols;
const row = Math.floor(jC / jCols);
if ((col + 1) === targetCol) {
// Target column: color and number multiple times
this.fillStyle = jColors[row];
if ($verbose) {
console.log("Column", col, "Row", row + 1, "Color", "https://www.color-hex.com/color/" + jColors[row % jColors.length].replace('#', ''));
}
this.fillRect(-1000, -1000, 2000, 2000);
// Font size and text
this.font = `bold ${fontSize}px sans-serif`;
const text = `${row + 1} `.repeat(100);
const x = -100;
this.fillStyle = 'black'; // Outline color
// Linewidth based on font size
this.lineWidth = fontSize / 4;
//this.lineWidth = 7; // Adjust the thickness of the outline
// Draw the outline text with a thicker stroke
this.strokeStyle = 'black'; // Set the stroke color
this.strokeText(text, x, 0); // Draw the outline text at the top
// Draw the inner text in white
this.fillStyle = 'white'; // Inner color
this.fillText(text, x, 0); // Draw the text in white at the top
// Draw the text in multiple rows
for (let i = -100; i <= 100; i++) {
const y = i * (fontSize * 1.2); // Adjust the spacing between rows
this.strokeText(text, x, y); // Outline
this.fillText(text, x, y); // Fill
}
}
else if ((col + 2) === targetCol) {
// Previous column: lightly color and number once
this.fillStyle = jColors[row % jColors.length];
this.fillRect(-1000, -1000, 2000, 2000);
// Fill with semi-transparent white
this.fillStyle = '#ffffffbb';
this.fillRect(-1000, -1000, 2000, 2000);
this.font = `bold ${fontSize}px sans-serif`;
this.fillStyle = 'black';
// Write in center, taking into account the size of the number
if (row < 10) {
this.fillText(`${row + 1}`, 0, 0);
}
else {
var textWidth = this.measureText(`${row}`).width;
this.fillText(`${row + 1}`, -textWidth / 2, 0);
}
}
else {
// Other columns: white-out
this.fillStyle = '#ffffff';
this.fillRect(-1000, -1000, 2000, 2000);
}
jC++;
}
}
});
})();