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 | /** |
| 27 | * Modify this to match your packages/apps/Settings/res/arrays.xml#entryvalues_font_size |
| 28 | * array so that all possible scales are generated. |
| 29 | */ |
| 30 | const scales = [1.15, 1.30, 1.5, 1.8, 2]; |
| 31 | |
| 32 | const commonSpSizes = [8, 10, 12, 14, 18, 20, 24, 30, 100]; |
| 33 | |
| 34 | /** |
| 35 | * Enum for GENERATION_STYLE which determines how to generate the arrays. |
| 36 | */ |
| 37 | const GenerationStyle = { |
| 38 | /** |
| 39 | * Interpolates between hand-tweaked curves. This is the best option and |
| 40 | * shouldn't require any additional tweaking. |
| 41 | */ |
| 42 | CUSTOM_TWEAKED: 'CUSTOM_TWEAKED', |
| 43 | |
| 44 | /** |
| 45 | * Uses a curve equation that is mostly correct, but will need manual tweaking |
| 46 | * at some scales. |
| 47 | */ |
| 48 | CURVE: 'CURVE', |
| 49 | |
| 50 | /** |
| 51 | * Uses straight linear multiplication. Good starting point for manual |
| 52 | * tweaking. |
| 53 | */ |
| 54 | LINEAR: 'LINEAR' |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Determines how arrays are generated. Must be one of the GenerationStyle |
| 59 | * values. |
| 60 | */ |
| 61 | const GENERATION_STYLE = GenerationStyle.CUSTOM_TWEAKED; |
| 62 | |
| 63 | // These are hand-tweaked curves from which we will derive the other |
| 64 | // interstitial curves using linear interpolation, in the case of using |
| 65 | // GenerationStyle.CUSTOM_TWEAKED. |
| 66 | const interpolationTargets = { |
| 67 | 1.0: commonSpSizes, |
| 68 | 1.5: [12, 15, 18, 22, 24, 26, 28, 30, 100], |
| 69 | 2.0: [16, 20, 24, 26, 30, 34, 36, 38, 100] |
| 70 | }; |
| 71 | |
| 72 | /** |
| 73 | * Interpolate a value with specified extrema, to a new value between new |
| 74 | * extrema. |
| 75 | * |
| 76 | * @param value the current value |
| 77 | * @param inputMin minimum the input value can reach |
| 78 | * @param inputMax maximum the input value can reach |
| 79 | * @param outputMin minimum the output value can reach |
| 80 | * @param outputMax maximum the output value can reach |
| 81 | */ |
| 82 | function map(value, inputMin, inputMax, outputMin, outputMax) { |
| 83 | return outputMin + (outputMax - outputMin) * ((value - inputMin) / (inputMax - inputMin)); |
| 84 | } |
| 85 | |
| 86 | /*** |
| 87 | * Interpolate between values a and b. |
| 88 | */ |
| 89 | function lerp(a, b, fraction) { |
| 90 | return (a * (1.0 - fraction)) + (b * fraction); |
| 91 | } |
| 92 | |
| 93 | function generateRatios(scale) { |
| 94 | // Find the best two arrays to interpolate between. |
| 95 | let startTarget, endTarget; |
| 96 | let startTargetScale, endTargetScale; |
| 97 | const targetScales = Object.keys(interpolationTargets).sort(); |
| 98 | for (let i = 0; i < targetScales.length - 1; i++) { |
| 99 | const targetScaleKey = targetScales[i]; |
| 100 | const targetScale = parseFloat(targetScaleKey, 10); |
| 101 | const startTargetScaleKey = targetScaleKey; |
| 102 | const endTargetScaleKey = targetScales[i + 1]; |
| 103 | |
| 104 | if (scale < parseFloat(startTargetScaleKey, 10)) { |
| 105 | break; |
| 106 | } |
| 107 | |
| 108 | startTargetScale = parseFloat(startTargetScaleKey, 10); |
| 109 | endTargetScale = parseFloat(endTargetScaleKey, 10); |
| 110 | startTarget = interpolationTargets[startTargetScaleKey]; |
| 111 | endTarget = interpolationTargets[endTargetScaleKey]; |
| 112 | } |
| 113 | const interpolationProgress = map(scale, startTargetScale, endTargetScale, 0, 1); |
| 114 | |
| 115 | return commonSpSizes.map((sp, i) => { |
| 116 | const originalSizeDp = sp; |
| 117 | let newSizeDp; |
| 118 | switch (GENERATION_STYLE) { |
| 119 | case GenerationStyle.CUSTOM_TWEAKED: |
| 120 | newSizeDp = lerp(startTarget[i], endTarget[i], interpolationProgress); |
| 121 | break; |
| 122 | case GenerationStyle.CURVE: { |
| 123 | let coeff1; |
| 124 | let coeff2; |
| 125 | if (scale < 1) { |
| 126 | // \left(1.22^{-\left(x+5\right)}+0.5\right)\cdot x |
| 127 | coeff1 = -5; |
| 128 | coeff2 = scale; |
| 129 | } else { |
| 130 | // (1.22^{-\left(x-10\right)}+1\right)\cdot x |
| 131 | coeff1 = map(scale, 1, 2, 2, 8); |
| 132 | coeff2 = 1; |
| 133 | } |
| 134 | newSizeDp = ((Math.pow(1.22, (-(originalSizeDp - coeff1))) + coeff2) * originalSizeDp); |
| 135 | break; |
| 136 | } |
| 137 | case GenerationStyle.LINEAR: |
| 138 | newSizeDp = originalSizeDp * scale; |
| 139 | break; |
| 140 | default: |
| 141 | throw new Error('Invalid GENERATION_STYLE'); |
| 142 | } |
| 143 | return { |
| 144 | fromSp: sp, |
| 145 | toDp: newSizeDp |
| 146 | } |
| 147 | }); |
| 148 | } |
| 149 | |
| 150 | const scaleArrays = |
| 151 | scales |
| 152 | .map(scale => { |
| 153 | const scaleString = (scale * 100).toFixed(0); |
| 154 | return { |
| 155 | scale, |
| 156 | name: `font_size_original_sp_to_scaled_dp_${scaleString}_percent` |
| 157 | } |
| 158 | }) |
| 159 | .map(scaleArray => { |
| 160 | const items = generateRatios(scaleArray.scale); |
| 161 | |
| 162 | return { |
| 163 | ...scaleArray, |
| 164 | items |
| 165 | } |
| 166 | }); |
| 167 | |
| 168 | function formatDigit(d) { |
| 169 | const twoSignificantDigits = Math.round(d * 100) / 100; |
| 170 | return String(twoSignificantDigits).padStart(4, ' '); |
| 171 | } |
| 172 | |
| 173 | console.log( |
| 174 | '' + |
| 175 | scaleArrays.reduce( |
| 176 | (previousScaleArray, currentScaleArray) => { |
| 177 | const itemsFromSp = currentScaleArray.items.map(d => d.fromSp) |
| 178 | .map(formatDigit) |
| 179 | .join('f, '); |
| 180 | const itemsToDp = currentScaleArray.items.map(d => d.toDp) |
| 181 | .map(formatDigit) |
| 182 | .join('f, '); |
| 183 | |
| 184 | return previousScaleArray + ` |
| 185 | put( |
| 186 | /* scaleKey= */ ${currentScaleArray.scale}f, |
| 187 | new FontScaleConverter( |
| 188 | /* fromSp= */ |
| 189 | new float[] {${itemsFromSp}}, |
| 190 | /* toDp= */ |
| 191 | new float[] {${itemsToDp}}) |
| 192 | ); |
| 193 | `; |
| 194 | }, |
| 195 | '')); |