Phase 21B - Story Intelligence Experience Prototype
21
PlotLine/Controllers/DevelopmentController.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PlotLine.Services;
|
||||||
|
|
||||||
|
namespace PlotLine.Controllers;
|
||||||
|
|
||||||
|
[Authorize(Policy = "AdminOnly")]
|
||||||
|
[Route("Development")]
|
||||||
|
public sealed class DevelopmentController(IWebHostEnvironment environment) : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("StoryIntelligenceExperience")]
|
||||||
|
public IActionResult StoryIntelligenceExperience()
|
||||||
|
{
|
||||||
|
if (!environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(StoryIntelligenceExperiencePrototypeData.Build());
|
||||||
|
}
|
||||||
|
}
|
||||||
200
PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
using PlotLine.ViewModels;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public static class StoryIntelligenceExperiencePrototypeData
|
||||||
|
{
|
||||||
|
private const string AssetRoot = "/images/story-intelligence/prototype";
|
||||||
|
|
||||||
|
public static StoryIntelligenceExperiencePrototypeViewModel Build()
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
ProjectName = "Alpha Flame",
|
||||||
|
BookTitle = "The Doweries Manuscript",
|
||||||
|
Scenes =
|
||||||
|
[
|
||||||
|
Scene(
|
||||||
|
"scene-01", 1, "A key turns in the rain", "Beth arrives at The Doweries and finds Maggie's letter waiting where it should not be.",
|
||||||
|
"Detecting scene structure", "Fri 18 Oct", "Beth reaches The Doweries", 8, 1, 12, "42 min",
|
||||||
|
"beth", Location("doweries", "The Doweries", "Rain-darkened estate house", "location-doweries.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Searching", 100, "character-beth.svg"), ("maggie", "Maggie", "Absent presence", "Remembered", 42, "character-maggie.svg"), ("graham", "Graham", "Name in the letter", "Unseen", 26, "character-graham.svg")),
|
||||||
|
Assets(("letter", "Letter", "Unopened evidence", 88, "asset-letter.svg"), ("notebook", "Notebook", "Beth's notes", 54, "asset-notebook.svg")),
|
||||||
|
Relationships(("rel-beth-maggie", "beth", "maggie", "Family tie detected", "grief and loyalty", 72)),
|
||||||
|
Threads(("thread-maggie-secret", "Why did Maggie hide the letter?", "A secret begins around the flat and the missing key.", "mystery")),
|
||||||
|
Observations(("obs-arrival", "The story opens with withheld knowledge.", "The reader knows the letter matters before Beth understands why.", "insight")),
|
||||||
|
Discoveries("Possible character found: Maggie", "Location found: The Doweries", "Asset found: Letter"),
|
||||||
|
Feed("Chapter 1 queued", "Scene boundary detected", "Beth identified as point of view")),
|
||||||
|
Scene(
|
||||||
|
"scene-02", 2, "Maggie's flat", "Inside Maggie's flat, Beth finds signs that someone searched the room before her.",
|
||||||
|
"Reading scenes", "Fri 18 Oct", "Flat searched before Beth arrives", 17, 1, 12, "38 min",
|
||||||
|
"beth", Location("maggie-flat", "Maggie's flat", "A small flat inside The Doweries", "location-flat.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Investigating", 100, "character-beth.svg"), ("maggie", "Maggie", "Absent presence", "Everywhere in the room", 64, "character-maggie.svg"), ("rosie", "Rosie", "Neighbour", "Possible witness", 38, "character-rosie.svg")),
|
||||||
|
Assets(("letter", "Letter", "Hidden in plain sight", 92, "asset-letter.svg"), ("rucksack", "Rucksack", "Recently moved", 72, "asset-rucksack.svg"), ("notebook", "Notebook", "Fresh page torn out", 58, "asset-notebook.svg")),
|
||||||
|
Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship strengthened", "protective grief", 82), ("rel-beth-rosie", "beth", "rosie", "Witness connection", "cautious trust", 45)),
|
||||||
|
Threads(("thread-maggie-secret", "Maggie's missing page", "A torn notebook page suggests Maggie prepared for Beth's arrival.", "mystery"), ("thread-reader-knows", "Reader knows the flat was searched.", "Beth notices the disturbed objects but not yet the pattern.", "secret")),
|
||||||
|
Observations(("obs-flat", "The location narrows from estate to flat.", "PlotDirector treats Maggie's flat as the active location rather than showing the parent estate.", "insight")),
|
||||||
|
Discoveries("Location refined: Maggie's flat", "Possible asset found: Rucksack", "Knowledge thread opened: missing page"),
|
||||||
|
Feed("Scene 2 analysis started", "Flat recognised as child location", "Beth-Maggie relationship evidence increased")),
|
||||||
|
Scene(
|
||||||
|
"scene-03", 3, "Rosie at the landing", "Rosie interrupts Beth and reveals Graham visited Maggie two nights before.",
|
||||||
|
"Reading scenes", "Fri 18 Oct", "Graham placed near Maggie", 26, 2, 12, "34 min",
|
||||||
|
"beth", Location("maggie-flat", "Maggie's flat", "Landing light spilling through the open door", "location-flat.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Questioning", 100, "character-beth.svg"), ("rosie", "Rosie", "Witness", "Nervous but helpful", 76, "character-rosie.svg"), ("graham", "Graham", "Named suspect", "Recently present", 58, "character-graham.svg"), ("maggie", "Maggie", "Absent presence", "Connected to Graham", 44, "character-maggie.svg")),
|
||||||
|
Assets(("letter", "Letter", "Still unread", 70, "asset-letter.svg"), ("notebook", "Notebook", "Confirms missing page", 62, "asset-notebook.svg")),
|
||||||
|
Relationships(("rel-beth-rosie", "beth", "rosie", "Relationship discovered", "cautious trust", 64), ("rel-graham-maggie", "graham", "maggie", "Possible conflict detected", "recent visit", 70)),
|
||||||
|
Threads(("thread-graham", "Why did Graham visit Maggie?", "Rosie's memory puts Graham at the flat before the search.", "question"), ("thread-maggie-secret", "Maggie's missing page", "The missing page may describe Graham's visit.", "mystery")),
|
||||||
|
Observations(("obs-rosie", "A witness changes the centre of gravity.", "Rosie grows because she supplies new causal information.", "insight")),
|
||||||
|
Discoveries("Possible relationship found: Graham and Maggie", "Character relevance increased: Rosie", "Timeline clue found: two nights before"),
|
||||||
|
Feed("Rosie promoted to important scene character", "Graham inferred from dialogue", "Relationship signal found")),
|
||||||
|
Scene(
|
||||||
|
"scene-04", 4, "The red TR6", "Beth spots Graham's red TR6 outside the estate and realises he has returned.",
|
||||||
|
"Extracting assets", "Fri 18 Oct", "Red TR6 returns to The Doweries", 36, 2, 12, "30 min",
|
||||||
|
"beth", Location("doweries", "The Doweries", "Driveway under sodium light", "location-doweries.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Watching", 100, "character-beth.svg"), ("graham", "Graham", "Active threat", "Returned", 82, "character-graham.svg"), ("rosie", "Rosie", "Witness", "Off to the side", 36, "character-rosie.svg")),
|
||||||
|
Assets(("red-tr6", "Red TR6", "Graham's car", 96, "asset-red-tr6.svg"), ("letter", "Letter", "Unopened evidence", 56, "asset-letter.svg"), ("keys", "Keys", "Estate access", 48, "asset-keys.svg")),
|
||||||
|
Relationships(("rel-beth-graham", "beth", "graham", "Possible conflict detected", "avoidance", 78), ("rel-graham-maggie", "graham", "maggie", "Relationship strengthened", "unexplained visit", 74)),
|
||||||
|
Threads(("thread-graham", "Graham returns before Beth can leave.", "The car turns a memory into an immediate threat.", "question")),
|
||||||
|
Observations(("obs-tr6", "An asset becomes narrative evidence.", "The car is not background colour; it proves Graham is physically present.", "insight")),
|
||||||
|
Discoveries("Asset found: Red TR6", "Relationship signal found: Beth and Graham", "Location changed: The Doweries driveway"),
|
||||||
|
Feed("Scene 4 analysed", "Vehicle classified as significant asset", "Conflict confidence increased")),
|
||||||
|
Scene(
|
||||||
|
"scene-05", 5, "Graham on the stairs", "Graham blocks Beth on the stairwell and asks whether she has opened Maggie's letter.",
|
||||||
|
"Inferring relationships", "Fri 18 Oct", "Graham confronts Beth", 47, 3, 12, "26 min",
|
||||||
|
"beth", Location("stairwell", "The Doweries stairwell", "Narrow stairwell between flat and exit", "location-stairwell.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Cornered", 100, "character-beth.svg"), ("graham", "Graham", "Opposing force", "Pressing", 96, "character-graham.svg"), ("maggie", "Maggie", "Absent pressure", "The reason for the question", 46, "character-maggie.svg")),
|
||||||
|
Assets(("letter", "Letter", "Graham wants it", 96, "asset-letter.svg"), ("keys", "Keys", "Exit blocked", 55, "asset-keys.svg")),
|
||||||
|
Relationships(("rel-beth-graham", "beth", "graham", "Relationship changed", "open conflict", 95), ("rel-graham-maggie", "graham", "maggie", "Suspicion strengthened", "knows about letter", 84)),
|
||||||
|
Threads(("thread-letter", "What is in Maggie's letter?", "Graham's question proves the letter contains something he fears.", "mystery"), ("thread-reader-knows", "Reader knows Graham is afraid of the letter.", "Beth now knows the letter has leverage.", "secret")),
|
||||||
|
Observations(("obs-stairs", "The relationship graph tightens.", "The scene is now Beth versus Graham, with the letter acting as pressure point.", "insight")),
|
||||||
|
Discoveries("Relationship changed: Beth and Graham", "Knowledge thread opened: letter contents", "Asset ownership pressure detected: Letter"),
|
||||||
|
Feed("Graham promoted to primary opposing character", "Letter confidence increased", "Conflict classified")),
|
||||||
|
Scene(
|
||||||
|
"scene-06", 6, "Rosie's warning", "Rosie pulls Beth into the laundry room and warns her not to trust Graham's version of Maggie.",
|
||||||
|
"Reading scenes", "Fri 18 Oct", "Rosie contradicts Graham", 58, 3, 12, "22 min",
|
||||||
|
"rosie", Location("laundry", "Laundry room", "Low-lit utility room behind the stairwell", "location-laundry.svg"),
|
||||||
|
Characters(("rosie", "Rosie", "POV character", "Warning Beth", 100, "character-rosie.svg"), ("beth", "Beth", "Protected listener", "Reassessing", 82, "character-beth.svg"), ("graham", "Graham", "Absent threat", "His story is challenged", 58, "character-graham.svg"), ("maggie", "Maggie", "Remembered", "Misrepresented", 42, "character-maggie.svg")),
|
||||||
|
Assets(("notebook", "Notebook", "Rosie saw it", 78, "asset-notebook.svg"), ("letter", "Letter", "Still decisive", 70, "asset-letter.svg")),
|
||||||
|
Relationships(("rel-beth-rosie", "beth", "rosie", "Relationship strengthened", "protective trust", 86), ("rel-rosie-graham", "rosie", "graham", "Possible conflict detected", "fear and mistrust", 74)),
|
||||||
|
Threads(("thread-graham", "Graham's account is unreliable.", "Rosie introduces contradiction and a motive to lie.", "question"), ("thread-letter", "The letter may expose Graham.", "Both Rosie and Graham orbit the same hidden evidence.", "mystery")),
|
||||||
|
Observations(("obs-pov-shift", "The POV changes to Rosie.", "The camera follows the emotional source of new information, not just the protagonist.", "insight")),
|
||||||
|
Discoveries("POV changed: Rosie", "Relationship strengthened: Beth and Rosie", "Contradictory evidence detected"),
|
||||||
|
Feed("Scene 6 started", "POV character changed", "Knowledge thread updated")),
|
||||||
|
Scene(
|
||||||
|
"scene-07", 7, "Notebook page", "Beth reconstructs the torn notebook page and links the missing line to the red TR6.",
|
||||||
|
"Building knowledge", "Fri 18 Oct", "Notebook and car clue connect", 69, 4, 12, "18 min",
|
||||||
|
"beth", Location("maggie-flat", "Maggie's flat", "Desk, lamplight and torn paper edge", "location-flat.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Connecting clues", 100, "character-beth.svg"), ("maggie", "Maggie", "Absent author", "Left a trail", 70, "character-maggie.svg"), ("graham", "Graham", "Implied target", "Named by evidence", 50, "character-graham.svg")),
|
||||||
|
Assets(("notebook", "Notebook", "Torn page reconstructed", 96, "asset-notebook.svg"), ("red-tr6", "Red TR6", "Matches hidden note", 84, "asset-red-tr6.svg"), ("letter", "Letter", "Next evidence", 72, "asset-letter.svg")),
|
||||||
|
Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship strengthened", "Maggie left Beth a trail", 90), ("rel-graham-maggie", "graham", "maggie", "Relationship changed", "direct evidence", 88)),
|
||||||
|
Threads(("thread-letter", "The letter is part of a trail.", "Maggie appears to have staged evidence for Beth to find.", "mystery"), ("thread-tr6", "The car links Graham to the missing page.", "Asset evidence connects two earlier scenes.", "secret")),
|
||||||
|
Observations(("obs-thread", "Knowledge threads begin to braid.", "PlotDirector links asset, relationship and location evidence into one story question.", "insight")),
|
||||||
|
Discoveries("Knowledge thread opened: Red TR6 link", "Relationship updated: Graham and Maggie", "Asset association found: Notebook and Red TR6"),
|
||||||
|
Feed("Scene 7 analysed", "Notebook promoted to key asset", "Cross-scene link inferred")),
|
||||||
|
Scene(
|
||||||
|
"scene-08", 8, "The call", "Beth hears Maggie's recorded message and realises the warning was meant for Rosie too.",
|
||||||
|
"Building knowledge", "Fri 18 Oct", "Recorded message expands the secret", 79, 4, 12, "14 min",
|
||||||
|
"beth", Location("maggie-flat", "Maggie's flat", "Phone speaker glowing on the table", "location-flat.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Listening", 100, "character-beth.svg"), ("maggie", "Maggie", "Voice on recording", "Direct presence", 88, "character-maggie.svg"), ("rosie", "Rosie", "Named ally", "Also warned", 64, "character-rosie.svg"), ("graham", "Graham", "Threat", "Closing in", 54, "character-graham.svg")),
|
||||||
|
Assets(("letter", "Letter", "Names the danger", 86, "asset-letter.svg"), ("phone", "Phone", "Recorded message", 92, "asset-phone.svg"), ("notebook", "Notebook", "Supports warning", 66, "asset-notebook.svg")),
|
||||||
|
Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship changed", "message from beyond absence", 94), ("rel-maggie-rosie", "maggie", "rosie", "Relationship discovered", "trusted ally", 72)),
|
||||||
|
Threads(("thread-letter", "Maggie's warning has two recipients.", "The secret is bigger than Beth's inheritance of the letter.", "mystery"), ("thread-reader-knows", "Reader now knows Rosie was part of Maggie's plan.", "Beth understands Rosie differently.", "secret")),
|
||||||
|
Observations(("obs-message", "An absent character becomes visually central.", "Maggie's portrait grows because the scene gives her direct agency.", "insight")),
|
||||||
|
Discoveries("Relationship discovered: Maggie and Rosie", "Asset found: Phone recording", "Observation created: absent character has agency"),
|
||||||
|
Feed("Scene 8 analysed", "Maggie promoted through recorded evidence", "Knowledge thread expanded")),
|
||||||
|
Scene(
|
||||||
|
"scene-09", 9, "Car park exit", "Beth and Rosie try to leave, but the red TR6 is parked across the only exit.",
|
||||||
|
"Inferring continuity", "Fri 18 Oct", "Exit blocked", 89, 5, 12, "9 min",
|
||||||
|
"beth", Location("car-park", "The Doweries car park", "Wet tarmac and one blocked exit", "location-car-park.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Deciding", 100, "character-beth.svg"), ("rosie", "Rosie", "Ally", "At Beth's side", 84, "character-rosie.svg"), ("graham", "Graham", "Threat", "Controls the exit", 78, "character-graham.svg")),
|
||||||
|
Assets(("red-tr6", "Red TR6", "Blocks escape", 98, "asset-red-tr6.svg"), ("letter", "Letter", "Motive for pursuit", 74, "asset-letter.svg"), ("keys", "Keys", "Possible escape route", 62, "asset-keys.svg")),
|
||||||
|
Relationships(("rel-beth-rosie", "beth", "rosie", "Relationship strengthened", "active alliance", 92), ("rel-beth-graham", "beth", "graham", "Relationship intensified", "physical obstruction", 98)),
|
||||||
|
Threads(("thread-exit", "How do Beth and Rosie leave?", "The location and asset combine into an immediate continuity problem.", "question"), ("thread-letter", "Graham's pursuit confirms the letter's value.", "The hidden evidence now drives action.", "mystery")),
|
||||||
|
Observations(("obs-blocked", "Location, asset and relationship align.", "The car park is important because the red TR6 changes what characters can do.", "insight")),
|
||||||
|
Discoveries("Continuity observation: exit blocked", "Relationship intensified: Beth and Graham", "Asset state changed: Red TR6 blocks exit"),
|
||||||
|
Feed("Scene 9 analysed", "Location changed to car park", "Asset movement inferred")),
|
||||||
|
Scene(
|
||||||
|
"scene-10", 10, "The opened letter", "Beth opens Maggie's letter and the hidden pattern around Graham finally becomes explicit.",
|
||||||
|
"Preparing review", "Fri 18 Oct", "Letter reveals Graham's motive", 100, 5, 12, "Ready to review",
|
||||||
|
"beth", Location("car-park", "The Doweries car park", "Car lights, rain and an opened letter", "location-car-park.svg"),
|
||||||
|
Characters(("beth", "Beth", "POV character", "Understands", 100, "character-beth.svg"), ("maggie", "Maggie", "Truth teller", "Her plan revealed", 90, "character-maggie.svg"), ("rosie", "Rosie", "Ally", "Confirmed", 78, "character-rosie.svg"), ("graham", "Graham", "Exposed", "Motive visible", 74, "character-graham.svg")),
|
||||||
|
Assets(("letter", "Letter", "Opened evidence", 100, "asset-letter.svg"), ("red-tr6", "Red TR6", "Proof of presence", 76, "asset-red-tr6.svg"), ("notebook", "Notebook", "Corroborates letter", 68, "asset-notebook.svg")),
|
||||||
|
Relationships(("rel-beth-maggie", "beth", "maggie", "Relationship resolved", "trusted inheritance", 96), ("rel-beth-rosie", "beth", "rosie", "Relationship confirmed", "alliance", 90), ("rel-beth-graham", "beth", "graham", "Conflict confirmed", "motive exposed", 98)),
|
||||||
|
Threads(("thread-letter", "Maggie's letter explains Graham's motive.", "The central question becomes review-ready.", "mystery"), ("thread-tr6", "The Red TR6 evidence is confirmed.", "The car, notebook and letter point to the same conclusion.", "secret")),
|
||||||
|
Observations(("obs-complete", "PlotDirector has a coherent story map for this sequence.", "Characters, location, assets and relationships now tell one readable story.", "insight")),
|
||||||
|
Discoveries("Analysis complete: 10 scenes ready for review", "Relationship confirmed: Beth and Rosie", "Knowledge thread resolved: Letter contents"),
|
||||||
|
Feed("Scene 10 analysed", "Chapter sequence ready", "Review data prepared"))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
private static StoryIntelligenceExperienceSceneState Scene(
|
||||||
|
string id,
|
||||||
|
int number,
|
||||||
|
string title,
|
||||||
|
string summary,
|
||||||
|
string stage,
|
||||||
|
string timelineLabel,
|
||||||
|
string timelineEvent,
|
||||||
|
int progress,
|
||||||
|
int chapters,
|
||||||
|
int sceneTotal,
|
||||||
|
string remaining,
|
||||||
|
string pov,
|
||||||
|
StoryIntelligenceExperienceLocationState location,
|
||||||
|
IReadOnlyList<StoryIntelligenceExperienceCharacterState> characters,
|
||||||
|
IReadOnlyList<StoryIntelligenceExperienceAssetState> assets,
|
||||||
|
IReadOnlyList<StoryIntelligenceExperienceRelationshipState> relationships,
|
||||||
|
IReadOnlyList<StoryIntelligenceExperienceTextItem> threads,
|
||||||
|
IReadOnlyList<StoryIntelligenceExperienceTextItem> observations,
|
||||||
|
IReadOnlyList<string> discoveries,
|
||||||
|
IReadOnlyList<string> feed)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
SceneNumber = number,
|
||||||
|
Title = title,
|
||||||
|
Summary = summary,
|
||||||
|
Stage = stage,
|
||||||
|
TimelineLabel = timelineLabel,
|
||||||
|
TimelineEvent = timelineEvent,
|
||||||
|
ProgressPercent = progress,
|
||||||
|
ChaptersAnalysed = chapters,
|
||||||
|
ChapterTotal = 5,
|
||||||
|
ScenesAnalysed = number,
|
||||||
|
SceneTotal = sceneTotal,
|
||||||
|
EstimatedRemaining = remaining,
|
||||||
|
PovCharacterId = pov,
|
||||||
|
Location = location,
|
||||||
|
Characters = characters,
|
||||||
|
Assets = assets,
|
||||||
|
Relationships = relationships,
|
||||||
|
KnowledgeThreads = threads,
|
||||||
|
Observations = observations,
|
||||||
|
LatestDiscoveries = discoveries,
|
||||||
|
ActivityFeed = feed
|
||||||
|
};
|
||||||
|
|
||||||
|
private static StoryIntelligenceExperienceLocationState Location(string id, string name, string label, string image)
|
||||||
|
=> new() { Id = id, Name = name, Label = label, ImagePath = $"{AssetRoot}/{image}" };
|
||||||
|
|
||||||
|
private static IReadOnlyList<StoryIntelligenceExperienceCharacterState> Characters(params (string Id, string Name, string Role, string Relevance, int Weight, string Image)[] items)
|
||||||
|
=> items.Select(item => new StoryIntelligenceExperienceCharacterState { Id = item.Id, Name = item.Name, Role = item.Role, Relevance = item.Relevance, Weight = item.Weight, ImagePath = $"{AssetRoot}/{item.Image}" }).ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<StoryIntelligenceExperienceAssetState> Assets(params (string Id, string Name, string Label, int Weight, string Image)[] items)
|
||||||
|
=> items.Select(item => new StoryIntelligenceExperienceAssetState { Id = item.Id, Name = item.Name, Label = item.Label, Weight = item.Weight, ImagePath = $"{AssetRoot}/{item.Image}" }).ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<StoryIntelligenceExperienceRelationshipState> Relationships(params (string Id, string Source, string Target, string Label, string State, int Weight)[] items)
|
||||||
|
=> items.Select(item => new StoryIntelligenceExperienceRelationshipState { Id = item.Id, SourceId = item.Source, TargetId = item.Target, Label = item.Label, State = item.State, Weight = item.Weight }).ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> Threads(params (string Id, string Title, string Detail, string Tone)[] items)
|
||||||
|
=> TextItems(items);
|
||||||
|
|
||||||
|
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> Observations(params (string Id, string Title, string Detail, string Tone)[] items)
|
||||||
|
=> TextItems(items);
|
||||||
|
|
||||||
|
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> TextItems(params (string Id, string Title, string Detail, string Tone)[] items)
|
||||||
|
=> items.Select(item => new StoryIntelligenceExperienceTextItem { Id = item.Id, Title = item.Title, Detail = item.Detail, Tone = item.Tone }).ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> Discoveries(params string[] items) => items;
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> Feed(params string[] items) => items;
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
namespace PlotLine.ViewModels;
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExperiencePrototypeViewModel
|
||||||
|
{
|
||||||
|
public string ProjectName { get; init; } = string.Empty;
|
||||||
|
public string BookTitle { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<StoryIntelligenceExperienceSceneState> Scenes { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExperienceSceneState
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public int SceneNumber { get; init; }
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
public string Summary { get; init; } = string.Empty;
|
||||||
|
public string Stage { get; init; } = string.Empty;
|
||||||
|
public string TimelineLabel { get; init; } = string.Empty;
|
||||||
|
public string TimelineEvent { get; init; } = string.Empty;
|
||||||
|
public int ProgressPercent { get; init; }
|
||||||
|
public int ChaptersAnalysed { get; init; }
|
||||||
|
public int ChapterTotal { get; init; }
|
||||||
|
public int ScenesAnalysed { get; init; }
|
||||||
|
public int SceneTotal { get; init; }
|
||||||
|
public string EstimatedRemaining { get; init; } = string.Empty;
|
||||||
|
public string PovCharacterId { get; init; } = string.Empty;
|
||||||
|
public StoryIntelligenceExperienceLocationState Location { get; init; } = new();
|
||||||
|
public IReadOnlyList<StoryIntelligenceExperienceCharacterState> Characters { get; init; } = [];
|
||||||
|
public IReadOnlyList<StoryIntelligenceExperienceAssetState> Assets { get; init; } = [];
|
||||||
|
public IReadOnlyList<StoryIntelligenceExperienceRelationshipState> Relationships { get; init; } = [];
|
||||||
|
public IReadOnlyList<StoryIntelligenceExperienceTextItem> KnowledgeThreads { get; init; } = [];
|
||||||
|
public IReadOnlyList<StoryIntelligenceExperienceTextItem> Observations { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> LatestDiscoveries { get; init; } = [];
|
||||||
|
public IReadOnlyList<string> ActivityFeed { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExperienceCharacterState
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Role { get; init; } = string.Empty;
|
||||||
|
public string Relevance { get; init; } = string.Empty;
|
||||||
|
public int Weight { get; init; }
|
||||||
|
public string ImagePath { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExperienceLocationState
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Label { get; init; } = string.Empty;
|
||||||
|
public string ImagePath { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExperienceAssetState
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Label { get; init; } = string.Empty;
|
||||||
|
public int Weight { get; init; }
|
||||||
|
public string ImagePath { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExperienceRelationshipState
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string SourceId { get; init; } = string.Empty;
|
||||||
|
public string TargetId { get; init; } = string.Empty;
|
||||||
|
public string Label { get; init; } = string.Empty;
|
||||||
|
public string State { get; init; } = string.Empty;
|
||||||
|
public int Weight { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExperienceTextItem
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
public string Detail { get; init; } = string.Empty;
|
||||||
|
public string Tone { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
135
PlotLine/Views/Development/StoryIntelligenceExperience.cshtml
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
@using System.Text.Json
|
||||||
|
@model StoryIntelligenceExperiencePrototypeViewModel
|
||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||||
|
}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Story Intelligence Experience Prototype - PlotDirector</title>
|
||||||
|
<link rel="stylesheet" href="~/css/story-intelligence-experience-prototype.css" asp-append-version="true" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script id="storyExperienceData" type="application/json">@Html.Raw(JsonSerializer.Serialize(Model, jsonOptions))</script>
|
||||||
|
|
||||||
|
<main class="story-exp" data-story-experience>
|
||||||
|
<header class="story-exp-header" aria-label="Story Intelligence status">
|
||||||
|
<div class="story-exp-brand">
|
||||||
|
<strong>PlotDirector</strong>
|
||||||
|
<span>Story Intelligence</span>
|
||||||
|
</div>
|
||||||
|
<div class="story-exp-context">
|
||||||
|
<span>@Model.ProjectName</span>
|
||||||
|
<span>@Model.BookTitle</span>
|
||||||
|
<span data-stage-label>Preparing</span>
|
||||||
|
<span>Scene <strong data-current-scene>1</strong></span>
|
||||||
|
</div>
|
||||||
|
<button class="story-exp-background" type="button">Continue in Background</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="story-exp-shell" aria-label="Story Intelligence visual prototype">
|
||||||
|
<aside class="story-exp-left" aria-label="Analysis progress">
|
||||||
|
<div class="story-exp-progress">
|
||||||
|
<p class="story-exp-eyebrow">Overall progress</p>
|
||||||
|
<div class="story-exp-progress-row">
|
||||||
|
<strong data-progress-percent>0%</strong>
|
||||||
|
<span data-estimated-remaining>Calculating</span>
|
||||||
|
</div>
|
||||||
|
<div class="story-exp-progress-track" aria-hidden="true">
|
||||||
|
<span data-progress-bar></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="story-exp-stats">
|
||||||
|
<div>
|
||||||
|
<dt>Chapters analysed</dt>
|
||||||
|
<dd data-chapter-count>0 of 5</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Scenes analysed</dt>
|
||||||
|
<dd data-scene-count>0 of 12</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Current stage</dt>
|
||||||
|
<dd data-current-stage>Preparing</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<section class="story-exp-panel" aria-labelledby="latest-discoveries-title">
|
||||||
|
<h2 id="latest-discoveries-title">Latest discoveries</h2>
|
||||||
|
<ul class="story-exp-list" data-discoveries></ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="story-exp-panel" aria-labelledby="activity-feed-title">
|
||||||
|
<h2 id="activity-feed-title">Live activity</h2>
|
||||||
|
<ol class="story-exp-feed" data-activity-feed></ol>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="story-exp-stage" aria-labelledby="living-story-title">
|
||||||
|
<div class="story-exp-stage-copy">
|
||||||
|
<p class="story-exp-eyebrow">Living story view</p>
|
||||||
|
<h1 id="living-story-title" data-scene-title>Reading the manuscript</h1>
|
||||||
|
<p data-scene-summary></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="story-exp-visual" data-visual-stage>
|
||||||
|
<svg class="story-exp-connections" data-connections aria-hidden="true"></svg>
|
||||||
|
<div class="story-exp-zone story-exp-zone--characters" aria-label="Characters">
|
||||||
|
<p class="story-exp-zone-label">Characters</p>
|
||||||
|
<div class="story-exp-characters" data-characters></div>
|
||||||
|
</div>
|
||||||
|
<div class="story-exp-zone story-exp-zone--location" aria-label="Active location">
|
||||||
|
<p class="story-exp-zone-label">Location</p>
|
||||||
|
<div class="story-exp-location" data-location></div>
|
||||||
|
</div>
|
||||||
|
<div class="story-exp-zone story-exp-zone--assets" aria-label="Significant assets">
|
||||||
|
<p class="story-exp-zone-label">Assets</p>
|
||||||
|
<div class="story-exp-assets" data-assets></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="story-exp-ribbon-wrap" aria-label="Recent scenes and timeline">
|
||||||
|
<div class="story-exp-ribbon" data-scene-ribbon></div>
|
||||||
|
<div class="story-exp-timeline" data-timeline></div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="story-exp-right" aria-label="Story understanding">
|
||||||
|
<section class="story-exp-panel" aria-labelledby="relationships-title">
|
||||||
|
<h2 id="relationships-title">Relationship changes</h2>
|
||||||
|
<div class="story-exp-insight-list" data-relationships></div>
|
||||||
|
</section>
|
||||||
|
<section class="story-exp-panel" aria-labelledby="knowledge-title">
|
||||||
|
<h2 id="knowledge-title">Knowledge threads</h2>
|
||||||
|
<div class="story-exp-insight-list" data-knowledge></div>
|
||||||
|
</section>
|
||||||
|
<section class="story-exp-panel" aria-labelledby="observations-title">
|
||||||
|
<h2 id="observations-title">Story observations</h2>
|
||||||
|
<div class="story-exp-insight-list" data-observations></div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="story-exp-controls" aria-label="Prototype controls">
|
||||||
|
<div>
|
||||||
|
<span>Prototype controls</span>
|
||||||
|
<small>Simulation only</small>
|
||||||
|
</div>
|
||||||
|
<button type="button" data-control="restart">Restart</button>
|
||||||
|
<button type="button" data-control="previous">Previous</button>
|
||||||
|
<button type="button" data-control="play">Pause</button>
|
||||||
|
<button type="button" data-control="next">Next</button>
|
||||||
|
<label>
|
||||||
|
Speed
|
||||||
|
<input type="range" min="3500" max="10000" step="500" value="6200" data-speed-control />
|
||||||
|
</label>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="~/js/story-intelligence-experience-prototype.js" asp-append-version="true"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
721
PlotLine/wwwroot/css/story-intelligence-experience-prototype.css
Normal file
@ -0,0 +1,721 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--story-bg: #07111f;
|
||||||
|
--story-bg-deep: #030813;
|
||||||
|
--story-panel: rgba(10, 22, 39, 0.74);
|
||||||
|
--story-panel-strong: rgba(14, 30, 52, 0.9);
|
||||||
|
--story-line: rgba(151, 185, 218, 0.18);
|
||||||
|
--story-line-strong: rgba(169, 205, 240, 0.34);
|
||||||
|
--story-text: #f3f8ff;
|
||||||
|
--story-muted: #9eb1c7;
|
||||||
|
--story-soft: #c4d6eb;
|
||||||
|
--story-gold: #d7a85d;
|
||||||
|
--story-gold-soft: rgba(215, 168, 93, 0.18);
|
||||||
|
--story-blue: #6db5ff;
|
||||||
|
--story-green: #79d2a6;
|
||||||
|
--story-rose: #e38ca8;
|
||||||
|
--story-radius: 18px;
|
||||||
|
--story-shadow: 0 28px 80px rgba(0, 0, 0, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 18%, rgba(47, 106, 168, 0.25), transparent 28rem),
|
||||||
|
radial-gradient(circle at 90% 80%, rgba(215, 168, 93, 0.12), transparent 22rem),
|
||||||
|
linear-gradient(145deg, var(--story-bg-deep), var(--story-bg) 48%, #101725);
|
||||||
|
color: var(--story-text);
|
||||||
|
font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible,
|
||||||
|
input:focus-visible {
|
||||||
|
outline: 2px solid var(--story-blue);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 70px minmax(0, 1fr);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -2;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255, 255, 255, 0.026) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
|
||||||
|
background-size: 64px 64px;
|
||||||
|
mask-image: radial-gradient(circle at 50% 50%, black, transparent 75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(210px, 0.65fr) minmax(460px, 1.35fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 22px;
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-bottom: 1px solid var(--story-line);
|
||||||
|
background: rgba(4, 10, 20, 0.68);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-brand strong {
|
||||||
|
font-size: 1.12rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-brand span,
|
||||||
|
.story-exp-context span,
|
||||||
|
.story-exp-eyebrow,
|
||||||
|
.story-exp-zone-label,
|
||||||
|
.story-exp-controls span {
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-context {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-context span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-background,
|
||||||
|
.story-exp-controls button {
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid rgba(215, 168, 93, 0.46);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0 14px;
|
||||||
|
background: rgba(215, 168, 93, 0.13);
|
||||||
|
color: #fff5dc;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-background:hover,
|
||||||
|
.story-exp-controls button:hover {
|
||||||
|
background: rgba(215, 168, 93, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(250px, 0.72fr) minmax(620px, 1.92fr) minmax(280px, 0.86fr);
|
||||||
|
gap: 18px;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 18px 24px 84px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-left,
|
||||||
|
.story-exp-right {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 14px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-left,
|
||||||
|
.story-exp-right,
|
||||||
|
.story-exp-stage {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-progress,
|
||||||
|
.story-exp-panel,
|
||||||
|
.story-exp-stats {
|
||||||
|
border: 1px solid var(--story-line);
|
||||||
|
border-radius: var(--story-radius);
|
||||||
|
background: linear-gradient(180deg, rgba(18, 34, 58, 0.84), rgba(7, 17, 31, 0.68));
|
||||||
|
box-shadow: var(--story-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-progress,
|
||||||
|
.story-exp-panel {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-progress-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 4px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-progress-row strong {
|
||||||
|
font-size: clamp(2.1rem, 4vw, 3.2rem);
|
||||||
|
line-height: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-progress-row span {
|
||||||
|
color: var(--story-soft);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-progress-track {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-progress-track span {
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, var(--story-blue), var(--story-gold));
|
||||||
|
box-shadow: 0 0 22px rgba(109, 181, 255, 0.42);
|
||||||
|
transition: width 900ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stats {
|
||||||
|
display: grid;
|
||||||
|
gap: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stats div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stats dt {
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stats dd {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--story-text);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-panel h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-list,
|
||||||
|
.story-exp-feed {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-list li,
|
||||||
|
.story-exp-feed li,
|
||||||
|
.story-exp-insight {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 11px;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
color: var(--story-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-feed li {
|
||||||
|
color: var(--story-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stage {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) 160px;
|
||||||
|
gap: 14px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stage-copy {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr minmax(240px, 0.72fr);
|
||||||
|
align-items: end;
|
||||||
|
gap: 22px;
|
||||||
|
min-height: 86px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stage-copy h1 {
|
||||||
|
grid-column: 1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.8rem, 3vw, 3.8rem);
|
||||||
|
line-height: 1.02;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stage-copy p:last-child {
|
||||||
|
grid-column: 2;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--story-soft);
|
||||||
|
font-size: clamp(0.9rem, 1.2vw, 1.05rem);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-visual {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(360px, 1.1fr) minmax(260px, 0.92fr) minmax(230px, 0.68fr);
|
||||||
|
grid-template-rows: minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--story-line);
|
||||||
|
border-radius: 26px;
|
||||||
|
padding: 18px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 28% 34%, rgba(109, 181, 255, 0.13), transparent 20rem),
|
||||||
|
radial-gradient(circle at 69% 45%, rgba(215, 168, 93, 0.12), transparent 18rem),
|
||||||
|
linear-gradient(180deg, rgba(11, 24, 43, 0.82), rgba(4, 10, 20, 0.72));
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), var(--story-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-connections {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-connection {
|
||||||
|
stroke: rgba(215, 168, 93, 0.48);
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-dasharray: 1;
|
||||||
|
stroke-dashoffset: 1;
|
||||||
|
filter: drop-shadow(0 0 8px rgba(215, 168, 93, 0.28));
|
||||||
|
animation: drawLine 900ms ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-zone {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-zone-label {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-characters {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
width: var(--node-size, 150px);
|
||||||
|
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
||||||
|
opacity: var(--opacity, 1);
|
||||||
|
transition: transform 900ms cubic-bezier(.2, .8, .2, 1), opacity 700ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character.is-pov {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character__image,
|
||||||
|
.story-location__image,
|
||||||
|
.story-asset__image {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--story-line-strong);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character__image {
|
||||||
|
width: var(--node-size, 150px);
|
||||||
|
height: var(--node-size, 150px);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character__image img,
|
||||||
|
.story-location__image img,
|
||||||
|
.story-asset__image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character.is-pov .story-character__image {
|
||||||
|
border-color: rgba(215, 168, 93, 0.82);
|
||||||
|
box-shadow: 0 0 0 6px rgba(215, 168, 93, 0.12), 0 28px 70px rgba(215, 168, 93, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character__text {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character__text strong {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-character__text span {
|
||||||
|
max-width: 150px;
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-location {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
height: calc(100% - 28px);
|
||||||
|
transition: opacity 500ms ease, transform 700ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-location.is-changing {
|
||||||
|
opacity: 0.35;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-location__image {
|
||||||
|
aspect-ratio: 4 / 5;
|
||||||
|
border-radius: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-location h2,
|
||||||
|
.story-location p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-location h2 {
|
||||||
|
font-size: clamp(1.45rem, 2vw, 2.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-location p {
|
||||||
|
color: var(--story-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-assets {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
height: calc(100% - 28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-asset {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 72px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
opacity: var(--opacity, 1);
|
||||||
|
transform: translateY(var(--y, 0)) scale(var(--scale, 1));
|
||||||
|
transition: transform 760ms ease, opacity 560ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-asset__image {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-asset strong,
|
||||||
|
.story-asset span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-asset span {
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-ribbon-wrap {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--story-line);
|
||||||
|
border-radius: 22px;
|
||||||
|
padding: 14px;
|
||||||
|
background: rgba(6, 16, 30, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-ribbon {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 86px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-card {
|
||||||
|
flex: 0 0 130px;
|
||||||
|
display: grid;
|
||||||
|
align-content: space-between;
|
||||||
|
gap: 7px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
color: var(--story-muted);
|
||||||
|
opacity: 0.62;
|
||||||
|
transform: scale(0.92);
|
||||||
|
transition: transform 760ms ease, opacity 760ms ease, background 760ms ease, border-color 760ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-card.is-current {
|
||||||
|
flex-basis: 170px;
|
||||||
|
border-color: rgba(215, 168, 93, 0.58);
|
||||||
|
background: rgba(215, 168, 93, 0.14);
|
||||||
|
color: var(--story-text);
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-card strong {
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-scene-card span {
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-timeline {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--timeline-count, 1), minmax(0, 1fr));
|
||||||
|
gap: 0;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-timeline::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 16px;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, rgba(109, 181, 255, 0.28), rgba(215, 168, 93, 0.68));
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-timeline-marker {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-timeline-marker::before {
|
||||||
|
content: "";
|
||||||
|
z-index: 1;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-top: 11px;
|
||||||
|
border: 2px solid rgba(215, 168, 93, 0.8);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--story-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-timeline-marker.is-current::before {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--story-gold);
|
||||||
|
box-shadow: 0 0 20px rgba(215, 168, 93, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-insight-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-insight {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
transition: opacity 500ms ease, transform 600ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-insight strong {
|
||||||
|
color: var(--story-text);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-insight span {
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-controls {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
bottom: 18px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 9px 10px 9px 14px;
|
||||||
|
background: rgba(4, 10, 20, 0.82);
|
||||||
|
box-shadow: var(--story-shadow);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-controls div {
|
||||||
|
display: grid;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-controls small {
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-controls label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-controls input {
|
||||||
|
width: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drawLine {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
body {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-header,
|
||||||
|
.story-exp-shell,
|
||||||
|
.story-exp-stage-copy,
|
||||||
|
.story-exp-visual {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-shell {
|
||||||
|
padding-bottom: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stage {
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-visual {
|
||||||
|
min-height: 760px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-stage-copy p:last-child {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-context {
|
||||||
|
justify-content: start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-controls {
|
||||||
|
left: 14px;
|
||||||
|
right: 14px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border-radius: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.story-exp-header,
|
||||||
|
.story-exp-shell {
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-ribbon {
|
||||||
|
justify-content: start;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
animation-duration: 1ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 1ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-connection {
|
||||||
|
stroke-dasharray: none;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
|
||||||
|
<rect width="220" height="220" rx="24" fill="#102035"/>
|
||||||
|
<circle cx="84" cy="84" r="34" fill="none" stroke="#d7a85d" stroke-width="12"/>
|
||||||
|
<path d="M108 108l62 62M142 142l18-18M158 158l18-18" stroke="#d7a85d" stroke-width="13" stroke-linecap="round"/>
|
||||||
|
<path d="M55 150c24 20 61 25 94 12" stroke="#8fb4d4" stroke-width="5" fill="none" opacity=".55"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 425 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
|
||||||
|
<rect width="220" height="220" rx="24" fill="#102035"/>
|
||||||
|
<path d="M42 70h136v90H42z" fill="#f0d8a8" stroke="#c49b5b" stroke-width="5"/>
|
||||||
|
<path d="M42 72l68 54 68-54M42 160l54-48M178 160l-54-48" fill="none" stroke="#9d7440" stroke-width="5"/>
|
||||||
|
<circle cx="110" cy="126" r="14" fill="#9c2f2f"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 368 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
|
||||||
|
<rect width="220" height="220" rx="24" fill="#102035"/>
|
||||||
|
<rect x="62" y="36" width="104" height="148" rx="8" fill="#274257" stroke="#b9c9d6" stroke-width="5"/>
|
||||||
|
<path d="M82 66h58M82 92h58M82 118h46M82 144h52" stroke="#d7a85d" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M62 58h-16M62 90h-16M62 122h-16M62 154h-16" stroke="#d7e3ee" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
|
||||||
|
<rect width="220" height="220" rx="24" fill="#102035"/>
|
||||||
|
<rect x="70" y="34" width="80" height="152" rx="16" fill="#111820" stroke="#8fb4d4" stroke-width="6"/>
|
||||||
|
<rect x="82" y="58" width="56" height="92" rx="6" fill="#24445e"/>
|
||||||
|
<circle cx="110" cy="166" r="7" fill="#d7a85d"/>
|
||||||
|
<path d="M92 92c12-11 24-11 36 0M84 78c18-18 34-18 52 0" stroke="#d7a85d" stroke-width="5" fill="none" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 483 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
|
||||||
|
<rect width="220" height="220" rx="24" fill="#102035"/>
|
||||||
|
<path d="M38 118h144l18 24v28H20v-30z" fill="#9f2525"/>
|
||||||
|
<path d="M68 88h78l28 30H44z" fill="#be3a32"/>
|
||||||
|
<rect x="78" y="94" width="34" height="24" fill="#dce9f4" opacity=".75"/>
|
||||||
|
<rect x="118" y="94" width="28" height="24" fill="#dce9f4" opacity=".55"/>
|
||||||
|
<circle cx="58" cy="170" r="20" fill="#050914" stroke="#aabdd0" stroke-width="6"/>
|
||||||
|
<circle cx="162" cy="170" r="20" fill="#050914" stroke="#aabdd0" stroke-width="6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 559 B |
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220">
|
||||||
|
<rect width="220" height="220" rx="24" fill="#102035"/>
|
||||||
|
<path d="M76 80c2-35 66-35 68 0" fill="none" stroke="#d7a85d" stroke-width="9"/>
|
||||||
|
<rect x="58" y="70" width="104" height="112" rx="20" fill="#33495d" stroke="#8aa8bf" stroke-width="5"/>
|
||||||
|
<rect x="78" y="118" width="64" height="42" rx="10" fill="#1c2c3a"/>
|
||||||
|
<path d="M80 94h60" stroke="#d7a85d" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||||
|
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#183a5a"/><stop offset="1" stop-color="#c28b58"/></linearGradient></defs>
|
||||||
|
<rect width="300" height="300" fill="url(#g)"/>
|
||||||
|
<circle cx="150" cy="118" r="62" fill="#f0c7a4"/>
|
||||||
|
<path d="M82 132c5-70 41-94 85-82 34 9 52 36 51 82-28-30-76-34-136 0z" fill="#3b251d"/>
|
||||||
|
<path d="M58 300c14-74 50-111 92-111s78 37 92 111z" fill="#1b2638"/>
|
||||||
|
<path d="M110 157c23 22 57 22 80 0" fill="none" stroke="#8f5b48" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<circle cx="126" cy="120" r="6" fill="#1c2530"/><circle cx="174" cy="120" r="6" fill="#1c2530"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 692 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||||
|
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#1d2330"/><stop offset="1" stop-color="#714033"/></linearGradient></defs>
|
||||||
|
<rect width="300" height="300" fill="url(#g)"/>
|
||||||
|
<circle cx="150" cy="116" r="59" fill="#d7ad91"/>
|
||||||
|
<path d="M91 93c12-39 41-58 76-50 27 6 47 23 54 52-42-15-84-16-130-2z" fill="#2b2423"/>
|
||||||
|
<path d="M58 300c15-72 50-108 92-108s77 36 92 108z" fill="#2a2f38"/>
|
||||||
|
<path d="M119 158c18 5 44 5 62 0" fill="none" stroke="#6f463e" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<circle cx="126" cy="120" r="6" fill="#111820"/><circle cx="174" cy="120" r="6" fill="#111820"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 690 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||||
|
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#11243b"/><stop offset="1" stop-color="#755a8c"/></linearGradient></defs>
|
||||||
|
<rect width="300" height="300" fill="url(#g)"/>
|
||||||
|
<circle cx="150" cy="120" r="60" fill="#e8bea3"/>
|
||||||
|
<path d="M78 126c-2-55 26-92 72-92s74 37 72 92c-23-38-49-52-72-52s-49 14-72 52z" fill="#d8d1c3"/>
|
||||||
|
<path d="M62 300c17-70 52-105 88-105s71 35 88 105z" fill="#473a56"/>
|
||||||
|
<path d="M118 157c19 16 45 16 64 0" fill="none" stroke="#8d5b50" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<circle cx="127" cy="121" r="5" fill="#28313d"/><circle cx="173" cy="121" r="5" fill="#28313d"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 702 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
||||||
|
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#163d48"/><stop offset="1" stop-color="#b45d75"/></linearGradient></defs>
|
||||||
|
<rect width="300" height="300" fill="url(#g)"/>
|
||||||
|
<circle cx="150" cy="120" r="58" fill="#f1c9a8"/>
|
||||||
|
<path d="M84 118c9-52 42-80 80-70 33 9 54 36 55 75-46-28-90-29-135-5z" fill="#8a4f35"/>
|
||||||
|
<path d="M66 300c19-68 51-103 84-103s65 35 84 103z" fill="#1d4f58"/>
|
||||||
|
<path d="M119 158c19 15 43 15 62 0" fill="none" stroke="#8f5b48" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<circle cx="127" cy="121" r="5" fill="#1c2530"/><circle cx="173" cy="121" r="5" fill="#1c2530"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 692 B |
@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
|
||||||
|
<rect width="420" height="520" fill="#07111f"/>
|
||||||
|
<path d="M0 365c120-50 300-50 420 0v155H0z" fill="#1a2430"/>
|
||||||
|
<path d="M70 376h280" stroke="#d7a85d" stroke-width="7" stroke-dasharray="28 20" opacity=".7"/>
|
||||||
|
<path d="M82 300h252l48 52v46H42v-48z" fill="#8e2424"/>
|
||||||
|
<circle cx="114" cy="398" r="34" fill="#050914" stroke="#9ab3c7" stroke-width="7"/>
|
||||||
|
<circle cx="310" cy="398" r="34" fill="#050914" stroke="#9ab3c7" stroke-width="7"/>
|
||||||
|
<rect x="142" y="314" width="92" height="40" fill="#dce9f4" opacity=".7"/>
|
||||||
|
<rect x="250" y="316" width="62" height="38" fill="#dce9f4" opacity=".55"/>
|
||||||
|
<circle cx="58" cy="86" r="34" fill="#d7a85d" opacity=".35"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 730 B |
@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
|
||||||
|
<defs><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#10243d"/><stop offset="1" stop-color="#050914"/></linearGradient></defs>
|
||||||
|
<rect width="420" height="520" fill="url(#sky)"/>
|
||||||
|
<path d="M45 470h330V210L210 95 45 210z" fill="#1b2b3a" stroke="#596f84" stroke-width="5"/>
|
||||||
|
<path d="M80 220h260v250H80z" fill="#243648"/>
|
||||||
|
<path d="M138 470V330h72v140z" fill="#111923"/>
|
||||||
|
<g fill="#d5a65a"><rect x="104" y="250" width="48" height="58"/><rect x="268" y="250" width="48" height="58"/><rect x="218" y="350" width="52" height="64"/></g>
|
||||||
|
<path d="M0 472c95-31 190-31 420 0v48H0z" fill="#07111f"/>
|
||||||
|
<g stroke="#8fb4d4" stroke-width="2" opacity=".35"><path d="M40 30v90"/><path d="M95 10v100"/><path d="M340 22v120"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 816 B |
@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
|
||||||
|
<rect width="420" height="520" fill="#101a2a"/>
|
||||||
|
<rect x="46" y="66" width="328" height="388" rx="18" fill="#29384a"/>
|
||||||
|
<rect x="74" y="96" width="138" height="110" fill="#c79b5a"/>
|
||||||
|
<rect x="234" y="96" width="110" height="230" fill="#172130"/>
|
||||||
|
<rect x="94" y="250" width="112" height="56" fill="#5e4234"/>
|
||||||
|
<rect x="103" y="230" width="84" height="28" fill="#f0d2a5"/>
|
||||||
|
<path d="M72 398c75-36 175-34 276 0" fill="none" stroke="#d7a85d" stroke-width="5" opacity=".45"/>
|
||||||
|
<circle cx="296" cy="388" r="34" fill="#d7a85d" opacity=".28"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 616 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
|
||||||
|
<rect width="420" height="520" fill="#091522"/>
|
||||||
|
<rect x="50" y="82" width="320" height="354" rx="16" fill="#1c3341"/>
|
||||||
|
<circle cx="146" cy="286" r="64" fill="#0e1b25" stroke="#9db7c9" stroke-width="8"/>
|
||||||
|
<circle cx="146" cy="286" r="34" fill="#5e8fa0" opacity=".65"/>
|
||||||
|
<rect x="232" y="222" width="86" height="130" rx="8" fill="#d7a85d" opacity=".72"/>
|
||||||
|
<path d="M80 148h260" stroke="#d8e7f2" stroke-width="5" opacity=".35"/>
|
||||||
|
<path d="M96 164c42 24 86 24 132 0" stroke="#f2d8a4" stroke-width="6" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 590 B |
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 520">
|
||||||
|
<rect width="420" height="520" fill="#07111f"/>
|
||||||
|
<path d="M64 90h292v360H64z" fill="#162333"/>
|
||||||
|
<path d="M86 424h248M108 380h226M130 336h204M152 292h182M174 248h160" stroke="#8da8bf" stroke-width="18" opacity=".55"/>
|
||||||
|
<path d="M246 80v390" stroke="#d7a85d" stroke-width="8" opacity=".7"/>
|
||||||
|
<rect x="270" y="112" width="54" height="92" fill="#c9904f" opacity=".68"/>
|
||||||
|
<path d="M0 520L420 80v440z" fill="#000" opacity=".22"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 501 B |
401
PlotLine/wwwroot/js/story-intelligence-experience-prototype.js
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
(() => {
|
||||||
|
const root = document.querySelector("[data-story-experience]");
|
||||||
|
const dataNode = document.getElementById("storyExperienceData");
|
||||||
|
if (!root || !dataNode) return;
|
||||||
|
|
||||||
|
const model = JSON.parse(dataNode.textContent || "{}");
|
||||||
|
const scenes = model.scenes || [];
|
||||||
|
if (!scenes.length) return;
|
||||||
|
|
||||||
|
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
const state = {
|
||||||
|
index: 0,
|
||||||
|
playing: true,
|
||||||
|
intervalMs: 6200,
|
||||||
|
timer: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const dom = {
|
||||||
|
stageLabel: root.querySelector("[data-stage-label]"),
|
||||||
|
currentScene: root.querySelector("[data-current-scene]"),
|
||||||
|
progressPercent: root.querySelector("[data-progress-percent]"),
|
||||||
|
estimatedRemaining: root.querySelector("[data-estimated-remaining]"),
|
||||||
|
progressBar: root.querySelector("[data-progress-bar]"),
|
||||||
|
chapterCount: root.querySelector("[data-chapter-count]"),
|
||||||
|
sceneCount: root.querySelector("[data-scene-count]"),
|
||||||
|
currentStage: root.querySelector("[data-current-stage]"),
|
||||||
|
discoveries: root.querySelector("[data-discoveries]"),
|
||||||
|
activityFeed: root.querySelector("[data-activity-feed]"),
|
||||||
|
sceneTitle: root.querySelector("[data-scene-title]"),
|
||||||
|
sceneSummary: root.querySelector("[data-scene-summary]"),
|
||||||
|
visualStage: root.querySelector("[data-visual-stage]"),
|
||||||
|
connections: root.querySelector("[data-connections]"),
|
||||||
|
characters: root.querySelector("[data-characters]"),
|
||||||
|
location: root.querySelector("[data-location]"),
|
||||||
|
assets: root.querySelector("[data-assets]"),
|
||||||
|
ribbon: root.querySelector("[data-scene-ribbon]"),
|
||||||
|
timeline: root.querySelector("[data-timeline]"),
|
||||||
|
relationships: root.querySelector("[data-relationships]"),
|
||||||
|
knowledge: root.querySelector("[data-knowledge]"),
|
||||||
|
observations: root.querySelector("[data-observations]"),
|
||||||
|
play: root.querySelector("[data-control='play']"),
|
||||||
|
speed: root.querySelector("[data-speed-control]")
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadSceneState(index) {
|
||||||
|
return scenes[Math.max(0, Math.min(scenes.length - 1, index))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function transitionToNextState(nextIndex) {
|
||||||
|
const previous = loadSceneState(state.index);
|
||||||
|
state.index = Math.max(0, Math.min(scenes.length - 1, nextIndex));
|
||||||
|
const current = loadSceneState(state.index);
|
||||||
|
|
||||||
|
updateProgress(current);
|
||||||
|
updateSceneCopy(current);
|
||||||
|
reconcileCharacters(current, previous);
|
||||||
|
reconcileLocation(current, previous);
|
||||||
|
reconcileAssets(current, previous);
|
||||||
|
reconcileRelationships(current);
|
||||||
|
updateKnowledgeThreads(current);
|
||||||
|
updateSceneRibbon(current);
|
||||||
|
updateTimeline(current);
|
||||||
|
updateFeeds(current);
|
||||||
|
scheduleConnectionDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(scene) {
|
||||||
|
text(dom.stageLabel, scene.stage);
|
||||||
|
text(dom.currentScene, scene.sceneNumber);
|
||||||
|
text(dom.progressPercent, `${scene.progressPercent}%`);
|
||||||
|
text(dom.estimatedRemaining, scene.estimatedRemaining);
|
||||||
|
text(dom.chapterCount, `${scene.chaptersAnalysed} of ${scene.chapterTotal}`);
|
||||||
|
text(dom.sceneCount, `${scene.scenesAnalysed} of ${scene.sceneTotal}`);
|
||||||
|
text(dom.currentStage, scene.stage);
|
||||||
|
if (dom.progressBar) dom.progressBar.style.width = `${scene.progressPercent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSceneCopy(scene) {
|
||||||
|
text(dom.sceneTitle, scene.title);
|
||||||
|
text(dom.sceneSummary, scene.summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileCharacters(scene) {
|
||||||
|
const activeIds = new Set(scene.characters.map((character) => character.id));
|
||||||
|
scene.characters.forEach((character, index) => {
|
||||||
|
let node = dom.characters.querySelector(`[data-character-id="${cssEscape(character.id)}"]`);
|
||||||
|
if (!node) {
|
||||||
|
node = createCharacterNode(character);
|
||||||
|
dom.characters.append(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = characterPosition(character, index, scene.povCharacterId);
|
||||||
|
node.classList.toggle("is-pov", character.id === scene.povCharacterId);
|
||||||
|
node.style.setProperty("--node-size", `${position.size}px`);
|
||||||
|
node.style.setProperty("--x", `${position.x}px`);
|
||||||
|
node.style.setProperty("--y", `${position.y}px`);
|
||||||
|
node.style.setProperty("--scale", String(position.scale));
|
||||||
|
node.style.setProperty("--opacity", String(position.opacity));
|
||||||
|
node.querySelector("img").src = character.imagePath;
|
||||||
|
node.querySelector("img").alt = `${character.name} prototype portrait`;
|
||||||
|
text(node.querySelector("[data-character-name]"), character.name);
|
||||||
|
text(node.querySelector("[data-character-role]"), `${character.role} - ${character.relevance}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.characters.querySelectorAll("[data-character-id]").forEach((node) => {
|
||||||
|
if (!activeIds.has(node.dataset.characterId)) {
|
||||||
|
node.style.setProperty("--opacity", "0");
|
||||||
|
node.style.setProperty("--scale", "0.86");
|
||||||
|
window.setTimeout(() => node.remove(), reducedMotion ? 0 : 620);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileLocation(scene, previous) {
|
||||||
|
const changed = previous?.location?.id !== scene.location.id;
|
||||||
|
if (changed) {
|
||||||
|
dom.location.firstElementChild?.classList.add("is-changing");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
dom.location.innerHTML = "";
|
||||||
|
const node = document.createElement("article");
|
||||||
|
node.className = "story-location";
|
||||||
|
node.dataset.locationId = scene.location.id;
|
||||||
|
node.innerHTML = `
|
||||||
|
<div class="story-location__image"><img alt="${escapeHtml(scene.location.name)} prototype location" /></div>
|
||||||
|
<div>
|
||||||
|
<h2></h2>
|
||||||
|
<p></p>
|
||||||
|
</div>`;
|
||||||
|
node.querySelector("img").src = scene.location.imagePath;
|
||||||
|
text(node.querySelector("h2"), scene.location.name);
|
||||||
|
text(node.querySelector("p"), scene.location.label);
|
||||||
|
dom.location.append(node);
|
||||||
|
}, changed && !reducedMotion ? 260 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileAssets(scene) {
|
||||||
|
const activeIds = new Set(scene.assets.map((asset) => asset.id));
|
||||||
|
scene.assets.forEach((asset, index) => {
|
||||||
|
let node = dom.assets.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`);
|
||||||
|
if (!node) {
|
||||||
|
node = createAssetNode(asset);
|
||||||
|
dom.assets.append(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.style.setProperty("--y", `${(index - 1) * 2}px`);
|
||||||
|
node.style.setProperty("--scale", String(0.92 + Math.min(asset.weight, 100) / 500));
|
||||||
|
node.style.setProperty("--opacity", "1");
|
||||||
|
node.querySelector("img").src = asset.imagePath;
|
||||||
|
node.querySelector("img").alt = `${asset.name} prototype asset`;
|
||||||
|
text(node.querySelector("[data-asset-name]"), asset.name);
|
||||||
|
text(node.querySelector("[data-asset-label]"), asset.label);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.assets.querySelectorAll("[data-asset-id]").forEach((node) => {
|
||||||
|
if (!activeIds.has(node.dataset.assetId)) {
|
||||||
|
node.style.setProperty("--opacity", "0");
|
||||||
|
node.style.setProperty("--scale", "0.8");
|
||||||
|
window.setTimeout(() => node.remove(), reducedMotion ? 0 : 520);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileRelationships(scene) {
|
||||||
|
renderInsights(dom.relationships, scene.relationships.map((relationship) => ({
|
||||||
|
id: relationship.id,
|
||||||
|
title: relationship.label,
|
||||||
|
detail: relationship.state,
|
||||||
|
tone: relationship.weight >= 90 ? "confirmed" : relationship.weight >= 70 ? "strengthening" : "emerging"
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateKnowledgeThreads(scene) {
|
||||||
|
renderInsights(dom.knowledge, scene.knowledgeThreads);
|
||||||
|
renderInsights(dom.observations, scene.observations);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
dom.ribbon.innerHTML = "";
|
||||||
|
visible.forEach((item) => {
|
||||||
|
const card = document.createElement("article");
|
||||||
|
card.className = "story-scene-card";
|
||||||
|
card.classList.toggle("is-current", item.id === scene.id);
|
||||||
|
card.innerHTML = `<span>Scene ${item.sceneNumber}</span><strong></strong><small></small>`;
|
||||||
|
text(card.querySelector("strong"), item.title);
|
||||||
|
text(card.querySelector("small"), item.timelineEvent);
|
||||||
|
dom.ribbon.append(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimeline(scene) {
|
||||||
|
const completed = scenes.slice(0, state.index + 1);
|
||||||
|
dom.timeline.style.setProperty("--timeline-count", String(completed.length));
|
||||||
|
dom.timeline.innerHTML = "";
|
||||||
|
completed.forEach((item) => {
|
||||||
|
const marker = document.createElement("div");
|
||||||
|
marker.className = "story-timeline-marker";
|
||||||
|
marker.classList.toggle("is-current", item.id === scene.id);
|
||||||
|
marker.innerHTML = `<span></span>`;
|
||||||
|
text(marker.querySelector("span"), `${item.timelineLabel} - S${item.sceneNumber}`);
|
||||||
|
dom.timeline.append(marker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFeeds(scene) {
|
||||||
|
renderList(dom.discoveries, scene.latestDiscoveries);
|
||||||
|
renderList(dom.activityFeed, scene.activityFeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInsights(container, items) {
|
||||||
|
const activeIds = new Set(items.map((item) => item.id));
|
||||||
|
items.forEach((item) => {
|
||||||
|
let node = container.querySelector(`[data-insight-id="${cssEscape(item.id)}"]`);
|
||||||
|
if (!node) {
|
||||||
|
node = document.createElement("article");
|
||||||
|
node.className = "story-exp-insight";
|
||||||
|
node.dataset.insightId = item.id;
|
||||||
|
node.innerHTML = `<span></span><strong></strong><p></p>`;
|
||||||
|
container.append(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.dataset.tone = item.tone || "";
|
||||||
|
text(node.querySelector("span"), item.tone || "Signal");
|
||||||
|
text(node.querySelector("strong"), item.title);
|
||||||
|
text(node.querySelector("p"), item.detail);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.querySelectorAll("[data-insight-id]").forEach((node) => {
|
||||||
|
if (!activeIds.has(node.dataset.insightId)) {
|
||||||
|
node.style.opacity = "0";
|
||||||
|
node.style.transform = "translateY(8px)";
|
||||||
|
window.setTimeout(() => node.remove(), reducedMotion ? 0 : 360);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(container, items) {
|
||||||
|
container.innerHTML = "";
|
||||||
|
items.forEach((item) => {
|
||||||
|
const node = document.createElement(container.tagName === "OL" ? "li" : "li");
|
||||||
|
node.textContent = item;
|
||||||
|
container.append(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleConnectionDraw() {
|
||||||
|
window.requestAnimationFrame(() => drawConnections(loadSceneState(state.index)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawConnections(scene) {
|
||||||
|
if (!dom.connections || !dom.visualStage) return;
|
||||||
|
const stageRect = dom.visualStage.getBoundingClientRect();
|
||||||
|
dom.connections.innerHTML = "";
|
||||||
|
|
||||||
|
scene.relationships.forEach((relationship) => {
|
||||||
|
const source = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.sourceId)}"]`);
|
||||||
|
const target = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.targetId)}"]`);
|
||||||
|
if (!source || !target) return;
|
||||||
|
|
||||||
|
const start = centrePoint(source, stageRect);
|
||||||
|
const end = centrePoint(target, stageRect);
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
const midY = Math.min(start.y, end.y) - 36;
|
||||||
|
path.setAttribute("d", `M ${start.x} ${start.y} C ${start.x} ${midY}, ${end.x} ${midY}, ${end.x} ${end.y}`);
|
||||||
|
path.setAttribute("pathLength", "1");
|
||||||
|
path.setAttribute("class", "story-exp-connection");
|
||||||
|
path.style.strokeWidth = String(1.5 + Math.min(relationship.weight, 100) / 28);
|
||||||
|
dom.connections.append(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pov = dom.visualStage.querySelector(`[data-character-id="${cssEscape(scene.povCharacterId)}"]`);
|
||||||
|
const location = dom.visualStage.querySelector("[data-location-id]");
|
||||||
|
if (pov && location) {
|
||||||
|
const start = centrePoint(pov, stageRect);
|
||||||
|
const end = centrePoint(location, stageRect);
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
path.setAttribute("d", `M ${start.x} ${start.y} C ${start.x + 120} ${start.y + 30}, ${end.x - 110} ${end.y - 30}, ${end.x} ${end.y}`);
|
||||||
|
path.setAttribute("pathLength", "1");
|
||||||
|
path.setAttribute("class", "story-exp-connection");
|
||||||
|
path.style.stroke = "rgba(109, 181, 255, 0.42)";
|
||||||
|
dom.connections.append(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCharacterNode(character) {
|
||||||
|
const node = document.createElement("article");
|
||||||
|
node.className = "story-character";
|
||||||
|
node.dataset.characterId = character.id;
|
||||||
|
node.innerHTML = `
|
||||||
|
<div class="story-character__image"><img loading="eager" /></div>
|
||||||
|
<div class="story-character__text">
|
||||||
|
<strong data-character-name></strong>
|
||||||
|
<span data-character-role></span>
|
||||||
|
</div>`;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAssetNode(asset) {
|
||||||
|
const node = document.createElement("article");
|
||||||
|
node.className = "story-asset";
|
||||||
|
node.dataset.assetId = asset.id;
|
||||||
|
node.innerHTML = `
|
||||||
|
<div class="story-asset__image"><img loading="eager" /></div>
|
||||||
|
<div>
|
||||||
|
<strong data-asset-name></strong>
|
||||||
|
<span data-asset-label></span>
|
||||||
|
</div>`;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function characterPosition(character, index, povId) {
|
||||||
|
if (character.id === povId) {
|
||||||
|
return { x: 145, y: 130, size: 210, scale: 1, opacity: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const important = character.weight >= 70;
|
||||||
|
const positions = important
|
||||||
|
? [{ x: 18, y: 54 }, { x: 344, y: 44 }, { x: 322, y: 286 }]
|
||||||
|
: [{ x: 34, y: 300 }, { x: 396, y: 240 }, { x: 20, y: 198 }];
|
||||||
|
const slot = positions[index % positions.length];
|
||||||
|
const size = important ? 138 + (character.weight - 70) * 0.9 : 92 + Math.max(0, character.weight - 24) * 0.45;
|
||||||
|
return { x: slot.x, y: slot.y, size: Math.round(size), scale: 1, opacity: important ? 0.94 : 0.72 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function centrePoint(node, parentRect) {
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: rect.left - parentRect.left + rect.width / 2,
|
||||||
|
y: rect.top - parentRect.top + rect.height / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
stopTimer();
|
||||||
|
if (!state.playing) return;
|
||||||
|
state.timer = window.setInterval(() => {
|
||||||
|
const next = state.index >= scenes.length - 1 ? 0 : state.index + 1;
|
||||||
|
transitionToNextState(next);
|
||||||
|
}, state.intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (state.timer) {
|
||||||
|
window.clearInterval(state.timer);
|
||||||
|
state.timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(node, value) {
|
||||||
|
if (node) node.textContent = value ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "").replace(/[&<>"']/g, (match) => ({
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
"\"": """,
|
||||||
|
"'": "'"
|
||||||
|
}[match]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssEscape(value) {
|
||||||
|
return window.CSS?.escape ? window.CSS.escape(value) : String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelector("[data-control='restart']")?.addEventListener("click", () => {
|
||||||
|
transitionToNextState(0);
|
||||||
|
startTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
root.querySelector("[data-control='previous']")?.addEventListener("click", () => {
|
||||||
|
transitionToNextState(state.index <= 0 ? scenes.length - 1 : state.index - 1);
|
||||||
|
startTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
root.querySelector("[data-control='next']")?.addEventListener("click", () => {
|
||||||
|
transitionToNextState(state.index >= scenes.length - 1 ? 0 : state.index + 1);
|
||||||
|
startTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.play?.addEventListener("click", () => {
|
||||||
|
state.playing = !state.playing;
|
||||||
|
text(dom.play, state.playing ? "Pause" : "Play");
|
||||||
|
if (state.playing) startTimer(); else stopTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.speed?.addEventListener("input", () => {
|
||||||
|
state.intervalMs = Number.parseInt(dom.speed.value, 10) || 6200;
|
||||||
|
startTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => scheduleConnectionDraw());
|
||||||
|
|
||||||
|
transitionToNextState(0);
|
||||||
|
startTimer();
|
||||||
|
})();
|
||||||
123
docs/phases/Phase-21B-Story-Intelligence-Experience-Prototype.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# Phase 21B - Story Intelligence Experience Prototype
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Phase 21B creates an isolated, development-only visual prototype for the PlotDirector Story Intelligence Experience. The product specification is `PlotLine/Docs/Features/PlotDirector_Story_Intelligence_Experience_Design_v2.md`; this note records the implementation approach and limits.
|
||||||
|
|
||||||
|
## Route
|
||||||
|
|
||||||
|
- `/Development/StoryIntelligenceExperience`
|
||||||
|
- Implemented by `DevelopmentController`.
|
||||||
|
- Requires the existing `AdminOnly` policy.
|
||||||
|
- Returns `NotFound` unless `IWebHostEnvironment.IsDevelopment()` is true.
|
||||||
|
- Not linked from normal navigation.
|
||||||
|
|
||||||
|
## Files added or changed
|
||||||
|
|
||||||
|
- `PlotLine/Controllers/DevelopmentController.cs`
|
||||||
|
- `PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs`
|
||||||
|
- `PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs`
|
||||||
|
- `PlotLine/Views/Development/StoryIntelligenceExperience.cshtml`
|
||||||
|
- `PlotLine/wwwroot/css/story-intelligence-experience-prototype.css`
|
||||||
|
- `PlotLine/wwwroot/js/story-intelligence-experience-prototype.js`
|
||||||
|
- `PlotLine/wwwroot/images/story-intelligence/prototype/*.svg`
|
||||||
|
- `docs/phases/Phase-21B-Story-Intelligence-Experience-Prototype.md`
|
||||||
|
|
||||||
|
No existing Story Intelligence import, progress, SignalR, worker, persistence or database files were changed.
|
||||||
|
|
||||||
|
## Prototype data structure
|
||||||
|
|
||||||
|
The prototype uses `StoryIntelligenceExperiencePrototypeData.Build()` to provide simulated Alpha Flame-style scene states. Each state includes:
|
||||||
|
|
||||||
|
- Scene number, title, summary and stage
|
||||||
|
- Progress figures and estimated remaining time
|
||||||
|
- POV character
|
||||||
|
- Important and incidental characters
|
||||||
|
- One active location
|
||||||
|
- Significant assets
|
||||||
|
- Relationship changes
|
||||||
|
- Knowledge threads
|
||||||
|
- Story observations
|
||||||
|
- Timeline label/event
|
||||||
|
- Latest discoveries and activity feed
|
||||||
|
|
||||||
|
The Razor view serializes this model into a JSON script tag. The JavaScript renderer consumes that state and reconciles DOM elements by stable prototype IDs.
|
||||||
|
|
||||||
|
## Rendering approach
|
||||||
|
|
||||||
|
The prototype uses a hybrid renderer:
|
||||||
|
|
||||||
|
- HTML/CSS for the full-screen shell, panels, imagery, progress, scene ribbon and timeline.
|
||||||
|
- SVG for relationship and location connection lines.
|
||||||
|
- JavaScript for current-to-next state reconciliation and playback controls.
|
||||||
|
|
||||||
|
Cytoscape was not reused. It remains useful for the existing Relationship Map, but this prototype is not a free-form network graph. The experience needs authored composition around POV, location, assets, narrative rhythm and side-panel understanding, which is better tested here with HTML/CSS/SVG.
|
||||||
|
|
||||||
|
## Animation approach
|
||||||
|
|
||||||
|
The JavaScript updates from current scene state to next scene state and reconciles:
|
||||||
|
|
||||||
|
- Character prominence, position and role changes
|
||||||
|
- POV changes
|
||||||
|
- Location crossfades
|
||||||
|
- Asset appearance/removal/prominence
|
||||||
|
- Relationship line drawing
|
||||||
|
- Insight panel updates
|
||||||
|
- Scene ribbon movement
|
||||||
|
- Timeline growth
|
||||||
|
- Progress/stat changes
|
||||||
|
|
||||||
|
The default automatic playback interval is controlled by the prototype speed slider. Manual controls allow play/pause, previous, next and restart for development review only.
|
||||||
|
|
||||||
|
## Imagery approach
|
||||||
|
|
||||||
|
Prototype-only SVG artwork is stored under:
|
||||||
|
|
||||||
|
`PlotLine/wwwroot/images/story-intelligence/prototype/`
|
||||||
|
|
||||||
|
The artwork includes character portraits, locations and assets. It is intentionally isolated so it can later be replaced by curated canonical imagery and should not be treated as final artwork.
|
||||||
|
|
||||||
|
## Accessibility and reduced motion
|
||||||
|
|
||||||
|
The prototype provides:
|
||||||
|
|
||||||
|
- Ordinary HTML text for progress, status, discoveries and insights.
|
||||||
|
- Button elements for all controls.
|
||||||
|
- Visible focus styles.
|
||||||
|
- Semantic sections and labelled side panels.
|
||||||
|
- `prefers-reduced-motion` support that shortens transitions and disables animated line drawing.
|
||||||
|
|
||||||
|
## Responsive behaviour
|
||||||
|
|
||||||
|
The primary target is desktop/laptop. At narrower widths, the layout stacks and the page becomes scrollable so the current-story view remains usable rather than shrinking into unreadable tiles. A polished mobile version is deferred.
|
||||||
|
|
||||||
|
## Technical limitations
|
||||||
|
|
||||||
|
- Uses simulated data only.
|
||||||
|
- No SignalR or API integration.
|
||||||
|
- No database storage.
|
||||||
|
- No durable event stream.
|
||||||
|
- No real import pipeline connection.
|
||||||
|
- No post-import Story Map.
|
||||||
|
- No frontend package manager or bundler was introduced.
|
||||||
|
- The prototype view includes its own full-screen document shell rather than the standard layout.
|
||||||
|
|
||||||
|
## Items intentionally deferred
|
||||||
|
|
||||||
|
- Real Story Intelligence data binding.
|
||||||
|
- Snapshot/replay endpoints.
|
||||||
|
- Durable provisional discovery IDs.
|
||||||
|
- Event-history tables.
|
||||||
|
- Production navigation.
|
||||||
|
- Sigma.js/Graphology evaluation.
|
||||||
|
- Permanent Story Map route.
|
||||||
|
- Mobile polish.
|
||||||
|
|
||||||
|
## Recommendations for Phase 21C
|
||||||
|
|
||||||
|
Phase 21C should keep the current progress page intact while adding the first real data seam behind the prototype:
|
||||||
|
|
||||||
|
1. Define a read-only snapshot contract shaped like the prototype scene state.
|
||||||
|
2. Map existing `StoryIntelligenceRuns` and `StoryIntelligenceSceneResults` into that contract.
|
||||||
|
3. Add durable discovery/event storage only after the snapshot shape is validated.
|
||||||
|
4. Preserve the same browser-side state reconciliation API so simulated data can be replaced incrementally.
|
||||||