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