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.
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||