blog: Rubiks' Cube Scrambler post

Implement the rubiks-cube-scrambler custom element, including JS and template
files. Put these things in the body-extras.html partial that the termlite theme
added.

resource-builders: Update submodule commit
termlite: Update submodule commit
This commit is contained in:
Eryn Wells 2024-11-13 17:06:01 -08:00
parent cb16a35020
commit 122e55b1fa
7 changed files with 221 additions and 2 deletions

View file

@ -0,0 +1,123 @@
class RubiksCubeScrambler extends HTMLElement {
static #RandomMoveHysteresisMaxLength = 2;
#shadowRoot;
#movesListElement;
#numberOfMovesToGenerate = 25;
constructor() {
super();
this.#shadowRoot = this.attachShadow({ mode: "open" });
}
scramble() {
console.log("Randomizing Rubik's cube...");
const movesList = this.#movesListElement;
while (movesList.childElementCount > this.#numberOfMovesToGenerate) {
movesList.removeChild(movesList.lastChild);
}
let randomMoveHysteresis = [];
for (let i = 0; i < this.#numberOfMovesToGenerate; i++) {
const randomMove = this.#randomMove(randomMoveHysteresis);
let moveItem;
if (i < movesList.childElementCount) {
moveItem = movesList.children[i];
} else {
moveItem = document.createElement("li");
movesList.appendChild(moveItem);
}
moveItem.classList.add("scrambler__move");
moveItem.classList.remove("scrambler__move--start", "scrambler__move--end");
if (randomMove.includes("2")) {
moveItem.classList.add("scrambler__move--start");
} else if (randomMove.includes("'")) {
moveItem.classList.add("scrambler__move--end");
}
moveItem.innerText = randomMove;
}
}
#randomMove(hysteresis) {
const faces = "FBLRUD";
let move;
do {
move = faces.charAt(Math.floor(Math.random() * faces.length));
} while (hysteresis && hysteresis.includes(move));
if (hysteresis) {
hysteresis.unshift(move);
while (hysteresis.length > RubiksCubeScrambler.#RandomMoveHysteresisMaxLength) {
hysteresis.pop();
}
}
const modifierFactor = Math.random();
if (modifierFactor < 0.33333) {
move = "2" + move;
} else if (modifierFactor < 0.666666) {
move = move + "'";
}
return move;
}
#removeAllMoves() {
const element = this.#movesListElement;
while (element.hasChildNodes()) {
element.removeChild(element.lastChild);
}
}
// MARK: Custom Element
connectedCallback() {
let template = document.getElementById("rubiks-cube-scrambler-template");
console.assert(template, "Couldn't find RubiksCubeScrambler component template in the document");
const shadowRoot = this.#shadowRoot;
shadowRoot.appendChild(template.content.cloneNode(true));
this.#movesListElement = shadowRoot.querySelector(".scrambler__move-list");
shadowRoot
.querySelector("button[name='scramble']")
.addEventListener("click", () => this.scramble());
const patternLengthInputElement = shadowRoot.querySelector(".scrambler__pattern-length > input");
patternLengthInputElement.value = this.#numberOfMovesToGenerate;
patternLengthInputElement.addEventListener("input", event => {
try {
const integerValue = parseInt(event.target.value);
this.#numberOfMovesToGenerate = integerValue;
} catch (e) {
console.error("Non-integer value of pattern length field", e);
}
});
this.scramble();
}
attributeChangedCallback(name, oldValue, newValue) {
console.debug("RubiksCubeScrambler attribute changed", name, oldValue, newValue);
if (name === "count") {
try {
let newIntValue = parseInt(newValue);
this.#numberOfMovesToGenerate = newIntValue;
} catch (e) {
console.error("`count` attribute should have an integer value.", e);
}
}
}
}
window.customElements.define("rubiks-cube-scrambler", RubiksCubeScrambler);

View file

@ -0,0 +1,30 @@
---
title: Rubik's Cube Scrambler
date: 2024-11-13T15:34:22-08:00
tags:
- Tech
- Puzzles
- Rubik's Cube
- HTML
- JavaScript
- CSS
- Web Components
---
Here's a silly thing I made while I was home sick today. It's a widget that
produces a randomized pattern of [moves][rmoves] to scramble a 3×3 [Rubik's
Cube][rcube].
<figure class="figure--main-column figure--object">
{{< rubiks/scrambler >}}
</figure>
This thing is a [Web Component][wc]. The interactive logic lives inside a custom
[HTMLElement][htmlelement], and the content and styling are specified inside a
[`<template>`][template] element.
[rcube]: https://www.rubiks.com
[rmoves]: https://jperm.net/3x3/moves
[wc]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components
[htmlelement]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
[template]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template

View file

@ -0,0 +1,4 @@
{{ if .HasShortcode "rubiks/scrambler" }}
{{ partial "rubiks/scrambler-template.html" . }}
{{ partial "resource_builders/script.html" (dict "resource" "scripts/rubiks/scrambler.js") }}
{{ end }}

View file

@ -0,0 +1,61 @@
<template id="rubiks-cube-scrambler-template">
<ol class="scrambler__move-list"></ol>
<div class="scrambler__pattern-length">
<label for="pattern-length" class="scrambler__label">Length</label>
<input type="text" name="pattern-length">
</div>
<div class="scrambler__scramble">
<button name="scramble" class="scrambler__button">Scramble!</button>
</div>
<style>
.scrambler__move-list {
display: grid;
font-family: var(--font-family-monospace);
gap: var(--space-xxs);
grid-template-columns: repeat(7, 3ch);
list-style: none;
margin-block: 0;
padding-inline: 0;
.scrambler__move {
justify-self: center;
margin: 1px;
}
.scrambler__move--start {
justify-self: start;
}
.scrambler__move--end {
justify-self: end;
}
}
.scrambler__scramble,
.scrambler__pattern-length
{
font-size: var(--text-s);
margin-block-start: var(--space-s);
}
.scrambler__button {
font-size: var(--text-s);
}
.scrambler__label {
color: var(--text-color-secondary);
font-family: var(--font-family-monospace);
text-transform: uppercase;
&::after {
content: " = ";
}
}
input[name="pattern-length"] {
font-size: var(--text-s);
text-align: end;
width: 4ch;
}
</style>
</template>

View file

@ -0,0 +1 @@
<rubiks-cube-scrambler></rubiks-cube-scrambler>

@ -1 +1 @@
Subproject commit b9aa2d4201d986b841ff8abf97eb72b5492fa8de
Subproject commit 73e3cec7117f3a061554f7a9799285a97a284a22

@ -1 +1 @@
Subproject commit ac168d7143d0437a837549a6cf96902e30409dac
Subproject commit c4821daa6e080cc16f339884d022c243b163c0dc