Public
Edited
May 13, 2023
1 fork
Insert cell
Insert cell
Insert cell
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;'>&nbsp;</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 = "&nbsp;";
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();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more