feat: Add brightness control functionality with signals

Add comprehensive brightness control feature with the following capabilities:
- Input signals for configuration (enableBrightnessControl, brightnessStep, minBrightness, maxBrightness)
- brightnessChanged output signal for reactive updates
- brightnessIn() and brightnessOut() methods for adjusting brightness
- resetBrightness() method to restore default value
- UI controls positioned at bottom center alongside zoom controls
- Keyboard accessibility (Enter/Space) and ARIA labels
- Visual feedback with disabled states at min/max brightness
- CSS brightness filter applied via reactive effect
This commit is contained in:
Claude 2025-11-15 16:07:36 +00:00
parent a6524480d5
commit 41ef9989e2
No known key found for this signature in database
3 changed files with 156 additions and 14 deletions

View file

@ -2,16 +2,47 @@
<ng-content></ng-content>
</div>
<!-- Control: one button -->
@if (isControl()) {
<div
class="pz-zoom-button pz-zoom-control-position-bottom"
[class.pz-zoom-button-out]="isZoomedIn()"
(click)="toggleZoom()"
(keydown.enter)="toggleZoom()"
(keydown.space)="toggleZoom(); $event.preventDefault()"
role="button"
[attr.aria-label]="isZoomedIn() ? 'Zoom out' : 'Zoom in'"
tabindex="0"
></div>
<!-- Controls container at bottom center -->
@if (isControl() || isBrightnessControl()) {
<div class="pz-controls-container">
<!-- Zoom control -->
@if (isControl()) {
<div
class="pz-zoom-button"
[class.pz-zoom-button-out]="isZoomedIn()"
(click)="toggleZoom()"
(keydown.enter)="toggleZoom()"
(keydown.space)="toggleZoom(); $event.preventDefault()"
role="button"
[attr.aria-label]="isZoomedIn() ? 'Zoom out' : 'Zoom in'"
tabindex="0"
></div>
}
<!-- Brightness controls -->
@if (isBrightnessControl()) {
<div class="pz-brightness-controls">
<div
class="pz-brightness-button pz-brightness-decrease"
[class.pz-brightness-button-disabled]="isBrightnessAtMin()"
(click)="brightnessOut()"
(keydown.enter)="brightnessOut()"
(keydown.space)="brightnessOut(); $event.preventDefault()"
role="button"
aria-label="Decrease brightness"
tabindex="0"
></div>
<div
class="pz-brightness-button pz-brightness-increase"
[class.pz-brightness-button-disabled]="isBrightnessAtMax()"
(click)="brightnessIn()"
(keydown.enter)="brightnessIn()"
(keydown.space)="brightnessIn(); $event.preventDefault()"
role="button"
aria-label="Increase brightness"
tabindex="0"
></div>
</div>
}
</div>
}

View file

@ -97,5 +97,54 @@
.pz-zoom-in
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgc3R5bGU9IiI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE5IDEzaC02djZoLTJ2LTZINXYtMmg2VjVoMnY2aDZ2MnoiIGlkPSJzdmdfMSIgY2xhc3M9IiIgc3Ryb2tlPSJub25lIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjEiLz48cGF0aCBkPSJNLTE1LjgzNjczNDQyMDQ2MTY1Myw0NC41MzU0MDkzMDY3MTAxOCBoNTguMjA0MDgwODI3NTkzMDkgdi02LjU3NjIyNjcyMzM2OTIyMTUgSC0xNS44MzY3MzQ0MjA0NjE2NTMgeiIgZmlsbD0ibm9uZSIgaWQ9InN2Z18yIiBjbGFzcz0iIiBzdHJva2U9Im5vbmUiLz48L2c+PC9zdmc+)
.pz-zoom-out
.pz-zoom-out
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3QgaWQ9ImJhY2tncm91bmRyZWN0IiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4PSIwIiB5PSIwIiBmaWxsPSJub25lIiBzdHJva2U9Im5vbmUiLz48ZyBjbGFzcz0iY3VycmVudExheWVyIiBzdHlsZT0iIj48dGl0bGU+TGF5ZXIgMTwvdGl0bGU+PHBhdGggZD0iTTE5IDEzSDV2LTJoMTR2MnoiIGlkPSJzdmdfMSIgY2xhc3M9IiIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIxIi8+PC9nPjwvc3ZnPg==)
/* Controls container */
.pz-controls-container
position: absolute
bottom: 16px
left: 50%
transform: translateX(-50%)
display: flex
gap: 12px
align-items: center
z-index: 1000
/* Brightness controls */
.pz-brightness-controls
display: flex
gap: 8px
align-items: center
.pz-brightness-button
position: relative
width: 48px
height: 48px
background-color: rgba(0, 0, 0, .8)
background-position: center
background-repeat: no-repeat
background-size: 24px
border-radius: 4px
opacity: 0.5
cursor: pointer
transition: opacity .1s
user-select: none
.pz-brightness-button:hover
opacity: 0.7
.pz-brightness-button-disabled
opacity: 0.3
cursor: not-allowed
.pz-brightness-button-disabled:hover
opacity: 0.3
.pz-brightness-increase
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0yMCA4LjY5VjRoLTQuNjlMMTIgLjY5IDguNjkgNEg0djQuNjlMLjY5IDEyIDQgMTUuMzFWMjBoNC42OUwxMiAyMy4zMSAxNS4zMSAyMEgyMHYtNC42OUwyMy4zMSAxMiAyMCA4LjY5ek0xMiAxOGMtMy4zMSAwLTYtMi42OS02LTZzMi42OS02IDYtNiA2IDIuNjkgNiA2LTIuNjkgNi02IDZ6bTAtMTBjLTIuMjEgMC00IDEuNzktNCA0czEuNzkgNCA0IDQgNC0xLjc5IDQtNC0xLjc5LTQtNC00eiIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg==)
.pz-brightness-decrease
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0yMCA4LjY5VjRoLTQuNjlMMTIgLjY5IDguNjkgNEg0djQuNjlMLjY5IDEyIDQgMTUuMzFWMjBoNC42OUwxMiAyMy4zMSAxNS4zMSAyMEgyMHYtNC42OUwyMy4zMSAxMiAyMCA4LjY5ek0xMiAxOGMtMy4zMSAwLTYtMi42OS02LTZzMi42OS02IDYtNiA2IDIuNjkgNiA2LTIuNjkgNi02IDZ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+)

