Tyler Freeman | 5f7036d | 2022-11-29 12:09:02 -0800 | [diff] [blame] | 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 | /** |
chihtinglo | 11bb037 | 2023-02-13 00:31:20 +0800 | [diff] [blame] | 27 | * Modify this to match your |
| 28 | * frameworks/base/packages/SettingsLib/res/values/arrays.xml#entryvalues_font_size |
Tyler Freeman | 5f7036d | 2022-11-29 12:09:02 -0800 | [diff] [blame] | 29 | * array so that all possible scales are generated. |
| 30 | */ |
| 31 | const scales = [1.15, 1.30, 1.5, 1.8, 2]; |
| 32 | |
| 33 | const 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 | */ |
| 38 | const 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 | */ |
| 62 | const 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. |
| 67 | const 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 | */ |
| 83 | function 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 | */ |
| 90 | function lerp(a, b, fraction) { |
| 91 | return (a * (1.0 - fraction)) + (b * fraction); |
| 92 | } |
| 93 | |
| 94 | function 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 | |
| 151 | const 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 | |
| 169 | function formatDigit(d) { |
| 170 | const twoSignificantDigits = Math.round(d * 100) / 100; |
| 171 | return String(twoSignificantDigits).padStart(4, ' '); |
| 172 | } |
| 173 | |
| 174 | console.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 | '')); |