Merge branch 'main' into develop
This commit is contained in:
commit
3b5f1e3876
23 changed files with 1424 additions and 138 deletions
|
|
@ -216,6 +216,23 @@
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-bottom: 1rem;
|
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
|
// Blog posts listing
|
||||||
|
|
@ -277,6 +294,7 @@
|
||||||
a {
|
a {
|
||||||
color: #ff9900;
|
color: #ff9900;
|
||||||
text-shadow: 0 0 5px rgba(255, 153, 0, 0.5);
|
text-shadow: 0 0 5px rgba(255, 153, 0, 0.5);
|
||||||
|
border-bottom: 1px dotted rgba(255, 200, 47, 0.5);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
background: #ff9900;
|
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 {
|
.tag-filter-link {
|
||||||
background: rgba(255, 153, 0, 0.2);
|
background: rgba(255, 153, 0, 0.2);
|
||||||
border-color: rgba(255, 153, 0, 0.5);
|
border-color: rgba(255, 153, 0, 0.5);
|
||||||
color: #ff9900;
|
color: #ff9900;
|
||||||
text-shadow: 0 0 5px rgba(255, 153, 0, 0.5);
|
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;
|
line-height: 1.6;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
color: #0f0;
|
color: #0f0;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
@include media-down(lg) {
|
@include media-down(lg) {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
@ -504,6 +562,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blog-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag filter navigation
|
// Tag filter navigation
|
||||||
|
|
@ -811,6 +876,7 @@
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
|
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
line-height: 2.2rem;
|
||||||
|
|
||||||
&:hover:not(.disabled) {
|
&:hover:not(.disabled) {
|
||||||
background: rgba(0, 255, 0, 0.2);
|
background: rgba(0, 255, 0, 0.2);
|
||||||
|
|
|
||||||
15
config.yml
15
config.yml
|
|
@ -14,6 +14,11 @@ outputFormats:
|
||||||
mediaType: "application/json"
|
mediaType: "application/json"
|
||||||
baseName: "webmentions"
|
baseName: "webmentions"
|
||||||
isPlainText: true
|
isPlainText: true
|
||||||
|
RSS:
|
||||||
|
mediaType: application/rss+xml
|
||||||
|
baseName: feed
|
||||||
|
isPlainText: false
|
||||||
|
rel: alternate
|
||||||
|
|
||||||
pagination:
|
pagination:
|
||||||
pagerSize: 5
|
pagerSize: 5
|
||||||
|
|
@ -41,16 +46,6 @@ params:
|
||||||
iconImageHeight: 35
|
iconImageHeight: 35
|
||||||
iconHeight: 70
|
iconHeight: 70
|
||||||
|
|
||||||
fuseOpts:
|
|
||||||
isCaseSensitive: false
|
|
||||||
shouldSort: true
|
|
||||||
location: 0
|
|
||||||
distance: 1000
|
|
||||||
threshold: 0.4
|
|
||||||
minMatchCharLength: 0
|
|
||||||
limit: 10
|
|
||||||
keys: ["title", "permalink", "summary", "content"]
|
|
||||||
|
|
||||||
markup:
|
markup:
|
||||||
highlight:
|
highlight:
|
||||||
noClasses: false
|
noClasses: false
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- 🗨️ 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 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
|
## 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
|
- [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
|
## Music
|
||||||
|
|
||||||
|
- 📺 [Mortal Kombat x Rhythm is a Dancer](https://www.youtube.com/watch?v=vKxn6P947PE)
|
||||||
|
|
||||||
## Next Week
|
## Next Week
|
||||||
|
|
||||||
Not necessary
|
Not necessary
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,5 @@ title: "Blog"
|
||||||
---
|
---
|
||||||
|
|
||||||
The home of my weekly updates and occasional other thoughts.
|
The home of my weekly updates and occasional other thoughts.
|
||||||
|
|
||||||
|
🛜 [Available via RSS](/blog/feed.xml)
|
||||||
|
|
|
||||||
34
content/blog/app-defaults-2026/index.md
Normal file
34
content/blog/app-defaults-2026/index.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -25,6 +25,7 @@ Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x3
|
||||||
- 07/01/2025 - Initial release.
|
- 07/01/2025 - Initial release.
|
||||||
- 08/01/2025 - Total refactor to be modular, added many more effects.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,13 @@
|
||||||
}}{{ end }}{{ end }}
|
}}{{ end }}{{ end }}
|
||||||
</title>
|
</title>
|
||||||
<meta name="viewport" content="width=device-width,minimum-scale=1" />
|
<meta name="viewport" content="width=device-width,minimum-scale=1" />
|
||||||
{{ hugo.Generator }} {{ $sass := resources.Get "sass/style.scss" }} {{
|
<link rel="me" href="https://indieweb.social/@ritual" />
|
||||||
|
<meta name="fediverse:creator" content="@ritual@indieweb.social">
|
||||||
|
{{ range .AlternativeOutputFormats }}
|
||||||
|
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .RelPermalink }}">
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ $sass := resources.Get "sass/style.scss" }} {{
|
||||||
$style := $sass | css.Sass | resources.Minify | resources.Fingerprint }}
|
$style := $sass | css.Sass | resources.Minify | resources.Fingerprint }}
|
||||||
<link rel="stylesheet" href="{{ $style.RelPermalink }}" />
|
<link rel="stylesheet" href="{{ $style.RelPermalink }}" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="blog-summary">{{ .Content }}</div>
|
<div class="blog-summary">{{ .Content }}</div>
|
||||||
|
<div class="blog-footer">
|
||||||
|
🛜 <a href="/blog/feed.xml">Available via RSS</a>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{{ partial "contact-section.html" . }}
|
{{ partial "contact-section.html" . }}
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,14 @@
|
||||||
<div class="blog-summary">
|
<div class="blog-summary">
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
</div>
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="blog-summary">
|
||||||
|
{{ .Summary }}
|
||||||
|
|
||||||
|
<div class="read-more" >
|
||||||
|
<a href="{{ .Permalink }}">Read More</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
56
layouts/rss.xml
Normal file
56
layouts/rss.xml
Normal file
|
|
@ -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 "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>Blog(gish) by Dan @ ritual.sh</title>
|
||||||
|
<link>{{ .Permalink }}</link>
|
||||||
|
<description>Dan's thoughts on web and game development, tech experiments, and whatever else catches my attention in the Golden Valley.</description>
|
||||||
|
<language>{{ site.Language.LanguageCode }}</language>{{ if not .Date.IsZero }}
|
||||||
|
<lastBuildDate>{{ (index $pages.ByLastmod.Reverse 0).Lastmod.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
|
||||||
|
{{- with .OutputFormats.Get "RSS" }}
|
||||||
|
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range $pages }}
|
||||||
|
{{- $content := .Content }}
|
||||||
|
{{- /* Remove graph containers with all their content including canvas and script tags */ -}}
|
||||||
|
{{- $content = replaceRE `(?s)<div class="graph-container"[^>]*>.*?</div>\s*<script>.*?</script>` "<p><em>[Interactive graph not available in RSS - please visit the full post to view the charts]</em></p>" $content }}
|
||||||
|
<item>
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
<link>{{ .Permalink }}</link>
|
||||||
|
<pubDate>{{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
|
||||||
|
{{- with $authorEmail }}<author>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</author>{{ end }}
|
||||||
|
<guid>{{ .Permalink }}</guid>
|
||||||
|
<description>{{ $content | transform.XMLEscape | safeHTML }}</description>
|
||||||
|
</item>
|
||||||
|
{{- end }}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
|
@ -174,6 +174,14 @@ export class ButtonGenerator {
|
||||||
values[id] = element.value;
|
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;
|
return values;
|
||||||
|
|
|
||||||
|
|
@ -245,12 +245,17 @@ export class ExternalImageBackgroundEffect extends ButtonEffect {
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const file = controlValues['bg-image-file'];
|
const file = controlValues['bg-image-file'];
|
||||||
|
const bgColor = controlValues['bg-color'] || '#FFFFFF';
|
||||||
const fitMode = controlValues['bg-image-fit'] || 'cover';
|
const fitMode = controlValues['bg-image-fit'] || 'cover';
|
||||||
const opacity = controlValues['bg-image-opacity'] ?? 1;
|
const opacity = controlValues['bg-image-opacity'] ?? 1;
|
||||||
const zoom = controlValues['bg-image-zoom'] ?? 100;
|
const zoom = controlValues['bg-image-zoom'] ?? 100;
|
||||||
const offsetX = controlValues['bg-image-offset-x'] ?? 50;
|
const offsetX = controlValues['bg-image-offset-x'] ?? 50;
|
||||||
const offsetY = controlValues['bg-image-offset-y'] ?? 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 no file selected, fill with a placeholder color
|
||||||
if (!file || !file.blobUrl) {
|
if (!file || !file.blobUrl) {
|
||||||
context.fillStyle = '#cccccc';
|
context.fillStyle = '#cccccc';
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export class SolidBackgroundEffect extends ButtonEffect {
|
||||||
label: "Background Color",
|
label: "Background Color",
|
||||||
defaultValue: "#4a90e2",
|
defaultValue: "#4a90e2",
|
||||||
showWhen: "bg-type",
|
showWhen: "bg-type",
|
||||||
description: "Solid background color",
|
description: "Background color (also used behind images)",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
static/js/button-generator/effects/flash-text.js
Normal file
88
static/js/button-generator/effects/flash-text.js
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,18 @@ export class RainbowTextEffect extends ButtonEffect {
|
||||||
|
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
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
|
// Get text configuration
|
||||||
const text = controlValues[`button-text${suffix}`] || '';
|
const text = controlValues[`button-text${suffix}`] || '';
|
||||||
if (!text || text.trim() === '') return;
|
if (!text || text.trim() === '') return;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,19 @@ export class SpinTextEffect extends ButtonEffect {
|
||||||
|
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? "" : "2";
|
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}`] || "";
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
|
|
||||||
if (!text || text.trim() === '') return;
|
if (!text || text.trim() === '') return;
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,12 @@ export class TextShadowEffect extends ButtonEffect {
|
||||||
apply(context, controlValues, animState, renderData) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? "" : "2";
|
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}`] || "";
|
const text = controlValues[`button-text${suffix}`] || "";
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,13 +150,20 @@ export class StandardTextEffect extends ButtonEffect {
|
||||||
const waveActive = controlValues[`animate-text-wave${suffix}`];
|
const waveActive = controlValues[`animate-text-wave${suffix}`];
|
||||||
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
const rainbowActive = controlValues[`animate-text-rainbow${suffix}`];
|
||||||
const spinActive = controlValues[`animate-text-spin${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) {
|
apply(context, controlValues, animState, renderData) {
|
||||||
const suffix = this.textLineNumber === 1 ? "" : "2";
|
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}`];
|
const text = controlValues[`button-text${suffix}`];
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
|
|
|
||||||
312
static/js/button-generator/effects/ticker-text.js
Normal file
312
static/js/button-generator/effects/ticker-text.js
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,18 @@ export class WaveTextEffect extends ButtonEffect {
|
||||||
|
|
||||||
const suffix = this.textLineNumber === 1 ? '' : '2';
|
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
|
// Get text configuration
|
||||||
const text = controlValues[`button-text${suffix}`] || '';
|
const text = controlValues[`button-text${suffix}`] || '';
|
||||||
if (!text || text.trim() === '') return;
|
if (!text || text.trim() === '') return;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,9 @@ import * as standardText from "./effects/text-standard.js";
|
||||||
import * as textShadow from "./effects/text-shadow.js";
|
import * as textShadow from "./effects/text-shadow.js";
|
||||||
import * as waveText from "./effects/wave-text.js";
|
import * as waveText from "./effects/wave-text.js";
|
||||||
import * as rainbowText from "./effects/rainbow-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 spinText from "./effects/spin-text.js";
|
||||||
|
import * as tickerText from "./effects/ticker-text.js";
|
||||||
import * as glitch from "./effects/glitch.js";
|
import * as glitch from "./effects/glitch.js";
|
||||||
import * as pulse from "./effects/pulse.js";
|
import * as pulse from "./effects/pulse.js";
|
||||||
import * as shimmer from "./effects/shimmer.js";
|
import * as shimmer from "./effects/shimmer.js";
|
||||||
|
|
@ -101,7 +103,9 @@ async function setupApp() {
|
||||||
textShadow.register(generator);
|
textShadow.register(generator);
|
||||||
waveText.register(generator);
|
waveText.register(generator);
|
||||||
rainbowText.register(generator);
|
rainbowText.register(generator);
|
||||||
|
flashText.register(generator);
|
||||||
spinText.register(generator);
|
spinText.register(generator);
|
||||||
|
tickerText.register(generator);
|
||||||
glitch.register(generator);
|
glitch.register(generator);
|
||||||
pulse.register(generator);
|
pulse.register(generator);
|
||||||
shimmer.register(generator);
|
shimmer.register(generator);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ export class UIBuilder {
|
||||||
*/
|
*/
|
||||||
setupTooltip() {
|
setupTooltip() {
|
||||||
// Wait for DOM to be ready
|
// Wait for DOM to be ready
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
this.createTooltipElement();
|
this.createTooltipElement();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -26,8 +26,8 @@ export class UIBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
createTooltipElement() {
|
createTooltipElement() {
|
||||||
this.tooltip = document.createElement('div');
|
this.tooltip = document.createElement("div");
|
||||||
this.tooltip.className = 'control-tooltip';
|
this.tooltip.className = "control-tooltip";
|
||||||
this.tooltip.style.cssText = `
|
this.tooltip.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: linear-gradient(135deg, rgba(0, 120, 200, 0.98) 0%, rgba(0, 100, 180, 0.98) 100%);
|
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);
|
clearTimeout(this.tooltipTimeout);
|
||||||
|
|
||||||
this.tooltip.textContent = text;
|
this.tooltip.textContent = text;
|
||||||
this.tooltip.style.opacity = '1';
|
this.tooltip.style.opacity = "1";
|
||||||
|
|
||||||
// Position tooltip above the element
|
// Position tooltip above the element
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
// Set initial position to measure
|
// Set initial position to measure
|
||||||
this.tooltip.style.left = '0px';
|
this.tooltip.style.left = "0px";
|
||||||
this.tooltip.style.top = '0px';
|
this.tooltip.style.top = "0px";
|
||||||
this.tooltip.style.visibility = 'hidden';
|
this.tooltip.style.visibility = "hidden";
|
||||||
this.tooltip.style.display = 'block';
|
this.tooltip.style.display = "block";
|
||||||
|
|
||||||
const tooltipRect = this.tooltip.getBoundingClientRect();
|
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 left = rect.left + rect.width / 2 - tooltipRect.width / 2;
|
||||||
let top = rect.top - tooltipRect.height - 10;
|
let top = rect.top - tooltipRect.height - 10;
|
||||||
|
|
@ -96,7 +96,7 @@ export class UIBuilder {
|
||||||
if (!this.tooltip) return;
|
if (!this.tooltip) return;
|
||||||
clearTimeout(this.tooltipTimeout);
|
clearTimeout(this.tooltipTimeout);
|
||||||
this.tooltipTimeout = setTimeout(() => {
|
this.tooltipTimeout = setTimeout(() => {
|
||||||
this.tooltip.style.opacity = '0';
|
this.tooltip.style.opacity = "0";
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,17 +106,17 @@ export class UIBuilder {
|
||||||
addTooltipHandlers(element, description) {
|
addTooltipHandlers(element, description) {
|
||||||
if (!description) return;
|
if (!description) return;
|
||||||
|
|
||||||
element.addEventListener('mouseenter', () => {
|
element.addEventListener("mouseenter", () => {
|
||||||
this.showTooltip(element, description);
|
this.showTooltip(element, description);
|
||||||
});
|
});
|
||||||
|
|
||||||
element.addEventListener('mouseleave', () => {
|
element.addEventListener("mouseleave", () => {
|
||||||
this.hideTooltip();
|
this.hideTooltip();
|
||||||
});
|
});
|
||||||
|
|
||||||
element.addEventListener('mousemove', () => {
|
element.addEventListener("mousemove", () => {
|
||||||
// Update position on mouse move for better following
|
// 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);
|
this.showTooltip(element, description);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -128,7 +128,7 @@ export class UIBuilder {
|
||||||
*/
|
*/
|
||||||
buildUI(effects) {
|
buildUI(effects) {
|
||||||
// Clear existing content
|
// Clear existing content
|
||||||
this.container.innerHTML = '';
|
this.container.innerHTML = "";
|
||||||
this.controlGroups.clear();
|
this.controlGroups.clear();
|
||||||
|
|
||||||
// Group effects by category
|
// Group effects by category
|
||||||
|
|
@ -148,8 +148,8 @@ export class UIBuilder {
|
||||||
categorizeEffects(effects) {
|
categorizeEffects(effects) {
|
||||||
const categories = new Map();
|
const categories = new Map();
|
||||||
|
|
||||||
effects.forEach(effect => {
|
effects.forEach((effect) => {
|
||||||
const category = effect.category || 'Other';
|
const category = effect.category || "Other";
|
||||||
if (!categories.has(category)) {
|
if (!categories.has(category)) {
|
||||||
categories.set(category, []);
|
categories.set(category, []);
|
||||||
}
|
}
|
||||||
|
|
@ -159,17 +159,17 @@ export class UIBuilder {
|
||||||
// Sort categories in a logical order
|
// Sort categories in a logical order
|
||||||
const orderedCategories = new Map();
|
const orderedCategories = new Map();
|
||||||
const categoryOrder = [
|
const categoryOrder = [
|
||||||
'Text Line 1',
|
"Text Line 1",
|
||||||
'Text Line 2',
|
"Text Line 2",
|
||||||
'Background',
|
"Background",
|
||||||
'Background Animations',
|
"Background Animations",
|
||||||
'Border',
|
"Border",
|
||||||
'Visual Effects',
|
"Visual Effects",
|
||||||
'General Effects',
|
"General Effects",
|
||||||
'Special Effects'
|
"Special Effects",
|
||||||
];
|
];
|
||||||
|
|
||||||
categoryOrder.forEach(cat => {
|
categoryOrder.forEach((cat) => {
|
||||||
if (categories.has(cat)) {
|
if (categories.has(cat)) {
|
||||||
orderedCategories.set(cat, categories.get(cat));
|
orderedCategories.set(cat, categories.get(cat));
|
||||||
}
|
}
|
||||||
|
|
@ -191,29 +191,29 @@ export class UIBuilder {
|
||||||
* @param {Array<ButtonEffect>} effects - Effects in this category
|
* @param {Array<ButtonEffect>} effects - Effects in this category
|
||||||
*/
|
*/
|
||||||
createControlGroup(category, effects) {
|
createControlGroup(category, effects) {
|
||||||
const groupDiv = document.createElement('div');
|
const groupDiv = document.createElement("div");
|
||||||
groupDiv.className = 'control-group';
|
groupDiv.className = "control-group";
|
||||||
|
|
||||||
// Create header
|
// Create header
|
||||||
const header = document.createElement('h3');
|
const header = document.createElement("h3");
|
||||||
header.className = 'control-group-header';
|
header.className = "control-group-header";
|
||||||
header.innerHTML = `
|
header.innerHTML = `
|
||||||
<span>${category}</span>
|
<span>${category}</span>
|
||||||
<span class="toggle-icon">−</span>
|
<span class="toggle-icon">−</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Create content container
|
// Create content container
|
||||||
const content = document.createElement('div');
|
const content = document.createElement("div");
|
||||||
content.className = 'control-group-content';
|
content.className = "control-group-content";
|
||||||
|
|
||||||
// Add controls for each effect in this category
|
// Add controls for each effect in this category
|
||||||
effects.forEach(effect => {
|
effects.forEach((effect) => {
|
||||||
this.addEffectControls(content, effect);
|
this.addEffectControls(content, effect);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add click handler for collapsing
|
// Add click handler for collapsing
|
||||||
header.addEventListener('click', () => {
|
header.addEventListener("click", () => {
|
||||||
groupDiv.classList.toggle('collapsed');
|
groupDiv.classList.toggle("collapsed");
|
||||||
});
|
});
|
||||||
|
|
||||||
groupDiv.appendChild(header);
|
groupDiv.appendChild(header);
|
||||||
|
|
@ -229,7 +229,7 @@ export class UIBuilder {
|
||||||
* @param {ButtonEffect} effect - Effect to create controls for
|
* @param {ButtonEffect} effect - Effect to create controls for
|
||||||
*/
|
*/
|
||||||
addEffectControls(container, effect) {
|
addEffectControls(container, effect) {
|
||||||
effect.controls.forEach(control => {
|
effect.controls.forEach((control) => {
|
||||||
const controlEl = this.createControl(control);
|
const controlEl = this.createControl(control);
|
||||||
if (controlEl) {
|
if (controlEl) {
|
||||||
container.appendChild(controlEl);
|
container.appendChild(controlEl);
|
||||||
|
|
@ -243,26 +243,85 @@ export class UIBuilder {
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
createControl(controlDef) {
|
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) {
|
switch (type) {
|
||||||
case 'checkbox':
|
case "checkbox":
|
||||||
return this.createCheckbox(id, label, defaultValue, showWhen, description);
|
return this.createCheckbox(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
|
||||||
case 'range':
|
case "range":
|
||||||
return this.createRange(id, label, defaultValue, min, max, step, description, showWhen);
|
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);
|
return this.createColor(id, label, defaultValue, showWhen, description);
|
||||||
|
|
||||||
case 'select':
|
case "select":
|
||||||
return this.createSelect(id, label, defaultValue, options, showWhen, description);
|
return this.createSelect(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
options,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
|
||||||
case 'text':
|
case "text":
|
||||||
return this.createTextInput(id, label, defaultValue, showWhen, description);
|
return this.createTextInput(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
);
|
||||||
|
|
||||||
case 'file':
|
case "file":
|
||||||
return this.createFileInput(id, label, defaultValue, showWhen, description, controlDef.accept);
|
return this.createFileInput(
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
showWhen,
|
||||||
|
description,
|
||||||
|
controlDef.accept,
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown control type: ${type}`);
|
console.warn(`Unknown control type: ${type}`);
|
||||||
|
|
@ -274,22 +333,22 @@ export class UIBuilder {
|
||||||
* Create a checkbox control
|
* Create a checkbox control
|
||||||
*/
|
*/
|
||||||
createCheckbox(id, label, defaultValue, showWhen, description) {
|
createCheckbox(id, label, defaultValue, showWhen, description) {
|
||||||
const wrapper = document.createElement('label');
|
const wrapper = document.createElement("label");
|
||||||
wrapper.className = 'checkbox-label';
|
wrapper.className = "checkbox-label";
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'checkbox';
|
input.type = "checkbox";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.checked = defaultValue || false;
|
input.checked = defaultValue || false;
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement("span");
|
||||||
span.textContent = label;
|
span.textContent = label;
|
||||||
|
|
||||||
wrapper.appendChild(input);
|
wrapper.appendChild(input);
|
||||||
wrapper.appendChild(span);
|
wrapper.appendChild(span);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
wrapper.style.display = 'none';
|
wrapper.style.display = "none";
|
||||||
wrapper.dataset.showWhen = showWhen;
|
wrapper.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,14 +362,14 @@ export class UIBuilder {
|
||||||
* Create a range slider control
|
* Create a range slider control
|
||||||
*/
|
*/
|
||||||
createRange(id, label, defaultValue, min, max, step, description, showWhen) {
|
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.htmlFor = id;
|
||||||
labelEl.innerHTML = `${label}: <span id="${id}-value">${defaultValue}</span>`;
|
labelEl.innerHTML = `${label}: <span id="${id}-value">${defaultValue}</span>`;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'range';
|
input.type = "range";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.min = min !== undefined ? min : 0;
|
input.min = min !== undefined ? min : 0;
|
||||||
input.max = max !== undefined ? max : 100;
|
input.max = max !== undefined ? max : 100;
|
||||||
|
|
@ -320,7 +379,7 @@ export class UIBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update value display on input
|
// Update value display on input
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener("input", () => {
|
||||||
const valueDisplay = document.getElementById(`${id}-value`);
|
const valueDisplay = document.getElementById(`${id}-value`);
|
||||||
if (valueDisplay) {
|
if (valueDisplay) {
|
||||||
valueDisplay.textContent = input.value;
|
valueDisplay.textContent = input.value;
|
||||||
|
|
@ -331,7 +390,7 @@ export class UIBuilder {
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,26 +400,239 @@ export class UIBuilder {
|
||||||
return container;
|
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}: <span id="${id}-value">${defaultValueStart}-${defaultValueEnd}</span>`;
|
||||||
|
|
||||||
|
// 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
|
* Create a color picker control
|
||||||
*/
|
*/
|
||||||
createColor(id, label, defaultValue, showWhen, description) {
|
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.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'color';
|
input.type = "color";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.value = defaultValue || '#ffffff';
|
input.value = defaultValue || "#ffffff";
|
||||||
|
|
||||||
container.appendChild(labelEl);
|
container.appendChild(labelEl);
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,17 +646,17 @@ export class UIBuilder {
|
||||||
* Create a select dropdown control
|
* Create a select dropdown control
|
||||||
*/
|
*/
|
||||||
createSelect(id, label, defaultValue, options, showWhen, description) {
|
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.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const select = document.createElement('select');
|
const select = document.createElement("select");
|
||||||
select.id = id;
|
select.id = id;
|
||||||
|
|
||||||
options.forEach(opt => {
|
options.forEach((opt) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement("option");
|
||||||
option.value = opt.value;
|
option.value = opt.value;
|
||||||
option.textContent = opt.label;
|
option.textContent = opt.label;
|
||||||
if (opt.value === defaultValue) {
|
if (opt.value === defaultValue) {
|
||||||
|
|
@ -397,7 +669,7 @@ export class UIBuilder {
|
||||||
container.appendChild(select);
|
container.appendChild(select);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,19 +683,19 @@ export class UIBuilder {
|
||||||
* Create a text input control
|
* Create a text input control
|
||||||
*/
|
*/
|
||||||
createTextInput(id, label, defaultValue, showWhen, description) {
|
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.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'text';
|
input.type = "text";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
input.value = defaultValue || '';
|
input.value = defaultValue || "";
|
||||||
|
|
||||||
// Only set maxLength for text inputs that aren't URLs
|
// Only set maxLength for text inputs that aren't URLs
|
||||||
if (id !== 'bg-image-url') {
|
if (id !== "bg-image-url") {
|
||||||
input.maxLength = 20;
|
input.maxLength = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,7 +703,7 @@ export class UIBuilder {
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,21 +717,21 @@ export class UIBuilder {
|
||||||
* Create a file input control
|
* Create a file input control
|
||||||
*/
|
*/
|
||||||
createFileInput(id, label, defaultValue, showWhen, description, accept) {
|
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.htmlFor = id;
|
||||||
labelEl.textContent = label;
|
labelEl.textContent = label;
|
||||||
|
|
||||||
const input = document.createElement('input');
|
const input = document.createElement("input");
|
||||||
input.type = 'file';
|
input.type = "file";
|
||||||
input.id = id;
|
input.id = id;
|
||||||
if (accept) {
|
if (accept) {
|
||||||
input.accept = accept;
|
input.accept = accept;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the file data on the input element
|
// Store the file data on the input element
|
||||||
input.addEventListener('change', (e) => {
|
input.addEventListener("change", (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
// Create a blob URL for the file
|
// Create a blob URL for the file
|
||||||
|
|
@ -482,7 +754,7 @@ export class UIBuilder {
|
||||||
container.appendChild(input);
|
container.appendChild(input);
|
||||||
|
|
||||||
if (showWhen) {
|
if (showWhen) {
|
||||||
container.style.display = 'none';
|
container.style.display = "none";
|
||||||
container.dataset.showWhen = showWhen;
|
container.dataset.showWhen = showWhen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,64 +770,103 @@ export class UIBuilder {
|
||||||
*/
|
*/
|
||||||
setupConditionalVisibility() {
|
setupConditionalVisibility() {
|
||||||
// Find all controls with showWhen attribute
|
// 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 triggerControlId = control.dataset.showWhen;
|
||||||
const triggerControl = document.getElementById(triggerControlId);
|
const triggerControl = document.getElementById(triggerControlId);
|
||||||
|
|
||||||
if (triggerControl) {
|
if (triggerControl) {
|
||||||
const updateVisibility = () => {
|
const updateVisibility = () => {
|
||||||
if (triggerControl.type === 'checkbox') {
|
if (triggerControl.type === "checkbox") {
|
||||||
control.style.display = triggerControl.checked ? 'block' : 'none';
|
control.style.display = triggerControl.checked ? "block" : "none";
|
||||||
} else if (triggerControl.tagName === 'SELECT') {
|
} else if (triggerControl.tagName === "SELECT") {
|
||||||
// Get the control ID to determine what value to check for
|
// 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
|
// For background controls
|
||||||
if (triggerControlId === 'bg-type') {
|
if (triggerControlId === "bg-type") {
|
||||||
if (controlId === 'bg-color') {
|
if (controlId === "bg-color") {
|
||||||
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId && (controlId.startsWith('gradient-') || controlId === 'gradient-angle')) {
|
triggerControl.value === "solid" ||
|
||||||
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
triggerControl.value === "external-image"
|
||||||
} else if (controlId && (controlId.startsWith('texture-') || controlId === 'texture-type' || controlId === 'texture-scale')) {
|
? "block"
|
||||||
control.style.display = triggerControl.value === 'texture' ? 'block' : 'none';
|
: "none";
|
||||||
} else if (controlId && (controlId.startsWith('emoji-') || controlId === 'emoji-text')) {
|
} else if (
|
||||||
control.style.display = triggerControl.value === 'emoji-wallpaper' ? 'block' : 'none';
|
controlId &&
|
||||||
} else if (controlId && (controlId.startsWith('bg-image-') || controlId === 'bg-image-file' || controlId === 'bg-image-fit' || controlId === 'bg-image-opacity')) {
|
(controlId.startsWith("gradient-") ||
|
||||||
control.style.display = triggerControl.value === 'external-image' ? 'block' : 'none';
|
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)
|
// For image fit controls (zoom and position only show when manual mode)
|
||||||
else if (triggerControlId === 'bg-image-fit') {
|
else if (triggerControlId === "bg-image-fit") {
|
||||||
if (controlId && (controlId === 'bg-image-zoom' || controlId === 'bg-image-offset-x' || controlId === 'bg-image-offset-y')) {
|
if (
|
||||||
control.style.display = triggerControl.value === 'manual' ? 'block' : 'none';
|
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
|
// For text color controls
|
||||||
else if (triggerControlId === 'text-color-type') {
|
else if (triggerControlId === "text-color-type") {
|
||||||
if (controlId === 'text-color') {
|
if (controlId === "text-color") {
|
||||||
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId && controlId.startsWith('text-gradient-')) {
|
triggerControl.value === "solid" ? "block" : "none";
|
||||||
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
} else if (controlId && controlId.startsWith("text-gradient-")) {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "gradient" ? "block" : "none";
|
||||||
}
|
}
|
||||||
} else if (triggerControlId === 'text2-color-type') {
|
} else if (triggerControlId === "text2-color-type") {
|
||||||
if (controlId === 'text2-color') {
|
if (controlId === "text2-color") {
|
||||||
control.style.display = triggerControl.value === 'solid' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId && controlId.startsWith('text2-gradient-')) {
|
triggerControl.value === "solid" ? "block" : "none";
|
||||||
control.style.display = triggerControl.value === 'gradient' ? 'block' : 'none';
|
} else if (controlId && controlId.startsWith("text2-gradient-")) {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "gradient" ? "block" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For border style controls
|
// For border style controls
|
||||||
else if (triggerControlId === 'border-style') {
|
else if (triggerControlId === "border-style") {
|
||||||
if (controlId === 'border-rainbow-speed') {
|
if (controlId === "border-rainbow-speed") {
|
||||||
control.style.display = triggerControl.value === 'rainbow' ? 'block' : 'none';
|
control.style.display =
|
||||||
} else if (controlId === 'border-march-speed') {
|
triggerControl.value === "rainbow" ? "block" : "none";
|
||||||
control.style.display = triggerControl.value === 'marching-ants' ? 'block' : 'none';
|
} else if (controlId === "border-march-speed") {
|
||||||
|
control.style.display =
|
||||||
|
triggerControl.value === "marching-ants" ? "block" : "none";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default: show when any value is selected
|
// 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();
|
updateVisibility();
|
||||||
|
|
||||||
// Update on change
|
// Update on change
|
||||||
triggerControl.addEventListener('change', updateVisibility);
|
triggerControl.addEventListener("change", updateVisibility);
|
||||||
triggerControl.addEventListener('input', updateVisibility);
|
triggerControl.addEventListener("input", updateVisibility);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
331
static/test/moleskine.html
Normal file
331
static/test/moleskine.html
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Moleskine Notebook</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--page-width: 500px;
|
||||||
|
--page-height: calc(var(--page-width) * 1.618);
|
||||||
|
--binding-width: 1px;
|
||||||
|
--total-width: calc(var(--page-width) * 2 + var(--binding-width));
|
||||||
|
--border-radius: 30px;
|
||||||
|
|
||||||
|
--paper-color: #fff8dc;
|
||||||
|
--cover-color-top: #be8f32;
|
||||||
|
--cover-color-bottom: #846f24;
|
||||||
|
--line-color: rgba(135, 180, 210, 0.25);
|
||||||
|
|
||||||
|
--internal-border-radius: calc(var(--border-radius) * 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||||
|
font-family: "Georgia", serif;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-container {
|
||||||
|
position: relative;
|
||||||
|
width: var(--total-width);
|
||||||
|
height: var(--page-height);
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
filter: drop-shadow(0 30px 60px rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Covers */
|
||||||
|
.cover {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--cover-color-top) 0%,
|
||||||
|
var(--cover-color-bottom) 100%
|
||||||
|
);
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-left {
|
||||||
|
left: -10px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
ellipse at 20% 30%,
|
||||||
|
rgba(160, 130, 90, 0.4) 0%,
|
||||||
|
transparent 50%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
ellipse at 80% 70%,
|
||||||
|
rgba(70, 50, 30, 0.3) 0%,
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--cover-color-top) 0%,
|
||||||
|
var(--cover-color-top) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset -2px 0 8px rgba(0, 0, 0, 0.4),
|
||||||
|
inset 2px 2px 4px rgba(160, 130, 90, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-right {
|
||||||
|
right: -10px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
ellipse at 80% 30%,
|
||||||
|
rgba(160, 130, 90, 0.4) 0%,
|
||||||
|
transparent 50%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
ellipse at 20% 70%,
|
||||||
|
rgba(70, 50, 30, 0.3) 0%,
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
225deg,
|
||||||
|
var(--cover-color-top) 0%,
|
||||||
|
var(--cover-color-bottom) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset 2px 0 8px rgba(0, 0, 0, 0.4),
|
||||||
|
inset -2px 2px 4px rgba(160, 130, 90, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page stacks */
|
||||||
|
.page-stack {
|
||||||
|
position: relative;
|
||||||
|
width: var(--page-width);
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-left {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-right {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page layers stack kinda*/
|
||||||
|
.page-layer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--paper-color);
|
||||||
|
border-radius: var(--internal-border-radius);
|
||||||
|
box-shadow: inset 1px 0 1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-left .page-layer:nth-child(1) {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
filter: brightness(0.85);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-left .page-layer:nth-child(2) {
|
||||||
|
transform: translateX(-2px);
|
||||||
|
filter: brightness(0.9);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-left .page-layer:nth-child(3) {
|
||||||
|
transform: translateX(-1px);
|
||||||
|
filter: brightness(0.95);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-right .page-layer:nth-child(2) {
|
||||||
|
transform: translateX(5px);
|
||||||
|
filter: brightness(0.85);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-right .page-layer:nth-child(3) {
|
||||||
|
transform: translateX(2px);
|
||||||
|
filter: brightness(0.9);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-stack-right .page-layer:nth-child(4) {
|
||||||
|
transform: translateX(1px);
|
||||||
|
filter: brightness(0.95);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main pages */
|
||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--paper-color);
|
||||||
|
border-radius: var(--internal-border-radius);
|
||||||
|
z-index: 4;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 2px 4px rgba(255, 255, 255, 0.3),
|
||||||
|
inset 0 -2px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
ellipse at 30% 20%,
|
||||||
|
rgba(255, 255, 255, 0.5) 0%,
|
||||||
|
transparent 40%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
ellipse at 70% 80%,
|
||||||
|
rgba(0, 0, 0, 0.02) 0%,
|
||||||
|
transparent 50%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: var(--internal-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-left {
|
||||||
|
border-top-left-radius: var(--internal-border-radius);
|
||||||
|
border-bottom-left-radius: var(--internal-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-right {
|
||||||
|
border-top-right-radius: var(--internal-border-radius);
|
||||||
|
border-bottom-right-radius: var(--internal-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shadow for the binding effect **/
|
||||||
|
.page-left::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 10px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0, 0, 0, 0.1) 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-right::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 10px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(0, 0, 0, 0.1) 0%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page lines */
|
||||||
|
.page-lines {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 40px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
180deg,
|
||||||
|
transparent 0px,
|
||||||
|
transparent 27px,
|
||||||
|
var(--line-color) 27px,
|
||||||
|
var(--line-color) 28px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Central binding */
|
||||||
|
.binding {
|
||||||
|
position: absolute;
|
||||||
|
left: 49.95%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: var(--binding-width);
|
||||||
|
height: calc((var(--page-height) - (var(--border-radius) * 1.5)));
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(0, 0, 0, 0.05) 0%,
|
||||||
|
rgba(0, 0, 0, 0.15) 50%,
|
||||||
|
rgba(0, 0, 0, 0.05) 100%
|
||||||
|
);
|
||||||
|
z-index: 5;
|
||||||
|
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 2rem;
|
||||||
|
padding-top: 3.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="notebook-container">
|
||||||
|
<!-- Left cover -->
|
||||||
|
<div class="cover cover-left"></div>
|
||||||
|
|
||||||
|
<!-- Left page stack -->
|
||||||
|
<div class="page-stack page-stack-left">
|
||||||
|
<div class="page-layer"></div>
|
||||||
|
<div class="page-layer"></div>
|
||||||
|
<div class="page-layer"></div>
|
||||||
|
<div class="page page-left">
|
||||||
|
<div class="page-lines"></div>
|
||||||
|
<div class="page-content left">
|
||||||
|
<p>This is where the left page content goes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center binding -->
|
||||||
|
<div class="binding"></div>
|
||||||
|
|
||||||
|
<!-- Right page stack -->
|
||||||
|
<div class="page-stack page-stack-right">
|
||||||
|
<div class="page page-right">
|
||||||
|
<div class="page-lines"></div>
|
||||||
|
<div class="page-content right">
|
||||||
|
<p>This is where the right page content goes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-layer"></div>
|
||||||
|
<div class="page-layer"></div>
|
||||||
|
<div class="page-layer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right cover -->
|
||||||
|
<div class="cover cover-right"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue