Bewegtbild: die Aufgabe entsteht an der Nachricht, nicht im Plus-Menue

Geprueft im laufenden Team-Chat: an jeder Nachricht haengt ein Menue mit
Reagieren, Antworten, Im Thread antworten, Anpinnen, Aufgabe erstellen,
Bearbeiten, Loeschen. Das ist die Funktion, die die Website meint mit "Aus
'mach das mal' wird mit einem Klick eine echte Aufgabe".

Die Animation zeigt jetzt genau das: Tobias' Nachricht anfassen, der Pfeil
erscheint an ihr, Menue auf, "Aufgabe erstellen", die Aufgabenkarte klappt in
der Nachricht auf, die Systemzeile bestaetigt sie im Projekt.

Tobias' Nachricht und keine andere: die Aufgabenkarte aus der Quelle traegt
"Tobias Wagner · offen" als Zustaendigkeit. An einer fremden Nachricht muesste
der Name erfunden werden. So bleiben alle vier Felder der Karte genau die aus
GeneralTeamChatPage.tsx.

Der Vorgaenger nahm das Menue hinter dem Pluszeichen. Das war sauber belegt --
Datei, Zeile, Grep ueber beide Repos, dazu die Staging-API, die eine Aufgabe
nur mit contact/company/lead/project verknuepft und eine Nachricht als Bezug
gar nicht kennt -- und trotzdem falsch. Ein Pitch-Paket vom Juli belegt, was im
Juli gebaut war, nicht was das Produkt heute kann. Wo eine Aussage der Website
keine Entsprechung im vorliegenden Code hat, ist das ein Grund nachzufragen und
nicht, die Aussage auf den Code zu biegen. Steht so in POST-6.md §6.

Sechs statt acht Sekunden: das Tippen faellt mit dem richtigen Weg weg.
Instagram nimmt je Carousel-Element drei bis sechzig Sekunden an, die Laenge
richtet sich also allein nach dem Ablauf.

Dazu in den Bausteinen: fuenf Symbole fuer das Menue, und eine Blase kann eine
zweite Anhangkarte tragen, die ueber einen Anteil aufklappt -- aufgedeckt per
clipPath, nicht gestaucht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-26 13:42:53 +02:00
1 parent d4feaa175b
commit a8f79c1224
4 files changed
+227 -215

No files matched your search

