chart = {
const svg = d3.create("svg").attr("width", w).attr("height", w);
let plugboardstr = "AB HK";
let speed = 16;
let labels = html`<input type='checkbox' name='labels' id='labels' />`;
let reflectorSettings = html`<form>
<div style='font-weight:bold;margin-top:1em;'>Reflector</div>
<select name=r1>
${REFLECTORS.map((r, i) => html`<option value='${r.value}'>${r.name}</option>`)}
</select>
</form>`;
let plugboardSettings = html`<form>
<div style='font-weight:bold;margin-top:1em;'>Plugboard</div>
<input value='${plugboardstr}' type=text maxlength=20 style='width:150px;font-family:inherit;box-sizing:border-box;font-weight:bold;font-size:inherit;' />
</form>`;
let speedForm = html`<form>
<div style='font-weight:bold;margin-top:1em;'>Speed</div>
<input value=16 style='width:150px;' type=range min=1 max=16 step=5 />
</form>`;
speedForm.addEventListener("input", (e) => {
speed = e.target.valueAsNumber;
});
let rotorSettings = html`<form>
<div style='font-weight:bold;margin-top:1em;'>Rotors</div>
<select name=r1 data-rotor=0>
${ROTORS.map((r, i) => html`<option value='${r.value}'>${r.name}</option>`)}
</select>
<select name=r2 data-rotor=1>
${ROTORS.map(
(r, i) =>
html`<option ${i == 1 ? "selected" : ""} value='${r.value}'>${
r.name
}</option>`
)}
</select>
<select name=r3 data-rotor=2>
${ROTORS.map(
(r, i) =>
html`<option ${i == 2 ? "selected" : ""} value='${r.value}'>${
r.name
}</option>`
)}
</select>
</form>`;
let value = "Hello world";
let input = html`
<div style='font-weight:bold;margin-top:1em;'>Input</div>
<input id=in value='${value}' type=text maxlength=20 style='width:150px;font-family:inherit;box-sizing:border-box;font-weight:bold;font-size:inherit;' />`;
input.addEventListener("input", (e) => {
value = e.target.value;
});
let i = input.querySelector("#in");
let output = html`
<div style='font-weight:bold;margin-top:1em;'>Output</div>
<div id=out style='border:1px solid #000;'> </div>`;
let o = output.querySelector("#out");
let go = html`
<button style='margin-top:0.5em;width:150px;font-family:inherit;background:#000;color:#fff;padding:2px;box-sizing:border-box;font-weight:bold;font-size:inherit;border:none;'>Encrypt</button>`;
let ui = html`<div id=ui>
<div id=controls>
<h1>Enigma machine</h1>
<div>
${labels}<label for='labels' style='margin-left:0.2em;'>Labels</label>
</div>
${rotorSettings}
${reflectorSettings}
${plugboardSettings}
${speedForm}
${input}
${go}
${output}
<p id='explanation' style='font-size: 80%;'>
This is a simulated Enigma machine. Letters to be encrypted enter at the boundary,
move through the wire matrix, and exit.
</p>
</div>
${svg.node()}
</div>
<style>
#ui {
font-family: IBM Plex Mono;
${mobile ? "" : "display: flex;"}
font-size:15px;
}
#ui h1 {
font-size: ${mobile ? "18px" : "23px"};
font-family: IBM Plex Mono;
}
#ui svg {
flex-shrink: 0;
}
#ui #controls {
padding-right: 1vw;
flex: 1 1 auto;
}
</style>`;
yield ui;
const g = svg.append("g").attr("transform", `translate(${w / 2}, ${w / 2})`);
const topG = svg
.append("g")
.attr("transform", `translate(${w / 2}, ${w / 2})`);
const defs = svg.append("defs");
// Configurable
let reflector = new Reflector(REFLECTORS[0].value);
let reflectorLinks = makeReflectorLinks(reflector);
let rotorValues = [0, 1, 2].map((r) => ROTORS[r].value);
let plugboard,
rotors = [],
linkData;
function setRotors() {
plugboard = new Plugboard(plugboardstr);
rotors = [];
let index = 0;
for (let val of rotorValues) {
rotors.push(new Rotor(...parseRotorStr(val, 1), "A", "A"));
}
rotors.reverse();
for (let i = 0; i < rotors.length; i++) rotors[i].index = i;
linkData = [plugboard]
.concat(rotors)
.map((rotor, i) =>
rotorLinks(rotor, d3.interpolateNumber(radii[i], radii[i + 1]))
);
}
setRotors();
plugboardSettings.addEventListener("input", (e) => {
const value = e.target.value;
if (validatePlugboard(value)) {
e.target.style.backgroundColor = "white";
plugboardstr = value;
} else {
e.target.style.backgroundColor = "red";
}
setRotors();
updateRotors();
});
const line = d3
.line()
.x((d) => d.x)
.y((d) => d.y);
let rotorGroups;
console.log(linkData);
function updateRotors() {
g.selectAll("g.rotor").remove();
rotorGroups = g
.selectAll("g.rotor")
.data(rotors.concat(plugboard), (r, i) => {
return i;
})
.join((enter) => {
let g = enter.append("g").attr("class", "rotor").attr("opacity", 0.4);
g.selectAll("g.link")
.data(
(d, i) => linkData[i],
(d) => d.source
)
.join((enter) => {
let g = enter.append("g").attr("class", "link");
g.append("path")
.attr("class", "halo")
.attr("stroke", (d) => "#fff")
.attr("stroke-width", 3)
.attr("fill", "none")
.attr("d", (d) => line(d.points));
g.append("path")
.attr("class", "link")
.attr("stroke", "#000")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("d", (d) => line(d.points));
return g;
});
return g;
});
}
updateRotors();
rotorSettings.addEventListener("change", (e) => {
let value = e.target.value;
let rotor = e.target.dataset.rotor;
rotorValues[rotor] = value;
setRotors();
updateRotors();
});
reflectorSettings.addEventListener("change", (e) => {
let value = e.target.value;
reflector = new Reflector(value);
reflectorLinks = makeReflectorLinks(reflector);
rLinks
.data(reflectorLinks, (d) => d.source)
.join(
(enter) => {},
(update) => {
update
.select("path.halo")
.transition()
.delay((d, i) => i * 20)
.attr("d", (d) => d.d);
update
.select("path.line")
.transition()
.delay((d, i) => i * 20)
.attr("d", (d) => d.d);
}
);
});
let rLinks = g
.selectAll("g.reflector-link")
.data(reflectorLinks, (d) => d.source)
.join((enter) => {
let g = enter
.append("g")
.attr("class", "reflector-link")
.attr("opacity", 0.4);
g.append("path")
.attr("d", (d) => d.d)
.attr("fill", "none")
.attr("stroke", "#fff")
.attr("class", "halo")
.attr("stroke-width", 2);
g.append("path")
.attr("d", (d) => d.d)
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-width", 1);
return g;
});
reflectorSettings.addEventListener("change", (e) => {
let value = e.target.value;
reflector = new Reflector(value);
reflectorLinks = makeReflectorLinks(reflector);
rLinks
.data(reflectorLinks, (d) => d.source)
.join(
(enter) => {},
(update) => {
update
.select("path.halo")
.transition()
.delay((d, i) => i * 20)
.attr("d", (d) => d.d);
update
.select("path.line")
.transition()
.delay((d, i) => i * 20)
.attr("d", (d) => d.d);
}
);
});
let borderCircles = g
.selectAll("circle.border")
.data(radii)
.join("circle")
.attr("class", "border")
.attr("r", (d) => d)
.attr("stroke", (d, i) => (i == 0 ? "#000" : "#888"))
.attr("stroke-width", 1.5)
.attr("fill", "none");
const ids = [0, 1, 2, 3, 4].map((i) => DOM.uid(`ring-${i}`));
let ringLabels = topG
.selectAll("g.label")
.data(radii)
.join((enter) => {
let g = enter
.append("g")
.attr("class", "label")
.attr(
"transform",
(d, i) => `translate(0, ${-(radii[i] + radii[i + 1]) / 2})`
);
g.append("rect")
.attr("height", 25)
.attr("transform", "translate(-10, -14)")
.attr("width", w / 6);
g.append("text")
.attr("fill", "#fff")
.style("font-size", "1.7vw")
.attr("alignment-baseline", "middle")
.attr("font-weight", "bold")
.text((d, i) => {
return i === 0 ? "Plugboard" : i === 4 ? `Reflector` : `Rotor ${i}`;
});
return g;
})
.attr("opacity", 0);
let inputDots = topG
.selectAll("g.input")
.data(d3.range(0, 26))
.join((enter) => {
let g = enter.append("g").attr(
"transform",
(i) =>
`translate(
${Math.cos(Math.PI * 2 * (i / 26)) * radii[0]},
${Math.sin(Math.PI * 2 * (i / 26)) * radii[0]})`
);
g.append("circle")
.attr("class", "input")
.attr("r", 10)
.attr("fill", "#fff")
.attr("stroke", "#000");
g.append("text")
.text((d) => LETTERS[d])
.style("font-family", "IBM Plex Mono")
.style("font-size", "2vw")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr(
"transform",
(i) =>
`translate(
${Math.cos(Math.PI * 2 * (i / 26)) * 20},
${Math.sin(Math.PI * 2 * (i / 26)) * 20})`
);
return g;
});
// The moving dot
const dot = topG
.append("circle")
.attr("class", "focus")
.attr("r", 7)
.attr("fill", "#da1e28")
.attr("opacity", 0);
labels.addEventListener("change", (e) => {
if (e.target.checked) {
ringLabels.transition().attr("opacity", 1);
} else {
ringLabels.transition().attr("opacity", 0);
}
});
while (true) {
await new Promise((resolve) =>
go.addEventListener("click", (e) => {
o.innerHTML = " ";
go.style.background = "#aaa";
go.disabled = true;
i.disabled = true;
resolve();
})
);
const { steps, result } = encrypt(value, rotors, reflector, plugboard);
console.log(steps, result);
let link = 0;
let frame = 0;
// ****************************************
// Got new input,
// thus, new session starts.
// Prepare tracePoints
topG.selectAll(".tracePoints").remove();
document.getElementById("wordContainer").innerHTML = "";
/*
* Color map for the characters.
*/
function colorMap(i) {
return d3.schemeTableau10[i % 10];
}
/*
* Update the dot with pos,
* if rotateFlag, rotate the dot with step.pos.
*/
function updateDot(pos, rotateFlag = false, step) {
if (rotateFlag) {
dot.attr(
"transform",
`
rotate(${-(360 * (step.pos / 26)).toFixed(5)})
translate(${pos.x}, ${pos.y})
`
);
topG
.append("circle")
.attr("class", "tracePoints")
.attr("r", 2)
.attr("fill", traceColor) //"#da1e28")
.attr("opacity", 0.5)
.attr(
"transform",
`
rotate(${-(360 * (step.pos / 26)).toFixed(5)})
translate(${pos.x}, ${pos.y})
`
);
} else {
dot.attr("transform", `translate(${pos.x}, ${pos.y})`);
topG
.append("circle")
.attr("class", "tracePoints")
.attr("r", 2)
.attr("fill", traceColor) //"#da1e28")
.attr("opacity", 0.5)
.attr("transform", `translate(${pos.x}, ${pos.y})`);
}
}
let charIdx = -1,
traceColor = colorMap(charIdx);
for (let { type, ...step } of steps) {
// ****************************************
// Change the traceColor for the next character
if (type === ROTOR_STEP) {
charIdx += 1;
traceColor = colorMap(charIdx);
}
if (step.output !== undefined) {
document.getElementById("wordContainer").innerHTML =
document.getElementById("wordContainer").innerHTML += `
<span style="color: ${traceColor}">
${value[charIdx] + step.output}
</span>
`;
}
switch (type) {
case THROUGH_ROTOR: {
rotorGroups
.transition()
.attr("opacity", (d, i) => (i === step.rotor + 1 ? 1 : 0.2));
let { points, dest, rotor } = linkData[step.rotor + 1].find(
(l) => l.source === step.source
);
dot.attr("opacity", 0.5);
for (let pos of points) {
updateDot(pos, true, step);
if (++frame % speed === 0) yield ui;
}
dot.attr("opacity", 1.0);
break;
}
case ROTOR_STEP: {
await rotorGroups
.filter((d, i) => i - 1 === step.rotor)
.transition()
.duration(2000 / speed)
.attr("transform", (d, i) => {
return `rotate(${-(360 * (step.pos / 26)).toFixed(5)})`;
})
.end();
break;
}
case BACK_THROUGH_ROTOR: {
rotorGroups
.transition()
.attr("opacity", (d, i) => (i === step.rotor + 1 ? 1 : 0.2));
let { points, rotor, dest } = linkData[step.rotor + 1].find(
(l) => l.source === step.source
);
for (let pos of points.slice().reverse()) {
// dot.attr(
// "transform",
// `rotate(${-(360 * (step.pos / 26)).toFixed(5)})
// translate(${pos.x}, ${pos.y})`
// );
updateDot(pos, true, step);
if (++frame % speed === 0) yield ui;
}
break;
}
case THROUGH_REFLECTOR: {
rotorGroups.transition().attr("opacity", 0.2);
rLinks.transition().attr("opacity", 1);
let r = reflectorLinks.find((r) => r.target === step.dest);
for (let pos of r.points.slice().reverse()) {
// dot.attr("transform", `translate(${pos.x}, ${pos.y})`);
updateDot(pos);
if (++frame % speed === 0) yield ui;
}
rLinks.transition().attr("opacity", 0.2);
break;
}
case THROUGH_PLUGBOARD: {
rotorGroups
.transition()
.attr("opacity", (d, i) => (i === 0 ? 1 : 0.2));
console.log(linkData[0]);
let r = linkData[0].find((l) => l.source === step.wire);
dot.attr("opacity", 1.0);
for (let pos of r.points) {
// dot.attr("transform", `translate(${pos.x}, ${pos.y})`);
updateDot(pos);
if (++frame % speed === 0) yield ui;
}
break;
}
case BACK_THROUGH_PLUGBOARD: {
rotorGroups
.transition()
.attr("opacity", (d, i) => (i === 0 ? 1 : 0.2));
let r = linkData[0][step.wire];
for (let pos of r.points.slice().reverse()) {
// dot.attr("transform", `translate(${pos.x}, ${pos.y})`);
updateDot(pos);
if (++frame % speed === 0) yield ui;
}
o.innerText = o.innerText += step.output;
break;
}
}
yield ui;
}
go.style.background = "#000";
go.disabled = false;
i.disabled = false;
}
//yield run();
}