Last active
July 7, 2022 11:27
-
-
Save sslipchenko/bb8f1ffe4437ac84dbaad44cb2c2ca48 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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