+166 -168
View File
@@ -1464,12 +1464,12 @@ if (location.search.indexOf("nur=anim") >= 0)
ist `dauer` und der zweite Wert t, den die Zeichenfunktion bekommt. */
const ANIMS = [
{
/* Acht Sekunden. Fuenf waren zu knapp, sobald der Ablauf stimmt:
tippen, Menue, waehlen, abschicken, bestaetigen -- das sind fuenf
Schritte, und jeder braucht seinen Moment. Unter drei Sekunden nimmt
Instagram ein Video im Carousel gar nicht erst an, nach oben sind
sechzig erlaubt. */
key: "animChat", type: "feature", dauer: 8,
/* Sechs Sekunden. Instagram nimmt im Carousel drei bis sechzig an; das
ist also weit von jeder Grenze weg, und die Laenge richtet sich
allein nach dem Ablauf. Vier Schritte -- anfassen, waehlen,
aufklappen, bestaetigen -- brauchen etwa so lange, und eine kurze
Schleife wiederholt sich oefter, waehrend jemand liest. */
key: "animChat", type: "feature", dauer: 6,
eyebrow: "Bewegtbild",
head: 'Getippt,<br><span class="a">und schon im Projekt.</span>',
body: "Eine Nachricht, ein Klick, und die Aufgabe hängt am richtigen Projekt.",
@@ -2051,6 +2051,12 @@ if (location.search.indexOf("nur=anim") >= 0)
"workflow": '<rect width="8" height="8" x="3" y="3" rx="2"/><path d="M7 11v4a2 2 0 0 0 2 2h4"/><rect width="8" height="8" x="13" y="13" rx="2"/>',
"x": '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
"zap": '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
/* Fuer das Menue an der Nachricht, aus dem laufenden Team-Chat. */
"thumbs-up": '<path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/>',
"corner-up-left": '<polyline points="9 14 4 9 9 4"/><path d="M20 20v-7a4 4 0 0 0-4-4H4"/>',
"pencil": '<path d="M21.2 6.8a1 1 0 0 0-4-4L3.8 16.2a2 2 0 0 0-.5.8l-1.3 4.4a.5.5 0 0 0 .6.6l4.4-1.3a2 2 0 0 0 .8-.5z"/><path d="m15 5 4 4"/>',
"trash-2": '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>',
"chevron-down": '<path d="m6 9 6 6 6-6"/>',
"rocket": '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>'
};
@@ -2703,12 +2709,19 @@ if (location.search.indexOf("nur=anim") >= 0)
const innen = (max || BLASE_MAX) - 32;
const zeilen = m.text ? umbrechen(m.text, innen, 17, 400) : [];
const innenB = (max || BLASE_MAX) - 32;
/* Eine Blase kann eine zweite Anhangkarte tragen: die Aufgabe, die aus
ihr entstanden ist. `a2` ist ihr Anteil zwischen 0 und 1 -- damit
waechst die Blase mit, waehrend die Karte aufklappt, und der ganze
Verlauf rutscht von selbst nach. */
const f2 = m.anhang2 ? (m.a2 == null ? 1 : m.a2) : 0;
let h = 14 + (m.mein ? 0 : 22) + zeilen.length * 25 +
(m.anhang ? anhangHoehe(m.anhang, innenB) + 8 : 0) + 24;
(m.anhang ? anhangHoehe(m.anhang, innenB) + 8 : 0) +
(f2 > 0 ? (anhangHoehe(m.anhang2, innenB) + 8) * f2 : 0) + 24;
let b = 32 + Math.max(
...zeilen.map(z => messen(z, 17, 400)),
m.mein ? 0 : messen(m.autor, 14, 800),
m.anhang ? anhangNoetig(m.anhang) : 0, 90);
m.anhang ? anhangNoetig(m.anhang) : 0,
m.anhang2 ? anhangNoetig(m.anhang2) : 0, 90);
return { zeilen, hoehe: Math.round(h), breite: Math.round(Math.min(b, max || BLASE_MAX)) };
}
@@ -2717,6 +2730,7 @@ if (location.search.indexOf("nur=anim") >= 0)
als die Rechnung ein zweites Mal zu fuehren und sie auseinanderlaufen zu
lassen. */
let letzterAnhang = null;
let klappNr = 0;
function blase(x, y, b, m) {
// Die Systemmeldung ist keine Blase, sondern eine Zeile in der Mitte.
@@ -2762,6 +2776,21 @@ if (location.search.indexOf("nur=anim") >= 0)
m.anhang[2], m.anhang[3]);
cy += ah + 8;
}
/* Die zweite Karte klappt auf: sie wird von oben freigegeben, nicht
gestaucht -- eine gestauchte Karte sieht kaputt aus, eine
aufgedeckte sieht nach Aufklappen aus. */
const f2 = m.anhang2 ? (m.a2 == null ? 1 : m.a2) : 0;
if (f2 > .004) {
const ah = anhangHoehe(m.anhang2, ma.breite - 32);
const sicht = ah * f2;
const id = "anhklapp" + (++klappNr);
g += '<clipPath id="' + id + '"><rect x="' + n1(bx + 14) + '" y="' + n1(cy + 4) +
'" width="' + n1(ma.breite - 28) + '" height="' + n1(sicht) + '"/></clipPath>' +
'<g clip-path="url(#' + id + ')" opacity="' + n1(Math.min(1, f2 * 2.2)) + '">' +
anhangKarte(bx + 16, cy + 4, ma.breite - 32, m.anhang2[0], m.anhang2[1],
m.anhang2[2], m.anhang2[3]) + '</g>';
cy += sicht + 8 * f2;
}
g += uiText(bx + ma.breite - 16, cy + 16, m.zeit + (m.mein ? " ✓✓" : ""),
13, 500, CHAT_TEXT3, { anker: "end" });
return g;
@@ -3265,127 +3294,120 @@ if (location.search.indexOf("nur=anim") >= 0)
/* ─── Das Bewegtbild ────────────────────────────────────────────────────
Wie aus einer Nachricht eine echte Aufgabe wird, in dem Weg, den das
Werkzeug tatsaechlich hat.
Wie aus einer Nachricht eine echte Aufgabe wird -- der Weg, den das
laufende Werkzeug hat: Nachricht anklicken, Menue oeffnet sich,
"Aufgabe erstellen".
Das ist der Kern, und ein erster Anlauf hatte ihn falsch: dort wurde die
Nachricht "Danke. Ich habe dazu direkt eine Aufgabe erstellt." getippt,
abgeschickt -- und *dann* wuchs an ihr eine Aufgabenkarte. Die Nachricht
meldete also eine Aufgabe, die es noch gar nicht gab, und wurde
anschliessend selbst zu einer. Ein Kreis, kein Ablauf.
Zwei Anlaeufe lagen davor daneben, beide aus derselben Ursache. Der
erste liess die Nachricht "Danke. Ich habe dazu direkt eine Aufgabe
erstellt." tippen und danach eine Aufgabenkarte an ihr wachsen -- sie
meldete also eine Aufgabe, die es noch nicht gab, und wurde dann selbst
zu einer. Der zweite nahm das Menue hinter dem Pluszeichen, weil das im
Pitch-Paket der einzige Weg ist, der dort ausgezeichnet ist.
Nachgesehen in GeneralTeamChatPage.tsx: eine Aufgabe entsteht ueber das
Menue hinter dem Pluszeichen ("Aufgabe erstellen", Zeile 275), und das
Ergebnis ist Nachricht m4 -- Text *und* angehaengte Aufgabenkarte in
einem -- gefolgt von der Systemzeile m5. Ein Kontextmenue an einer
bestehenden Nachricht gibt es dort nicht; wer eine Umwandlung zeigt,
zeigt eine Funktion, die wir nicht haben.
Beides war zu wenig geprueft. Das Pitch-Paket ist ein Stand vom Juli;
im laufenden Team-Chat haengt an jeder Nachricht ein Menue mit
Reagieren, Antworten, Im Thread antworten, Anpinnen, Aufgabe erstellen,
Bearbeiten, Loeschen. Das ist die Funktion, die die Website meint, wenn
sie schreibt: "Aus 'mach das mal' wird mit einem Klick eine echte
Aufgabe."
Also die richtige Reihenfolge: Notiz tippen, Pluszeichen, "Aufgabe
erstellen", und beides geht zusammen als eine Nachricht raus. Der Satz
im Perfekt stimmt dann auch -- wenn die Nachricht ankommt, haengt die
Aufgabe daran.
Gewaehlt wird Tobias' Nachricht, nicht irgendeine: die Aufgabenkarte aus
der Quelle traegt "Tobias Wagner · offen" als Zustaendigkeit. Wer sie an
einer fremden Nachricht entstehen laesst, muss den Namen erfinden.
Der Ablauf, in Anteilen der Laufzeit (8 s):
Der Ablauf, in Anteilen der Laufzeit (6 s):
0,00 - 0,05 ruhig, der Verlauf steht, das Feld ist leer
0,05 - 0,36 die Notiz wird getippt, Zeichen fuer Zeichen
0,36 - 0,45 der Zeiger faehrt auf das Pluszeichen und klickt
0,45 - 0,56 das Menue klappt nach oben auf, die Zeilen gestaffelt
0,56 - 0,71 der Zeiger geht auf "Aufgabe erstellen" und klickt
0,71 - 0,76 das Menue schliesst, das Feld leert sich
0,72 - 0,84 die Nachricht mit der Aufgabenkarte waechst in den Verlauf
0,84 - 0,92 die Bestaetigung im Projekt erscheint
0,92 - 1,00 Halt auf dem Ergebnis
0,00 - 0,10 ruhig, der Verlauf steht
0,10 - 0,22 der Zeiger faehrt auf die Nachricht, der Pfeil erscheint
0,22 - 0,27 Klick
0,26 - 0,40 das Menue klappt auf, die Zeilen gestaffelt
0,40 - 0,56 der Zeiger geht auf "Aufgabe erstellen", die Zeile hebt sich
0,56 - 0,61 Klick
0,60 - 0,66 das Menue schliesst, der Zeiger geht
0,62 - 0,80 die Aufgabenkarte klappt in der Nachricht auf
0,80 - 0,90 die Bestaetigung im Projekt erscheint
0,90 - 1,00 Halt auf dem Ergebnis
Am Ende wird nichts zurueckgenommen. Der Vorgaenger blendete die neue
Nachricht wieder aus, damit der Schleifenstoss unsichtbar wird -- das
sah aus, als loesche sich eine abgeschickte Nachricht von selbst. Eine
Schleife darf springen, eine Nachricht darf nicht verschwinden. */
const ANIM_NOTIZ = VERLAUF[3].text;
Am Ende wird nichts zurueckgenommen. Ein frueherer Stand blendete die
Aufgabe wieder aus, damit der Schleifenstoss unsichtbar wird -- das sah
aus, als loesche sie sich von selbst. Eine Schleife darf springen. */
/* Das Menue an der Nachricht, aus dem laufenden Team-Chat. Reihenfolge und
Beschriftung wie dort; die Symbole sind unsere, weil dort ein Emoji
steht und wir durchgehend Strichsymbole benutzen. */
const NACHRICHT_MENUE = [
["thumbs-up", "Reagieren"],
["corner-up-left", "Antworten"],
["message-square", "Im Thread antworten"],
["pin", "Anpinnen"],
["square-check", "Aufgabe erstellen"],
["pencil", "Bearbeiten"],
["trash-2", "Löschen"],
];
const MENUE_ZIEL = 4; // "Aufgabe erstellen"
/* Die Aufgabe, die daraus entsteht. Alle vier Felder stehen so in
GeneralTeamChatPage.tsx an der Nachricht m4. */
const ANIM_AUFGABE = ["square-check", "Materialliste prüfen · fällig 31.07.2026",
"Tobias Wagner · offen", "Offen"];
/* Der Mauszeiger. Kein Bestandteil des Werkzeugs, sondern das Mittel, mit
dem eine Bedienung erzaehlt wird -- ohne ihn sieht man ein Menue
aufgehen und weiss nicht, warum. Weiss mit dunkler Kante, damit er auf
dem Verlauf wie auf dem Menue liest. */
const zeiger = (x, y, gr) =>
'<g transform="translate(' + n1(x) + ',' + n1(y) + ') scale(' + n1(gr) + ')" ' +
'filter="url(#hgklein)">' +
'<path d="M0,0 L0,17.5 L4.3,13.4 L7,19.4 L10.1,18 L7.4,12.2 L13,12.2 Z" ' +
'fill="#FFFFFF" stroke="#24242B" stroke-width="1.5" stroke-linejoin="round"/></g>';
/* Der Klick: ein Ring, der einmal aufgeht und dabei verschwindet. */
const klickRing = (x, y, p) => p <= 0 || p >= 1 ? "" :
'<circle cx="' + n1(x) + '" cy="' + n1(y) + '" r="' + n1(7 + 17 * p) +
'" fill="none" stroke="#24242B" stroke-width="' + n1(2.4 * (1 - p)) +
'" opacity="' + n1(.55 * (1 - p)) + '"/>';
/* Das Menue hinter dem Pluszeichen, aus GeneralTeamChatPage.tsx Zeile 275.
Dieselben sieben Eintraege und derselbe Trenner wie auf Slide 4, nur
kleiner gesetzt: hier haengt es im Fenster und steht nicht allein auf
der Slide. side="top" align="start" heisst, es klappt nach oben auf und
sitzt links am Knopf. */
function animMenue(ax, ay, auf, hoverNr) {
const MW = 306, ZH = 34, PAD = 9, TRENN = 11;
const hoehe = 2 * PAD + MENUE.reduce((a, e) => a + (e ? ZH : TRENN), 0);
const x = ax, y = ay - hoehe;
/* Es klappt nach oben auf, weil die Nachricht unten im Fenster haengt --
nach unten waere kein Platz, und ein Menue dreht dort um. */
function animMenue(rechtsX, untenY, auf, hoverNr) {
const MW = 262, ZH = 34, PAD = 8;
const hoehe = 2 * PAD + NACHRICHT_MENUE.length * ZH;
const x = rechtsX - MW, y = untenY - hoehe;
let g = "";
let yy = y + PAD, nr = 0;
for (const e of MENUE) {
if (!e) {
g += '<path d="M' + n1(x + PAD + 4) + ',' + n1(yy + TRENN / 2) + 'H' +
n1(x + MW - PAD - 4) + '" stroke="' + CHAT_RAND + '" stroke-width="1.1"/>';
yy += TRENN; continue;
}
/* Die gewaehlte Zeile bekommt den Grund, den sie im Werkzeug beim
Ueberfahren bekommt: hover:bg-surface-neutral-tertiary. */
NACHRICHT_MENUE.forEach((e, nr) => {
const yy = y + PAD + nr * ZH;
if (nr === hoverNr)
g += '<rect x="' + n1(x + PAD - 1) + '" y="' + n1(yy) + '" width="' +
n1(MW - 2 * PAD + 2) + '" height="' + ZH + '" rx="7" fill="' + FL_SPALTE + '"/>';
g += feather(e[0], x + PAD + 18, yy + ZH / 2, 16, CHAT_TEXT2, 1.9);
g += uiText(x + PAD + 36, yy + ZH / 2 + 5.5, e[1], 15, 500, CHAT_TEXT);
yy += ZH; nr++;
}
});
/* Es faehrt aus dem Knopf heraus auf: von unten links skaliert, damit es
am Pluszeichen zu haengen scheint. */
const sk = .94 + .06 * auf;
return { hoehe, html:
'<g opacity="' + n1(Math.min(1, auf * 3.2)) + '" transform="translate(' +
n1(x) + ',' + n1(ay) + ') scale(' + n1(sk) + ') translate(' + n1(-x) + ',' +
n1(-ay) + ')">' +
'<g filter="url(#hgschatten)"><path d="' + rundRechteck(x, y, MW, hoehe, 14) +
'" fill="#FFFFFF" stroke="' + CHAT_RAND + '" stroke-width="1.2"/></g>' +
g + '</g>',
// Mitte der Zeile "Aufgabe erstellen", fuer den Zeiger.
zielY: y + PAD + 5 * ZH + TRENN + ZH / 2, zielX: x + MW / 2 };
return { hoehe,
zielX: x + PAD + 30,
zielY: y + PAD + MENUE_ZIEL * ZH + ZH / 2,
html:
'<g opacity="' + n1(Math.min(1, auf * 4.5)) + '" transform="translate(' +
n1(rechtsX) + ',' + n1(untenY) + ') scale(' + n1(sk) + ') translate(' +
n1(-rechtsX) + ',' + n1(-untenY) + ')">' +
'<g filter="url(#hgschatten)"><path d="' + rundRechteck(x, y, MW, hoehe, 14) +
'" fill="#FFFFFF" stroke="' + CHAT_RAND + '" stroke-width="1.2"/></g>' +
g + '</g>' };
}
function animChat(j, t) {
/* 620 x 740: hoeher waere das Fenster oben leer, weil der Verlauf unten
haengt, und die Slide stiesse an den Fliesstext. Was nicht mehr
hineinpasst, schiebt sich oben hinaus -- so macht es ein Chat auch. */
const B = 620, H = 740, RAND = 22, KOPF = 74, FELD = 96;
const innenB = B - 2 * RAND;
const bahnO = KOPF + 8, bahnU = H - FELD - 8;
const dauer = (ANIMS[j] && ANIMS[j].dauer) || 8;
const tippen = stufe(t, .05, .36);
const zeigerAn = stufe(t, .36, .43); // Zeiger faehrt zum Plus
const klick1 = stufe(t, .43, .48);
const auf = daempfen(stufe(t, .45, .56)); // Menue klappt auf
const wandern = daempfen(stufe(t, .56, .68)); // Zeiger zur Zeile
const klick2 = stufe(t, .68, .73);
const zu = daempfen(stufe(t, .71, .76)); // Menue schliesst
/* Die Blase setzt an, waehrend sich das Feld leert. Ein Zwischenstand
hatte dazwischen eine Viertelsekunde Leere: die Notiz war weg, die
Nachricht noch nicht da -- das las sich, als sei sie verschluckt
worden. Die beiden Bewegungen greifen jetzt ineinander. */
const wachsen = daempfen(stufe(t, .72, .84)); // Nachricht kommt herein
const system = daempfen(stufe(t, .84, .92));
const menueAn = Math.max(0, auf - zu);
const zeigerAn = stufe(t, .10, .22);
const pfeil = daempfen(stufe(t, .16, .24)); // der Pfeil an der Blase
const klick1 = stufe(t, .22, .27);
const auf = daempfen(stufe(t, .26, .40));
const wandern = daempfen(stufe(t, .40, .54));
const klick2 = stufe(t, .56, .61);
const zu = daempfen(stufe(t, .60, .66));
const klappen = daempfen(stufe(t, .62, .80)); // die Aufgabe klappt auf
const system = daempfen(stufe(t, .80, .90));
const menueAn = Math.max(0, auf - zu);
let g = "";
@@ -3397,110 +3419,86 @@ if (location.search.indexOf("nur=anim") >= 0)
g += uiText(RAND + 52, RAND + 30, "P-2026-0042 · 8 Mitglieder · aktiv", 15, 500, CHAT_TEXT3);
g += '<path d="M0,' + KOPF + 'H' + B + '" stroke="' + CHAT_RAND + '" stroke-width="1.2"/>';
/* Der Verlauf haengt unten. Waechst er, schiebt er sich nach oben aus dem
Fenster -- so entsteht das Scrollen von selbst, ohne eigenen Zaehler. */
const alt = [VERLAUF[0], VERLAUF[1], VERLAUF[2]];
const neu = VERLAUF[3], sys = VERLAUF[4];
/* Der Verlauf haengt unten. Waechst die Blase, schiebt er sich nach oben
aus dem Fenster -- so entsteht das Scrollen von selbst. */
const spalte = innenB;
const hAlt = alt.map(m => blaseHoehe(m, spalte));
const hNeu = blaseHoehe(neu, spalte) * wachsen;
const m1 = VERLAUF[0], m2 = VERLAUF[1];
// Tobias' Nachricht bekommt die Aufgabe, die aus ihr entsteht.
const m3 = Object.assign({}, VERLAUF[2], { anhang2: ANIM_AUFGABE, a2: klappen });
const sys = VERLAUF[4];
const h1 = blaseHoehe(m1, spalte), h2 = blaseHoehe(m2, spalte);
const h3 = blaseHoehe(m3, spalte);
const hSys = blaseHoehe(sys, spalte) * system;
const LUFT = 16;
const gesamt = hAlt.reduce((a, h) => a + h + LUFT, 0) +
(wachsen > .01 ? hNeu + LUFT : 0) + (system > .01 ? hSys + LUFT : 0);
const gesamt = h1 + h2 + h3 + 3 * LUFT + (system > .01 ? hSys + LUFT : 0);
let y = bahnU - gesamt;
let bahn = "";
alt.forEach((m, i) => { bahn += blase(RAND, y, spalte, m); y += hAlt[i] + LUFT; });
bahn += blase(RAND, y, spalte, m1); y += h1 + LUFT;
bahn += blase(RAND, y, spalte, m2); y += h2 + LUFT;
/* Die gewaehlte Nachricht. Ihre Lage wird gemerkt, damit Pfeil, Zeiger
und Menue daran haengen und nicht an einer zweiten Rechnung. */
const dritteY = y;
const ma3 = blaseMasse(m3, blaseBreite(spalte));
const bx3 = RAND + 52, br3 = ma3.breite;
bahn += blase(RAND, y, spalte, m3);
y += h3 + LUFT;
if (wachsen > .01) {
/* Sie kommt von unten und richtet sich auf: skaliert um den eigenen
unteren Rand, damit sie aus dem Eingabefeld heraus zu wachsen
scheint und nicht aus der Mitte heraus aufplatzt. Text und
Aufgabenkarte kommen zusammen -- es ist eine Nachricht, und der
Klick auf "Aufgabe erstellen" hat beides erzeugt. */
const sk = .88 + .12 * wachsen;
bahn += '<g opacity="' + n1(Math.min(1, wachsen * 1.7)) + '" transform="translate(0,' +
n1((1 - wachsen) * 30) + ') translate(' + n1(B / 2) + ',' + n1(y + hNeu) +
') scale(' + n1(sk) + ') translate(' + n1(-B / 2) + ',' + n1(-(y + hNeu)) + ')">' +
blase(RAND, y, spalte, neu) + '</g>';
y += hNeu + LUFT;
}
if (system > .01) {
bahn += '<g opacity="' + n1(system) + '" transform="translate(0,' +
n1((1 - system) * 12) + ')">' + blase(RAND, y, spalte, sys) + '</g>';
}
/* Der Pfeil an der Blase, wie im Werkzeug: er erscheint, sobald die
Nachricht angefasst wird, und sitzt oben rechts in ihr. */
if (pfeil > .01 && zu < .5) {
const px = bx3 + br3 - 18, py = dritteY + 16;
bahn += '<g opacity="' + n1(pfeil * (1 - stufe(t, .60, .66))) + '">' +
'<circle cx="' + n1(px) + '" cy="' + n1(py) + '" r="12" fill="' + FL_SPALTE + '"/>' +
feather("chevron-down", px, py + 1, 14, CHAT_TEXT2, 2.2) + '</g>';
}
// Was ueber die Bahn hinausragt, wird oben abgeschnitten -- wie im Fenster.
g += '<clipPath id="animbahn"><rect x="0" y="' + bahnO + '" width="' + B +
'" height="' + (bahnU - bahnO + 6) + '"/></clipPath>';
g += '<g clip-path="url(#animbahn)">' + bahn + '</g>';
/* Das Eingabefeld. Waehrend des Tippens laeuft der Text mit, dahinter
blinkt der Strich; sobald etwas drinsteht, wird aus dem Mikrofon der
Sendeknopf -- so macht es der Composer auch. */
// Das Eingabefeld bleibt ruhig: hier wird nichts geschrieben.
const fy = H - FELD;
const plusX = RAND + 14, plusY = fy + 44;
g += '<path d="M0,' + n1(fy) + 'H' + B + '" stroke="' + CHAT_RAND + '" stroke-width="1.2"/>';
g += feather("plus", plusX, plusY, 21, menueAn > .3 ? CHAT_TEXT : CHAT_TEXT2, 2);
g += feather("plus", RAND + 14, fy + 44, 21, CHAT_TEXT2, 2);
const feldX = RAND + 36, feldB = B - 2 * RAND - 100;
g += '<g filter="url(#hgklein)"><rect x="' + n1(feldX) + '" y="' + n1(fy + 24) +
'" width="' + n1(feldB) + '" height="40" rx="20" fill="' + FL_KARTE + '" stroke="' +
CHAT_RAND + '" stroke-width="1.2"/></g>';
g += uiText(feldX + 18, fy + 50, "Nachricht schreiben …", 16, 400, "#C9C9CE");
g += feather("mic", B - RAND - 24, fy + 44, 21, CHAT_TEXT2, 2);
// Nach dem Klick auf "Aufgabe erstellen" geht die Notiz mit raus.
const abgeschickt = zu > .45;
const zeichen = Math.round(ANIM_NOTIZ.length * tippen);
const getippt = abgeschickt ? "" : ANIM_NOTIZ.slice(0, zeichen);
if (getippt) {
const passt = umbrechen(getippt, feldB - 44, 16, 400, 1)[0];
g += uiText(feldX + 18, fy + 50, passt, 16, 400, CHAT_TEXT);
// Der Strich blinkt zweimal je Sekunde, gerechnet auf die Laufzeit.
if (tippen < 1 && Math.floor(t * dauer * 2) % 2 === 0) {
const bx = feldX + 20 + messen(passt, 16, 400);
g += '<rect x="' + n1(bx) + '" y="' + n1(fy + 36) + '" width="2" height="18" ' +
'fill="' + CHAT_TEXT + '"/>';
}
} else {
g += uiText(feldX + 18, fy + 50, "Nachricht schreiben …", 16, 400, "#C9C9CE");
}
if (getippt) {
const px = B - RAND - 24;
g += '<circle cx="' + n1(px) + '" cy="' + n1(plusY) + '" r="19" fill="#24242B"/>';
g += feather("send", px - 1, plusY, 18, "#FFFFFF", 2);
} else {
g += feather("mic", B - RAND - 24, plusY, 21, CHAT_TEXT2, 2);
}
/* Das Menue haengt am Pfeil und liegt ueber allem. */
const chevX = bx3 + br3 - 18, chevY = dritteY + 16;
const m = animMenue(chevX + 14, chevY - 16, Math.max(auf, .001),
wandern > .55 && menueAn > .2 ? MENUE_ZIEL : -1);
/* Unter drei Prozent ist es kein Aufblenden mehr, sondern ein Schemen
ueber dem Verlauf. Dann lieber gar nichts. */
if (menueAn > .03) g += m.html;
/* Das Menue liegt ueber allem, auch ueber dem Eingabefeld -- es haengt am
Pluszeichen und klappt nach oben auf. Die Zeile "Aufgabe erstellen"
hebt sich, sobald der Zeiger dort angekommen ist. */
const m = animMenue(RAND + 2, fy - 6, Math.max(auf, .001),
wandern > .55 && menueAn > .2 ? 5 : -1);
if (menueAn > .01) g += m.html;
/* Der Zeiger. Er kommt von rechts unten herein, faehrt auf das
Pluszeichen, spaeter auf die Zeile, und geht nach dem zweiten Klick
wieder hinaus. */
/* Nach dem zweiten Klick geht er zuegig weg. Ein Zeiger, der ohne
Aufgabe ueber dem Verlauf stehen bleibt, zieht den Blick auf sich,
waehrend gerade die Nachricht ankommt. */
const zWeg = stufe(t, .72, .77);
/* Der Zeiger: herein auf den Pfeil, dann auf die Zeile, dann weg. */
const zWeg = stufe(t, .61, .67);
const zAn = zeigerAn * (1 - zWeg);
if (zAn > .01) {
const vonX = B - 70, vonY = H + 40;
const vonX = B - 60, vonY = H + 40;
const p1 = daempfen(zeigerAn);
let zx = vonX + (plusX + 2 - vonX) * p1;
let zy = vonY + (plusY + 2 - vonY) * p1;
let zx = vonX + (chevX - 2 - vonX) * p1;
let zy = vonY + (chevY - 2 - vonY) * p1;
if (wandern > 0) {
const p2 = wandern;
zx += (m.zielX - 40 - zx) * p2;
zy += (m.zielY - 6 - zy) * p2;
zx += (m.zielX - zx) * wandern;
zy += (m.zielY - 4 - zy) * wandern;
}
g += klickRing(zx - 2, zy - 2, klick1 < 1 ? klick1 : 0);
g += klickRing(zx - 2, zy - 2, klick2 < 1 && klick2 > 0 ? klick2 : 0);
g += klickRing(zx + 2, zy + 2, klick1 < 1 ? klick1 : 0);
g += klickRing(zx + 2, zy + 2, klick2 > 0 && klick2 < 1 ? klick2 : 0);
g += '<g opacity="' + n1(Math.min(1, zeigerAn * 2) * (1 - zWeg)) + '">' +
zeiger(zx, zy, 1.5) + '</g>';
}