Føx er webutviklingsgrenen til Kvåle Solutions. Føx spesialiserer seg på å lage lette, effektive og semantiske nettsider. En Føx nettside er som en rev: lett, kjapp og lur.
Å lage et nettsted innebærer mye mer enn bare programmering. Hvis du bruker Føx, trenger du ikke reise langt for å finne de andre tjenestene du behøver. Kvåle Solutions har flere andre avdelinger og tilbyr grafisk design, typesetting og språktjenester. Ditt prosjekt kan bli helt fullført av Kvåle Solutions med bare ett fast kontaktpunkt (SPOC).
Føx lagde nettstedet du er på akkurat nå. Utforsk den gjerne, og hvis du er interessert i prosessen bak skapingen, kan du scrolle ned for å lese utviklerens oppsummering.
Byggingen av Kvåle Solutions-nettstedet
Nettstedet til Kvåle Solutions er en komplett graf; hver side kan nås fra hvilken som helst annen side. Dette designet reflekterer sammenkoblingen av de fire hovedtjenestene, og gjør det enkelt for kunder å navigere nettstedet.
Siden er tilgjengelig på engelsk, bokmål og nynorsk, som er håndtert
ved bruk av /nb/ og /nn/ undermapper.
Vi tok en litt ukonvensjonell fremgang med PC-versjonen til fremsiden. Vi ønsket at hvert kort skulle være stort og synlig, og vi ønsket siden skulle være ikke-scrollbar. Det var umulig å møte begge disse kravene dersom vi plasserte logoen som overskrift. Så... vi satt logoen i senteret i stedet. En slik ukonvensjonell løsning kan forfjamse noen kunder, men det er en risiko verdt å ta. Vi lever i en oppmerksomhetsøkonomi, og hvis du kombinerer ukonvensjonalitet med høy kvalitet, så vinner du oppmerksomhetskonkurransen.
Mobilversjonen til frontsiden er derimot en scrollbar side med et normalt oppsett, da størrelsesbegrensingene gjorde den andre løsningen suboptimal. Vi tar alltid hensyn til forskjellige medier og designer sidene våre deretter, og vi holder en konsekvent stil på tvers av de mediene, men vi lar aldri begrensingene av ett medium holde oss tilbake med et annet.
Hvert eneste bilde på tvers av nettstedet er en .svg, takket være Æxis og TeXtract som har supplert nettstedet med deres vektor-baserte grafikk.
Den mest teknisk avanserte siden er den som tilhører Aexis, som viser fram en Bézier-kurve som bøyer seg som en funksjon av den besøkendes scrolling ned siden.
const track = document.querySelector('.animation-track');
if (track) {
const mainCurve = document.getElementById('mainCurve');
const dash1 = document.getElementById('dash1');
const dash2 = document.getElementById('dash2');
const cp1 = document.getElementById('cp1');
const cp2 = document.getElementById('cp2');
let ticking = false;
const updateCurve = () => {
const rect = track.getBoundingClientRect();
// Jeg fant disse to gjennom finjustering, jeg syns det gir en fin balanse mellom å la kurven
// bøye seg, men ikke bruke for mye tid
const progress = Math.max(0, (window.innerHeight / 2) - rect.top);
const baseMove = Math.min(400, progress * 0.8);
// Siden jeg ville at hele kurven og styringspunktene skulle være synlige på slutten,
// så var jeg mer begrenset vertikalt enn horisontalt,
// så jeg gjorde slik at styringspunktene bevegde seg mer horisontalt enn vertikalt
const moveY = baseMove * 0.55;
const moveX = baseMove * 2;
// Jeg fikk dem til å bevege seg i motsatte retninger for den mest dramatiske effekten
const newCy1 = 250 - moveY;
const newCy2 = 250 + moveY;
const newCx1 = 400 + moveX;
const newCx2 = 600 - moveX;
cp1.setAttribute('cx', newCx1);
cp1.setAttribute('cy', newCy1);
cp2.setAttribute('cx', newCx2);
cp2.setAttribute('cy', newCy2);
dash1.setAttribute('x2', newCx1);
dash1.setAttribute('y2', newCy1);
dash2.setAttribute('x2', newCx2);
dash2.setAttribute('y2', newCy2);
mainCurve.setAttribute('d', `M 100 250 C ${newCx1} ${newCy1}, ${newCx2} ${newCy2}, 900 250`);
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateCurve);
ticking = true;
}
};
const observerOptions = {
root: null,
rootMargin: '100px',
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
window.addEventListener('scroll', onScroll, { passive: true });
} else {
window.removeEventListener('scroll', onScroll);
}
});
}, observerOptions);
observer.observe(track);
}
Aexis har også en 2 699x zoom inn i et LaTeX-generert .svg bilde av en Fibonacci-mosaikk, helt kodet i vanilla JS.
const zoomBtn = document.getElementById('zoom-trigger-btn');
if (zoomBtn) {
const fibSvg = document.getElementById('fibonacci-svg');
// Koordinatene av det innerste kvadratet ble funnet ved bruk av DevTools.
const fStart = { x: 12.79, y: 12.04, w: 539.86, h: 321.62 };
const fTarget = { x: 387.571, y: 244.817 };
const startCenter = { x: fStart.x + (fStart.w / 2), y: fStart.y + (fStart.h / 2) };
const fEndWidth = 0.2;
const fDuration = 17000;
const fPauseTime = 2500;
const fEndHeight = (fEndWidth * fStart.h) / fStart.w;
let fStartTime = null;
let isZooming = false;
const formatX = (num) => Math.round(num).toLocaleString() + "x";
function runFibonacciZoom(timestamp) {
if (!fStartTime) fStartTime = timestamp;
const elapsed = timestamp - fStartTime;
let directionProgress;
let isPaused = false;
if (elapsed < fDuration) {
directionProgress = elapsed / fDuration;
} else if (elapsed < fDuration + fPauseTime) {
directionProgress = 1;
isPaused = true;
} else if (elapsed < (fDuration * 2) + fPauseTime) {
const timeIntoOutro = elapsed - fDuration - fPauseTime;
directionProgress = 1 - (timeIntoOutro / fDuration);
} else {
stopZoom();
return;
}
// Jeg måtte legge inn en glidende overgang til målkoordinatene for å unngå
// et stygt hopp inn i posisjon
const glideEase = 1 - Math.pow(1 - directionProgress, 10);
const zoomEase = Math.pow(directionProgress, 2);
const currentW = fStart.w * Math.pow((fEndWidth / fStart.w), zoomEase);
const currentH = fStart.h * Math.pow((fEndHeight / fStart.h), zoomEase);
const multiplier = fStart.w / currentW;
if (isPaused) {
zoomBtn.textContent = `${pageText.maxZoom}${formatX(multiplier)}`;
} else {
zoomBtn.textContent = `${pageText.zooming}${formatX(multiplier)}`;
}
const currentCenterX = startCenter.x + (fTarget.x - startCenter.x) * glideEase;
const currentCenterY = startCenter.y + (fTarget.y - startCenter.y) * glideEase;
const currentX = currentCenterX - (currentW / 2);
const currentY = currentCenterY - (currentH / 2);
fibSvg.setAttribute('viewBox', `${currentX} ${currentY} ${currentW} ${currentH}`);
if (isZooming) {
requestAnimationFrame(runFibonacciZoom);
}
}
function stopZoom() {
isZooming = false;
fStartTime = null;
zoomBtn.textContent = pageText.diveBackIn;
zoomBtn.style.opacity = "1";
zoomBtn.style.pointerEvents = "auto";
fibSvg.setAttribute('viewBox', `${fStart.x} ${fStart.y} ${fStart.w} ${fStart.h}`);
}
zoomBtn.addEventListener('click', () => {
if (isZooming) return;
isZooming = true;
fStartTime = null;
zoomBtn.style.opacity = "0.9";
zoomBtn.style.pointerEvents = "none";
requestAnimationFrame(runFibonacciZoom);
});
}