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