Føx er webutviklingsgreina til Kvåle Solutions. Føx spesialiserer seg på å lage lette, effektive og semantiske nettsider. Ei Føx-nettside er som ein rev: lett, kjapp og lur.
Å lage nettstad involverer mykje meir enn berre programmering. Viss du bruker Føx, treng du ikkje reise langt for å finne dei andre tenestene du behøver. Kvåle Solutions har fleire andre avdelingar og tilbyr grafisk design, typesetting og språktenester. Ditt prosjekt kan verte heilt fullført av Kvåle Solutions med berre eitt fast kontaktpunkt (SPOC).
Føx laga nettstaden du er på akkurat no. Du kan gjerne utforske han, og om du er interessert i prosessen bak skapinga, kan du rulle ned for å lese utviklaren si oppsummering.
Bygginga av Kvåle Solutions-nettstaden
Nettstaden til Kvåle Solutions er ein komplett graf; kvar side kan nåast frå kvar som helst anna side. Dette designet reflekterer samankoplinga av dei fire hovudtenestene, og gjer det enkelt for kundar å navigere på nettstaden.
Sida er tilgjengeleg på engelsk, bokmål og nynorsk, noko som blir
handtert ved bruk av underkatalogane /nb/ og
/nn/.
Me valde ein litt ukonvensjonell framgangsmåte med PC-versjonen til framsida. Me ønskte at kvart kort skulle vere stort og synleg, og me ønskte at sida skulle vere ikkje-rullbar. Det var umogleg å møte begge desse kriteria om me samstundes skulle ha hovudlogoen som overskrift. Så... me sette logoen i midten i staden. Ei slik ukonvensjonell løysing kan kanskje forfjamsa nokre kundar, men det er ein risiko verdt å ta. Me lever i ein merksemdsøkonomi, og om du kombinerer ukonvensjonalitet med høg kvalitet, så vinn du merksemdskonkurransen.
Mobilversjonen til framsida er derimot ei rullbar side med eit normalt oppsett, då storleiken gjorde den andre løysinga suboptimal. Me tek alltid omsyn til ulike medium og utformar sidene våre deretter. Me held ein konsekvent stil på tvers av mediuma, men me lèt aldri avgrensingane ved eitt medium halde oss tilbake i eit anna.
Kvart einaste bilete på tvers av nettstaden er ein .svg, takka vere Æxis og TeXtract som har forsynt nettstaden med sin vektorbaserte grafikk.
Den mest teknisk avanserte sida er den som tilhøyrer Aexis, som viser fram ei Bézier-kurve som bøyer seg som ein funksjon av den besøkande si rulling ned sida.
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();
// Eg fann desse to gjennom finjustering. Eg syns dei gjev ein god balanse mellom å la kurva bøye seg,
// og å ikkje ta for mykje tid.
const progress = Math.max(0, (window.innerHeight / 2) - rect.top);
const baseMove = Math.min(400, progress * 0.8);
// Sidan eg ville at heile kurva og styringspunkta skulle vere synlege på slutten,
// så var eg meir avgrensa vertikalt enn horisontalt,
// så eg gjorde slik at styringspunkta bevegde seg meir horisontalt enn vertikalt
const moveY = baseMove * 0.55;
const moveX = baseMove * 2;
// Eg fekk dei til å bevege seg i motsatte retningar 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 òg ein 2 699x zoom inn i eit LaTeX -generert .svg-bilete av ein Fibonacci-mosaikk, heilt koda i vanilla JS.
const zoomBtn = document.getElementById('zoom-trigger-btn');
if (zoomBtn) {
const fibSvg = document.getElementById('fibonacci-svg');
// Koordinatane av det inste kvadratet vart funne 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;
}
// Eg måtte leggje inn ein glidande overgang til målkoordinatane for å unngå
// eit 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);
});
}