Skip to content

Instantly share code, notes, and snippets.

@sslipchenko
Last active July 7, 2022 11:27
Show Gist options
  • Save sslipchenko/bb8f1ffe4437ac84dbaad44cb2c2ca48 to your computer and use it in GitHub Desktop.
Save sslipchenko/bb8f1ffe4437ac84dbaad44cb2c2ca48 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<title>PassCard Generator</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1 class="text-center">PassCard Generator</h1>
<div class="row mb-3 mt-3">
<div class="col">
<svg id="passwords-card" class="d-block m-auto mb-3" width="85.60mm" height="53.98mm"></svg>
<div class="text-center">
<button id="passwords-generate" class="btn btn-primary">Generate</button>
<button id="passwords-export" class="btn btn-secondary">Export</button>
</div>
<label for="passwords-list" class="form-label">Passwords</label>
<textarea id="passwords-list" class="form-control" cols="20" rows="10"></textarea>
</div>
<div class="col">
<svg id="pin-codes-card" class="d-block m-auto mb-3" width="85.60mm" height="53.98mm"></svg>
<div class="text-center">
<button id="pin-codes-generate" class="btn btn-primary">Generate</button>
<button id="pin-codes-export" class="btn btn-secondary">Export</button>
</div>
<label for="pin-codes-list" class="form-label">PIN codes</label>
<textarea id="pin-codes-list" class="form-control" cols="20" rows="10"></textarea>
</div>
<button id="export-all" class="btn btn-primary mt-3">Export All</button>
</div>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="passcard.js"></script>
</body>
</html>
const colors = ["#0F0F0F", "#F60000", "#FF8C00", "#FFEE00", "#4DE94C", "#3783FF", "#4815AA"];
const digits = ['1', '2', '3', '4', '5', '6', '7'];
const names = ['B', 'R', 'O', 'Y', 'G', 'I', 'V'];
const icons = {
"bugs": "\ue4d0",
"cat": "\uf6be",
"crow": "\uf520",
"cow": "\uf6c8",
"dog": "\uf6d3",
"dove": "\uf4ba",
"fish": "\ue4f2",
"frog": "\uf52e",
"dragon": "\uf6d5",
"hippo": "\uf6ed",
"spider": "\uf717",
"horse": "\uf6f0",
"shrimp": "\ue448"
};
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M'];
function drawCard(id, secrets, exported) {
const svg = d3.select(`#${id}`);
if (svg.select(".outline").empty()) {
// @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/solid.min.css");
svg
.append("style")
.attr("type", "text/css")
.text(`
@font-face {
font-family: "Font Awesome 6 Free";
font-style: normal;
font-weight: 900;
font-display: block;
src: url(https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/webfonts/fa-solid-900.woff2) format("woff2"),
url(https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/webfonts/fa-solid-900.ttf) format("truetype")
}
text {
font-family: "Font Awesome 6 Free";
font-size: 3mm;
}
.symbol {
font-family: "Arial";
font-size: 3mm;
font-weight: bold;
}`);
svg
.append("rect")
.attr("class", "outline")
.attr("x", "0.25mm")
.attr("y", "0.25mm")
.attr("width", "84.60mm")
.attr("height", "52.98mm")
.attr("rx", "5mm")
.attr("stroke", "lightgray")
.attr("stroke-width", "0.5mm")
.attr("fill", "none");
}
const barHeight = (53.98 - 2 * 5) / colors.length;
const barWidth = 85.60 - 10;
svg
.selectAll(".bar")
.data(colors)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", "5mm")
.attr("y", function (_, i) {
return `${i * barHeight + 5}mm`;
})
.attr("width", `${barWidth}mm`)
.attr("height", `${barHeight}mm`)
.attr("fill", function (c) { return c; });
const iconWidth = barWidth / Object.keys(icons).length;
function horizontalScale(type, values) {
const top = type == 'icon' ? 4 : 52.98 - 1;
svg
.selectAll(`.${type}`)
.data(values)
.enter()
.append("text")
.attr("class", `${type}`)
.attr("x", function (_, i) {
return `${5 + i * iconWidth + iconWidth / 2 - 1.5}mm`
})
.attr("y", `${top}mm`)
.text(function (s) { return s; });
}
horizontalScale('icon', Object.values(icons));
horizontalScale('letter', letters);
function verticalScale(type, values) {
const left = type == "digit" ? 2 : 81.60;
svg
.selectAll(`.${type}`)
.data(values)
.enter()
.append("text")
.attr("class", `${type} fa`)
.attr("x", `${left}mm`)
.attr("y", function (_, i) {
return `${i * barHeight + barHeight / 2 + 1.5 + 5}mm`;
})
.text(function (s) { return s; });
}
verticalScale('digit', digits);
verticalScale('color', names);
if (secrets) {
function drawSecrets(type) {
const color = (type === 'valid' || exported) ? "#FFFFFF" : "#AAAAAA";
svg
.selectAll(`.${type}`)
.remove();
svg
.selectAll(`.${type}`)
.data(type == 'valid' ? secrets.valid : secrets.invalid)
.enter()
.append("g")
.attr("class", type)
.style("transform", function (s) {
return `translate(${s.col * iconWidth + 5}mm, ${s.row * barHeight + 5}mm)`
})
.each(function (s) {
d3.select(this)
.selectAll("text")
.data(s.text.split(""))
.enter()
.append("text")
.attr("class", "symbol")
.attr("x", function (_, i) {
return `${i * iconWidth + 2}mm`;
})
.attr("y", "4mm")
.attr("fill", color)
.text(function (s, i) { return s; });
});
}
drawSecrets('valid');
drawSecrets('invalid');
}
}
function generate(secrets, alphabet) {
const valid = [];
const slots = colors.map((_, i) => ({ row: i, start: 0, end: Object.keys(icons).length }));
while (secrets.length) {
const index = Math.floor(Math.random() * secrets.length);
const secret = secrets[index];
secrets.splice(index, 1);
for (let i = 0; i < slots.length; ++i) {
const j = Math.floor(Math.random() * (slots.length - i));
const s = slots[j];
slots[j] = slots[i];
slots[i] = s;
if (s.end - s.start >= secret.length) {
const start = Math.floor(Math.random() * (s.end - s.start - secret.length)) + s.start;
valid.push({
row: s.row,
col: start,
text: secret
});
slots.splice(i, 1);
if (s.start < start) {
slots.push({ ...s, end: start });
}
if (s.end > start + secret.length) {
slots.push({ ...s, start: start + secret.length });
}
break;
}
}
}
const invalid = slots.map(s => {
let text = "";
for (let i = s.start; i < s.end; ++i) {
text += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
}
return {
row: s.row,
col: s.start,
text: text
};
});
return { valid, invalid }
}
function getImage(type) {
return new Promise(resolve => {
const cardKey = `${type}-card`;
const svg = document.getElementById(cardKey);
const rect = svg.getBoundingClientRect();
const width = Math.floor(rect.width);
const height = Math.floor(rect.height);
const xml = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${svg.innerHTML}</svg>`;
const url_regex = /url\([^()]+\)/gm;
Promise.all(
xml.match(url_regex).map(url =>
fetch(url.substring(4, url.length - 1))
.then(response => response.blob())
.then(blob =>
new Promise(resolve => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.readAsDataURL(blob);
}))))
.then(fonts => {
const exml = xml.replace(url_regex, () => `url(${fonts.pop()})`);
const image = new Image(width, height);
image.src = `data:image/svg+xml,${encodeURIComponent(exml)}`;
image.onload = () => resolve(image);
});
});
}
const passwordAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~`!@#$%^&*()_-+={[}]|\\:;\"'<,>.?/";
const pincodeAlphabet = "0123456789";
window.addEventListener("load", () => {
["passwords", "pin-codes"].forEach(type => {
const listKey = `${type}-list`;
const cardKey = `${type}-card`;
const textarea = document.getElementById(listKey);
textarea.value = sessionStorage.getItem(listKey);
document.getElementById(`${type}-generate`)
.addEventListener("click", () => {
const alphabet = type === "passwords" ? passwordAlphabet : pincodeAlphabet;
sessionStorage.setItem(listKey, textarea.value);
const secrets = generate(textarea.value.split("\n"), alphabet);
sessionStorage.setItem(cardKey, JSON.stringify(secrets));
document.getElementById(`${type}-export`).disabled = false;
drawCard(cardKey, secrets);
});
document.getElementById(`${type}-export`)
.addEventListener("click", () => {
drawCard(cardKey, JSON.parse(sessionStorage.getItem(cardKey)), true);
getImage(type)
.then(image => {
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, image.width, image.height);
ctx.drawImage(image, 0, 0);
canvas.toBlob(blob => {
const a = document.createElement("a");
a.download = `${type}.png`;
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href);
}, "image/png");
});
});
const secrets = sessionStorage.getItem(cardKey);
document.getElementById(`${type}-export`).disabled = !secrets;
drawCard(cardKey, secrets ? JSON.parse(secrets) : undefined);
});
document.getElementById("export-all")
.addEventListener("click", () => {
Promise.all(["passwords", "pin-codes"].map(type => {
const cardKey = `${type}-card`;
drawCard(cardKey, JSON.parse(sessionStorage.getItem(cardKey)), true);
return getImage(type);
}))
.then(images => {
const canvas = document.createElement("canvas");
canvas.width = 378;
canvas.height = 567;
const ctx = canvas.getContext("2d");
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
images.forEach((image, index) => {
const half = Math.floor(canvas.height / 2);
ctx.drawImage(image,
Math.floor((canvas.width - image.width) / 2),
index * half + Math.floor((half - image.height) / 2));
});
canvas.toBlob(blob => {
const a = document.createElement("a");
a.download = "password-pin.png";
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href);
}, "image/png");
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment