Fronteers — vakvereniging voor front-end developers

Een "that's all folks" animatie in CSS met slechts één div

CSS is opgebouwd uit allemaal rechthoeken. Rechthoeken kunnen boven of onder andere rechthoeken liggen. Rechthoeken kunnen ook weer andere rechthoeken in zich hebben, en dan kan je er voor kiezen dat de binnenste rechthoeken ook buiten hun omringende rechthoek zichtbaar zijn (dat ze overflowen) of dat ze afgekapt worden door de omringende rechthoek (met overflow:hidden).

Maar als je wilt dat een rechthoek aan één zijde wel buiten de omringende rechthoek zichtbaar is, maar niet aan de andere kant, dan kan dat niet. Toch?

Drie vierkanten waarbij bij de eerste een binnenste element er uit komt met de tekst "Dit kan" er onder. Bij de tweede blijft het binnenste element binnen de zichtbare rand, hier staat "Dit kan ook" onder. Bij de derde komt het binnenste element aan de bovenkant er uit, maar aan de onderkant niet. Hier onder staat "Dit kan ...niet".

Misschien gaan er bij jou nu wel allemaal radertjes draaien: wat nou als ik de binnenste rechthoek kopieer en de helft clip en dan er precies boven positioneer dan lijkt het net alsof... Maar in principe kan je er niet voor kiezen dat één element aan bijvoorbeeld de bovenkant uitsteekt, maar niet aan de onderkant. Of toch wel?

3D transformaties

Een tijdje terug was ik een lijst aan het verzamelen van websites die interessant gebruik maken van CSS 3D transforms. Met CSS 3D transforms kan je elementen in 3D roteren, transformeren en verplaatsen, en ook cross-browser werkt dat ondertussen gewoon prima.

Er zijn twee CSS properties die je moet inzetten om elementen in 3D te kunnen positioneren:

* perspective met een waarde in pixels, om te bepalen hoe sterk het 3d effect moet zijn.
* transform-style: preserve-3d, om de browser te vertellen dat je de 3D-transformaties wilt behouden

Toch zie je het niet veel gebruikt worden, en ook ik had er nog niet heel veel mee geprobeerd. Websites blijven toch een "2d" iets, een scrollende, platte pagina.

De meest interessante 3D transform die ik tegen kwam was deze:

Drie vlakken die in 3d ruimte boven elkaar zweven.

Hoewel je drie vlakken ziet, is dit slechts éen div. De andere twee vlakken zijn de ::before en ::after, die door middel van de translate() CSS functie omhoog en omlaag zijn verplaatst, en zo boven elkaar liggen.

Wat me daar op viel was hoe het ::after element, wat normaal gesproken over een element ligt achter de div zelf zat. Dat kreeg de maker voor elkaar met transform: translateZ(-1px);.

Hoewel ik daarvoor al veel andere transforms had gezien was dit de eerste die me echt deed beseffen dat ik elementen in 3D ruimte aan het positioneren was. En als dat kan, dan kunnen ze elkaar dus ook doorkruizen:

Twee vlakken die elkaar in 3D ruimte doorkruizen.

Ik wist niet direct wat ik daar mee kon, maar niet veel later zag ik een plaatje van een tekenfilmfiguurtje wat uit een frame leek te komen: aan de onderkant zat het achter het frame, maar het gezicht aan de bovenkant kwam er uit. Zou ik dat met CSS kunnen namaken? En als extra uitdaging, met maar één element?

Ik zat wat te pielen en al vrij snel had ik dit:

Een oranje vierkant dat door een blauwe rand steekt: aan de bovenkant zit het over de blauwe rand, maar aan de onderkant zit het achter de rand.

In dit plaatje zie je één div met een ::before en een ::after. De div zelf is transparant, de ::before heeft een border en de ::after is op de X-as geroteerd. Doordat de div perspective heeft wordt alles in 3d gepositioneerd, en daarmee ligt het ::after element aan de bovenkant voor de border, en aan de onderkant achter de border.

div {
transform: perspective(3000px);
transform-style: preserve-3d;
position: relative;
width: 200px;
height: 200px;
}

div::before {
content: " ";
width: 100%;
height: 100%;
border:10px solid darkblue;
}

div::after {
content: " ";
position: absolute;
background: orangered;
width: 80%;
height: 150%;
display: block;
left: 10%;
bottom: -25%;
transform: rotateX(-10deg);
}

Klik door naar de Codepen om het in actie te zien.

Tussendoor, perspective

Met perspective kan je aangeven hoever je als "kijker" af zit van z=0, wat je kan zien als "de horizon". Hoe hoger het perspectief, hoe minder sterk het 3D effect (en hoe kleiner, hoe sterker het effect). Voor de meeste effecten is een perspectief van tussen de 500 en 1000 pixels prima, en je kan met dit getal spelen om het effect te krijgen wat je wilt.

Je kan dit vergelijken met perspectieftekenen. Als je twee horizonspunten dicht bij elkaar tekent heb je een sterk perspectief, maar als je ze ver van elkaar af tekent lijkt alles veel platter.

Van rechthoeken naar cartoons

De rechthoekjes zijn leuk, maar wat ik eigelijk wilde maken was dit:

Een filmcel van Porky Pig die uit een cirkel lijkt te komen met de tekst "That's all folks".

Het klassieke einde van de looney tunes cartoons, waarbij porky pig uit een cirkel tevoorschijn komt en "that's all folks" stottert. Een mooi uitgeknipte versie van Porky Pig in dit plaatje kon ik niet vinden, maar de wikipedia pagina heeft een ander mooi uitgeknipt plaatje, dus die gebruiken we.

Om het effect te bouwen delen we het plaatje op in drie onderdelen:

  • De blauwe achtergrond, dat is de div zelf
  • De rode cirkels, dit wordt de ::before
  • Porky Pig, die gebruiken we als background voor de ::after.

We beginnen met de div, dit wordt de achtergrond en ook de basis voor de rest van de onderdelen. Op deze div zetten we het eerder genoemde perspective en transform-style, samen met de achtergrondkleur en wat afmetingen:

div {
transform: perspective(3000px);
transform-style:preserve-3d;
position: relative;
width: 200px;
height: 200px;
background: #4992AD;
}

Dan komen we bij het tweede onderdeel, de rode cirkels. Het "element" zelf moet hier transparant blijven, dat is het gat waar Porky zometeen doorheen komt. Hoe zorgen we dan voor de rode cirkels? We kunnen een border gebruiken zoals in het eerdere voorbeeld, maar daar hebben we er maar eentje van. In dit geval heb je een serie van cirkels, waar ook nog eens een kleurverloop in zit.

We kunnen dit namaken met box-shadows. Je kan meerdere box-shadows toevoegen en door een blur radius van 0 maar een grote spread radius te gebruiken kan je meerdere "borders" toevoegen.

box-shadow: <x-offset> <y-offset> <blur-radius> <spread-radius> <color>

We gebruiken een border-radius die even groot is als de div zelf, waardoor de ::before een cirkel wordt, en voegen daar de box-shadows aan toe. Wanneer je box-shadow gebruikt om een aantal rode cirkels te maken waar witte schaduwen met een blur radius op zitten, krijg je een effect wat erg dicht bij de afbeelding zit:

box-shadow: 0 0 20px   0px #fff, 0 0 0  30px #CF331F,
0 0 20px 30px #fff, 0 0 0 60px #CF331F,
0 0 20px 60px #fff, 0 0 0 90px #CF331F,
0 0 20px 90px #fff, 0 0 0 120px #CF331F,
0 0 20px 120px #fff, 0 0 0 150px #CF331F;

We maken hier vijf cirkels van steeds 30 pixels breed. Iedere cirkel heeft een vlakke rode achtergrond en daarboven maken we met behulp van een witte schaduw met een blur radius van 20 pixels het kleurverloop-effect.

De achtergrond en de cirkels in pure CSS, nog zonder Porky.

Als laatste hebben we Porky, laten we beginnen met die op de plek neer te zetten waar we hem uiteindelijk willen hebben, maar voor nu nog vóór de cirkels.

div::after {
position: absolute;
content: " ";
width:80%;
height:150%;
display: block;
left:10%;
bottom: -12%;
background:url("https://upload.wikimedia.org/wikipedia/en/thumb/8/88/Porky_Pig.svg/800px-Porky_Pig.svg.png") no-repeat center/contain;
}

Porky Pig is boven op de cirkels gepositioneerd.

De afmetingen en positionering hier is een beetje willekeurig, ik heb gekeken wat voor het plaatje mooi uitkwam.

De truuk

Dan nu de truuk: Porky's benen moeten achter de rode cirkels, maar zijn hoofd en petje er boven. Om dit voor elkaar te krijgen moeten we zowel de cirkels als Porky in 3d-ruimte verplaatsen.

Als we Porky willen roteren, dan moeten we op een aantal dingen letten:

  • Hij mag niet door de achtergrond clippen
  • We kunnen niet zo ver roteren dat de afbeelding vervormt
  • Hij moet met zijn benen achter en met zijn hoofd voor de cirkels zetten

Om er voor te zorgen dat Porky niet door de achtergrond clipt doen we een aantal dingen. Eerst verplaatsen we de cirkels in de Z-as zodat ze dichter naar ons toe komen. Omdat preserve-3d aan staat betekent dat ook dat ze inzoomen, maar als we ze slechts een klein beetje naar voren verschuiven hebben we genoeg ruimte tussen de achtergrond en de cirkels:

transform: translateZ(20px);

Vervolgens hebben we Porky zelf. Die gaan we op de x-as roteren, zo dat de bovenkant van de afbeelding dichterbij is dan de onderkant, bijvoorbeeld met

transform: rotateX(-10deg);

Maar als we dat doen, zien we dat het er niet helemaal goed uit ziet, Porky zit nu namelijk deels verstopt achter de blauwe achtergrond, en bij de cirkels gaat er ook iets mis.

Porky Pig wordt deels afgekapt door de achtergrond en door de cirkels.

We kunnen dit proberen op te lossen door Porky ook dichterbij te halen, met translateZ, maar wat we beter kunnen doen is de plek waar we roteren te veranderen. Nu doen we dat namelijk in het midden van de afbeelding, waardoor het onderste deel naar achter gaat.

Als we aan de onderkant van de afbeelding draaien, of zelfs ietsje daar onder, dan draait Porky als het ware in zijn geheel naar ons toe. En omdat we de cirkels al dichterbij hebben gehaald, komt alles mooi uit:

transform: rotateX(-10deg);
transform-origin:center 120%;

Porky Pig komt nu uit de cirkel: Zijn benen zitten achter de cirkels, maar zijn hoofd er boven.

Mocht je nou in 3d willen zien hoe alles zich verhoud, kijk dan op deze code pen en klik op "show debug".

Codepen voorbeeld

Animatie

Nu hebben we Porky, en hij piept mooi achter de cirkels vandaan, maar als het een statisch plaatje blijft hadden we niet al deze moeite hoeven nemen. Door een animatie toe te voegen kunnen we de gelaagdheid zichtbaar maken en het effect versterken.

De animatie die ik wil maken doet het volgende: Porky begint klein, en vergroot dan vanuit de blauwe achtergrond naar over de rode achtergrond. Daar blijft hij even staan, en zoomt daarna weer terug.

Voor de beste prestaties gebruiken we ook voor de animatie transform, en omdat we dat doen moeten we zorgen dat de rotateX behouden blijft.

@keyframes zoom {
0% {
transform: rotateX(-10deg) scale(0.66);
}
40% {
transform: rotateX(-10deg) scale(1);
}

60% {
transform: rotateX(-10deg) scale(1);
}

100% {
transform: rotateX(-10deg) scale(0.66);
}
}

Met scale() zoomen we in en uit, en omdat we al eerder de transform-origin hebben gezet, schaalt het vanuit midden onder, precies het effect wat we willen.

Door tussen 40% en 60% even te pauzeren hebben we dat moment rust als hij boven de rode cirkels zit.

Die animatie voegen we dan toe aan de ::after. Om de animatie wat natuurlijker te laten verlopen gebruiken we een timing functie, "ease-in-out", waardoor de animatie aan het begin en eind iets slomer gaat.

div::after {
animation-name: zoom;
animation-duration: 4s;
animation-iteration-count: infinite;
animation-fill-mode:forwards;
animation-timing-function: ease-in-out;
}

