@vectoriox/iox-ui 5.71.1 → 5.72.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/fesm2022/vectoriox-iox-ui-actions-core.mjs +15 -0
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +285 -11
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +58 -6
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +26 -4
- package/package.json +1 -1
- package/scene-core/index.d.ts +93 -3
- package/src/actions-core/element-actions.ts +15 -0
- package/src/scene-core/public-api.ts +1 -0
- package/src/scene-core/scene-edit.ts +19 -3
- package/src/scene-core/scene.ts +67 -5
- package/src/scene-core/scroll-driver.ts +52 -3
- package/src/scene-core/split-text.ts +187 -0
package/index.d.ts
CHANGED
|
@@ -2206,6 +2206,12 @@ declare enum VisibilityActionType {
|
|
|
2206
2206
|
Hide = "hide",
|
|
2207
2207
|
Toggle = "toggleVisibility"
|
|
2208
2208
|
}
|
|
2209
|
+
/** The reveals (premium sites SC3): a wipe from a chosen side, a picture settling, text rising line by line. */
|
|
2210
|
+
declare enum RevealActionType {
|
|
2211
|
+
Clip = "clipReveal",
|
|
2212
|
+
Zoom = "zoomOut",
|
|
2213
|
+
Text = "revealText"
|
|
2214
|
+
}
|
|
2209
2215
|
/** Actions that reveal an element (hidden → visible). Their FIRST frame is the pre-state. */
|
|
2210
2216
|
declare const ENTRANCE_ACTION_TYPES: ReadonlySet<string>;
|
|
2211
2217
|
/** Actions that hide an element (visible → hidden). */
|
|
@@ -2220,6 +2226,19 @@ declare function effectiveActionType(type: string, isHidden: boolean): string;
|
|
|
2220
2226
|
* directly it answers with `hide`'s frames, the toggle of an element that starts visible.
|
|
2221
2227
|
*/
|
|
2222
2228
|
declare function buildInteractionKeyframes(type: string, params?: Readonly<Record<string, unknown>> | null): InteractionKeyframe[] | null;
|
|
2229
|
+
interface AnimateActionOptions {
|
|
2230
|
+
/** The frames are `buildInteractionKeyframes`' reversed: a reverse pass never splits text, it just fades back. */
|
|
2231
|
+
reverse?: boolean;
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Start an element action's animation: THE call both runtimes make. Most actions are one `element.animate`; a text
|
|
2235
|
+
* reveal plays its pieces and a clip reveal opens from the side the page's direction makes of Start and End, then takes
|
|
2236
|
+
* its clip away so a resting element never crops its own shadow. Each still comes back as one Animation.
|
|
2237
|
+
*/
|
|
2238
|
+
declare function animateAction(el: HTMLElement, action: {
|
|
2239
|
+
type: string;
|
|
2240
|
+
params?: Readonly<Record<string, unknown>> | null;
|
|
2241
|
+
}, frames: ReadonlyArray<InteractionKeyframe>, timing: KeyframeAnimationOptions, options?: AnimateActionOptions): Animation;
|
|
2223
2242
|
/** `translateX` → `translate-x`, `opacity` → `opacity`: the CSS property name of a keyframe key. */
|
|
2224
2243
|
declare function keyframePropertyName(key: string): string;
|
|
2225
2244
|
/** A keyframe as `[css-property, value]` pairs — what a runtime writes inline. */
|
|
@@ -2249,8 +2268,11 @@ declare const ENTRANCE_PRESTATE_ATTRIBUTE = "data-iox-enter";
|
|
|
2249
2268
|
*/
|
|
2250
2269
|
declare const ENTRANCE_FALLBACK_CSS: string;
|
|
2251
2270
|
/** The same, for a visitor with no script: every pre-stated element shown at once (served inside `<noscript>`). */
|
|
2252
|
-
declare const ENTRANCE_NOSCRIPT_CSS = "[data-iox-enter]{opacity:1!important;transform:none!important;visibility:visible!important;pointer-events:auto!important;animation:none!important}";
|
|
2253
|
-
/**
|
|
2271
|
+
declare const ENTRANCE_NOSCRIPT_CSS = "[data-iox-enter]{opacity:1!important;transform:none!important;clip-path:none!important;visibility:visible!important;pointer-events:auto!important;animation:none!important}";
|
|
2272
|
+
/**
|
|
2273
|
+
* The frame an animation comes to rest on (what reduced motion applies at once). A clip that has fully opened rests as
|
|
2274
|
+
* no clip at all: `inset(0)` would still crop the element's shadow and stack it, for the rest of the visit.
|
|
2275
|
+
*/
|
|
2254
2276
|
declare function finalKeyframe(frames: ReadonlyArray<InteractionKeyframe> | null | undefined): InteractionKeyframe | null;
|
|
2255
2277
|
|
|
2256
2278
|
/**
|
|
@@ -3196,5 +3218,5 @@ declare class GalleryLoop {
|
|
|
3196
3218
|
private emit;
|
|
3197
3219
|
}
|
|
3198
3220
|
|
|
3199
|
-
export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DeclarationContext, ENTRANCE_ACTION_TYPES, ENTRANCE_FALLBACK_CSS, ENTRANCE_NOSCRIPT_CSS, ENTRANCE_PRESTATE_ATTRIBUTE, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, SCROLL_EASINGS, SELF_PLAYING_TRIGGERS, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, applyBindingPipes, applyFeel, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, entranceStartsHidden, feelFor, finalKeyframe, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isLastEmittedStateKey, isOverriddenAt, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, planResponsiveRules, planScroll, planSwiperOptions, playDualLayerTransition, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, sliderControlElements, sliderInputsFromProps, sliderInputsOf, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, viewportTransitionContext, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
3200
|
-
export type { A11yNodeSource, BoundValueSpec, BreakpointDef, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, InteractionKeyframe, IoxContextSlot, IoxPipe, MatchMediaHost, NodeA11yContext, OverrideBreakpoint, PageScrollStart, PageStyleFieldState, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionContext, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, PlannedPageTransition, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteAnimationLike, ScrollPlanContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SliderBootOptions, SliderControlElements, SliderInputs, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, SwiperPlan, SwiperPlanContext, ToggleableAction, TransitionFeel, TransitionPhase, TransitionPreviewLayers, TransitionTimelineInput, TransitionViewport, ValueSource, VirtualTraitGroup };
|
|
3221
|
+
export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DeclarationContext, ENTRANCE_ACTION_TYPES, ENTRANCE_FALLBACK_CSS, ENTRANCE_NOSCRIPT_CSS, ENTRANCE_PRESTATE_ATTRIBUTE, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, RevealActionType, SCROLL_EASINGS, SELF_PLAYING_TRIGGERS, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, animateAction, applyBindingPipes, applyFeel, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, entranceStartsHidden, feelFor, finalKeyframe, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isLastEmittedStateKey, isOverriddenAt, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, planResponsiveRules, planScroll, planSwiperOptions, playDualLayerTransition, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, sliderControlElements, sliderInputsFromProps, sliderInputsOf, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, viewportTransitionContext, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
3222
|
+
export type { A11yNodeSource, AnimateActionOptions, BoundValueSpec, BreakpointDef, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, InteractionKeyframe, IoxContextSlot, IoxPipe, MatchMediaHost, NodeA11yContext, OverrideBreakpoint, PageScrollStart, PageStyleFieldState, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionContext, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, PlannedPageTransition, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteAnimationLike, ScrollPlanContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SliderBootOptions, SliderControlElements, SliderInputs, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, SwiperPlan, SwiperPlanContext, ToggleableAction, TransitionFeel, TransitionPhase, TransitionPreviewLayers, TransitionTimelineInput, TransitionViewport, ValueSource, VirtualTraitGroup };
|
package/package.json
CHANGED
package/scene-core/index.d.ts
CHANGED
|
@@ -19,19 +19,33 @@ declare enum SceneProp {
|
|
|
19
19
|
Rotate = "rotate",
|
|
20
20
|
/** 0–1. */
|
|
21
21
|
Opacity = "opacity",
|
|
22
|
-
/**
|
|
22
|
+
/** How much of the layer shows, in % (100 = whole, 0 = gone). Its track's `from` says where it opens from. */
|
|
23
23
|
Clip = "clip",
|
|
24
|
+
/** How many of the layer's words are lit, in % (100 = all): the word-by-word light-up. Split in the browser only. */
|
|
25
|
+
Words = "words",
|
|
24
26
|
/** The layer's background colour. */
|
|
25
27
|
Ground = "ground",
|
|
26
28
|
/** The layer's text colour. */
|
|
27
29
|
Ink = "ink"
|
|
28
30
|
}
|
|
31
|
+
/** Where a reveal opens from. Start and End mirror in RTL. */
|
|
32
|
+
declare enum SceneRevealFrom {
|
|
33
|
+
Centre = "centre",
|
|
34
|
+
Bottom = "bottom",
|
|
35
|
+
Top = "top",
|
|
36
|
+
Start = "start",
|
|
37
|
+
End = "end"
|
|
38
|
+
}
|
|
39
|
+
/** What a reveal opens from when nobody has said: the page's own direction of travel, upward as it scrolls. */
|
|
40
|
+
declare const SCENE_REVEAL_DEFAULT = SceneRevealFrom.Bottom;
|
|
29
41
|
/** One key: at progress `[0]` (0–1) the property holds `[1]`: a number, or a colour for Ground and Ink. */
|
|
30
42
|
type SceneKey = readonly [number, number | string];
|
|
31
43
|
interface SceneTrack {
|
|
32
44
|
prop: SceneProp;
|
|
33
45
|
/** Ascending by progress. Between two keys the value eases (smoothstep); outside them it holds the nearest. */
|
|
34
46
|
keys: ReadonlyArray<SceneKey>;
|
|
47
|
+
/** A Clip track's direction. */
|
|
48
|
+
from?: SceneRevealFrom;
|
|
35
49
|
}
|
|
36
50
|
/** What a node carries (both a builder node and a saved one). */
|
|
37
51
|
interface SceneNodeSource {
|
|
@@ -63,6 +77,11 @@ declare const SCENE_PHONE_MAX_WIDTH = 767;
|
|
|
63
77
|
* keys sorted by progress, a track left with no key dropped. Never throws on what a page document holds.
|
|
64
78
|
*/
|
|
65
79
|
declare function normalizeTracks(tracks: unknown): SceneTrack[];
|
|
80
|
+
/**
|
|
81
|
+
* The clip-path of a layer `showing` % revealed, opening from `from`. The one shape behind a Scene's Clip track and the
|
|
82
|
+
* clipReveal action. `clip-path` has no logical sides, so Start and End take the direction from `inlineSign`.
|
|
83
|
+
*/
|
|
84
|
+
declare function revealClipPath(showing: number, from?: SceneRevealFrom, inlineSign?: 1 | -1): string;
|
|
66
85
|
/** A numeric track's value at progress `p`. */
|
|
67
86
|
declare function valueAt(keys: ReadonlyArray<SceneKey>, p: number): number;
|
|
68
87
|
/**
|
|
@@ -79,6 +98,12 @@ interface SceneFrameOptions {
|
|
|
79
98
|
* canvas zoom, on the server and with no measuring: the stage is the size container (`iox-design.css`).
|
|
80
99
|
*/
|
|
81
100
|
declare function sceneFrame(tracks: ReadonlyArray<SceneTrack>, p: number, options?: SceneFrameOptions): SceneFrame;
|
|
101
|
+
/** How far an unlit word still shows: readable as a shape, plainly not yet said. */
|
|
102
|
+
declare const SCENE_WORD_DIM = 0.16;
|
|
103
|
+
/** How many of a layer's words are lit at `p`, in %; null when it has no such track. */
|
|
104
|
+
declare function sceneWordsLit(tracks: ReadonlyArray<SceneTrack>, p: number): number | null;
|
|
105
|
+
/** Word `index` of `count`, with `lit` % lit: words light one after the other, each easing in as the last finishes. */
|
|
106
|
+
declare function sceneWordOpacity(index: number, count: number, lit: number): number;
|
|
82
107
|
/** A frame as kebab-case style declarations, for a string renderer. */
|
|
83
108
|
declare function sceneFrameDeclarations(frame: SceneFrame): Record<string, string>;
|
|
84
109
|
/** The attributes a node's own element carries — the same decision in all three render paths. The runtime reads them,
|
|
@@ -129,6 +154,13 @@ declare function sceneDisplayValue(tracks: ReadonlyArray<SceneTrack>, prop: Scen
|
|
|
129
154
|
*/
|
|
130
155
|
declare function sceneColourKeyAt(tracks: ReadonlyArray<SceneTrack>, prop: SceneProp, p: number): string;
|
|
131
156
|
declare function sceneHasKeyAt(tracks: ReadonlyArray<SceneTrack>, prop: SceneProp, p: number): boolean;
|
|
157
|
+
/** Where a layer's reveal opens from: its track's direction, else where a new one would open from. */
|
|
158
|
+
declare function sceneRevealFromOf(tracks: ReadonlyArray<SceneTrack>): SceneRevealFrom;
|
|
159
|
+
/**
|
|
160
|
+
* Open a layer's reveal from `from`. With no reveal yet it starts one with a key at `p` holding the layer whole, so the
|
|
161
|
+
* direction is kept and nothing on screen changes until the author sets a second key.
|
|
162
|
+
*/
|
|
163
|
+
declare function withSceneRevealFrom(tracks: ReadonlyArray<SceneTrack>, from: SceneRevealFrom, p: number): SceneTrack[];
|
|
132
164
|
/**
|
|
133
165
|
* Set a key at `p`, in the author's units. A number is clamped to its bounds; a colour that is no colour changes nothing,
|
|
134
166
|
* because `normalizeTracks` would drop it anyway and the author's other keys must survive.
|
|
@@ -275,22 +307,80 @@ declare class ScrollDriver {
|
|
|
275
307
|
offsetFor(root: HTMLElement, p: number): number | null;
|
|
276
308
|
/** Land on the next frame with no glide: a scrubber sets a position, and the author is waiting to see that moment. */
|
|
277
309
|
settle(root: HTMLElement): void;
|
|
310
|
+
/**
|
|
311
|
+
* Hold every stage against the scroll, now. For a host that moves the scroll itself inside a frame (the canvas's
|
|
312
|
+
* Lenis): waiting for the scroll event paints that frame with the stage carried off its pin, up to a whole frame's
|
|
313
|
+
* scroll, which reads as a shake. Damping is untouched: only the pin has to be exact.
|
|
314
|
+
*/
|
|
315
|
+
pinNow(): void;
|
|
278
316
|
/** Something moved that no scroll or resize announces (the canvas's zoom). */
|
|
279
317
|
wake(): void;
|
|
280
318
|
destroy(): void;
|
|
281
319
|
/** Read a root's stage and layers from its DOM: its own Scene's only, never a nested one's. */
|
|
282
320
|
private readScene;
|
|
283
321
|
private letGo;
|
|
322
|
+
private restoreWords;
|
|
284
323
|
private readLayer;
|
|
285
324
|
private remeasureAll;
|
|
286
325
|
private measure;
|
|
287
326
|
private readonly tick;
|
|
288
327
|
private read;
|
|
289
328
|
private write;
|
|
329
|
+
/** One opacity per word, and a write only for the few the scroll actually changed. */
|
|
330
|
+
private writeWords;
|
|
290
331
|
}
|
|
291
332
|
/** A page's Scenes, driven by one driver; null when it holds none, or when the visitor asked for less motion (the
|
|
292
333
|
* page's own CSS then lets each Scene flow). The live site's door. */
|
|
293
334
|
declare function attachScenes(container: HTMLElement, options?: ScrollDriverOptions): ScrollDriver | null;
|
|
294
335
|
|
|
295
|
-
|
|
296
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Text into pieces, in the browser only (premium sites SC3, Motion v2 §3c). A saved node keeps its plain text and the
|
|
338
|
+
* server sends it whole, so every word is in the raw response, editing never meets a piece, and there is no markup for
|
|
339
|
+
* hydration to disagree with. Each original text node is kept and put back as it was, never rebuilt from a string: a
|
|
340
|
+
* framework may hold that node, and a layer may hold live components.
|
|
341
|
+
*/
|
|
342
|
+
declare enum TextRevealBy {
|
|
343
|
+
Line = "line",
|
|
344
|
+
Word = "word"
|
|
345
|
+
}
|
|
346
|
+
/** Past these a text is plainly no heading: it fades whole, and lights up whole. */
|
|
347
|
+
declare const REVEAL_PIECE_CAP = 120;
|
|
348
|
+
declare const LIGHT_UP_PIECE_CAP = 300;
|
|
349
|
+
interface TextPiece {
|
|
350
|
+
text: string;
|
|
351
|
+
/** False for the space between two words, which stays bare text so lines wrap exactly as they did. */
|
|
352
|
+
word: boolean;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* A string as words and the space between them. With `keepLtrRuns` a run of words holding no RTL letter stays ONE word:
|
|
356
|
+
* an inline-block is neutral to the bidi algorithm, so "Studio Alma" inside a Hebrew heading, split, would read reversed.
|
|
357
|
+
*/
|
|
358
|
+
declare function textPieces(text: string, keepLtrRuns?: boolean): TextPiece[];
|
|
359
|
+
interface SplitTextOptions {
|
|
360
|
+
cap: number;
|
|
361
|
+
/** Each piece in a clipping mask, to rise out of (and so an inline-block). Without it a piece is a plain inline span. */
|
|
362
|
+
mask?: boolean;
|
|
363
|
+
/** The element reads right-to-left. Only a mask needs to know. */
|
|
364
|
+
rtl?: boolean;
|
|
365
|
+
}
|
|
366
|
+
interface SplitText {
|
|
367
|
+
/** In reading order. With a mask, the moving part inside it. */
|
|
368
|
+
pieces: HTMLElement[];
|
|
369
|
+
/** Put every original text node back. Safe after the text was replaced: what is gone stays gone. */
|
|
370
|
+
restore(): void;
|
|
371
|
+
}
|
|
372
|
+
/** Split `root`'s text, or null when it holds none or more than `cap` pieces (nothing is changed then). */
|
|
373
|
+
declare function splitText(root: HTMLElement, options: SplitTextOptions): SplitText | null;
|
|
374
|
+
/** The line each piece stands on: 0, 0, 1, 1, 1, 2… Read once, as a reveal starts. */
|
|
375
|
+
declare function lineIndexes(tops: ReadonlyArray<number>, heights: ReadonlyArray<number>): number[];
|
|
376
|
+
declare function revealStagger(groups: number, by: TextRevealBy): number;
|
|
377
|
+
/**
|
|
378
|
+
* Play a text reveal: each piece rises out of its mask, line by line or word by word. Returns ONE animation for the
|
|
379
|
+
* whole, so a runtime treats it as any other: it finishes when the last piece lands, and on finish or cancel the text
|
|
380
|
+
* is put back whole, which leaves nothing for a screen reader to stumble on and nothing to re-split on a resize. Null
|
|
381
|
+
* when there is nothing to split or it is being edited: the caller plays its plain fade.
|
|
382
|
+
*/
|
|
383
|
+
declare function playTextReveal(el: HTMLElement, by: TextRevealBy, timing: KeyframeAnimationOptions): Animation | null;
|
|
384
|
+
|
|
385
|
+
export { LIGHT_UP_PIECE_CAP, REVEAL_PIECE_CAP, SCENE_BUDGET, SCENE_DAMPING, SCENE_FLOW_RULES, SCENE_LENGTH, SCENE_MAX_SCREENS, SCENE_MIN_SCREENS, SCENE_PHONE_LENGTH, SCENE_PHONE_MAX_WIDTH, SCENE_REDUCED_MOTION_CSS, SCENE_REVEAL_DEFAULT, SCENE_STAGE_HEIGHT, SCENE_TRACK_DEFS, SCENE_WORDS, SCENE_WORD_DIM, SceneAttribute, SceneProp, SceneRevealFrom, ScrollDriver, TextRevealBy, applySceneFirstFrame, attachScenes, buildSceneRecipe, colourAt, dampToward, lineIndexes, movedSceneKey, normalizeTracks, playTextReveal, revealClipPath, revealStagger, sceneBudget, sceneColourKeyAt, sceneDisplayValue, sceneFirstFrame, sceneFrame, sceneFrameDeclarations, sceneHasKeyAt, sceneHeightForScreens, sceneKeyPosition, sceneLayersOf, sceneNodeAttributes, sceneRevealFromOf, sceneRootOnPath, sceneScreensOf, sceneScrollState, sceneTrackDef, sceneTrackSpan, sceneWordOpacity, sceneWordsLit, scenesIn, splitText, syncSceneAttributes, textPieces, valueAt, withSceneKey, withSceneRevealFrom, withoutSceneKey };
|
|
386
|
+
export type { SceneBudget, SceneFrame, SceneFrameOptions, SceneFrameState, SceneKey, SceneLayerNode, SceneNodeSource, SceneTrack, SceneTrackDef, SceneTreeNode, SceneWords, ScrollDriverOptions, SplitText, SplitTextOptions, TextPiece };
|
|
@@ -31,6 +31,21 @@ export const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.fr
|
|
|
31
31
|
{ type: 'scaleOut', labelKey: 'builder.actions.scaleOut', kind: 'animated', group: 'motion', builderBehaviour: 'run' },
|
|
32
32
|
{ type: 'rotate', labelKey: 'builder.actions.rotate', kind: 'animated', group: 'motion', builderBehaviour: 'run',
|
|
33
33
|
params: [{ name: 'degrees', type: 'number', label: 'Degrees' }] },
|
|
34
|
+
// SC3: the reveals. Each opens from where the author says and rests as the element itself.
|
|
35
|
+
{ type: 'clipReveal', labelKey: 'builder.actions.clipReveal', kind: 'animated', group: 'motion', builderBehaviour: 'run',
|
|
36
|
+
params: [{ name: 'from', type: 'string', label: 'builder.actions.params.revealFrom', options: [
|
|
37
|
+
{ value: 'bottom', labelKey: 'builder.scene.from.bottom' },
|
|
38
|
+
{ value: 'top', labelKey: 'builder.scene.from.top' },
|
|
39
|
+
{ value: 'start', labelKey: 'builder.scene.from.start' },
|
|
40
|
+
{ value: 'end', labelKey: 'builder.scene.from.end' },
|
|
41
|
+
{ value: 'centre', labelKey: 'builder.scene.from.centre' },
|
|
42
|
+
] }] },
|
|
43
|
+
{ type: 'zoomOut', labelKey: 'builder.actions.zoomOut', kind: 'animated', group: 'motion', builderBehaviour: 'run' },
|
|
44
|
+
{ type: 'revealText', labelKey: 'builder.actions.revealText', kind: 'animated', group: 'motion', builderBehaviour: 'run',
|
|
45
|
+
params: [{ name: 'by', type: 'string', label: 'builder.actions.params.revealBy', options: [
|
|
46
|
+
{ value: 'line', labelKey: 'builder.actions.params.byLine' },
|
|
47
|
+
{ value: 'word', labelKey: 'builder.actions.params.byWord' },
|
|
48
|
+
] }] },
|
|
34
49
|
|
|
35
50
|
// ── Visibility (shipped) ─────────────────────────────────────────────────
|
|
36
51
|
{ type: 'show', labelKey: 'builder.actions.show', kind: 'instant', group: 'visibility', builderBehaviour: 'run' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SceneRole } from '@vectoriox/iox-ui/composite-core';
|
|
2
2
|
|
|
3
|
-
import { normalizeTracks, SceneKey, SceneNodeSource, SceneProp, SceneTrack, valueAt } from './scene';
|
|
3
|
+
import { normalizeTracks, SCENE_REVEAL_DEFAULT, SceneKey, SceneNodeSource, SceneProp, SceneRevealFrom, SceneTrack, valueAt } from './scene';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Authoring a Scene (premium-sites plan §5b, SC2): the decisions the builder's timeline and its panel share, so neither
|
|
@@ -43,7 +43,8 @@ export const SCENE_TRACK_DEFS: ReadonlyArray<SceneTrackDef> = [
|
|
|
43
43
|
{ prop: SceneProp.Scale, colour: false, rest: 1, min: 0, max: 2000, step: 1, factor: 100, unit: PERCENT },
|
|
44
44
|
{ prop: SceneProp.Rotate, colour: false, rest: 0, min: -720, max: 720, step: 1, factor: 1, unit: DEGREES },
|
|
45
45
|
{ prop: SceneProp.Opacity, colour: false, rest: 1, min: 0, max: 100, step: 1, factor: 100, unit: PERCENT },
|
|
46
|
-
{ prop: SceneProp.Clip, colour: false, rest:
|
|
46
|
+
{ prop: SceneProp.Clip, colour: false, rest: 100, min: 0, max: 100, step: 1, factor: 1, unit: PERCENT },
|
|
47
|
+
{ prop: SceneProp.Words, colour: false, rest: 100, min: 0, max: 100, step: 1, factor: 1, unit: PERCENT },
|
|
47
48
|
{ prop: SceneProp.Ground, colour: true, rest: 0, min: 0, max: 0, step: 0, factor: 1, unit: '' },
|
|
48
49
|
{ prop: SceneProp.Ink, colour: true, rest: 0, min: 0, max: 0, step: 0, factor: 1, unit: '' },
|
|
49
50
|
];
|
|
@@ -91,10 +92,25 @@ function replaceTrack(tracks: ReadonlyArray<SceneTrack>, prop: SceneProp, keys:
|
|
|
91
92
|
if (!keys.length) return rest;
|
|
92
93
|
const at = tracks.findIndex(t => t.prop === prop);
|
|
93
94
|
const next = [...rest];
|
|
94
|
-
|
|
95
|
+
const from = prop === SceneProp.Clip ? { from: tracks[at]?.from ?? SCENE_REVEAL_DEFAULT } : {};
|
|
96
|
+
next.splice(at < 0 ? rest.length : at, 0, { prop, ...from, keys });
|
|
95
97
|
return next;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/** Where a layer's reveal opens from: its track's direction, else where a new one would open from. */
|
|
101
|
+
export function sceneRevealFromOf(tracks: ReadonlyArray<SceneTrack>): SceneRevealFrom {
|
|
102
|
+
return trackOf(tracks, SceneProp.Clip)?.from ?? SCENE_REVEAL_DEFAULT;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Open a layer's reveal from `from`. With no reveal yet it starts one with a key at `p` holding the layer whole, so the
|
|
107
|
+
* direction is kept and nothing on screen changes until the author sets a second key.
|
|
108
|
+
*/
|
|
109
|
+
export function withSceneRevealFrom(tracks: ReadonlyArray<SceneTrack>, from: SceneRevealFrom, p: number): SceneTrack[] {
|
|
110
|
+
if (!trackOf(tracks, SceneProp.Clip)) return [...tracks, { prop: SceneProp.Clip, from, keys: [[sceneKeyPosition(p), sceneTrackDef(SceneProp.Clip).rest]] }];
|
|
111
|
+
return tracks.map(track => (track.prop === SceneProp.Clip ? { ...track, from } : track));
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
/**
|
|
99
115
|
* Set a key at `p`, in the author's units. A number is clamped to its bounds; a colour that is no colour changes nothing,
|
|
100
116
|
* because `normalizeTracks` would drop it anyway and the author's other keys must survive.
|
package/src/scene-core/scene.ts
CHANGED
|
@@ -20,14 +20,28 @@ export enum SceneProp {
|
|
|
20
20
|
Rotate = 'rotate',
|
|
21
21
|
/** 0–1. */
|
|
22
22
|
Opacity = 'opacity',
|
|
23
|
-
/**
|
|
23
|
+
/** How much of the layer shows, in % (100 = whole, 0 = gone). Its track's `from` says where it opens from. */
|
|
24
24
|
Clip = 'clip',
|
|
25
|
+
/** How many of the layer's words are lit, in % (100 = all): the word-by-word light-up. Split in the browser only. */
|
|
26
|
+
Words = 'words',
|
|
25
27
|
/** The layer's background colour. */
|
|
26
28
|
Ground = 'ground',
|
|
27
29
|
/** The layer's text colour. */
|
|
28
30
|
Ink = 'ink',
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** Where a reveal opens from. Start and End mirror in RTL. */
|
|
34
|
+
export enum SceneRevealFrom {
|
|
35
|
+
Centre = 'centre',
|
|
36
|
+
Bottom = 'bottom',
|
|
37
|
+
Top = 'top',
|
|
38
|
+
Start = 'start',
|
|
39
|
+
End = 'end',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What a reveal opens from when nobody has said: the page's own direction of travel, upward as it scrolls. */
|
|
43
|
+
export const SCENE_REVEAL_DEFAULT = SceneRevealFrom.Bottom;
|
|
44
|
+
|
|
31
45
|
/** One key: at progress `[0]` (0–1) the property holds `[1]`: a number, or a colour for Ground and Ink. */
|
|
32
46
|
export type SceneKey = readonly [number, number | string];
|
|
33
47
|
|
|
@@ -35,6 +49,8 @@ export interface SceneTrack {
|
|
|
35
49
|
prop: SceneProp;
|
|
36
50
|
/** Ascending by progress. Between two keys the value eases (smoothstep); outside them it holds the nearest. */
|
|
37
51
|
keys: ReadonlyArray<SceneKey>;
|
|
52
|
+
/** A Clip track's direction. */
|
|
53
|
+
from?: SceneRevealFrom;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
/** What a node carries (both a builder node and a saved one). */
|
|
@@ -76,6 +92,9 @@ const smoothstep = (t: number): number => t * t * (3 - 2 * t);
|
|
|
76
92
|
const COLOUR = /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\([\d\s.,%/]+\)|var\(--iox-[\w-]+\))$/i;
|
|
77
93
|
const COLOUR_PROPS: ReadonlySet<SceneProp> = new Set([SceneProp.Ground, SceneProp.Ink]);
|
|
78
94
|
const ALL_PROPS: ReadonlySet<string> = new Set(Object.values(SceneProp));
|
|
95
|
+
const REVEAL_FROMS: ReadonlySet<string> = new Set(Object.values(SceneRevealFrom));
|
|
96
|
+
const PERCENT = 100;
|
|
97
|
+
const clampPercent = (v: number): number => Math.min(PERCENT, Math.max(0, v));
|
|
79
98
|
|
|
80
99
|
function validKey(key: unknown, colour: boolean): key is SceneKey {
|
|
81
100
|
if (!Array.isArray(key) || key.length !== 2) return false;
|
|
@@ -98,11 +117,36 @@ export function normalizeTracks(tracks: unknown): SceneTrack[] {
|
|
|
98
117
|
.filter(key => validKey(key, COLOUR_PROPS.has(prop)))
|
|
99
118
|
.slice()
|
|
100
119
|
.sort((a, b) => a[0] - b[0]);
|
|
101
|
-
if (keys.length)
|
|
120
|
+
if (!keys.length) continue;
|
|
121
|
+
out.push(prop === SceneProp.Clip ? clipTrack(keys, (track as SceneTrack).from) : { prop, keys });
|
|
102
122
|
}
|
|
103
123
|
return out;
|
|
104
124
|
}
|
|
105
125
|
|
|
126
|
+
/**
|
|
127
|
+
* A Clip track with a direction. One with none was saved when a reveal could only close in from every side, its value
|
|
128
|
+
* the inset per side (50 = gone): it reads as the same movement, from the centre, in % showing.
|
|
129
|
+
*/
|
|
130
|
+
function clipTrack(keys: ReadonlyArray<SceneKey>, from: unknown): SceneTrack {
|
|
131
|
+
if (typeof from === 'string' && REVEAL_FROMS.has(from)) return { prop: SceneProp.Clip, from: from as SceneRevealFrom, keys };
|
|
132
|
+
return { prop: SceneProp.Clip, from: SceneRevealFrom.Centre, keys: keys.map(([p, inset]): SceneKey => [p, clampPercent(PERCENT - Number(inset) * 2)]) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The clip-path of a layer `showing` % revealed, opening from `from`. The one shape behind a Scene's Clip track and the
|
|
137
|
+
* clipReveal action. `clip-path` has no logical sides, so Start and End take the direction from `inlineSign`.
|
|
138
|
+
*/
|
|
139
|
+
export function revealClipPath(showing: number, from: SceneRevealFrom = SCENE_REVEAL_DEFAULT, inlineSign: 1 | -1 = 1): string {
|
|
140
|
+
const hidden = round(PERCENT - clampPercent(showing));
|
|
141
|
+
switch (from) {
|
|
142
|
+
case SceneRevealFrom.Centre: return `inset(${round(hidden / 2)}%)`;
|
|
143
|
+
case SceneRevealFrom.Top: return `inset(0 0 ${hidden}% 0)`;
|
|
144
|
+
case SceneRevealFrom.Start: return inlineSign === 1 ? `inset(0 ${hidden}% 0 0)` : `inset(0 0 0 ${hidden}%)`;
|
|
145
|
+
case SceneRevealFrom.End: return inlineSign === 1 ? `inset(0 0 0 ${hidden}%)` : `inset(0 ${hidden}% 0 0)`;
|
|
146
|
+
default: return `inset(${hidden}% 0 0 0)`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
106
150
|
/** The two keys around `p` and how far between them it eases. */
|
|
107
151
|
function span(keys: ReadonlyArray<SceneKey>, p: number): { a: SceneKey; b: SceneKey; f: number } {
|
|
108
152
|
const at = clamp01(p);
|
|
@@ -154,14 +198,14 @@ const TRANSFORM_REST: Readonly<Partial<Record<SceneProp, number>>> = { [ScenePro
|
|
|
154
198
|
export function sceneFrame(tracks: ReadonlyArray<SceneTrack>, p: number, options: SceneFrameOptions = {}): SceneFrame {
|
|
155
199
|
const frame: SceneFrame = {};
|
|
156
200
|
const moves: Partial<Record<SceneProp, number>> = {};
|
|
157
|
-
for (const { prop, keys } of tracks) {
|
|
201
|
+
for (const { prop, keys, from } of tracks) {
|
|
158
202
|
if (prop in TRANSFORM_REST) moves[prop] = valueAt(keys, p);
|
|
159
203
|
else if (prop === SceneProp.Opacity) {
|
|
160
204
|
const opacity = clamp01(valueAt(keys, p));
|
|
161
205
|
frame.opacity = String(opacity);
|
|
162
206
|
frame.pointerEvents = opacity > 0 ? '' : 'none';
|
|
163
207
|
}
|
|
164
|
-
else if (prop === SceneProp.Clip) frame.clipPath =
|
|
208
|
+
else if (prop === SceneProp.Clip) frame.clipPath = revealClipPath(valueAt(keys, p), from ?? SceneRevealFrom.Centre, options.inlineSign ?? 1);
|
|
165
209
|
else if (prop === SceneProp.Ground) frame.backgroundColor = colourAt(keys, p);
|
|
166
210
|
else if (prop === SceneProp.Ink) frame.color = colourAt(keys, p);
|
|
167
211
|
}
|
|
@@ -173,6 +217,20 @@ export function sceneFrame(tracks: ReadonlyArray<SceneTrack>, p: number, options
|
|
|
173
217
|
return frame;
|
|
174
218
|
}
|
|
175
219
|
|
|
220
|
+
/** How far an unlit word still shows: readable as a shape, plainly not yet said. */
|
|
221
|
+
export const SCENE_WORD_DIM = 0.16;
|
|
222
|
+
|
|
223
|
+
/** How many of a layer's words are lit at `p`, in %; null when it has no such track. */
|
|
224
|
+
export function sceneWordsLit(tracks: ReadonlyArray<SceneTrack>, p: number): number | null {
|
|
225
|
+
const track = tracks.find(t => t.prop === SceneProp.Words);
|
|
226
|
+
return track ? clampPercent(valueAt(track.keys, p)) : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Word `index` of `count`, with `lit` % lit: words light one after the other, each easing in as the last finishes. */
|
|
230
|
+
export function sceneWordOpacity(index: number, count: number, lit: number): number {
|
|
231
|
+
return round(SCENE_WORD_DIM + (1 - SCENE_WORD_DIM) * clamp01((lit / PERCENT) * count - index));
|
|
232
|
+
}
|
|
233
|
+
|
|
176
234
|
const CSS_NAME: Readonly<Record<keyof SceneFrame, string>> = {
|
|
177
235
|
transform: 'transform',
|
|
178
236
|
opacity: 'opacity',
|
|
@@ -204,7 +262,11 @@ export function sceneNodeAttributes(node: SceneNodeSource | null | undefined): R
|
|
|
204
262
|
/** The frame a layer ships in from the server: where it stands before anyone has scrolled. Without it the page would
|
|
205
263
|
* paint every layer at once, and then jump when the script arrives. */
|
|
206
264
|
export function sceneFirstFrame(node: SceneNodeSource | null | undefined): SceneFrame {
|
|
207
|
-
|
|
265
|
+
const tracks = normalizeTracks(node?.sceneTracks);
|
|
266
|
+
const frame = sceneFrame(tracks, 0);
|
|
267
|
+
// The words are split in the browser, so until then the whole layer stands for them, or it paints lit and then dims.
|
|
268
|
+
const lit = sceneWordsLit(tracks, 0);
|
|
269
|
+
return lit !== null && lit < PERCENT && frame.opacity === undefined ? { ...frame, opacity: String(SCENE_WORD_DIM) } : frame;
|
|
208
270
|
}
|
|
209
271
|
|
|
210
272
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SceneRole } from '@vectoriox/iox-ui/composite-core';
|
|
2
2
|
|
|
3
|
-
import { normalizeTracks, SCENE_PHONE_MAX_WIDTH, SceneAttribute, sceneFrame, SceneFrame, SceneTrack } from './scene';
|
|
3
|
+
import { normalizeTracks, SCENE_PHONE_MAX_WIDTH, SceneAttribute, sceneFrame, SceneFrame, sceneWordOpacity, sceneWordsLit, SceneTrack } from './scene';
|
|
4
|
+
import { LIGHT_UP_PIECE_CAP, SplitText, splitText } from './split-text';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The ONE scroll loop (premium-sites plan §5b, SC0). Every Scene on a page reads one progress value from it, on the
|
|
@@ -65,6 +66,10 @@ interface Layer {
|
|
|
65
66
|
last: SceneFrame;
|
|
66
67
|
/** Write every property on the next frame, changed or not: a measure may have moved what a frame resolves to. */
|
|
67
68
|
stale: boolean;
|
|
69
|
+
/** Its words, split while a Words track plays them. Null until the first frame that needs them. */
|
|
70
|
+
words: SplitText | null;
|
|
71
|
+
/** Too many words to split: it holds as it is, and is not counted again on every frame. */
|
|
72
|
+
wordsRefused: boolean;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
interface Scene {
|
|
@@ -127,7 +132,10 @@ export class ScrollDriver {
|
|
|
127
132
|
scene.mutations?.disconnect();
|
|
128
133
|
this.resize?.unobserve(root);
|
|
129
134
|
if (scene.stage) scene.stage.style.removeProperty('translate');
|
|
130
|
-
for (const layer of scene.layers)
|
|
135
|
+
for (const layer of scene.layers) {
|
|
136
|
+
layer.el.style.removeProperty('will-change');
|
|
137
|
+
this.restoreWords(layer);
|
|
138
|
+
}
|
|
131
139
|
this.scenes.delete(root);
|
|
132
140
|
}
|
|
133
141
|
|
|
@@ -161,6 +169,18 @@ export class ScrollDriver {
|
|
|
161
169
|
this.wake();
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Hold every stage against the scroll, now. For a host that moves the scroll itself inside a frame (the canvas's
|
|
174
|
+
* Lenis): waiting for the scroll event paints that frame with the stage carried off its pin, up to a whole frame's
|
|
175
|
+
* scroll, which reads as a shake. Damping is untouched: only the pin has to be exact.
|
|
176
|
+
*/
|
|
177
|
+
pinNow(): void {
|
|
178
|
+
if (!this.options.pin) return;
|
|
179
|
+
for (const scene of this.scenes.values()) {
|
|
180
|
+
if (scene.stage) scene.stage.style.setProperty('translate', `0 ${this.read(scene).local}px`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
164
184
|
/** Something moved that no scroll or resize announces (the canvas's zoom). */
|
|
165
185
|
wake(): void {
|
|
166
186
|
if (this.frame) return;
|
|
@@ -195,12 +215,18 @@ export class ScrollDriver {
|
|
|
195
215
|
private letGo(layer: Layer): void {
|
|
196
216
|
layer.el.style.removeProperty('will-change');
|
|
197
217
|
for (const key of Object.keys(layer.last) as (keyof SceneFrame)[]) layer.el.style[key] = '';
|
|
218
|
+
this.restoreWords(layer);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private restoreWords(layer: Layer): void {
|
|
222
|
+
layer.words?.restore();
|
|
223
|
+
layer.words = null;
|
|
198
224
|
}
|
|
199
225
|
|
|
200
226
|
private readLayer(el: HTMLElement, scene: Scene): Layer {
|
|
201
227
|
const previous = scene.layers.find(layer => layer.el === el);
|
|
202
228
|
el.style.setProperty('will-change', TRANSFORM_WILL_CHANGE);
|
|
203
|
-
return { el, tracks: parseTracks(el.getAttribute(SceneAttribute.Tracks)), phoneTracks: parseTracks(el.getAttribute(SceneAttribute.PhoneTracks)), last: previous?.last ?? {}, stale: true };
|
|
229
|
+
return { el, tracks: parseTracks(el.getAttribute(SceneAttribute.Tracks)), phoneTracks: parseTracks(el.getAttribute(SceneAttribute.PhoneTracks)), last: previous?.last ?? {}, stale: true, words: previous?.words ?? null, wordsRefused: false };
|
|
204
230
|
}
|
|
205
231
|
|
|
206
232
|
private remeasureAll(): void {
|
|
@@ -248,6 +274,7 @@ export class ScrollDriver {
|
|
|
248
274
|
private write(scene: Scene): void {
|
|
249
275
|
for (const layer of scene.layers) {
|
|
250
276
|
const tracks = scene.phone && layer.phoneTracks.length ? layer.phoneTracks : layer.tracks;
|
|
277
|
+
this.writeWords(layer, sceneWordsLit(tracks, scene.progress));
|
|
251
278
|
const frame = sceneFrame(tracks, scene.progress, { inlineSign: scene.inlineSign });
|
|
252
279
|
for (const key of Object.keys(frame) as (keyof SceneFrame)[]) {
|
|
253
280
|
if (layer.stale || frame[key] !== layer.last[key]) layer.el.style[key] = frame[key] ?? '';
|
|
@@ -260,6 +287,28 @@ export class ScrollDriver {
|
|
|
260
287
|
layer.stale = false;
|
|
261
288
|
}
|
|
262
289
|
}
|
|
290
|
+
|
|
291
|
+
/** One opacity per word, and a write only for the few the scroll actually changed. */
|
|
292
|
+
private writeWords(layer: Layer, lit: number | null): void {
|
|
293
|
+
if (lit === null) {
|
|
294
|
+
this.restoreWords(layer);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Gone from the page: the text was replaced under it (an edit on the canvas), so its words are new.
|
|
298
|
+
if (layer.words && !layer.words.pieces[0]?.isConnected) layer.words = null;
|
|
299
|
+
if (!layer.words && !layer.wordsRefused) {
|
|
300
|
+
layer.words = splitText(layer.el, { cap: LIGHT_UP_PIECE_CAP });
|
|
301
|
+
layer.wordsRefused = !layer.words;
|
|
302
|
+
// The whole layer stood for its words until now (`sceneFirstFrame`); an Opacity track's frame writes after this.
|
|
303
|
+
layer.el.style.removeProperty('opacity');
|
|
304
|
+
layer.stale = true;
|
|
305
|
+
}
|
|
306
|
+
const pieces = layer.words?.pieces ?? [];
|
|
307
|
+
pieces.forEach((piece, i) => {
|
|
308
|
+
const opacity = String(sceneWordOpacity(i, pieces.length, lit));
|
|
309
|
+
if (piece.style.opacity !== opacity) piece.style.opacity = opacity;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
263
312
|
}
|
|
264
313
|
|
|
265
314
|
/** A page's Scenes, driven by one driver; null when it holds none, or when the visitor asked for less motion (the
|