dsh-fonttune 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +19 -0
- package/CHANGELOG.md +19 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/lib/client.js +4898 -4071
- package/lib/index.js +6 -0
- package/lib/shared.cjs +265 -60
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -217,6 +217,12 @@ export const Config = volatileWhenSupported(z.object({
|
|
|
217
217
|
.description(
|
|
218
218
|
`Code font-weight offset; added to each code surface's own weight and independent of the interface weight (the slider spans whatever weights the chosen font can render), ${shared.WEIGHT_UNSET} keeps DSH's own weights. A legacy absolute weight (${shared.WEIGHT_MIN}..${shared.WEIGHT_MAX}) is accepted and converted to its offset.`
|
|
219
219
|
),
|
|
220
|
+
[shared.WEIGHT_OFFSETS_FIELD]: z
|
|
221
|
+
.boolean()
|
|
222
|
+
.default(false)
|
|
223
|
+
.description(
|
|
224
|
+
"Scale marker: the three weight fields hold offsets, not legacy absolute weights (set automatically the first time the card writes a weight)"
|
|
225
|
+
),
|
|
220
226
|
[LINE_HEIGHT_FIELD]: z
|
|
221
227
|
.number()
|
|
222
228
|
.min(shared.LINE_HEIGHT_MIN)
|
package/lib/shared.cjs
CHANGED
|
@@ -77,6 +77,23 @@ var WEIGHT_DIALOG_FIELD = "weightDialog";
|
|
|
77
77
|
/** Field carrying the code font weight (0 = leave DSH alone). */
|
|
78
78
|
var CODE_WEIGHT_FIELD = "weightCode";
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Marks a document whose weight fields are OFFSETS, not absolute weights.
|
|
82
|
+
*
|
|
83
|
+
* Before 0.2.8 the three weight axes stored an absolute CSS weight (`300…600`,
|
|
84
|
+
* `400` = normal) and `clampWeightDelta` converted one on sight. That inference
|
|
85
|
+
* only works while offsets never land inside the absolute window, which capped
|
|
86
|
+
* the offset range at +200 — and +200 is not enough room for a step the eye can
|
|
87
|
+
* see on a family with few faces (measured: one 200-weight step is a +30% change
|
|
88
|
+
* on Noto Serif SC, one 100-weight step only +16%).
|
|
89
|
+
*
|
|
90
|
+
* So the document says which scale it is on, and the inference is borrowed for
|
|
91
|
+
* exactly one write: the first time 0.3.5+ writes a weight, the card converts any
|
|
92
|
+
* remaining absolute value, converts the saved presets the same way, and sets
|
|
93
|
+
* this field. From then on a stored weight is an offset, whatever its size.
|
|
94
|
+
*/
|
|
95
|
+
var WEIGHT_OFFSETS_FIELD = "weightOffsets";
|
|
96
|
+
|
|
80
97
|
/**
|
|
81
98
|
* Field carrying the interface line-height ratio in percent (100 = untouched).
|
|
82
99
|
* A ratio rather than a px offset: interface line heights form a design ladder
|
|
@@ -208,17 +225,18 @@ var WEIGHT_LADDER_TOP = DIALOG_WEIGHT_GROUPS.reduce(function (top, group) {
|
|
|
208
225
|
/**
|
|
209
226
|
* How far above its base an offset may reach at all.
|
|
210
227
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
228
|
+
* The ceiling comes from the layer a notch MUST move: body text sits at
|
|
229
|
+
* `WEIGHT_BASE`, so the range runs as far as the text a reader is reading can
|
|
230
|
+
* still change — measured up to +400 on a family whose faces reach 900. Bold text
|
|
231
|
+
* stops moving at +300 and headings at +200; that is accepted, because a heading
|
|
232
|
+
* already at its heaviest face stays there (see `WEIGHT_LADDER_STRONG`).
|
|
215
233
|
*
|
|
216
|
-
*
|
|
217
|
-
* `WEIGHT_MIN…WEIGHT_MAX`, the
|
|
218
|
-
* document occupies
|
|
219
|
-
*
|
|
234
|
+
* It used to stop at `WEIGHT_CSS_MAX - WEIGHT_LADDER_TOP` (= +200) so an offset
|
|
235
|
+
* could never land inside `WEIGHT_MIN…WEIGHT_MAX`, the window an ABSOLUTE weight
|
|
236
|
+
* from a pre-relative document occupies. The document's own marker resolves that
|
|
237
|
+
* window now (`WEIGHT_OFFSETS_FIELD`), so the range is free to grow.
|
|
220
238
|
*/
|
|
221
|
-
var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX -
|
|
239
|
+
var WEIGHT_DELTA_UP_LIMIT = WEIGHT_CSS_MAX - WEIGHT_BASE;
|
|
222
240
|
|
|
223
241
|
/**
|
|
224
242
|
* The heaviest layer a notch still has to move: bold text.
|
|
@@ -314,9 +332,12 @@ var VALUE_FIELDS = [
|
|
|
314
332
|
var RETIRED_FIELDS = [SIZE_FIELD, LINE_HEIGHT_FIELD];
|
|
315
333
|
|
|
316
334
|
/**
|
|
317
|
-
* Every durable
|
|
335
|
+
* Every durable field normalization covers: the live axes, the retired pair, and
|
|
336
|
+
* the scale marker (`WEIGHT_OFFSETS_FIELD`, which is deliberately NOT a value
|
|
337
|
+
* axis — a preset must never carry it, or applying that preset would re-label a
|
|
338
|
+
* document still holding absolute weights).
|
|
318
339
|
*/
|
|
319
|
-
var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS);
|
|
340
|
+
var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS, [WEIGHT_OFFSETS_FIELD]);
|
|
320
341
|
|
|
321
342
|
/**
|
|
322
343
|
* Longest accepted font stack, in characters (mirrored by the host schema).
|
|
@@ -435,7 +456,7 @@ var FEATURES_UNSAFE = /[^a-zA-Z0-9"' ,]/g;
|
|
|
435
456
|
* ------------------------------------------------------------------ */
|
|
436
457
|
|
|
437
458
|
/** Clamp/validate one field by name (shared by config and value sets). */
|
|
438
|
-
function normalizeField(field, value) {
|
|
459
|
+
function normalizeField(field, value, relative) {
|
|
439
460
|
switch (field) {
|
|
440
461
|
case SANS_FIELD:
|
|
441
462
|
case STACK_DIALOG_FIELD:
|
|
@@ -448,7 +469,7 @@ function normalizeField(field, value) {
|
|
|
448
469
|
case WEIGHT_FIELD:
|
|
449
470
|
case WEIGHT_DIALOG_FIELD:
|
|
450
471
|
case CODE_WEIGHT_FIELD:
|
|
451
|
-
return clampWeightDelta(value);
|
|
472
|
+
return clampWeightDelta(value, relative);
|
|
452
473
|
case LINE_HEIGHT_FIELD:
|
|
453
474
|
case LINE_HEIGHT_DIALOG_FIELD:
|
|
454
475
|
return clampRatio(value);
|
|
@@ -456,6 +477,9 @@ function normalizeField(field, value) {
|
|
|
456
477
|
return clampLineOffset(value);
|
|
457
478
|
case LIGATURES_FIELD:
|
|
458
479
|
return clampLigatures(value);
|
|
480
|
+
case WEIGHT_OFFSETS_FIELD:
|
|
481
|
+
// The document's own scale marker: false/absent = the pre-relative scale.
|
|
482
|
+
return toBool(value);
|
|
459
483
|
case FEATURES_FIELD:
|
|
460
484
|
return sanitizeFeatures(value);
|
|
461
485
|
case NO_SYNTHETIC_ITALIC_FIELD:
|
|
@@ -479,14 +503,17 @@ function normalizeField(field, value) {
|
|
|
479
503
|
*/
|
|
480
504
|
function normalizeConfig(value) {
|
|
481
505
|
var source = value !== null && typeof value === "object" ? value : {};
|
|
506
|
+
// The scale marker is read BEFORE the axes: it decides whether a weight inside
|
|
507
|
+
// `300…600` is a legacy absolute value or an offset (see `clampWeightDelta`).
|
|
508
|
+
var relative = toBool(source[WEIGHT_OFFSETS_FIELD]);
|
|
482
509
|
var config = {};
|
|
483
510
|
for (var index = 0; index < DURABLE_FIELDS.length; index += 1) {
|
|
484
511
|
var field = DURABLE_FIELDS[index];
|
|
485
|
-
config[field] = normalizeField(field, source[field]);
|
|
512
|
+
config[field] = normalizeField(field, source[field], relative);
|
|
486
513
|
}
|
|
487
514
|
config[PER_THEME_FIELD] = toBool(source[PER_THEME_FIELD]);
|
|
488
|
-
config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD]);
|
|
489
|
-
config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD]);
|
|
515
|
+
config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD], relative);
|
|
516
|
+
config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD], relative);
|
|
490
517
|
config[ACTIVE_PRESET_FIELD] = sanitize(source[ACTIVE_PRESET_FIELD]).slice(0, MAX_PRESET_NAME);
|
|
491
518
|
return config;
|
|
492
519
|
}
|
|
@@ -524,21 +551,21 @@ function clampWeight(value) {
|
|
|
524
551
|
* Clamp one RELATIVE weight to its allowed range, migrating an absolute value.
|
|
525
552
|
*
|
|
526
553
|
* A document written before the axes became relative stores an absolute weight
|
|
527
|
-
* (`300…600`, `400` = normal)
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* instead of jumping to the top of the new scale.
|
|
554
|
+
* (`300…600`, `400` = normal) and has no marker, so a value inside `300…600` is
|
|
555
|
+
* converted to its distance from normal (`400 → 0`, `560 → +160`, `300 → −100`).
|
|
556
|
+
* A marked document is on the offset scale already and is never converted — that
|
|
557
|
+
* is what lets an offset grow past `+200` without being mistaken for one of those
|
|
558
|
+
* absolute values (the two windows overlap; see `WEIGHT_OFFSETS_FIELD`).
|
|
533
559
|
*
|
|
534
560
|
* @param {unknown} value - candidate offset, or a pre-0.2.8 absolute weight.
|
|
561
|
+
* @param {boolean} [relative] - true when the document carries the offset marker.
|
|
535
562
|
* @returns {number} the offset the stylesheet may use.
|
|
536
563
|
*/
|
|
537
|
-
function clampWeightDelta(value) {
|
|
564
|
+
function clampWeightDelta(value, relative) {
|
|
538
565
|
var delta = Number(value);
|
|
539
566
|
if (!isFinite(delta)) return WEIGHT_UNSET;
|
|
540
567
|
delta = Math.round(delta);
|
|
541
|
-
if (delta >= WEIGHT_MIN && delta <= WEIGHT_MAX) delta -= WEIGHT_BASE;
|
|
568
|
+
if (relative !== true && delta >= WEIGHT_MIN && delta <= WEIGHT_MAX) delta -= WEIGHT_BASE;
|
|
542
569
|
// The slider's own range comes from the font (see `weightProfileFrom`) and can
|
|
543
570
|
// reach further than the nominal one, so the hard bounds are the widest range
|
|
544
571
|
// any font-derived slider may use.
|
|
@@ -586,13 +613,13 @@ function clampLigatures(value) {
|
|
|
586
613
|
* @param {unknown} values - candidate map.
|
|
587
614
|
* @returns {Record<string, string|number|boolean>} the safe subset.
|
|
588
615
|
*/
|
|
589
|
-
function normalizeValueSet(values) {
|
|
616
|
+
function normalizeValueSet(values, relative) {
|
|
590
617
|
var out = {};
|
|
591
618
|
if (values === null || typeof values !== "object" || Array.isArray(values)) return out;
|
|
592
619
|
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
593
620
|
var field = VALUE_FIELDS[index];
|
|
594
621
|
if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
|
|
595
|
-
out[field] = normalizeField(field, values[field]);
|
|
622
|
+
out[field] = normalizeField(field, values[field], relative);
|
|
596
623
|
}
|
|
597
624
|
return out;
|
|
598
625
|
}
|
|
@@ -602,7 +629,7 @@ function normalizeValueSet(values) {
|
|
|
602
629
|
* @param {unknown} value - candidate JSON text or object.
|
|
603
630
|
* @returns {Record<string, string|number|boolean>} the sparse override map.
|
|
604
631
|
*/
|
|
605
|
-
function normalizeDarkValues(value) {
|
|
632
|
+
function normalizeDarkValues(value, relative) {
|
|
606
633
|
if (typeof value === "string") {
|
|
607
634
|
if (value.trim() === "") return {};
|
|
608
635
|
try {
|
|
@@ -611,7 +638,133 @@ function normalizeDarkValues(value) {
|
|
|
611
638
|
return {};
|
|
612
639
|
}
|
|
613
640
|
}
|
|
614
|
-
return normalizeValueSet(value);
|
|
641
|
+
return normalizeValueSet(value, relative);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** The three axes that share the weight scale (and its marker). */
|
|
645
|
+
var WEIGHT_FIELDS = [WEIGHT_FIELD, WEIGHT_DIALOG_FIELD, CODE_WEIGHT_FIELD];
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Convert the weight fields of saved preset snapshots to offsets.
|
|
649
|
+
*
|
|
650
|
+
* Runs exactly once per document, right before the card sets
|
|
651
|
+
* `WEIGHT_OFFSETS_FIELD`: a snapshot saved before 0.2.8 holds an absolute weight,
|
|
652
|
+
* and once the document is marked a stored weight is an offset, so applying that
|
|
653
|
+
* snapshot later would read `450` as +450 instead of +50. Converting the snapshots
|
|
654
|
+
* in the same breath as the document keeps the two on one scale.
|
|
655
|
+
*
|
|
656
|
+
* Only values inside the legacy absolute window are touched, so a snapshot that
|
|
657
|
+
* already holds offsets (any 0.3.x one) comes back unchanged and `changed` stays
|
|
658
|
+
* false — the caller then writes nothing.
|
|
659
|
+
*
|
|
660
|
+
* @param {unknown} presets - normalized presets, or anything shaped like them.
|
|
661
|
+
* @returns {{presets: object[], changed: boolean}} the converted list.
|
|
662
|
+
*/
|
|
663
|
+
function migratePresetWeights(presets) {
|
|
664
|
+
if (!Array.isArray(presets)) return { presets: [], changed: false };
|
|
665
|
+
var changed = false;
|
|
666
|
+
var out = [];
|
|
667
|
+
for (var index = 0; index < presets.length; index += 1) {
|
|
668
|
+
var entry = presets[index];
|
|
669
|
+
if (entry === null || typeof entry !== "object" || entry.values === null) {
|
|
670
|
+
out.push(entry);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
var values = entry.values;
|
|
674
|
+
if (typeof values !== "object") {
|
|
675
|
+
out.push(entry);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
var converted = null;
|
|
679
|
+
for (var axis = 0; axis < WEIGHT_FIELDS.length; axis += 1) {
|
|
680
|
+
var field = WEIGHT_FIELDS[axis];
|
|
681
|
+
if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
|
|
682
|
+
var stored = Number(values[field]);
|
|
683
|
+
if (!isFinite(stored) || stored < WEIGHT_MIN || stored > WEIGHT_MAX) continue;
|
|
684
|
+
if (converted === null) {
|
|
685
|
+
converted = {};
|
|
686
|
+
for (var key in values) {
|
|
687
|
+
if (Object.prototype.hasOwnProperty.call(values, key)) converted[key] = values[key];
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
converted[field] = stored - WEIGHT_BASE;
|
|
691
|
+
}
|
|
692
|
+
if (converted === null) {
|
|
693
|
+
out.push(entry);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
changed = true;
|
|
697
|
+
var copy = {};
|
|
698
|
+
for (var name in entry) {
|
|
699
|
+
if (Object.prototype.hasOwnProperty.call(entry, name)) copy[name] = entry[name];
|
|
700
|
+
}
|
|
701
|
+
copy.values = converted;
|
|
702
|
+
out.push(copy);
|
|
703
|
+
}
|
|
704
|
+
return { presets: out, changed: changed };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** The two shapes one locally painted move takes. */
|
|
708
|
+
var PENDING_SET = "set";
|
|
709
|
+
var PENDING_UNSET = "unset";
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Merge the values the card painted locally over the document's own values.
|
|
713
|
+
*
|
|
714
|
+
* The card paints a move the moment the user lets go, while the settings document
|
|
715
|
+
* only echoes it after the host's debounced write. Painting the DOCUMENT's value
|
|
716
|
+
* in the meantime would show the previous one again — and an echo of an older move
|
|
717
|
+
* arriving after a newer one would visibly step the page backwards. So the local
|
|
718
|
+
* values win until the document catches up (`reconcilePendingValues` decides when
|
|
719
|
+
* that is).
|
|
720
|
+
*
|
|
721
|
+
* @param {unknown} document - the raw settings value.
|
|
722
|
+
* @param {Record<string, {op: string, value: unknown}>} pending - local moves.
|
|
723
|
+
* @returns {Record<string, unknown>} the values to render with.
|
|
724
|
+
*/
|
|
725
|
+
function overlayPendingValues(document, pending) {
|
|
726
|
+
var source = document !== null && typeof document === "object" ? document : {};
|
|
727
|
+
var merged = {};
|
|
728
|
+
for (var key in source) {
|
|
729
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) merged[key] = source[key];
|
|
730
|
+
}
|
|
731
|
+
var local = pending !== null && typeof pending === "object" ? pending : {};
|
|
732
|
+
for (var field in local) {
|
|
733
|
+
if (!Object.prototype.hasOwnProperty.call(local, field)) continue;
|
|
734
|
+
if (local[field].op === PENDING_UNSET) delete merged[field];
|
|
735
|
+
else merged[field] = local[field].value;
|
|
736
|
+
}
|
|
737
|
+
return merged;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Keep only the local moves that are still worth painting.
|
|
742
|
+
*
|
|
743
|
+
* A paint is deliberately NOT retired when the document agrees with it: the host
|
|
744
|
+
* debounces its writes, so a patch built EARLIER can land after a newer one and
|
|
745
|
+
* drag the document back to an older value. Retiring on agreement made exactly
|
|
746
|
+
* that echo visible — the page jumped back and forth and only ended up right at
|
|
747
|
+
* the end. The card corrects the document instead (its re-assert), and the paint
|
|
748
|
+
* stays until the patience runs out. A refused write is retired by the caller,
|
|
749
|
+
* which learns about it before this rule sees the document again.
|
|
750
|
+
*
|
|
751
|
+
* @param {Record<string, {op: string, value: unknown, at: number}>} pending - local moves.
|
|
752
|
+
* @param {unknown} document - the raw settings value (kept for the caller's shape).
|
|
753
|
+
* @param {number} now - current time in ms.
|
|
754
|
+
* @param {number} patienceMs - how long to keep an unconfirmed move.
|
|
755
|
+
* @returns {Record<string, object>} the entries still worth painting.
|
|
756
|
+
*/
|
|
757
|
+
function reconcilePendingValues(pending, document, now, patienceMs) {
|
|
758
|
+
var local = pending !== null && typeof pending === "object" ? pending : {};
|
|
759
|
+
var kept = {};
|
|
760
|
+
for (var field in local) {
|
|
761
|
+
if (!Object.prototype.hasOwnProperty.call(local, field)) continue;
|
|
762
|
+
var entry = local[field];
|
|
763
|
+
var stale = typeof entry.at === "number" && now - entry.at > patienceMs;
|
|
764
|
+
if (stale) continue;
|
|
765
|
+
kept[field] = entry;
|
|
766
|
+
}
|
|
767
|
+
return kept;
|
|
615
768
|
}
|
|
616
769
|
|
|
617
770
|
/**
|
|
@@ -619,7 +772,7 @@ function normalizeDarkValues(value) {
|
|
|
619
772
|
* @param {unknown} value - candidate JSON text or array.
|
|
620
773
|
* @returns {{name: string, values: object, savedAt: number}[]}
|
|
621
774
|
*/
|
|
622
|
-
function normalizePresets(value) {
|
|
775
|
+
function normalizePresets(value, relative) {
|
|
623
776
|
var list = value;
|
|
624
777
|
if (typeof value === "string") {
|
|
625
778
|
if (value.trim() === "") return [];
|
|
@@ -639,7 +792,7 @@ function normalizePresets(value) {
|
|
|
639
792
|
var stamp = Math.round(Number(entry.savedAt));
|
|
640
793
|
out.push({
|
|
641
794
|
name: name,
|
|
642
|
-
values: normalizeValueSet(entry.values),
|
|
795
|
+
values: normalizeValueSet(entry.values, relative),
|
|
643
796
|
savedAt: isFinite(stamp) && stamp > 0 ? stamp : 0,
|
|
644
797
|
});
|
|
645
798
|
}
|
|
@@ -1692,6 +1845,17 @@ function resolveAxes(config) {
|
|
|
1692
1845
|
*/
|
|
1693
1846
|
var WEIGHT_STEP_CANDIDATES = [1, 5, 10, 25, 50, 100, 200, 300];
|
|
1694
1847
|
|
|
1848
|
+
/**
|
|
1849
|
+
* How many positions a slider must still offer before a coarser step is refused.
|
|
1850
|
+
*
|
|
1851
|
+
* Coarseness is the point (see `weightProfileFrom`), but a control with two
|
|
1852
|
+
* positions is not a slider. Four is the smallest count that still lets a reader
|
|
1853
|
+
* go one notch either side of the neutral position and one further up — and on
|
|
1854
|
+
* the measured seven-face family it lands on a 200-weight step, where one notch is
|
|
1855
|
+
* a +30% change in ink, against +16% at 100 (which is why 100 was not enough).
|
|
1856
|
+
*/
|
|
1857
|
+
var WEIGHT_MIN_POSITIONS = 4;
|
|
1858
|
+
|
|
1695
1859
|
/** The weight grid the profile is measured on. */
|
|
1696
1860
|
var WEIGHT_PROBE_LOW = 100;
|
|
1697
1861
|
var WEIGHT_PROBE_HIGH = 900;
|
|
@@ -1733,19 +1897,28 @@ function everyPositionDiffers(sample, bases, min, max, step) {
|
|
|
1733
1897
|
/**
|
|
1734
1898
|
* The slider a family deserves: how far it can go, and by how much.
|
|
1735
1899
|
*
|
|
1736
|
-
* A font
|
|
1737
|
-
*
|
|
1738
|
-
* fine step and a wide range. Measured on a weight grid:
|
|
1900
|
+
* A font can be made lighter or heavier only as far as its faces reach, and only
|
|
1901
|
+
* in jumps between them. Measured on a weight grid:
|
|
1739
1902
|
* - the downward reach is the highest weight that still renders as light as the
|
|
1740
1903
|
* family can (asking for less cannot change anything);
|
|
1741
|
-
* - the upward reach is the LOWEST weight at or above
|
|
1742
|
-
*
|
|
1743
|
-
*
|
|
1744
|
-
*
|
|
1745
|
-
*
|
|
1746
|
-
*
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1904
|
+
* - the upward reach is the LOWEST weight at or above body text that already
|
|
1905
|
+
* renders as heavy as the family can, so the range ends where the text a reader
|
|
1906
|
+
* is reading stops changing (see `WEIGHT_DELTA_UP_LIMIT` for the ceiling);
|
|
1907
|
+
* - the step is the COARSEST candidate that still leaves `WEIGHT_MIN_POSITIONS`
|
|
1908
|
+
* positions with body text changing at every one of them. Coarse is the point:
|
|
1909
|
+
* a finer step splits one face into several positions that all render the same,
|
|
1910
|
+
* so the control feels dead (measured on Noto Serif SC, whose faces are
|
|
1911
|
+
* 100-250 / 300 / 400 / 500 / 600 / 700 / 800-900: one 100-weight step is a
|
|
1912
|
+
* +16% change in ink, one 200-weight step +30%).
|
|
1913
|
+
*
|
|
1914
|
+
* Bold text and headings are deliberately NOT required to change at every
|
|
1915
|
+
* position: the heaviest ladder layer sits at 600 and reaches the ceiling at +300,
|
|
1916
|
+
* so no range can move it further. Body text is what a reader reads, so it carries
|
|
1917
|
+
* the requirement, and a heading already at its boldest face stays there.
|
|
1918
|
+
*
|
|
1919
|
+
* Every position is a whole number of steps from 0, so the card's step counts map
|
|
1920
|
+
* back onto exactly the offsets the document stores.
|
|
1921
|
+
*
|
|
1749
1922
|
* @param {(weight: number) => string|number} sample - renders a probe at one
|
|
1750
1923
|
* weight and returns a signature; equal signatures mean identical rendering.
|
|
1751
1924
|
* @param {{candidates?: number[], floor?: number, ceil?: number, top?: number}}
|
|
@@ -1793,27 +1966,49 @@ function weightProfileFrom(sample, options) {
|
|
|
1793
1966
|
}
|
|
1794
1967
|
var reachUp = Math.min(ceil, WEIGHT_DELTA_UP_LIMIT, headroom);
|
|
1795
1968
|
var reachDown = Math.max(floor, Math.min(0, lightest - WEIGHT_BASE));
|
|
1796
|
-
var
|
|
1797
|
-
|
|
1969
|
+
var cap = Math.min(reachUp, WEIGHT_DELTA_UP_LIMIT);
|
|
1970
|
+
/**
|
|
1971
|
+
* The shape one candidate step describes.
|
|
1972
|
+
*
|
|
1973
|
+
* Every position is a whole number of steps from 0, so the card's step counts
|
|
1974
|
+
* multiply back into exactly the offsets the document stores. The reach is
|
|
1975
|
+
* snapped to that grid without ever passing the hard floor or the ceiling —
|
|
1976
|
+
* rounding the top UP (which the first version did) put a position past the
|
|
1977
|
+
* widest allowed offset, and rounding the bottom DOWN could pass the floor.
|
|
1978
|
+
*/
|
|
1979
|
+
var shapeAt = function (step) {
|
|
1980
|
+
var down = Math.min(ceilTo(-reachDown, step), floorTo(-floor, step));
|
|
1981
|
+
return { min: -down, max: floorTo(cap, step), step: step };
|
|
1982
|
+
};
|
|
1983
|
+
var best = null;
|
|
1984
|
+
var bestCount = 0;
|
|
1985
|
+
// Coarsest candidate first: the coarsest step that still leaves enough visible
|
|
1986
|
+
// positions is the one a reader can actually work with.
|
|
1987
|
+
for (var index = candidates.length - 1; index >= 0; index -= 1) {
|
|
1798
1988
|
var step = candidates[index];
|
|
1799
1989
|
if (!(step > 0)) continue;
|
|
1800
|
-
var
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
var
|
|
1804
|
-
|
|
1805
|
-
if (
|
|
1806
|
-
|
|
1807
|
-
|
|
1990
|
+
var shape = shapeAt(step);
|
|
1991
|
+
if (shape.max < shape.min) continue;
|
|
1992
|
+
if (!everyPositionDiffers(sample, [WEIGHT_BASE], shape.min, shape.max, step)) continue;
|
|
1993
|
+
var count = (shape.max - shape.min) / step + 1;
|
|
1994
|
+
if (count >= WEIGHT_MIN_POSITIONS) return shape;
|
|
1995
|
+
if (count > bestCount) {
|
|
1996
|
+
best = shape;
|
|
1997
|
+
bestCount = count;
|
|
1808
1998
|
}
|
|
1809
1999
|
}
|
|
2000
|
+
if (best !== null) {
|
|
2001
|
+
// Fewer positions than asked for is still usable; one position is the whole
|
|
2002
|
+
// range, and any step describes it, so use the finest and keep the unit small.
|
|
2003
|
+
if (best.max === best.min) return { min: best.min, max: best.max, step: 1 };
|
|
2004
|
+
return best;
|
|
2005
|
+
}
|
|
2006
|
+
// No step kept body text alive: the coarsest one that still fits is the honest
|
|
2007
|
+
// answer, never a step the control cannot reach.
|
|
1810
2008
|
var coarsest = candidates[candidates.length - 1];
|
|
1811
|
-
var
|
|
1812
|
-
var
|
|
1813
|
-
|
|
1814
|
-
// honest answer, never a step the control cannot reach.
|
|
1815
|
-
var span = max - min;
|
|
1816
|
-
return { min: min, max: max, step: span > 0 ? Math.min(coarsest, span) : 1 };
|
|
2009
|
+
var plain = shapeAt(coarsest);
|
|
2010
|
+
var span = plain.max - plain.min;
|
|
2011
|
+
return { min: plain.min, max: plain.max, step: span > 0 ? Math.min(coarsest, span) : 1 };
|
|
1817
2012
|
}
|
|
1818
2013
|
|
|
1819
2014
|
/**
|
|
@@ -1896,9 +2091,11 @@ function isDormant(config) {
|
|
|
1896
2091
|
sanitize(axis[MONO_FIELD]) === "" &&
|
|
1897
2092
|
clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
|
|
1898
2093
|
clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
clampWeightDelta(axis[
|
|
2094
|
+
// These arrive normalized, so they are offsets already: converting them again
|
|
2095
|
+
// would rescale an offset that legitimately sits inside `300…600`.
|
|
2096
|
+
clampWeightDelta(axis[WEIGHT_FIELD], true) === WEIGHT_UNSET &&
|
|
2097
|
+
clampWeightDelta(axis[WEIGHT_DIALOG_FIELD], true) === WEIGHT_UNSET &&
|
|
2098
|
+
clampWeightDelta(axis[CODE_WEIGHT_FIELD], true) === WEIGHT_UNSET &&
|
|
1902
2099
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
1903
2100
|
clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
|
|
1904
2101
|
clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
|
|
@@ -2778,6 +2975,13 @@ var shared = {
|
|
|
2778
2975
|
WEIGHT_FIELD: WEIGHT_FIELD,
|
|
2779
2976
|
WEIGHT_DIALOG_FIELD: WEIGHT_DIALOG_FIELD,
|
|
2780
2977
|
CODE_WEIGHT_FIELD: CODE_WEIGHT_FIELD,
|
|
2978
|
+
WEIGHT_OFFSETS_FIELD: WEIGHT_OFFSETS_FIELD,
|
|
2979
|
+
WEIGHT_FIELDS: WEIGHT_FIELDS,
|
|
2980
|
+
PENDING_SET: PENDING_SET,
|
|
2981
|
+
PENDING_UNSET: PENDING_UNSET,
|
|
2982
|
+
overlayPendingValues: overlayPendingValues,
|
|
2983
|
+
reconcilePendingValues: reconcilePendingValues,
|
|
2984
|
+
migratePresetWeights: migratePresetWeights,
|
|
2781
2985
|
LINE_HEIGHT_FIELD: LINE_HEIGHT_FIELD,
|
|
2782
2986
|
LINE_HEIGHT_DIALOG_FIELD: LINE_HEIGHT_DIALOG_FIELD,
|
|
2783
2987
|
CODE_LINE_HEIGHT_FIELD: CODE_LINE_HEIGHT_FIELD,
|
|
@@ -2872,6 +3076,7 @@ var shared = {
|
|
|
2872
3076
|
WEIGHT_STEP_CANDIDATES: WEIGHT_STEP_CANDIDATES,
|
|
2873
3077
|
WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
|
|
2874
3078
|
WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
|
|
3079
|
+
WEIGHT_MIN_POSITIONS: WEIGHT_MIN_POSITIONS,
|
|
2875
3080
|
WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
|
|
2876
3081
|
WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
|
|
2877
3082
|
WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"0.1.7-alpha.2": "compatible",
|
|
35
35
|
"0.1.7-rc.1": "compatible",
|
|
36
36
|
"0.1.7-rc.2": "compatible",
|
|
37
|
-
"0.2.0-rc.1": "compatible"
|
|
37
|
+
"0.2.0-rc.1": "compatible",
|
|
38
|
+
"0.2.0-rc.2": "compatible"
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
},
|