Phase 21J.3 - Illustration Style and Prototype Final Polish
This commit is contained in:
parent
69e4ac8201
commit
ca73c9e6f3
@ -16,7 +16,7 @@ public interface IIllustrationPromptBuilder
|
||||
|
||||
public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
{
|
||||
public const string CurrentTemplateVersion = "21D.2";
|
||||
public const string CurrentTemplateVersion = "21J.3";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@ -73,14 +73,15 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
Requirements:
|
||||
- representative visual identifier only, not a canonical depiction of any manuscript
|
||||
- modern cinematic digital illustration with premium narrative game concept art quality
|
||||
- vivid natural colour, contemporary polished rendering, realistic anatomy and believable proportions
|
||||
- bright balanced lighting, clear facial illumination where faces are present, and strong local contrast
|
||||
- subdued atmospheric background treatment with contemporary polished rendering, realistic anatomy and believable proportions
|
||||
- balanced cinematic lighting, clear facial illumination where faces are present, and strong local contrast
|
||||
- crisp subject definition with strong foreground-background separation and clean readable silhouettes
|
||||
- natural but richer colour with bright blues, reds, greens, and warm skin tones where appropriate
|
||||
- natural controlled colour with rich but restrained accents and warm skin tones where appropriate
|
||||
- polished promotional artwork for a premium story-driven adventure game, not a literal photograph
|
||||
- subtle depth of field, clean modern rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
|
||||
- no visible text, captions, logos, watermarks, UI, signatures, or typography
|
||||
- no anime, cartoon, Pixar-like, caricature, celebrity likeness, or photoreal identity claim
|
||||
- no bright flat studio backdrop, no plain colour-block background, no stock-photo lighting, and no cartoon simplicity
|
||||
- avoid private manuscript details, real names, excerpts, or recognisable copyrighted characters
|
||||
- avoid muddy olive and brown grading, heavy sepia tones, underexposure, excessive darkness, low-contrast lighting, flat colour, antique oil-painting texture, distressed canvas texture, rough brushwork, overly sombre default expressions, backgrounds that merge into the subject, and monochrome or near-monochrome results
|
||||
- square composition that still reads clearly as a small thumbnail
|
||||
@ -100,7 +101,7 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
private static string CategoryRules(string category)
|
||||
=> category switch
|
||||
{
|
||||
IllustrationLibraryCategories.Character => "Square image; head-and-shoulders or upper body; one person only; face-forward enough to read at small size; naturally expressive; cleanly lit face; distinct from background; suitable for circular cropping; avoid dark collars vanishing into black backgrounds.",
|
||||
IllustrationLibraryCategories.Character => "Square image; head-and-shoulders or upper body; one person only; face-forward enough to read at small size; naturally expressive; cleanly lit face; distinct from a subtle atmospheric story background; suitable for circular cropping; avoid bright flat studio backgrounds, plain colour blocks, dark collars vanishing into black backgrounds, and photographic headshot styling.",
|
||||
IllustrationLibraryCategories.Location => "One clear focal environment; vivid and readable; clearly lit and visually inviting even when dramatic; rich colour and depth; strong composition for circular and landscape crops; no prominent people; no text or readable signage; do not default every location to nighttime, rain-soaked, blue-black, ominous, or haunted-looking.",
|
||||
IllustrationLibraryCategories.Asset => "Square image; one dominant centred object; bright clean high-contrast presentation; crisp silhouette; clear separation from background; immediately recognisable at small size; entire object visible where practical.",
|
||||
_ => string.Empty
|
||||
@ -219,7 +220,8 @@ public static class IllustrationStarterBatchDefinition
|
||||
{
|
||||
["libraryPurpose"] = "Story Intelligence representative identifier",
|
||||
["privacy"] = "generic-no-manuscript-data",
|
||||
["style"] = "editorial-stylised-realism"
|
||||
["style"] = "editorial-stylised-realism",
|
||||
["generationSource"] = "Starter batch"
|
||||
};
|
||||
|
||||
var common = new IllustrationGenerationSpecification
|
||||
|
||||
@ -238,23 +238,25 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
Title = $"{age} {presentation.ToLowerInvariant()} character",
|
||||
Description = "Demand-created reusable character illustration for Story Intelligence matching.",
|
||||
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair and {skinTone.ToLowerInvariant()} skin tone range.",
|
||||
Composition = "clear head-and-shoulders portrait with bright readable face, clean background separation, suitable for circular cropping",
|
||||
Mood = "neutral, story-ready, emotionally readable",
|
||||
Composition = "clear head-and-shoulders cinematic portrait with readable face, subtle atmospheric story background, clean separation, suitable for circular cropping",
|
||||
Mood = "neutral, story-ready, emotionally readable, subdued and cinematic",
|
||||
ApparentAgeBand = age,
|
||||
Presentation = presentation,
|
||||
HairColour = hairColour,
|
||||
HairLength = hairLength,
|
||||
SkinTone = skinTone,
|
||||
ClothingEra = "Contemporary",
|
||||
ClothingStyle = "Plain reusable story wardrobe",
|
||||
ClothingStyle = "Understated reusable story wardrobe, not studio fashion styling",
|
||||
Features = features,
|
||||
Palette = ["clear blue", "warm skin tones", "fresh accent colour"],
|
||||
Palette = ["dark navy atmosphere", "warm skin tones", "restrained amber accent"],
|
||||
MetadataTags = ["character", "demand-generated", "story-intelligence"],
|
||||
Metadata = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["libraryPurpose"] = "Story Intelligence demand-generated identifier",
|
||||
["privacy"] = "generic-no-manuscript-data",
|
||||
["style"] = "editorial-stylised-realism",
|
||||
["styleIntent"] = "match-approved-starter-library",
|
||||
["generationSource"] = "Demand generation",
|
||||
["demandKey"] = demand.DemandKey,
|
||||
["requestCount"] = demand.RequestCount.ToString("N0")
|
||||
}
|
||||
|
||||
@ -190,13 +190,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
parsedScenes);
|
||||
var sourceChapterCount = (await sources.ListChaptersAsync(session.BookID)).Count;
|
||||
var bookChapterTotal = Math.Max(1, Math.Max(sourceChapterCount, runSnapshots.Count));
|
||||
var overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isTerminal: false);
|
||||
var completedChapterRuns = runSnapshots.Count(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||
var isTerminal = runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status));
|
||||
var isSuccessfullyComplete = runSnapshots.All(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||
overallProgressPercent = BookProgress(runSnapshots, bookChapterTotal, isSuccessfullyComplete);
|
||||
var sessionSuccessfullyComplete = IsSuccessfulSession(session);
|
||||
var allKnownRunsSuccessfullyComplete = runSnapshots.Count > 0 && runSnapshots.All(snapshot => IsSuccessfulTerminal(snapshot.Run.Status));
|
||||
var isSuccessfullyComplete = sessionSuccessfullyComplete || allKnownRunsSuccessfullyComplete;
|
||||
var isTerminal = isSuccessfullyComplete || IsTerminalSession(session) || runSnapshots.All(snapshot => IsTerminal(snapshot.Run.Status));
|
||||
var calculatedVisualisationProgress = OverallProgress(parsedScenes, totalScenes, isSuccessfullyComplete);
|
||||
var authoritativeImportProgress = BookProgress(runSnapshots, bookChapterTotal, isSuccessfullyComplete);
|
||||
var displayedProgressReason = sessionSuccessfullyComplete
|
||||
? "Successful terminal import session"
|
||||
: allKnownRunsSuccessfullyComplete
|
||||
? "All known chapter runs completed successfully"
|
||||
: "Weighted chapter-run progress";
|
||||
var overallProgressPercent = authoritativeImportProgress;
|
||||
var overallElapsed = SessionElapsed(runSnapshots);
|
||||
var overallRemaining = isTerminal ? "Ready to review" : EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes);
|
||||
var overallRemaining = isSuccessfullyComplete ? "Ready to review" : isTerminal ? "Ready to review" : EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes);
|
||||
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
||||
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
||||
var nextSceneNumber = 1;
|
||||
@ -240,7 +248,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
ChangeToken = SessionChangeToken(session, runSnapshots),
|
||||
IsTerminal = isTerminal,
|
||||
SnapshotMessage = SessionSnapshotMessage(session, runSnapshots),
|
||||
Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, totalScenes, overallProgressPercent, storyMemory),
|
||||
Diagnostics = BuildSessionDiagnostics(
|
||||
session,
|
||||
runSnapshots,
|
||||
activeSnapshot,
|
||||
current,
|
||||
parsedScenes,
|
||||
totalPersistedResults,
|
||||
totalScenes,
|
||||
overallProgressPercent,
|
||||
authoritativeImportProgress,
|
||||
calculatedVisualisationProgress,
|
||||
bookChapterTotal,
|
||||
completedChapterRuns,
|
||||
displayedProgressReason,
|
||||
storyMemory),
|
||||
ProjectName = DisplayTitle(session.ProjectName, "Untitled project"),
|
||||
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
||||
Scenes = scenes
|
||||
@ -761,6 +783,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
|
||||
private static string SessionStatus(StoryIntelligenceBookPipelineState session, IReadOnlyList<RunSnapshot> snapshots)
|
||||
{
|
||||
if (IsSuccessfulSession(session))
|
||||
{
|
||||
return StoryIntelligenceRunStatuses.Completed;
|
||||
}
|
||||
|
||||
if (snapshots.Any(snapshot => !IsTerminal(snapshot.Run.Status)))
|
||||
{
|
||||
return StoryIntelligenceRunStatuses.Running;
|
||||
@ -773,6 +800,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
|
||||
private static string SessionSnapshotMessage(StoryIntelligenceBookPipelineState session, IReadOnlyList<RunSnapshot> snapshots)
|
||||
{
|
||||
if (IsSuccessfulSession(session))
|
||||
{
|
||||
return "Book analysis complete and ready for review.";
|
||||
}
|
||||
|
||||
var active = snapshots.FirstOrDefault(snapshot => !IsTerminal(snapshot.Run.Status));
|
||||
if (active is not null)
|
||||
{
|
||||
@ -1224,6 +1256,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
int totalPersistedResults,
|
||||
int totalScenes,
|
||||
int overallProgressPercent,
|
||||
int authoritativeImportProgress,
|
||||
int calculatedVisualisationProgress,
|
||||
int totalChapters,
|
||||
int completedChapters,
|
||||
string displayedProgressReason,
|
||||
StoryMemory storyMemory)
|
||||
{
|
||||
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
||||
@ -1237,10 +1274,17 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
ActiveChapterRunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
||||
SessionStatus = session.Status,
|
||||
OverallBookProgress = overallProgressPercent,
|
||||
AuthoritativeImportProgress = authoritativeImportProgress,
|
||||
CalculatedVisualisationProgress = calculatedVisualisationProgress,
|
||||
TotalChapters = totalChapters,
|
||||
CompletedChapters = completedChapters,
|
||||
CurrentChapter = activeSnapshot.Run.ChapterNumber.HasValue ? $"Chapter {activeSnapshot.Run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||
CurrentScene = CurrentSceneText(activeSnapshot),
|
||||
CompletedScenes = parsedScenes,
|
||||
TotalScenes = totalScenes,
|
||||
ParsedScenes = parsedScenes,
|
||||
TerminalRunSessionStatus = $"{session.Status}/{SessionStatus(session, snapshots)}",
|
||||
DisplayedProgressReason = displayedProgressReason,
|
||||
ProgressSource = "ImportSession",
|
||||
CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run),
|
||||
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
||||
@ -1281,19 +1325,30 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
var warning = failed.Count == 0
|
||||
? null
|
||||
: $"{failed.Count:N0} scene result(s) skipped because they could not be parsed.";
|
||||
var totalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count);
|
||||
var completedSceneCount = run.CompletedScenes ?? completed.Count;
|
||||
var runProgress = Progress(run, completedSceneCount, totalScenes);
|
||||
var calculatedProgress = OverallProgress(completed.Count, totalScenes, IsSuccessfulTerminal(run.Status));
|
||||
|
||||
return new()
|
||||
{
|
||||
RequestedMode = "real",
|
||||
ActiveChapterRunId = run.StoryIntelligenceRunID,
|
||||
SessionStatus = run.Status,
|
||||
OverallBookProgress = Progress(run, run.CompletedScenes ?? completed.Count, Math.Max(run.TotalDetectedScenes ?? 0, results.Count)),
|
||||
OverallBookProgress = runProgress,
|
||||
AuthoritativeImportProgress = runProgress,
|
||||
CalculatedVisualisationProgress = calculatedProgress,
|
||||
TotalChapters = 1,
|
||||
CompletedChapters = IsSuccessfulTerminal(run.Status) ? 1 : 0,
|
||||
CurrentChapter = run.ChapterNumber.HasValue ? $"Chapter {run.ChapterNumber:0.##}" : "Chapter unknown",
|
||||
CurrentScene = run.TotalDetectedScenes.HasValue
|
||||
? $"Scene {Math.Clamp((run.CompletedScenes ?? completed.Count) + (IsTerminal(run.Status) ? 0 : 1), 1, run.TotalDetectedScenes.Value):N0} of {run.TotalDetectedScenes.Value:N0}"
|
||||
? $"Scene {Math.Clamp(completedSceneCount + (IsTerminal(run.Status) ? 0 : 1), 1, run.TotalDetectedScenes.Value):N0} of {run.TotalDetectedScenes.Value:N0}"
|
||||
: (current is null ? "Waiting" : $"Scene {SceneSortNumber(current):0.##}"),
|
||||
CompletedScenes = run.CompletedScenes ?? completed.Count,
|
||||
TotalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count),
|
||||
CompletedScenes = completedSceneCount,
|
||||
TotalScenes = totalScenes,
|
||||
ParsedScenes = completed.Count,
|
||||
TerminalRunSessionStatus = run.Status,
|
||||
DisplayedProgressReason = IsSuccessfulTerminal(run.Status) ? "Successful terminal chapter run" : "Chapter run scene progress",
|
||||
ProgressSource = "ChapterRun",
|
||||
CurrentBackendStage = BackendStageDisplay(run),
|
||||
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
||||
@ -1397,6 +1452,15 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
private static bool IsSuccessfulTerminal(string status)
|
||||
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings;
|
||||
|
||||
private static bool IsSuccessfulSession(StoryIntelligenceBookPipelineState session)
|
||||
=> string.Equals(session.Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase)
|
||||
&& (string.Equals(session.LastCompletedStage, StoryIntelligencePipelineStages.KnowledgeImport, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(session.CurrentStage, StoryIntelligencePipelineStages.Complete, StringComparison.OrdinalIgnoreCase)
|
||||
|| session.CompletedUtc.HasValue);
|
||||
|
||||
private static bool IsTerminalSession(StoryIntelligenceBookPipelineState session)
|
||||
=> IsSuccessfulSession(session);
|
||||
|
||||
private static int SceneNumber(ParsedSceneResult item)
|
||||
=> item.Scene?.SceneReference?.SceneNumber.HasValue == true ? Convert.ToInt32(item.Scene.SceneReference.SceneNumber.Value) : item.Result.TemporarySceneNumber;
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
Phase 21J.3 demand illustration style consistency marker.
|
||||
|
||||
This does not queue generation or regenerate the library. It marks existing
|
||||
demand-generated character illustrations produced before template 21J.3 so the
|
||||
admin can deliberately regenerate them with the corrected art direction.
|
||||
*/
|
||||
|
||||
UPDATE dbo.IllustrationLibraryItems
|
||||
SET MetadataJson = JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END,
|
||||
'$.generationSource',
|
||||
N'Demand generation'),
|
||||
'$.styleReview',
|
||||
N'Needs 21J.3 regeneration'),
|
||||
'$.styleIntent',
|
||||
N'match-approved-starter-library'),
|
||||
AdminNotes = CONCAT(
|
||||
COALESCE(NULLIF(AdminNotes, N''), N''),
|
||||
CASE WHEN NULLIF(AdminNotes, N'') IS NULL THEN N'' ELSE N' ' END,
|
||||
N'Phase 21J.3: demand-generated before the 21J.3 style template; regenerate with the current template before approval.'),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE StableCode LIKE N'char-demand-%'
|
||||
AND Status IN (N'Generated', N'Approved')
|
||||
AND ISNULL(PromptTemplateVersion, N'') <> N'21J.3'
|
||||
AND (AdminNotes IS NULL OR AdminNotes NOT LIKE N'%Phase 21J.3:%');
|
||||
@ -26,10 +26,17 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
public int? ActiveChapterRunId { get; init; }
|
||||
public string SessionStatus { get; init; } = string.Empty;
|
||||
public int OverallBookProgress { get; init; }
|
||||
public int AuthoritativeImportProgress { get; init; }
|
||||
public int CalculatedVisualisationProgress { get; init; }
|
||||
public int TotalChapters { get; init; }
|
||||
public int CompletedChapters { get; init; }
|
||||
public string CurrentChapter { get; init; } = string.Empty;
|
||||
public string CurrentScene { get; init; } = string.Empty;
|
||||
public int CompletedScenes { get; init; }
|
||||
public int TotalScenes { get; init; }
|
||||
public int ParsedScenes { get; init; }
|
||||
public string TerminalRunSessionStatus { get; init; } = string.Empty;
|
||||
public string DisplayedProgressReason { get; init; } = string.Empty;
|
||||
public string ProgressSource { get; init; } = string.Empty;
|
||||
public string CurrentBackendStage { get; init; } = string.Empty;
|
||||
public int StoryMemoryCharacterCount { get; init; }
|
||||
|
||||
@ -118,6 +118,13 @@
|
||||
@foreach (var item in Model.Items)
|
||||
{
|
||||
var preview = !string.IsNullOrWhiteSpace(item.ThumbnailPath) ? item.ThumbnailPath : item.FilePath;
|
||||
var generationSource = item.StableCode.StartsWith("char-demand-", StringComparison.OrdinalIgnoreCase)
|
||||
? "Demand generation"
|
||||
: item.ParentItemID.HasValue
|
||||
? "Manual regeneration"
|
||||
: IllustrationStarterBatchDefinition.StarterCodes.Contains(item.StableCode)
|
||||
? "Starter batch"
|
||||
: "Manual library item";
|
||||
<article class="project-card">
|
||||
<div class="project-card__body">
|
||||
<div class="d-flex justify-content-between gap-2 align-items-start">
|
||||
@ -137,6 +144,10 @@
|
||||
<dd><code>@item.StableCode</code></dd>
|
||||
<dt>Provider</dt>
|
||||
<dd>@(item.Provider ?? "Not generated") @(item.Model is null ? string.Empty : $" / {item.Model}")</dd>
|
||||
<dt>Template</dt>
|
||||
<dd>@(item.PromptTemplateVersion ?? "Not generated")</dd>
|
||||
<dt>Source</dt>
|
||||
<dd>@generationSource</dd>
|
||||
<dt>Updated</dt>
|
||||
<dd>@item.UpdatedUtc.ToString("yyyy-MM-dd HH:mm") UTC</dd>
|
||||
@if (item.ApprovedUtc.HasValue)
|
||||
|
||||
@ -212,6 +212,22 @@
|
||||
<dt>Overall book progress</dt>
|
||||
<dd data-diagnostic-key="overallBookProgress">@Model.Diagnostics.OverallBookProgress</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Authoritative import progress</dt>
|
||||
<dd data-diagnostic-key="authoritativeImportProgress">@Model.Diagnostics.AuthoritativeImportProgress</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Calculated visualisation progress</dt>
|
||||
<dd data-diagnostic-key="calculatedVisualisationProgress">@Model.Diagnostics.CalculatedVisualisationProgress</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Total chapters</dt>
|
||||
<dd data-diagnostic-key="totalChapters">@Model.Diagnostics.TotalChapters</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Completed chapters</dt>
|
||||
<dd data-diagnostic-key="completedChapters">@Model.Diagnostics.CompletedChapters</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Current chapter</dt>
|
||||
<dd data-diagnostic-key="currentChapter">@Model.Diagnostics.CurrentChapter</dd>
|
||||
@ -228,6 +244,18 @@
|
||||
<dt>Total scenes</dt>
|
||||
<dd data-diagnostic-key="totalScenes">@Model.Diagnostics.TotalScenes</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Parsed scenes</dt>
|
||||
<dd data-diagnostic-key="parsedScenes">@Model.Diagnostics.ParsedScenes</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Terminal run/session status</dt>
|
||||
<dd data-diagnostic-key="terminalRunSessionStatus">@Model.Diagnostics.TerminalRunSessionStatus</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Displayed progress reason</dt>
|
||||
<dd data-diagnostic-key="displayedProgressReason">@Model.Diagnostics.DisplayedProgressReason</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Progress source</dt>
|
||||
<dd data-diagnostic-key="progressSource">@Model.Diagnostics.ProgressSource</dd>
|
||||
|
||||
@ -124,7 +124,7 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-exp-stage {
|
||||
grid-template-rows: auto minmax(0, 1fr) 126px;
|
||||
grid-template-rows: auto minmax(0, 1fr) 170px;
|
||||
}
|
||||
|
||||
.story-exp-stage-copy {
|
||||
@ -133,24 +133,28 @@ input:focus-visible {
|
||||
|
||||
.story-exp-ribbon-wrap {
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
grid-template-rows: minmax(92px, auto) 46px;
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.story-exp-ribbon {
|
||||
min-height: 70px;
|
||||
gap: 12px;
|
||||
min-height: 92px;
|
||||
}
|
||||
|
||||
.story-scene-card {
|
||||
flex-basis: 126px;
|
||||
padding: 8px 10px;
|
||||
flex-basis: clamp(132px, 14vw, 168px);
|
||||
min-height: 84px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.story-scene-card.is-current {
|
||||
flex-basis: 172px;
|
||||
flex-basis: clamp(188px, 18vw, 224px);
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.story-exp-timeline {
|
||||
min-height: 34px;
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.story-exp-controls {
|
||||
@ -160,6 +164,20 @@ input:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) and (min-width: 1181px) {
|
||||
.story-exp-ribbon {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.story-scene-card {
|
||||
flex-basis: clamp(132px, 11vw, 160px);
|
||||
}
|
||||
|
||||
.story-scene-card.is-current {
|
||||
flex-basis: clamp(188px, 15vw, 220px);
|
||||
}
|
||||
}
|
||||
|
||||
.story-exp-brand strong {
|
||||
font-size: 1.12rem;
|
||||
letter-spacing: 0.02em;
|
||||
@ -482,7 +500,7 @@ input:focus-visible {
|
||||
|
||||
.story-exp-stage {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) 154px;
|
||||
grid-template-rows: auto minmax(0, 1fr) 186px;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
@ -917,12 +935,12 @@ input:focus-visible {
|
||||
|
||||
.story-exp-ribbon-wrap {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(76px, auto) 54px;
|
||||
gap: 10px;
|
||||
grid-template-rows: minmax(104px, auto) 56px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(151, 185, 218, 0.1);
|
||||
border-radius: 24px;
|
||||
padding: 12px 14px 14px;
|
||||
padding: 14px 16px 16px;
|
||||
background: rgba(6, 16, 30, 0.54);
|
||||
box-shadow: 0 20px 58px rgba(0, 0, 0, 0.22);
|
||||
backdrop-filter: blur(14px);
|
||||
@ -933,24 +951,25 @@ input:focus-visible {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 76px;
|
||||
gap: 14px;
|
||||
min-height: 104px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.story-scene-card {
|
||||
flex: 0 0 136px;
|
||||
flex: 0 0 clamp(174px, 16vw, 218px);
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
align-content: start;
|
||||
gap: 5px;
|
||||
gap: 7px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 15px;
|
||||
padding: 9px 10px;
|
||||
min-height: 94px;
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
background: rgba(255, 255, 255, 0.032);
|
||||
color: var(--story-muted);
|
||||
opacity: 0.68;
|
||||
transform: scale(0.91) translateY(4px);
|
||||
transform: scale(0.96) translateY(3px);
|
||||
transition: transform 840ms cubic-bezier(.16, .84, .28, 1), opacity 760ms ease, background 760ms ease, border-color 760ms ease, box-shadow 760ms ease;
|
||||
}
|
||||
|
||||
@ -959,12 +978,13 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-scene-card.is-current {
|
||||
flex-basis: 186px;
|
||||
flex-basis: clamp(224px, 20vw, 280px);
|
||||
min-height: 106px;
|
||||
border-color: rgba(215, 168, 93, 0.64);
|
||||
background: linear-gradient(180deg, rgba(215, 168, 93, 0.18), rgba(109, 181, 255, 0.075));
|
||||
color: var(--story-text);
|
||||
opacity: 1;
|
||||
transform: scale(1.04) translateY(0);
|
||||
transform: scale(1.02) translateY(0);
|
||||
box-shadow: 0 18px 42px rgba(215, 168, 93, 0.16);
|
||||
}
|
||||
|
||||
@ -972,28 +992,28 @@ input:focus-visible {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.12;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.16;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.story-scene-card span {
|
||||
color: var(--story-muted);
|
||||
font-size: 0.66rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.story-scene-card small {
|
||||
display: -webkit-box;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--story-muted);
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.1;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.15;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.story-exp-timeline {
|
||||
@ -1001,7 +1021,7 @@ input:focus-visible {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--timeline-count, 1), minmax(0, 1fr));
|
||||
gap: 0;
|
||||
min-height: 54px;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.story-exp-timeline::before {
|
||||
|
||||
@ -352,9 +352,7 @@
|
||||
}
|
||||
|
||||
function updateSceneRibbon(scene) {
|
||||
const currentIndex = state.index;
|
||||
const start = Math.max(0, Math.min(scenes.length - 7, currentIndex - 3));
|
||||
const visible = scenes.slice(start, start + 7);
|
||||
const visible = visibleSceneWindow();
|
||||
const activeIds = new Set(visible.map((item) => item.id));
|
||||
visible.forEach((item) => {
|
||||
let card = dom.ribbon.querySelector(`[data-scene-id="${cssEscape(item.id)}"]`);
|
||||
@ -367,19 +365,19 @@
|
||||
}
|
||||
card.classList.toggle("is-current", item.id === scene.id);
|
||||
card.classList.toggle("is-complete", item.sceneNumber < scene.sceneNumber);
|
||||
text(card.querySelector("span"), `${(item.chapterLabel || "Chapter").toUpperCase()} · SCENE ${item.sceneNumber}`);
|
||||
text(card.querySelector("strong"), compactPhrase(cleanSceneTitle(item.title), item.id === scene.id ? 44 : 30));
|
||||
text(card.querySelector("small"), compactPhrase(item.timelineEvent, 34));
|
||||
text(card.querySelector("span"), `${abbreviateChapter(item.chapterLabel)} · Scene ${item.sceneNumber}`);
|
||||
text(card.querySelector("strong"), compactPhrase(cleanSceneTitle(item.title), 54));
|
||||
text(card.querySelector("small"), compactPhrase(item.timelineEvent || item.summary, 48));
|
||||
dom.ribbon.append(card);
|
||||
});
|
||||
removeInactive(dom.ribbon, "[data-scene-id]", "sceneId", activeIds);
|
||||
}
|
||||
|
||||
function updateTimeline(scene) {
|
||||
const completed = scenes.slice(0, state.index + 1);
|
||||
dom.timeline.style.setProperty("--timeline-count", String(completed.length));
|
||||
const activeIds = new Set(completed.map((item) => item.id));
|
||||
completed.forEach((item) => {
|
||||
const visible = visibleSceneWindow();
|
||||
dom.timeline.style.setProperty("--timeline-count", String(visible.length));
|
||||
const activeIds = new Set(visible.map((item) => item.id));
|
||||
visible.forEach((item) => {
|
||||
let marker = dom.timeline.querySelector(`[data-timeline-id="${cssEscape(item.id)}"]`);
|
||||
if (!marker) {
|
||||
marker = document.createElement("div");
|
||||
@ -389,19 +387,19 @@
|
||||
settleEntering(marker);
|
||||
}
|
||||
marker.classList.toggle("is-current", item.id === scene.id);
|
||||
const shouldLabel = shouldShowTimelineLabel(completed, item, scene);
|
||||
marker.classList.toggle("is-label-hidden", !shouldLabel);
|
||||
marker.classList.toggle("is-complete", item.sceneNumber < scene.sceneNumber);
|
||||
marker.classList.remove("is-label-hidden");
|
||||
text(marker.querySelector("span"), shortTimelineLabel(item));
|
||||
dom.timeline.append(marker);
|
||||
});
|
||||
removeInactive(dom.timeline, "[data-timeline-id]", "timelineId", activeIds);
|
||||
}
|
||||
|
||||
function shouldShowTimelineLabel(items, item, scene) {
|
||||
const index = items.findIndex((candidate) => candidate.id === item.id);
|
||||
if (index === 0 || index === items.length - 1 || item.id === scene.id) return true;
|
||||
const interval = items.length > 12 ? 5 : items.length > 8 ? 4 : 3;
|
||||
return index % interval === 0 && Math.abs(item.sceneNumber - scene.sceneNumber) > 1;
|
||||
function visibleSceneWindow() {
|
||||
const visibleCount = Math.min(5, scenes.length);
|
||||
const currentIndex = Math.max(0, Math.min(state.index, scenes.length - 1));
|
||||
const start = Math.max(0, Math.min(scenes.length - visibleCount, currentIndex - Math.floor(visibleCount / 2)));
|
||||
return scenes.slice(start, start + visibleCount);
|
||||
}
|
||||
|
||||
function shortTimelineLabel(item) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user