|
@use "sass:math"; |
|
|
|
// ------------------------------------------------------------------- |
|
// Modular Type Scale Ratios |
|
// ------------------------------------------------------------------- |
|
$scale-ratios: (minor-second: 1.067, |
|
major-second: 1.125, |
|
minor-third: 1.2, |
|
major-third: 1.25, |
|
perfect-fourth: 1.333, |
|
augmented-fourth: 1.414, |
|
perfect-fifth: 1.5, |
|
golden-ratio: 1.618); |
|
|
|
// ------------------------------------------------------------------- |
|
// Utility: Round to a fixed number of decimal places |
|
// ------------------------------------------------------------------- |
|
@function round($value, $places: 4) { |
|
$factor: math.pow(10, $places); |
|
@return math.div(math.round($value * $factor), $factor); |
|
} |
|
|
|
// ------------------------------------------------------------------- |
|
// Utility: Convert between px and rem |
|
// ------------------------------------------------------------------- |
|
@function convert-to-unit($value, $target-unit, $stringify: false) { |
|
$from-unit: unit($value); |
|
$from-lower: to-lower-case($from-unit); |
|
$target-lower: to-lower-case($target-unit); |
|
|
|
@if $target-lower=="unitless" { |
|
@return math.div($value, 1#{$from-unit}); |
|
} |
|
|
|
// Return raw numeric result; format only if $stringify is true |
|
@if $from-lower==$target-lower { |
|
@return if($stringify and $target-unit=="PX", unquote("#{math.div($value, 1px)}PX"), $value); |
|
} |
|
|
|
// rem → px |
|
@if $from-lower=="rem" and $target-lower=="px" { |
|
$px-value: math.div($value, 1rem) * 16px; |
|
@return if($stringify and $target-unit=="PX", unquote("#{math.div($px-value, 1px)}PX"), $px-value); |
|
} |
|
|
|
// px → rem |
|
@if $from-lower=="px" and $target-lower=="rem" { |
|
@return math.div($value, 16px) * 1rem; |
|
} |
|
|
|
@error "Unsupported conversion: #{$from-unit} to #{$target-unit}"; |
|
} |
|
|
|
// ------------------------------------------------------------------- |
|
// Utility: Generate viewport-based clamp() |
|
// ------------------------------------------------------------------- |
|
|
|
@function generate-clamp-interpolation($min-font, |
|
$max-font, |
|
$min-screen: 320px, |
|
$max-screen: 1200px, |
|
$output-unit: px) { |
|
// Use math.div only on numeric values |
|
$slope: math.div(($max-font - $min-font), ($max-screen - $min-screen)); |
|
$intercept: $min-font - ($slope * $min-screen); |
|
|
|
$vw-coeff: round($slope * 100, 4); |
|
$base: round($intercept, 4); |
|
|
|
// Optional formatting |
|
@if $output-unit=="unitless" { |
|
@return unquote("clamp(#{math.div($min-font, 1px)}, #{$vw-coeff}vw + #{math.div($base, 1px)}, #{math.div($max-font, 1px)})"); |
|
} |
|
|
|
@if $output-unit=="PX" { |
|
@return unquote("clamp(#{math.div($min-font, 1px)}PX, #{$vw-coeff}vw + #{math.div($base, 1px)}PX, #{math.div($max-font, 1px)}PX)"); |
|
} |
|
|
|
@return unquote("clamp(#{$min-font}, #{$vw-coeff}vw + #{$base}, #{$max-font})"); |
|
} |
|
|
|
// ------------------------------------------------------------------- |
|
// Mixin: Static Modular Scale |
|
// ------------------------------------------------------------------- |
|
@mixin generate-static-scale($steps, |
|
$base-size: 1rem, |
|
$scale: perfect-fourth, |
|
$output-unit: rem) { |
|
$ratio: map-get($scale-ratios, $scale); |
|
|
|
@if not $ratio { |
|
@error "Unknown scale `#{$scale}`. Available: #{map-keys($scale-ratios)}"; |
|
} |
|
|
|
@each $label, |
|
$step in $steps { |
|
$size: $base-size * math.pow($ratio, $step); |
|
$converted-size: convert-to-unit(round($size), $output-unit, false); // math-safe |
|
$formatted-size: convert-to-unit($converted-size, $output-unit, true); // stringify for output |
|
|
|
#{"--ts-#{$label}"}: #{$formatted-size}; |
|
|
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------- |
|
// Mixin: Fluid Modular Scale with clamp() |
|
// ------------------------------------------------------------------- |
|
@mixin generate-fluid-modular-scale($steps, |
|
$base-size: 1rem, |
|
$scale: perfect-fourth, |
|
$unit-step: 1cqw, |
|
$bias: 0.5rem, |
|
$min-mult: 0.9, |
|
$max-mult: 1.1, |
|
$interpolation: false, |
|
$output-unit: rem) { |
|
$ratio: map-get($scale-ratios, $scale); |
|
|
|
@if not $ratio { |
|
@error "Unknown scale `#{$scale}`. Available: #{map-keys($scale-ratios)}"; |
|
} |
|
|
|
@each $label, |
|
$step in $steps { |
|
$size: if($interpolation, |
|
$base-size * (math.pow($ratio, math.floor($step)) * (1 - ($step - math.floor($step))) + math.pow($ratio, math.ceil($step)) * ($step - math.floor($step))), |
|
$base-size * math.pow($ratio, $step)); |
|
|
|
// Convert with raw math-safe values |
|
$min: convert-to-unit(round($size * $min-mult), $output-unit, false); |
|
$max: convert-to-unit(round($size * $max-mult), $output-unit, false); |
|
$bias-conv: convert-to-unit($bias, $output-unit, false); |
|
$fluid: $step * $unit-step; |
|
|
|
@if $min==$max or $fluid==0 or $bias-conv==0 { |
|
$final-val: convert-to-unit($min, $output-unit, true); |
|
#{"--ts-#{$label}"}: #{$final-val}; |
|
|
|
} |
|
|
|
@else { |
|
$min-str: convert-to-unit($min, $output-unit, true); |
|
$max-str: convert-to-unit($max, $output-unit, true); |
|
$bias-str: convert-to-unit($bias-conv, $output-unit, true); |
|
#{"--ts-#{$label}"}: unquote("clamp(#{$min-str}, calc(#{$fluid} + #{$bias-str}), #{$max-str})"); |
|
|
|
} |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------- |
|
// Mixin: Tool-style Interpolated Clamp Scale |
|
// ------------------------------------------------------------------- |
|
@mixin generate-interpolated-scale($steps, |
|
$scale: null, |
|
$base-size: 1rem, |
|
$min-mult: 0.9, |
|
$max-mult: 1.1, |
|
$output-unit: rem, |
|
$min-screen: 320px, |
|
$max-screen: 1200px) { |
|
|
|
@each $label, |
|
$step in $steps { |
|
@if unitless($step) { |
|
@if $scale==null { |
|
@error "Interpolation mode requires $scale for unitless step `#{$label}`."; |
|
} |
|
|
|
$ratio: map-get($scale-ratios, $scale); |
|
|
|
@if not $ratio { |
|
@error "Unknown scale `#{$scale}`. Available: #{map-keys($scale-ratios)}"; |
|
} |
|
|
|
$size: $base-size * math.pow($ratio, $step); |
|
|
|
$min: convert-to-unit(round($size * $min-mult), $output-unit, false); |
|
$max: convert-to-unit(round($size * $max-mult), $output-unit, false); |
|
|
|
#{"--ts-#{$label}"}: #{generate-clamp-interpolation($min, $max, $min-screen, $max-screen, $output-unit)}; |
|
|
|
} |
|
|
|
@else { |
|
$min: convert-to-unit($step, $output-unit, false); |
|
$max: convert-to-unit($step * $max-mult, $output-unit, false); |
|
|
|
#{"--ts-#{$label}"}: #{generate-clamp-interpolation($min, $max, $min-screen, $max-screen, $output-unit)}; |
|
|
|
} |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------- |
|
// Mixin: Step-to-Step Interpolated Scale |
|
// ------------------------------------------------------------------- |
|
// Each label interpolates between the previous and current step |
|
// Must provide exact size values (e.g. in px or rem) per label |
|
// ------------------------------------------------------------------- |
|
|
|
@mixin generate-step-to-step-interpolated-scale($steps, |
|
$output-unit: rem, |
|
$min-screen: 320px, |
|
$max-screen: 1200px) { |
|
$prev-size: null; |
|
|
|
@each $label, |
|
$value in $steps { |
|
// Use raw mode — don't stringify here |
|
$curr-size: convert-to-unit($value, $output-unit, false); |
|
|
|
@if $prev-size==null { |
|
// Only stringify when assigning to CSS custom prop |
|
$formatted: convert-to-unit($curr-size, $output-unit, true); |
|
#{"--ts-#{$label}"}: #{$formatted}; |
|
|
|
} |
|
|
|
@else { |
|
$min: $prev-size; |
|
$max: $curr-size; |
|
#{"--ts-#{$label}"}: #{generate-clamp-interpolation($min, $max, $min-screen, $max-screen, $output-unit)}; |
|
|
|
} |
|
|
|
$prev-size: $curr-size; |
|
} |
|
} |