Files
enricobuehler aec6ccf887 feat(clients/apple): the shell's push/pop becomes the console's spring, and the contract is pinned
The fourth slice of the console-UI parity port (design/apple-console-ui-parity.md, PR-4):

- `ConsoleContract.swift` (PunktfunkShared): `ConsoleMotion` — the vectors' `motion_spring`
  numbers (response 0.42, damping 0.88, slide 36, scales 0.985/0.96, reveal 0.4, interruptible)
  — and `GpSettingsTab`, moved out of the executable target so `ConsoleVectorsTests` can reach
  both. `testTabNamesMatchTheSharedVectors` (this client's trailing About is its own; the
  desktop's Input is `desktop_only`) and `testMotionMatchesTheSharedVectors` (the v2 block) join
  the palette test; the vectors' v1 `$deprecated` note now names Android as the last v1 reader.
- `GamepadShellMotion.screen` is `.spring(0.42, 0.88)` — the console's `springs::NAV` — instead
  of the 0.26 s ease-out-cubic; Reduce Motion crossfades on the desktop's `REDUCED_NAV`
  (0.22, 1.0) with no slide and no scale, instead of snapping.
- Interruptible: while a push is in flight no layer owns the controller, so a zero-size
  `MidPushBackCatcher` takes B alone and clears the trigger that raised the top screen — the same
  spring carries it back down, the way the desktop retargets its NAV spring to 0. A and the rest
  stay dropped until the spring has passed 0.85 of its travel (`inputOpensAfter`, 0.20 s), not
  until a tween has finished (0.28 s): the wall is gone, the double-tap protection stays.
- The library layer's place stack rides the same spring and the same reduce-motion crossfade.
2026-08-18 11:36:56 +02:00

2018 lines
32 KiB
JSON

{
"$comment": "Cross-client CONSOLE-UI parity vectors: the background palette table, the settings section tabs, and the screen-transition motion contract. Consumed by pf-console-ui's Rust tests, the Android app's ConsoleVectorsTest, and the Apple client's ConsoleVectorsTests -- so the three hand-copies of these tables can no longer drift in silence. Sibling of deeplink-vectors.json, same rules: this file is the contract, and a value absent here is a value no client may assume.",
"version": 2,
"cell_ramp": [
0.1,
-0.06,
0.04,
-0.12,
-0.08,
0.14,
-0.1,
0.06,
0.06,
-0.12,
0.16,
-0.04,
-0.1,
0.08,
-0.06,
0.12
],
"mesh_interior": [
[
0.333,
0.333,
0.11,
0.049,
0.063,
0.4
],
[
0.667,
0.333,
0.1,
0.055,
0.052,
2.1
],
[
0.333,
0.667,
0.1,
0.058,
0.049,
3.6
],
[
0.667,
0.667,
0.12,
0.047,
0.061,
5.0
]
],
"palettes": [
{
"id": "violet",
"name": "Violet",
"light": false,
"stops": [],
"ground": [
0.075,
0.06,
0.16
],
"accent": [
0.525,
0.471,
0.961
],
"mesh": [
[
0.075,
0.06,
0.16
],
[
0.34,
0.27,
0.72
],
[
0.3,
0.26,
0.74
],
[
0.075,
0.06,
0.16
],
[
0.42,
0.2,
0.54
],
[
0.49,
0.39,
0.95
],
[
0.28,
0.31,
0.84
],
[
0.16,
0.26,
0.64
],
[
0.45,
0.23,
0.6
],
[
0.53,
0.31,
0.75
],
[
0.35,
0.35,
0.91
],
[
0.19,
0.28,
0.7
],
[
0.075,
0.06,
0.16
],
[
0.22,
0.18,
0.54
],
[
0.24,
0.2,
0.58
],
[
0.075,
0.06,
0.16
]
],
"blobs": [
[
0.356,
0.308,
0.816
],
[
0.468,
0.26,
0.768
],
[
0.38,
0.348,
0.836
],
[
0.406,
0.434,
0.92
]
]
},
{
"id": "oled",
"name": "Eclipse",
"light": false,
"stops": [
[
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0
],
[
0.01,
0.02,
0.1
],
[
0.045,
0.016,
0.115
],
[
0.12,
0.024,
0.13
]
],
"ground": [
0.0,
0.0,
0.0
],
"accent": [
0.525,
0.471,
0.961
],
"mesh": [
[
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0
],
[
0.004933333,
0.009866667,
0.049333333
],
[
0.0052,
0.0104,
0.052
],
[
0.0,
0.0,
0.0
],
[
0.008933333,
0.017866667,
0.089333333
],
[
0.006,
0.012,
0.06
],
[
0.041733333,
0.016373333,
0.1136
],
[
0.005733333,
0.011466667,
0.057333333
],
[
0.0052,
0.0104,
0.052
],
[
0.068,
0.018453333,
0.1196
],
[
0.058,
0.017386667,
0.1176
],
[
0.006,
0.012,
0.06
],
[
0.044533333,
0.016053333,
0.1148
],
[
0.052,
0.016746667,
0.1164
],
[
0.12,
0.024,
0.13
]
],
"blobs": [
[
0.0,
0.0,
0.0
],
[
0.006,
0.012,
0.06
],
[
0.031,
0.0176,
0.109
],
[
0.09,
0.0208,
0.124
]
]
},
{
"id": "nebula",
"name": "Nebula",
"light": false,
"stops": [
[
0.07,
0.05,
0.2
],
[
0.26,
0.14,
0.54
],
[
0.52,
0.2,
0.72
],
[
0.82,
0.26,
0.62
],
[
0.98,
0.46,
0.68
]
],
"ground": [
0.055,
0.04,
0.135
],
"accent": [
0.95,
0.42,
0.72
],
"mesh": [
[
0.146,
0.086,
0.336
],
[
0.151066667,
0.0884,
0.345066667
],
[
0.388266667,
0.1696,
0.6288
],
[
0.3952,
0.1712,
0.6336
],
[
0.135866667,
0.0812,
0.317866667
],
[
0.492266667,
0.1936,
0.7008
],
[
0.416,
0.176,
0.648
],
[
0.792,
0.2544,
0.629333333
],
[
0.409066667,
0.1744,
0.6432
],
[
0.3952,
0.1712,
0.6336
],
[
0.869066667,
0.321333333,
0.6384
],
[
0.847733333,
0.294666667,
0.6304
],
[
0.416,
0.176,
0.648
],
[
0.816,
0.2592,
0.621333333
],
[
0.834933333,
0.278666667,
0.6256
],
[
0.98,
0.46,
0.68
]
],
"blobs": [
[
0.184,
0.104,
0.404
],
[
0.416,
0.176,
0.648
],
[
0.7,
0.236,
0.66
],
[
0.916,
0.38,
0.656
]
]
},
{
"id": "abyss",
"name": "Abyss",
"light": false,
"stops": [
[
0.02,
0.1,
0.17
],
[
0.04,
0.28,
0.42
],
[
0.07,
0.46,
0.63
],
[
0.16,
0.38,
0.78
],
[
0.26,
0.22,
0.58
]
],
"ground": [
0.018,
0.07,
0.13
],
"accent": [
0.26,
0.76,
0.92
],
"mesh": [
[
0.028,
0.172,
0.27
],
[
0.028533333,
0.1768,
0.276666667
],
[
0.0548,
0.3688,
0.5236
],
[
0.0556,
0.3736,
0.5292
],
[
0.026933333,
0.1624,
0.256666667
],
[
0.0668,
0.4408,
0.6076
],
[
0.058,
0.388,
0.546
],
[
0.1516,
0.387466667,
0.766
],
[
0.0572,
0.3832,
0.5404
],
[
0.0556,
0.3736,
0.5292
],
[
0.190666667,
0.330933333,
0.718666667
],
[
0.177333333,
0.352266667,
0.745333333
],
[
0.058,
0.388,
0.546
],
[
0.1588,
0.381066667,
0.778
],
[
0.169333333,
0.365066667,
0.761333333
],
[
0.26,
0.22,
0.58
]
],
"blobs": [
[
0.032,
0.208,
0.32
],
[
0.058,
0.388,
0.546
],
[
0.124,
0.412,
0.72
],
[
0.22,
0.284,
0.66
]
]
},
{
"id": "ember",
"name": "Ember",
"light": false,
"stops": [
[
0.16,
0.03,
0.1
],
[
0.45,
0.06,
0.12
],
[
0.72,
0.18,
0.06
],
[
0.9,
0.42,
0.08
],
[
0.95,
0.68,
0.18
]
],
"ground": [
0.09,
0.035,
0.04
],
"accent": [
0.98,
0.62,
0.26
],
"mesh": [
[
0.276,
0.042,
0.108
],
[
0.283733333,
0.0428,
0.108533333
],
[
0.5832,
0.1192,
0.0904
],
[
0.5904,
0.1224,
0.0888
],
[
0.260533333,
0.0404,
0.106933333
],
[
0.6912,
0.1672,
0.0664
],
[
0.612,
0.132,
0.084
],
[
0.8832,
0.3976,
0.078133333
],
[
0.6048,
0.1288,
0.0856
],
[
0.5904,
0.1224,
0.0888
],
[
0.915333333,
0.499733333,
0.110666667
],
[
0.908666667,
0.465066667,
0.097333333
],
[
0.612,
0.132,
0.084
],
[
0.8976,
0.4168,
0.079733333
],
[
0.904666667,
0.444266667,
0.089333333
],
[
0.95,
0.68,
0.18
]
],
"blobs": [
[
0.334,
0.048,
0.112
],
[
0.612,
0.132,
0.084
],
[
0.828,
0.324,
0.072
],
[
0.93,
0.576,
0.14
]
]
},
{
"id": "moss",
"name": "Moss",
"light": false,
"stops": [
[
0.03,
0.11,
0.09
],
[
0.06,
0.27,
0.2
],
[
0.09,
0.45,
0.31
],
[
0.28,
0.61,
0.28
],
[
0.58,
0.77,
0.31
]
],
"ground": [
0.025,
0.085,
0.07
],
"accent": [
0.48,
0.86,
0.46
],
"mesh": [
[
0.042,
0.174,
0.134
],
[
0.0428,
0.178266667,
0.136933333
],
[
0.0748,
0.3588,
0.254266667
],
[
0.0756,
0.3636,
0.2572
],
[
0.0404,
0.165466667,
0.128133333
],
[
0.0868,
0.4308,
0.298266667
],
[
0.078,
0.378,
0.266
],
[
0.262266667,
0.595066667,
0.2828
],
[
0.0772,
0.3732,
0.263066667
],
[
0.0756,
0.3636,
0.2572
],
[
0.372,
0.659066667,
0.2892
],
[
0.332,
0.637733333,
0.2852
],
[
0.078,
0.378,
0.266
],
[
0.277466667,
0.607866667,
0.2804
],
[
0.308,
0.624933333,
0.2828
],
[
0.58,
0.77,
0.31
]
],
"blobs": [
[
0.048,
0.206,
0.156
],
[
0.078,
0.378,
0.266
],
[
0.204,
0.546,
0.292
],
[
0.46,
0.706,
0.298
]
]
},
{
"id": "graphite",
"name": "Graphite",
"light": false,
"stops": [
[
0.06,
0.07,
0.11
],
[
0.15,
0.18,
0.25
],
[
0.3,
0.31,
0.35
],
[
0.45,
0.42,
0.38
],
[
0.6,
0.56,
0.49
]
],
"ground": [
0.055,
0.055,
0.07
],
"accent": [
0.78,
0.8,
0.86
],
"mesh": [
[
0.096,
0.114,
0.166
],
[
0.0984,
0.116933333,
0.169733333
],
[
0.224,
0.244133333,
0.299333333
],
[
0.228,
0.2476,
0.302
],
[
0.0912,
0.108133333,
0.158533333
],
[
0.284,
0.296133333,
0.339333333
],
[
0.24,
0.258,
0.31
],
[
0.436,
0.409733333,
0.3772
],
[
0.236,
0.254533333,
0.307333333
],
[
0.228,
0.2476,
0.302
],
[
0.496,
0.462933333,
0.413733333
],
[
0.476,
0.444266667,
0.399066667
],
[
0.24,
0.258,
0.31
],
[
0.448,
0.418533333,
0.3796
],
[
0.464,
0.433066667,
0.390266667
],
[
0.6,
0.56,
0.49
]
],
"blobs": [
[
0.114,
0.136,
0.194
],
[
0.24,
0.258,
0.31
],
[
0.39,
0.376,
0.368
],
[
0.54,
0.504,
0.446
]
]
},
{
"id": "holo",
"name": "Holo",
"light": true,
"stops": [
[
0.99,
0.72,
0.9
],
[
0.8,
0.6,
0.98
],
[
0.58,
0.62,
0.99
],
[
0.55,
0.86,
0.98
],
[
0.94,
0.98,
1.0
]
],
"ground": [
0.96,
0.92,
0.99
],
"accent": [
0.42,
0.28,
0.86
],
"mesh": [
[
0.914,
0.672,
0.932
],
[
0.908933333,
0.6688,
0.934133333
],
[
0.691466667,
0.609866667,
0.984933333
],
[
0.6856,
0.6104,
0.9852
],
[
0.924133333,
0.6784,
0.927733333
],
[
0.603466667,
0.617866667,
0.988933333
],
[
0.668,
0.612,
0.986
],
[
0.5528,
0.8376,
0.980933333
],
[
0.673866667,
0.611466667,
0.985733333
],
[
0.6856,
0.6104,
0.9852
],
[
0.6696,
0.8968,
0.986133333
],
[
0.6176,
0.8808,
0.983466667
],
[
0.668,
0.612,
0.986
],
[
0.5504,
0.8568,
0.980133333
],
[
0.5864,
0.8712,
0.981866667
],
[
0.94,
0.98,
1.0
]
],
"blobs": [
[
0.876,
0.648,
0.948
],
[
0.668,
0.612,
0.986
],
[
0.562,
0.764,
0.984
],
[
0.784,
0.932,
0.992
]
]
},
{
"id": "sunset",
"name": "Sunset",
"light": true,
"stops": [
[
0.55,
0.45,
0.92
],
[
0.86,
0.31,
0.66
],
[
0.97,
0.26,
0.34
],
[
0.99,
0.51,
0.18
],
[
1.0,
0.8,
0.22
]
],
"ground": [
0.98,
0.74,
0.34
],
"accent": [
0.64,
0.13,
0.44
],
"mesh": [
[
0.674,
0.394,
0.816
],
[
0.682266667,
0.390266667,
0.809066667
],
[
0.914266667,
0.285333333,
0.502133333
],
[
0.9172,
0.284,
0.4936
],
[
0.657466667,
0.401466667,
0.829866667
],
[
0.958266667,
0.265333333,
0.374133333
],
[
0.926,
0.28,
0.468
],
[
0.988133333,
0.486666667,
0.194933333
],
[
0.923066667,
0.281333333,
0.476533333
],
[
0.9172,
0.284,
0.4936
],
[
0.993066667,
0.598933333,
0.192266667
],
[
0.991733333,
0.560266667,
0.186933333
],
[
0.926,
0.28,
0.468
],
[
0.989733333,
0.506666667,
0.182133333
],
[
0.990933333,
0.537066667,
0.183733333
],
[
1.0,
0.8,
0.22
]
],
"blobs": [
[
0.736,
0.366,
0.764
],
[
0.926,
0.28,
0.468
],
[
0.982,
0.41,
0.244
],
[
0.996,
0.684,
0.204
]
]
},
{
"id": "bloom",
"name": "Bloom",
"light": true,
"stops": [
[
1.0,
0.86,
0.72
],
[
0.99,
0.73,
0.79
],
[
0.95,
0.65,
0.89
],
[
0.82,
0.68,
0.96
],
[
0.73,
0.79,
0.99
]
],
"ground": [
0.99,
0.9,
0.89
],
"accent": [
0.72,
0.24,
0.55
],
"mesh": [
[
0.996,
0.808,
0.748
],
[
0.995733333,
0.804533333,
0.749866667
],
[
0.970266667,
0.690533333,
0.839333333
],
[
0.9692,
0.6884,
0.842
],
[
0.996533333,
0.814933333,
0.744266667
],
[
0.954266667,
0.658533333,
0.879333333
],
[
0.966,
0.682,
0.85
],
[
0.832133333,
0.6772,
0.953466667
],
[
0.967066667,
0.684133333,
0.847333333
],
[
0.9692,
0.6884,
0.842
],
[
0.7924,
0.713733333,
0.9692
],
[
0.8044,
0.699066667,
0.9652
],
[
0.966,
0.682,
0.85
],
[
0.821733333,
0.6796,
0.959066667
],
[
0.8116,
0.690266667,
0.9628
],
[
0.73,
0.79,
0.99
]
],
"blobs": [
[
0.994,
0.782,
0.762
],
[
0.966,
0.682,
0.85
],
[
0.872,
0.668,
0.932
],
[
0.766,
0.746,
0.978
]
]
},
{
"id": "dawn",
"name": "Dawn",
"light": true,
"stops": [
[
1.0,
0.92,
0.7
],
[
1.0,
0.8,
0.62
],
[
0.99,
0.66,
0.62
],
[
0.9,
0.62,
0.78
],
[
0.77,
0.69,
0.95
]
],
"ground": [
1.0,
0.93,
0.82
],
"accent": [
0.82,
0.33,
0.28
],
"mesh": [
[
1.0,
0.872,
0.668
],
[
1.0,
0.8688,
0.665866667
],
[
0.995066667,
0.730933333,
0.62
],
[
0.9948,
0.7272,
0.62
],
[
1.0,
0.8784,
0.672266667
],
[
0.991066667,
0.674933333,
0.62
],
[
0.994,
0.716,
0.62
],
[
0.9084,
0.623733333,
0.765066667
],
[
0.994266667,
0.719733333,
0.62
],
[
0.9948,
0.7272,
0.62
],
[
0.860133333,
0.641466667,
0.832133333
],
[
0.877466667,
0.632133333,
0.809466667
],
[
0.994,
0.716,
0.62
],
[
0.9012,
0.620533333,
0.777866667
],
[
0.887866667,
0.626533333,
0.795866667
],
[
0.77,
0.69,
0.95
]
],
"blobs": [
[
1.0,
0.848,
0.652
],
[
0.994,
0.716,
0.62
],
[
0.936,
0.636,
0.716
],
[
0.822,
0.662,
0.882
]
]
},
{
"id": "mint",
"name": "Mint",
"light": true,
"stops": [
[
0.82,
0.98,
0.9
],
[
0.62,
0.94,
0.88
],
[
0.55,
0.88,
0.95
],
[
0.63,
0.82,
0.99
],
[
0.82,
0.87,
1.0
]
],
"ground": [
0.9,
0.98,
0.96
],
"accent": [
0.04,
0.42,
0.4
],
"mesh": [
[
0.74,
0.964,
0.892
],
[
0.734666667,
0.962933333,
0.891466667
],
[
0.585466667,
0.9104,
0.914533333
],
[
0.5836,
0.9088,
0.9164
],
[
0.750666667,
0.966133333,
0.893066667
],
[
0.557466667,
0.8864,
0.942533333
],
[
0.578,
0.904,
0.922
],
[
0.622533333,
0.8256,
0.986266667
],
[
0.579866667,
0.9056,
0.920133333
],
[
0.5836,
0.9088,
0.9164
],
[
0.688266667,
0.835333333,
0.993066667
],
[
0.662933333,
0.828666667,
0.991733333
],
[
0.578,
0.904,
0.922
],
[
0.628933333,
0.8208,
0.989466667
],
[
0.647733333,
0.824666667,
0.990933333
],
[
0.82,
0.87,
1.0
]
],
"blobs": [
[
0.7,
0.956,
0.888
],
[
0.578,
0.904,
0.922
],
[
0.598,
0.844,
0.974
],
[
0.744,
0.85,
0.996
]
]
},
{
"id": "opal",
"name": "Opal",
"light": true,
"stops": [
[
0.98,
0.92,
0.96
],
[
0.87,
0.93,
0.99
],
[
0.91,
0.99,
0.95
],
[
0.99,
0.96,
0.88
],
[
0.94,
0.9,
0.99
]
],
"ground": [
0.97,
0.96,
0.99
],
"accent": [
0.36,
0.32,
0.44
],
"mesh": [
[
0.936,
0.924,
0.972
],
[
0.933066667,
0.924266667,
0.9728
],
[
0.889733333,
0.9596,
0.970266667
],
[
0.8908,
0.9612,
0.9692
],
[
0.941866667,
0.923466667,
0.9704
],
[
0.905733333,
0.9836,
0.954266667
],
[
0.894,
0.966,
0.966
],
[
0.982533333,
0.9628,
0.886533333
],
[
0.892933333,
0.9644,
0.967066667
],
[
0.8908,
0.9612,
0.9692
],
[
0.974666667,
0.9416,
0.913733333
],
[
0.981333333,
0.9496,
0.899066667
],
[
0.894,
0.966,
0.966
],
[
0.988933333,
0.9604,
0.880933333
],
[
0.985333333,
0.9544,
0.890266667
],
[
0.94,
0.9,
0.99
]
],
"blobs": [
[
0.914,
0.926,
0.978
],
[
0.894,
0.966,
0.966
],
[
0.958,
0.972,
0.908
],
[
0.96,
0.924,
0.946
]
]
}
],
"tabs": [
{
"name": "Stream"
},
{
"name": "Video"
},
{
"name": "Audio"
},
{
"name": "Controller"
},
{
"name": "Input",
"desktop_only": true,
"$why": "Touch mode, mouse, invert-scroll and shortcuts are desktop-host settings with nothing to set on a phone or a TV, so the mobile clients ship six tabs and the desktop console seven. Modelled rather than omitted: a flat six-name list would red the desktop on day one, and a seven-name list would red both mobile clients."
},
{
"name": "Interface"
},
{
"name": "Profiles"
}
],
"motion": {
"$deprecated": "SUPERSEDED by `motion_spring` (version 2). The desktop console's transition is a damped spring now, not a 0.26 s ease-out-cubic, so it no longer implements this block and its test reads `motion_spring` instead. Kept in place, and still correct, for the client that has not migrated: the Android client's ConsoleVectorsTest pins it. (The Apple client moved to `motion_spring` on 2026-08-18 - `ConsoleMotion` in PunktfunkShared, pinned by its ConsoleVectorsTests.) DELETE THIS BLOCK when Android moves; until then the drift is visible rather than silent, because each client's test names which block it implements.",
"$why": "The console screen transition, from pf-console-ui's shell: TRANSITION_S and the paint geometry in shell/render.rs. Every client re-implements this by hand in its own animation system, which is why it is pinned here.",
"transition_s": 0.26,
"push_slide_dp": 36.0,
"enter_scale": 0.985,
"exit_scale": 0.96,
"reveal_alpha": 0.4,
"ease_out_cubic": {
"$why": "Sampled, not given as Bezier control points: the desktop's curve is the analytic 1-(1-t)^3, Android reproduces it exactly, and SwiftUI can only approximate it with a timing curve. A tolerance on samples is the only form all three can meet. Two different beziers are published under the name 'easeOutCubic' and neither is this curve -- that is the drift this pins.",
"tolerance": 0.02,
"samples": [
{
"t": 0.0,
"p": 0.0
},
{
"t": 0.125,
"p": 0.330078125
},
{
"t": 0.25,
"p": 0.578125
},
{
"t": 0.375,
"p": 0.755859375
},
{
"t": 0.5,
"p": 0.875
},
{
"t": 0.625,
"p": 0.947265625
},
{
"t": 0.75,
"p": 0.984375
},
{
"t": 0.875,
"p": 0.998046875
},
{
"t": 1.0,
"p": 1.0
}
]
}
},
"motion_spring": {
"$why": "The console screen transition, from pf-console-ui's shell. Springs are INTEGRATOR-dependent - semi-implicit Euler in <=8 ms substeps here, a different solver in every other runtime - so unlike the v1 curve this pins PARAMETERS, not sampled positions. Two implementations that both honour response/damping will agree to the eye and disagree in the third decimal, and sampling would pin the disagreement instead of the feel. The geometry below is unchanged from v1: the spring replaced the time-course, not the choreography.",
"response": 0.42,
"damping": 0.88,
"push_slide_dp": 36.0,
"enter_scale": 0.985,
"exit_scale": 0.96,
"reveal_alpha": 0.4,
"interruptible": true,
"$interruptible": "Back pressed mid-push retargets the SAME spring to 0 - the entering screen decelerates, turns, and goes back - and mid-pop starts the next pop immediately. Input other than Back stays queued-dropped until the spring passes 0.85, which keeps a double-tapped A from pushing two screens."
}
}