import {abortableEffect} from '@pmndrs/uikit'; import {computed, ReadonlySignal, signal, Signal} from '@preact/signals-core'; import * as THREE from 'three'; import {DEFAULT_GRADIENT_PANEL_PROPS} from '../constants/GradientPanelConstants'; import {PANEL_BACKFACE_MARKER} from '../internal/PanelBackfaceVisibility'; import {Paint, StrokeAlign} from '../types/ShaderTypes'; import {ShaderPanel, ShaderPanelProperties} from './ShaderPanel'; import {UnifiedPanelLayer} from './layers/UnifiedPanelLayer'; /** * Properties for configuring a GradientPanel. * Supports Gradients for Stroke, InnerShadow, and DropShadow. */ export type GradientPanelProperties = Omit< ShaderPanelProperties, | 'borderColor' | 'borderOpacity' | 'backgroundColor' | 'backgroundOpacity' | 'borderRadius' | 'borderWidth' > & { /** Corner radius of the panel. */ cornerRadius?: number; /** Fill color or gradient. */ fillColor?: Paint; /** Optional fill rendered only on the rear face. */ backfaceColor?: Paint; /** Inner shadow color or gradient. */ innerShadowColor?: Paint; /** Blurring radius of the inner shadow. */ innerShadowBlur?: number; /** Directional offset of the inner shadow. */ innerShadowPosition?: THREE.Vector2 | [number, number]; /** Expansion of the inner shadow silhouette. */ innerShadowSpread?: number; /** Rate of exponential decay for the inner shadow. */ innerShadowFalloff?: number; /** Drop shadow color or gradient. */ dropShadowColor?: Paint; /** Blurring radius of the drop shadow. */ dropShadowBlur?: number; /** Directional offset of the drop shadow. */ dropShadowPosition?: THREE.Vector2 | [number, number]; /** Expansion of the drop shadow silhouette. */ dropShadowSpread?: number; /** Rate of exponential decay for the drop shadow. */ dropShadowFalloff?: number; /** Stroke color or gradient. */ strokeColor?: Paint; /** Width of the stroke. */ strokeWidth?: number; /** Alignment of the stroke (inside, outside, center). */ strokeAlign?: StrokeAlign; }; /** * GradientPanel. * Supports Gradients for Stroke, InnerShadow, and DropShadow. */ export class GradientPanel extends ShaderPanel { name = 'GradientPanel'; /** Signal storing the nesting level for order offset. */ private nestingLevelSignal!: ReadonlySignal; // Corner Radius /** Signal storing the corner radius. */ private cornerRadiusSignal: Signal; // Fill /** Signal storing the fill color/gradient. */ private fillColorSignal: Signal; /** Signal storing the optional rear-face color/gradient. */ private backfaceColorSignal?: Signal; // Inner Shadow /** Signal storing the inner shadow color/gradient. */ private innerShadowColorSignal: Signal; /** Signal storing the inner shadow blur radius. */ private innerShadowBlurSignal: Signal; /** Signal storing the inner shadow position offset. */ private innerShadowPositionSignal: Signal< THREE.Vector2 | [number, number] | undefined >; /** Signal storing the inner shadow spread expansion. */ private innerShadowSpreadSignal: Signal; /** Signal storing the inner shadow falloff rate. */ private innerShadowFalloffSignal: Signal; // Drop Shadow /** Signal storing the computed expansion margin for all layers. */ private expansionMarginSignal: Signal; /** Signal storing the drop shadow color/gradient. */ private dropShadowColorSignal: Signal; /** Signal storing the drop shadow blur radius. */ private dropShadowBlurSignal: Signal; /** Signal storing the drop shadow position offset. */ private dropShadowPositionSignal: Signal< THREE.Vector2 | [number, number] | undefined >; /** Signal storing the drop shadow spread expansion. */ private dropShadowSpreadSignal: Signal; /** Signal storing the drop shadow falloff rate. */ private dropShadowFalloffSignal: Signal; // Stroke /** Signal storing the stroke color/gradient. */ private strokeColorSignal: Signal; /** Signal storing the stroke width. */ private strokeWidthSignal: Signal; /** Signal storing the stroke alignment. */ private strokeAlignSignal!: Signal; // Layers /** Unified single-pass layer for rendering drop shadow, fill, inner shadow, and stroke. */ private unifiedLayer!: UnifiedPanelLayer; /** Optional layer for rendering one clean rear surface. */ private backfaceLayer?: UnifiedPanelLayer; // Constructor constructor(properties: GradientPanelProperties = {}) { const rawProps = properties as Record; // Corner Radius const cornerRadiusSignal = signal( rawProps.borderRadius !== undefined ? (rawProps.borderRadius as number) : ((properties.cornerRadius as number) ?? DEFAULT_GRADIENT_PANEL_PROPS.cornerRadius) ); // Fill const fillColorSignal = signal( rawProps.backgroundColor !== undefined ? (rawProps.backgroundColor as Paint) : (properties.fillColor ?? DEFAULT_GRADIENT_PANEL_PROPS.fillColor) ); const backfaceColorSignal = properties.backfaceColor === undefined ? undefined : signal(properties.backfaceColor); // Inner Shadow. const innerShadowColorSignal = signal( properties.innerShadowColor ?? DEFAULT_GRADIENT_PANEL_PROPS.innerShadowColor ); const innerShadowBlurSignal = signal( properties.innerShadowBlur ?? DEFAULT_GRADIENT_PANEL_PROPS.innerShadowBlur ); const innerShadowPositionSignal = signal( properties.innerShadowPosition ?? DEFAULT_GRADIENT_PANEL_PROPS.innerShadowPosition ); const innerShadowSpreadSignal = signal( properties.innerShadowSpread ?? DEFAULT_GRADIENT_PANEL_PROPS.innerShadowSpread ); const innerShadowFalloffSignal = signal( properties.innerShadowFalloff ?? DEFAULT_GRADIENT_PANEL_PROPS.innerShadowFalloff ); // Drop Shadow. const dropShadowColorSignal = signal( properties.dropShadowColor ?? DEFAULT_GRADIENT_PANEL_PROPS.dropShadowColor ); const dropShadowBlurSignal = signal( properties.dropShadowBlur ?? DEFAULT_GRADIENT_PANEL_PROPS.dropShadowBlur ); const dropShadowPositionSignal = signal( properties.dropShadowPosition ?? DEFAULT_GRADIENT_PANEL_PROPS.dropShadowPosition ); const dropShadowSpreadSignal = signal( properties.dropShadowSpread ?? DEFAULT_GRADIENT_PANEL_PROPS.dropShadowSpread ); const dropShadowFalloffSignal = signal( properties.dropShadowFalloff ?? DEFAULT_GRADIENT_PANEL_PROPS.dropShadowFalloff ); // Margin = max(DropShadowBlur + Position + Spread, StrokeWidth / 2). const shadowExpansion = computed(() => { const blur = Math.max(dropShadowBlurSignal.value, 0); const spread = Math.max(dropShadowSpreadSignal.value, 0); const pos = dropShadowPositionSignal.value; let extra = 0; if (pos) { if (Array.isArray(pos)) extra = Math.max(Math.abs(pos[0]), Math.abs(pos[1])); else extra = Math.max( Math.abs((pos as THREE.Vector2).x), Math.abs((pos as THREE.Vector2).y) ); } return blur + extra + spread; }); // Stroke const strokeColorSignal = signal( rawProps.borderColor !== undefined ? (rawProps.borderColor as Paint) : (properties.strokeColor ?? DEFAULT_GRADIENT_PANEL_PROPS.strokeColor) ); const strokeWidthSignal = signal( rawProps.borderWidth !== undefined ? (rawProps.borderWidth as number) : ((properties.strokeWidth as number) ?? DEFAULT_GRADIENT_PANEL_PROPS.strokeWidth) ); const strokeAlignSignal = signal( (properties.strokeAlign as StrokeAlign) ?? DEFAULT_GRADIENT_PANEL_PROPS.strokeAlign ); // Margin for layout expansion. const expansionMarginSignal = computed(() => { const s = shadowExpansion.value; const sWidth = strokeWidthSignal.value; const align = strokeAlignSignal.value; let strokeExpand = 0; if (align === 'center') strokeExpand = sWidth * 0.5; else if (align === 'outside') strokeExpand = sWidth; return s + strokeExpand; }); super({ ...properties, overflow: 'visible', }); // Unified single-pass layer this.unifiedLayer = new UnifiedPanelLayer({ fillColor: fillColorSignal, dropShadowColor: dropShadowColorSignal, dropShadowBlur: dropShadowBlurSignal, dropShadowPosition: dropShadowPositionSignal, dropShadowSpread: dropShadowSpreadSignal, dropShadowFalloff: dropShadowFalloffSignal, innerShadowColor: innerShadowColorSignal, innerShadowBlur: innerShadowBlurSignal, innerShadowPosition: innerShadowPositionSignal, innerShadowSpread: innerShadowSpreadSignal, innerShadowFalloff: innerShadowFalloffSignal, strokeColor: strokeColorSignal, strokeWidth: strokeWidthSignal, strokeAlign: strokeAlignSignal, cornerRadius: cornerRadiusSignal, dropShadowMargin: expansionMarginSignal, }); if (backfaceColorSignal) { this.backfaceLayer = new UnifiedPanelLayer( { fillColor: backfaceColorSignal, strokeColor: strokeColorSignal, strokeWidth: strokeWidthSignal, strokeAlign: strokeAlignSignal, cornerRadius: cornerRadiusSignal, dropShadowMargin: expansionMarginSignal, }, undefined, {side: THREE.BackSide} ); this.backfaceLayer.name = 'BackfaceLayer'; this.backfaceLayer.userData[PANEL_BACKFACE_MARKER] = true; this.addLayer(this.backfaceLayer); } this.addLayer(this.unifiedLayer); // Store Signals. this.cornerRadiusSignal = cornerRadiusSignal; this.fillColorSignal = fillColorSignal; this.backfaceColorSignal = backfaceColorSignal; this.innerShadowColorSignal = innerShadowColorSignal; this.innerShadowBlurSignal = innerShadowBlurSignal; this.innerShadowPositionSignal = innerShadowPositionSignal; this.innerShadowSpreadSignal = innerShadowSpreadSignal; this.innerShadowFalloffSignal = innerShadowFalloffSignal; this.dropShadowColorSignal = dropShadowColorSignal; this.dropShadowBlurSignal = dropShadowBlurSignal; this.dropShadowPositionSignal = dropShadowPositionSignal; this.dropShadowSpreadSignal = dropShadowSpreadSignal; this.dropShadowFalloffSignal = dropShadowFalloffSignal; this.expansionMarginSignal = expansionMarginSignal; this.strokeColorSignal = strokeColorSignal; this.strokeWidthSignal = strokeWidthSignal; this.strokeAlignSignal = strokeAlignSignal; // Layout & Z-Order. const absProps = { positionType: 'absolute' as const, pointerEvents: 'none' as const, }; this.unifiedLayer.setProperties({ ...absProps, transformTranslateZ: 0.001, pointerEvents: properties.pointerEvents ?? 'auto', }); this.backfaceLayer?.setProperties({ ...absProps, transformTranslateZ: -0.001, }); // Sync layout. const marginNeg = computed(() => -this.expansionMarginSignal.value); abortableEffect(() => { const m = marginNeg.value; const props = { positionTop: m, positionLeft: m, positionRight: m, positionBottom: m, }; this.unifiedLayer.setProperties(props); this.backfaceLayer?.setProperties(props); }, this.abortSignal); // Auto-calculate renderOrder based on Nesting Level to prevent nested Z-fighting. const nestingLevelSignal = computed(() => { let count = 0; let p = (this as unknown as {parentContainer?: {value: unknown}}) .parentContainer?.value; while (p != null) { count++; p = (p as unknown as {parentContainer?: {value: unknown}}) .parentContainer?.value; } return count; }); this.nestingLevelSignal = nestingLevelSignal; abortableEffect(() => { const level = nestingLevelSignal.value; // Physically separate sequential layers inside local Group Z-stack. const baseZ = level * 0.01; if (this.backfaceLayer) { this.backfaceLayer.position.z = baseZ - 0.001; } this.unifiedLayer.position.z = baseZ + 0.0015; const contentZ = baseZ + 0.004; for (const child of this.children) { if (child === this.unifiedLayer || child === this.backfaceLayer) { continue; } const childWithProps = child as unknown as { setProperties?: (props: unknown) => void; }; if (typeof childWithProps.setProperties === 'function') { childWithProps.setProperties({transformTranslateZ: contentZ}); } else { child.position.z = contentZ; } } }, this.abortSignal); } /** Overrides add method to cascade physical Z-position offsets for nested panels. */ override add(...objects: THREE.Object3D[]): this { super.add(...objects); const level = this.nestingLevelSignal?.value ?? 0; const baseZ = level * 0.01; const contentZ = baseZ + 0.004; for (const obj of objects) { if (obj === this.unifiedLayer || obj === this.backfaceLayer) { continue; } const objWithProps = obj as unknown as { setProperties?: (props: unknown) => void; }; if (typeof objWithProps.setProperties === 'function') { objWithProps.setProperties({transformTranslateZ: contentZ}); } else { obj.position.z = contentZ; } } return this; } // Setters. /** Sets the corner radius of the panel. */ setCornerRadius(radius: number) { this.cornerRadiusSignal.value = radius; } /** Sets the fill color or gradient. */ setFillColor(c: Paint) { this.fillColorSignal.value = c; } /** Sets the optional rear-face color or gradient. */ setBackfaceColor(c: Paint) { if (this.backfaceColorSignal) this.backfaceColorSignal.value = c; } /** Sets the inner shadow color or gradient. */ setInnerShadowColor(c: Paint) { this.innerShadowColorSignal.value = c; } /** Sets the inner shadow blur radius. */ setInnerShadowBlur(v: number) { this.innerShadowBlurSignal.value = v; } /** Sets the inner shadow position offset. */ setInnerShadowPosition(v: THREE.Vector2) { this.innerShadowPositionSignal.value = v; } /** Sets the inner shadow spread expansion. */ setInnerShadowSpread(v: number) { this.innerShadowSpreadSignal.value = v; } /** Sets the inner shadow falloff rate. */ setInnerShadowFalloff(v: number) { this.innerShadowFalloffSignal.value = v; } /** Sets the drop shadow color or gradient. */ setDropShadowColor(c: Paint) { this.dropShadowColorSignal.value = c; } /** Sets the drop shadow blur radius. */ setDropShadowBlur(v: number) { this.dropShadowBlurSignal.value = v; } /** Sets the drop shadow position offset. */ setDropShadowPosition(v: THREE.Vector2) { this.dropShadowPositionSignal.value = v; } /** Sets the drop shadow spread expansion. */ setDropShadowSpread(v: number) { this.dropShadowSpreadSignal.value = v; } /** Sets the drop shadow falloff rate. */ setDropShadowFalloff(v: number) { this.dropShadowFalloffSignal.value = v; } /** Sets the stroke color or gradient. */ setStrokeColor(c: Paint) { this.strokeColorSignal.value = c; } /** Sets the stroke width. */ setStrokeWidth(width: number) { this.strokeWidthSignal.value = width; } /** Sets the stroke alignment (inside, outside, center). */ setStrokeAlign(align: StrokeAlign) { this.strokeAlignSignal.value = align; } /** * Updates multiple properties at once. * Extracts known properties to apply them via signals/setters, * passing remaining properties to the super class. * @param props - Object containing properties to update. */ setProperties( props: Partial & Record ) { const { fillColor: rawFillColor, backgroundColor, backfaceColor, innerShadowColor, innerShadowBlur, innerShadowPosition, innerShadowSpread, innerShadowFalloff, dropShadowColor, dropShadowBlur, dropShadowPosition, dropShadowSpread, dropShadowFalloff, strokeColor: rawStrokeColor, borderColor, strokeWidth: rawStrokeWidth, borderWidth, strokeAlign, cornerRadius: rawCornerRadius, borderRadius, ...superProps } = props; const fillColor = ( backgroundColor !== undefined ? backgroundColor : rawFillColor ) as Paint | undefined; const strokeColor = ( borderColor !== undefined ? borderColor : rawStrokeColor ) as Paint | undefined; const strokeWidth = ( borderWidth !== undefined ? borderWidth : rawStrokeWidth ) as number | undefined; const cornerRadius = ( borderRadius !== undefined ? borderRadius : rawCornerRadius ) as number | undefined; // Pass the rest to ShaderPanel. super.setProperties(superProps); // 1. Fill. if (fillColor !== undefined) { this.setFillColor(fillColor); } if (backfaceColor !== undefined) { this.setBackfaceColor(backfaceColor); } // 2. Inner Shadow. if (innerShadowColor !== undefined) this.innerShadowColorSignal.value = innerShadowColor; if (innerShadowBlur !== undefined) this.innerShadowBlurSignal.value = innerShadowBlur; if (innerShadowPosition !== undefined) this.innerShadowPositionSignal.value = innerShadowPosition; if (innerShadowSpread !== undefined) this.innerShadowSpreadSignal.value = innerShadowSpread; if (innerShadowFalloff !== undefined) this.innerShadowFalloffSignal.value = innerShadowFalloff; // 3. Drop Shadow. if (dropShadowColor !== undefined) this.dropShadowColorSignal.value = dropShadowColor; if (dropShadowBlur !== undefined) this.dropShadowBlurSignal.value = dropShadowBlur; if (dropShadowPosition !== undefined) this.dropShadowPositionSignal.value = dropShadowPosition; if (dropShadowSpread !== undefined) this.dropShadowSpreadSignal.value = dropShadowSpread; if (dropShadowFalloff !== undefined) this.dropShadowFalloffSignal.value = dropShadowFalloff; // 4. Stroke. if (strokeColor !== undefined) { this.setStrokeColor(strokeColor); } if (strokeWidth !== undefined) { this.setStrokeWidth(strokeWidth); } if (strokeAlign !== undefined) { this.setStrokeAlign(strokeAlign); } // 5. Corner Radius. if (cornerRadius !== undefined) { this.setCornerRadius(cornerRadius); } } }