Klik door naar de codepen om te zien hoe het er nu uit ziet, maar we kunnen nog twee dingen aanpassen om het er nog nét iets leuker uit te laten zien:

  • We kunnen meer diepte creeëren door een schaduw achter Porky te laten groeien, zodat het lijkt alsof Porky dichterbij komt
  • We kunnen Porky een beetje draaien tijdens de animatie waardoor het effect verstrekt wordt

Die tweede kunnen we oplossen met rotateZ, maar voor die eerste moeten we nog een truukje uithalen. Omdat we een plaatje gebruiken kunnen we niet box-shadow gebruiken, die maakt namelijk een vierkant schaduwtje om het ::after element.

In plaats daarvan kunnen gebruik maken van een filter. Wanneer je die gebruikt wordt er gekeken naar de ondoorzichtige pixels van een element, en die krijgen een schaduwtje. Voeg je dat samen, dan krijg je dit:

@keyframes zoom {
0% {
transform: rotateX(-10deg) scale(0.66);
filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0));
}
40% {
transform: rotateZ(-10deg) rotateX(-10deg) scale(1);
filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5));
}

60% {
transform: rotateZ(-10deg) rotateX(-10deg) scale(1);
filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5));
}

100% {
transform: rotateX(-10deg) scale(0.66);
filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0));
}
}

En hier is de uiteindelijke Codepen:

Porky Pig "That's all Folks" in pure CSS

En zo hebben we een animatie gemaakt van Porky Pig die ons uitzwaait en rest mij enkel nog het volgende te zeggen: "That's all Folks!"

Basics van front-end testing

Als front-end developer zullen de begrippen unit test, integration test en end-to-end test je waarschijnlijk wel bekend voorkomen. Maar wat is het en wanneer gebruik je welke test? Dit artikel is een kleine intro tot de verschillende soorten tests en tools die je als front-end developer tot je beschikking hebt om je code te testen en uiteindelijk de kwaliteit te verbeteren.

Unit test

Het begint meestal met unit tests. Een unit test is een test die een op zichzelf staand onderdeel van je code controleert. Bij een unit test is de context van de functie in de applicatie niet van belang. Er wordt puur getest of een functie bij dezelfde input altijd dezelfde output teruggeeft. Oftewel, als je a er in stopt, moet je altijd b terug krijgen.

Unit tests maken het ook mogelijk om alle mogelijke uitkomsten van een functie snel te controleren. Je hoeft dan niet je applicatie op te starten en handmatig de mogelijkheden te testen; je kunt de unit tests starten. Daarmee kun je tijdens het ontwikkelen tijd besparen en vervelende verrassingen voorkomen bij een acceptance test.

Zo is het bijvoorbeeld bij een checkout van een webshop belangrijk dat een klant een geldige postcode invult. Ter validatie van je code kun je een eenvoudige unit test maken. Stel dat dit je validatie functie is:

function validatePostcode(postcode) {
const matches = postcode.match(/(?:<getal>\d\d\d\d) (?:<letters>[A-Z])/);
if (!matches) return false;
const number = parseInt(matches.groups.getal, 10);
const chars = matches.groups.letters;

// Check number range
if (number < 1000 || number > 9992) return false;

return true;
}

Dan kun je met deze unit test checken of je bij verschillende waardes de juiste boolean terug krijgt. Voor dit voorbeeld gebruik ik Jest, een JavaScript testing framework dat met vrijwel alle JavaScript frameworks werkt.

// Import jest

test('validatePostcode should return true', () => {
expect(validatePostcode('1000 AA')).toBe(true);
expect(validatePostcode('7777 AA')).toBe(true);
expect(validatePostcode('9992 AA')).toBe(true);
});

test('validatePostcode should return false', () => {
expect(validatePostcode('000 AB')).toBe(false);
expect(validatePostcode('1000 A')).toBe(false);
expect(validatePostcode('0000 AB')).toBe(false);
expect(validatePostcode('0001 AB')).toBe(false);
expect(validatePostcode('9993 AB')).toBe(false);
});

Je kunt je waarschijnlijk wel voorstellen dat dit een hoop tijd bespaart. Je hoeft niet de website te openen en de checkout meerdere malen te doorlopen om verschillende postcodes te testen.

Integration test

