feat(docs-infra): react with Angie in the playground minigame

The angle-guessing minigame's result popup personifies your score with a
hand-drawn stick-figure that changes expression by accuracy. Now that Angie
appears across adev — the 404 page, docs search, the embedded editor, the
tutorial intros and completions — give the minigame the same treatment so
its result feels consistent with the rest of the docs.

Replace the result reactor's stick-figure illustration with an Angie pose
keyed to accuracy (seven tiers, superhero down to angry). She is revealed
once the accuracy counter finishes counting up, popping in beside a speech
bubble that carries the round's existing quote. The result popup is widened
so Angie and the bubble sit side by side, and the share link is moved from
twitter.com to x.com.

This removes the previous hand-drawn stick-figure result art. NG the Angle,
the interactive character in the play area, is unchanged.
This commit is contained in:
Kam
2026-07-21 17:22:58 +03:00
committed by Pawel Kozlowski
parent 791b0646c8
commit 8422da814d
10 changed files with 83 additions and 87 deletions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.5 KiB

@@ -234,38 +234,73 @@ a.gradient-button:focus-visible span:nth-of-type(2) {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
width: min(90%, 480px);
display: flex;
flex-direction: column;
justify-content: space-around;
justify-content: center;
align-items: center;
gap: 1rem;
padding: 2rem;
z-index: 10;
}
svg.personified {
height: 125px;
.angie-reaction {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.personified .g {
fill: #fff;
.personified {
height: 160px;
width: auto;
flex: none;
animation: pop-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.personified .h {
stroke-miterlimit: 10;
stroke-width: 4px;
@keyframes pop-in {
from {
opacity: 0;
transform: scale(0.6) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.personified .h,
.personified .i {
fill: none;
stroke: #000;
stroke-linecap: round;
.result-quote {
position: relative;
margin-top: 2rem;
max-width: 32ch;
padding: 0.85rem 1.25rem;
border-radius: 1rem;
background: #f1f1f3;
text-align: center;
animation: fade-in 0.4s ease 0.15s both;
}
.personified .i {
stroke-linejoin: round;
stroke-width: 3px;
.result-quote::before {
content: '';
position: absolute;
top: 1.1rem;
left: -0.4rem;
width: 0.9rem;
height: 0.9rem;
background: inherit;
transform: rotate(45deg);
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#close {
@@ -170,73 +170,12 @@
<span>{{ animatedAccuracy() | number: '1.1-1' }}%</span>
accurate
</h2>
<svg class="personified" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119.07 114.91">
<g>
<polyline class="i" points="1.5 103.62 56.44 1.5 40.73 8.68" />
<line class="i" x1="59.1" y1="18.56" x2="56.44" y2="1.5" />
<polyline class="i" points="1.61 103.6 117.57 102.9 103.74 92.56" />
<line class="i" x1="103.86" y1="113.41" x2="117.57" y2="102.9" />
<path class="i" d="m12.97,84.22c6.4,4.04,10.47,11.28,10.2,19.25" />
</g>
@if (animatedAccuracy() > 95) {
<g>
<path
class="i"
d="m52.68,72.99c-.04.35-.07.71-.07,1.07,0,6.5,7.28,11.77,16.26,11.77s16.26-5.27,16.26-11.77c0-.36-.03-.72-.07-1.07h-32.37Z"
/>
<circle cx="51.13" cy="53.25" r="6.73" />
<circle cx="86.5" cy="53.25" r="6.73" />
<circle class="g" cx="48.73" cy="51.28" r="2.31" />
<circle class="g" cx="84.35" cy="51.18" r="2.31" />
</g>
} @else if (animatedAccuracy() > 80) {
<g>
<path class="h" d="m52.59,70.26c3.95,4.3,10.25,7.08,17.34,7.08s13.38-2.78,17.34-7.08" />
<path
class="h"
d="m43.44,54.08c1.82-3.11,5.2-5.19,9.06-5.19,3.62,0,6.82,1.84,8.71,4.63"
/>
<path
class="h"
d="m96.41,54.08c-1.82-3.11-5.2-5.19-9.06-5.19-3.62,0-6.82,1.84-8.71,4.63"
/>
</g>
} @else if (animatedAccuracy() > 60) {
<g>
<path
class="h"
d="m77.38,76.81v5.99c0,4.21-3.41,7.62-7.62,7.62h0c-4.21,0-7.62-3.41-7.62-7.62v-5.99"
/>
<line class="h" x1="69.69" y1="82.25" x2="69.76" y2="77.47" />
<path class="h" d="m52.42,70.4c3.95,4.3,10.25,7.08,17.34,7.08s13.38-2.78,17.34-7.08" />
<path
class="h"
d="m43.28,54.21c1.82-3.11,5.2-5.19,9.06-5.19,3.62,0,6.82,1.84,8.71,4.63"
/>
<path
class="h"
d="m96.24,54.21c-1.82-3.11-5.2-5.19-9.06-5.19-3.62,0-6.82,1.84-8.71,4.63"
/>
</g>
} @else if (animatedAccuracy() > 40) {
<g>
<circle cx="51.55" cy="53.15" r="6.73" />
<circle cx="86.92" cy="53.15" r="6.73" />
<circle class="g" cx="49.15" cy="51.17" r="2.31" />
<circle class="g" cx="84.77" cy="51.08" r="2.31" />
<line class="h" x1="61.21" y1="76.81" x2="78.15" y2="76.81" />
</g>
} @else {
<g>
<circle cx="51.55" cy="53.12" r="6.73" />
<circle cx="86.92" cy="53.12" r="6.73" />
<circle class="g" cx="49.15" cy="51.14" r="2.31" />
<circle class="g" cx="84.77" cy="51.05" r="2.31" />
<path class="h" d="m84.01,81.41c-2.37-5.86-8.11-10-14.83-10s-12.45,4.14-14.83,10" />
</g>
}
</svg>
<div>"{{ resultQuote() }}"</div>
@if (showPose()) {
<div class="angie-reaction">
<img class="personified" [src]="resultPose()" alt="" aria-hidden="true" />
<div class="result-quote">{{ resultQuote() }}</div>
</div>
}
<div class="result-buttons">
<button type="button" (click)="close()" class="gradient-button">
<span></span><span>again?</span>
@@ -244,7 +183,7 @@
<a
target="_blank"
class="gradient-button"
[href]="'https://twitter.com/intent/tweet?text=' + getText()"
[href]="'https://x.com/intent/tweet?text=' + getText()"
><span></span><span>share<img src="share.svg" aria-hidden="true" /></span
></a>
</div>
@@ -62,6 +62,7 @@ export class Playground {
protected readonly rotateVal = signal(40);
protected readonly goal = signal(85);
protected readonly animatedAccuracy = signal(0);
protected readonly finalAccuracy = signal(0);
protected readonly gameStats = signal({
level: 0,
totalAccuracy: 0,
@@ -100,6 +101,19 @@ export class Playground {
return this.currentInteractions;
});
protected readonly resultPose = computed(() => {
const accuracy = this.finalAccuracy();
if (accuracy > 95) return 'superhero.svg';
if (accuracy > 80) return 'greeting.svg';
if (accuracy > 60) return 'coffee.svg';
if (accuracy > 40) return 'sad.svg';
if (accuracy > 20) return 'error.svg';
if (accuracy > 10) return 'laughing.svg';
return 'angry.svg';
});
protected readonly showPose = computed(() => this.animatedAccuracy() >= this.finalAccuracy());
protected readonly rotation = computed(() => `rotate(${this.rotateVal()}deg)`);
protected readonly indicatorStyle = computed(() => 0.487 * this.rotateVal() - 179.5);
@@ -161,6 +175,7 @@ export class Playground {
this.isGuessModalOpen.set(true);
const calcAcc = Math.abs(100 - (Math.abs(this.goal() - this.rotateVal()) / 180) * 100);
this.resultQuote.set(getResultQuote(calcAcc));
this.finalAccuracy.set(calcAcc);
this.animatedAccuracy.set(calcAcc > 20 ? calcAcc - 20 : 0);
this.powerUpAccuracy(calcAcc);
this.gameStats.update(({level, totalAccuracy}) => ({
@@ -203,9 +218,9 @@ export class Playground {
emojiAccuracy += roundedAcc >= 20 * (i + 1) ? '🟩' : '⬜️';
}
return encodeURI(
`📐 ${emojiAccuracy} \n My angles are ${roundedAcc}% accurate on level ${
`📐 ${emojiAccuracy}\n\nI'm ${roundedAcc}% accurate at angles on level ${
this.gameStats().level
}. \n\nHow @Angular are you? \nhttps://angular.dev/playground`,
}.\n\nHow @Angular are you?\nhttps://angular.dev/playground`,
);
}