diff --git a/Components/Inputs/CooldownButtonComponent.razor b/Components/Inputs/CooldownButtonComponent.razor new file mode 100644 index 0000000..73c5b1f --- /dev/null +++ b/Components/Inputs/CooldownButtonComponent.razor @@ -0,0 +1,138 @@ +@implements IDisposable + +
+ + @if (_isCooldown) + { +
+ @_remainingSeconds +
+ } +
+ + + +@code { + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public int CooldownSeconds { get; set; } = 12; + [Parameter] public int Size { get; set; } = 120; + + private bool _isCooldown; + private int _elapsedAngle; + private int _remainingSeconds; + private DateTime _startTime; + private System.Timers.Timer? _timer; + + private async Task HandleClick() + { + if (_isCooldown) return; + await OnClick.InvokeAsync(null); + StartCooldown(); + } + + private void StartCooldown() + { + _isCooldown = true; + _startTime = DateTime.UtcNow; + _elapsedAngle = 0; + _remainingSeconds = CooldownSeconds; + + _timer = new System.Timers.Timer(33); + _timer.Elapsed += OnTick; + _timer.AutoReset = true; + _timer.Enabled = true; + } + + private void OnTick(object? sender, System.Timers.ElapsedEventArgs e) + { + var elapsed = (DateTime.UtcNow - _startTime).TotalSeconds; + if (elapsed >= CooldownSeconds) + { + _isCooldown = false; + _timer?.Stop(); + _timer?.Dispose(); + _timer = null; + InvokeAsync(StateHasChanged); + return; + } + + _elapsedAngle = (int)(elapsed / CooldownSeconds * 360); + _remainingSeconds = CooldownSeconds - (int)elapsed; + InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + if (_timer != null) + { + _timer.Stop(); + _timer.Dispose(); + _timer = null; + } + } +} diff --git a/Pages/Pages/Home/HomePage.razor b/Pages/Pages/Home/HomePage.razor index db4b189..acbdc2c 100644 --- a/Pages/Pages/Home/HomePage.razor +++ b/Pages/Pages/Home/HomePage.razor @@ -14,6 +14,15 @@
Refer to various aspects of "IMMORTAL: Gates of Pyre" from this external reference!
+ +
+
Cooldown Demo
+ + Click Me + +
@@ -42,6 +51,18 @@ .mainContainer { padding-bottom: 32px; } + .cooldown-demo { + margin-top: 32px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + } + .cooldown-demo-label { + font-size: 1rem; + font-weight: 600; + color: var(--text-secondary, #aaa); + } .mainTitle { font-size: 2.2rem; @@ -62,4 +83,11 @@ grid-template-columns: 1fr; } } - \ No newline at end of file + + +@code { + private Task OnCooldownClick() + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/docs/.obsidian/workspace.json b/docs/.obsidian/workspace.json index 1ce87e7..f956ad5 100644 --- a/docs/.obsidian/workspace.json +++ b/docs/.obsidian/workspace.json @@ -42,12 +42,12 @@ "state": { "type": "markdown", "state": { - "file": "Build Calculator CmdLine.md", + "file": "Feature Proposals.md", "mode": "source", "source": false }, "icon": "lucide-file", - "title": "Build Calculator CmdLine" + "title": "Feature Proposals" } } ], @@ -225,10 +225,11 @@ }, "active": "68e1ba2b54081b9a", "lastOpenFiles": [ - "Device MAUI Pages Setup.md", + "cooldown-button.md", "Build Calculator CmdLine.md", - "Feature Proposals.md", "_Tasks Kanban.base", + "Device MAUI Pages Setup.md", + "Feature Proposals.md", "Tasks/Worker Income UI and Tests.md", "Tasks/Update the Reference Tables with Telerik.md", "Tasks/WebAssembly back to Azure.md", @@ -251,7 +252,6 @@ "Tasks/Highest Alloy and Ether Tests.md", "Tasks/Get AI to Add easy Test Tasks.md", "Tasks/Helper Tutorial Info Improvements.md", - "Tasks/Fix Entity Recursion Error - Parent.md", "Tasks", "Images/Pasted image 20260601093510.png", "Images/Pasted image 20260601083333.png", diff --git a/docs/cooldown-button.md b/docs/cooldown-button.md new file mode 100644 index 0000000..5bc85c2 --- /dev/null +++ b/docs/cooldown-button.md @@ -0,0 +1,111 @@ +# Cooldown Button Component + +A square Blazor button with a 12-second cooldown animation: when clicked, the button greys out and a circular transparency wedge dials open clockwise, progressively revealing the normal button state underneath. + +--- + +## Visual Design + +The button has two visual states: + +**Idle state** – A solid square button with the project's standard `--paper` background and `--primary` border. Hover inverts or brightens the paper. Click triggers a brief scale-down. + +**Cooldown state** – The native button fades to `opacity: 0` while an absolutely-positioned overlay covers it. The overlay uses a `conic-gradient` CSS mask to create a "dialling open" effect. A number in the centre shows the remaining seconds. + +--- + +## Core Technique: Conic-Gradient Mask + +The cooldown reveal is accomplished with a **conic-gradient mask** applied to the overlay `
`: + +``` +mask-image: conic-gradient( + transparent 0deg, + transparent {angle}deg, + #000 {angle}deg, + #000 360deg +); +``` + +- **`transparent`** – lets the button underneath show through (revealed area) +- **`#000` (black)** – fully masks the overlay, making it visible (greyed-out area) + +At `angle = 0deg`, transparent covers nothing and the mask is entirely black → the overlay is fully opaque (button completely greyed out). + +At `angle = 360deg`, transparent covers the full circle and black covers nothing → the overlay is fully transparent (button completely visible). + +The angle animates linearly from 0 to 360 over the cooldown duration. Because `conic-gradient` starts at the 12 o'clock position and sweeps clockwise, the reveal begins at the top of the button and rotates around, like a clock hand or a dial opening. + +--- + +## Implementation Architecture + +### Component Parameters + +| Parameter | Type | Default | Description | +|------------------|-------------------|---------|------------------------------------| +| `ChildContent` | `RenderFragment` | `null` | Text or content inside the button | +| `OnClick` | `EventCallback` | — | Fired when the button is clicked | +| `CooldownSeconds`| `int` | `12` | Duration of the cooldown in seconds| +| `Size` | `int` | `120` | Width and height in pixels (square)| + +### Timer Loop + +A `System.Timers.Timer` fires every ~33 ms (≈30 fps) during the cooldown: + +``` +OnTick: + elapsed = UtcNow - startTime + if elapsed >= CooldownSeconds → end cooldown, dispose timer + _elapsedAngle = (elapsed / CooldownSeconds) * 360 + _remainingSeconds = CooldownSeconds - (int)elapsed + InvokeAsync(StateHasChanged) +``` + +On each tick, `_elapsedAngle` is written into the overlay's inline `style` attribute, causing Blazor to re-render the `mask-image`. The timer is disposed in `Dispose()` to prevent leaks. + +### Disposal + +The component implements `IDisposable` to clean up the timer when the component is removed from the render tree. This follows the same pattern used by `SearchDialogComponent` and `BuildChartComponent` elsewhere in the codebase. + +--- + +## CSS Masking Details + +Two vendor-prefixed properties are set to ensure cross-browser support: + +``` +mask-image: conic-gradient(...); +-webkit-mask-image: conic-gradient(...); +``` + +The overlay uses `pointer-events: none` and `user-select: none` so that interaction passes through to the button underneath (which is disabled and transparent). + +An `rgba(22, 22, 24, 0.82)` semi-transparent background on the overlay produces the greyed-out appearance. The mask controls *where* this background is visible. + +--- + +## Usage on the Home Page + +The component is added to `Pages/Pages/Home/HomePage.razor` inside the first `PaperComponent`: + +```razor + + Click Me + +``` + +The `OnCooldownClick` handler in the page's `@code` block currently returns `Task.CompletedTask` (a no-op). This is the extension point where real work (e.g. triggering a game action, calling an API, showing a toast) would go. + +--- + +## Adapting the Component + +- **Change cooldown duration** – set `CooldownSeconds` to any positive integer. +- **Change button size** – set `Size` to any pixel dimension (button remains square). +- **Custom content** – pass any Blazor markup as `ChildContent` (text, icons, spinners). +- **Handle the click** – attach a handler to `OnClick` that returns `Task` or `void`. + +The `--cooldown-size` CSS custom property is set inline on the wrapper so that the label, overlay, and button all scale together.