Een integration test is een test waarbij in tegenstelling tot een unit test de context er wel toe doet. Je test meerdere onderdelen van een keten, bijvoorbeeld om te checken of een functie correct is aangeroepen. Voor een integration test kun je ook Jest gebruiken. Soms kan het benodigd zijn om bij een integration test bepaalde onderdelen van de applicatie te mocken. Stel je voor dat je een submit van een formulier wilt testen, dan kan het zijn dat je de uitkomst van een api callback moet mocken omdat je bij het uitvoeren van de test geen toegang hebt tot de API.

End-to-end (e2e) test

Een end-to-end test is een test waarbij het gedrag van de gebruiker wordt gesimuleerd, die verschillende scenario's probeert uit te voeren met een bepaalde gewenste uitkomst. Zo kun je de belangrijkste happy- en non-happy flows testen. Een end-to-end test is een blackbox test. De test heeft geen kennis van de code, maar voert acties uit: aanklikken van een button, invullen van een formulier, submit, etc. Unit tests behoren tot een whitebox test, de test roept direct de code aan.

Wat je vaak ziet is dat e2e tests worden geschreven met behulp van cucumber in het format gherkin. In leesbare taal staat uitgeschreven welke stappen uitgevoerd worden en vervolgens kunnen die stappen bij scenarios worden hergebruikt.

Je kunt user stories als uitgangspunt voor je scenario's. Het begint met een feature: ‘Als klant wil ik betalen’, met scenario’s als: ‘Als klant wil ik betalen met iDeal’. De focus ligt altijd op de business value die gecreëerd wordt voor een gebruiker. Zo'n leesbare e2e test kan er zo uitzien:

Feature: Als klant wil ik betalen

Scenario: Als klant wil ik betalen met iDeal
Given Een correct ingevuld checkout formulier
When de klant probeert te betalen
Then de klant ziet de iDeal-pagina van de payment provider

Er zijn allerlei soorten software om een end-to-end tests uit te voeren. In het Angular ecosysteem is de combinatie Protractor met Selenium een bekende. Een andere interessante app is Cypress.io, waarbij je elke stap ook daadwerkelijk kunt zien gebeuren. Dit helpt bij het ontwikkelen van de tests. Als er iets onverwachts gebeurd, dan blijft het proces op de stap haken en kun je zien wat er aan de hand is.

Testen, testen, testen

Je weet nu waar te starten met front-end testing. Het is een goede basis, maar met unit tests, integration tests of end-to-end tests zijn we er helaas nog niet. Je hebt onder andere ook nog: acceptance tests door acceptanten, accessibility tests, cross browser compatibility tests, system integration testing (over meerdere delen van de keten testen) en natuurlijk visuele regressie testing. Front-end development is zo eenvoudig nog niet.

"Even snel" een project starten

Je kent het wel, je wil even snel iets bouwen of wat code uitproberen. Je maakt een map aan, gooit daarin een index.html, een CSS bestand en een JavaScript bestandje, om vervolgens met wat code te rommelen. In dit voorbeeld heb je gewoon statische bestanden die je in een browser (naar keuze) kunt laden om te checken of het klopt wat je aan het maken bent.

Je kent het wel, je wil even snel iets bouwen of wat code uitproberen. Je maakt een map aan, gooit daarin een index.html, een CSS bestand en een JavaScript bestandje, om vervolgens met wat code te rommelen. In dit voorbeeld heb je gewoon statische bestanden die je in een browser (naar keuze) kunt laden om te checken of het klopt wat je aan het maken bent.

Als ik met een onderdeel bezig ben voor een project en het wil maar niet lukken, doe ik dit soms. Dan vind ik het handig als ik dat ene onderdeeltje even uit het grotere geheel kan halen om er los van het project mee aan de gang te gaan. Ik vind het zelf dan wel heel handig als de browser de pagina herlaad als ik iets aangepast heb. Maar dan komen er nog meer handelingen aan te pas voordat ik kan beginnen. Dat is allemaal best te doen, maar wat zou het toch handig zijn als dat met één commando veel sneller kan?

Natuurlijk kan dat! Ik heb in mijn command line een functie gemaakt die in een mum van tijd een klein projectje voor me aanmaakt, zodat ik snel iets uit kan proberen.