View file

@ -66,11 +66,19 @@ export class PinchZoomComponent implements OnInit, OnDestroy {
draggableImage = input<boolean>(defaultProperties.draggableImage!);
draggableOnPinch = input<boolean>(defaultProperties.draggableOnPinch!);
// Output signal
// Brightness control inputs
enableBrightnessControl = input<boolean>(false);
brightnessStep = input<number>(0.1);
minBrightness = input<number>(0.1);
maxBrightness = input<number>(2.0);
// Output signals
zoomChanged = output<number>();
brightnessChanged = output<number>();
// Internal signals
private currentScale = signal<number>(1);
private currentBrightness = signal<number>(1.0);
// Computed signals for reactive properties
mergedProperties = computed<ComponentProperties>(() => {
@ -173,6 +181,26 @@ export class PinchZoomComponent implements OnInit, OnDestroy {
return true;
});
// Brightness computed signals
brightness = computed<number>(() => {
return this.currentBrightness();
});
isBrightnessControl = computed<boolean>(() => {
if (this.isDisabled()) {
return false;
}
return this.enableBrightnessControl();
});
isBrightnessAtMin = computed<boolean>(() => {
return this.brightness() <= this.minBrightness();
});
isBrightnessAtMax = computed<boolean>(() => {
return this.brightness() >= this.maxBrightness();
});
constructor() {
this.defaultComponentProperties = this.getDefaultComponentProperties();
@ -184,6 +212,15 @@ export class PinchZoomComponent implements OnInit, OnDestroy {
this.detectLimitZoom();
}
});
// Effect to apply brightness filter
effect(() => {
const brightness = this.brightness();
const element = this.elementRef.nativeElement.querySelector('.pinch-zoom-content') as HTMLElement;
if (element) {
element.style.filter = `brightness(${brightness})`;
}
});
}
ngOnInit(): void {
@ -231,6 +268,31 @@ export class PinchZoomComponent implements OnInit, OnDestroy {
return this.pinchZoom?.zoomOut(value) || this.scale();
}
brightnessIn(): number {
const newBrightness = Math.min(
this.brightness() + this.brightnessStep(),
this.maxBrightness()
);
this.currentBrightness.set(newBrightness);
this.brightnessChanged.emit(newBrightness);
return newBrightness;
}
brightnessOut(): number {
const newBrightness = Math.max(
this.brightness() - this.brightnessStep(),
this.minBrightness()
);
this.currentBrightness.set(newBrightness);
this.brightnessChanged.emit(newBrightness);
return newBrightness;
}
resetBrightness(): void {
this.currentBrightness.set(1.0);
this.brightnessChanged.emit(1.0);
}
detectLimitZoom(): void {
this.pinchZoom?.detectLimitZoom();
}