@vectoriox/iox-ui 5.77.0 → 5.79.0
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/design-core/index.d.ts +34 -4
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +34 -4
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +950 -15
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs +38 -3
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-media-core.mjs +23 -1
- package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +3 -6
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/generation-core/index.d.ts +585 -8
- package/harness-core/index.d.ts +40 -2
- package/media-core/index.d.ts +19 -1
- package/package.json +1 -1
- package/scene-core/index.d.ts +1 -4
- package/src/design-core/flavor-spec.generated.ts +34 -4
- package/src/generation-core/compose-site.ts +25 -2
- package/src/generation-core/film.ts +91 -0
- package/src/generation-core/patterns.ts +41 -4
- package/src/generation-core/plan-wire.ts +4 -2
- package/src/generation-core/public-api.ts +1 -0
- package/src/generation-core/serialized-node.ts +3 -0
- package/src/generation-core/showcase.ts +32 -7
- package/src/generation-core/site-plan.model.ts +8 -0
- package/src/generation-core/site-spec.generated.ts +738 -1
- package/src/generation-core/validate-site-plan.ts +57 -2
- package/src/harness-core/business-types.ts +3 -0
- package/src/harness-core/generation-view.ts +3 -1
- package/src/harness-core/harness-spec.generated.ts +32 -2
- package/src/harness-core/motion.ts +8 -1
- package/src/media-core/frames.ts +23 -0
- package/src/scene-core/scene-edit.ts +3 -5
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { flavorPackById, parseHexColor, SPEC_AXES } from '@vectoriox/iox-ui/design-core';
|
|
2
2
|
import { formPresetById } from '@vectoriox/iox-ui/forms-core';
|
|
3
|
+
import { filmScreensFor, normalizeFrameSequence } from '@vectoriox/iox-ui/media-core';
|
|
3
4
|
import { SEO_DESCRIPTION_MAX, SEO_TITLE_MAX } from '@vectoriox/iox-ui/seo-core';
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
BindScope, patternById, patternHasForm, patternLinkSlots, patternNeedsBinding, patternOptionalParts, patternRoleNeeds, patternTakesBinding, patternScopes,
|
|
7
|
-
patternSlots, PatternGlobalPosition, slotBudget,
|
|
8
|
-
} from './patterns';
|
|
8
|
+
patternSlots, PatternGlobalPosition, slotBudget, SCENE_LEVEL, SCENE_OTHER_PAGES } from './patterns';
|
|
9
9
|
import { ContentFieldControl, FIELD_TYPE_OF_CONTROL,
|
|
10
10
|
FACT_KEYS, GlobalSlot, PAGE_ARCHETYPES, SITE_NEED_IDS, PlanDataSourceMode, PlanDataSourceType, RelativeOp, REQUIRED_CONTENT_ROLES, SitePlan, SitePlanSourceKind,
|
|
11
11
|
SITE_LOCALES, SITE_PLAN_VERSION,
|
|
@@ -70,6 +70,12 @@ export enum SitePlanIssueCode {
|
|
|
70
70
|
UnknownNeed = 'unknown-need',
|
|
71
71
|
/** A page key that is a global slot's name (`header`, `footer`): its sections' ids would be the global's. */
|
|
72
72
|
ReservedPageKey = 'reserved-page-key',
|
|
73
|
+
/** SC6: a Scene below How animated Expressive, a second Scene on a page, or Scenes on more than the home page and
|
|
74
|
+
* one other — the pages answer by choosing another pattern. */
|
|
75
|
+
SceneBudget = 'scene-budget',
|
|
76
|
+
/** SC6b: the owner's film — pictures that cannot play, too few to play smoothly, or a film's pattern on a plan with
|
|
77
|
+
* no film (or twice). Set by the build, never by a model. */
|
|
78
|
+
Film = 'film',
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
export type SitePlanIssue = PathIssue<SitePlanIssueCode>;
|
|
@@ -108,6 +114,7 @@ const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
|
|
|
108
114
|
const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
|
|
109
115
|
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
|
|
110
116
|
const PAGE_KEYS = shapeKeys(SHAPE['page']);
|
|
117
|
+
const FILM_KEYS = shapeKeys(SHAPE['film']);
|
|
111
118
|
const SECTION_KEYS = shapeKeys(SHAPE['section']);
|
|
112
119
|
const GLOBAL_POSITION: Readonly<Record<`${GlobalSlot}`, `${PatternGlobalPosition}`>> = {
|
|
113
120
|
[GlobalSlot.Header]: PatternGlobalPosition.Before,
|
|
@@ -455,6 +462,52 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
|
|
|
455
462
|
}
|
|
456
463
|
|
|
457
464
|
|
|
465
|
+
/** SC6 (premium-sites plan, APPROVED 2026-11-05): who may hold the page — Scenes only at Expressive, one per page, on the
|
|
466
|
+
* home page and at most one other. A Scene past that is an issue at its pattern, so the pages choose another. */
|
|
467
|
+
function validateScenes(c: Collector, pages: unknown[], animate: string | null): void {
|
|
468
|
+
let otherPages = 0;
|
|
469
|
+
pages.forEach((raw, i) => {
|
|
470
|
+
if (!isObject(raw) || !Array.isArray(raw['sections'])) return;
|
|
471
|
+
let onPage = 0;
|
|
472
|
+
const counted = raw['isHome'] === true;
|
|
473
|
+
raw['sections'].forEach((s, j) => {
|
|
474
|
+
const pattern = isObject(s) && typeof s['pattern'] === 'string' ? patternById(s['pattern']) : null;
|
|
475
|
+
if (!pattern?.scene) return;
|
|
476
|
+
const at = `pages[${i}].sections[${j}].pattern`;
|
|
477
|
+
if (animate !== SCENE_LEVEL) return c.add(SitePlanIssueCode.SceneBudget, at, `"${s['pattern']}" holds the page as it scrolls — only on a site whose How animated is Expressive (this one is ${animate ?? 'unset'})`);
|
|
478
|
+
if (++onPage > 1) return c.add(SitePlanIssueCode.SceneBudget, at, 'one Scene per page — choose a section that does not hold the page');
|
|
479
|
+
if (!counted && onPage === 1 && ++otherPages > SCENE_OTHER_PAGES) c.add(SitePlanIssueCode.SceneBudget, at, `Scenes on the home page and at most ${SCENE_OTHER_PAGES} other page — choose a section that does not hold the page`);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** SC6b: a film plays only when its pictures can (and smoothly over the shortest hold); its pattern only with a film, once. */
|
|
485
|
+
function validateFilm(c: Collector, film: unknown, pages: unknown[]): void {
|
|
486
|
+
let playable = false;
|
|
487
|
+
if (film !== undefined) {
|
|
488
|
+
if (!isObject(film)) {
|
|
489
|
+
c.add(SitePlanIssueCode.Shape, 'film', 'must be an object');
|
|
490
|
+
} else {
|
|
491
|
+
c.keys(film, FILM_KEYS, 'film');
|
|
492
|
+
const sequence = normalizeFrameSequence(film['sequence']);
|
|
493
|
+
if (!sequence) c.add(SitePlanIssueCode.Film, 'film.sequence', 'not a film\'s pictures (a media-core FrameSequence)');
|
|
494
|
+
else if (filmScreensFor(sequence.wide.count) === null) c.add(SitePlanIssueCode.Film, 'film.sequence', `${sequence.wide.count} pictures — too few to play smoothly`);
|
|
495
|
+
else playable = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
let placed = 0;
|
|
499
|
+
pages.forEach((raw, i) => {
|
|
500
|
+
if (!isObject(raw) || !Array.isArray(raw['sections'])) return;
|
|
501
|
+
raw['sections'].forEach((s, j) => {
|
|
502
|
+
const pattern = isObject(s) && typeof s['pattern'] === 'string' ? patternById(s['pattern']) : null;
|
|
503
|
+
if (!pattern?.film) return;
|
|
504
|
+
const path = `pages[${i}].sections[${j}].pattern`;
|
|
505
|
+
if (!playable) c.add(SitePlanIssueCode.Film, path, `"${s['pattern']}" plays the owner's film — this plan has none`);
|
|
506
|
+
else if (++placed > 1) c.add(SitePlanIssueCode.Film, path, 'the film plays once on a site');
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
458
511
|
function validatePageSeo(c: Collector, seo: unknown, path: string): void {
|
|
459
512
|
if (seo === undefined) return;
|
|
460
513
|
if (!isObject(seo)) return c.add(SitePlanIssueCode.Shape, path, 'must be an object');
|
|
@@ -501,6 +554,8 @@ export function validateSitePlan(plan: unknown): SitePlanValidation {
|
|
|
501
554
|
const design = isObject(plan['design']) ? plan['design'] : {};
|
|
502
555
|
validatePages(c, c.array(plan, 'pages', ''), types, sources, typeof design['density'] === 'string' ? design['density'] : null);
|
|
503
556
|
validateGlobals(c, plan['globals']);
|
|
557
|
+
validateScenes(c, c.array(plan, 'pages', ''), typeof design['animate'] === 'string' ? design['animate'] : null);
|
|
558
|
+
validateFilm(c, plan['film'], c.array(plan, 'pages', ''));
|
|
504
559
|
return { valid: c.issues.length === 0, issues: c.issues };
|
|
505
560
|
}
|
|
506
561
|
|
|
@@ -111,6 +111,9 @@ export interface BusinessType {
|
|
|
111
111
|
readonly motion: MotionId;
|
|
112
112
|
/** How its site usually scrolls, how animated it is and what plays between pages (rev 3.1). */
|
|
113
113
|
readonly moves: Readonly<DesignMotion>;
|
|
114
|
+
/** SC6: the most How animated a GENERATED site of this type may start at (`motionFor`). A Scene needs Expressive,
|
|
115
|
+
* so a type capped below it never gets one. Binds generated output only — the owner may choose any level. */
|
|
116
|
+
readonly ceiling: DesignMotion['animate'];
|
|
114
117
|
readonly imagery: string;
|
|
115
118
|
readonly pages: ReadonlyArray<string>;
|
|
116
119
|
/** The needs its page set implies, in page order (derived from `pages` at contract build). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DesignChoices } from '@vectoriox/iox-ui/design-core';
|
|
2
|
+
import type { FrameSequence } from '@vectoriox/iox-ui/media-core';
|
|
2
3
|
import type { AiGenerationStage, AiStep, PlanChange, PlanEdit, SitePlan } from '@vectoriox/iox-ui/generation-core';
|
|
3
4
|
|
|
4
5
|
import type { StyleRead } from './rank';
|
|
@@ -78,7 +79,8 @@ export interface AiGenerationView {
|
|
|
78
79
|
stage: `${AiGenerationStage}`;
|
|
79
80
|
/** The model step the runner is working on; null = the owner's move. */
|
|
80
81
|
task: `${AiStep}` | null;
|
|
81
|
-
|
|
82
|
+
/** `film`: the owner's film as pictures (SC6b), told by the wizard at Build — `placeFilm` places it. */
|
|
83
|
+
intake: { prompt: string; files: GenerationFileView[]; addresses: string[]; auto: boolean; film?: FrameSequence };
|
|
82
84
|
readers: GenerationReadersView;
|
|
83
85
|
reads: { analysis?: { value: SiteAnalysis; model: string; usageIds: string[] }; style?: StyleRead | null };
|
|
84
86
|
/** What Design and Plan use: the owner's edit, else the reading. */
|
|
@@ -206,6 +206,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
206
206
|
"animate": "subtle",
|
|
207
207
|
"between": "cinematic"
|
|
208
208
|
},
|
|
209
|
+
"ceiling": "expressive",
|
|
209
210
|
"imagery": "Your own photos only, large and uncropped; never stock.",
|
|
210
211
|
"pages": [
|
|
211
212
|
"home",
|
|
@@ -310,6 +311,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
310
311
|
"animate": "subtle",
|
|
311
312
|
"between": "cinematic"
|
|
312
313
|
},
|
|
314
|
+
"ceiling": "expressive",
|
|
313
315
|
"imagery": "Wide, bright room shots; details of materials.",
|
|
314
316
|
"pages": [
|
|
315
317
|
"home",
|
|
@@ -419,6 +421,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
419
421
|
"animate": "subtle",
|
|
420
422
|
"between": "fade"
|
|
421
423
|
},
|
|
424
|
+
"ceiling": "expressive",
|
|
422
425
|
"imagery": "Large project photos and drawings.",
|
|
423
426
|
"pages": [
|
|
424
427
|
"home",
|
|
@@ -524,9 +527,10 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
524
527
|
"motion": "standard",
|
|
525
528
|
"moves": {
|
|
526
529
|
"scroll": "smooth",
|
|
527
|
-
"animate": "
|
|
530
|
+
"animate": "expressive",
|
|
528
531
|
"between": "slide"
|
|
529
532
|
},
|
|
533
|
+
"ceiling": "expressive",
|
|
530
534
|
"imagery": "Case-study images of your own work.",
|
|
531
535
|
"pages": [
|
|
532
536
|
"home",
|
|
@@ -625,6 +629,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
625
629
|
"animate": "standard",
|
|
626
630
|
"between": "fade"
|
|
627
631
|
},
|
|
632
|
+
"ceiling": "expressive",
|
|
628
633
|
"imagery": "Your pieces, photographed flat and true to colour.",
|
|
629
634
|
"pages": [
|
|
630
635
|
"home",
|
|
@@ -726,9 +731,10 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
726
731
|
"motion": "calm",
|
|
727
732
|
"moves": {
|
|
728
733
|
"scroll": "normal",
|
|
729
|
-
"animate": "
|
|
734
|
+
"animate": "expressive",
|
|
730
735
|
"between": "slide"
|
|
731
736
|
},
|
|
737
|
+
"ceiling": "expressive",
|
|
732
738
|
"imagery": "Your pieces and hands at work; natural light.",
|
|
733
739
|
"pages": [
|
|
734
740
|
"home",
|
|
@@ -826,6 +832,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
826
832
|
"animate": "subtle",
|
|
827
833
|
"between": "cinematic"
|
|
828
834
|
},
|
|
835
|
+
"ceiling": "expressive",
|
|
829
836
|
"imagery": "Close-ups on a plain ground.",
|
|
830
837
|
"pages": [
|
|
831
838
|
"home",
|
|
@@ -927,6 +934,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
927
934
|
"animate": "standard",
|
|
928
935
|
"between": "slide"
|
|
929
936
|
},
|
|
937
|
+
"ceiling": "expressive",
|
|
930
938
|
"imagery": "Healed work, clean photos.",
|
|
931
939
|
"pages": [
|
|
932
940
|
"home",
|
|
@@ -1022,6 +1030,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1022
1030
|
"animate": "subtle",
|
|
1023
1031
|
"between": "fade"
|
|
1024
1032
|
},
|
|
1033
|
+
"ceiling": "standard",
|
|
1025
1034
|
"imagery": "Rooms, textures, hands; no faces needed.",
|
|
1026
1035
|
"pages": [
|
|
1027
1036
|
"home",
|
|
@@ -1140,6 +1149,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1140
1149
|
"animate": "standard",
|
|
1141
1150
|
"between": "fade"
|
|
1142
1151
|
},
|
|
1152
|
+
"ceiling": "lively",
|
|
1143
1153
|
"imagery": "Your own before-and-after photos.",
|
|
1144
1154
|
"pages": [
|
|
1145
1155
|
"home",
|
|
@@ -1241,6 +1251,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1241
1251
|
"animate": "subtle",
|
|
1242
1252
|
"between": "fade"
|
|
1243
1253
|
},
|
|
1254
|
+
"ceiling": "lively",
|
|
1244
1255
|
"imagery": "The room, real classes, daylight.",
|
|
1245
1256
|
"pages": [
|
|
1246
1257
|
"home",
|
|
@@ -1350,6 +1361,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1350
1361
|
"animate": "lively",
|
|
1351
1362
|
"between": "slide"
|
|
1352
1363
|
},
|
|
1364
|
+
"ceiling": "lively",
|
|
1353
1365
|
"imagery": "Real people training; movement.",
|
|
1354
1366
|
"pages": [
|
|
1355
1367
|
"home",
|
|
@@ -1451,6 +1463,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1451
1463
|
"animate": "subtle",
|
|
1452
1464
|
"between": "none"
|
|
1453
1465
|
},
|
|
1466
|
+
"ceiling": "subtle",
|
|
1454
1467
|
"imagery": "A calm room, daylight; your portrait.",
|
|
1455
1468
|
"pages": [
|
|
1456
1469
|
"home",
|
|
@@ -1560,6 +1573,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1560
1573
|
"animate": "subtle",
|
|
1561
1574
|
"between": "fade"
|
|
1562
1575
|
},
|
|
1576
|
+
"ceiling": "subtle",
|
|
1563
1577
|
"imagery": "Your team and rooms; no stock doctors.",
|
|
1564
1578
|
"pages": [
|
|
1565
1579
|
"home",
|
|
@@ -1664,6 +1678,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1664
1678
|
"animate": "subtle",
|
|
1665
1679
|
"between": "fade"
|
|
1666
1680
|
},
|
|
1681
|
+
"ceiling": "subtle",
|
|
1667
1682
|
"imagery": "Your clinic and team.",
|
|
1668
1683
|
"pages": [
|
|
1669
1684
|
"home",
|
|
@@ -1769,6 +1784,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1769
1784
|
"animate": "standard",
|
|
1770
1785
|
"between": "fade"
|
|
1771
1786
|
},
|
|
1787
|
+
"ceiling": "standard",
|
|
1772
1788
|
"imagery": "Real animals you treat (with permission).",
|
|
1773
1789
|
"pages": [
|
|
1774
1790
|
"home",
|
|
@@ -1866,6 +1882,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1866
1882
|
"animate": "standard",
|
|
1867
1883
|
"between": "fade"
|
|
1868
1884
|
},
|
|
1885
|
+
"ceiling": "expressive",
|
|
1869
1886
|
"imagery": "Your own food and room photos.",
|
|
1870
1887
|
"pages": [
|
|
1871
1888
|
"home",
|
|
@@ -1974,6 +1991,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
1974
1991
|
"animate": "standard",
|
|
1975
1992
|
"between": "slide"
|
|
1976
1993
|
},
|
|
1994
|
+
"ceiling": "lively",
|
|
1977
1995
|
"imagery": "Your bakes and drinks, close up.",
|
|
1978
1996
|
"pages": [
|
|
1979
1997
|
"home",
|
|
@@ -2076,6 +2094,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2076
2094
|
"animate": "subtle",
|
|
2077
2095
|
"between": "cinematic"
|
|
2078
2096
|
},
|
|
2097
|
+
"ceiling": "expressive",
|
|
2079
2098
|
"imagery": "The cellar, the bar, the bottle.",
|
|
2080
2099
|
"pages": [
|
|
2081
2100
|
"home",
|
|
@@ -2178,6 +2197,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2178
2197
|
"animate": "standard",
|
|
2179
2198
|
"between": "slide"
|
|
2180
2199
|
},
|
|
2200
|
+
"ceiling": "expressive",
|
|
2181
2201
|
"imagery": "Product photos on plain grounds; real people wearing them.",
|
|
2182
2202
|
"pages": [
|
|
2183
2203
|
"home",
|
|
@@ -2272,6 +2292,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2272
2292
|
"animate": "standard",
|
|
2273
2293
|
"between": "fade"
|
|
2274
2294
|
},
|
|
2295
|
+
"ceiling": "lively",
|
|
2275
2296
|
"imagery": "Your own arrangements in daylight.",
|
|
2276
2297
|
"pages": [
|
|
2277
2298
|
"home",
|
|
@@ -2367,6 +2388,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2367
2388
|
"animate": "subtle",
|
|
2368
2389
|
"between": "fade"
|
|
2369
2390
|
},
|
|
2391
|
+
"ceiling": "expressive",
|
|
2370
2392
|
"imagery": "Pieces in real rooms and on plain grounds.",
|
|
2371
2393
|
"pages": [
|
|
2372
2394
|
"home",
|
|
@@ -2463,6 +2485,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2463
2485
|
"animate": "subtle",
|
|
2464
2486
|
"between": "fade"
|
|
2465
2487
|
},
|
|
2488
|
+
"ceiling": "standard",
|
|
2466
2489
|
"imagery": "Your portrait; diagrams over stock.",
|
|
2467
2490
|
"pages": [
|
|
2468
2491
|
"home",
|
|
@@ -2563,6 +2586,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2563
2586
|
"animate": "standard",
|
|
2564
2587
|
"between": "fade"
|
|
2565
2588
|
},
|
|
2589
|
+
"ceiling": "standard",
|
|
2566
2590
|
"imagery": "Your portrait, working with people.",
|
|
2567
2591
|
"pages": [
|
|
2568
2592
|
"home",
|
|
@@ -2658,6 +2682,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2658
2682
|
"animate": "still",
|
|
2659
2683
|
"between": "none"
|
|
2660
2684
|
},
|
|
2685
|
+
"ceiling": "subtle",
|
|
2661
2686
|
"imagery": "Your team; your office.",
|
|
2662
2687
|
"pages": [
|
|
2663
2688
|
"home",
|
|
@@ -2767,6 +2792,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2767
2792
|
"animate": "standard",
|
|
2768
2793
|
"between": "slide"
|
|
2769
2794
|
},
|
|
2795
|
+
"ceiling": "lively",
|
|
2770
2796
|
"imagery": "Your own property photos; never stock.",
|
|
2771
2797
|
"pages": [
|
|
2772
2798
|
"home",
|
|
@@ -2871,6 +2897,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2871
2897
|
"animate": "standard",
|
|
2872
2898
|
"between": "fade"
|
|
2873
2899
|
},
|
|
2900
|
+
"ceiling": "standard",
|
|
2874
2901
|
"imagery": "Real people you help (with permission).",
|
|
2875
2902
|
"pages": [
|
|
2876
2903
|
"home",
|
|
@@ -2963,6 +2990,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
2963
2990
|
"animate": "standard",
|
|
2964
2991
|
"between": "cinematic"
|
|
2965
2992
|
},
|
|
2993
|
+
"ceiling": "expressive",
|
|
2966
2994
|
"imagery": "Real events at your venue.",
|
|
2967
2995
|
"pages": [
|
|
2968
2996
|
"home",
|
|
@@ -3064,6 +3092,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
3064
3092
|
"animate": "subtle",
|
|
3065
3093
|
"between": "cinematic"
|
|
3066
3094
|
},
|
|
3095
|
+
"ceiling": "expressive",
|
|
3067
3096
|
"imagery": "Rooms, views, breakfast; your own photos.",
|
|
3068
3097
|
"pages": [
|
|
3069
3098
|
"home",
|
|
@@ -3171,6 +3200,7 @@ export const SPEC_BUSINESS_TYPES = [
|
|
|
3171
3200
|
"animate": "lively",
|
|
3172
3201
|
"between": "slide"
|
|
3173
3202
|
},
|
|
3203
|
+
"ceiling": "lively",
|
|
3174
3204
|
"imagery": "Real classes (with permission).",
|
|
3175
3205
|
"pages": [
|
|
3176
3206
|
"home",
|
|
@@ -25,6 +25,8 @@ export interface MotionBasis {
|
|
|
25
25
|
references: boolean;
|
|
26
26
|
/** The owner asked for little movement: all three were capped. */
|
|
27
27
|
littleMovement: boolean;
|
|
28
|
+
/** SC6: How animated was held at the type's ceiling (a law firm stays calm whatever the style or references). */
|
|
29
|
+
ceiling: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface MotionStart extends DesignMotion {
|
|
@@ -66,9 +68,14 @@ export function motionFor(
|
|
|
66
68
|
const wanted = ref ?? base.animate;
|
|
67
69
|
let out: DesignMotion = { scroll: base.scroll, animate: calmerChoice('animate', wanted, preset.moves.animate), between: base.between };
|
|
68
70
|
const why = [type ? `${type.label}: ${triple(base)}` : `${preset.name}: the style's own`];
|
|
69
|
-
const basis: MotionBasis = { type: type?.id ?? null, calmerStyle: out.animate !== wanted, references: !!(ref && type), littleMovement: false };
|
|
71
|
+
const basis: MotionBasis = { type: type?.id ?? null, calmerStyle: out.animate !== wanted, references: !!(ref && type), littleMovement: false, ceiling: false };
|
|
70
72
|
if (basis.calmerStyle) why.push(`${preset.name} moves less (${label('animate', preset.moves.animate)})`);
|
|
71
73
|
if (basis.references) why.push(`your references: ${label('animate', ref!)}`);
|
|
74
|
+
if (type && calmerChoice('animate', out.animate, type.ceiling) !== out.animate) {
|
|
75
|
+
out = { ...out, animate: type.ceiling };
|
|
76
|
+
basis.ceiling = true;
|
|
77
|
+
why.push(`${type.label} goes no further than ${label('animate', type.ceiling)}`);
|
|
78
|
+
}
|
|
72
79
|
if ((input.avoid ?? []).includes(LITTLE_MOVEMENT_AVOID)) {
|
|
73
80
|
basis.littleMovement = true;
|
|
74
81
|
out = {
|
package/src/media-core/frames.ts
CHANGED
|
@@ -226,6 +226,29 @@ export function videoFrameCount(seconds: number): number {
|
|
|
226
226
|
return Math.min(VIDEO_MAX_FRAMES, Math.max(2, Math.ceil(seconds * VIDEO_FRAMES_PER_SECOND)));
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Pictures per screen of scrolling at which a played film stays smooth as a glide slows to a stop. Measured on Doron's
|
|
231
|
+
* iPhone (2026-11-05): Studio Alma's film at about 11 a screen stepped, at about 55 it did not.
|
|
232
|
+
*/
|
|
233
|
+
export const FILM_SMOOTH_PER_SCREEN = 40;
|
|
234
|
+
/** The fewest and most screens a film the build places holds the page for (SC6b): long enough to feel, short enough to pass. */
|
|
235
|
+
export const FILM_MIN_SCREENS = 1;
|
|
236
|
+
export const FILM_MAX_SCREENS = 4;
|
|
237
|
+
/** What the build prepares a film to: about 60 pictures a screen at the longest hold — denser than smooth, never wasted. */
|
|
238
|
+
export const FILM_PREPARED_PICTURES = FILM_MAX_SCREENS * 60 + 1;
|
|
239
|
+
/** The fewest pictures a placed film may have: smooth over the shortest hold. */
|
|
240
|
+
export const FILM_MIN_PICTURES = FILM_MIN_SCREENS * FILM_SMOOTH_PER_SCREEN + 1;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The screens a film of `count` pictures holds the page for (SC6b, the composer's density rule): the stretches between
|
|
244
|
+
* pictures are what scroll, so `(count − 1) ÷ 40`, rounded DOWN to a tenth (150 pictures over 3.75 screens measure 39.7 a
|
|
245
|
+
* screen, thin), within the hold's limits. Null: too few pictures to play smoothly over the shortest hold.
|
|
246
|
+
*/
|
|
247
|
+
export function filmScreensFor(count: number): number | null {
|
|
248
|
+
if (!Number.isInteger(count) || count < FILM_MIN_PICTURES) return null;
|
|
249
|
+
return Math.min(FILM_MAX_SCREENS, Math.floor(((count - 1) / FILM_SMOOTH_PER_SCREEN) * 10) / 10);
|
|
250
|
+
}
|
|
251
|
+
|
|
229
252
|
/** Which of `sourceCount` pictures make a set of `count`, spread evenly, first and last always in. */
|
|
230
253
|
export function sampledFrameIndexes(sourceCount: number, count: number): number[] {
|
|
231
254
|
if (count >= sourceCount) return Array.from({ length: sourceCount }, (_, i) => i);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SceneRole } from '@vectoriox/iox-ui/composite-core';
|
|
2
|
+
import { FILM_SMOOTH_PER_SCREEN } from '@vectoriox/iox-ui/media-core';
|
|
2
3
|
|
|
3
4
|
import { normalizeTracks, SCENE_REVEAL_DEFAULT, SceneKey, SceneNodeSource, SceneProp, SceneRevealFrom, SceneTrack, valueAt } from './scene';
|
|
4
5
|
|
|
@@ -238,11 +239,8 @@ export function sceneHeightForScreens(screens: number): string {
|
|
|
238
239
|
return `${roundTo((held + 1) * 100, 1)}vh`;
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
/**
|
|
242
|
-
|
|
243
|
-
* iPhone (2026-11-05): Studio Alma's film at about 11 a screen stepped, at about 55 it did not.
|
|
244
|
-
*/
|
|
245
|
-
export const SCENE_FILM_SMOOTH = 40;
|
|
242
|
+
/** Pictures per screen of scrolling at which a played film stays smooth (media-core's, which the composer sizes by). */
|
|
243
|
+
export const SCENE_FILM_SMOOTH = FILM_SMOOTH_PER_SCREEN;
|
|
246
244
|
|
|
247
245
|
export interface SceneFilmDensity {
|
|
248
246
|
/** Pictures per screen of scrolling over most of the play: half its scroll holds at least this many. */
|