2022-12-16 21:21:49 +01:00
|
|
|
const capImageSize = 50;
|
|
|
|
const halfCapImageSize = capImageSize / 2;
|
|
|
|
|
|
|
|
const numberOfCaps = 1875;
|
|
|
|
const numberOfColumns = 40;
|
|
|
|
|
|
|
|
const numberOfRows = Math.ceil(numberOfCaps / numberOfColumns);
|
|
|
|
const effectiveHeight = Math.ceil(capImageSize*Math.cos(Math.PI/6));
|
|
|
|
|
|
|
|
function sourcePath(capId) {
|
|
|
|
return "thumbnails/" + `${capId}`.padStart(4, '0') + '.jpg'
|
|
|
|
}
|
|
|
|
|
|
|
|
function createImage(position) {
|
|
|
|
return '<img src="' + sourcePath(position) + '" loading="lazy"/>'
|
|
|
|
};
|
|
|
|
|
|
|
|
function setCapImageAtPosition(capId, position) {
|
|
|
|
elements[position].src = sourcePath(capId);
|
|
|
|
capPositions[position] = capId;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCapIdAtPosition(position) {
|
|
|
|
return capPositions[position];
|
|
|
|
}
|
|
|
|
|
2022-12-16 21:35:26 +01:00
|
|
|
const imageCanvas = document.getElementById("image-canvas");
|
2022-12-16 21:21:49 +01:00
|
|
|
|
|
|
|
var capPositions = Array.from(
|
|
|
|
{length: numberOfCaps},
|
|
|
|
(_, index) => index + 1
|
|
|
|
);
|
|
|
|
|
|
|
|
imageCanvas.innerHTML += capPositions.map(createImage).join("");
|
|
|
|
|
|
|
|
var selectedPosition = null;
|
|
|
|
|
|
|
|
const elements = imageCanvas.getElementsByTagName("img");
|
2022-12-16 21:35:26 +01:00
|
|
|
const selectedCapCircle = document.getElementById("selected-cap-circle");
|
2022-12-16 21:21:49 +01:00
|
|
|
|
|
|
|
imageCanvas.addEventListener("click", function(event) {
|
2022-12-20 00:44:11 +01:00
|
|
|
const yOffset = 50;
|
2022-12-16 21:21:49 +01:00
|
|
|
const xPosition = event.clientX - imageCanvas.getBoundingClientRect().left;
|
2022-12-20 00:56:56 +01:00
|
|
|
const yPosition = event.clientY - imageCanvas.getBoundingClientRect().top - yOffset;
|
2022-12-16 21:21:49 +01:00
|
|
|
|
|
|
|
var row = Math.floor(yPosition / effectiveHeight);
|
|
|
|
|
|
|
|
var rowIsEven = (row % 2 == 0)
|
|
|
|
const adjustedX = rowIsEven ? xPosition - halfCapImageSize : xPosition;
|
|
|
|
|
|
|
|
var column = Math.floor(adjustedX / capImageSize);
|
|
|
|
|
|
|
|
const rowRemainder = yPosition % effectiveHeight - halfCapImageSize;
|
|
|
|
const columnRemainder = adjustedX % capImageSize - halfCapImageSize;
|
|
|
|
|
|
|
|
const distanceToCenter = Math.sqrt(rowRemainder**2 + columnRemainder**2);
|
|
|
|
if (distanceToCenter > halfCapImageSize && rowRemainder < 6 - halfCapImageSize) {
|
|
|
|
// Move to the top left cap
|
|
|
|
row -= 1;
|
|
|
|
|
|
|
|
if (!rowIsEven && (columnRemainder < 0)) {
|
|
|
|
// Move to the top right cap
|
|
|
|
column -= 1;
|
|
|
|
}
|
|
|
|
if (rowIsEven && (columnRemainder > 0)) {
|
|
|
|
// Move to the top left cap
|
|
|
|
column += 1;
|
|
|
|
}
|
|
|
|
rowIsEven = !rowIsEven
|
|
|
|
}
|
|
|
|
|
|
|
|
if (row < 0 || row >= numberOfRows) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (column < 0 || column >= numberOfColumns) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const currentPosition = row * numberOfColumns + column;
|
|
|
|
|
|
|
|
if (selectedPosition === null) {
|
|
|
|
selectedPosition = currentPosition;
|
|
|
|
const circlePositionX = rowIsEven ? column * capImageSize + halfCapImageSize : column * capImageSize;
|
2022-12-20 00:56:56 +01:00
|
|
|
const circlePositionY = row * effectiveHeight + yOffset;
|
2022-12-16 21:21:49 +01:00
|
|
|
selectedCapCircle.style.left = circlePositionX + "px";
|
|
|
|
selectedCapCircle.style.top = circlePositionY + "px";
|
|
|
|
selectedCapCircle.style.display = "block";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
selectedCapCircle.style.display = "none";
|
|
|
|
|
|
|
|
if (currentPosition === selectedPosition) {
|
2022-12-16 21:26:03 +01:00
|
|
|
selectedPosition = null;
|
2022-12-16 21:21:49 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Switch cap images
|
|
|
|
const currentCapId = getCapIdAtPosition(currentPosition);
|
|
|
|
const selectedCapId = getCapIdAtPosition(selectedPosition);
|
|
|
|
|
|
|
|
setCapImageAtPosition(selectedCapId, currentPosition);
|
|
|
|
setCapImageAtPosition(currentCapId, selectedPosition);
|
|
|
|
|
|
|
|
selectedPosition = null;
|
|
|
|
});
|
2022-12-20 00:44:11 +01:00
|
|
|
|
|
|
|
function averageColor(imageElement) {
|
|
|
|
|
|
|
|
var canvas = document.createElement('canvas');
|
|
|
|
var context = canvas.getContext && canvas.getContext('2d');
|
|
|
|
|
|
|
|
if (!context) {
|
|
|
|
return { r: 255, g: 0, b: 0 };
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the height and width equal
|
|
|
|
// to that of the canvas and the image
|
|
|
|
var height = canvas.height =
|
|
|
|
imageElement.naturalHeight ||
|
|
|
|
imageElement.offsetHeight ||
|
|
|
|
imageElement.height;
|
|
|
|
var width = canvas.width =
|
|
|
|
imageElement.naturalWidth ||
|
|
|
|
imageElement.offsetWidth ||
|
|
|
|
imageElement.width;
|
|
|
|
|
|
|
|
// Draw the image to the canvas
|
|
|
|
context.drawImage(imageElement, 0, 0);
|
|
|
|
|
|
|
|
// Get the data of the image
|
|
|
|
try {
|
|
|
|
var imgData = context.getImageData(0, 0, width, height);
|
|
|
|
} catch(e) {
|
|
|
|
/* security error, img on diff domain */alert('x');
|
|
|
|
return { r: 255, g: 0, b: 0 };
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the length of image data object
|
|
|
|
var length = imgData.data.length;
|
|
|
|
const pixelCount = height * width;
|
|
|
|
const halfX = width / 2;
|
|
|
|
const halfY = height / 2;
|
|
|
|
|
|
|
|
var rgb = { r: 0, g: 0, b: 0 };
|
|
|
|
var count = 0;
|
|
|
|
for (var i = 0; i < pixelCount; i += 1) {
|
|
|
|
|
|
|
|
// Exclude everything outside of circle
|
|
|
|
const x = i % width - halfX;
|
|
|
|
const y = Math.floor(i / width) - halfY;
|
|
|
|
const distanceToCenter = Math.sqrt(x**2 + y**2);
|
|
|
|
if (distanceToCenter > halfY) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sum all values of the colour
|
|
|
|
const offset = 4 * i;
|
|
|
|
rgb.r += imgData.data[offset];
|
|
|
|
rgb.g += imgData.data[offset + 1];
|
|
|
|
rgb.b += imgData.data[offset + 2];
|
|
|
|
count += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the averages
|
|
|
|
rgb.r = Math.floor(rgb.r / count);
|
|
|
|
rgb.g = Math.floor(rgb.g / count);
|
|
|
|
rgb.b = Math.floor(rgb.b / count);
|
|
|
|
|
|
|
|
return rgb;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < capPositions.length; i += 1) {
|
|
|
|
const element = elements[i];
|
|
|
|
element.onload = function() {
|
|
|
|
const rgb = averageColor(element);
|
|
|
|
const color = `rgb(${rgb.r},${rgb.g},${rgb.b})`;
|
|
|
|
element.style.backgroundColor = color;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var showColors = false;
|
|
|
|
|
|
|
|
function toggleColors() {
|
|
|
|
showColors = !showColors;
|
|
|
|
const padding = showColors ? "50px" : "0px";
|
|
|
|
for (let i = 0; i < capPositions.length; i += 1) {
|
|
|
|
elements[i].style.paddingLeft = padding;
|
|
|
|
}
|
|
|
|
}
|
2022-12-20 12:52:31 +01:00
|
|
|
|
|
|
|
function exportCapArrangement() {
|
|
|
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
a.href = URL.createObjectURL(new Blob([JSON.stringify(capPositions)], {
|
|
|
|
type: "application/json"
|
|
|
|
}));
|
|
|
|
a.setAttribute("download", "caps.json");
|
|
|
|
document.body.appendChild(a);
|
|
|
|
a.click();
|
|
|
|
document.body.removeChild(a);
|
|
|
|
URL.revokeObjectURL(a.href)
|
|
|
|
}
|
|
|
|
|
|
|
|
function resetCaps() {
|
|
|
|
// Set each cap in the data
|
|
|
|
for (let i = 0; i < capPositions.length; i++) {
|
|
|
|
setCapImageAtPosition(i+1, i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function isInt(value) {
|
|
|
|
if (isNaN(value)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
var x = parseFloat(value);
|
|
|
|
return (x | 0) === x;
|
|
|
|
}
|
|
|
|
|
|
|
|
function decodeCapFile(content) {
|
|
|
|
var caps = JSON.parse(content);
|
|
|
|
if (!Array.isArray(caps)) {
|
|
|
|
alert("Invalid file (not a JSON array)");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const capCount = capPositions.length;
|
|
|
|
if (caps.length > capPositions.length) {
|
|
|
|
alert("Invalid file (Too many entries)");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (let i = 0; i < caps.length; i++) {
|
|
|
|
const cap = caps[i];
|
|
|
|
if (!Number.isInteger(cap)) {
|
|
|
|
alert("Invalid file (Not all integers)");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (cap < 1 || cap > capCount) {
|
|
|
|
alert("Invalid file (Not all valid cap counts)");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// TODO: Check if each id is used once
|
|
|
|
}
|
|
|
|
// Set each cap in the data
|
|
|
|
for (let i = 0; i < caps.length; i++) {
|
|
|
|
const cap = caps[i];
|
|
|
|
setCapImageAtPosition(cap, i);
|
|
|
|
}
|
|
|
|
// Reset the remaining caps which may be missing from the loaded file
|
|
|
|
// since they are newer
|
|
|
|
for (let i = caps.length; i < capCount; i++) {
|
|
|
|
setCapImageAtPosition(i+1, i);
|
|
|
|
}
|
|
|
|
console.log("Cap image loaded")
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadCapArrangementFile(e) {
|
|
|
|
console.log("File loaded")
|
|
|
|
var file = e.target.files[0];
|
|
|
|
if (!file) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
|
|
var contents = e.target.result;
|
|
|
|
document.getElementById('caps-upload').value = null;
|
|
|
|
decodeCapFile(contents);
|
|
|
|
};
|
|
|
|
reader.readAsText(file);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register when user selects a file
|
|
|
|
document.getElementById('caps-upload')
|
|
|
|
.addEventListener('change', loadCapArrangementFile, false);
|