diff --git a/assets/sass/pages/blog.scss b/assets/sass/pages/blog.scss index f3dface..4e08641 100644 --- a/assets/sass/pages/blog.scss +++ b/assets/sass/pages/blog.scss @@ -216,6 +216,23 @@ line-height: 1.6; margin-bottom: 1rem; } + + a { + color: greenyellow; + text-decoration: none; + position: relative; + padding-bottom: 2px; + border-bottom: 1px dotted rgba(173, 255, 47, 0.5); + transition: all 0.3s ease; + text-shadow: 0 0 5px rgba(173, 255, 47, 0.3); + + &:hover { + border-bottom-style: solid; + border-bottom-color: rgba(173, 255, 47, 0.8); + text-shadow: 0 0 10px rgba(173, 255, 47, 0.8); + background: rgba(173, 255, 47, 0.05); + } + } } // Blog posts listing @@ -277,6 +294,7 @@ a { color: #ff9900; text-shadow: 0 0 5px rgba(255, 153, 0, 0.5); + border-bottom: 1px dotted rgba(255, 200, 47, 0.5); &::after { background: #ff9900; @@ -285,11 +303,50 @@ } } + .blog-summary, + .blogs-content { + color: #ff9900; + text-shadow: 0 0 10px rgba(255, 153, 0, 0.5); + + a { + color: #ffb13d; + text-shadow: 0 0 5px rgba(255, 162, 23, 0.5); + border-bottom: 1px dotted rgba(255, 200, 47, 0.5); + + &:hover { + border-bottom-color: orange; + text-shadow: 0 0 10px rgba(255, 217, 47, 0.8); + } + + &::after { + background: #ff9900; + box-shadow: 0 0 10px #ff9900; + } + } + + > .read-more { + color: #ff9900; + text-shadow: 0 0 10px rgba(255, 153, 0, 0.5); + + &:after { + background: #ff9900; + box-shadow: 0 0 10px rgb(255, 213, 47); + } + } + } + .tag-filter-link { background: rgba(255, 153, 0, 0.2); border-color: rgba(255, 153, 0, 0.5); color: #ff9900; text-shadow: 0 0 5px rgba(255, 153, 0, 0.5); + + &:hover { + background: rgba(255, 153, 0, 0.2); + border-color: rgba(255, 177, 60, 0.828); + box-shadow: 0 0 10px rgba(255, 164, 29, 0.4); + text-shadow: 0 0 10px rgba(255, 169, 39, 0.8); + } } } @@ -371,6 +428,7 @@ line-height: 1.6; opacity: 0.9; color: #0f0; + margin-top: 1rem; @include media-down(lg) { font-size: 0.9rem; @@ -504,6 +562,13 @@ } } } + + .blog-footer { + text-align: center; + padding: 1rem; + margin: auto; + font-size: 1rem; + } } // Tag filter navigation @@ -811,6 +876,7 @@ transition: all 0.3s ease; text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); border-radius: 3px; + line-height: 2.2rem; &:hover:not(.disabled) { background: rgba(0, 255, 0, 0.2); diff --git a/config.yml b/config.yml index 6b6a541..1873c7e 100644 --- a/config.yml +++ b/config.yml @@ -14,6 +14,11 @@ outputFormats: mediaType: "application/json" baseName: "webmentions" isPlainText: true + RSS: + mediaType: application/rss+xml + baseName: feed + isPlainText: false + rel: alternate pagination: pagerSize: 5 @@ -41,16 +46,6 @@ params: iconImageHeight: 35 iconHeight: 70 - fuseOpts: - isCaseSensitive: false - shouldSort: true - location: 0 - distance: 1000 - threshold: 0.4 - minMatchCharLength: 0 - limit: 10 - keys: ["title", "permalink", "summary", "content"] - markup: highlight: noClasses: false diff --git a/content/blog/2026-01-12-week-4-got-webmentions/index.md b/content/blog/2026-01-12-week-4-got-webmentions/index.md index 8194ce9..a4c0718 100644 --- a/content/blog/2026-01-12-week-4-got-webmentions/index.md +++ b/content/blog/2026-01-12-week-4-got-webmentions/index.md @@ -9,14 +9,19 @@ draft: false - 🗨️ Added the ability to send and receive [webmentions](https://indieweb.org/Webmention) to the blog. Haven't automated displaying of received ones yet, but I'll get there. - 🧰 Setup a little personal API to handle the guestbook, visitor counter, and aforementioned webmentions. -- 📺 Setup a little dashboard on my homelab for monitoring all of the above! +- 📺 Setup a dashboard on my homelab for monitoring all of the above. +- 🛜 Added an RSS feed for the blog. I've made my full posts available via RSS so you can consume them however you please. ## Links I Found Interesting - [iCloud Photos Downloader](https://github.com/icloud-photos-downloader/icloud_photos_downloader) and [Photos Backup Anywhere](https://photosbackup.app/) - Two useful looking things for backing up my Photos library +- [Zootopia 2](https://blog.yiningkarlli.com/2025/12/zootopia-2.html) - CG Engineer from Walt Disney Studios talks about some of the technical stuff from making Zootopia 2. + ## Music +- 📺 [Mortal Kombat x Rhythm is a Dancer](https://www.youtube.com/watch?v=vKxn6P947PE) + ## Next Week Not necessary diff --git a/content/blog/_index.md b/content/blog/_index.md index 2124d6f..710bc1b 100644 --- a/content/blog/_index.md +++ b/content/blog/_index.md @@ -3,3 +3,5 @@ title: "Blog" --- The home of my weekly updates and occasional other thoughts. + +🛜 [Available via RSS](/blog/feed.xml) diff --git a/content/blog/app-defaults-2026/index.md b/content/blog/app-defaults-2026/index.md new file mode 100644 index 0000000..a9ed154 --- /dev/null +++ b/content/blog/app-defaults-2026/index.md @@ -0,0 +1,34 @@ +--- +title: "App Defaults - 2026" +date: 2026-01-13T18:27:20Z +tags: + - meta +draft: false +--- + +I've seen a lot of these popping up around the tech space and find them very interesting to see what tools and apps are peoples go-to for various every day things. Apparently there are [over 500 posts](https://defaults.rknight.me/) and counting! Here's mine right now: + +- 📨 Mail Client - Proton Mail +- 📮 Mail Server - [Proton Mail](https://pr.tn/ref/MNB13JYX) +- 📝 Notes - Notes.app +- ✅ To-Do - Notes.app +- 📷 Phone Photo Shooting - Camera.app +- 🟦 Photo Management - Photos.app +- 📆 Calendar - Calendar.app +- 📁 Cloud File Storage - None +- 📖 RSS - [FreshRSS](https://github.com/FreshRSS/FreshRSS) +- 🙍🏻‍♂️ Contacts - Contacts.app +- 🌐 Browser - [Zen](https://zen-browser.app/) +- 💬 Chat - Discord +- 🔖 Bookmarks - Firefox Bookmarks Sync +- 📑 Read It Later - Notes.app +- 🧑‍💻 Code Editor - [VSCodium](https://vscodium.com/) +- 🛒 Shopping Lists - Notes.app +- 📰 News - Avoid at all costs +- 🎵 Music - Tidal, [High Tide](https://github.com/Nokse22/high-tide), Turntable +- 🎤 Podcasts - Apple Podcasts +- 🔐 Password Management - Proton Pass + +Apparently I dump a lot of stuff in the Notes app on my phone. It's the closest thing to hand and has almost zero barrier to entry. Also I know where all my stuff is then. + +I should probably find better ways of organising things, but you know what they say - the best system is the one that works for you. diff --git a/content/resources/button-generator/index.md b/content/resources/button-generator/index.md index 5d074e1..f17c143 100644 --- a/content/resources/button-generator/index.md +++ b/content/resources/button-generator/index.md @@ -25,6 +25,7 @@ Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x3 - 07/01/2025 - Initial release. - 08/01/2025 - Total refactor to be modular, added many more effects. - 09/01/2025 - Rewrote the bevel inset and outset border code so they look a lot nicer at the corners. Updates throughout so that multibyte (emoji!) characters should work. +- 13/01/2025 - Added ticker scrolling and flashing text options. Added a background image selector. --- diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 9e4c950..428b850 100755 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -9,7 +9,13 @@ }}{{ end }}{{ end }} - {{ hugo.Generator }} {{ $sass := resources.Get "sass/style.scss" }} {{ + + + {{ range .AlternativeOutputFormats }} + + {{ end }} + + {{ $sass := resources.Get "sass/style.scss" }} {{ $style := $sass | css.Sass | resources.Minify | resources.Fingerprint }} diff --git a/layouts/blog/single.html b/layouts/blog/single.html index b8ea142..0aa3eaa 100644 --- a/layouts/blog/single.html +++ b/layouts/blog/single.html @@ -21,6 +21,9 @@ {{ end }}
{{ .Content }}
+ {{ partial "contact-section.html" . }} diff --git a/layouts/blog/summary.html b/layouts/blog/summary.html index 50c1709..5ed05cb 100644 --- a/layouts/blog/summary.html +++ b/layouts/blog/summary.html @@ -23,5 +23,14 @@
{{ .Content }}
+ {{ else }} +
+ {{ .Summary }} + +
+ Read More +
+ +
{{ end }} diff --git a/layouts/rss.xml b/layouts/rss.xml new file mode 100644 index 0000000..2dfb093 --- /dev/null +++ b/layouts/rss.xml @@ -0,0 +1,56 @@ +{{- $authorEmail := "" }} +{{- with site.Params.author }} + {{- if reflect.IsMap . }} + {{- with .email }} + {{- $authorEmail = . }} + {{- end }} + {{- end }} +{{- end }} +{{- $authorName := "" }} +{{- with site.Params.author }} + {{- if reflect.IsMap . }} + {{- with .name }} + {{- $authorName = . }} + {{- end }} + {{- else }} + {{- $authorName = . }} + {{- end }} +{{- end }} +{{- $pctx := . }} +{{- if .IsHome }}{{ $pctx = .Site }}{{ end }} +{{- $pages := slice }} +{{- if or $.IsHome $.IsSection }} +{{- $pages = $pctx.RegularPages }} +{{- else }} +{{- $pages = $pctx.Pages }} +{{- end }} +{{- $limit := .Site.Config.Services.RSS.Limit }} +{{- if ge $limit 1 }} +{{- $pages = $pages | first $limit }} +{{- end }} +{{- printf "" | safeHTML }} + + +Blog(gish) by Dan @ ritual.sh +{{ .Permalink }} +Dan's thoughts on web and game development, tech experiments, and whatever else catches my attention in the Golden Valley. +{{ site.Language.LanguageCode }}{{ if not .Date.IsZero }} +{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} + {{- with .OutputFormats.Get "RSS" }} + {{ printf "" .Permalink .MediaType | safeHTML }} + {{- end }} + {{- range $pages }} + {{- $content := .Content }} + {{- /* Remove graph containers with all their content including canvas and script tags */ -}} + {{- $content = replaceRE `(?s)
]*>.*?
\s*` "

[Interactive graph not available in RSS - please visit the full post to view the charts]

" $content }} + +{{ .Title }} +{{ .Permalink }} +{{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} + {{- with $authorEmail }}{{ . }}{{ with $authorName }} ({{ . }}){{ end }}{{ end }} +{{ .Permalink }} +{{ $content | transform.XMLEscape | safeHTML }} + + {{- end }} +
+
\ No newline at end of file diff --git a/static/js/button-generator/button-generator-core.js b/static/js/button-generator/button-generator-core.js index 292bc4f..2c5ba45 100644 --- a/static/js/button-generator/button-generator-core.js +++ b/static/js/button-generator/button-generator-core.js @@ -174,6 +174,14 @@ export class ButtonGenerator { values[id] = element.value; } } + + // For range-dual controls, also check for -start and -end suffixed elements + const startElement = document.getElementById(`${id}-start`); + const endElement = document.getElementById(`${id}-end`); + if (startElement && endElement) { + values[`${id}-start`] = parseFloat(startElement.value); + values[`${id}-end`] = parseFloat(endElement.value); + } }); return values; diff --git a/static/js/button-generator/effects/background-external-image.js b/static/js/button-generator/effects/background-external-image.js index d3ffc9d..28af29d 100644 --- a/static/js/button-generator/effects/background-external-image.js +++ b/static/js/button-generator/effects/background-external-image.js @@ -245,12 +245,17 @@ export class ExternalImageBackgroundEffect extends ButtonEffect { apply(context, controlValues, animState, renderData) { const file = controlValues['bg-image-file']; + const bgColor = controlValues['bg-color'] || '#FFFFFF'; const fitMode = controlValues['bg-image-fit'] || 'cover'; const opacity = controlValues['bg-image-opacity'] ?? 1; const zoom = controlValues['bg-image-zoom'] ?? 100; const offsetX = controlValues['bg-image-offset-x'] ?? 50; const offsetY = controlValues['bg-image-offset-y'] ?? 50; + // Draw background color first (always, for all states) + context.fillStyle = bgColor; + context.fillRect(0, 0, renderData.width, renderData.height); + // If no file selected, fill with a placeholder color if (!file || !file.blobUrl) { context.fillStyle = '#cccccc'; diff --git a/static/js/button-generator/effects/background-solid.js b/static/js/button-generator/effects/background-solid.js index a835100..3c3b965 100644 --- a/static/js/button-generator/effects/background-solid.js +++ b/static/js/button-generator/effects/background-solid.js @@ -35,7 +35,7 @@ export class SolidBackgroundEffect extends ButtonEffect { label: "Background Color", defaultValue: "#4a90e2", showWhen: "bg-type", - description: "Solid background color", + description: "Background color (also used behind images)", }, ]; } diff --git a/static/js/button-generator/effects/flash-text.js b/static/js/button-generator/effects/flash-text.js new file mode 100644 index 0000000..bfd7a3d --- /dev/null +++ b/static/js/button-generator/effects/flash-text.js @@ -0,0 +1,88 @@ +import { ButtonEffect } from '../effect-base.js'; + +export class FlashTextEffect extends ButtonEffect { + constructor(textLineNumber = 1) { + const suffix = textLineNumber === 1 ? '' : '2'; + super({ + id: `text-flash${suffix}`, + name: `Flash Text ${textLineNumber}`, + type: textLineNumber === 1 ? 'text' : 'text2', + category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2', + renderOrder: 1, // Execute very early, before all other text effects + textLineNumber: textLineNumber + }); + this.textLineNumber = textLineNumber; + } + + defineControls() { + const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1; + const suffix = textLineNumber === 1 ? '' : '2'; + + return [ + { + id: `animate-text-flash${suffix}`, + type: 'checkbox', + label: 'Flash Visibility', + defaultValue: false + }, + { + id: `flash-range${suffix}`, + type: 'range-dual', + label: 'Visible Frame Range', + defaultValueStart: textLineNumber === 1 ? 0 : 20, + defaultValueEnd: textLineNumber === 1 ? 19 : 39, + min: 0, + max: 39, + step: 1, + showWhen: `animate-text-flash${suffix}`, + description: 'Frame range where text is visible (0-39 frames total)' + } + ]; + } + + isEnabled(controlValues) { + const suffix = this.textLineNumber === 1 ? '' : '2'; + return controlValues[`animate-text-flash${suffix}`] === true; + } + + apply(context, controlValues, animState, renderData) { + if (!animState) return; // Flash requires animation + + const suffix = this.textLineNumber === 1 ? '' : '2'; + const startFrame = controlValues[`flash-range${suffix}-start`] || 0; + const endFrame = controlValues[`flash-range${suffix}-end`] || 39; + + // Check if current frame is within visible range + const isVisible = this.isFrameVisible(animState.frame, startFrame, endFrame); + + // Store visibility state in renderData so other text effects can check it + const visibilityKey = `textFlashVisible${suffix}`; + renderData[visibilityKey] = isVisible; + + // Also set globalAlpha (even though it gets restored, it helps during this effect's lifecycle) + if (!isVisible) { + context.globalAlpha = 0; + } + } + + /** + * Check if frame is within visible range + * @param {number} frame - Current frame number (0-39) + * @param {number} startFrame - Start of visible range + * @param {number} endFrame - End of visible range + * @returns {boolean} - True if frame is in visible range + */ + isFrameVisible(frame, startFrame, endFrame) { + // Ensure start is less than or equal to end + const start = Math.min(startFrame, endFrame); + const end = Math.max(startFrame, endFrame); + + return frame >= start && frame <= end; + } +} + +// Auto-register effects for both text lines +export function register(generator) { + generator.registerEffect(new FlashTextEffect(1)); + generator.registerEffect(new FlashTextEffect(2)); +} diff --git a/static/js/button-generator/effects/rainbow-text.js b/static/js/button-generator/effects/rainbow-text.js index 5431dae..213f931 100644 --- a/static/js/button-generator/effects/rainbow-text.js +++ b/static/js/button-generator/effects/rainbow-text.js @@ -52,6 +52,18 @@ export class RainbowTextEffect extends ButtonEffect { const suffix = this.textLineNumber === 1 ? '' : '2'; + // Check if ticker is active - if so, ticker handles rendering + const tickerActive = controlValues[`animate-text-ticker${suffix}`]; + if (tickerActive) { + return; + } + + // Check flash visibility - if flash is active and text is invisible, don't render + const flashActive = controlValues[`animate-text-flash${suffix}`]; + if (flashActive && renderData[`textFlashVisible${suffix}`] === false) { + return; + } + // Get text configuration const text = controlValues[`button-text${suffix}`] || ''; if (!text || text.trim() === '') return; diff --git a/static/js/button-generator/effects/spin-text.js b/static/js/button-generator/effects/spin-text.js index 7c4b3ef..b50bada 100644 --- a/static/js/button-generator/effects/spin-text.js +++ b/static/js/button-generator/effects/spin-text.js @@ -61,6 +61,19 @@ export class SpinTextEffect extends ButtonEffect { apply(context, controlValues, animState, renderData) { const suffix = this.textLineNumber === 1 ? "" : "2"; + + // Check if ticker is active - if so, ticker handles rendering + const tickerActive = controlValues[`animate-text-ticker${suffix}`]; + if (tickerActive) { + return; + } + + // Check flash visibility - if flash is active and text is invisible, don't render + const flashActive = controlValues[`animate-text-flash${suffix}`]; + if (flashActive && renderData[`textFlashVisible${suffix}`] === false) { + return; + } + const text = controlValues[`button-text${suffix}`] || ""; if (!text || text.trim() === '') return; diff --git a/static/js/button-generator/effects/text-shadow.js b/static/js/button-generator/effects/text-shadow.js index 1d7a882..09b141d 100644 --- a/static/js/button-generator/effects/text-shadow.js +++ b/static/js/button-generator/effects/text-shadow.js @@ -98,6 +98,12 @@ export class TextShadowEffect extends ButtonEffect { apply(context, controlValues, animState, renderData) { const suffix = this.textLineNumber === 1 ? "" : "2"; + // Check flash visibility - if flash is active and text is invisible, don't render + const flashActive = controlValues[`animate-text-flash${suffix}`]; + if (flashActive && renderData[`textFlashVisible${suffix}`] === false) { + return; + } + const text = controlValues[`button-text${suffix}`] || ""; if (!text) return; diff --git a/static/js/button-generator/effects/text-standard.js b/static/js/button-generator/effects/text-standard.js index fb90f87..c1ebf71 100644 --- a/static/js/button-generator/effects/text-standard.js +++ b/static/js/button-generator/effects/text-standard.js @@ -150,13 +150,20 @@ export class StandardTextEffect extends ButtonEffect { const waveActive = controlValues[`animate-text-wave${suffix}`]; const rainbowActive = controlValues[`animate-text-rainbow${suffix}`]; const spinActive = controlValues[`animate-text-spin${suffix}`]; + const tickerActive = controlValues[`animate-text-ticker${suffix}`]; - return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive; + return text && text.trim() !== "" && !waveActive && !rainbowActive && !spinActive && !tickerActive; } apply(context, controlValues, animState, renderData) { const suffix = this.textLineNumber === 1 ? "" : "2"; + // Check flash visibility - if flash is active and text is invisible, don't render + const flashActive = controlValues[`animate-text-flash${suffix}`]; + if (flashActive && renderData[`textFlashVisible${suffix}`] === false) { + return; + } + const text = controlValues[`button-text${suffix}`]; if (!text) return; diff --git a/static/js/button-generator/effects/ticker-text.js b/static/js/button-generator/effects/ticker-text.js new file mode 100644 index 0000000..360f130 --- /dev/null +++ b/static/js/button-generator/effects/ticker-text.js @@ -0,0 +1,312 @@ +import { ButtonEffect } from '../effect-base.js'; + +/** + * Ticker text animation effect + * Makes text scroll across the button in various directions with seamless looping + */ +export class TickerTextEffect extends ButtonEffect { + constructor(textLineNumber = 1) { + const suffix = textLineNumber === 1 ? '' : '2'; + super({ + id: `text-ticker${suffix}`, + name: `Ticker Text ${textLineNumber}`, + type: textLineNumber === 1 ? 'text' : 'text2', + category: textLineNumber === 1 ? 'Text Line 1' : 'Text Line 2', + renderOrder: 12, // After wave(10) and spin(8), before shadow(19) and standard(20) + textLineNumber: textLineNumber + }); + this.textLineNumber = textLineNumber; + } + + defineControls() { + const textLineNumber = this.textLineNumber || this.config?.textLineNumber || 1; + const suffix = textLineNumber === 1 ? '' : '2'; + return [ + { + id: `animate-text-ticker${suffix}`, + type: 'checkbox', + label: 'Ticker Scroll', + defaultValue: false + }, + { + id: `ticker-direction${suffix}`, + type: 'select', + label: 'Scroll Direction', + defaultValue: 'left', + options: [ + { value: 'left', label: 'Right to Left' }, + { value: 'right', label: 'Left to Right' }, + { value: 'up', label: 'Down to Up' }, + { value: 'down', label: 'Up to Down' } + ], + showWhen: `animate-text-ticker${suffix}`, + description: 'Direction of text scrolling' + } + ]; + } + + isEnabled(controlValues) { + const suffix = this.textLineNumber === 1 ? '' : '2'; + return controlValues[`animate-text-ticker${suffix}`] === true; + } + + apply(context, controlValues, animState, renderData) { + if (!animState) return; // Ticker requires animation + + const suffix = this.textLineNumber === 1 ? '' : '2'; + + // Check flash visibility - if flash is active and text is invisible, don't render + const flashActive = controlValues[`animate-text-flash${suffix}`]; + if (flashActive && renderData[`textFlashVisible${suffix}`] === false) { + return; + } + + // Get text configuration + const text = controlValues[`button-text${suffix}`] || ''; + if (!text || text.trim() === '') return; + + const fontSize = controlValues[`font-size${suffix}`] || 12; + const fontWeight = controlValues[`font-bold${suffix}`] ? 'bold' : 'normal'; + const fontStyle = controlValues[`font-italic${suffix}`] ? 'italic' : 'normal'; + const fontFamily = controlValues[`font-family${suffix}`] || 'Arial'; + + const baseX = (controlValues[`text${suffix}-x`] / 100) * renderData.width; + const baseY = (controlValues[`text${suffix}-y`] / 100) * renderData.height; + + const direction = controlValues[`ticker-direction${suffix}`] || 'left'; + + // Set font + context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + + // Check if other effects are active + const waveActive = controlValues[`animate-text-wave${suffix}`]; + const spinActive = controlValues[`animate-text-spin${suffix}`]; + const rainbowActive = controlValues[`animate-text-rainbow${suffix}`]; + + // Split text into grapheme clusters (handles emojis properly) + const chars = this.splitGraphemes(text); + + // Measure total width + const totalWidth = context.measureText(text).width; + + // Calculate scroll parameters - SIMPLIFIED + const horizontal = direction === 'left' || direction === 'right'; + const gapSize = 50; // Gap between text repetitions + + // For ticker to work: text must scroll across full screen width PLUS its own width PLUS gap + // This ensures text fully enters, crosses, and exits with proper spacing + const copySpacing = horizontal + ? (renderData.width + totalWidth + gapSize) + : (renderData.height + fontSize * 2 + gapSize); + + // For a seamless loop, offset scrolls through one full copy spacing in 40 frames + // At frame 0: offset = 0 + // At frame 39: offset approaches copySpacing (ready to wrap to next copy) + const offset = animState.progress * copySpacing; + + // Apply direction + const scrollOffset = { + x: direction === 'left' ? -offset : (direction === 'right' ? offset : 0), + y: direction === 'up' ? -offset : (direction === 'down' ? offset : 0) + }; + + // Calculate how many copies we need to fill the screen + const numCopies = horizontal + ? Math.ceil(renderData.width / copySpacing) + 3 + : Math.ceil(renderData.height / copySpacing) + 3; + + // Get colors + const colors = this.getTextColors(context, controlValues, text, baseX, baseY, fontSize, animState, rainbowActive); + + // Set ticker active flag so other effects can skip rendering if needed + renderData[`tickerActive${suffix}`] = true; + + // Render scrolling text + this.renderScrollingText( + context, chars, scrollOffset, numCopies, + totalWidth, fontSize, copySpacing, horizontal, direction, + { wave: waveActive, spin: spinActive, rainbow: rainbowActive }, + controlValues, animState, renderData, colors + ); + } + + /** + * Split text into grapheme clusters (emoji-safe) + */ + splitGraphemes(text) { + if (typeof Intl !== 'undefined' && Intl.Segmenter) { + const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' }); + return Array.from(segmenter.segment(text), s => s.segment); + } else { + // Fallback: spread operator handles basic emoji + return [...text]; + } + } + + /** + * Render scrolling text with multiple copies for seamless looping + */ + renderScrollingText( + context, chars, scrollOffset, numCopies, + totalWidth, fontSize, copySpacing, horizontal, direction, + effects, controlValues, animState, renderData, colors + ) { + const suffix = this.textLineNumber === 1 ? '' : '2'; + + // Get wave parameters if active + let waveAmplitude, waveSpeed; + if (effects.wave) { + waveAmplitude = controlValues[`wave-amplitude${suffix}`] || 3; + waveSpeed = controlValues[`wave-speed${suffix}`] || 1; + } + + // Get spin parameters if active + let spinSpeed, spinStagger; + if (effects.spin) { + spinSpeed = controlValues[`spin-speed${suffix}`] || 1; + spinStagger = controlValues[`spin-stagger${suffix}`] || 0.3; + } + + // Get base positioning from controls + const baseX = (controlValues[`text${suffix}-x`] / 100) * renderData.width; + const baseY = (controlValues[`text${suffix}-y`] / 100) * renderData.height; + + // Loop through copies - render multiple instances for seamless wrap + for (let copy = 0; copy < numCopies; copy++) { + // Each copy is spaced by copySpacing (which includes text width + gap) + const copyOffsetX = horizontal ? copy * copySpacing : 0; + const copyOffsetY = !horizontal ? copy * copySpacing : 0; + + // Calculate starting position based on direction + // The key: text should be fully OFF screen before appearing on the other side + let startX, startY; + + if (direction === 'left') { + // Right to left: Position so after scrolling copySpacing left, text fully exits + // copySpacing = totalWidth + gap + // Start with left edge at: copySpacing (so right edge is at copySpacing + totalWidth) + // After scrolling copySpacing left: right edge is at totalWidth (still need to exit more!) + // Actually: start at renderData.width so left edge begins at right screen edge + startX = renderData.width; + } else if (direction === 'right') { + // Left to right: Start with RIGHT edge of text at left edge of screen + // Text scrolls right, exits when LEFT edge reaches right edge of screen + startX = -totalWidth; + } else if (direction === 'up') { + // Down to up: Start off-screen below + startX = baseX - totalWidth / 2; // Center the text horizontally + startY = renderData.height; + } else { // down + // Up to down: Start off-screen above + startX = baseX - totalWidth / 2; // Center the text horizontally + startY = -fontSize * 2; + } + + // Calculate current position with scroll offset and copy offset + let currentX = startX + scrollOffset.x + copyOffsetX; + let currentY = horizontal ? baseY : (startY + scrollOffset.y + copyOffsetY); + + // Render each character + for (let i = 0; i < chars.length; i++) { + const char = chars[i]; + const charWidth = context.measureText(char).width; + const charCenterX = currentX + charWidth / 2; + let charY = currentY; + + // Apply wave effect if active + let waveY = 0; + if (effects.wave) { + const phase = animState.getPhase(waveSpeed); + const charOffset = i / chars.length; + waveY = Math.sin(phase + charOffset * Math.PI * 2) * waveAmplitude; + } + + // Apply spin effect if active + if (effects.spin) { + const phase = animState.getPhase(spinSpeed); + const rotation = (phase + i * spinStagger * Math.PI * 2) % (Math.PI * 2); + + context.save(); + context.translate(charCenterX, charY + waveY); + context.rotate(rotation); + + // Draw outline if enabled + if (controlValues[`text${suffix}-outline`]) { + context.strokeStyle = colors.strokeStyle; + context.lineWidth = 2; + context.strokeText(char, 0, 0); + } + + // Draw character + context.fillStyle = colors.fillStyle; + context.fillText(char, 0, 0); + context.restore(); + } else { + // No spin - draw normally with wave offset + const finalY = charY + waveY; + + // Draw outline if enabled + if (controlValues[`text${suffix}-outline`]) { + context.strokeStyle = colors.strokeStyle; + context.lineWidth = 2; + context.strokeText(char, charCenterX, finalY); + } + + // Draw character + context.fillStyle = colors.fillStyle; + context.fillText(char, charCenterX, finalY); + } + + currentX += charWidth; + } + } + } + + /** + * Get text colors (solid, gradient, or rainbow) + */ + getTextColors(context, controlValues, text, x, y, fontSize, animState, rainbowActive) { + const suffix = this.textLineNumber === 1 ? '' : '2'; + + let fillStyle, strokeStyle; + + // Check if rainbow text is also enabled + if (animState && rainbowActive) { + const speed = controlValues[`text-rainbow-speed${suffix}`] || 1; + const hue = (animState.progress * speed * 360) % 360; + fillStyle = `hsl(${hue}, 80%, 60%)`; + strokeStyle = `hsl(${hue}, 80%, 30%)`; + } else { + const colorType = controlValues[`text${suffix}-color-type`] || 'solid'; + + if (colorType === 'solid') { + fillStyle = controlValues[`text${suffix}-color`] || '#ffffff'; + strokeStyle = controlValues[`outline${suffix}-color`] || '#000000'; + } else { + // Gradient + const angle = (controlValues[`text${suffix}-gradient-angle`] || 0) * (Math.PI / 180); + const textWidth = context.measureText(text).width; + const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2; + const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2; + const x2 = x + textWidth / 2 - (Math.cos(angle) * textWidth) / 2; + const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2; + + const gradient = context.createLinearGradient(x1, y1, x2, y2); + gradient.addColorStop(0, controlValues[`text${suffix}-gradient-color1`] || '#ffffff'); + gradient.addColorStop(1, controlValues[`text${suffix}-gradient-color2`] || '#00ffff'); + fillStyle = gradient; + strokeStyle = controlValues[`outline${suffix}-color`] || '#000000'; + } + } + + return { fillStyle, strokeStyle }; + } +} + +// Auto-register effect +export function register(generator) { + generator.registerEffect(new TickerTextEffect(1)); + generator.registerEffect(new TickerTextEffect(2)); +} diff --git a/static/js/button-generator/effects/wave-text.js b/static/js/button-generator/effects/wave-text.js index 61e097c..f764d25 100644 --- a/static/js/button-generator/effects/wave-text.js +++ b/static/js/button-generator/effects/wave-text.js @@ -63,6 +63,18 @@ export class WaveTextEffect extends ButtonEffect { const suffix = this.textLineNumber === 1 ? '' : '2'; + // Check if ticker is active - if so, ticker handles rendering + const tickerActive = controlValues[`animate-text-ticker${suffix}`]; + if (tickerActive) { + return; + } + + // Check flash visibility - if flash is active and text is invisible, don't render + const flashActive = controlValues[`animate-text-flash${suffix}`]; + if (flashActive && renderData[`textFlashVisible${suffix}`] === false) { + return; + } + // Get text configuration const text = controlValues[`button-text${suffix}`] || ''; if (!text || text.trim() === '') return; diff --git a/static/js/button-generator/main.js b/static/js/button-generator/main.js index 030236d..695507d 100644 --- a/static/js/button-generator/main.js +++ b/static/js/button-generator/main.js @@ -25,7 +25,9 @@ import * as standardText from "./effects/text-standard.js"; import * as textShadow from "./effects/text-shadow.js"; import * as waveText from "./effects/wave-text.js"; import * as rainbowText from "./effects/rainbow-text.js"; +import * as flashText from "./effects/flash-text.js"; import * as spinText from "./effects/spin-text.js"; +import * as tickerText from "./effects/ticker-text.js"; import * as glitch from "./effects/glitch.js"; import * as pulse from "./effects/pulse.js"; import * as shimmer from "./effects/shimmer.js"; @@ -101,7 +103,9 @@ async function setupApp() { textShadow.register(generator); waveText.register(generator); rainbowText.register(generator); + flashText.register(generator); spinText.register(generator); + tickerText.register(generator); glitch.register(generator); pulse.register(generator); shimmer.register(generator); diff --git a/static/js/button-generator/ui-builder.js b/static/js/button-generator/ui-builder.js index 2ab5e1d..ed79f49 100644 --- a/static/js/button-generator/ui-builder.js +++ b/static/js/button-generator/ui-builder.js @@ -16,8 +16,8 @@ export class UIBuilder { */ setupTooltip() { // Wait for DOM to be ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { this.createTooltipElement(); }); } else { @@ -26,8 +26,8 @@ export class UIBuilder { } createTooltipElement() { - this.tooltip = document.createElement('div'); - this.tooltip.className = 'control-tooltip'; + this.tooltip = document.createElement("div"); + this.tooltip.className = "control-tooltip"; this.tooltip.style.cssText = ` position: fixed; background: linear-gradient(135deg, rgba(0, 120, 200, 0.98) 0%, rgba(0, 100, 180, 0.98) 100%); @@ -57,20 +57,20 @@ export class UIBuilder { clearTimeout(this.tooltipTimeout); this.tooltip.textContent = text; - this.tooltip.style.opacity = '1'; + this.tooltip.style.opacity = "1"; // Position tooltip above the element const rect = element.getBoundingClientRect(); // Set initial position to measure - this.tooltip.style.left = '0px'; - this.tooltip.style.top = '0px'; - this.tooltip.style.visibility = 'hidden'; - this.tooltip.style.display = 'block'; + this.tooltip.style.left = "0px"; + this.tooltip.style.top = "0px"; + this.tooltip.style.visibility = "hidden"; + this.tooltip.style.display = "block"; const tooltipRect = this.tooltip.getBoundingClientRect(); - this.tooltip.style.visibility = 'visible'; + this.tooltip.style.visibility = "visible"; let left = rect.left + rect.width / 2 - tooltipRect.width / 2; let top = rect.top - tooltipRect.height - 10; @@ -96,7 +96,7 @@ export class UIBuilder { if (!this.tooltip) return; clearTimeout(this.tooltipTimeout); this.tooltipTimeout = setTimeout(() => { - this.tooltip.style.opacity = '0'; + this.tooltip.style.opacity = "0"; }, 100); } @@ -106,17 +106,17 @@ export class UIBuilder { addTooltipHandlers(element, description) { if (!description) return; - element.addEventListener('mouseenter', () => { + element.addEventListener("mouseenter", () => { this.showTooltip(element, description); }); - element.addEventListener('mouseleave', () => { + element.addEventListener("mouseleave", () => { this.hideTooltip(); }); - element.addEventListener('mousemove', () => { + element.addEventListener("mousemove", () => { // Update position on mouse move for better following - if (this.tooltip && this.tooltip.style.opacity === '1') { + if (this.tooltip && this.tooltip.style.opacity === "1") { this.showTooltip(element, description); } }); @@ -128,7 +128,7 @@ export class UIBuilder { */ buildUI(effects) { // Clear existing content - this.container.innerHTML = ''; + this.container.innerHTML = ""; this.controlGroups.clear(); // Group effects by category @@ -148,8 +148,8 @@ export class UIBuilder { categorizeEffects(effects) { const categories = new Map(); - effects.forEach(effect => { - const category = effect.category || 'Other'; + effects.forEach((effect) => { + const category = effect.category || "Other"; if (!categories.has(category)) { categories.set(category, []); } @@ -159,17 +159,17 @@ export class UIBuilder { // Sort categories in a logical order const orderedCategories = new Map(); const categoryOrder = [ - 'Text Line 1', - 'Text Line 2', - 'Background', - 'Background Animations', - 'Border', - 'Visual Effects', - 'General Effects', - 'Special Effects' + "Text Line 1", + "Text Line 2", + "Background", + "Background Animations", + "Border", + "Visual Effects", + "General Effects", + "Special Effects", ]; - categoryOrder.forEach(cat => { + categoryOrder.forEach((cat) => { if (categories.has(cat)) { orderedCategories.set(cat, categories.get(cat)); } @@ -191,29 +191,29 @@ export class UIBuilder { * @param {Array} effects - Effects in this category */ createControlGroup(category, effects) { - const groupDiv = document.createElement('div'); - groupDiv.className = 'control-group'; + const groupDiv = document.createElement("div"); + groupDiv.className = "control-group"; // Create header - const header = document.createElement('h3'); - header.className = 'control-group-header'; + const header = document.createElement("h3"); + header.className = "control-group-header"; header.innerHTML = ` ${category} `; // Create content container - const content = document.createElement('div'); - content.className = 'control-group-content'; + const content = document.createElement("div"); + content.className = "control-group-content"; // Add controls for each effect in this category - effects.forEach(effect => { + effects.forEach((effect) => { this.addEffectControls(content, effect); }); // Add click handler for collapsing - header.addEventListener('click', () => { - groupDiv.classList.toggle('collapsed'); + header.addEventListener("click", () => { + groupDiv.classList.toggle("collapsed"); }); groupDiv.appendChild(header); @@ -229,7 +229,7 @@ export class UIBuilder { * @param {ButtonEffect} effect - Effect to create controls for */ addEffectControls(container, effect) { - effect.controls.forEach(control => { + effect.controls.forEach((control) => { const controlEl = this.createControl(control); if (controlEl) { container.appendChild(controlEl); @@ -243,26 +243,85 @@ export class UIBuilder { * @returns {HTMLElement} */ createControl(controlDef) { - const { id, type, label, defaultValue, min, max, step, options, showWhen, description } = controlDef; + const { + id, + type, + label, + defaultValue, + min, + max, + step, + options, + showWhen, + description, + } = controlDef; switch (type) { - case 'checkbox': - return this.createCheckbox(id, label, defaultValue, showWhen, description); + case "checkbox": + return this.createCheckbox( + id, + label, + defaultValue, + showWhen, + description, + ); - case 'range': - return this.createRange(id, label, defaultValue, min, max, step, description, showWhen); + case "range": + return this.createRange( + id, + label, + defaultValue, + min, + max, + step, + description, + showWhen, + ); - case 'color': + case "range-dual": + return this.createRangeDual( + id, + label, + controlDef.defaultValueStart, + controlDef.defaultValueEnd, + min, + max, + step, + description, + showWhen, + ); + + case "color": return this.createColor(id, label, defaultValue, showWhen, description); - case 'select': - return this.createSelect(id, label, defaultValue, options, showWhen, description); + case "select": + return this.createSelect( + id, + label, + defaultValue, + options, + showWhen, + description, + ); - case 'text': - return this.createTextInput(id, label, defaultValue, showWhen, description); + case "text": + return this.createTextInput( + id, + label, + defaultValue, + showWhen, + description, + ); - case 'file': - return this.createFileInput(id, label, defaultValue, showWhen, description, controlDef.accept); + case "file": + return this.createFileInput( + id, + label, + defaultValue, + showWhen, + description, + controlDef.accept, + ); default: console.warn(`Unknown control type: ${type}`); @@ -274,22 +333,22 @@ export class UIBuilder { * Create a checkbox control */ createCheckbox(id, label, defaultValue, showWhen, description) { - const wrapper = document.createElement('label'); - wrapper.className = 'checkbox-label'; + const wrapper = document.createElement("label"); + wrapper.className = "checkbox-label"; - const input = document.createElement('input'); - input.type = 'checkbox'; + const input = document.createElement("input"); + input.type = "checkbox"; input.id = id; input.checked = defaultValue || false; - const span = document.createElement('span'); + const span = document.createElement("span"); span.textContent = label; wrapper.appendChild(input); wrapper.appendChild(span); if (showWhen) { - wrapper.style.display = 'none'; + wrapper.style.display = "none"; wrapper.dataset.showWhen = showWhen; } @@ -303,14 +362,14 @@ export class UIBuilder { * Create a range slider control */ createRange(id, label, defaultValue, min, max, step, description, showWhen) { - const container = document.createElement('div'); + const container = document.createElement("div"); - const labelEl = document.createElement('label'); + const labelEl = document.createElement("label"); labelEl.htmlFor = id; labelEl.innerHTML = `${label}: ${defaultValue}`; - const input = document.createElement('input'); - input.type = 'range'; + const input = document.createElement("input"); + input.type = "range"; input.id = id; input.min = min !== undefined ? min : 0; input.max = max !== undefined ? max : 100; @@ -320,7 +379,7 @@ export class UIBuilder { } // Update value display on input - input.addEventListener('input', () => { + input.addEventListener("input", () => { const valueDisplay = document.getElementById(`${id}-value`); if (valueDisplay) { valueDisplay.textContent = input.value; @@ -331,7 +390,7 @@ export class UIBuilder { container.appendChild(input); if (showWhen) { - container.style.display = 'none'; + container.style.display = "none"; container.dataset.showWhen = showWhen; } @@ -341,26 +400,239 @@ export class UIBuilder { return container; } + /** + * Create a dual-range slider control (two handles on one track) + */ + createRangeDual( + id, + label, + defaultValueStart, + defaultValueEnd, + min, + max, + step, + description, + showWhen, + ) { + const container = document.createElement("div"); + container.className = "range-dual-container"; + + const labelEl = document.createElement("label"); + labelEl.innerHTML = `${label}: ${defaultValueStart}-${defaultValueEnd}`; + + // Create wrapper for the dual range slider + const sliderWrapper = document.createElement("div"); + sliderWrapper.className = "range-dual-wrapper"; + sliderWrapper.style.cssText = ` + position: relative; + height: 30px; + margin: 10px 0; + `; + + // Create the track + const track = document.createElement("div"); + track.className = "range-dual-track"; + track.style.cssText = ` + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + height: 4px; + background: #444; + border-radius: 2px; + z-index: 1; + `; + + // Create the filled range indicator + const range = document.createElement("div"); + range.className = "range-dual-range"; + range.id = `${id}-range`; + range.style.cssText = ` + position: absolute; + top: 50%; + transform: translateY(-50%); + height: 4px; + background: linear-gradient(90deg, #ff9966, #00bcf2); // Change this to orange + border-radius: 2px; + pointer-events: none; + z-index: 10; + `; + + // Create start handle input + const inputStart = document.createElement("input"); + inputStart.type = "range"; + inputStart.id = `${id}-start`; + inputStart.className = "range-dual-input"; + inputStart.min = min !== undefined ? min : 0; + inputStart.max = max !== undefined ? max : 100; + inputStart.value = + defaultValueStart !== undefined ? defaultValueStart : min || 0; + if (step !== undefined) { + inputStart.step = step; + } + inputStart.style.cssText = ` + position: absolute; + width: 100%; + top: 50%; + transform: translateY(-50%); + -webkit-appearance: none; + appearance: none; + background: transparent; + pointer-events: none; + margin: 0; + z-index: 3; + `; + + // Create end handle input + const inputEnd = document.createElement("input"); + inputEnd.type = "range"; + inputEnd.id = `${id}-end`; + inputEnd.className = "range-dual-input"; + inputEnd.min = min !== undefined ? min : 0; + inputEnd.max = max !== undefined ? max : 100; + inputEnd.value = + defaultValueEnd !== undefined ? defaultValueEnd : max || 100; + if (step !== undefined) { + inputEnd.step = step; + } + inputEnd.style.cssText = ` + position: absolute; + width: 100%; + top: 50%; + transform: translateY(-50%); + -webkit-appearance: none; + appearance: none; + background: transparent; + pointer-events: none; + margin: 0; + z-index: 4; + `; + + // Add CSS for the range inputs + const style = document.createElement("style"); + style.textContent = ` + .range-dual-input::-webkit-slider-runnable-track { + background: transparent; + border: none; + height: 4px; + } + .range-dual-input::-moz-range-track { + background: transparent; + border: none; + height: 4px; + } + .range-dual-input::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: #0078d4; + cursor: pointer; + pointer-events: all; + border: 2px solid #fff; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + position: relative; + z-index: 100; + } + .range-dual-input::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: #0078d4; + cursor: pointer; + pointer-events: all; + border: 2px solid #fff; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + position: relative; + z-index: 100; + } + .range-dual-input::-webkit-slider-thumb:hover { + background: #00bcf2; + } + .range-dual-input::-moz-range-thumb:hover { + background: #00bcf2; + } + `; + if (!document.getElementById("range-dual-styles")) { + style.id = "range-dual-styles"; + document.head.appendChild(style); + } + + // Update function + const updateRange = () => { + let startVal = parseInt(inputStart.value); + let endVal = parseInt(inputEnd.value); + + // Ensure start is never greater than end + if (startVal > endVal) { + const temp = startVal; + startVal = endVal; + endVal = temp; + inputStart.value = startVal; + inputEnd.value = endVal; + } + + const minVal = parseInt(inputStart.min); + const maxVal = parseInt(inputStart.max); + const startPercent = ((startVal - minVal) / (maxVal - minVal)) * 100; + const endPercent = ((endVal - minVal) / (maxVal - minVal)) * 100; + + range.style.left = `${startPercent}%`; + range.style.width = `${endPercent - startPercent}%`; + + const valueDisplay = document.getElementById(`${id}-value`); + if (valueDisplay) { + valueDisplay.textContent = `${startVal}-${endVal}`; + } + }; + + inputStart.addEventListener("input", updateRange); + inputEnd.addEventListener("input", updateRange); + + // Assemble the dual range slider + sliderWrapper.appendChild(track); + sliderWrapper.appendChild(range); + sliderWrapper.appendChild(inputStart); + sliderWrapper.appendChild(inputEnd); + + container.appendChild(labelEl); + container.appendChild(sliderWrapper); + + if (showWhen) { + container.style.display = "none"; + container.dataset.showWhen = showWhen; + } + + // Add tooltip handlers to the label + this.addTooltipHandlers(labelEl, description); + + // Initialize range display + updateRange(); + + return container; + } + /** * Create a color picker control */ createColor(id, label, defaultValue, showWhen, description) { - const container = document.createElement('div'); + const container = document.createElement("div"); - const labelEl = document.createElement('label'); + const labelEl = document.createElement("label"); labelEl.htmlFor = id; labelEl.textContent = label; - const input = document.createElement('input'); - input.type = 'color'; + const input = document.createElement("input"); + input.type = "color"; input.id = id; - input.value = defaultValue || '#ffffff'; + input.value = defaultValue || "#ffffff"; container.appendChild(labelEl); container.appendChild(input); if (showWhen) { - container.style.display = 'none'; + container.style.display = "none"; container.dataset.showWhen = showWhen; } @@ -374,17 +646,17 @@ export class UIBuilder { * Create a select dropdown control */ createSelect(id, label, defaultValue, options, showWhen, description) { - const container = document.createElement('div'); + const container = document.createElement("div"); - const labelEl = document.createElement('label'); + const labelEl = document.createElement("label"); labelEl.htmlFor = id; labelEl.textContent = label; - const select = document.createElement('select'); + const select = document.createElement("select"); select.id = id; - options.forEach(opt => { - const option = document.createElement('option'); + options.forEach((opt) => { + const option = document.createElement("option"); option.value = opt.value; option.textContent = opt.label; if (opt.value === defaultValue) { @@ -397,7 +669,7 @@ export class UIBuilder { container.appendChild(select); if (showWhen) { - container.style.display = 'none'; + container.style.display = "none"; container.dataset.showWhen = showWhen; } @@ -411,19 +683,19 @@ export class UIBuilder { * Create a text input control */ createTextInput(id, label, defaultValue, showWhen, description) { - const container = document.createElement('div'); + const container = document.createElement("div"); - const labelEl = document.createElement('label'); + const labelEl = document.createElement("label"); labelEl.htmlFor = id; labelEl.textContent = label; - const input = document.createElement('input'); - input.type = 'text'; + const input = document.createElement("input"); + input.type = "text"; input.id = id; - input.value = defaultValue || ''; + input.value = defaultValue || ""; // Only set maxLength for text inputs that aren't URLs - if (id !== 'bg-image-url') { + if (id !== "bg-image-url") { input.maxLength = 20; } @@ -431,7 +703,7 @@ export class UIBuilder { container.appendChild(input); if (showWhen) { - container.style.display = 'none'; + container.style.display = "none"; container.dataset.showWhen = showWhen; } @@ -445,21 +717,21 @@ export class UIBuilder { * Create a file input control */ createFileInput(id, label, defaultValue, showWhen, description, accept) { - const container = document.createElement('div'); + const container = document.createElement("div"); - const labelEl = document.createElement('label'); + const labelEl = document.createElement("label"); labelEl.htmlFor = id; labelEl.textContent = label; - const input = document.createElement('input'); - input.type = 'file'; + const input = document.createElement("input"); + input.type = "file"; input.id = id; if (accept) { input.accept = accept; } // Store the file data on the input element - input.addEventListener('change', (e) => { + input.addEventListener("change", (e) => { const file = e.target.files[0]; if (file) { // Create a blob URL for the file @@ -482,7 +754,7 @@ export class UIBuilder { container.appendChild(input); if (showWhen) { - container.style.display = 'none'; + container.style.display = "none"; container.dataset.showWhen = showWhen; } @@ -498,64 +770,103 @@ export class UIBuilder { */ setupConditionalVisibility() { // Find all controls with showWhen attribute - const conditionalControls = this.container.querySelectorAll('[data-show-when]'); + const conditionalControls = + this.container.querySelectorAll("[data-show-when]"); - conditionalControls.forEach(control => { + conditionalControls.forEach((control) => { const triggerControlId = control.dataset.showWhen; const triggerControl = document.getElementById(triggerControlId); if (triggerControl) { const updateVisibility = () => { - if (triggerControl.type === 'checkbox') { - control.style.display = triggerControl.checked ? 'block' : 'none'; - } else if (triggerControl.tagName === 'SELECT') { + if (triggerControl.type === "checkbox") { + control.style.display = triggerControl.checked ? "block" : "none"; + } else if (triggerControl.tagName === "SELECT") { // Get the control ID to determine what value to check for - const controlId = control.querySelector('input, select')?.id; + const controlId = control.querySelector("input, select")?.id; // For background controls - if (triggerControlId === 'bg-type') { - if (controlId === 'bg-color') { - control.style.display = triggerControl.value === 'solid' ? 'block' : 'none'; - } else if (controlId && (controlId.startsWith('gradient-') || controlId === 'gradient-angle')) { - control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none'; - } else if (controlId && (controlId.startsWith('texture-') || controlId === 'texture-type' || controlId === 'texture-scale')) { - control.style.display = triggerControl.value === 'texture' ? 'block' : 'none'; - } else if (controlId && (controlId.startsWith('emoji-') || controlId === 'emoji-text')) { - control.style.display = triggerControl.value === 'emoji-wallpaper' ? 'block' : 'none'; - } else if (controlId && (controlId.startsWith('bg-image-') || controlId === 'bg-image-file' || controlId === 'bg-image-fit' || controlId === 'bg-image-opacity')) { - control.style.display = triggerControl.value === 'external-image' ? 'block' : 'none'; + if (triggerControlId === "bg-type") { + if (controlId === "bg-color") { + control.style.display = + triggerControl.value === "solid" || + triggerControl.value === "external-image" + ? "block" + : "none"; + } else if ( + controlId && + (controlId.startsWith("gradient-") || + controlId === "gradient-angle") + ) { + control.style.display = + triggerControl.value === "gradient" ? "block" : "none"; + } else if ( + controlId && + (controlId.startsWith("texture-") || + controlId === "texture-type" || + controlId === "texture-scale") + ) { + control.style.display = + triggerControl.value === "texture" ? "block" : "none"; + } else if ( + controlId && + (controlId.startsWith("emoji-") || controlId === "emoji-text") + ) { + control.style.display = + triggerControl.value === "emoji-wallpaper" ? "block" : "none"; + } else if ( + controlId && + (controlId.startsWith("bg-image-") || + controlId === "bg-image-file" || + controlId === "bg-image-fit" || + controlId === "bg-image-opacity") + ) { + control.style.display = + triggerControl.value === "external-image" ? "block" : "none"; } } // For image fit controls (zoom and position only show when manual mode) - else if (triggerControlId === 'bg-image-fit') { - if (controlId && (controlId === 'bg-image-zoom' || controlId === 'bg-image-offset-x' || controlId === 'bg-image-offset-y')) { - control.style.display = triggerControl.value === 'manual' ? 'block' : 'none'; + else if (triggerControlId === "bg-image-fit") { + if ( + controlId && + (controlId === "bg-image-zoom" || + controlId === "bg-image-offset-x" || + controlId === "bg-image-offset-y") + ) { + control.style.display = + triggerControl.value === "manual" ? "block" : "none"; } } // For text color controls - else if (triggerControlId === 'text-color-type') { - if (controlId === 'text-color') { - control.style.display = triggerControl.value === 'solid' ? 'block' : 'none'; - } else if (controlId && controlId.startsWith('text-gradient-')) { - control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none'; + else if (triggerControlId === "text-color-type") { + if (controlId === "text-color") { + control.style.display = + triggerControl.value === "solid" ? "block" : "none"; + } else if (controlId && controlId.startsWith("text-gradient-")) { + control.style.display = + triggerControl.value === "gradient" ? "block" : "none"; } - } else if (triggerControlId === 'text2-color-type') { - if (controlId === 'text2-color') { - control.style.display = triggerControl.value === 'solid' ? 'block' : 'none'; - } else if (controlId && controlId.startsWith('text2-gradient-')) { - control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none'; + } else if (triggerControlId === "text2-color-type") { + if (controlId === "text2-color") { + control.style.display = + triggerControl.value === "solid" ? "block" : "none"; + } else if (controlId && controlId.startsWith("text2-gradient-")) { + control.style.display = + triggerControl.value === "gradient" ? "block" : "none"; } } // For border style controls - else if (triggerControlId === 'border-style') { - if (controlId === 'border-rainbow-speed') { - control.style.display = triggerControl.value === 'rainbow' ? 'block' : 'none'; - } else if (controlId === 'border-march-speed') { - control.style.display = triggerControl.value === 'marching-ants' ? 'block' : 'none'; + else if (triggerControlId === "border-style") { + if (controlId === "border-rainbow-speed") { + control.style.display = + triggerControl.value === "rainbow" ? "block" : "none"; + } else if (controlId === "border-march-speed") { + control.style.display = + triggerControl.value === "marching-ants" ? "block" : "none"; } } else { // Default: show when any value is selected - control.style.display = triggerControl.value ? 'block' : 'none'; + control.style.display = triggerControl.value ? "block" : "none"; } } }; @@ -564,8 +875,8 @@ export class UIBuilder { updateVisibility(); // Update on change - triggerControl.addEventListener('change', updateVisibility); - triggerControl.addEventListener('input', updateVisibility); + triggerControl.addEventListener("change", updateVisibility); + triggerControl.addEventListener("input", updateVisibility); } }); } diff --git a/static/test/moleskine.html b/static/test/moleskine.html new file mode 100644 index 0000000..fca01a2 --- /dev/null +++ b/static/test/moleskine.html @@ -0,0 +1,331 @@ + + + + + + Moleskine Notebook + + + +
+ +
+ + +
+
+
+
+
+
+
+

This is where the left page content goes

+
+
+
+ + +
+ + +
+
+
+
+

This is where the right page content goes

+
+
+
+
+
+
+ + +
+
+ +