Bewegtbild: der richtige Weg von der Nachricht zur Aufgabe

Die erste Fassung hatte den Ablauf im Kreis: getippt wurde "Danke. Ich habe
dazu direkt eine Aufgabe erstellt.", abgeschickt -- und *danach* wuchs an
dieser Nachricht eine Aufgabenkarte. Sie meldete also eine Aufgabe, die es
noch gar nicht gab, und wurde anschliessend selbst zu einer.

Nachgesehen in GeneralTeamChatPage.tsx: eine Aufgabe entsteht ueber das Menue
hinter dem Pluszeichen ("Aufgabe erstellen", Zeile 275). 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; eine Umwandlung "Nachricht -> Aufgabe" zu zeigen waere eine Funktion,
die wir nicht haben.

Jetzt also: Notiz tippen, Pluszeichen, "Aufgabe erstellen" waehlen, und beides
geht zusammen als eine Nachricht raus, danach die Bestaetigung im Projekt. Der
Satz im Perfekt stimmt damit auch -- wenn die Nachricht ankommt, haengt die
Aufgabe daran.

Am Ende wird nichts mehr 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.

Dazu ein Mauszeiger, der die Bedienung erzaehlt -- ohne ihn geht ein Menue
auf und man weiss nicht, warum -- und acht statt fuenf Sekunden: der Ablauf
hat jetzt fuenf Schritte, und jeder braucht seinen Moment.

Was ein Standbild traegt, traegt eine Animation nicht automatisch mit: auf
Slide 3 liest sich dieselbe Nachricht einwandfrei, weil dort ein Zustand steht
und keine Reihenfolge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-26 00:09:45 +02:00
1 parent 9e8b2ea607
commit d4feaa175b
4 files changed
+231 -88

No files matched your search

