1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17/** 18 Generates arrays for non-linear font scaling, to be pasted into 19 frameworks/base/core/java/android/content/res/FontScaleConverterFactory.java 20 21 To use: 22 `node font-scaling-array-generator.js` 23 or just open a browser, open DevTools, and paste into the Console. 24*/ 25 26/** 27 * Modify this to match your 28 * frameworks/base/packages/SettingsLib/res/values/arrays.xml#entryvalues_font_size 29 * array so that all possible scales are generated. 30 */ 31const scales = [1.15, 1.30, 1.5, 1.8, 2]; 32 33const commonSpSizes = [8, 10, 12, 14, 18, 20, 24, 30, 100]; 34 35/** 36 * Enum for GENERATION_STYLE which determines how to generate the arrays. 37 */ 38const GenerationStyle = { 39 /** 40 * Interpolates between hand-tweaked curves. This is the best option and 41 * shouldn't require any additional tweaking. 42 */ 43 CUSTOM_TWEAKED: 'CUSTOM_TWEAKED', 44 45 /** 46 * Uses a curve equation that is mostly correct, but will need manual tweaking 47 * at some scales. 48 */ 49 CURVE: 'CURVE', 50 51 /** 52 * Uses straight linear multiplication. Good starting point for manual 53 * tweaking. 54 */ 55 LINEAR: 'LINEAR' 56} 57 58/** 59 * Determines how arrays are generated. Must be one of the GenerationStyle 60 * values. 61 */ 62const GENERATION_STYLE = GenerationStyle.CUSTOM_TWEAKED; 63 64// These are hand-tweaked curves from which we will derive the other 65// interstitial curves using linear interpolation, in the case of using 66// GenerationStyle.CUSTOM_TWEAKED. 67const interpolationTargets = { 68 1.0: commonSpSizes, 69 1.5: [12, 15, 18, 22, 24, 26, 28, 30, 100], 70 2.0: [16, 20, 24, 26, 30, 34, 36, 38, 100] 71}; 72 73/** 74 * Interpolate a value with specified extrema, to a new value between new 75 * extrema. 76 * 77 * @param value the current value 78 * @param inputMin minimum the input value can reach 79 * @param inputMax maximum the input value can reach 80 * @param outputMin minimum the output value can reach 81 * @param outputMax maximum the output value can reach 82 */ 83function map(value, inputMin, inputMax, outputMin, outputMax) { 84 return outputMin + (outputMax - outputMin) * ((value - inputMin) / (inputMax - inputMin)); 85} 86 87/*** 88 * Interpolate between values a and b. 89 */ 90function lerp(a, b, fraction) { 91 return (a * (1.0 - fraction)) + (b * fraction); 92} 93 94function generateRatios(scale) { 95 // Find the best two arrays to interpolate between. 96 let startTarget, endTarget; 97 let startTargetScale, endTargetScale; 98 const targetScales = Object.keys(interpolationTargets).sort(); 99 for (let i = 0; i < targetScales.length - 1; i++) { 100 const targetScaleKey = targetScales[i]; 101 const targetScale = parseFloat(targetScaleKey, 10); 102 const startTargetScaleKey = targetScaleKey; 103 const endTargetScaleKey = targetScales[i + 1]; 104 105 if (scale < parseFloat(startTargetScaleKey, 10)) { 106 break; 107 } 108 109 startTargetScale = parseFloat(startTargetScaleKey, 10); 110 endTargetScale = parseFloat(endTargetScaleKey, 10); 111 startTarget = interpolationTargets[startTargetScaleKey]; 112 endTarget = interpolationTargets[endTargetScaleKey]; 113 } 114 const interpolationProgress = map(scale, startTargetScale, endTargetScale, 0, 1); 115 116 return commonSpSizes.map((sp, i) => { 117 const originalSizeDp = sp; 118 let newSizeDp; 119 switch (GENERATION_STYLE) { 120 case GenerationStyle.CUSTOM_TWEAKED: 121 newSizeDp = lerp(startTarget[i], endTarget[i], interpolationProgress); 122 break; 123 case GenerationStyle.CURVE: { 124 let coeff1; 125 let coeff2; 126 if (scale < 1) { 127 // \left(1.22^{-\left(x+5\right)}+0.5\right)\cdot x 128 coeff1 = -5; 129 coeff2 = scale; 130 } else { 131 // (1.22^{-\left(x-10\right)}+1\right)\cdot x 132 coeff1 = map(scale, 1, 2, 2, 8); 133 coeff2 = 1; 134 } 135 newSizeDp = ((Math.pow(1.22, (-(originalSizeDp - coeff1))) + coeff2) * originalSizeDp); 136 break; 137 } 138 case GenerationStyle.LINEAR: 139 newSizeDp = originalSizeDp * scale; 140 break; 141 default: 142 throw new Error('Invalid GENERATION_STYLE'); 143 } 144 return { 145 fromSp: sp, 146 toDp: newSizeDp 147 } 148 }); 149} 150 151const scaleArrays = 152 scales 153 .map(scale => { 154 const scaleString = (scale * 100).toFixed(0); 155 return { 156 scale, 157 name: `font_size_original_sp_to_scaled_dp_${scaleString}_percent` 158 } 159 }) 160 .map(scaleArray => { 161 const items = generateRatios(scaleArray.scale); 162 163 return { 164 ...scaleArray, 165 items 166 } 167 }); 168 169function formatDigit(d) { 170 const twoSignificantDigits = Math.round(d * 100) / 100; 171 return String(twoSignificantDigits).padStart(4, ' '); 172} 173 174console.log( 175 '' + 176 scaleArrays.reduce( 177 (previousScaleArray, currentScaleArray) => { 178 const itemsFromSp = currentScaleArray.items.map(d => d.fromSp) 179 .map(formatDigit) 180 .join('f, '); 181 const itemsToDp = currentScaleArray.items.map(d => d.toDp) 182 .map(formatDigit) 183 .join('f, '); 184 185 return previousScaleArray + ` 186 put( 187 /* scaleKey= */ ${currentScaleArray.scale}f, 188 new FontScaleConverter( 189 /* fromSp= */ 190 new float[] {${itemsFromSp}}, 191 /* toDp= */ 192 new float[] {${itemsToDp}}) 193 ); 194 `; 195 }, 196 '')); 197