| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /** |
| Generates arrays for non-linear font scaling, to be pasted into |
| frameworks/base/core/java/android/content/res/FontScaleConverterFactory.java |
| |
| To use: |
| `node font-scaling-array-generator.js` |
| or just open a browser, open DevTools, and paste into the Console. |
| */ |
| |
| /** |
| * Modify this to match your |
| * frameworks/base/packages/SettingsLib/res/values/arrays.xml#entryvalues_font_size |
| * array so that all possible scales are generated. |
| */ |
| const scales = [1.15, 1.30, 1.5, 1.8, 2]; |
| |
| const commonSpSizes = [8, 10, 12, 14, 18, 20, 24, 30, 100]; |
| |
| /** |
| * Enum for GENERATION_STYLE which determines how to generate the arrays. |
| */ |
| const GenerationStyle = { |
| /** |
| * Interpolates between hand-tweaked curves. This is the best option and |
| * shouldn't require any additional tweaking. |
| */ |
| CUSTOM_TWEAKED: 'CUSTOM_TWEAKED', |
| |
| /** |
| * Uses a curve equation that is mostly correct, but will need manual tweaking |
| * at some scales. |
| */ |
| CURVE: 'CURVE', |
| |
| /** |
| * Uses straight linear multiplication. Good starting point for manual |
| * tweaking. |
| */ |
| LINEAR: 'LINEAR' |
| } |
| |
| /** |
| * Determines how arrays are generated. Must be one of the GenerationStyle |
| * values. |
| */ |
| const GENERATION_STYLE = GenerationStyle.CUSTOM_TWEAKED; |
| |
| // These are hand-tweaked curves from which we will derive the other |
| // interstitial curves using linear interpolation, in the case of using |
| // GenerationStyle.CUSTOM_TWEAKED. |
| const interpolationTargets = { |
| 1.0: commonSpSizes, |
| 1.5: [12, 15, 18, 22, 24, 26, 28, 30, 100], |
| 2.0: [16, 20, 24, 26, 30, 34, 36, 38, 100] |
| }; |
| |
| /** |
| * Interpolate a value with specified extrema, to a new value between new |
| * extrema. |
| * |
| * @param value the current value |
| * @param inputMin minimum the input value can reach |
| * @param inputMax maximum the input value can reach |
| * @param outputMin minimum the output value can reach |
| * @param outputMax maximum the output value can reach |
| */ |
| function map(value, inputMin, inputMax, outputMin, outputMax) { |
| return outputMin + (outputMax - outputMin) * ((value - inputMin) / (inputMax - inputMin)); |
| } |
| |
| /*** |
| * Interpolate between values a and b. |
| */ |
| function lerp(a, b, fraction) { |
| return (a * (1.0 - fraction)) + (b * fraction); |
| } |
| |
| function generateRatios(scale) { |
| // Find the best two arrays to interpolate between. |
| let startTarget, endTarget; |
| let startTargetScale, endTargetScale; |
| const targetScales = Object.keys(interpolationTargets).sort(); |
| for (let i = 0; i < targetScales.length - 1; i++) { |
| const targetScaleKey = targetScales[i]; |
| const targetScale = parseFloat(targetScaleKey, 10); |
| const startTargetScaleKey = targetScaleKey; |
| const endTargetScaleKey = targetScales[i + 1]; |
| |
| if (scale < parseFloat(startTargetScaleKey, 10)) { |
| break; |
| } |
| |
| startTargetScale = parseFloat(startTargetScaleKey, 10); |
| endTargetScale = parseFloat(endTargetScaleKey, 10); |
| startTarget = interpolationTargets[startTargetScaleKey]; |
| endTarget = interpolationTargets[endTargetScaleKey]; |
| } |
| const interpolationProgress = map(scale, startTargetScale, endTargetScale, 0, 1); |
| |
| return commonSpSizes.map((sp, i) => { |
| const originalSizeDp = sp; |
| let newSizeDp; |
| switch (GENERATION_STYLE) { |
| case GenerationStyle.CUSTOM_TWEAKED: |
| newSizeDp = lerp(startTarget[i], endTarget[i], interpolationProgress); |
| break; |
| case GenerationStyle.CURVE: { |
| let coeff1; |
| let coeff2; |
| if (scale < 1) { |
| // \left(1.22^{-\left(x+5\right)}+0.5\right)\cdot x |
| coeff1 = -5; |
| coeff2 = scale; |
| } else { |
| // (1.22^{-\left(x-10\right)}+1\right)\cdot x |
| coeff1 = map(scale, 1, 2, 2, 8); |
| coeff2 = 1; |
| } |
| newSizeDp = ((Math.pow(1.22, (-(originalSizeDp - coeff1))) + coeff2) * originalSizeDp); |
| break; |
| } |
| case GenerationStyle.LINEAR: |
| newSizeDp = originalSizeDp * scale; |
| break; |
| default: |
| throw new Error('Invalid GENERATION_STYLE'); |
| } |
| return { |
| fromSp: sp, |
| toDp: newSizeDp |
| } |
| }); |
| } |
| |
| const scaleArrays = |
| scales |
| .map(scale => { |
| const scaleString = (scale * 100).toFixed(0); |
| return { |
| scale, |
| name: `font_size_original_sp_to_scaled_dp_${scaleString}_percent` |
| } |
| }) |
| .map(scaleArray => { |
| const items = generateRatios(scaleArray.scale); |
| |
| return { |
| ...scaleArray, |
| items |
| } |
| }); |
| |
| function formatDigit(d) { |
| const twoSignificantDigits = Math.round(d * 100) / 100; |
| return String(twoSignificantDigits).padStart(4, ' '); |
| } |
| |
| console.log( |
| '' + |
| scaleArrays.reduce( |
| (previousScaleArray, currentScaleArray) => { |
| const itemsFromSp = currentScaleArray.items.map(d => d.fromSp) |
| .map(formatDigit) |
| .join('f, '); |
| const itemsToDp = currentScaleArray.items.map(d => d.toDp) |
| .map(formatDigit) |
| .join('f, '); |
| |
| return previousScaleArray + ` |
| put( |
| /* scaleKey= */ ${currentScaleArray.scale}f, |
| new FontScaleConverter( |
| /* fromSp= */ |
| new float[] {${itemsFromSp}}, |
| /* toDp= */ |
| new float[] {${itemsToDp}}) |
| ); |
| `; |
| }, |
| '')); |