+175 -67
View File
@@ -1464,9 +1464,12 @@ if (location.search.indexOf("nur=anim") >= 0)
ist `dauer` und der zweite Wert t, den die Zeichenfunktion bekommt. */
const ANIMS = [
{
/* Fuenf Sekunden: unter drei nimmt Instagram ein Video im Carousel
nicht an, ueber sechs verliert eine Schleife ihren Reiz. */
key: "animChat", type: "feature", dauer: 5,
/* 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,
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.",
@@ -3261,29 +3264,103 @@ if (location.search.indexOf("nur=anim") >= 0)
}
/* ─── Slide 6 · Die Animation ────────────────────────────────────────────
Eine Nachricht entsteht: getippt, abgeschickt, und aus ihr wird eine
Aufgabe im Projekt. Darueber liegen drei Beitraege, die schon da waren --
man sieht also einen laufenden Verlauf und nicht ein leeres Fenster.
/* ─── Das Bewegtbild ────────────────────────────────────────────────────
Wie aus einer Nachricht eine echte Aufgabe wird, in dem Weg, den das
Werkzeug tatsaechlich hat.
Alle Texte sind dieselben wie auf Slide 1 und 3, aus INITIAL_MESSAGES.
Eine Animation ist kein Anlass, etwas zu erfinden.
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.
Der Ablauf, in Anteilen der Laufzeit:
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.
0,00 - 0,06 ruhig, der Verlauf steht, das Feld ist leer
0,06 - 0,44 die Nachricht wird getippt, Zeichen fuer Zeichen
0,44 - 0,50 abgeschickt: das Feld leert sich
0,48 - 0,66 die Blase waechst von unten herein, der Verlauf rutscht hoch
0,64 - 0,80 die Aufgabenkarte darin blendet auf
0,78 - 0,92 die Bestaetigung erscheint, der Verlauf rutscht nach
0,92 - 1,00 zurueck auf Anfang
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.
Der letzte Abschnitt ist wichtig: im Carousel laeuft so ein Bild in
Schleife, und ein harter Schnitt zurueck faellt auf. Deshalb blenden die
beiden neuen Teile am Ende wieder aus und der Verlauf faehrt zurueck --
das letzte Bild ist damit dasselbe wie das erste. */
const ANIM_TIPP = VERLAUF[3].text;
Der Ablauf, in Anteilen der Laufzeit (8 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
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;
/* 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;
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. */
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 };
}
function animChat(j, t) {
/* 620 x 740: hoeher waere das Fenster oben leer, weil der Verlauf unten
@@ -3292,20 +3369,23 @@ if (location.search.indexOf("nur=anim") >= 0)
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;
/* Die Abschnitte. stufe() liefert 0 davor, 1 danach und dazwischen den
Anteil; daempfen() nimmt der Bewegung die Maschinenhaftigkeit. */
const tippen = stufe(t, .06, .44);
const leeren = stufe(t, .44, .50);
const wachsen = daempfen(stufe(t, .48, .66));
const karte = daempfen(stufe(t, .64, .80));
const system = daempfen(stufe(t, .78, .92));
const zurueck = daempfen(stufe(t, .92, 1));
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));
// Am Ende faehrt alles wieder auf den Anfang.
const neuAn = Math.max(0, wachsen - zurueck);
const karteAn = Math.max(0, karte - zurueck);
const sysAn = Math.max(0, system - zurueck);
const menueAn = Math.max(0, auf - zu);
let g = "";
@@ -3324,37 +3404,33 @@ if (location.search.indexOf("nur=anim") >= 0)
const spalte = innenB;
const hAlt = alt.map(m => blaseHoehe(m, spalte));
// Die neue Blase ohne und mit Karte -- dazwischen wird geblendet.
const mOhne = { autor: neu.autor, kuerzel: neu.kuerzel, mein: true,
zeit: neu.zeit, text: neu.text };
const hOhne = blaseHoehe(mOhne, spalte);
const hMit = blaseHoehe(neu, spalte);
const hNeu = (hOhne + (hMit - hOhne) * karteAn) * neuAn;
const hSys = blaseHoehe(sys, spalte) * sysAn;
const hNeu = blaseHoehe(neu, spalte) * wachsen;
const hSys = blaseHoehe(sys, spalte) * system;
const LUFT = 16;
const gesamt = hAlt.reduce((a, h) => a + h + LUFT, 0) +
(neuAn > .01 ? hNeu + LUFT : 0) + (sysAn > .01 ? hSys + LUFT : 0);
(wachsen > .01 ? hNeu + LUFT : 0) + (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; });
if (neuAn > .01) {
/* Die neue Blase kommt von unten und waechst auf. Sie wird um ihren
unteren Rand skaliert, damit sie sich aus dem Eingabefeld heraus
aufzurichten scheint und nicht aus der Mitte heraus aufplatzt. */
const s = .86 + .14 * neuAn;
const inhalt = karteAn > .01 ? neu : mOhne;
bahn += '<g opacity="' + n1(Math.min(1, neuAn * 1.6)) + '" transform="translate(0,' +
n1((1 - neuAn) * 34) + ') translate(' + n1(B / 2) + ',' + n1(y + hNeu) +
') scale(' + n1(s) + ') translate(' + n1(-B / 2) + ',' + n1(-(y + hNeu)) + ')">' +
blase(RAND, y, spalte, inhalt) + '</g>';
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 (sysAn > .01) {
bahn += '<g opacity="' + n1(sysAn) + '" transform="translate(0,' +
n1((1 - sysAn) * 12) + ')">' + blase(RAND, y, spalte, sys) + '</g>';
if (system > .01) {
bahn += '<g opacity="' + n1(system) + '" transform="translate(0,' +
n1((1 - system) * 12) + ')">' + blase(RAND, y, spalte, sys) + '</g>';
}
// Was ueber die Bahn hinausragt, wird oben abgeschnitten -- wie im Fenster.
@@ -3364,23 +3440,25 @@ if (location.search.indexOf("nur=anim") >= 0)
/* 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 Composer auch. */
Sendeknopf -- so macht es der Composer auch. */
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", RAND + 14, fy + 44, 21, CHAT_TEXT2, 2);
g += feather("plus", plusX, plusY, 21, menueAn > .3 ? CHAT_TEXT : 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>';
const zeichen = Math.round(ANIM_TIPP.length * tippen);
const getippt = leeren > .5 || zurueck > .5 ? "" : ANIM_TIPP.slice(0, zeichen);
// 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.
const blink = Math.floor(t * (SLIDES[j].dauer || 5) * 2) % 2 === 0;
if (blink && tippen < 1) {
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 + '"/>';
@@ -3388,13 +3466,43 @@ if (location.search.indexOf("nur=anim") >= 0)
} else {
g += uiText(feldX + 18, fy + 50, "Nachricht schreiben …", 16, 400, "#C9C9CE");
}
const senden = getippt.length > 0;
if (senden) {
const px = B - RAND - 24, py = fy + 44;
g += '<circle cx="' + n1(px) + '" cy="' + n1(py) + '" r="19" fill="#24242B"/>';
g += feather("send", px - 1, py, 18, "#FFFFFF", 2);
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, fy + 44, 21, CHAT_TEXT2, 2);
g += feather("mic", B - RAND - 24, plusY, 21, CHAT_TEXT2, 2);
}
/* 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);
const zAn = zeigerAn * (1 - zWeg);
if (zAn > .01) {
const vonX = B - 70, vonY = H + 40;
const p1 = daempfen(zeigerAn);
let zx = vonX + (plusX + 2 - vonX) * p1;
let zy = vonY + (plusY + 2 - vonY) * p1;
if (wandern > 0) {
const p2 = wandern;
zx += (m.zielX - 40 - zx) * p2;
zy += (m.zielY - 6 - zy) * p2;
}
g += klickRing(zx - 2, zy - 2, klick1 < 1 ? klick1 : 0);
g += klickRing(zx - 2, zy - 2, klick2 < 1 && klick2 > 0 ? klick2 : 0);
g += '<g opacity="' + n1(Math.min(1, zeigerAn * 2) * (1 - zWeg)) + '">' +
zeiger(zx, zy, 1.5) + '</g>';
}
return stapelKasten(B, H, 22, g);