diff --git a/book.toml b/book.toml index 9e1f042..8b11abd 100644 --- a/book.toml +++ b/book.toml @@ -7,6 +7,6 @@ title = "Lapwing for Beginners" [output.html] additional-css = ["theme/pagetoc.css"] -additional-js = ["theme/pagetoc.js"] +additional-js = ["theme/pagetoc.js", "theme/steno-viz.js"] [preprocessor.external-links] diff --git a/src/Chapter-01.md b/src/Chapter-01.md index 5204b83..a2ebcde 100644 --- a/src/Chapter-01.md +++ b/src/Chapter-01.md @@ -25,7 +25,7 @@ This results in 10 successive key strokes. On the other hand, steno uses *chords com/pli/cate ``` -![writing "complicate" with steno](img/1-complicate-steno.gif) + We could notate writing this word using steno like so: diff --git a/src/img/steno-outline.svg b/src/img/steno-outline.svg new file mode 100644 index 0000000..bdeed6c --- /dev/null +++ b/src/img/steno-outline.svg @@ -0,0 +1,796 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/theme/steno-viz.js b/theme/steno-viz.js new file mode 100644 index 0000000..53c82ae --- /dev/null +++ b/theme/steno-viz.js @@ -0,0 +1,167 @@ +function getThemeColors() { + const rootStyle = getComputedStyle(document.documentElement); + + // Fetch specific color variables used by mdBook + const backgroundColor = rootStyle.getPropertyValue("--fg").trim(); + const textColor = rootStyle.getPropertyValue("--bg").trim(); + + return { backgroundColor, textColor }; +} + +class SVGHelper { + constructor(svg) { + // Just create a document fragment to parse the SVG + const parser = new DOMParser(); + const doc = parser.parseFromString(svg, "image/svg+xml"); + this.shadowRoot = doc; + this.shadowRoot.querySelectorAll(".pressed > path").forEach((element) => { + element.style.stroke = getThemeColors().backgroundColor; + }); + this.shadowRoot + .querySelectorAll(".notpressed > path ") + .forEach((element) => { + element.style.stroke = getThemeColors().backgroundColor; + element.style.fill = getThemeColors().backgroundColor; + }); + this.shadowRoot.querySelectorAll(".notpressed > g ").forEach((element) => { + element.style.stroke = getThemeColors().textColor; + element.style.fill = getThemeColors().textColor; + }); + } + get string() { + const serializer = new XMLSerializer(); + return serializer.serializeToString(this.shadowRoot); + } +} + +class StenoViz extends HTMLElement { + static outline_regex = + /^(?#)?(?:(?S)?(?T)?(?K)?(?P)?(?W)?(?H)?(?R)?)?(?:(?A)?(?O)?)?(?\*)?(?:(?E)?(?U)?(?:-)?)?(?:(?F)?(?R)?(?P)?(?B)?(?L)?(?G)?(?T)?(?S)?(?D)?(?Z)?)?$/; + static steno_outline_svg_cache = null; + + static observedAttributes = [ + "stroke", + "steno_outline", + "opacity", + "noAnimate", + "delay", + "width", + "alt", + ]; + + get stroke() { + return this.getAttribute("stroke") || ""; + } + get strokes() { + return this.stroke.split("/"); + } + + get opacity() { + return this.getAttribute("opacity") || "0.0"; + } + + get animate() { + return !this.hasAttribute("noAnimate"); + } + + get delay() { + return Number(this.getAttribute("delay")) || 1000; + } + + animation_frame() { + this._animationframe = (this._animationframe + 1) % this.strokes.length; + return this._animationframe; + } + + async steno_outline() { + if (this.getAttribute("steno_outline")) { + const response = await fetch(this.getAttribute("steno_outline")); + const text = await response.text(); + return text; + } + if (!StenoViz.steno_outline_svg_cache) { + const response = await fetch("img/steno-outline.svg"); + StenoViz.steno_outline_svg_cache = await response.text(); + } + return StenoViz.steno_outline_svg_cache; + } + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.observer = new MutationObserver(() => { + this.connectedCallback(); + }); + this._timestamp = 0; + this._animation_frame = 0; + this.render_loop = this.render_loop.bind(this); + } + + async connectedCallback() { + // Start observing the HTML element for class changes + this.observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + if (this.animate && this.strokes.length > 0) { + requestAnimationFrame(this.render_loop); + } else { + html = ""; + for (const i of this.strokes) { + html = html + (await this.render(i)); + } + this.shadowRoot.innerHTML = html; + } + } + + disconnectedCallback() { + // Clean up the observer when the element is removed + this.observer.disconnect(); + } + + async attributeChangedCallback(name, oldValue, newValue) { + await this.connectedCallback(); + } + + async render(stroke) { + // Load base SVG + const svg = new SVGHelper(await this.steno_outline()); + if (this.hasAttribute("width")) { + console.log(svg.shadowRoot.documentElement); + svg.shadowRoot.documentElement.setAttribute( + "width", + this.getAttribute("width") + ); + svg.shadowRoot.documentElement.removeAttribute("height"); + } + // Create helper with pressed SVG + + // Get the matched keys + const match = stroke.match(StenoViz.outline_regex); + + // Replace elements for matched keys + if (match && match.groups) { + // Iterate through the named groups + Object.entries(match.groups).forEach(([key, value]) => { + if (!value) { + // If this key is pressed + const target = svg.shadowRoot.querySelector(`#${key}`); + target.setAttribute("opacity", this.opacity); + } + }); + } + return svg.string; + } + async render_loop(timestamp) { + const elapsed = timestamp - this._timestamp; + if (elapsed >= this.delay) { + let frame = this._animation_frame; + this.shadowRoot.innerHTML = await this.render(this.strokes[frame]); + this._timestamp = timestamp; + this._animation_frame = (this._animation_frame + 1) % this.strokes.length; + } + requestAnimationFrame(this.render_loop); + } +} + +customElements.define("steno-outline", StenoViz);