From 122e55b1fa7201752080de44e4e6ecc647641ce1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 13 Nov 2024 17:06:01 -0800 Subject: [PATCH] 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 --- assets/scripts/rubiks/scrambler.js | 123 ++++++++++++++++++ content/blog/2024/rubiks-scrambler.md | 30 +++++ layouts/partials/base/body-extras.html | 4 + .../partials/rubiks/scrambler-template.html | 61 +++++++++ layouts/shortcodes/rubiks/scrambler.html | 1 + themes/resource-builders | 2 +- themes/termlite | 2 +- 7 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 assets/scripts/rubiks/scrambler.js create mode 100644 content/blog/2024/rubiks-scrambler.md create mode 100644 layouts/partials/base/body-extras.html create mode 100644 layouts/partials/rubiks/scrambler-template.html create mode 100644 layouts/shortcodes/rubiks/scrambler.html diff --git a/assets/scripts/rubiks/scrambler.js b/assets/scripts/rubiks/scrambler.js new file mode 100644 index 0000000..10a03c3 --- /dev/null +++ b/assets/scripts/rubiks/scrambler.js @@ -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); diff --git a/content/blog/2024/rubiks-scrambler.md b/content/blog/2024/rubiks-scrambler.md new file mode 100644 index 0000000..a0ac788 --- /dev/null +++ b/content/blog/2024/rubiks-scrambler.md @@ -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]. + +
+ {{< rubiks/scrambler >}} +
+ +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 +[`