diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index a3110ca39996..d33167a8e55e 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -994,6 +994,22 @@ +
+
+ + typed effect + +
+
Change how typed words are shown.
+
+ + + + +
+
diff --git a/frontend/src/styles/animations.scss b/frontend/src/styles/animations.scss index d826c7659e35..2ddc5002d2ca 100644 --- a/frontend/src/styles/animations.scss +++ b/frontend/src/styles/animations.scss @@ -19,6 +19,15 @@ } } +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + @keyframes caretFlashSmooth { 0%, 100% { @@ -130,3 +139,37 @@ background-position: 0% 50%; } } + +@keyframes typedEffectFadeIn { + 0% { + opacity: 0; + } + 75% { + opacity: 0.4; + } + 100% { + opacity: 1; + } +} + +@keyframes typedEffectToDust { + 0% { + transform: scale(1); + color: var(--current-color); + } + 10% { + /* transform: scale(1); */ + } + 15% { + transform: scale(1); + color: var(--c-dot); + } + 80% { + /* transform: scale(0.5); */ + color: var(--c-dot); + } + 100% { + transform: scale(0.4); + color: transparent; + } +} diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index d0064f2c7c24..c935deffed67 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -396,6 +396,8 @@ --untyped-letter-color: var(--sub-color); --incorrect-letter-color: var(--colorful-error-color); --extra-letter-color: var(--colorful-error-extra-color); + --c-dot: var(--main-color); + --c-dot--error: var(--colorful-error-color); &.blind .word.error { border-bottom: 2px solid transparent; } @@ -518,6 +520,62 @@ } } } + + &.typed-effect-hide { + .word.typed { + opacity: 0; + } + } + + &.typed-effect-fade { + .word.typed { + animation: fadeOut 250ms ease-in 1 forwards; + } + } + + &.typed-effect-dots:not(.withLigatures) { + /* transform already typed letters into appropriately colored dots */ + + .word letter { + position: relative; + &::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 1em; + aspect-ratio: 1; + border-radius: 50%; + opacity: 0; + } + } + .typed letter { + color: var(--bg-color); + animation: typedEffectToDust 200ms ease-out 0ms 1 forwards !important; + &::after { + animation: typedEffectFadeIn 100ms ease-in 100ms 1 forwards; + background: var(--c-dot); + } + } + &:not(.blind) { + .word letter.incorrect::after { + background: var(--c-dot--error); + } + } + + @media (prefers-reduced-motion) { + .typed letter { + animation: none !important; + transform: scale(0.4); + color: transparent; + &::after { + animation: none !important; + opacity: 1; + } + } + } + } } .word { @@ -1416,6 +1474,9 @@ rgba(0, 0, 0, 0) 99% ); } + + --c-dot: var(--text-color); + --c-dot--error: var(--error-color); } #memoryTimer, #layoutfluidTimer { diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts index c8e4ab6a5400..c6255d843fbc 100644 --- a/frontend/src/ts/commandline/commandline-metadata.ts +++ b/frontend/src/ts/commandline/commandline-metadata.ts @@ -546,6 +546,11 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = { options: "fromSchema", }, }, + typedEffect: { + subgroup: { + options: "fromSchema", + }, + }, tapeMode: { subgroup: { options: "fromSchema", diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index ec7fed5f62c5..169212ae540e 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -160,6 +160,7 @@ export const commands: CommandsSubgroup = { "timerColor", "timerOpacity", "highlightMode", + "typedEffect", "tapeMode", "tapeMargin", diff --git a/frontend/src/ts/config-metadata.ts b/frontend/src/ts/config-metadata.ts index a9794c851c81..12787734b205 100644 --- a/frontend/src/ts/config-metadata.ts +++ b/frontend/src/ts/config-metadata.ts @@ -558,6 +558,12 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "appearance", }, + typedEffect: { + icon: "fa-eye", + displayString: "typed effect", + changeRequiresRestart: false, + group: "appearance", + }, tapeMode: { icon: "fa-tape", triggerResize: true, diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts index 8c14cc3bb76f..7f2df921e2af 100644 --- a/frontend/src/ts/constants/default-config.ts +++ b/frontend/src/ts/constants/default-config.ts @@ -77,6 +77,7 @@ const obj: Config = { minWpm: "off", minWpmCustomSpeed: 100, highlightMode: "letter", + typedEffect: "keep", typingSpeedUnit: "wpm", ads: "result", hideExtraLetters: false, diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 6b2614dd24f6..72fff7a2da59 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -207,6 +207,7 @@ async function initGroups(): Promise { groups["liveAccStyle"] = new SettingsGroup("liveAccStyle", "button"); groups["liveBurstStyle"] = new SettingsGroup("liveBurstStyle", "button"); groups["highlightMode"] = new SettingsGroup("highlightMode", "button"); + groups["typedEffect"] = new SettingsGroup("typedEffect", "button"); groups["tapeMode"] = new SettingsGroup("tapeMode", "button"); groups["tapeMargin"] = new SettingsGroup("tapeMargin", "input", { validation: { schema: true, inputValueConvert: Number }, diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 1283eded513a..36b0b88279a6 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -457,10 +457,17 @@ function updateWordWrapperClasses(): void { const existing = wordsEl.native.className .split(/\s+/) - .filter((className) => !className.startsWith("highlight-")) ?? []; + .filter( + (className) => + !className.startsWith("highlight-") && + !className.startsWith("typed-effect-"), + ) ?? []; if (Config.highlightMode !== null) { existing.push("highlight-" + Config.highlightMode.replaceAll("_", "-")); } + if (Config.typedEffect !== null) { + existing.push("typed-effect-" + Config.typedEffect.replaceAll("_", "-")); + } wordsEl.native.className = existing.join(" "); @@ -2056,6 +2063,7 @@ ConfigEvent.subscribe(({ key, newValue }) => { if ( [ "highlightMode", + "typedEffect", "blindMode", "indicateTypos", "tapeMode", diff --git a/frontend/static/themes/dark_note.css b/frontend/static/themes/dark_note.css index 99fa4f7fd1bf..06d3c25476be 100644 --- a/frontend/static/themes/dark_note.css +++ b/frontend/static/themes/dark_note.css @@ -70,103 +70,3 @@ body::before { text-decoration-color: var(--error-color); text-decoration-thickness: 2px; } - -/* transform already typed letters into appropriately colored dots */ - -/* setting variables to the appropriate colors */ -#wordsWrapper { - --c-dot: var(--text-color); - --c-dot--error: var(--error-color); -} - -.colorfulMode { - --c-dot: var(--main-color); - --c-dot--error: var(--colorful-error-color); -} - -#words .typed letter { - animation: darkNoteToDust 200ms ease-out 0ms 1 forwards !important; -} -#words .typed letter::after { - animation: darkNoteFadeIn 100ms ease-in 100ms 1 forwards; -} - -.word letter { - position: relative; -} - -#words:not(.withLigatures) .word letter::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 1em; - height: 1em; - border-radius: 50%; - opacity: 0; -} - -#wordsWrapper .typed letter::after { - background: var(--c-dot); -} - -#wordsWrapper #words:not(.blind) .word letter.incorrect::after { - background: var(--c-dot--error); -} - -/* hide hint during dot transformation */ -hint { - transition: 300ms ease opacity; - opacity: 1; -} - -#wordsWrapper .word:not(.active) letter.incorrect hint { - opacity: 0; -} - -@media (prefers-reduced-motion) { - #words .typed letter { - animation: none !important; - transform: scale(0.4); - color: transparent; - } - #words .typed letter::after { - animation: none !important; - opacity: 1; - } -} - -@keyframes darkNoteFadeIn { - 0% { - opacity: 0; - } - 75% { - opacity: 0.4; - } - 100% { - opacity: 1; - } -} - -@keyframes darkNoteToDust { - 0% { - transform: scale(1); - color: var(--current-color); - } - 10% { - /* transform: scale(1); */ - } - 15% { - transform: scale(1); - color: var(--c-dot); - } - 80% { - /* transform: scale(0.5); */ - color: var(--c-dot); - } - 100% { - transform: scale(0.4); - color: transparent; - } -} diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts index d6e8e1d510cf..998ce464fbf2 100644 --- a/packages/schemas/src/configs.ts +++ b/packages/schemas/src/configs.ts @@ -182,6 +182,9 @@ export const HighlightModeSchema = z.enum([ ]); export type HighlightMode = z.infer; +export const TypedEffectSchema = z.enum(["keep", "hide", "fade", "dots"]); +export type TypedEffect = z.infer; + export const TapeModeSchema = z.enum(["off", "letter", "word"]); export type TapeMode = z.infer; @@ -441,6 +444,7 @@ export const ConfigSchema = z timerColor: TimerColorSchema, timerOpacity: TimerOpacitySchema, highlightMode: HighlightModeSchema, + typedEffect: TypedEffectSchema, tapeMode: TapeModeSchema, tapeMargin: TapeMarginSchema, smoothLineScroll: z.boolean(),