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:
parent
cb16a35020
commit
122e55b1fa
7 changed files with 221 additions and 2 deletions
123
assets/scripts/rubiks/scrambler.js
Normal file
123
assets/scripts/rubiks/scrambler.js
Normal 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);
|
30
content/blog/2024/rubiks-scrambler.md
Normal file
30
content/blog/2024/rubiks-scrambler.md
Normal 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
|
4
layouts/partials/base/body-extras.html
Normal file
4
layouts/partials/base/body-extras.html
Normal 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 }}
|
61
layouts/partials/rubiks/scrambler-template.html
Normal file
61
layouts/partials/rubiks/scrambler-template.html
Normal 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>
|
1
layouts/shortcodes/rubiks/scrambler.html
Normal file
1
layouts/shortcodes/rubiks/scrambler.html
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue