fix(startseite): saubere Skyline links, deutlich grössere Agentenbilder
Runde 7. §1 Hintergrund. Die Häuserzeile auf der linken Seite zeigte verwässerte Doppelstrukturen. Drei Ursachen, alle behoben: - Die Fortsetzung lief mit Massstab 2.45 und war damit gegenüber dem Hauptfoto um das 2.4-fache zu gross. Der Faktor ist jetzt aus den Fotos gemessen (Münsterhöhe über der Wasserlinie) und beträgt 1.06. - Die 320 Pixel breite Überblendung mischte zwei verschiedene Häuserreihen zu halbdurchsichtigen Geisterhäusern. In der Häuserzeile wird jetzt praktisch hart getrennt, in Himmel und Wasser weiterhin weich — dort gibt es nichts zu vermischen. - Die Umschaltlinie lag zwischen den Dächern, sodass die Gauben des Hauptfotos als Reihe schwebender Klötzchen im Himmel stehen blieben. Sie liegt jetzt über allen Dächern beider Aufnahmen. Dazu: Helligkeit zeilenweise statt global angeglichen, Himmel der Fortsetzung spaltenweise entlang der echten Dachkante entfernt, fehlende Bildbereiche über die Zeilenfarbe statt über gestreckte Randzeilen aufgefüllt, Häuserzeile nachgeschärft. §2 Agentenbilder. Alle Kreise deutlich grösser, tiefenlogisch gestaffelt und nur noch knapp über der Tischkante statt frei im Raum schwebend. Gegenüber vorher: Ferdi und Bruno 2.11×, Livia und Nora 2.22×, Sina 2.50×, Thomas 3.18×. Ferdi und Bruno rücken näher an die vordere Tischkante. Radius und Höhe stehen jetzt je Platz in der Sitzordnung; die Werte sind gemeinsam durchgerechnet, sodass sich im Bild kein einziges Kreispaar überschneidet. Nebenbei: waagrechter Bildwinkel nach oben begrenzt, damit flache Fenster den Raum nicht zerren; Sockelleiste vor das Wandpaneel gesetzt gegen die ausgefranste Kante. Geprüft bei 1600×900, 1366×768 und 1280×620: sechs Beschriftungen ohne Überlappung, kein Querbalken, Klick auf Ferdi führt auf seine Seite, Menü bündig am rechten Rand. 422 Tests, Lint und Build sauber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e825c04bf
commit
403161283c
5 files changed
+452
-104
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 144 KiB |
@@ -32,10 +32,6 @@ export const ROOM_COLORS = {
|
||||
tableTop: 0x8f7350,
|
||||
tableEdge: 0x6f5637,
|
||||
tableBase: 0x35322f,
|
||||
/** Stuhlschalen. */
|
||||
chair: 0x2f343c,
|
||||
/** Anzüge der Agenten, leicht variiert, damit die Reihe nicht uniform wirkt. */
|
||||
suits: [0x2c3444, 0x36302c, 0x2a3a3c, 0x3a2f36, 0x24303a, 0x1f2836],
|
||||
/** Ring um das Porträt — hebt den Kopf vom Hintergrund ab. */
|
||||
headRing: 0xffffff,
|
||||
/** Ring bei Hover: Gold aus dem Designsystem. */
|
||||
@@ -61,6 +57,12 @@ export const CAMERA = {
|
||||
/** Knapp hinter der vorderen Tischkante. */
|
||||
z: 1.9,
|
||||
fov: 48,
|
||||
/**
|
||||
* Obergrenze des waagrechten Bildwinkels in Grad. Greift nur bei sehr breiten
|
||||
* und gleichzeitig flachen Fenstern; bei üblichen Seitenverhältnissen liegt
|
||||
* der waagrechte Winkel darunter und die Ansicht bleibt unverändert.
|
||||
*/
|
||||
maxHFov: 84,
|
||||
/** Blickziel: leicht unter Augenhöhe, damit die Tischplatte Tiefe zeigt. */
|
||||
lookAt: [0, 1.24, -5.2] as const,
|
||||
/**
|
||||
@@ -82,23 +84,19 @@ export const TABLE = {
|
||||
zFar: -5.75,
|
||||
} as const
|
||||
|
||||
/** Sitzende Personen: Kopfhöhe und Grössen. */
|
||||
/** Sitzende Personen: Höhe der Porträtkreise. */
|
||||
export const SEATED = {
|
||||
/**
|
||||
* Höhe der Porträtkreise über dem Boden.
|
||||
* Luft zwischen Tischplatte und Unterkante des Porträtkreises (Runde 7, §2).
|
||||
*
|
||||
* Bewusst deutlich über der Blickachse der Kamera (1.19 m). Lagen die Kreise
|
||||
* auf Augenhöhe, projizierten alle fünf auf fast dieselbe Bildhöhe und die
|
||||
* hinteren verschwanden hinter den vorderen — unabhängig davon, wie weit man
|
||||
* sie seitlich auseinanderzog. Erst der Höhenunterschied zur Blickachse
|
||||
* trennt sie in der Tiefe (§3.2).
|
||||
* Daraus ergibt sich die Kopfhöhe: `TABLE.top + radius + headGap`. Die Kreise
|
||||
* schweben damit nur knapp über der Tischkante statt weit darüber. Vorher
|
||||
* hingen sie fest auf 1.52 m — bei den jetzt deutlich grösseren Kreisen sähe
|
||||
* das aus, als schwebten Köpfe frei im Raum.
|
||||
*
|
||||
* Getrennt gehalten werden die Plätze in der Tiefe nicht über die Höhe,
|
||||
* sondern seitlich: die Werte in `meetingRoomSeats` sind so gewählt, dass sich
|
||||
* im Bild keine zwei Kreise überschneiden.
|
||||
*/
|
||||
headY: 1.52,
|
||||
headRadius: 0.142,
|
||||
/** Torso vom Stuhl bis zum Hals. */
|
||||
torsoTop: 1.12,
|
||||
torsoBottom: 0.42,
|
||||
torsoRadiusTop: 0.19,
|
||||
torsoRadiusBottom: 0.25,
|
||||
shoulderWidth: 0.46,
|
||||
headGap: 0.05,
|
||||
} as const
|
||||
@@ -123,7 +123,9 @@ function addRoomShell(scene: THREE.Scene) {
|
||||
new THREE.MeshLambertMaterial({ color: ROOM_COLORS.wallSkirting }),
|
||||
)
|
||||
skirting.rotation.y = -Math.PI / 2
|
||||
skirting.position.set(halfW - 0.01, 0.04, -depth / 2)
|
||||
// Eine Idee vor dem Paneel: auf gleicher Höhe streiten sich die beiden
|
||||
// Flächen um dieselben Bildpunkte, und die Kante franst sichtbar aus.
|
||||
skirting.position.set(halfW - 0.015, 0.04, -depth / 2)
|
||||
scene.add(skirting)
|
||||
}
|
||||
|
||||
@@ -148,16 +150,27 @@ function addRoomShell(scene: THREE.Scene) {
|
||||
function addBaselBackdrop(scene: THREE.Scene, loader: THREE.TextureLoader, maxAnisotropy: number) {
|
||||
/** Abstand der Kulisse. Weit genug, dass die Perspektive sie als Ferne liest. */
|
||||
const radius = 62
|
||||
/**
|
||||
* Masse der Panoramadatei. Ändert sich das Bild, gehören die Zahlen
|
||||
* nachgeführt — sonst zieht der Zylinder es in die Länge oder staucht es.
|
||||
*/
|
||||
const panorama = { breite: 1028, hoehe: 660 }
|
||||
/**
|
||||
* Abgedeckter Bogen. Er beginnt rechts neben der Frontscheibe und läuft über
|
||||
* die Front nach links bis hinter die linke Glasfassade — mehr braucht es
|
||||
* nicht, und weniger Bogen heisst mehr Bildpunkte je Grad.
|
||||
*
|
||||
* Der Wert hält die Bildpunkte je Grad konstant: das Panorama ist seit
|
||||
* Runde 7 schmaler, weil die Fortsetzung nicht mehr überskaliert wird, und
|
||||
* der Bogen schrumpft im selben Verhältnis mit. Die Frontscheibe zeigt
|
||||
* dadurch unverändert denselben Ausschnitt in derselben Grösse (§1: der
|
||||
* Gesamteindruck bleibt, nur die Unsauberkeiten verschwinden).
|
||||
*/
|
||||
const arc = 2.62
|
||||
/** Höhe passend zum Seitenverhältnis des Panoramas (1523 × 660). */
|
||||
const height = (radius * arc) / (1523 / 660)
|
||||
const arc = 1.514
|
||||
/** Höhe passend zum Seitenverhältnis des Panoramas. */
|
||||
const height = (radius * arc) / (panorama.breite / panorama.hoehe)
|
||||
/** Wasserlinie im Panorama, als Anteil von oben. */
|
||||
const waterFraction = 400 / 660
|
||||
const waterFraction = 400 / panorama.hoehe
|
||||
/** Höhe der Rheinoberfläche relativ zum Bürofussboden — wir sind im Hochhaus. */
|
||||
const waterY = -7.5
|
||||
|
||||
@@ -179,7 +192,7 @@ function addBaselBackdrop(scene: THREE.Scene, loader: THREE.TextureLoader, maxAn
|
||||
mesh.position.y = waterY - height * (0.5 - waterFraction)
|
||||
/**
|
||||
* Drehung: geradeaus sichtbar ist die Bildstelle u = (π − Drehung) / Bogen.
|
||||
* Der Abzug von 0.68 setzt damit u ≈ 0.26 in die Blickachse — dort steht im
|
||||
* Der Abzug von 0.68 setzt damit u ≈ 0.45 in die Blickachse — dort steht im
|
||||
* Panorama das Münster. Die Frontscheibe zeigt so das gute Motiv vom
|
||||
* Tischende, die linke Fassade seine Fortsetzung nach links.
|
||||
*/
|
||||
@@ -310,15 +323,20 @@ function addSeatedAgent(
|
||||
: seat.side === SeatSide.LEFT ? 0.42
|
||||
: -0.42
|
||||
|
||||
// Radius und Höhe stammen vom Platz: näher sitzende Agenten haben einen
|
||||
// kleineren Kreis, erscheinen im Bild aber grösser (Runde 7, §2).
|
||||
const radius = seat.radius
|
||||
const kopfHoehe = TABLE.top + radius + SEATED.headGap
|
||||
|
||||
const head = new THREE.Group()
|
||||
head.position.y = SEATED.headY
|
||||
head.position.y = kopfHoehe
|
||||
head.rotation.y = yaw
|
||||
|
||||
const ring = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(SEATED.headRadius + 0.016, 48),
|
||||
new THREE.CircleGeometry(radius * 1.06, 48),
|
||||
new THREE.MeshBasicMaterial({ color: ROOM_COLORS.headRing }),
|
||||
)
|
||||
ring.position.z = -0.004
|
||||
ring.position.z = -0.006
|
||||
head.add(ring)
|
||||
|
||||
if (photoUrl) {
|
||||
@@ -343,7 +361,7 @@ function addSeatedAgent(
|
||||
tex.magFilter = THREE.LinearFilter
|
||||
|
||||
const face = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(SEATED.headRadius, 48),
|
||||
new THREE.CircleGeometry(radius, 96),
|
||||
new THREE.MeshBasicMaterial({ map: tex }),
|
||||
)
|
||||
head.add(face)
|
||||
@@ -360,12 +378,12 @@ function addSeatedAgent(
|
||||
* Klick auf Livia bei Ferdi. Die Trefferfläche deckt sich jetzt mit dem, was
|
||||
* man sieht.
|
||||
*/
|
||||
new THREE.SphereGeometry(SEATED.headRadius * 1.18, 12, 8),
|
||||
new THREE.SphereGeometry(radius * 1.1, 12, 8),
|
||||
// Unsichtbar, aber weiterhin vom Raycaster erfasst: `visible = false` würde
|
||||
// das Objekt aus der Strahlprüfung nehmen.
|
||||
new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false }),
|
||||
)
|
||||
hitBox.position.y = SEATED.headY
|
||||
hitBox.position.y = kopfHoehe
|
||||
group.add(hitBox)
|
||||
|
||||
scene.add(group)
|
||||
@@ -380,7 +398,7 @@ function addSeatedAgent(
|
||||
// die Plätze dort auf wenige Bildpunkte zusammenrücken (§8).
|
||||
labelAnchor: new THREE.Vector3(
|
||||
seat.x,
|
||||
SEATED.headY + SEATED.headRadius + 0.16 + Math.min(0.78, Math.max(0, (Math.abs(seat.z) - 1.75) * 0.135)),
|
||||
kopfHoehe + radius + 0.10 + Math.min(0.34, Math.max(0, (Math.abs(seat.z) - 0.9) * 0.06)),
|
||||
seat.z,
|
||||
),
|
||||
}
|
||||
@@ -459,7 +477,15 @@ export function createMeetingRoomScene(
|
||||
// schneidet das hohe Format den Tisch links und rechts ab. Statt einer
|
||||
// eigenen Mobilszene (§18: pragmatische Degradation) öffnet sich die
|
||||
// Kamera, bis der Raum wieder hineinpasst.
|
||||
camera.fov = CAMERA.fov + Math.max(0, (1.45 - aspect) * 26)
|
||||
const weit = CAMERA.fov + Math.max(0, (1.45 - aspect) * 26)
|
||||
/*
|
||||
* Nach oben begrenzt durch den waagrechten Bildwinkel. Bei sehr breiten,
|
||||
* flachen Fenstern — etwa 1280 × 380 auf einem Notebook — griffe sonst ein
|
||||
* waagrechter Winkel von über 110°: der Raum wirkt gezerrt, und der Blick
|
||||
* reicht seitlich über den Rand des Panoramas hinaus.
|
||||
*/
|
||||
const maxTan = Math.tan((CAMERA.maxHFov / 2) * Math.PI / 180) / aspect
|
||||
camera.fov = Math.min(weit, 2 * Math.atan(maxTan) * 180 / Math.PI)
|
||||
camera.updateProjectionMatrix()
|
||||
},
|
||||
|
||||
|
||||
@@ -39,34 +39,54 @@ export interface MeetingSeat {
|
||||
/** Sitzposition in Metern. */
|
||||
x: number
|
||||
z: number
|
||||
/** Radius des Porträtkreises in Metern. */
|
||||
radius: number
|
||||
}
|
||||
|
||||
/** Ein Platz, so wie er unten eingetragen wird. */
|
||||
type Platz = { side: SeatSide; x: number; z: number; radius: number }
|
||||
|
||||
/**
|
||||
* Sitzplätze der fünf Agenten, in Metern (Runde 6, §3.2).
|
||||
* Sitzplätze der fünf Agenten, in Metern (Runde 6 §3.2, Runde 7 §2).
|
||||
*
|
||||
* Ferdi und Bruno sind die direkten Nachbarn des Betrachters — einer links,
|
||||
* einer rechts, schräg frontal und dicht an der vorderen Tischkante. Dadurch
|
||||
* entsteht hinter ihnen Platz, in dem die übrigen drei gestaffelt Raum haben,
|
||||
* ohne sich zu überlagern.
|
||||
* einer rechts, schräg frontal und dicht an der vorderen Tischkante. Dahinter
|
||||
* staffeln sich die übrigen drei.
|
||||
*
|
||||
* Vorher standen alle fünf zu weit hinten und drängten sich am Fluchtpunkt.
|
||||
* Die Radien wachsen nach hinten, die **Bildgrösse** nimmt trotzdem mit der
|
||||
* Tiefe ab: ein Kreis von 0.30 m in 5 m Entfernung erscheint kleiner als einer
|
||||
* von 0.265 m in 3 m. Ohne diese Gegensteuerung wären die hinteren Porträts bei
|
||||
* gleichem Radius kaum noch erkennbar (§2: «tiefenlogisch», aber «insgesamt klar
|
||||
* besser sichtbar»).
|
||||
*
|
||||
* Alle Werte sind gemeinsam gewählt, nicht einzeln: mit den Kamerawerten aus
|
||||
* `meetingRoomPalette` durchgerechnet ergeben sie gegenüber dem Stand vor dieser
|
||||
* Runde die Faktoren 2.11× (Ferdi/Bruno), 2.22× (Livia/Nora), 2.50× (Sina) und
|
||||
* 3.18× (Thomas) — und dabei **keine** einzige Überschneidung zweier Kreise im
|
||||
* Bild. Wer einen Wert ändert, verschiebt beides; die Kreise überlappen dann
|
||||
* schnell wieder.
|
||||
*/
|
||||
const PLAETZE: Record<string, { side: SeatSide; x: number; z: number }> = {
|
||||
ferdi: { side: SeatSide.LEFT, x: -1.14, z: -1.20 },
|
||||
bruno: { side: SeatSide.RIGHT, x: 1.14, z: -1.20 },
|
||||
livia: { side: SeatSide.LEFT, x: -1.42, z: -2.90 },
|
||||
nora: { side: SeatSide.RIGHT, x: 1.42, z: -2.90 },
|
||||
sina: { side: SeatSide.LEFT, x: -1.30, z: -4.75 },
|
||||
const PLAETZE: Record<string, Platz> = {
|
||||
ferdi: { side: SeatSide.LEFT, x: -1.16, z: -0.88, radius: 0.268 },
|
||||
bruno: { side: SeatSide.RIGHT, x: 1.16, z: -0.88, radius: 0.268 },
|
||||
livia: { side: SeatSide.LEFT, x: -1.32, z: -3.35, radius: 0.345 },
|
||||
nora: { side: SeatSide.RIGHT, x: 1.32, z: -3.35, radius: 0.345 },
|
||||
sina: { side: SeatSide.LEFT, x: -0.95, z: -5.50, radius: 0.395 },
|
||||
}
|
||||
|
||||
/**
|
||||
* Notplatz für einen Agenten ohne Eintrag oben — sichtbar hinten rechts statt
|
||||
* lautlos im Nichts.
|
||||
*/
|
||||
const FALLBACK = { side: SeatSide.RIGHT, x: 1.30, z: -4.75 }
|
||||
const FALLBACK: Platz = { side: SeatSide.RIGHT, x: 0.95, z: -5.50, radius: 0.395 }
|
||||
|
||||
/** Kopfende gegenüber. Unverändert: die Position wirkt gut (§3.2). */
|
||||
const HEAD_Z = -6.75
|
||||
/**
|
||||
* Kopfende gegenüber. Position unverändert, nur der Kreis wächst kräftig: von
|
||||
* dort aus ist der Abstand am grössten, und Thomas war zuvor kaum auszumachen
|
||||
* (§2). Grösser darf er nicht werden — sein Kreis überträfe im Bild sonst den
|
||||
* von Sina, die näher sitzt, und die Tiefenwirkung kippte.
|
||||
*/
|
||||
const KOPFENDE: Platz = { side: SeatSide.HEAD, x: 0, z: -6.75, radius: 0.452 }
|
||||
|
||||
export const MEETING_SEATS: MeetingSeat[] = [
|
||||
...AGENT_WORKSPACES.map(agent => ({
|
||||
@@ -81,8 +101,6 @@ export const MEETING_SEATS: MeetingSeat[] = [
|
||||
name: AGENT_SUPERVISOR.name,
|
||||
role: AGENT_SUPERVISOR.role,
|
||||
path: AGENT_SUPERVISOR.path,
|
||||
side: SeatSide.HEAD,
|
||||
x: 0,
|
||||
z: HEAD_Z,
|
||||
...KOPFENDE,
|
||||
},
|
||||
]
|
||||
Reference in new issue
Block a user