blob: 59fd2e69764b38fd241cd655d56f0119c48e7545 [file] [log] [blame]
Tyler Freeman5f7036d2022-11-29 12:09:02 -08001/*
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/**
chihtinglo11bb0372023-02-13 00:31:20 +080027 * Modify this to match your
28 * frameworks/base/packages/SettingsLib/res/values/arrays.xml#entryvalues_font_size
Tyler Freeman5f7036d2022-11-29 12:09:02 -080029 * 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 ''));