put set captcha on top level
This commit is contained in:
parent
adb30416e2
commit
d5bf34e305
1 changed files with 178 additions and 0 deletions
178
set-captcha.html
Normal file
178
set-captcha.html
Normal file
|
@ -0,0 +1,178 @@
|
|||
<!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>
|
Loading…
Reference in a new issue