1, 'preset'=>'none', 'mode'=>'none', 'color1'=>'', 'color2'=>'', 'color3'=>'', 'angle'=>135, 'image'=>Document::image([]), 'imageSize'=>'cover', 'imageRepeat'=>'no-repeat', 'attachment'=>'scroll', 'overlay'=>'', 'overlayOpacity'=>0, 'blend'=>'normal', 'blur'=>0, 'pattern'=>'none', 'patternOpacity'=>15, 'motion'=>'none', ]; } public static function clean($raw): array { $defaults=self::defaults(); if(!is_array($raw) || (int)($raw['version']??1)!==1){return $defaults;} $mode=Document::choice($raw['mode']??'',self::MODES,$defaults['mode']); $preset=Document::choice($raw['preset']??'',self::PRESETS,$defaults['preset']); if($mode!=='none'){$preset='none';} return [ 'version'=>1, 'preset'=>$preset, 'mode'=>$mode, 'color1'=>Document::color($raw['color1']??''), 'color2'=>Document::color($raw['color2']??''), 'color3'=>Document::color($raw['color3']??''), 'angle'=>Document::number($raw['angle']??$defaults['angle'],0,360), 'image'=>Document::image($raw['image']??[]), 'imageSize'=>Document::choice($raw['imageSize']??'',self::IMAGE_SIZES,$defaults['imageSize']), 'imageRepeat'=>Document::choice($raw['imageRepeat']??'',self::IMAGE_REPEATS,$defaults['imageRepeat']), 'attachment'=>Document::choice($raw['attachment']??'',self::ATTACHMENTS,$defaults['attachment']), 'overlay'=>Document::color($raw['overlay']??''), 'overlayOpacity'=>Document::number($raw['overlayOpacity']??$defaults['overlayOpacity'],0,100), 'blend'=>Document::choice($raw['blend']??'',self::BLENDS,$defaults['blend']), 'blur'=>Document::number($raw['blur']??$defaults['blur'],0,40), 'pattern'=>Document::choice($raw['pattern']??'',self::PATTERNS,$defaults['pattern']), 'patternOpacity'=>Document::number($raw['patternOpacity']??$defaults['patternOpacity'],0,100), 'motion'=>Document::choice($raw['motion']??'',self::MOTIONS,$defaults['motion']), ]; } public static function defaults_button(): array { return [ 'version'=>1, 'preset'=>'inherit', 'color1'=>'', 'color2'=>'', 'textTone'=>'auto', 'textColor'=>'', 'hoverColor1'=>'', 'hoverColor2'=>'', 'hoverTextColor'=>'', 'borderColor'=>'', 'hoverBorderColor'=>'', 'radius'=>-1, 'paddingX'=>0, 'paddingY'=>0, 'weight'=>0, 'letterSpacing'=>0, 'glow'=>0, 'effect'=>'none', 'transition'=>180, ]; } /** Backwards-compatible spelling used by the document schema. */ public static function button_defaults(): array { return self::defaults_button(); } public static function clean_button($raw): array { $defaults=self::defaults_button(); if(!is_array($raw) || (int)($raw['version']??1)!==1){return $defaults;} return [ 'version'=>1, 'preset'=>Document::choice($raw['preset']??'',self::BUTTON_PRESETS,$defaults['preset']), 'color1'=>Document::color($raw['color1']??''), 'color2'=>Document::color($raw['color2']??''), 'textTone'=>Document::choice($raw['textTone']??'',self::BUTTON_TEXT_TONES,$defaults['textTone']), 'textColor'=>Document::color($raw['textColor']??''), 'hoverColor1'=>Document::color($raw['hoverColor1']??''), 'hoverColor2'=>Document::color($raw['hoverColor2']??''), 'hoverTextColor'=>Document::color($raw['hoverTextColor']??''), 'borderColor'=>Document::color($raw['borderColor']??''), 'hoverBorderColor'=>Document::color($raw['hoverBorderColor']??''), 'radius'=>Document::number($raw['radius']??$defaults['radius'],-1,999), 'paddingX'=>Document::number($raw['paddingX']??$defaults['paddingX'],0,80), 'paddingY'=>Document::number($raw['paddingY']??$defaults['paddingY'],0,80), 'weight'=>Document::number($raw['weight']??$defaults['weight'],0,900), 'letterSpacing'=>Document::number($raw['letterSpacing']??$defaults['letterSpacing'],-2,8), 'glow'=>Document::number($raw['glow']??$defaults['glow'],0,60), 'effect'=>Document::choice($raw['effect']??'',self::BUTTON_EFFECTS,$defaults['effect']), 'transition'=>Document::number($raw['transition']??$defaults['transition'],0,600), ]; } /** Return scoped renderer values for a document or node surface. */ public static function attributes($raw): array { return self::surface_attributes($raw); } public static function surface_attributes($raw): array { $a=self::clean($raw); $active=$a['preset']!=='none'||$a['mode']!=='none'||$a['pattern']!=='none'||$a['motion']!=='none'||$a['overlay']!==''||$a['blur']>0||$a['blend']!=='normal'; if(!$active){return ['class'=>'','style'=>''];} $classes=['dbs-surface']; if($a['preset']!=='none'){$classes[]='dbs-surface-'.$a['preset'];} if($a['mode']!=='none'){$classes[]='dbs-surface-mode-'.$a['mode'];} if($a['pattern']!=='none'){$classes[]='dbs-surface-pattern-'.$a['pattern'];} if($a['motion']!=='none'){$classes[]='dbs-surface-motion-'.$a['motion'];} if(in_array($a['mode'],['solid','linear','radial','conic'],true)){ $author_colors=array_values(array_filter([$a['color1'],$a['color2'],$a['color3']],static fn($color)=>$color!=='')); if($author_colors){$classes[]='dbs-surface-tone-auto-'.self::automatic_tone($author_colors);} } $css=[]; foreach(['color1'=>'--dbs-surface-color-1','color2'=>'--dbs-surface-color-2','color3'=>'--dbs-surface-color-3'] as $key=>$property){ if($a[$key]!==''){$css[]=$property.':'.$a[$key];} } if(in_array($a['mode'],['linear','conic'],true)){$css[]='--dbs-surface-angle:'.self::number_text($a['angle']).'deg';} if($a['mode']==='image' && $a['image']['url']!==''){ $css[]='--dbs-surface-image:url("'.self::css_url($a['image']['url']).'")'; $css[]='--dbs-surface-image-size:'.$a['imageSize']; $css[]='--dbs-surface-image-repeat:'.$a['imageRepeat']; $css[]='--dbs-surface-attachment:'.$a['attachment']; $css[]='--dbs-surface-position:'.self::number_text($a['image']['x']).'% '.self::number_text($a['image']['y']).'%'; } if($a['overlay']!==''){ $css[]='--dbs-surface-overlay:'.$a['overlay']; $css[]='--dbs-surface-overlay-opacity:'.self::ratio($a['overlayOpacity']); } if($a['blend']!=='normal'){$css[]='--dbs-surface-blend:'.$a['blend'];} if($a['blur']>0){$css[]='--dbs-surface-blur:'.self::number_text($a['blur']).'px';} if($a['pattern']!=='none'){$css[]='--dbs-surface-pattern-opacity:'.self::ratio($a['patternOpacity']);} return ['class'=>implode(' ',$classes),'style'=>self::style($css)]; } /** Return scoped renderer values for one button or a button wrapper. */ public static function button_attributes($raw): array { $b=self::clean_button($raw); $defaults=self::defaults_button(); $colors=['color1','color2','textColor','hoverColor1','hoverColor2','hoverTextColor','borderColor','hoverBorderColor']; $hasColor=false;foreach($colors as $key){if($b[$key]!==''){$hasColor=true;break;}} $active=$b['preset']!=='inherit'||$b['textTone']!=='auto'||$b['effect']!=='none'||$hasColor||$b['radius']>=0||$b['paddingX']>0||$b['paddingY']>0||$b['weight']>0||$b['letterSpacing']!=0||$b['glow']>0||$b['transition']!=$defaults['transition']; if(!$active){return ['class'=>'','style'=>''];} $classes=['dbs-button-style-local']; if($b['preset']!=='inherit'){$classes[]='dbs-button-preset-'.$b['preset'];} if($b['textTone']!=='auto'){$classes[]='dbs-button-tone-'.$b['textTone'];} elseif($b['textColor']===''&&($b['color1']!==''||$b['color2']!=='')){$classes[]='dbs-button-tone-auto-'.self::automatic_button_tone($b);} if($b['textTone']==='auto'&&$b['hoverTextColor']===''&&($b['hoverColor1']!==''||$b['hoverColor2']!=='')){ $classes[]='dbs-button-hover-tone-auto-'.self::automatic_tone(array_values(array_filter([$b['hoverColor1'],$b['hoverColor2']],static fn($color)=>$color!==''))); } if($b['effect']!=='none'){$classes[]='dbs-button-effect-'.$b['effect'];} $css=[]; $properties=[ 'color1'=>'--dbs-button-color-1','color2'=>'--dbs-button-color-2', 'textColor'=>'--dbs-button-text-local','hoverColor1'=>'--dbs-button-hover-color-1', 'hoverColor2'=>'--dbs-button-hover-color-2','hoverTextColor'=>'--dbs-button-hover-text', 'borderColor'=>'--dbs-button-border-local','hoverBorderColor'=>'--dbs-button-hover-border', ]; foreach($properties as $key=>$property){if($b[$key]!==''){$css[]=$property.':'.$b[$key];}} if($b['radius']>=0){$css[]='--dbs-button-radius:'.self::number_text($b['radius']).'px';} if($b['paddingX']>0){$css[]='--dbs-button-padding-x:'.self::number_text($b['paddingX']).'px';} if($b['paddingY']>0){$css[]='--dbs-button-padding-y:'.self::number_text($b['paddingY']).'px';} if($b['weight']>0){$css[]='--dbs-button-weight:'.self::number_text($b['weight']);} if($b['letterSpacing']!=0){$css[]='--dbs-button-letter-spacing:'.self::number_text($b['letterSpacing']).'px';} if($b['glow']>0){$css[]='--dbs-button-glow:'.self::number_text($b['glow']).'px';} $css[]='--dbs-button-transition:'.self::number_text($b['transition']).'ms'; return ['class'=>implode(' ',$classes),'style'=>self::style($css)]; } public static function button_wrapper_attributes($raw): array { return self::button_attributes($raw); } private static function css_url(string $url): string { return strtr($url,[ '"'=>'%22',"'"=>'%27','('=>'%28',')'=>'%29','\\'=>'%5C', "\n"=>'%0A',"\r"=>'%0D',"\t"=>'%09','<'=>'%3C','>'=>'%3E', ]); } private static function style(array $declarations): string { return $declarations?implode(';',$declarations).';':''; } private static function ratio($value): string { return self::number_text(((float)$value)/100); } /** Pick the higher-contrast bounded text tone for author-selected colours. */ private static function automatic_button_tone(array $button): string { return self::automatic_tone(array_values(array_filter([ (string)($button['color1']??''),(string)($button['color2']??''), ],static fn($color)=>$color!==''))); } /** Select white or deep-ink text using the strongest worst-case contrast. */ private static function automatic_tone(array $colors): string { $backgrounds=[]; foreach($colors as $color){$rgb=self::rgb((string)$color);if($rgb!==null){$backgrounds[]=$rgb;}} if(!$backgrounds){return 'light';} $white=[255.0,255.0,255.0];$dark=[23.0,40.0,60.0]; $white_min=INF;$dark_min=INF; foreach($backgrounds as $background){ $white_min=min($white_min,self::contrast($background,$white)); $dark_min=min($dark_min,self::contrast($background,$dark)); } return $white_min >= $dark_min ? 'light' : 'dark'; } private static function rgb(string $color): ?array { if(!preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6}|[a-f0-9]{8})$/iD',$color,$match)){return null;} $hex=$match[1]; if(strlen($hex)===3){$hex=$hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];} $alpha=1.0; if(strlen($hex)===8){$alpha=hexdec(substr($hex,6,2))/255;$hex=substr($hex,0,6);} $rgb=[hexdec(substr($hex,0,2)),hexdec(substr($hex,2,2)),hexdec(substr($hex,4,2))]; return array_map(static fn($channel)=>(float)$channel*$alpha+255.0*(1-$alpha),$rgb); } private static function contrast(array $first,array $second): float { $luminance=static function(array $rgb): float { $channels=array_map(static function($channel): float {$value=((float)$channel)/255;return $value<=0.04045?$value/12.92:pow(($value+0.055)/1.055,2.4);},$rgb); return 0.2126*$channels[0]+0.7152*$channels[1]+0.0722*$channels[2]; }; $a=$luminance($first);$b=$luminance($second); return (max($a,$b)+0.05)/(min($a,$b)+0.05); } private static function number_text($value): string { $text=rtrim(rtrim(number_format((float)$value,4,'.',''),'0'),'.'); return $text===''||$text==='-0'?'0':$text; } }