In het config bestand van de command line kun je namelijk zelf functies maken. De functie die ik gebruik ziet er als volgt uit:

function basicsetup() {

mkdir /home/reneedekruijf/Bureaublad/"$1"

cd /home/reneedekruijf/Bureaublad/"$1"

mkdir /home/reneedekruijf/Bureaublad/"$1"/images

mkdir /home/reneedekruijf/Bureaublad/"$1"/js

mkdir /home/reneedekruijf/Bureaublad/"$1"/css

touch index.pug js/script.js css/style.css

code .

npm init -y

parcel serve index.pug --open

}

Als ik nu iets wil uitproberen, type ik in de terminal: basicsetup "testproject". Er wordt nu een folder op mijn bureaublad aangemaakt met de naam testproject.

Deze functie basicsetup roep je aan door de naam in je CLI te typen, de functie wordt dan uitgevoerd. Ik heb op een paar plekken $1 gebruikt. Dit is een variabele die je een willekeurige waarde mee kunt geven. Dit doe je door achter de functienaam de naam van je project in te voeren tussen aanhalingstekens. In dit geval wordt $1 dus testproject. Die kun je vervolgens in de functie zo vaak als je wil terug laten komen.

Wat doet deze code?

Op de eerste regel maken we de folder aan door het commando mkdir te gebruiken, dit staat voor make directory.

Op de volgende regel gebruiken we cd (change directory) om in de folder testproject te komen, die we net hebben gemaakt. In de drie volgende regels gebruiken we weer mkdir om in de nieuw aangemaakte folder achtereenvolgens drie nieuwe folders te maken, een images-, een js- en css-folder.

Dan maken we met touch (hiermee kun je bestanden aanmaken) drie bestanden aan. Een CSS file en een JavaScript file en beide al meteen in de juiste folder. Als laatste van deze regels maken we een index.pug bestand in de root folder.

Deze index heeft niet de extensie .html maar .pug. PUG is een templating taal voor HTML. Met PUG kun je met een aangepaste notatie HTML genereren. Ook kun je templates maken en je kunt variabelen gebruiken. De header of de footer zet je 1x op en die kun je steeds blijven gebruiken in al je bestanden, als je nu iets verandert in de header of footer dan hoef je dat maar 1x te doen. PUG-code moet alleen wel omgezet worden naar HTML (net zoals SASS ook omgezet wordt in CSS) en daar heb je tooling voor nodig, dat wordt in de laatste 2 regels van de basicsetup opgezet.

Met code . start ik Visual Studio Code op, dit is de code editor die ik graag gebruik. Als je een andere editor gebruikt dan komt hier het commando voor die editor te staan.

De laatste twee regels dienen meerdere doelen. Als je dit wilt laten werken op je eigen computer heb je Nodejs nodig. Dit kun je gewoon installeren en is voor elk platform te downloaden. Zonder Nodejs kun je PUG niet gebruiken en werken de laatste twee regels dus ook niet.

De regel npm init -y zorgt ervoor dat je folder klaar wordt gemaakt voor het installeren van NPM packages, dit zijn kleine programmaatjes die je kunt installeren en die het leven van een developer makkelijker (kunnen) maken. Op https://www.npmjs.com/ kun je zoeken naar deze packages.

Parcel is zo'n NPM package en deze moet je van tevoren installeren. Op de site van Parcel kun je zien hoe dat moet. Parcel is een zogenaamde bundler en kan meerdere handige dingen voor je doen. Ik gebruik het hier vooral om mijn PUG code om te zetten naar HTML en ik gebruik het voor hot reloading. Dit laatste zorgt ervoor dat mijn browser automatisch wordt ververst als ik iets in de HTML, CSS of JavaScript aanpas. Je ziet dus meteen wat je aanpast in je code. Echt heel handig. Parcel werkt out-of-the-box. Je hebt daarvoor wel de laatste regel van de basicsetup functie nodig, dan wordt automatisch PUG omgezet naar HTML, en je krijgt hot reloading. Eigenlijk zorgt Parcel ervoor dat je een pagina in HTML krijgt met CSS en Javascript.

Dus wil je snel even wat dingetjes checken of uitproberen, dan kan deze functie je helpen. Door een kort commando in de command line heb je snel een basis setup waar je mee aan de slag kunt.