@panelwave/player 1.5.0 → 1.6.1
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/panelwave-player.mjs +494 -70
- package/fesm2022/panelwave-player.mjs.map +1 -1
- package/index.d.ts +136 -12
- package/package.json +1 -1
|
@@ -5456,6 +5456,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
5456
5456
|
* Manages audio playback with Web Audio API
|
|
5457
5457
|
*/
|
|
5458
5458
|
const ROLES = ['ambient', 'music', 'voiceover', 'sfx'];
|
|
5459
|
+
/**
|
|
5460
|
+
* Whether an audio URL must be loaded with `crossOrigin = 'anonymous'`: an
|
|
5461
|
+
* http(s) file on another origin than the page. Same-origin files, data:/
|
|
5462
|
+
* blob: URLs and `file:` pages (the offline self-hosting package) stay in
|
|
5463
|
+
* no-CORS mode — CORS requests from a file: page fail.
|
|
5464
|
+
*/
|
|
5465
|
+
function needsCorsMode(url, pageHref = typeof location !== 'undefined' ? location.href : '') {
|
|
5466
|
+
try {
|
|
5467
|
+
const target = new URL(url, pageHref || undefined);
|
|
5468
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:') {
|
|
5469
|
+
return false;
|
|
5470
|
+
}
|
|
5471
|
+
const page = pageHref ? new URL(pageHref) : null;
|
|
5472
|
+
if (page && page.protocol !== 'http:' && page.protocol !== 'https:') {
|
|
5473
|
+
return false;
|
|
5474
|
+
}
|
|
5475
|
+
return !page || target.origin !== page.origin;
|
|
5476
|
+
}
|
|
5477
|
+
catch {
|
|
5478
|
+
return false;
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5459
5481
|
/**
|
|
5460
5482
|
* Audio Engine Service
|
|
5461
5483
|
* Provides Web Audio API-based audio playback with multiple buses.
|
|
@@ -5571,10 +5593,10 @@ class AudioEngineService {
|
|
|
5571
5593
|
}
|
|
5572
5594
|
}
|
|
5573
5595
|
/**
|
|
5574
|
-
* Play audio track
|
|
5596
|
+
* Play audio track, optionally from `offsetSeconds` into the file.
|
|
5575
5597
|
*/
|
|
5576
|
-
async play(track) {
|
|
5577
|
-
await this.startTrack(track,
|
|
5598
|
+
async play(track, offsetSeconds = 0) {
|
|
5599
|
+
await this.startTrack(track, offsetSeconds);
|
|
5578
5600
|
}
|
|
5579
5601
|
/**
|
|
5580
5602
|
* Stop audio track
|
|
@@ -5859,9 +5881,20 @@ class AudioEngineService {
|
|
|
5859
5881
|
if (!bus) {
|
|
5860
5882
|
throw new Error(`Invalid role: ${track.role}`);
|
|
5861
5883
|
}
|
|
5862
|
-
// Create audio element
|
|
5863
|
-
|
|
5884
|
+
// Create audio element. A file from another origin (object storage, a
|
|
5885
|
+
// CDN) must be fetched in CORS mode: routed through Web Audio
|
|
5886
|
+
// (createMediaElementSource below), a no-CORS cross-origin element plays
|
|
5887
|
+
// but outputs silence. crossOrigin has to be set before the src.
|
|
5888
|
+
const audio = new Audio();
|
|
5889
|
+
if (needsCorsMode(track.url)) {
|
|
5890
|
+
audio.crossOrigin = 'anonymous';
|
|
5891
|
+
}
|
|
5892
|
+
audio.src = track.url;
|
|
5864
5893
|
audio.loop = track.loop || false;
|
|
5894
|
+
const rate = typeof track.playbackRate === 'number' && isFinite(track.playbackRate) ? Math.max(0.25, Math.min(4, track.playbackRate)) : 1;
|
|
5895
|
+
if (rate !== 1) {
|
|
5896
|
+
audio.playbackRate = rate;
|
|
5897
|
+
}
|
|
5865
5898
|
if (offsetSeconds > 0) {
|
|
5866
5899
|
audio.currentTime = offsetSeconds;
|
|
5867
5900
|
}
|
|
@@ -5884,9 +5917,11 @@ class AudioEngineService {
|
|
|
5884
5917
|
audio.addEventListener('ended', active.onEnded);
|
|
5885
5918
|
}
|
|
5886
5919
|
try {
|
|
5887
|
-
//
|
|
5888
|
-
|
|
5889
|
-
|
|
5920
|
+
// Fade in (shortened when starting mid-fade). The ramp runs while the
|
|
5921
|
+
// element plays — awaiting it first delayed the start by the fade.
|
|
5922
|
+
const fadeMs = track.fadeIn && track.fadeIn > 0 ? track.fadeIn - offsetSeconds * 1000 : 0;
|
|
5923
|
+
if (fadeMs > 0) {
|
|
5924
|
+
void this.fadeIn(track.id, fadeMs, volume);
|
|
5890
5925
|
}
|
|
5891
5926
|
// Play audio
|
|
5892
5927
|
await audio.play();
|
|
@@ -6040,6 +6075,381 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
6040
6075
|
}]
|
|
6041
6076
|
}], ctorParameters: () => [] });
|
|
6042
6077
|
|
|
6078
|
+
/**
|
|
6079
|
+
* Audio asset resolution shared by the panel audio and timeline (sequence)
|
|
6080
|
+
* audio services: catalog id → the first variant the browser can play →
|
|
6081
|
+
* absolute URL, plus the asset's own role / loop hints.
|
|
6082
|
+
*/
|
|
6083
|
+
/**
|
|
6084
|
+
* Resolve an audio asset; `{ error }` explains why it cannot be played (not
|
|
6085
|
+
* in the catalog, not audio, no variants, no URL).
|
|
6086
|
+
*/
|
|
6087
|
+
function resolveAudioAsset(asset, assetsBase) {
|
|
6088
|
+
if (!asset) {
|
|
6089
|
+
return { error: 'is not in the asset catalog' };
|
|
6090
|
+
}
|
|
6091
|
+
if (asset.category !== 'audio') {
|
|
6092
|
+
return { error: 'is not an audio asset' };
|
|
6093
|
+
}
|
|
6094
|
+
const audioAsset = asset;
|
|
6095
|
+
const variant = pickPlayableVariant(audioAsset.variants ?? []);
|
|
6096
|
+
if (!variant) {
|
|
6097
|
+
return { error: 'has no audio variants' };
|
|
6098
|
+
}
|
|
6099
|
+
const url = resolveAssetUrl(variant.src, assetsBase, 'audio');
|
|
6100
|
+
if (!url) {
|
|
6101
|
+
return { error: 'has no usable URL' };
|
|
6102
|
+
}
|
|
6103
|
+
return { url, role: audioAsset.role, loop: variant.loop };
|
|
6104
|
+
}
|
|
6105
|
+
/**
|
|
6106
|
+
* First variant the browser reports it can play; otherwise the first one
|
|
6107
|
+
* (the element will surface the real error).
|
|
6108
|
+
*/
|
|
6109
|
+
function pickPlayableVariant(variants) {
|
|
6110
|
+
if (variants.length <= 1) {
|
|
6111
|
+
return variants[0];
|
|
6112
|
+
}
|
|
6113
|
+
const probe = createProbe();
|
|
6114
|
+
if (!probe) {
|
|
6115
|
+
return variants[0];
|
|
6116
|
+
}
|
|
6117
|
+
const playable = variants.find((variant) => {
|
|
6118
|
+
const mime = variant.mime || guessMimeType(variant.src);
|
|
6119
|
+
return mime ? probe.canPlayType(mime) !== '' : false;
|
|
6120
|
+
});
|
|
6121
|
+
return playable ?? variants[0];
|
|
6122
|
+
}
|
|
6123
|
+
function createProbe() {
|
|
6124
|
+
if (typeof document === 'undefined') {
|
|
6125
|
+
return null;
|
|
6126
|
+
}
|
|
6127
|
+
try {
|
|
6128
|
+
const element = document.createElement('audio');
|
|
6129
|
+
return typeof element.canPlayType === 'function' ? element : null;
|
|
6130
|
+
}
|
|
6131
|
+
catch {
|
|
6132
|
+
return null;
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
|
|
6136
|
+
/**
|
|
6137
|
+
* Timeline (sequence) audio placement for the reader — the same model as the
|
|
6138
|
+
* CMS editor's timeline: the work is one line of panels (chapters in order →
|
|
6139
|
+
* the active format's pages → reading order), each panel lasting its
|
|
6140
|
+
* `durationMs` (else the work's seconds-per-panel, else 5 s).
|
|
6141
|
+
*
|
|
6142
|
+
* A chapter's `sequenceAudioTracks` sit on that line relative to their
|
|
6143
|
+
* chapter's start. `span` (format 1.8.0): 'page' / 'chapter' tracks cover
|
|
6144
|
+
* their startPanelId..endPanelId range; a 'work' track keeps playing across
|
|
6145
|
+
* chapter boundaries until endPanelId (else the work's last panel); every
|
|
6146
|
+
* other track ends with its chapter.
|
|
6147
|
+
*
|
|
6148
|
+
* The reader does not follow a clock, so the timeline is used per panel:
|
|
6149
|
+
* entering a panel tells which tracks cover it, from which offset, and
|
|
6150
|
+
* whether one starts or ends inside it (`cuesForPanel`).
|
|
6151
|
+
*/
|
|
6152
|
+
const DEFAULT_PANEL_MS = 5000;
|
|
6153
|
+
/** Panels of a page in reading order (placement order without one). */
|
|
6154
|
+
function pagePanels(page) {
|
|
6155
|
+
if (page.readingOrder?.length) {
|
|
6156
|
+
return page.readingOrder;
|
|
6157
|
+
}
|
|
6158
|
+
return (page.layout?.placements ?? []).map((placement) => placement.panelId);
|
|
6159
|
+
}
|
|
6160
|
+
/** The work's panel line for a page format (null = every page). */
|
|
6161
|
+
function buildWorkTimeline(manifest, format, defaultPanelMs = DEFAULT_PANEL_MS) {
|
|
6162
|
+
const entries = [];
|
|
6163
|
+
const byPanel = new Map();
|
|
6164
|
+
const chapters = new Map();
|
|
6165
|
+
let t = 0;
|
|
6166
|
+
for (const chapter of (manifest?.chapters ?? [])) {
|
|
6167
|
+
const pages = pagesForFormat(chapter.pages, format);
|
|
6168
|
+
const order = pages.length ? pages.flatMap(pagePanels) : chapterReadingOrder(chapter);
|
|
6169
|
+
const chapterStart = t;
|
|
6170
|
+
for (const panelId of order) {
|
|
6171
|
+
const panel = chapter.panels?.[panelId];
|
|
6172
|
+
if (!panel || byPanel.has(panelId)) {
|
|
6173
|
+
continue;
|
|
6174
|
+
}
|
|
6175
|
+
const ms = typeof panel.durationMs === 'number' && panel.durationMs > 0 ? panel.durationMs : defaultPanelMs;
|
|
6176
|
+
const entry = { panelId, chapterId: chapter.id, startMs: t, endMs: t + ms };
|
|
6177
|
+
entries.push(entry);
|
|
6178
|
+
byPanel.set(panelId, entry);
|
|
6179
|
+
t += ms;
|
|
6180
|
+
}
|
|
6181
|
+
if (t > chapterStart) {
|
|
6182
|
+
chapters.set(chapter.id, { startMs: chapterStart, endMs: t });
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
return { entries, byPanel, chapters, endMs: t };
|
|
6186
|
+
}
|
|
6187
|
+
/** Where each (format-matching, unmuted) timeline track sits on the work line. */
|
|
6188
|
+
function resolveTrackRanges(manifest, timeline, format) {
|
|
6189
|
+
const ranges = [];
|
|
6190
|
+
for (const chapter of (manifest?.chapters ?? [])) {
|
|
6191
|
+
const extent = timeline.chapters.get(chapter.id);
|
|
6192
|
+
if (!extent) {
|
|
6193
|
+
continue; // no panels of this chapter in this format
|
|
6194
|
+
}
|
|
6195
|
+
for (const track of chapter.sequenceAudioTracks ?? []) {
|
|
6196
|
+
if (!track?.assetId || track.muted) {
|
|
6197
|
+
continue;
|
|
6198
|
+
}
|
|
6199
|
+
if (format && track.format && track.format !== format) {
|
|
6200
|
+
continue;
|
|
6201
|
+
}
|
|
6202
|
+
const span = track.span ?? 'custom';
|
|
6203
|
+
const first = track.startPanelId ? timeline.byPanel.get(track.startPanelId) : undefined;
|
|
6204
|
+
const last = track.endPanelId ? timeline.byPanel.get(track.endPanelId) : undefined;
|
|
6205
|
+
let startMs;
|
|
6206
|
+
let endMs;
|
|
6207
|
+
if ((span === 'page' || span === 'chapter') && first && last) {
|
|
6208
|
+
startMs = first.startMs;
|
|
6209
|
+
endMs = last.endMs;
|
|
6210
|
+
}
|
|
6211
|
+
else {
|
|
6212
|
+
startMs = extent.startMs + Math.max(0, Number(track.startTime) || 0);
|
|
6213
|
+
const duration = Math.max(0, Number(track.duration) || 0);
|
|
6214
|
+
endMs = duration > 0 ? startMs + duration : extent.endMs;
|
|
6215
|
+
}
|
|
6216
|
+
if (span === 'work') {
|
|
6217
|
+
endMs = last ? last.endMs : timeline.endMs;
|
|
6218
|
+
}
|
|
6219
|
+
else {
|
|
6220
|
+
endMs = Math.min(endMs, extent.endMs);
|
|
6221
|
+
}
|
|
6222
|
+
if (endMs > startMs) {
|
|
6223
|
+
ranges.push({ track, chapterId: chapter.id, startMs, endMs });
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
return ranges;
|
|
6228
|
+
}
|
|
6229
|
+
/** The tracks that sound while the reader is on `entry`'s panel. */
|
|
6230
|
+
function cuesForPanel(ranges, entry) {
|
|
6231
|
+
const cues = [];
|
|
6232
|
+
for (const range of ranges) {
|
|
6233
|
+
if (range.startMs >= entry.endMs || range.endMs <= entry.startMs) {
|
|
6234
|
+
continue;
|
|
6235
|
+
}
|
|
6236
|
+
const cue = {
|
|
6237
|
+
range,
|
|
6238
|
+
delayMs: Math.max(0, range.startMs - entry.startMs),
|
|
6239
|
+
offsetMs: Math.max(0, entry.startMs - range.startMs),
|
|
6240
|
+
};
|
|
6241
|
+
if (range.endMs < entry.endMs) {
|
|
6242
|
+
cue.stopAfterMs = range.endMs - entry.startMs;
|
|
6243
|
+
}
|
|
6244
|
+
cues.push(cue);
|
|
6245
|
+
}
|
|
6246
|
+
return cues;
|
|
6247
|
+
}
|
|
6248
|
+
|
|
6249
|
+
/**
|
|
6250
|
+
* Sequence Audio Service
|
|
6251
|
+
*
|
|
6252
|
+
* Plays a work's timeline audio (`chapters[].sequenceAudioTracks` — music,
|
|
6253
|
+
* ambience, voiceover beds) while the reader moves through the panels:
|
|
6254
|
+
*
|
|
6255
|
+
* - entering a panel starts every track that covers it, at the right offset
|
|
6256
|
+
* (a jump into the middle of a scene picks the music up where it is);
|
|
6257
|
+
* - a track keeps playing while the reader stays inside its range, so
|
|
6258
|
+
* music does not restart on every panel; leaving the range fades it out
|
|
6259
|
+
* (its `fadeOut`, else 250 ms);
|
|
6260
|
+
* - a track that starts or ends inside a panel is delayed / stopped there;
|
|
6261
|
+
* - `span: 'work'` tracks continue across chapter boundaries.
|
|
6262
|
+
*
|
|
6263
|
+
* Placement follows the CMS timeline (see sequence-audio-timeline.ts). Roles
|
|
6264
|
+
* map onto the engine buses, so the reader's Audio / SFX / Speech toggles
|
|
6265
|
+
* apply. Tracks refused by the autoplay policy are retried on the first
|
|
6266
|
+
* user gesture.
|
|
6267
|
+
*/
|
|
6268
|
+
/** Fade when a track ends because the reader left its range (unless it has its own fadeOut). */
|
|
6269
|
+
const SEQUENCE_AUDIO_LEAVE_FADE_MS = 250;
|
|
6270
|
+
/** Engine ids are prefixed so they never collide with panel audio (asset ids). */
|
|
6271
|
+
const sequenceEngineId = (trackId) => `seq:${trackId}`;
|
|
6272
|
+
class SequenceAudioService {
|
|
6273
|
+
constructor() {
|
|
6274
|
+
this.engine = inject(AudioEngineService);
|
|
6275
|
+
this.manifestService = inject(ManifestService);
|
|
6276
|
+
this.gesture = inject(UserGestureService);
|
|
6277
|
+
/** Tracks the current panel wants, by engine id. */
|
|
6278
|
+
this.desired = new Map();
|
|
6279
|
+
/** Refused by the autoplay policy; retried on the first gesture. */
|
|
6280
|
+
this.blocked = new Set();
|
|
6281
|
+
this.warned = new Set();
|
|
6282
|
+
this.gestureSub = this.gesture.userHasInteracted$.subscribe((interacted) => {
|
|
6283
|
+
if (interacted) {
|
|
6284
|
+
this.onUserGesture();
|
|
6285
|
+
}
|
|
6286
|
+
});
|
|
6287
|
+
}
|
|
6288
|
+
ngOnDestroy() {
|
|
6289
|
+
this.gestureSub.unsubscribe();
|
|
6290
|
+
this.stopAll();
|
|
6291
|
+
}
|
|
6292
|
+
/**
|
|
6293
|
+
* Follow the reader: call on every panel change (and when the page format
|
|
6294
|
+
* changes). `panelId` undefined = silence (cover, locked panel, teardown).
|
|
6295
|
+
*/
|
|
6296
|
+
syncPanel(panelId, format) {
|
|
6297
|
+
const { timeline, ranges } = this.layout(format);
|
|
6298
|
+
const entry = panelId ? timeline.byPanel.get(panelId) : undefined;
|
|
6299
|
+
const cues = entry ? cuesForPanel(ranges, entry) : [];
|
|
6300
|
+
const next = new Map(cues.map((cue) => [sequenceEngineId(cue.range.track.id), cue]));
|
|
6301
|
+
for (const [id, playing] of this.desired) {
|
|
6302
|
+
if (!next.has(id)) {
|
|
6303
|
+
this.release(id, this.leaveFade(playing.cue));
|
|
6304
|
+
this.desired.delete(id);
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
for (const [id, cue] of next) {
|
|
6308
|
+
const playing = this.desired.get(id);
|
|
6309
|
+
if (playing) {
|
|
6310
|
+
// Still inside its range (started, pending, blocked — or a one-shot
|
|
6311
|
+
// that already ended): never restart it, only re-arm an end that
|
|
6312
|
+
// falls inside this panel.
|
|
6313
|
+
playing.cue = cue;
|
|
6314
|
+
this.armStop(id, playing);
|
|
6315
|
+
continue;
|
|
6316
|
+
}
|
|
6317
|
+
this.schedule(id, cue);
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
/** Stop everything this service started. */
|
|
6321
|
+
stopAll() {
|
|
6322
|
+
for (const id of Array.from(this.desired.keys())) {
|
|
6323
|
+
this.release(id, 0);
|
|
6324
|
+
}
|
|
6325
|
+
this.desired.clear();
|
|
6326
|
+
}
|
|
6327
|
+
/** Engine ids of the tracks the current panel wants (started, pending or blocked). */
|
|
6328
|
+
getDesiredTrackIds() {
|
|
6329
|
+
return Array.from(this.desired.keys());
|
|
6330
|
+
}
|
|
6331
|
+
// ---------------------------------------------------------------------------
|
|
6332
|
+
layout(format) {
|
|
6333
|
+
const manifest = this.manifestService.getManifest();
|
|
6334
|
+
const cache = this.layoutCache;
|
|
6335
|
+
if (cache && cache.manifest === manifest && cache.format === format) {
|
|
6336
|
+
return cache;
|
|
6337
|
+
}
|
|
6338
|
+
const seconds = Number(manifest?.settings?.ui?.secondsPerPanel);
|
|
6339
|
+
const defaultMs = Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : DEFAULT_PANEL_MS;
|
|
6340
|
+
const timeline = buildWorkTimeline(manifest, format, defaultMs);
|
|
6341
|
+
const ranges = resolveTrackRanges(manifest, timeline, format);
|
|
6342
|
+
this.layoutCache = { manifest, format, timeline, ranges };
|
|
6343
|
+
return this.layoutCache;
|
|
6344
|
+
}
|
|
6345
|
+
schedule(id, cue) {
|
|
6346
|
+
this.release(id, 0);
|
|
6347
|
+
const playing = { cue };
|
|
6348
|
+
this.desired.set(id, playing);
|
|
6349
|
+
if (cue.delayMs > 0) {
|
|
6350
|
+
playing.startTimer = setTimeout(() => {
|
|
6351
|
+
playing.startTimer = undefined;
|
|
6352
|
+
this.start(id, playing, 0);
|
|
6353
|
+
}, cue.delayMs);
|
|
6354
|
+
}
|
|
6355
|
+
else {
|
|
6356
|
+
this.start(id, playing, cue.offsetMs);
|
|
6357
|
+
}
|
|
6358
|
+
this.armStop(id, playing);
|
|
6359
|
+
}
|
|
6360
|
+
/** Stop the track inside this panel when its range ends here. */
|
|
6361
|
+
armStop(id, playing) {
|
|
6362
|
+
if (playing.stopTimer !== undefined) {
|
|
6363
|
+
clearTimeout(playing.stopTimer);
|
|
6364
|
+
playing.stopTimer = undefined;
|
|
6365
|
+
}
|
|
6366
|
+
const stopAfter = playing.cue.stopAfterMs;
|
|
6367
|
+
if (stopAfter === undefined) {
|
|
6368
|
+
return;
|
|
6369
|
+
}
|
|
6370
|
+
playing.stopTimer = setTimeout(() => {
|
|
6371
|
+
playing.stopTimer = undefined;
|
|
6372
|
+
if (this.desired.get(id) === playing) {
|
|
6373
|
+
this.desired.delete(id);
|
|
6374
|
+
this.release(id, this.leaveFade(playing.cue));
|
|
6375
|
+
}
|
|
6376
|
+
}, stopAfter);
|
|
6377
|
+
}
|
|
6378
|
+
start(id, playing, offsetMs) {
|
|
6379
|
+
const track = playing.cue.range.track;
|
|
6380
|
+
const resolved = resolveAudioAsset(this.manifestService.getAsset(track.assetId), this.manifestService.getManifest()?.assets?.base);
|
|
6381
|
+
if ('error' in resolved) {
|
|
6382
|
+
this.warnOnce(track.assetId, resolved.error);
|
|
6383
|
+
return;
|
|
6384
|
+
}
|
|
6385
|
+
const rate = typeof track.playbackRate === 'number' && isFinite(track.playbackRate) ? Math.max(0.25, Math.min(4, track.playbackRate)) : 1;
|
|
6386
|
+
this.blocked.delete(id);
|
|
6387
|
+
this.engine
|
|
6388
|
+
.play({
|
|
6389
|
+
id,
|
|
6390
|
+
url: resolved.url,
|
|
6391
|
+
role: track.role,
|
|
6392
|
+
loop: track.loop ?? resolved.loop ?? false,
|
|
6393
|
+
volume: typeof track.volume === 'number' && isFinite(track.volume) ? Math.max(0, Math.min(2, track.volume)) : 1,
|
|
6394
|
+
fadeIn: track.fadeIn,
|
|
6395
|
+
fadeOut: track.fadeOut,
|
|
6396
|
+
playbackRate: rate,
|
|
6397
|
+
}, (offsetMs / 1000) * rate)
|
|
6398
|
+
.catch((error) => {
|
|
6399
|
+
if (this.isAutoplayRefusal(error) && !this.gesture.hasInteracted() && this.desired.get(id) === playing) {
|
|
6400
|
+
this.blocked.add(id);
|
|
6401
|
+
}
|
|
6402
|
+
});
|
|
6403
|
+
}
|
|
6404
|
+
release(id, fadeMs) {
|
|
6405
|
+
const playing = this.desired.get(id);
|
|
6406
|
+
if (playing?.startTimer !== undefined) {
|
|
6407
|
+
clearTimeout(playing.startTimer);
|
|
6408
|
+
playing.startTimer = undefined;
|
|
6409
|
+
}
|
|
6410
|
+
if (playing?.stopTimer !== undefined) {
|
|
6411
|
+
clearTimeout(playing.stopTimer);
|
|
6412
|
+
playing.stopTimer = undefined;
|
|
6413
|
+
}
|
|
6414
|
+
this.blocked.delete(id);
|
|
6415
|
+
if (this.engine.isActive(id)) {
|
|
6416
|
+
void this.engine.stop(id, fadeMs);
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
leaveFade(cue) {
|
|
6420
|
+
const fadeOut = cue.range.track.fadeOut;
|
|
6421
|
+
return typeof fadeOut === 'number' && fadeOut > 0 ? fadeOut : SEQUENCE_AUDIO_LEAVE_FADE_MS;
|
|
6422
|
+
}
|
|
6423
|
+
onUserGesture() {
|
|
6424
|
+
for (const id of Array.from(this.blocked)) {
|
|
6425
|
+
this.blocked.delete(id);
|
|
6426
|
+
const playing = this.desired.get(id);
|
|
6427
|
+
if (playing) {
|
|
6428
|
+
this.start(id, playing, playing.cue.offsetMs);
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
isAutoplayRefusal(error) {
|
|
6433
|
+
const name = error?.name;
|
|
6434
|
+
return name === 'NotAllowedError' || name === 'AbortError';
|
|
6435
|
+
}
|
|
6436
|
+
warnOnce(assetId, reason) {
|
|
6437
|
+
if (this.warned.has(assetId)) {
|
|
6438
|
+
return;
|
|
6439
|
+
}
|
|
6440
|
+
this.warned.add(assetId);
|
|
6441
|
+
console.warn(`[SequenceAudioService] audio asset "${assetId}" ${reason}; timeline track skipped.`);
|
|
6442
|
+
}
|
|
6443
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SequenceAudioService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
6444
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SequenceAudioService, providedIn: 'root' }); }
|
|
6445
|
+
}
|
|
6446
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SequenceAudioService, decorators: [{
|
|
6447
|
+
type: Injectable,
|
|
6448
|
+
args: [{
|
|
6449
|
+
providedIn: 'root',
|
|
6450
|
+
}]
|
|
6451
|
+
}], ctorParameters: () => [] });
|
|
6452
|
+
|
|
6043
6453
|
/**
|
|
6044
6454
|
* Panel Audio Service
|
|
6045
6455
|
*
|
|
@@ -6250,61 +6660,22 @@ class PanelAudioService {
|
|
|
6250
6660
|
return result;
|
|
6251
6661
|
}
|
|
6252
6662
|
resolveTrack(entry) {
|
|
6253
|
-
const
|
|
6254
|
-
if (
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
const audioAsset = asset;
|
|
6259
|
-
const variant = this.pickVariant(audioAsset.variants ?? []);
|
|
6260
|
-
if (!variant) {
|
|
6261
|
-
this.warnOnce(entry.assetId, 'has no audio variants');
|
|
6262
|
-
return null;
|
|
6263
|
-
}
|
|
6264
|
-
const base = this.manifestService.getManifest()?.assets?.base;
|
|
6265
|
-
const url = resolveAssetUrl(variant.src, base, 'audio');
|
|
6266
|
-
if (!url) {
|
|
6663
|
+
const resolved = resolveAudioAsset(this.manifestService.getAsset(entry.assetId), this.manifestService.getManifest()?.assets?.base);
|
|
6664
|
+
if ('error' in resolved) {
|
|
6665
|
+
if (resolved.error !== 'has no usable URL') {
|
|
6666
|
+
this.warnOnce(entry.assetId, resolved.error);
|
|
6667
|
+
}
|
|
6267
6668
|
return null;
|
|
6268
6669
|
}
|
|
6269
6670
|
return {
|
|
6270
6671
|
id: entry.assetId,
|
|
6271
|
-
url,
|
|
6272
|
-
role: busForRole(entry.role ??
|
|
6273
|
-
loop: entry.loop ??
|
|
6672
|
+
url: resolved.url,
|
|
6673
|
+
role: busForRole(entry.role ?? resolved.role),
|
|
6674
|
+
loop: entry.loop ?? resolved.loop ?? false,
|
|
6274
6675
|
gain: this.clampGain(entry.gain),
|
|
6275
6676
|
startAtMs: typeof entry.startAtMs === 'number' && entry.startAtMs > 0 ? entry.startAtMs : 0,
|
|
6276
6677
|
};
|
|
6277
6678
|
}
|
|
6278
|
-
/**
|
|
6279
|
-
* First variant the browser reports it can play; otherwise the first one
|
|
6280
|
-
* (the element will surface the real error).
|
|
6281
|
-
*/
|
|
6282
|
-
pickVariant(variants) {
|
|
6283
|
-
if (variants.length <= 1) {
|
|
6284
|
-
return variants[0];
|
|
6285
|
-
}
|
|
6286
|
-
const probe = this.createProbe();
|
|
6287
|
-
if (!probe) {
|
|
6288
|
-
return variants[0];
|
|
6289
|
-
}
|
|
6290
|
-
const playable = variants.find((variant) => {
|
|
6291
|
-
const mime = variant.mime || guessMimeType(variant.src);
|
|
6292
|
-
return mime ? probe.canPlayType(mime) !== '' : false;
|
|
6293
|
-
});
|
|
6294
|
-
return playable ?? variants[0];
|
|
6295
|
-
}
|
|
6296
|
-
createProbe() {
|
|
6297
|
-
if (typeof document === 'undefined') {
|
|
6298
|
-
return null;
|
|
6299
|
-
}
|
|
6300
|
-
try {
|
|
6301
|
-
const element = document.createElement('audio');
|
|
6302
|
-
return typeof element.canPlayType === 'function' ? element : null;
|
|
6303
|
-
}
|
|
6304
|
-
catch {
|
|
6305
|
-
return null;
|
|
6306
|
-
}
|
|
6307
|
-
}
|
|
6308
6679
|
clampGain(gain) {
|
|
6309
6680
|
if (typeof gain !== 'number' || !isFinite(gain)) {
|
|
6310
6681
|
return 1;
|
|
@@ -11932,6 +12303,14 @@ class SpeechBubblesComponent {
|
|
|
11932
12303
|
* screens (phones) get smaller lettering.
|
|
11933
12304
|
*/
|
|
11934
12305
|
this.readingScale = 1;
|
|
12306
|
+
/**
|
|
12307
|
+
* Page view: place a tailed balloon by its AUTHORED tail tip (the speaker
|
|
12308
|
+
* on the artwork) instead of centring its body on the stored box, and keep
|
|
12309
|
+
* the tail at its authored length when the MIN_LETTERING_PX floor boosts
|
|
12310
|
+
* the balloon — the CMS editor's page view rule (PanelRenderer), which the
|
|
12311
|
+
* lettered PDF must reproduce. Panel view keeps the centre placement.
|
|
12312
|
+
*/
|
|
12313
|
+
this.anchorTailTip = false;
|
|
11935
12314
|
/**
|
|
11936
12315
|
* Work-level balloon config defaults (from settings.typography.balloon_config)
|
|
11937
12316
|
*/
|
|
@@ -11987,6 +12366,7 @@ class SpeechBubblesComponent {
|
|
|
11987
12366
|
changes['containerWidth'] ||
|
|
11988
12367
|
changes['containerHeight'] ||
|
|
11989
12368
|
changes['readingScale'] ||
|
|
12369
|
+
changes['anchorTailTip'] ||
|
|
11990
12370
|
changes['workBalloonConfig'] ||
|
|
11991
12371
|
changes['characters'];
|
|
11992
12372
|
if (needsRerender) {
|
|
@@ -12118,7 +12498,8 @@ class SpeechBubblesComponent {
|
|
|
12118
12498
|
// NOT proportionally with the authored shape box (which would blow
|
|
12119
12499
|
// text up on large screens). Capped so a balloon never exceeds its
|
|
12120
12500
|
// panel container, and clamped against broken measurements.
|
|
12121
|
-
|
|
12501
|
+
const baseScale = Math.min(4, Math.max(0.25, this.readingScale || 1));
|
|
12502
|
+
let scale = baseScale;
|
|
12122
12503
|
// Never letter below MIN_LETTERING_PX: on a page shown small the balloon
|
|
12123
12504
|
// grows just enough (still anchored and kept inside the panel below).
|
|
12124
12505
|
if (config.fontSize > 0) {
|
|
@@ -12129,18 +12510,37 @@ class SpeechBubblesComponent {
|
|
|
12129
12510
|
// The SVG can run a little wider than maxWidth (stroke, tail), so narrow
|
|
12130
12511
|
// by the remaining overshoot for up to three passes.
|
|
12131
12512
|
let maxWidth = Number(renderOpts['maxWidth']) || svgWidth;
|
|
12513
|
+
let usedOpts = renderOpts;
|
|
12132
12514
|
for (let pass = 0; pass < 3 && svgWidth > 0 && this.containerWidth > 0 && svgWidth * scale > this.containerWidth + 0.5; pass++) {
|
|
12133
12515
|
const narrower = Math.max(40, Math.floor(maxWidth * (this.containerWidth / (svgWidth * scale)) * 0.99));
|
|
12134
12516
|
if (narrower >= maxWidth)
|
|
12135
12517
|
break;
|
|
12136
12518
|
maxWidth = narrower;
|
|
12137
|
-
|
|
12519
|
+
usedOpts = { ...renderOpts, maxWidth, maxHeight: 0 };
|
|
12520
|
+
result = new ComicBalloon(balloonContainer, usedOpts).render(text, tailOpts);
|
|
12138
12521
|
svgWidth = result.svg?.style.width ? parseFloat(result.svg.style.width) : maxWidth;
|
|
12139
12522
|
svgHeight = result.svg?.style.height ? parseFloat(result.svg.style.height) : svgHeight;
|
|
12140
12523
|
}
|
|
12141
12524
|
if (svgWidth > 0 && svgHeight > 0) {
|
|
12142
12525
|
scale = Math.max(0.25, Math.min(scale, this.containerWidth / svgWidth, this.containerHeight / svgHeight));
|
|
12143
12526
|
}
|
|
12527
|
+
// Page view (anchorTailTip): the tail tip stays on the AUTHORED tip
|
|
12528
|
+
// and a readability boost above the base scale grows only the body —
|
|
12529
|
+
// the tail is rendered at length / boost so its visible length stays
|
|
12530
|
+
// authored (the editor's tail counter-scaling). Connectors keep the
|
|
12531
|
+
// centre rule: the editor glues them onto their target balloon.
|
|
12532
|
+
const tailLength = Number(tailOpts?.length) || 50;
|
|
12533
|
+
const cornerRadius = Number(renderOpts['cornerRadius'] ?? 0.45) || 0;
|
|
12534
|
+
const tipAnchored = this.anchorTailTip && !!tailOpts && tailOpts.position !== undefined
|
|
12535
|
+
&& config.balloonType !== 'connector';
|
|
12536
|
+
let renderedTailLength = tailLength;
|
|
12537
|
+
if (tipAnchored && scale > baseScale * 1.001) {
|
|
12538
|
+
renderedTailLength = tailLength * baseScale / scale;
|
|
12539
|
+
result = new ComicBalloon(balloonContainer, usedOpts)
|
|
12540
|
+
.render(text, { ...tailOpts, length: renderedTailLength });
|
|
12541
|
+
svgWidth = result.svg?.style.width ? parseFloat(result.svg.style.width) : svgWidth;
|
|
12542
|
+
svgHeight = result.svg?.style.height ? parseFloat(result.svg.style.height) : svgHeight;
|
|
12543
|
+
}
|
|
12144
12544
|
const visualW = svgWidth * scale;
|
|
12145
12545
|
const visualH = svgHeight * scale;
|
|
12146
12546
|
// Auto-grow (schema 1.8 textFit): when the text needed more room than
|
|
@@ -12183,14 +12583,28 @@ class SpeechBubblesComponent {
|
|
|
12183
12583
|
const eps = Math.max(4, 0.02 * Math.min(this.containerWidth, this.containerHeight));
|
|
12184
12584
|
let centerX = pos.left + grownOffsetX - bodyOffsetX * scale;
|
|
12185
12585
|
let centerY = pos.top + grownOffsetY - bodyOffsetY * scale;
|
|
12186
|
-
if (
|
|
12187
|
-
|
|
12188
|
-
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12586
|
+
if (tipAnchored) {
|
|
12587
|
+
// Authored tip: the stored box with the tail at the base scale
|
|
12588
|
+
// (the editor's tip before its re-measure and boost). The rendered
|
|
12589
|
+
// balloon's own tip, relative to its body centre, lands on it — a
|
|
12590
|
+
// grown or boosted body moves away from the speaker, the tip stays.
|
|
12591
|
+
const authoredW = boxRight - boxLeft > 1 ? boxRight - boxLeft : config.maxWidth * baseScale;
|
|
12592
|
+
const authoredH = boxBottom - boxTop > 1 ? boxBottom - boxTop : config.maxHeight * baseScale;
|
|
12593
|
+
const tip = tailConfigToTip({ cx: (boxLeft + boxRight) / 2, cy: (boxTop + boxBottom) / 2, width: authoredW, height: authoredH }, { position: Number(tailOpts.position), length: tailLength * baseScale }, cornerRadius);
|
|
12594
|
+
const localTip = tailConfigToTip({ cx: 0, cy: 0, width: result.width, height: result.height }, { position: Number(tailOpts.position), length: renderedTailLength }, cornerRadius);
|
|
12595
|
+
centerX = tip.x - (localTip.x + bodyOffsetX) * scale;
|
|
12596
|
+
centerY = tip.y - (localTip.y + bodyOffsetY) * scale;
|
|
12597
|
+
}
|
|
12598
|
+
else {
|
|
12599
|
+
if (boxLeft <= eps)
|
|
12600
|
+
centerX = visualW / 2;
|
|
12601
|
+
else if (boxRight >= this.containerWidth - eps)
|
|
12602
|
+
centerX = this.containerWidth - visualW / 2;
|
|
12603
|
+
if (boxTop <= eps)
|
|
12604
|
+
centerY = visualH / 2;
|
|
12605
|
+
else if (boxBottom >= this.containerHeight - eps)
|
|
12606
|
+
centerY = this.containerHeight - visualH / 2;
|
|
12607
|
+
}
|
|
12194
12608
|
// A balloon bigger than its authored box (small screens) must not be
|
|
12195
12609
|
// pushed across the panel border and clipped: keep it inside the panel.
|
|
12196
12610
|
centerX = clampCenter(centerX, visualW, this.containerWidth);
|
|
@@ -12279,7 +12693,7 @@ class SpeechBubblesComponent {
|
|
|
12279
12693
|
this.bubbleClick.emit(bubble);
|
|
12280
12694
|
}
|
|
12281
12695
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SpeechBubblesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
12282
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SpeechBubblesComponent, isStandalone: true, selector: "pw-speech-bubbles", inputs: { bubbles: "bubbles", locale: "locale", containerWidth: "containerWidth", containerHeight: "containerHeight", readingScale: "readingScale", workBalloonConfig: "workBalloonConfig", characters: "characters" }, outputs: { bubbleClick: "bubbleClick", bubbleAudioPlay: "bubbleAudioPlay" }, usesOnChanges: true, ngImport: i0, template: "<!-- Speech bubbles are rendered imperatively via ComicBalloon SVG renderer -->\n<div class=\"speech-bubbles-container\"></div>\n", styles: [":host{display:block;position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:20}.speech-bubbles-container{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.speech-bubble-wrapper{position:absolute;pointer-events:auto;cursor:pointer;z-index:10;animation:bubbleAppear .3s ease-out}.speech-bubble-wrapper:focus{outline:2px solid rgba(0,120,255,.6);outline-offset:4px;border-radius:4px}.speech-bubble-wrapper:hover{filter:drop-shadow(0 2px 6px rgba(0,0,0,.15))}.balloon-container{display:inline-block;position:relative}.balloon-container :deep(.comic-balloon-svg){display:block;overflow:visible}@keyframes bubbleAppear{0%{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
12696
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SpeechBubblesComponent, isStandalone: true, selector: "pw-speech-bubbles", inputs: { bubbles: "bubbles", locale: "locale", containerWidth: "containerWidth", containerHeight: "containerHeight", readingScale: "readingScale", anchorTailTip: "anchorTailTip", workBalloonConfig: "workBalloonConfig", characters: "characters" }, outputs: { bubbleClick: "bubbleClick", bubbleAudioPlay: "bubbleAudioPlay" }, usesOnChanges: true, ngImport: i0, template: "<!-- Speech bubbles are rendered imperatively via ComicBalloon SVG renderer -->\n<div class=\"speech-bubbles-container\"></div>\n", styles: [":host{display:block;position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:20}.speech-bubbles-container{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.speech-bubble-wrapper{position:absolute;pointer-events:auto;cursor:pointer;z-index:10;animation:bubbleAppear .3s ease-out}.speech-bubble-wrapper:focus{outline:2px solid rgba(0,120,255,.6);outline-offset:4px;border-radius:4px}.speech-bubble-wrapper:hover{filter:drop-shadow(0 2px 6px rgba(0,0,0,.15))}.balloon-container{display:inline-block;position:relative}.balloon-container :deep(.comic-balloon-svg){display:block;overflow:visible}@keyframes bubbleAppear{0%{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
12283
12697
|
}
|
|
12284
12698
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SpeechBubblesComponent, decorators: [{
|
|
12285
12699
|
type: Component,
|
|
@@ -12294,6 +12708,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
12294
12708
|
type: Input
|
|
12295
12709
|
}], readingScale: [{
|
|
12296
12710
|
type: Input
|
|
12711
|
+
}], anchorTailTip: [{
|
|
12712
|
+
type: Input
|
|
12297
12713
|
}], workBalloonConfig: [{
|
|
12298
12714
|
type: Input
|
|
12299
12715
|
}], characters: [{
|
|
@@ -14393,11 +14809,11 @@ class ViewportComponent {
|
|
|
14393
14809
|
console.groupEnd();
|
|
14394
14810
|
}
|
|
14395
14811
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ViewportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14396
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ViewportComponent, isStandalone: true, selector: "pw-viewport", inputs: { panel: "panel", page: "page", pageBackground: "pageBackground", panels: "panels", hiddenPanelIds: "hiddenPanelIds", preloadTargets: "preloadTargets", viewMode: "viewMode", currentPanelId: "currentPanelId", locale: "locale", variableContext: "variableContext", panX: "panX", panY: "panY", zoom: "zoom", reducedMotion: "reducedMotion", transition: "transition", interactive: "interactive", showOverflowArrows: "showOverflowArrows", enableViewportCulling: "enableViewportCulling", enableLazyLoading: "enableLazyLoading", workBalloonConfig: "workBalloonConfig", characters: "characters", speechEnabled: "speechEnabled", graph: "graph", preload: "preload" }, outputs: { viewportClick: "viewportClick", performanceMetrics: "performanceMetrics", transformChange: "transformChange", layerClick: "layerClick", swipe: "swipe", navigatePrevious: "navigatePrevious", navigateNext: "navigateNext", panelDoubleClick: "panelDoubleClick", panelFocus: "panelFocus", hotspotActivate: "hotspotActivate", deadClick: "deadClick", lockedPanelActivate: "lockedPanelActivate" }, host: { listeners: { "window:resize": "onWindowResize()", "mousedown": "onMouseDown($event)", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)", "wheel": "onWheel($event)", "touchstart": "onTouchStart($event)", "touchmove": "onTouchMove($event)", "touchend": "onTouchEnd($event)", "touchcancel": "onTouchEnd($event)", "keydown.tab": "onKeydownTab($event)", "keydown.arrowRight": "onKeydownNext($event)", "keydown.arrowDown": "onKeydownNext($event)", "keydown.arrowLeft": "onKeydownPrevious($event)", "keydown.arrowUp": "onKeydownPrevious($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n [style.background-color]=\"getViewportBackground()\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (dblclick)=\"onViewportDoubleClick($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <!-- The outgoing panel holds the end state of its animation. -->\n <div class=\"panel-container\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"leavingPanel.animations\"\n [pwPanelAnimationKey]=\"leavingPanelId || ''\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(leavingPanel, leavingPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"panel.animations\"\n [pwPanelAnimationKey]=\"currentPanelId || ''\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Everything that belongs to the artwork; a camera move transforms it as one. -->\n <div class=\"pw-panel-camera\">\n @if (isLocked(panel, currentPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [bubbles]=\"panel.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div\n class=\"viewport-page\"\n [style.--pw-page-aspect]=\"pageAspect\"\n [style.--pw-stage-aspect]=\"stageAspect\"\n [style.--pw-fit-x]=\"pageFit.x\"\n [style.--pw-fit-y]=\"pageFit.y\"\n [style.--pw-fit-w]=\"pageFit.w\"\n [style.--pw-fit-h]=\"pageFit.h\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\" [style.background-color]=\"pageBackgroundOf(leavingPage)\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (isBranchHidden(placement.panelId)) {\n <div class=\"branch-placeholder\" aria-hidden=\"true\"></div>\n } @else if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [style.background-color]=\"pageBackgroundOf(page)\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [class.panel-branch-hidden]=\"isBranchHidden(placement.panelId)\"\n [attr.aria-hidden]=\"isBranchHidden(placement.panelId) ? 'true' : null\"\n [attr.tabindex]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (isBranchHidden(placement.panelId)) {\n <!-- Not on the reader's path (yet): a grey box keeps the page's shape. -->\n <div class=\"branch-placeholder\"></div>\n } @else if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder\n [actionable]=\"true\"\n (activate)=\"lockedPanelActivate.emit(placement.panelId)\"></pw-locked-panel-placeholder>\n } @else {\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [bubbles]=\"panelData.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel' || (viewMode === 'page' && page)) {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Previous page' : 'Previous panel'\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Next page' : 'Next panel'\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#1a1a1a;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:transparent;width:1280px;height:720px;max-width:100%;max-height:100%}.pw-panel-camera{position:absolute;inset:0}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:0;box-sizing:border-box;container-type:size}.page-canvas{position:relative;width:100%;aspect-ratio:var(--pw-page-aspect, 16 / 9);background:#1a1a1a;overflow:hidden}.page-stage{position:relative;width:min(100cqw,100cqh * var(--pw-stage-aspect, var(--pw-page-aspect, 1.7778)));aspect-ratio:var(--pw-stage-aspect, var(--pw-page-aspect, 16 / 9));flex:none;overflow:hidden}.page-stage .page-canvas{position:absolute;left:calc(-100% * var(--pw-fit-x, 0) / var(--pw-fit-w, 1));top:calc(-100% * var(--pw-fit-y, 0) / var(--pw-fit-h, 1));width:calc(100% / var(--pw-fit-w, 1));height:calc(100% / var(--pw-fit-h, 1));max-width:none;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.branch-placeholder{position:absolute;inset:0;background:#c9ccd1;border-radius:inherit}.viewport-page .panel-container.panel-branch-hidden{pointer-events:none}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "bubbles", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "directive", type: PanelAnimationDirective, selector: "[pwPanelAnimation]", inputs: ["pwPanelAnimation", "pwPanelAnimationKey", "pwPanelAnimationReducedMotion", "pwPanelAnimationStatic"] }, { kind: "component", type: LockedPanelComponent, selector: "pw-locked-panel-placeholder", inputs: ["actionable"], outputs: ["activate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14812
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ViewportComponent, isStandalone: true, selector: "pw-viewport", inputs: { panel: "panel", page: "page", pageBackground: "pageBackground", panels: "panels", hiddenPanelIds: "hiddenPanelIds", preloadTargets: "preloadTargets", viewMode: "viewMode", currentPanelId: "currentPanelId", locale: "locale", variableContext: "variableContext", panX: "panX", panY: "panY", zoom: "zoom", reducedMotion: "reducedMotion", transition: "transition", interactive: "interactive", showOverflowArrows: "showOverflowArrows", enableViewportCulling: "enableViewportCulling", enableLazyLoading: "enableLazyLoading", workBalloonConfig: "workBalloonConfig", characters: "characters", speechEnabled: "speechEnabled", graph: "graph", preload: "preload" }, outputs: { viewportClick: "viewportClick", performanceMetrics: "performanceMetrics", transformChange: "transformChange", layerClick: "layerClick", swipe: "swipe", navigatePrevious: "navigatePrevious", navigateNext: "navigateNext", panelDoubleClick: "panelDoubleClick", panelFocus: "panelFocus", hotspotActivate: "hotspotActivate", deadClick: "deadClick", lockedPanelActivate: "lockedPanelActivate" }, host: { listeners: { "window:resize": "onWindowResize()", "mousedown": "onMouseDown($event)", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)", "wheel": "onWheel($event)", "touchstart": "onTouchStart($event)", "touchmove": "onTouchMove($event)", "touchend": "onTouchEnd($event)", "touchcancel": "onTouchEnd($event)", "keydown.tab": "onKeydownTab($event)", "keydown.arrowRight": "onKeydownNext($event)", "keydown.arrowDown": "onKeydownNext($event)", "keydown.arrowLeft": "onKeydownPrevious($event)", "keydown.arrowUp": "onKeydownPrevious($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n [style.background-color]=\"getViewportBackground()\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (dblclick)=\"onViewportDoubleClick($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <!-- The outgoing panel holds the end state of its animation. -->\n <div class=\"panel-container\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"leavingPanel.animations\"\n [pwPanelAnimationKey]=\"leavingPanelId || ''\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(leavingPanel, leavingPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"panel.animations\"\n [pwPanelAnimationKey]=\"currentPanelId || ''\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Everything that belongs to the artwork; a camera move transforms it as one. -->\n <div class=\"pw-panel-camera\">\n @if (isLocked(panel, currentPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [bubbles]=\"panel.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div\n class=\"viewport-page\"\n [style.--pw-page-aspect]=\"pageAspect\"\n [style.--pw-stage-aspect]=\"stageAspect\"\n [style.--pw-fit-x]=\"pageFit.x\"\n [style.--pw-fit-y]=\"pageFit.y\"\n [style.--pw-fit-w]=\"pageFit.w\"\n [style.--pw-fit-h]=\"pageFit.h\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\" [style.background-color]=\"pageBackgroundOf(leavingPage)\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (isBranchHidden(placement.panelId)) {\n <div class=\"branch-placeholder\" aria-hidden=\"true\"></div>\n } @else if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [anchorTailTip]=\"true\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [style.background-color]=\"pageBackgroundOf(page)\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [class.panel-branch-hidden]=\"isBranchHidden(placement.panelId)\"\n [attr.aria-hidden]=\"isBranchHidden(placement.panelId) ? 'true' : null\"\n [attr.tabindex]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (isBranchHidden(placement.panelId)) {\n <!-- Not on the reader's path (yet): a grey box keeps the page's shape. -->\n <div class=\"branch-placeholder\"></div>\n } @else if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder\n [actionable]=\"true\"\n (activate)=\"lockedPanelActivate.emit(placement.panelId)\"></pw-locked-panel-placeholder>\n } @else {\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [bubbles]=\"panelData.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [anchorTailTip]=\"true\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel' || (viewMode === 'page' && page)) {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Previous page' : 'Previous panel'\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Next page' : 'Next panel'\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#1a1a1a;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:transparent;width:1280px;height:720px;max-width:100%;max-height:100%}.pw-panel-camera{position:absolute;inset:0}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:0;box-sizing:border-box;container-type:size}.page-canvas{position:relative;width:100%;aspect-ratio:var(--pw-page-aspect, 16 / 9);background:#1a1a1a;overflow:hidden}.page-stage{position:relative;width:min(100cqw,100cqh * var(--pw-stage-aspect, var(--pw-page-aspect, 1.7778)));aspect-ratio:var(--pw-stage-aspect, var(--pw-page-aspect, 16 / 9));flex:none;overflow:hidden}.page-stage .page-canvas{position:absolute;left:calc(-100% * var(--pw-fit-x, 0) / var(--pw-fit-w, 1));top:calc(-100% * var(--pw-fit-y, 0) / var(--pw-fit-h, 1));width:calc(100% / var(--pw-fit-w, 1));height:calc(100% / var(--pw-fit-h, 1));max-width:none;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.branch-placeholder{position:absolute;inset:0;background:#c9ccd1;border-radius:inherit}.viewport-page .panel-container.panel-branch-hidden{pointer-events:none}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "anchorTailTip", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "bubbles", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "directive", type: PanelAnimationDirective, selector: "[pwPanelAnimation]", inputs: ["pwPanelAnimation", "pwPanelAnimationKey", "pwPanelAnimationReducedMotion", "pwPanelAnimationStatic"] }, { kind: "component", type: LockedPanelComponent, selector: "pw-locked-panel-placeholder", inputs: ["actionable"], outputs: ["activate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14397
14813
|
}
|
|
14398
14814
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ViewportComponent, decorators: [{
|
|
14399
14815
|
type: Component,
|
|
14400
|
-
args: [{ selector: 'pw-viewport', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent, PwIconComponent, PanelAnimationDirective, LockedPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n [style.background-color]=\"getViewportBackground()\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (dblclick)=\"onViewportDoubleClick($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <!-- The outgoing panel holds the end state of its animation. -->\n <div class=\"panel-container\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"leavingPanel.animations\"\n [pwPanelAnimationKey]=\"leavingPanelId || ''\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(leavingPanel, leavingPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"panel.animations\"\n [pwPanelAnimationKey]=\"currentPanelId || ''\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Everything that belongs to the artwork; a camera move transforms it as one. -->\n <div class=\"pw-panel-camera\">\n @if (isLocked(panel, currentPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [bubbles]=\"panel.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div\n class=\"viewport-page\"\n [style.--pw-page-aspect]=\"pageAspect\"\n [style.--pw-stage-aspect]=\"stageAspect\"\n [style.--pw-fit-x]=\"pageFit.x\"\n [style.--pw-fit-y]=\"pageFit.y\"\n [style.--pw-fit-w]=\"pageFit.w\"\n [style.--pw-fit-h]=\"pageFit.h\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\" [style.background-color]=\"pageBackgroundOf(leavingPage)\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (isBranchHidden(placement.panelId)) {\n <div class=\"branch-placeholder\" aria-hidden=\"true\"></div>\n } @else if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [style.background-color]=\"pageBackgroundOf(page)\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [class.panel-branch-hidden]=\"isBranchHidden(placement.panelId)\"\n [attr.aria-hidden]=\"isBranchHidden(placement.panelId) ? 'true' : null\"\n [attr.tabindex]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (isBranchHidden(placement.panelId)) {\n <!-- Not on the reader's path (yet): a grey box keeps the page's shape. -->\n <div class=\"branch-placeholder\"></div>\n } @else if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder\n [actionable]=\"true\"\n (activate)=\"lockedPanelActivate.emit(placement.panelId)\"></pw-locked-panel-placeholder>\n } @else {\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [bubbles]=\"panelData.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel' || (viewMode === 'page' && page)) {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Previous page' : 'Previous panel'\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Next page' : 'Next panel'\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#1a1a1a;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:transparent;width:1280px;height:720px;max-width:100%;max-height:100%}.pw-panel-camera{position:absolute;inset:0}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:0;box-sizing:border-box;container-type:size}.page-canvas{position:relative;width:100%;aspect-ratio:var(--pw-page-aspect, 16 / 9);background:#1a1a1a;overflow:hidden}.page-stage{position:relative;width:min(100cqw,100cqh * var(--pw-stage-aspect, var(--pw-page-aspect, 1.7778)));aspect-ratio:var(--pw-stage-aspect, var(--pw-page-aspect, 16 / 9));flex:none;overflow:hidden}.page-stage .page-canvas{position:absolute;left:calc(-100% * var(--pw-fit-x, 0) / var(--pw-fit-w, 1));top:calc(-100% * var(--pw-fit-y, 0) / var(--pw-fit-h, 1));width:calc(100% / var(--pw-fit-w, 1));height:calc(100% / var(--pw-fit-h, 1));max-width:none;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.branch-placeholder{position:absolute;inset:0;background:#c9ccd1;border-radius:inherit}.viewport-page .panel-container.panel-branch-hidden{pointer-events:none}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"] }]
|
|
14816
|
+
args: [{ selector: 'pw-viewport', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent, PwIconComponent, PanelAnimationDirective, LockedPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n [style.background-color]=\"getViewportBackground()\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (dblclick)=\"onViewportDoubleClick($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <!-- The outgoing panel holds the end state of its animation. -->\n <div class=\"panel-container\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"leavingPanel.animations\"\n [pwPanelAnimationKey]=\"leavingPanelId || ''\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(leavingPanel, leavingPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\"\n [style.width.px]=\"panelBox?.width\"\n [style.height.px]=\"panelBox?.height\"\n [pwPanelAnimation]=\"panel.animations\"\n [pwPanelAnimationKey]=\"currentPanelId || ''\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Everything that belongs to the artwork; a camera move transforms it as one. -->\n <div class=\"pw-panel-camera\">\n @if (isLocked(panel, currentPanelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [bubbles]=\"panel.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div\n class=\"viewport-page\"\n [style.--pw-page-aspect]=\"pageAspect\"\n [style.--pw-stage-aspect]=\"stageAspect\"\n [style.--pw-fit-x]=\"pageFit.x\"\n [style.--pw-fit-y]=\"pageFit.y\"\n [style.--pw-fit-w]=\"pageFit.w\"\n [style.--pw-fit-h]=\"pageFit.h\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\" [style.background-color]=\"pageBackgroundOf(leavingPage)\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (isBranchHidden(placement.panelId)) {\n <div class=\"branch-placeholder\" aria-hidden=\"true\"></div>\n } @else if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationStatic]=\"true\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [anchorTailTip]=\"true\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [style.background-color]=\"pageBackgroundOf(page)\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [class.panel-branch-hidden]=\"isBranchHidden(placement.panelId)\"\n [attr.aria-hidden]=\"isBranchHidden(placement.panelId) ? 'true' : null\"\n [attr.tabindex]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"isBranchHidden(placement.panelId) || panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (isBranchHidden(placement.panelId)) {\n <!-- Not on the reader's path (yet): a grey box keeps the page's shape. -->\n <div class=\"branch-placeholder\"></div>\n } @else if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n [pwPanelAnimation]=\"panelData.animations\"\n [pwPanelAnimationKey]=\"placement.panelId\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <div class=\"pw-panel-camera\">\n @if (isLocked(panelData, placement.panelId)) {\n <pw-locked-panel-placeholder\n [actionable]=\"true\"\n (activate)=\"lockedPanelActivate.emit(placement.panelId)\"></pw-locked-panel-placeholder>\n } @else {\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [bubbles]=\"panelData.speechBubbles ?? []\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getPageReadingScale(placement)\"\n [anchorTailTip]=\"true\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel' || (viewMode === 'page' && page)) {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Previous page' : 'Previous panel'\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Next page' : 'Next panel'\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#1a1a1a;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:transparent;width:1280px;height:720px;max-width:100%;max-height:100%}.pw-panel-camera{position:absolute;inset:0}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:0;box-sizing:border-box;container-type:size}.page-canvas{position:relative;width:100%;aspect-ratio:var(--pw-page-aspect, 16 / 9);background:#1a1a1a;overflow:hidden}.page-stage{position:relative;width:min(100cqw,100cqh * var(--pw-stage-aspect, var(--pw-page-aspect, 1.7778)));aspect-ratio:var(--pw-stage-aspect, var(--pw-page-aspect, 16 / 9));flex:none;overflow:hidden}.page-stage .page-canvas{position:absolute;left:calc(-100% * var(--pw-fit-x, 0) / var(--pw-fit-w, 1));top:calc(-100% * var(--pw-fit-y, 0) / var(--pw-fit-h, 1));width:calc(100% / var(--pw-fit-w, 1));height:calc(100% / var(--pw-fit-h, 1));max-width:none;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.branch-placeholder{position:absolute;inset:0;background:#c9ccd1;border-radius:inherit}.viewport-page .panel-container.panel-branch-hidden{pointer-events:none}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"] }]
|
|
14401
14817
|
}], propDecorators: { panel: [{
|
|
14402
14818
|
type: Input
|
|
14403
14819
|
}], page: [{
|
|
@@ -15603,7 +16019,7 @@ class CanvasStageComponent {
|
|
|
15603
16019
|
return Math.hypot(points[1].x - points[0].x, points[1].y - points[0].y);
|
|
15604
16020
|
}
|
|
15605
16021
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CanvasStageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15606
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CanvasStageComponent, isStandalone: true, selector: "pw-canvas-stage", inputs: { canvas: "canvas", panels: "panels", graph: "graph", currentPanelId: "currentPanelId", visitedPanelIds: "visitedPanelIds", locale: "locale", variableContext: "variableContext", speechEnabled: "speechEnabled", workBalloonConfig: "workBalloonConfig", characters: "characters", reducedMotion: "reducedMotion", interactive: "interactive", preload: "preload" }, outputs: { panelTap: "panelTap", navigateNext: "navigateNext", navigatePrevious: "navigatePrevious", viewportClick: "viewportClick", hotspotActivate: "hotspotActivate", cameraChange: "cameraChange" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"canvas-viewport\"\n [class.reduced-motion]=\"reducedMotion\"\n [style]=\"backgroundStyle()\"\n role=\"region\"\n aria-label=\"Canvas viewport\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (wheel)=\"onWheel($event)\">\n\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\n\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\n @if (worldBackgroundStyle(); as bgStyle) {\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\n }\n\n <!-- Non-panel set dressing -->\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\n <img\n class=\"canvas-decoration\"\n [src]=\"assetUrl(decoration.assetId)\"\n [style]=\"decorationStyle(decoration)\"\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"lazy\" />\n }\n\n <!-- Placed panels (virtualized) -->\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\n <div\n class=\"canvas-panel\"\n [class.current]=\"isCurrent(entry.panelId)\"\n [class.unrevealed]=\"!entry.revealed\"\n [style]=\"placementStyle(entry)\"\n [attr.data-panel-id]=\"entry.panelId\"\n [pwPanelAnimation]=\"entry.revealed ? entry.panel.animations : null\"\n [pwPanelAnimationKey]=\"entry.panelId\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\n\n @if (entry.revealed) {\n <!-- The panel's artwork; a panel camera move (PanelAnimations) transforms it as one. -->\n <div class=\"pw-panel-camera\">\n @if (isLocked(entry.panel, entry.panelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of entry.panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n viewMode=\"panel\"\n [viewActive]=\"isCurrent(entry.panelId)\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\n [panelId]=\"entry.panelId\">\n </pw-layer-renderer>\n }\n\n <!-- Hotspots are interactive on the CURRENT panel (the one the\n camera frames); other panels show them render-only, and a\n tap there keeps its panel-navigation meaning. -->\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"entry.panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [context]=\"variableContext\"\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\n </pw-hotspots-overlay>\n }\n\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"entry.panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n } @else {\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.pw-panel-camera{position:absolute;inset:0}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "bubbles", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "directive", type: PanelAnimationDirective, selector: "[pwPanelAnimation]", inputs: ["pwPanelAnimation", "pwPanelAnimationKey", "pwPanelAnimationReducedMotion", "pwPanelAnimationStatic"] }, { kind: "component", type: LockedPanelComponent, selector: "pw-locked-panel-placeholder", inputs: ["actionable"], outputs: ["activate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16022
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CanvasStageComponent, isStandalone: true, selector: "pw-canvas-stage", inputs: { canvas: "canvas", panels: "panels", graph: "graph", currentPanelId: "currentPanelId", visitedPanelIds: "visitedPanelIds", locale: "locale", variableContext: "variableContext", speechEnabled: "speechEnabled", workBalloonConfig: "workBalloonConfig", characters: "characters", reducedMotion: "reducedMotion", interactive: "interactive", preload: "preload" }, outputs: { panelTap: "panelTap", navigateNext: "navigateNext", navigatePrevious: "navigatePrevious", viewportClick: "viewportClick", hotspotActivate: "hotspotActivate", cameraChange: "cameraChange" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"canvas-viewport\"\n [class.reduced-motion]=\"reducedMotion\"\n [style]=\"backgroundStyle()\"\n role=\"region\"\n aria-label=\"Canvas viewport\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (wheel)=\"onWheel($event)\">\n\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\n\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\n @if (worldBackgroundStyle(); as bgStyle) {\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\n }\n\n <!-- Non-panel set dressing -->\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\n <img\n class=\"canvas-decoration\"\n [src]=\"assetUrl(decoration.assetId)\"\n [style]=\"decorationStyle(decoration)\"\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"lazy\" />\n }\n\n <!-- Placed panels (virtualized) -->\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\n <div\n class=\"canvas-panel\"\n [class.current]=\"isCurrent(entry.panelId)\"\n [class.unrevealed]=\"!entry.revealed\"\n [style]=\"placementStyle(entry)\"\n [attr.data-panel-id]=\"entry.panelId\"\n [pwPanelAnimation]=\"entry.revealed ? entry.panel.animations : null\"\n [pwPanelAnimationKey]=\"entry.panelId\"\n [pwPanelAnimationReducedMotion]=\"reducedMotion\"\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\n\n @if (entry.revealed) {\n <!-- The panel's artwork; a panel camera move (PanelAnimations) transforms it as one. -->\n <div class=\"pw-panel-camera\">\n @if (isLocked(entry.panel, entry.panelId)) {\n <pw-locked-panel-placeholder></pw-locked-panel-placeholder>\n } @else {\n @for (layer of entry.panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n viewMode=\"panel\"\n [viewActive]=\"isCurrent(entry.panelId)\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\n [panelId]=\"entry.panelId\">\n </pw-layer-renderer>\n }\n\n <!-- Hotspots are interactive on the CURRENT panel (the one the\n camera frames); other panels show them render-only, and a\n tap there keeps its panel-navigation meaning. -->\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"entry.panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [context]=\"variableContext\"\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\n </pw-hotspots-overlay>\n }\n\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"entry.panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n }\n </div>\n } @else {\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.pw-panel-camera{position:absolute;inset:0}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "anchorTailTip", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "bubbles", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "directive", type: PanelAnimationDirective, selector: "[pwPanelAnimation]", inputs: ["pwPanelAnimation", "pwPanelAnimationKey", "pwPanelAnimationReducedMotion", "pwPanelAnimationStatic"] }, { kind: "component", type: LockedPanelComponent, selector: "pw-locked-panel-placeholder", inputs: ["actionable"], outputs: ["activate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15607
16023
|
}
|
|
15608
16024
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CanvasStageComponent, decorators: [{
|
|
15609
16025
|
type: Component,
|
|
@@ -19423,6 +19839,7 @@ class PlayerShellComponent {
|
|
|
19423
19839
|
this.cdr = inject(ChangeDetectorRef);
|
|
19424
19840
|
this.audioEngine = inject(AudioEngineService);
|
|
19425
19841
|
this.panelAudio = inject(PanelAudioService);
|
|
19842
|
+
this.sequenceAudio = inject(SequenceAudioService);
|
|
19426
19843
|
/**
|
|
19427
19844
|
* Reading order per panel id (1-based, across chapters), computed once per
|
|
19428
19845
|
* manifest. Reported as `panelOrder` on panel_view / funnel events and as
|
|
@@ -19559,6 +19976,7 @@ class PlayerShellComponent {
|
|
|
19559
19976
|
this.autoplayEnabled = false;
|
|
19560
19977
|
this.videoSequencer.reset();
|
|
19561
19978
|
this.panelAudio.stopAll();
|
|
19979
|
+
this.sequenceAudio.stopAll();
|
|
19562
19980
|
this.endAnalyticsSession();
|
|
19563
19981
|
this.analyticsSessionStarted = this.analyticsSessionEnded = this.workCompleteTracked = false;
|
|
19564
19982
|
this.lastTrackedPanelId = undefined;
|
|
@@ -19625,6 +20043,7 @@ class PlayerShellComponent {
|
|
|
19625
20043
|
this.stopAutoplayProgress();
|
|
19626
20044
|
this.videoSequencer.reset();
|
|
19627
20045
|
this.panelAudio.stopAll();
|
|
20046
|
+
this.sequenceAudio.stopAll();
|
|
19628
20047
|
this.destroy$.next();
|
|
19629
20048
|
this.destroy$.complete();
|
|
19630
20049
|
}
|
|
@@ -20182,6 +20601,9 @@ class PlayerShellComponent {
|
|
|
20182
20601
|
(isLockedPanel(panel) || (panelId !== undefined && this.paywallService.isPanelLocked(panelId)));
|
|
20183
20602
|
// Under the cover the story has not started yet: no panel audio.
|
|
20184
20603
|
this.panelAudio.syncPanel(panelId, locked || this.coverVisible ? undefined : panel, this.variableContext);
|
|
20604
|
+
// Timeline music/ambience follows the reader's position on the work line
|
|
20605
|
+
// (silent under the cover and on locked panels too).
|
|
20606
|
+
this.sequenceAudio.syncPanel(locked || this.coverVisible ? undefined : panelId, this.activePageFormat);
|
|
20185
20607
|
}
|
|
20186
20608
|
/**
|
|
20187
20609
|
* Hotspot activated in the viewport (click or keyboard): execute its
|
|
@@ -21573,6 +21995,8 @@ class PlayerShellComponent {
|
|
|
21573
21995
|
return;
|
|
21574
21996
|
}
|
|
21575
21997
|
this.activePageFormat = next;
|
|
21998
|
+
// The format decides the panel line timeline audio is placed on.
|
|
21999
|
+
this.syncPanelAudio();
|
|
21576
22000
|
if (this.viewMode === 'page') {
|
|
21577
22001
|
const page = this.findPageContainingPanel();
|
|
21578
22002
|
if (page && page.id !== this.currentPage?.id) {
|
|
@@ -22464,5 +22888,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
22464
22888
|
* Generated bundle index. Do not edit.
|
|
22465
22889
|
*/
|
|
22466
22890
|
|
|
22467
|
-
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AssetUrlService, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_PAGE_BACKGROUND, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, LOCATION_PARAMS, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, THUMBNAIL_WIDTH, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, chapterOrderFromManifest, coverImageSrc, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, isExtraLocked, isLockedPanel, locationUrl, mergeBalloonConfig, pageAspectRatio, pageFormatOf, pageFormatsOf, pagesForFormat, panelThumbnailSrc, parseLocationSearch, pickPageFormat, rankPageFormats, readingOrderFromManifest, resolveMuted, resolvePageBackground, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, screenClassFor, selectVariant, toEntitlementStatus };
|
|
22891
|
+
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AssetUrlService, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_PAGE_BACKGROUND, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, LOCATION_PARAMS, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SEQUENCE_AUDIO_LEAVE_FADE_MS, SequenceAudioService, SpeechBubblesComponent, THUMBNAIL_WIDTH, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, buildWorkTimeline, busForRole, chapterOrderFromManifest, coverImageSrc, createBalloon, cuesForPanel, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, isExtraLocked, isLockedPanel, locationUrl, mergeBalloonConfig, pageAspectRatio, pageFormatOf, pageFormatsOf, pagesForFormat, panelThumbnailSrc, parseLocationSearch, pickPageFormat, rankPageFormats, readingOrderFromManifest, resolveMuted, resolvePageBackground, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveTrackRanges, resolveVideoConfig, rulesFromManifest, satisfiesRule, screenClassFor, selectVariant, toEntitlementStatus };
|
|
22468
22892
|
//# sourceMappingURL=panelwave-player.mjs.map
|