refactor(docs-infra): remove the orphaned home animation component

The new home page in #63662 stopped rendering `HomeAnimationComponent`
and its directory was left behind. The animation that runs today lives
in `features/home/animation`, which replaced it.

Nothing references the class, its `adev-home-animation` selector, or any
file in the directory, and no stylesheet or build target pulls it in.
This commit is contained in:
Kam
2026-08-20 16:20:48 +03:00
committed by Leon Senft
parent 4997521685
commit eb6570b06d
4 changed files with 0 additions and 1132 deletions
@@ -1,437 +0,0 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {AnimationDefinition, Styles} from '../../animation';
import {AnimationRule} from '../../animation/types';
/**
* CONSTANTS
*/
// Represents percentage of the total.
// Avoid using large waves (the total shouldn't be too big as well).
// Check meteorShower function for more details.
const FIRST_WAVE_METEORS = 0.05;
const SECOND_WAVE_METEORS = 0.15;
const THIRD_WAVE_METEORS = 0.25;
// Use to increase or decrease the animation duration (i.e. a fine tuning parameter).
// Employed by `timeframe()` and `at()`.
const TIMING_MULTIPLIER = 1.55;
export const ANIM_TIMESTEP = 10; // In milliseconds
/**
* SELECTORS
*/
const BANNERS_LAYER_ID = 'banners';
const ADEV_BANNER = `${BANNERS_LAYER_ID} >> .adev-banner`;
const LEARN_ANGULAR_BTN = `${BANNERS_LAYER_ID} >> .learn-angular`;
const LOGO_LAYER_ID = 'logo';
const LOGO = `${LOGO_LAYER_ID} >> .logo`;
const SHIELD = `${LOGO_LAYER_ID} >> .shield`;
const SHIELD_MIDDLE = `${LOGO_LAYER_ID} >> .shield-middle`;
const SHIELD_BOTTOM_A_ARC = `${LOGO_LAYER_ID} >> .shield-bottom-a-arc`;
const SHIELD_BOTTOM_EXTENSION = `${LOGO_LAYER_ID} >> .shield-bottom-extension`;
const CAPITAL_A_LETTER = `${LOGO_LAYER_ID} >> .capt-a-letter`;
const N_LETTER = `${LOGO_LAYER_ID} >> .n-letter`;
const G_LETTER = `${LOGO_LAYER_ID} >> .g-letter`;
const U_LETTER = `${LOGO_LAYER_ID} >> .u-letter`;
const L_LETTER = `${LOGO_LAYER_ID} >> .l-letter`;
const A_LETTER = `${LOGO_LAYER_ID} >> .a-letter`;
const R_LETTER = `${LOGO_LAYER_ID} >> .r-letter`;
const UWU_LAYER_ID = 'uwu';
const WORKS_AT_ANY_SCALE_LAYER_ID = 'works-at-any-scale';
const METEOR_FIELD_LAYER_ID = 'meteor-field';
const METEOR_FIELD = `${METEOR_FIELD_LAYER_ID} >> .field`;
const METEORS = `${METEOR_FIELD_LAYER_ID} >> .meteor`;
const METEOR_ID = (id: number) => `${METEOR_FIELD_LAYER_ID} >> .mt-${id}`;
const LOVED_BY_MILLIONS_LAYER_ID = 'loved-by-millions';
const BUILD_FOR_EVERYONE_LAYER_ID = 'build-for-everyone';
const BUILD_FOR_EVERYONE_TITLE = `${BUILD_FOR_EVERYONE_LAYER_ID} >> .title`;
/**
* ANIMATION/HELPER FUNCTIONS
*/
// Timing functions (they employ `TIMING_MULTIPLIER`) – Use when setting the time of a rule.
const at = (at: number): number => at * TIMING_MULTIPLIER;
const timeframe = (from: number, to: number): [number, number] => [
from * TIMING_MULTIPLIER,
to * TIMING_MULTIPLIER,
];
/** Duration: 1 second */
function hideLetter(selector: string, startTime: number): AnimationRule<Styles> {
return {
selector,
timeframe: timeframe(startTime, startTime + 1),
from: {
opacity: '1',
},
to: {
opacity: '0',
},
};
}
/** Duration: 1 to 2 seconds */
function showMeteor(selector: string, startTime: number): AnimationRule<Styles> {
const randomizedStartTime = startTime + Math.random(); // Up to +1 second (excl.)
return {
selector,
timeframe: timeframe(randomizedStartTime, randomizedStartTime + 1),
from: {
opacity: '0',
transform: 'translate(200%, 200%) scale(0.3)',
},
to: {
opacity: '1',
transform: 'translate(0, 0) scale(1)',
},
};
}
/** Duration: 1 to 2 seconds */
function meteorShower(
startTime: number,
size: number,
total: number,
inUse: Set<number>,
): AnimationDefinition {
const animations: AnimationRule<Styles>[] = [];
while (animations.length < size) {
// We pick a random meteor ID.
// If `inUse` is nearly full relative to `total`,
// we might run into a excessive amount of iterations
// until we fill `animation`. This is why we should keep
// the wave sizes (and their total) relatively small.
const id = Math.round(Math.random() * (total - 1) + 1);
if (!inUse.has(id)) {
animations.push(showMeteor(METEOR_ID(id), startTime));
inUse.add(id);
}
}
return animations;
}
/**
* DEFINITION
*/
/** Generate the animation definition for the home page animation. */
export function generateHomeAnimationDefinition(
isUwu: boolean,
meteorCount: number,
): AnimationDefinition {
// Banners and buttons layer
// *************************
const bannersLayerAnim: AnimationDefinition = [
{
selector: ADEV_BANNER,
timeframe: timeframe(2, 3),
from: {
transform: 'translateY(0)',
},
to: {
transform: 'translateY(-200px)',
},
},
{
selector: LEARN_ANGULAR_BTN,
timeframe: timeframe(2.5, 3.5),
from: {
opacity: '1',
},
to: {
opacity: '0',
},
},
{
selector: LEARN_ANGULAR_BTN,
at: at(4),
styles: {
visibility: 'hidden',
},
},
];
// Logo layer animation
// ********************
const logoLayerAnim: AnimationDefinition = [
{
selector: LOGO,
timeframe: timeframe(0, 5),
from: {
transform: 'translateX(0)',
},
to: {
transform: 'translateX(467px)', // Value based on the 1280x400 SVG view box
},
},
hideLetter(R_LETTER, 1),
hideLetter(A_LETTER, 1.5),
hideLetter(L_LETTER, 2),
hideLetter(U_LETTER, 2.5),
hideLetter(G_LETTER, 3),
hideLetter(N_LETTER, 3.5),
// Make sure that the last letter disappears at the end of layer transition,
// i.e. 4 + 1 = 5th second end time
hideLetter(CAPITAL_A_LETTER, 4),
{
selector: SHIELD_MIDDLE,
timeframe: timeframe(5.5, 5.6),
from: {
transform: 'scale(1)',
},
to: {
transform: 'scale(0)',
},
},
{
selector: SHIELD_BOTTOM_A_ARC,
timeframe: timeframe(5.5, 5.6),
from: {
transform: 'scaleY(1)',
},
to: {
transform: 'scaleY(0)',
},
},
{
selector: SHIELD_BOTTOM_EXTENSION,
timeframe: timeframe(5.5, 5.6),
from: {
transform: 'scale(0)',
},
to: {
transform: 'scale(1)',
},
},
{
selector: SHIELD,
timeframe: timeframe(5.5, 10),
from: {
transform: 'scale(1) rotate(0deg)',
},
to: {
transform: 'scale(50) rotate(-360deg)',
},
},
];
// "UwU logo" layer animation
// **************************
const uwuLayerAnimation: AnimationDefinition = [
{
selector: UWU_LAYER_ID,
timeframe: timeframe(0, 5.5),
from: {
transform: 'scale(1)',
},
to: {
transform: 'scale(0)',
},
},
{
selector: UWU_LAYER_ID,
timeframe: timeframe(4, 5.5),
from: {
opacity: '1',
},
to: {
opacity: '0',
},
},
];
// "Works at any scale" layer animation
// ************************************
const waasLayerAnim: AnimationDefinition = [
{
selector: WORKS_AT_ANY_SCALE_LAYER_ID,
timeframe: timeframe(5.7, 8), // Make sure it appears after SHIELD_MIDDLE disappears.
from: {
transform: 'scale(0.1)',
opacity: '0',
},
to: {
transform: 'scale(1)',
opacity: '1',
},
},
{
selector: WORKS_AT_ANY_SCALE_LAYER_ID,
timeframe: timeframe(11, 12.5),
from: {
transform: 'scale(1)',
opacity: '1',
},
to: {
transform: 'scale(1.5)',
opacity: '0',
},
},
];
// Meteor field layer animation
// ****************************
const firstWaveSize = meteorCount * FIRST_WAVE_METEORS;
const secondWaveSize = meteorCount * SECOND_WAVE_METEORS;
const thirdWaveSize = meteorCount * THIRD_WAVE_METEORS;
const meteorsInUse = new Set<number>();
const firstWave = meteorShower(8, firstWaveSize, meteorCount, meteorsInUse);
const secondWave = meteorShower(10, secondWaveSize, meteorCount, meteorsInUse);
const thirdWave = meteorShower(12, thirdWaveSize, meteorCount, meteorsInUse);
const lastWaveStart = 16;
// For the last wave, just use the remaining meteors (don't use `meteorShower`).
const lastWave: AnimationRule<Styles>[] = [];
for (let id = 1; id <= meteorCount; id++) {
if (!meteorsInUse.has(id)) {
lastWave.push(showMeteor(METEOR_ID(id), lastWaveStart));
}
}
const meteorFieldLayerAnim: AnimationDefinition = [
{
selector: METEOR_FIELD,
at: at(7),
styles: {
display: 'flex',
},
},
{
selector: METEOR_FIELD,
timeframe: timeframe(8, 18),
from: {
transform: 'scale(1.42)',
},
to: {
transform: 'scale(1)',
},
},
...firstWave,
...secondWave,
...thirdWave,
...lastWave,
{
selector: METEORS,
timeframe: timeframe(19.5, 21),
from: {
transform: 'translate(0, 0) scale(1)',
},
to: {
transform: 'translate(-200%, -200%) scale(0.3)',
},
},
{
selector: METEOR_FIELD,
timeframe: timeframe(19.5, 21),
from: {
opacity: '1',
},
to: {
opacity: '0',
},
},
{
selector: METEOR_FIELD,
at: at(22),
styles: {
display: 'none',
},
},
];
// "Loved by millions" layer animation
// ***********************************
const lovedByMillionsAnim: AnimationDefinition = [
{
selector: LOVED_BY_MILLIONS_LAYER_ID,
timeframe: timeframe(14, 15.5),
from: {
transform: 'scale(0.75)',
opacity: '0',
},
to: {
transform: 'scale(1)',
opacity: '1',
},
},
{
selector: LOVED_BY_MILLIONS_LAYER_ID,
timeframe: timeframe(19, 20.5),
from: {
transform: 'scale(1)',
opacity: '1',
},
to: {
transform: 'scale(1.5)',
opacity: '0',
},
},
];
// "Build for everyone" layer
// **************************
const buildForEveryoneAnim: AnimationDefinition = [
{
selector: BUILD_FOR_EVERYONE_LAYER_ID,
timeframe: timeframe(22, 25),
from: {
transform: 'scale(0.75)',
opacity: '0',
},
to: {
transform: 'scale(1)',
opacity: '1',
},
},
{
selector: BUILD_FOR_EVERYONE_TITLE,
timeframe: timeframe(23, 25),
from: {
'background-position-x': '100%',
},
to: {
'background-position-x': '0',
},
},
{
selector: BUILD_FOR_EVERYONE_LAYER_ID,
timeframe: timeframe(29, 31.5),
from: {
opacity: '1',
},
to: {
opacity: '0',
},
},
];
return [
...bannersLayerAnim,
...(!isUwu ? logoLayerAnim : uwuLayerAnimation),
...waasLayerAnim,
...meteorFieldLayerAnim,
...lovedByMillionsAnim,
...buildForEveryoneAnim,
];
}
@@ -1,122 +0,0 @@
<div class="animation" [class.reduced-motion]="reducedMotion()">
@if (!isUwu()) {
<!-- Angular logo -->
<div adevAnimationLayer layerId="logo" class="logo-layer layer">
<svg class="svg" viewBox="0 0 1280 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Do not remove `gradientTransform` attribute. Safari needs it in order to start the animation. -->
<linearGradient
id="gradient"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(0 640 125)"
>
<stop offset="0" stop-color="#ed0100" />
<stop offset="0.25" stop-color="#e449b4" />
<stop offset="0.5" stop-color="#ae3bfc" />
<stop offset="1" stop-color="#7f15fd" />
<animateTransform
attributeName="gradientTransform"
type="rotate"
from="0 640 125"
to="360 640 125"
dur="6.5s"
repeatCount="indefinite"
/>
</linearGradient>
</defs>
<g class="wrapper" fill="url(#gradient)">
<g class="logo">
<g class="shield">
<path
class="shield-base"
d="M311.8,101.7L302,255.6L208.3,53.8L311.8,101.7ZM246.9,300.7L176.1,341.1L105.3,300.7L246.9,300.7ZM50.1,255.6L40.4,101.7L143.9,53.8L50.1,255.6Z"
/>
<path class="shield-middle" d="M176.1,130.4L213.2,220.6L139,220.6L176.1,130.4Z" />
<path
class="shield-bottom-a-arc"
d="M106.419,301.339L105.3,300.7L119.7,265.8L232.5,265.8L246.9,300.7L245.781,301.339L106.419,301.339Z"
/>
<path
class="shield-bottom-extension"
d="M245.78,301.339L106.42,301.339L68.889,279.923L283.311,279.923L245.78,301.339Z"
/>
</g>
<path
class="capt-a-letter letter"
d="M515.4,288L553.1,288L484.4,92.7L440.7,92.7L372,288L409.7,288L425.8,239.9L499.2,239.9L515.4,288ZM435.4,211.4L461.8,132.8L463.3,132.8L489.7,211.4L435.4,211.4Z"
/>
<path
class="n-letter letter"
d="M590.4,202.2L590.4,288L555.9,288L555.9,141.6L588.9,141.6L588.9,166.5L590.6,166.5C594,158.3 599.4,151.8 606.8,147C614.2,142.2 623.4,139.8 634.3,139.8C644.4,139.8 653.2,142 660.8,146.3C668.3,150.6 674.2,156.9 678.3,165.1C682.5,173.3 684.5,183.2 684.4,194.9L684.4,288L649.9,288L649.9,200.1C649.9,190.3 647.4,182.7 642.3,177.1C637.2,171.6 630.3,168.8 621.4,168.8C615.4,168.8 610,170.1 605.3,172.8C600.6,175.4 597,179.2 594.3,184.2C591.8,189.1 590.4,195.1 590.4,202.2Z"
/>
<path
class="g-letter letter"
d="M760.5,345.9C748.1,345.9 737.5,344.2 728.6,340.9C719.7,337.6 712.6,333.1 707.2,327.5C701.8,321.9 698,315.7 696,308.9L727.1,301.4C728.5,304.3 730.5,307.1 733.2,309.9C735.9,312.7 739.5,315.1 744,317C748.5,318.9 754.3,319.8 761.2,319.8C771,319.8 779.1,317.4 785.5,312.7C791.9,308 795.1,300.2 795.1,289.4L795.1,261.7L793.4,261.7C791.6,265.3 789,268.9 785.6,272.7C782.2,276.5 777.7,279.6 772.2,282.1C766.6,284.6 759.7,285.9 751.3,285.9C740.1,285.9 729.9,283.2 720.7,277.9C711.6,272.6 704.3,264.6 699,254.1C693.6,243.5 690.9,230.3 690.9,214.3C690.9,198.2 693.6,184.6 699,173.5C704.4,162.4 711.6,154 720.8,148.2C730,142.4 740.2,139.6 751.4,139.6C760,139.6 767.1,141 772.6,143.9C778.2,146.8 782.6,150.3 785.9,154.3C789.2,158.3 791.7,162.1 793.4,165.7L795.3,165.7L795.3,141.5L829.3,141.5L829.3,290.3C829.3,302.8 826.3,313.2 820.3,321.4C814.3,329.6 806.2,335.7 795.8,339.8C785.4,343.9 773.6,345.9 760.5,345.9ZM760.8,258.8C768.1,258.8 774.3,257 779.5,253.5C784.6,249.9 788.6,244.8 791.2,238.2C793.9,231.5 795.2,223.5 795.2,214.2C795.2,205 793.9,196.9 791.2,190C788.6,183.1 784.7,177.7 779.6,173.8C774.5,170 768.2,168 760.8,168C753.1,168 746.7,170 741.5,174C736.4,178 732.5,183.5 729.9,190.4C727.3,197.4 726,205.3 726,214.1C726,223.1 727.3,230.9 730,237.6C732.6,244.3 736.5,249.5 741.7,253.2C746.8,256.9 753.2,258.8 760.8,258.8Z"
/>
<path
class="u-letter letter"
d="M933.8,226.4L933.8,141.6L968.3,141.6L968.3,288L934.8,288L934.8,262L933.3,262C930,270.2 924.6,276.9 917,282.1C909.5,287.3 900.2,289.9 889.2,289.9C879.6,289.9 871.1,287.8 863.8,283.5C856.5,279.2 850.7,273 846.6,264.7C842.5,256.5 840.4,246.5 840.4,234.8L840.4,141.6L875,141.6L875,229.5C875,238.8 877.5,246.2 882.6,251.6C887.7,257.1 894.4,259.8 902.6,259.8C907.7,259.8 912.6,258.6 917.4,256.1C922.2,253.6 926.1,249.9 929.2,245C932.3,240 933.8,233.8 933.8,226.4Z"
/>
<rect class="l-letter letter" x="979.6" y="92.7" width="34.5" height="195.3" />
<path
class="a-letter letter"
d="M1068.9,290.9C1059.6,290.9 1051.3,289.2 1043.9,285.9C1036.5,282.6 1030.6,277.6 1026.4,271.1C1022.1,264.6 1020,256.5 1020,246.9C1020,238.6 1021.5,231.8 1024.6,226.4C1027.7,221 1031.8,216.7 1037.1,213.4C1042.4,210.2 1048.3,207.7 1055,206C1061.6,204.3 1068.5,203.1 1075.6,202.3C1084.2,201.4 1091.1,200.6 1096.5,199.9C1101.8,199.2 1105.7,198 1108.2,196.5C1110.6,194.9 1111.9,192.5 1111.9,189.2L1111.9,188.6C1111.9,181.4 1109.8,175.9 1105.5,171.9C1101.2,168 1095.1,166 1087.1,166C1078.6,166 1072,167.8 1067,171.5C1062.1,175.2 1058.8,179.5 1057,184.6L1024.8,180C1027.3,171.1 1031.5,163.7 1037.4,157.6C1043.2,151.6 1050.4,147.1 1058.8,144.1C1067.3,141.1 1076.6,139.6 1086.8,139.6C1093.9,139.6 1100.9,140.4 1107.9,142.1C1114.9,143.8 1121.3,146.5 1127.1,150.3C1132.9,154.1 1137.5,159.2 1141.1,165.7C1144.6,172.2 1146.4,180.3 1146.4,190L1146.4,288L1113.2,288L1113.2,267.9L1112.1,267.9C1110,272 1107.1,275.8 1103.3,279.3C1099.5,282.8 1094.8,285.7 1089.1,287.8C1083.2,289.9 1076.6,290.9 1068.9,290.9ZM1077.9,265.6C1084.8,265.6 1090.8,264.2 1095.9,261.5C1101,258.7 1104.9,255.1 1107.7,250.5C1110.5,245.9 1111.8,240.9 1111.8,235.5L1111.8,218.2C1110.7,219.1 1108.9,219.9 1106.3,220.7C1103.7,221.5 1100.9,222.1 1097.7,222.7C1094.5,223.3 1091.4,223.8 1088.3,224.2C1085.2,224.6 1082.5,225 1080.2,225.3C1075.1,226 1070.4,227.1 1066.4,228.7C1062.3,230.3 1059.1,232.5 1056.8,235.3C1054.4,238.1 1053.3,241.8 1053.3,246.2C1053.3,252.6 1055.6,257.4 1060.3,260.6C1064.8,264 1070.7,265.6 1077.9,265.6Z"
/>
<path
class="r-letter letter"
d="M1156.9,288L1156.9,141.6L1190.4,141.6L1190.4,166L1191.9,166C1194.6,157.5 1199.2,151 1205.7,146.4C1212.2,141.8 1219.7,139.5 1228.1,139.5C1230,139.5 1232.2,139.6 1234.5,139.7C1236.9,139.9 1238.9,140.1 1240.5,140.4L1240.5,172.1C1239,171.6 1236.7,171.1 1233.6,170.7C1230.5,170.3 1227.4,170.1 1224.5,170.1C1218.2,170.1 1212.6,171.5 1207.6,174.2C1202.6,176.9 1198.7,180.6 1195.8,185.4C1192.9,190.2 1191.5,195.7 1191.5,201.9L1191.5,288L1156.9,288Z"
/>
</g>
</g>
</svg>
</div>
} @else {
<!-- UwU logo -->
<div adevAnimationLayer layerId="uwu" class="uwu-layer layer">
<img src="assets/images/uwu.png" alt="Angular logo" />
</div>
}
<!-- Works at any scale -->
<div adevAnimationLayer layerId="works-at-any-scale" class="works-at-any-scale-layer layer">
<h2>Works at any scale</h2>
<p>
Angular lets you start small on a well-lit path and supports you as your team and apps grow.
</p>
</div>
<!-- Meteor field -->
@if (meteorFieldData(); as meteorFieldData) {
<div adevAnimationLayer layerId="meteor-field" class="meteor-field-layer layer">
<div
class="field"
[style.width]="meteorFieldData.width + 'px'"
[style.height]="meteorFieldData.height + 'px'"
[style.marginLeft]="meteorFieldData.marginLeft + 'px'"
[style.marginTop]="meteorFieldData.marginTop + 'px'"
>
@for (type of meteors(); track $index) {
<div [class]="['meteor', 'type-' + type, 'mt-' + ($index + 1)]"></div>
}
</div>
</div>
}
<!-- Loved by millions -->
<div adevAnimationLayer layerId="loved-by-millions" class="loved-by-millions-layer layer">
<h2>Loved by millions</h2>
<p>
Join the millions of developers all over the world building with Angular in a thriving and
friendly community.
</p>
</div>
<!-- Build for everyone -->
<div adevAnimationLayer layerId="build-for-everyone" class="build-for-everyone-layer layer">
<h2 class="title">Build for everyone</h2>
<p>
Rely on Angular's built-in hydration, internationalization, security, and accessibility
support to build for everyone around the world.
</p>
</div>
</div>
@@ -1,403 +0,0 @@
@use 'sass:math';
@use '@angular/docs/styles/media-queries' as mq;
/*
* Global transition.
*
* Scrolling via mouse scroll wheel will result in a non-continuous scrollY,
* unlike a touchpad, which in turn results in "gaps" in the animation calculations.
* Therefore, in order to keep things smooth, we are systematically adding CSS
* transitions to the CSS properties that else look jagged when scrolling.
*/
$transition: 200ms linear;
:host {
display: block;
position: relative;
.animation {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
.layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
h2 {
font-size: 4vw;
font-weight: 600;
white-space: nowrap;
margin-top: 0;
margin-bottom: 0.5em;
z-index: 1;
@include mq.for-tablet-landscape-down {
& {
font-size: 2rem;
}
}
}
p {
font-weight: 400;
color: var(--quaternary-contrast);
font-size: clamp(1rem, 1vw, 2rem);
line-height: 1.5;
width: clamp(345px, 50%, 600px);
margin: 0 auto;
}
h2,
p {
background-color: var(--page-background);
box-shadow: 0 0 20px 20px var(--page-background);
@include mq.for-desktop-down {
box-shadow: 0 0 10px 10px var(--page-background);
}
}
}
.banners-layer {
z-index: 10;
.adev-banner-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
position: absolute;
transition:
background 0.3s ease,
border 0.3s ease,
transform $transition;
top: var(--layout-padding);
left: calc(var(--layout-padding) + var(--primary-nav-width));
@include mq.for-tablet-landscape-down {
top: 6rem;
left: var(--layout-padding);
/* Assuming the container width is identical to the viewport width (mobile device). */
max-width: calc(100% - var(--layout-padding) * 2);
}
@include mq.for-phone-only {
& {
top: 5rem;
}
}
}
.adev-banner {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
border: 1px solid var(--senary-contrast);
background: var(--page-background);
border-radius: 0.25rem;
padding: 10px;
max-width: 100%;
width: fit-content;
box-sizing: border-box;
h1,
p {
display: inline;
font-size: 0.875rem;
margin: 0;
background-image: var(--red-to-pink-to-purple-horizontal-gradient);
background-clip: text;
color: transparent;
width: fit-content;
font-weight: 500;
box-shadow: none;
position: relative;
&.adev-banner-cta {
color: var(--tertiary-contrast);
&::after {
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 1px;
bottom: -2px;
left: 0;
background: var(--red-to-pink-to-purple-horizontal-gradient);
transform-origin: bottom right;
transition: transform 0.3s ease;
}
}
}
&:hover {
.adev-banner-cta {
&::after {
transform: scaleX(1);
transform-origin: bottom left;
}
}
}
}
.learn-angular {
position: absolute;
left: 50%;
bottom: 5%;
transform: translateX(-50%);
transition: opacity $transition;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 50px;
button {
font-size: 1rem;
padding: 1rem 1.75rem;
&::after {
font-size: 1rem;
}
}
.adev-arrow {
transform: rotate(45deg);
border: solid var(--primary-contrast);
border-width: 0 2px 2px 0;
display: inline-block;
padding: 7px;
}
}
}
.logo-layer {
user-select: none;
pointer-events: none;
.svg {
position: absolute;
width: 100%;
height: 100%;
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
}
.wrapper {
transform: scale(0.55);
transform-origin: 50% 50%;
@include mq.for-tablet-down {
& {
transform: scale(0.8);
}
}
@include mq.for-extra-large-desktop-up {
& {
transform: scale(0.5);
}
}
.logo {
transition: transform $transition;
.shield {
/* `transform-origin` values are calculated based on the 1280x400 SVG view box */
&,
.shield-middle {
transform-origin: 177px 200px;
transition: transform $transition;
}
.shield-bottom-a-arc {
transform-origin: 0 302px;
transition: transform $transition;
}
.shield-bottom-extension {
transform-origin: 177px 340px;
transition: transform $transition;
transform: scale(0);
}
}
.letter {
transition: opacity $transition;
}
}
}
}
.uwu-layer {
display: flex;
align-items: center;
justify-content: center;
transition:
transform $transition,
opacity $transition;
img {
user-select: none;
width: max(calc(700 * min(100vw, 2560px) / 1470), 350px);
height: auto;
}
}
.works-at-any-scale-layer,
.loved-by-millions-layer,
.build-for-everyone-layer {
user-select: none;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
opacity: 0;
z-index: 1;
transition:
transform $transition,
opacity $transition;
}
.works-at-any-scale-layer {
transform: scale(0);
}
.loved-by-millions-layer,
.build-for-everyone-layer {
transform: scale(0.75);
}
.build-for-everyone-layer {
.title {
color: transparent;
display: inline-block;
background: linear-gradient(110deg, #f31a5b 0, #8737e9 50%, #3a373f 0%);
background-size: 205% 100%;
background-clip: text;
background-size: 205% 100%;
background-position-x: 100%;
transition: background-position-x $transition;
}
}
.meteor-field-layer {
overflow: hidden;
.field {
--math-pi: #{math.$pi}rad;
position: absolute;
flex-wrap: wrap;
align-content: flex-start;
gap: var(--meteor-gap);
display: none;
transform: scale(1.42);
top: 0;
left: 0;
transform-origin: 50% 50%;
transition:
opacity $transition,
transform $transition;
.meteor {
position: relative;
width: var(--meteor-width);
height: var(--meteor-height);
opacity: 0;
transform: translate(200%, 200%) scale(0.3);
transition:
opacity $transition,
transform $transition;
&::after {
content: '';
position: absolute;
width: 4px;
height: var(--meteor-tail-length);
border-radius: 2px;
transform-origin: top center;
transform: rotate(var(--meteor-tilt-angle));
top: 0;
left: 0;
}
@mixin meteor-gradient($startColor, $endColor) {
background: linear-gradient(
calc(var(--math-pi) + var(--meteor-tilt-angle)),
$startColor 0,
$endColor 66%,
transparent 100%
);
}
&.type-1::after {
@include meteor-gradient(rgb(228, 49, 85), rgb(219, 64, 219));
}
&.type-2::after {
@include meteor-gradient(rgb(152, 56, 226), rgb(217, 18, 167));
}
&.type-3::after {
@include meteor-gradient(rgb(214, 53, 150), rgb(229, 91, 229));
}
}
}
}
&.reduced-motion {
position: relative;
.banners-layer {
height: 100vh;
@include mq.for-phone-only {
.adev-banner {
top: 6rem;
}
}
@include mq.for-tablet-landscape-up {
.adev-banner {
left: var(--layout-padding);
}
}
}
.logo-layer {
position: relative;
height: 100vh;
@include mq.for-tablet-landscape-down {
margin-top: -75px;
}
}
.works-at-any-scale-layer,
.loved-by-millions-layer,
.build-for-everyone-layer {
position: relative;
height: 120vh;
opacity: 1;
transform: scale(1);
}
.build-for-everyone-layer > .title {
background-position-x: 0;
}
}
}
}
@@ -1,170 +0,0 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
afterNextRender,
Component,
DestroyRef,
ElementRef,
inject,
Injector,
input,
output,
signal,
viewChildren,
} from '@angular/core';
import {isIos, shouldReduceMotion, WINDOW} from '@angular/docs';
import {RouterLink} from '@angular/router';
import {AnimationCreatorService, AnimationLayerDirective} from '../../animation';
import {AnimationScrollHandler} from '../../animation/plugins/animation-scroll-handler';
import {ANIM_TIMESTEP, generateHomeAnimationDefinition} from './animation-definition';
export const METEOR_HW_RATIO = 1.42; // Height to width ratio
export const METEOR_GAP_RATIO = 1.33; // Use 0.7 for WebGL-like field. Renders a lot of elements though.
// A map with screen size to meteor width
export const METEOR_WIDTH_MAP = [
[800, 60],
[1100, 90],
];
export const METEOR_WIDTH_DEFAULT = 120; // For screens larger than 1100px
type MeteorDimensions = {
width: number;
height: number;
tailLength: number;
gap: number;
tiltAngle: number; // In radians
};
type MeteorFieldData = {
width: number;
height: number;
count: number;
marginLeft: number;
marginTop: number;
};
@Component({
selector: 'adev-home-animation',
imports: [AnimationLayerDirective, RouterLink],
templateUrl: './home-animation.component.html',
styleUrl: './home-animation.component.scss',
})
export class HomeAnimationComponent {
private readonly win = inject(WINDOW);
private readonly animCreator = inject(AnimationCreatorService);
private readonly injector = inject(Injector);
private readonly elementRef = inject(ElementRef);
private readonly destroyRef = inject(DestroyRef);
readonly animationLayers = viewChildren(AnimationLayerDirective);
readonly ctaLink = isIos ? 'overview' : 'tutorials/learn-angular';
readonly isUwu = input.required<boolean>();
readonly ready = output<boolean>();
readonly reducedMotion = signal<boolean>(shouldReduceMotion());
readonly meteorFieldData = signal<MeteorFieldData | null>(null);
readonly meteors = signal<number[]>([]);
constructor() {
if (!this.reducedMotion()) {
this.initAnimation();
} else {
this.ready.emit(true);
}
}
private initAnimation() {
// Limitation: Meteor dimensions won't change on page resize
const meteorDimensions = this.calculateMeteorDimensions();
const data = this.calculateMeteorFieldData(meteorDimensions);
this.setCssVariables(meteorDimensions);
this.meteorFieldData.set(data);
// Generate a meteor field. The number represents the type [1, 3]
this.meteors.set(new Array(data.count).fill(1).map(() => Math.round(Math.random() * 2 + 1)));
afterNextRender({
read: () => {
const animation = this.animCreator
.createAnimation(this.animationLayers(), {timestep: ANIM_TIMESTEP})
.define(generateHomeAnimationDefinition(this.isUwu(), this.meteors().length))
.addPlugin(new AnimationScrollHandler(this.elementRef, this.injector));
this.ready.emit(true);
this.destroyRef.onDestroy(() => animation.dispose());
},
});
}
/** Calculate the dimensions and sizes of a meteor – width, height, tail, tilt angle, etc. */
private calculateMeteorDimensions(): MeteorDimensions {
let width = METEOR_WIDTH_DEFAULT;
for (const [screenSize, meteorWidth] of METEOR_WIDTH_MAP) {
if (this.win.innerWidth <= screenSize) {
width = meteorWidth;
}
}
const height = width * METEOR_HW_RATIO;
const gap = width * METEOR_GAP_RATIO;
// Pythagorean theorem + some trigonometry
const tailLength = Math.sqrt(width * width + height * height);
const tiltAngle = -Math.asin(width / tailLength);
return {
width,
height,
gap,
tailLength,
tiltAngle,
};
}
/** Calculate the number of meteors and size of the field. */
private calculateMeteorFieldData(meteorDim: MeteorDimensions): MeteorFieldData {
const mW = meteorDim.width + meteorDim.gap;
const mH = meteorDim.height + meteorDim.gap;
let rows = 1;
let cols = 1;
while (cols * mW - meteorDim.gap <= this.win.innerWidth) {
cols++;
}
while (rows * mH - meteorDim.gap <= this.win.innerHeight) {
rows++;
}
const width = cols * mW - meteorDim.gap;
const height = rows * mH - meteorDim.gap;
return {
count: rows * cols,
width,
height,
marginLeft: -(width - this.win.innerWidth) / 2,
marginTop: -(height - this.win.innerHeight) / 2,
};
}
private setCssVariables({width, height, tailLength, tiltAngle, gap}: MeteorDimensions) {
const styleRef = this.elementRef.nativeElement.style;
styleRef.setProperty('--meteor-width', width + 'px');
styleRef.setProperty('--meteor-height', height + 'px');
styleRef.setProperty('--meteor-tail-length', tailLength + 'px');
styleRef.setProperty('--meteor-tilt-angle', tiltAngle + 'rad');
styleRef.setProperty('--meteor-gap', gap + 'px');
}
}