PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/mio.php +262 -28 1.1.31.1.10 View file →
@@ -22,39 +22,24 @@
22 22
23 23 defined( 'ABSPATH' ) || exit;
24 24
25 25 /**
26 - * Returns Mio configuration for the current user.
26 + * Mio's shipped look and feel, before any filter.
27 27 *
28 - * Shape mirrors `MioConfig` in `src/mio/types.ts`:
28 + * Split out of {@see openstation_mio_config()} so a consumer that is
29 + * NOT the current user's own companion can start from the reference
30 + * design. An agent's portrait is exactly that: `openstation_mio_config`
31 + * is a filter about the desk companion this person sees, and letting it
32 + * silently restyle every agent's face on the site would be a surprise
33 + * with no way to opt out.
29 34 *
30 - * array(
31 - * 'appearance' => array( radius, bodyColor, bodyAlpha, hueStart,
32 - * hueSpan, hueDrift, hueLoop, hueAngle,
33 - * saturation, lightness, iridescence,
34 - * outlineWidth, glow, glowBlur,
35 - * eyeColor, eyeScale ),
36 - * 'physics' => array( points, shapePreset, shapeLobes,
37 - * shapeAmount, shapeAngle, shapeShuffle,
38 - * radialStiffness, edgeStiffness,
39 - * bendStiffness, pressure, damping,
40 - * airDamping, magnetStrength, magnetRange,
41 - * magnetGrip, magnetDamping, floatAmplitude,
42 - * floatSpeed, idleWobble, idleWobbleSpeed,
43 - * speedStretch, friction, restitution,
44 - * dragStiffness, throwBoost, minStretch,
45 - * maxStretch, minAngularGap,
46 - * limitIterations, dragMaxAccel, subStep,
47 - * maxSubSteps ),
48 - * )
35 + * Shape mirrors `MIO_DEFAULTS` in `src/mio/config.ts`, and
36 + * `tests/vitest/mio-defaults-parity.test.ts` holds the two together.
49 37 *
50 - * Colours may be given as integers (`0x05050a`) or CSS hex strings
51 - * (`'#05050a'`); the client accepts both.
52 - *
53 38 * @return array Mio configuration.
54 39 */
55 -function openstation_mio_config() {
56 - $defaults = array(
40 +function openstation_mio_default_config() {
41 + return array(
57 42 'appearance' => array(
58 43 'radius' => 56,
59 44 // Void, the palette's base. The brand's own Mio is
60 45 // `fill="none"` over the Void page; the shell floats over
@@ -91,9 +76,22 @@
91 76 // The official artwork has no hologram and no interior
92 77 // sheen — a flat gradient over dead black. One number here
93 78 // turns both back on for a whole site.
94 79 'iridescence' => 0,
95 - 'outlineWidth' => 3,
80 + // The artwork's ring is 13 units on a body of roughly 240 —
81 + // 5.4%, or 6px at this radius. That 6 is the whole drawn
82 + // ring, chroma and white line together, split two to one.
83 + 'outlineWidth' => 4,
84 + // The white line the artwork draws between the body and the
85 + // chroma. It reaches INWARD, so thickening it eats into the
86 + // body rather than widening the ring — 'outlineWidth' keeps
87 + // meaning the coloured band whatever this says.
88 + //
89 + // Must match `MIO_DEFAULTS` in `src/mio/config.ts`.
90 + 'linerWidth' => 2,
91 + // Starlight again: the brand has one white, and the line and
92 + // the eyes are both drawn in it.
93 + 'linerColor' => '#fffbff',
96 94 // Reach of the light, as a multiple of Mio's own radius:
97 95 // `10` carries the wash about one and a half radii past the
98 96 // outline. Deliberately generous — Mio sits on a dark desk
99 97 // and the glow is the thing that makes her read as lit
@@ -155,9 +153,43 @@
155 153 'subStep' => 1 / 240,
156 154 'maxSubSteps' => 8,
157 155 ),
158 156 );
157 +}
159 158
159 +/**
160 + * Returns Mio configuration for the current user.
161 + *
162 + * Shape mirrors `MioConfig` in `src/mio/types.ts`:
163 + *
164 + * array(
165 + * 'appearance' => array( radius, bodyColor, bodyAlpha, hueStart,
166 + * hueSpan, hueDrift, hueLoop, hueAngle,
167 + * saturation, lightness, iridescence,
168 + * outlineWidth, linerWidth, linerColor,
169 + * glow, glowBlur, eyeColor, eyeScale ),
170 + * 'physics' => array( points, shapePreset, shapeLobes,
171 + * shapeAmount, shapeAngle, shapeShuffle,
172 + * radialStiffness, edgeStiffness,
173 + * bendStiffness, pressure, damping,
174 + * airDamping, magnetStrength, magnetRange,
175 + * magnetGrip, magnetDamping, floatAmplitude,
176 + * floatSpeed, idleWobble, idleWobbleSpeed,
177 + * speedStretch, friction, restitution,
178 + * dragStiffness, throwBoost, minStretch,
179 + * maxStretch, minAngularGap,
180 + * limitIterations, dragMaxAccel, subStep,
181 + * maxSubSteps ),
182 + * )
183 + *
184 + * Colours may be given as integers (`0x05050a`) or CSS hex strings
185 + * (`'#05050a'`); the client accepts both.
186 + *
187 + * @return array Mio configuration.
188 + */
189 +function openstation_mio_config() {
190 + $defaults = openstation_mio_default_config();
191 +
160 192 /**
161 193 * Filters Mio's appearance and physics.
162 194 *
163 195 * Runs once per shell render. Returning a partial array is fine —
@@ -203,8 +235,10 @@
203 235 'saturation',
204 236 'lightness',
205 237 'iridescence',
206 238 'outlineWidth',
239 + 'linerWidth',
240 + 'linerColor',
207 241 'glow',
208 242 'glowBlur',
209 243 'eyeColor',
210 244 'eyeScale',
@@ -234,8 +268,161 @@
234 268 );
235 269 }
236 270
237 271 /**
272 + * Numeric ranges every look value is held inside.
273 + *
274 + * Mirrors `LIMITS` in `src/mio/config.ts`, and exists for the same
275 + * reason the client one does: the shipped values are a design, not a
276 + * boundary, and everything downstream of them assumes a sane number.
277 + *
278 + * Only the keys a stored look may carry are listed. The rest of
279 + * `MioPhysics` is spring constants the panel deliberately never
280 + * exposes.
281 + *
282 + * @return array<string, array{0: float, 1: float}>
283 + */
284 +function openstation_mio_look_limits() {
285 + return array(
286 + 'radius' => array( 16, 220 ),
287 + 'bodyAlpha' => array( 0, 1 ),
288 + 'hueStart' => array( -720, 720 ),
289 + 'hueSpan' => array( -360, 360 ),
290 + 'hueDrift' => array( -180, 180 ),
291 + 'hueAngle' => array( -360, 360 ),
292 + 'hueSpin' => array( -180, 180 ),
293 + 'saturation' => array( 0, 1 ),
294 + 'lightness' => array( 0.15, 1 ),
295 + 'iridescence' => array( 0, 2 ),
296 + 'outlineWidth' => array( 0.5, 24 ),
297 + 'linerWidth' => array( 0, 12 ),
298 + 'glow' => array( 0, 20 ),
299 + 'eyeScale' => array( 0.05, 0.6 ),
300 + 'shapeLobes' => array( 0, 8 ),
301 + 'shapeAmount' => array( 0, 1.4 ),
302 + 'shapeAngle' => array( -360, 360 ),
303 + 'shapeShuffle' => array( 0, 3600 ),
304 + 'idleWobble' => array( 0, 0.4 ),
305 + 'idleWobbleSpeed' => array( 0, 8 ),
306 + );
307 +}
308 +
309 +/**
310 + * Every silhouette `shapePreset` accepts. Mirrors `SHAPE_PRESETS`.
311 + *
312 + * @return string[]
313 + */
314 +function openstation_mio_shape_presets() {
315 + return array(
316 + 'circle',
317 + 'blob',
318 + 'ghost',
319 + 'potato',
320 + 'star',
321 + 'flower',
322 + 'heart',
323 + 'diamond',
324 + 'drop',
325 + 'cloud',
326 + 'custom',
327 + );
328 +}
329 +
330 +/**
331 + * Coerce a colour to a 24-bit int. Mirrors `color()` in `config.ts`.
332 + *
333 + * The shipped defaults write colours as CSS hex strings because that is
334 + * what reads well in a config array; the renderers want integers.
335 + *
336 + * @param mixed $candidate Colour as int or `#rrggbb` / `#rgb` string.
337 + * @param int $fallback Value to use when the candidate is unusable.
338 + * @return int Packed 24-bit colour.
339 + */
340 +function openstation_mio_color_int( $candidate, $fallback = 0 ) {
341 + if ( is_int( $candidate ) || is_float( $candidate ) ) {
342 + if ( ! is_finite( (float) $candidate ) ) {
343 + return $fallback;
344 + }
345 + return (int) min( 0xffffff, max( 0, floor( $candidate ) ) );
346 + }
347 + if ( is_string( $candidate ) ) {
348 + $hex = ltrim( trim( $candidate ), '#' );
349 + if ( preg_match( '/^[0-9a-fA-F]{6}$/', $hex ) ) {
350 + return (int) hexdec( $hex );
351 + }
352 + if ( preg_match( '/^[0-9a-fA-F]{3}$/', $hex ) ) {
353 + return (int) hexdec( $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2] );
354 + }
355 + }
356 + return $fallback;
357 +}
358 +
359 +/**
360 + * Hold a look inside its ranges and resolve it against the defaults.
361 + *
362 + * `openstation_sanitize_mio_look()` is a SHAPE check: right keys,
363 + * right kinds. That was enough while every look was handed straight to
364 + * the client, where `sanitizeMioConfig()` clamped it before anything
365 + * drew with it. It is not enough now. A stored look reaches a PHP
366 + * renderer that samples trigonometry and builds a path, so an
367 + * `outlineWidth` of -400 or a `shapeAmount` of 1e9 arrives as a number
368 + * nobody checked.
369 + *
370 + * The result is a COMPLETE config with integer colours, ready to draw:
371 + * unlike the sanitizer, this is not what you store.
372 + *
373 + * `shapeShuffle` is dropped rather than clamped. It means "pick a new
374 + * silhouette every so often", which is meaningless in a still portrait
375 + * and would be a bug if anything ever honoured it there.
376 + *
377 + * @param mixed $raw Stored look, or anything at all.
378 + * @return array Complete `array( 'appearance' => ..., 'physics' => ... )`.
379 + */
380 +function openstation_mio_clamp_look( $raw ) {
381 + $defaults = openstation_mio_default_config();
382 + $limits = openstation_mio_look_limits();
383 + $look = openstation_sanitize_mio_look( $raw );
384 +
385 + $resolve = static function ( $group, $overrides ) use ( $limits ) {
386 + $out = array();
387 + foreach ( $group as $key => $default ) {
388 + $value = array_key_exists( $key, $overrides ) ? $overrides[ $key ] : $default;
389 +
390 + if ( 'shapePreset' === $key ) {
391 + $presets = openstation_mio_shape_presets();
392 + $out[ $key ] = in_array( $value, $presets, true ) ? $value : $default;
393 + continue;
394 + }
395 + if ( 'bodyColor' === $key || 'eyeColor' === $key || 'linerColor' === $key ) {
396 + $out[ $key ] = openstation_mio_color_int( $value, openstation_mio_color_int( $default ) );
397 + continue;
398 + }
399 + if ( is_bool( $default ) ) {
400 + $out[ $key ] = is_bool( $value ) ? $value : $default;
401 + continue;
402 + }
403 + if ( ! is_numeric( $value ) || ! is_finite( (float) $value ) ) {
404 + $value = $default;
405 + }
406 + if ( isset( $limits[ $key ] ) ) {
407 + $value = min( $limits[ $key ][1], max( $limits[ $key ][0], (float) $value ) );
408 + }
409 + $out[ $key ] = $value;
410 + }
411 + return $out;
412 + };
413 +
414 + $physics = $resolve( $defaults['physics'], $look['physics'] );
415 + // A face that changed silhouette on a timer is not a portrait.
416 + $physics['shapeShuffle'] = 0;
417 +
418 + return array(
419 + 'appearance' => $resolve( $defaults['appearance'], $look['appearance'] ),
420 + 'physics' => $physics,
421 + );
422 +}
423 +
424 +/**
238 425 * Sanitizes a user's saved Mio look for storage in user meta.
239 426 *
240 427 * **A shape check, not a clamp.** It answers "are these the right keys
241 428 * carrying the right kinds of value" and nothing more. Deciding what a
@@ -289,9 +476,9 @@
289 476 $clean[ $group ][ $key ] = 0 + $value;
290 477 }
291 478 } elseif ( is_string( $value ) ) {
292 479 // The only string-valued keys are `shapePreset` and the
293 - // two colours in `#rrggbb` form.
480 + // three colours in `#rrggbb` form.
294 481 $clean[ $group ][ $key ] = sanitize_text_field( $value );
295 482 }
296 483 }
297 484 }
@@ -296,5 +483,52 @@
296 483 }
297 484 }
298 485
299 486 return $clean;
487 +}
488 +
489 +/**
490 + * Narrow a partial look to the keys it carried, with every number in range.
491 + *
492 + * Two passes that answer different questions.
493 + * {@see openstation_sanitize_mio_look()} asks "are these the right keys
494 + * carrying the right kinds of value", and keeps only what was actually
495 + * set. {@see openstation_mio_clamp_look()} then asks "is every number
496 + * inside its range", because a stored look now reaches a PHP renderer
497 + * that samples trigonometry and builds a path. The clamp resolves
498 + * against the defaults and hands back a *complete* config, which is
499 + * what you draw with and not what you store, so the carried keys are
500 + * picked back out of it afterwards.
501 + *
502 + * Keeping only the overridden keys is what lets a future change to the
503 + * shipped Mio still show through wherever nobody had an opinion.
504 + *
505 + * Lives here rather than beside the agent store because two callers on
506 + * different sides of the feature flag need it: the store, when an agent
507 + * saves a face, and the WP Explorer config, when the flag is off and
508 + * the section previews the cast it would seed. One owner of the rule
509 + * means the preview cannot draw a face the seeder would not store.
510 + *
511 + * @param mixed $raw Raw look (array), from the client or from our own data.
512 + * @return array {
513 + * @type array $appearance Clamped appearance overrides.
514 + * @type array $physics Clamped silhouette + idle overrides.
515 + * }
516 + */
517 +function openstation_mio_narrow_look( $raw ) {
518 + $look = openstation_sanitize_mio_look( $raw );
519 + $clamped = openstation_mio_clamp_look( $look );
520 + $out = array(
521 + 'appearance' => array(),
522 + 'physics' => array(),
523 + );
524 +
525 + foreach ( array( 'appearance', 'physics' ) as $group ) {
526 + foreach ( array_keys( $look[ $group ] ) as $key ) {
527 + if ( array_key_exists( $key, $clamped[ $group ] ) ) {
528 + $out[ $group ][ $key ] = $clamped[ $group ][ $key ];
529 + }
530 + }
531 + }
532 +
533 + return $out;
300 534 }