diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index 4bc31f9c639b..c8b1a24ed56d 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -994,6 +994,22 @@ +
+
+ + typed words + +
+
Change how typed words are shown.
+
+ + + + +
+
diff --git a/frontend/src/styles/animations.scss b/frontend/src/styles/animations.scss index d826c7659e35..326d5825f10a 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 typedWordsFadeIn { + 0% { + opacity: 0; + } + 75% { + opacity: 0.4; + } + 100% { + opacity: 1; + } +} + +@keyframes typedWordsToDust { + 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..6e71b255aa83 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-words-hide { + .word.typed { + opacity: 0; + } + } + + &.typed-words-fade { + .word.typed { + animation: fadeOut 250ms ease-in 1 forwards; + } + } + + &.typed-words-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: typedWordsToDust 200ms ease-out 0ms 1 forwards !important; + &::after { + animation: typedWordsFadeIn 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..44ecf78a0219 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", }, }, + typedWords: { + subgroup: { + options: "fromSchema", + }, + }, tapeMode: { subgroup: { options: "fromSchema", diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 2aab6e96cd60..862a5c4662f6 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -157,6 +157,7 @@ export const commands: CommandsSubgroup = { "timerColor", "timerOpacity", "highlightMode", + "typedWords", "tapeMode", "tapeMargin", diff --git a/frontend/src/ts/config-metadata.ts b/frontend/src/ts/config-metadata.ts index a9794c851c81..ae7457d345a9 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", }, + typedWords: { + icon: "fa-eye", + displayString: "typed words", + 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..81433e246102 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", + typedWords: "show", typingSpeedUnit: "wpm", ads: "result", hideExtraLetters: false, diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 201a3b52f10c..23fc839d7b3f 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -206,6 +206,7 @@ async function initGroups(): Promise { groups["liveAccStyle"] = new SettingsGroup("liveAccStyle", "button"); groups["liveBurstStyle"] = new SettingsGroup("liveBurstStyle", "button"); groups["highlightMode"] = new SettingsGroup("highlightMode", "button"); + groups["typedWords"] = new SettingsGroup("typedWords", "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 efbe30db3784..8bd96127be5e 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -450,10 +450,17 @@ function updateWordWrapperClasses(): void { const existing = wordsEl?.className .split(/\s+/) - .filter((className) => !className.startsWith("highlight-")) ?? []; + .filter( + (className) => + !className.startsWith("highlight-") && + !className.startsWith("typed-words-"), + ) ?? []; if (Config.highlightMode !== null) { existing.push("highlight-" + Config.highlightMode.replaceAll("_", "-")); } + if (Config.typedWords !== null) { + existing.push("typed-words-" + Config.typedWords.replaceAll("_", "-")); + } wordsEl.className = existing.join(" "); updateWordsWidth(); @@ -2033,6 +2040,7 @@ ConfigEvent.subscribe(({ key, newValue }) => { if ( [ "highlightMode", + "typedWords", "blindMode", "indicateTypos", "tapeMode", diff --git a/frontend/static/themes/dark_note.css b/frontend/static/themes/dark_note.css index 5b156b33ebbf..8205587ab5bf 100644 --- a/frontend/static/themes/dark_note.css +++ b/frontend/static/themes/dark_note.css @@ -88,103 +88,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..d02f7fae56aa 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 TypedWordsSchema = z.enum(["show", "hide", "fade", "dots"]); +export type TypedWords = 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, + typedWords: TypedWordsSchema, tapeMode: TapeModeSchema, tapeMargin: TapeMarginSchema, smoothLineScroll: z.boolean(),