public/set-captcha.html

179 lines
5.1 KiB
HTML
Raw Normal View History

2024-08-18 10:21:18 +00:00
<!DOCTYPE html>
<html>
<head>
<title>set captcha</title>
<style>
* {
box-sizing: border-box;
}
#cards {
padding: 8px;
background: #eeeeee;
display: inline-grid;
grid-template-columns: auto auto auto auto;
gap: 4px;
}
#cards > canvas {
border: solid #222222 1px;
margin: 3px;
}
#cards > canvas.selected {
margin: 0;
border: solid blue 4px;
}
</style>
</head>
<body>
<p><a href="https://en.wikipedia.org/wiki/Set_(card_game)">set</a> captcha</p>
<p id="status">select a set of 3 cards to continue</p>
<div id="cards"></div>
<script>
const cardsEl = document.getElementById("cards");
const statusEl = document.getElementById("status");
const cardColors = [0, 90, 250];
const cardShapes = ["diamond", "oval", "squiggle"];
const cardCounts = [1, 2, 3];
const cardFills = ["solid", "hollow", "striped"];
function simcolor(color) {
return `hsl(${color + Math.random() * 20 - 10}deg, ${Math.random() * 50 + 50}%, ${Math.random() * 30 + 35}%)`;
}
function makeCard(info) {
const canvas = document.createElement("canvas");
canvas.height = 200;
canvas.width = 200;
const ctx = canvas.getContext("2d");
const color = cardColors[info.colorId];
ctx.fillStyle = simcolor(color);
ctx.strokeStyle = simcolor(color);
ctx.lineWidth = 5;
ctx.lineJoin = "round";
for (let i = 0; i < cardCounts[info.countId]; i++) {
ctx.save();
ctx.translate(100, 100);
ctx.rotate(Math.random() * Math.PI * 2);
ctx.scale(Math.random() + .5, Math.random() + .5);
ctx.lineWidth = 5;
ctx.lineJoin = "round";
ctx.beginPath();
switch (cardShapes[info.shapeId]) {
case "diamond": traceDiamond(); break;
case "oval": traceOval(); break;
case "squiggle": traceSquiggle(); break;
}
switch (cardFills[info.fillId]) {
case "solid": {
ctx.globalAlpha = .8;
ctx.fill();
break;
}
case "hollow": ctx.stroke(); break;
case "striped": {
// couldn't figure out how to make stripes work
ctx.stroke();
ctx.globalAlpha = .3;
ctx.fill();
break;
}
}
ctx.restore();
}
function traceDiamond() {
ctx.moveTo(75, 0);
ctx.lineTo(150, 30);
ctx.lineTo(75, 60);
ctx.lineTo(0, 30);
ctx.lineTo(75, 0);
}
function traceOval() {
ctx.roundRect(0, 0, 150, 60, 30);
}
function traceSquiggle() {
ctx.moveTo(0, 60);
ctx.bezierCurveTo(10, 0, 20, 10, 30, 10);
ctx.bezierCurveTo(50, 10, 100, 40, 110, 30);
ctx.bezierCurveTo(120, 30, 140, 0, 150, 0);
ctx.bezierCurveTo(150, 30, 130, 60, 120, 60);
ctx.bezierCurveTo(100, 60, 50, 30, 40, 40);
ctx.bezierCurveTo(30, 40, 20, 60, 0, 60);
}
return canvas;
}
const pack = generatePack();
const selected = new Set();
for (let i = 0; i < pack.length; i++) {
const card = pack[i];
const el = makeCard(card);
el.addEventListener("click", (e) => {
const isSelected = selected.has(i);
el.classList.toggle("selected");
if (isSelected) {
selected.delete(i);
} else {
selected.add(i);
}
if (selected.size !== 3) {
statusEl.textContent = "sets must have 3 cards";
} else if (isSet([...selected].map(i => pack[i]))) {
statusEl.textContent = "nice";
} else {
statusEl.textContent = "not a set";
}
});
cardsEl.append(el);
}
function generatePack() {
// TODO: smarter set generation?
const rndId = () => Math.floor(Math.random() * 3);
const hash = (c) => c.colorId * 27 + c.shapeId * 9 + c.countId * 3 + c.fillId;
const seenCards = new Set();
const cards = [];
for (let i = 0; i < 12; i++) {
const card = {
colorId: rndId(),
shapeId: rndId(),
countId: rndId(),
fillId: rndId(),
};
if (seenCards.has(hash(card))) {
i--;
} else {
cards.push(card);
}
}
return cards;
}
function isSet(cards) {
console.log(cards);
function allSameOrDifferent(prop) {
const unique = new Set(cards.map(i => i[prop])).size;
return unique === cards.length || unique === 1;
}
return allSameOrDifferent("colorId") &&
allSameOrDifferent("shapeId") &&
allSameOrDifferent("countId") &&
allSameOrDifferent("fillId");
}
</script>
</body>
</html>