blob: 9754697bfb51d3cbc4feb23e503e36c0159c9ada [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/**
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 */
30const scales = [1.15, 1.30, 1.5, 1.8, 2];
31
32const 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 */
37const 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 */
61const 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.
66const 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 */
82function 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 */
89function lerp(a, b, fraction) {
90 return (a * (1.0 - fraction)) + (b * fraction);
91}
92
93function 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
150const 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
168function formatDigit(d) {
169 const twoSignificantDigits = Math.round(d * 100) / 100;
170 return String(twoSignificantDigits).padStart(4, ' ');
171}
172
173console.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 ''));