179 lines
5.1 KiB
HTML
179 lines
5.1 KiB
HTML
|
<!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>
|