From 2f3511dfb442aca0529a065f42d9854a81a09475 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Wed, 21 Oct 2026 14:52:54 +0200 Subject: [PATCH] Hero: navnet delt av scrollen over en lynlinje; bildeserier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hero.js er forsidens hero (site.yaml hero.kind: module): Westra og Engineering satt som i logoen, serif i kapiteler, og en lynlinje (gpupaper's light_line.frag, uendret, i ordenes blå) tegnet over hele den klebrige headeren på en WebGL-canvas, siktet inn under ordene der logoens bånd går. Scrollen er tidslinjen: Westra glir ut til venstre og Engineering til høyre, med litt etterslep. Logo-bildet skjules på forsiden; hvem som er inne står øverst til høyre. Åpningsspørsmålet sier "vi" i stedet for å gjenta navnet under det. Bildeserier (slideshow) på forsiden, i headeren under navnet, og på /prosjekter med alle prosjektbildene; bildene som lå inne i kortene på /prosjekter er tatt ut, serien er galleriet. Co-Authored-By: Claude Fable 5.1 --- hero.js | 605 ++++++++++++++++++++++++++++++++++++++ questions/index.yaml | 34 ++- questions/prosjekter.yaml | 78 +++-- site.yaml | 3 + 4 files changed, 693 insertions(+), 27 deletions(-) create mode 100644 hero.js diff --git a/hero.js b/hero.js new file mode 100644 index 0000000..fc55dbc --- /dev/null +++ b/hero.js @@ -0,0 +1,605 @@ +// Westra Engineering's hero: the name, parted by the scroll, over a +// lightning line drawn full-screen on a sticky canvas. +// +// Content-owned: portal knows only site.yaml's `hero: {kind: module, +// module: hero.js}` and the contract - `mount(container) -> handle` +// with `stop()` - and serves this file same-origin at /site/hero.js. +// The module owns its look. The stylesheet it injects below restyles +// portal's .hero-module header the way uhhm's hero does: sticky at +// the viewport top for the whole page, so the line keeps running +// behind the translucent cards that scroll over it. On the landing +// page the name drawn here is the wordmark, so the image one is +// hidden there. +// +// Scroll is the timeline. Over the first SPLIT_RANGE of a viewport +// "Westra" slides left and "Engineering" right, each until it has +// left the screen, the way the logo already sets them: Westra to the +// left, Engineering to the right. The logo's ribbon and tagline are +// not drawn: the line takes the ribbon's place under the two words. +// The transform follows the scroll +// position through a short exponential lag (TAU_MS), so a wheel notch +// reads as a movement, not a jump; with reduced motion asked for it +// follows exactly. The copy under the name fades over the same range +// so it does not ghost through the cards. +// +// The line is /usr/share/gpupaper/shaders/light_line.frag, Brandon +// Fogerty's "Lightning" in glslsandbox conventions (time, mouse, +// resolution), the one gpupaper draws on the desktop. It is embedded +// verbatim; the two edits made to it at compile time are listed and +// explained at FRAG_EDITS. WebGL 1 is enough for it and is everywhere +// (a CSS paint worklet is not: no shaders, no per-pixel access, and +// Chromium only). Without WebGL the name and the scroll still work +// over plain paper. + +const STYLE_ID = 'westra-hero-style'; +// Viewport heights of scroll over which the name parts completely. +const SPLIT_RANGE = 0.6; +// How far the transform lags the scroll, as the time constant of an +// exponential approach: after one TAU it has closed 63% of the gap. +const TAU_MS = 140; +// The shader is cheap per pixel but runs for every pixel, three times +// over (its loop), and a 4K screen at a device pixel ratio of 2 is 33 +// million pixels a frame: choppy on an integrated GPU. The line is a +// glow, so it is drawn at no more than this many pixels and the +// browser scales the canvas up to the screen; at 1080p that is about +// 0.7 of the screen's pixels, at 4K about a third. +const MAX_PIXELS = 1.2e6; + +// /usr/share/gpupaper/shaders/light_line.frag, as is. +const LIGHT_LINE_FRAG = `// Lightning +// By: Brandon Fogerty +// bfogerty at gmail dot com +// xdpixel.com + + +#ifdef GL_ES +precision mediump float; +#endif + +uniform float time; +uniform vec2 mouse; +uniform vec2 resolution; + + +float Hash( vec2 p) +{ + vec3 p2 = vec3(p.xy,2.0); + return fract(sin(dot(p2,vec3(27.1,20.7, 2.4)))*0.5453123); +} + +float noise(in vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + f *= f * (3.0-2.0*f); + return mix(mix(Hash(i + vec2(0.,0.)), Hash(i + vec2(1.,0.)),f.x), + mix(Hash(i + vec2(0.,1.)), Hash(i + vec2(1.,1.)),f.x), + f.y); +} + +float fbm(vec2 p) +{ + float v = 0.0; + v += noise(p*1.0) * .5; + v += noise(p*2.) * .25; + v += noise(p*4.) * .15; + return v; +} + +void main( void ) +{ + + vec2 uv = ( gl_FragCoord.xy / resolution.xy ) * 2.0 - 1.0; + + vec2 center = vec2(0.0, 0.0); + float angle = time * 0.4; + + //uv.x = cos(angle) * (uv.x - center.x) - sin(angle) * (uv.y - center.y) + center.x; + //uv.y = sin(angle) * (uv.x - center.y) + cos(angle) * (uv.y - center.y) + center.y; + + vec3 finalColor = vec3( 0.0 ); + for( int i=0; i < 3; ++i ) + { + float t = abs( 1.4 / (sin(uv.y + fbm( uv + time * 0.04 )) * 100.0) ); + + finalColor += t * vec3( 2.9, 1.5, .25 ); + finalColor = vec3(t); + } + + gl_FragColor = vec4( finalColor, 1.0 ); + +} +`; + +// The edits, as exact replacements, so the source above stays a copy +// of the file. Each must match or the line is not drawn at all. +// +// 1. mediump → highp where the GPU has it: the hash takes sin() of a +// large dot product, which at 16-bit precision bands into stripes +// on phones. The two uniforms the second edit uses go in here too, +// after the precision statement, where GLSL ES wants them. +// 2. One line after uv is set: uv.y is shifted by `shift`, so the line +// (which the shader draws where uv.y + fbm crosses zero, about 45% +// below the middle of its own frame) lies under the two words, at +// the height of the logo's ribbon, wherever the layout has put +// them (see size()). The shader's own wander stays. +// 3. The last line. The file paints the line white on black, hot in +// the middle. Here it is one blue, the words' own (--accent), with +// the line's brightness as alpha, so the paper shows through +// everywhere else and the middle is that same blue, fully opaque. +const FRAG_EDITS = [ + [ + '#ifdef GL_ES\nprecision mediump float;\n#endif\n', + '#ifdef GL_ES\nprecision PRECISION float;\n#endif\nuniform vec3 tint;\nuniform float shift;\n', + ], + [ + '\tvec2 uv = ( gl_FragCoord.xy / resolution.xy ) * 2.0 - 1.0;\n', + '\tvec2 uv = ( gl_FragCoord.xy / resolution.xy ) * 2.0 - 1.0;\n\tuv.y -= shift;\n', + ], + [ + '\tgl_FragColor = vec4( finalColor, 1.0 );\n', + '\tfloat a = clamp( finalColor.r, 0.0, 1.0 );\n' + '\tgl_FragColor = vec4( tint * a, a );\n', + ], +]; + +const VERT = 'attribute vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }'; + +const CSS = ` +.hero-module { + /* Sticky at the viewport top for the whole scroll (its containing + block is the page itself), z-index 0 so the cards (z-index 1) + stack over it. The height is a floor, frozen to a measured pixel + value by the module once it runs (see size()); the svh ladder is + the first paint and the no-JS fallback. A floor and not a height, + so a page whose strip and copy need more than a screen (a phone) + grows rather than clips, and the cards start under it. */ + position: sticky; + top: 0; + z-index: 0; + max-width: none; + width: 100%; + min-height: 100vh; + min-height: 100svh; + padding: 0; + justify-content: flex-end; + overflow: hidden; +} + +/* The piece is whatever the header has left above the copy, not + portal's 55svh reservation; the name centres in it. It keeps a + quarter of the screen at least, so a strip and a long copy cannot + squeeze the name out. */ +.hero-module .hero-piece { + position: relative; + flex: 1 1 auto; + width: 100%; + min-height: 26svh; +} + +/* With a picture strip (slideshow): the name and the line take the + top, the strip runs the width under them, the question under that, + all in the one sticky header. The wordmark's bar is empty here (who + is here went to the corner), so it takes no room; the strip keeps + to its own height but never more than two fifths of the screen. */ +.hero-module .hero-bar { + padding: 0; +} + +.hero-module .slideshow { + flex: none; + margin: 0 0 1.25rem; + max-height: 40svh; +} + +.hero-module.hero-slides .hero-copy { + padding-bottom: 2.5rem; +} + +/* The name drawn below is the wordmark here. Who is here (portal + 0.6: on the wordmark's own line, .hero-top, which would sit level + with the mark) goes to the top right corner of the header instead, + the sticky header being its containing block now that the copy is + not positioned. */ +.hero-module .wordmark { + display: none; +} + +.hero-module .hero-top { + position: absolute; + top: 0.9rem; + right: 1.5rem; + width: auto; + justify-content: flex-end; +} + +.hero-module .hero-top .session { + position: static; + transform: none; +} + +.hero-module .hero-copy { + position: static; + padding: 0 1.5rem 3.5rem; + gap: 0.6rem; + text-shadow: 0 0.12em 1.4em var(--hero-glow); +} + +/* Behind everything in the header (the header is a stacking context + of its own, so -1 is still above the page). */ +.westra-line { + position: absolute; + inset: 0; + z-index: -1; + width: 100%; + height: 100%; + display: block; + pointer-events: none; +} + +/* The name, laid out as the logo lays it out: a box with the logo's + proportions (its 430x128 viewBox), Westra large at the top left, + Engineering under it and to the right. Positions and sizes are the + logo's own numbers as hundredths of the box width (cqw), so the + layout is the logo's at every size; the ribbon's place under both + words is where the line is aimed (see aim()). --p is the parting: + Westra leaves by half the viewport plus its own width, Engineering + the other way. */ +.westra-name { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 1.5rem; + box-sizing: border-box; + container-type: size; + pointer-events: none; + --p: 0; +} + +/* As wide as the piece allows, and no taller than most of it. */ +.westra-mark { + position: relative; + width: min(100%, 40rem, 260cqh); + aspect-ratio: 430 / 128; + container-type: inline-size; + font-family: Georgia, 'Times New Roman', 'Liberation Serif', serif; + font-variant: small-caps; + font-feature-settings: 'smcp' 1; + font-weight: 600; + letter-spacing: 0.028em; + line-height: 1; + color: var(--accent); +} + +.westra-mark > * { + position: absolute; + white-space: nowrap; +} + +/* A halo of the paper's own colour around the glyphs, dense close in + and fading out, so the line's glow does not blur the words where it + runs under or through them. */ +.westra-mark .westra-a, +.westra-mark .westra-b { + will-change: transform; + text-shadow: + 0 0 0.04em var(--paper), + 0 0 0.1em var(--paper), + 0 0 0.2em var(--paper), + 0 0 0.4em var(--paper), + 0 0.06em 0.9em var(--hero-glow); +} + +/* x 6, baseline 54, size 54 */ +.westra-mark .westra-a { + left: 1.4cqw; + top: 2.8cqw; + font-size: 12.56cqw; + transform: translateX(calc(var(--p) * (-50vw - 100%))); +} + +/* x 150, baseline 96, size 41 */ +.westra-mark .westra-b { + left: 34.9cqw; + top: 14.8cqw; + font-size: 9.53cqw; + transform: translateX(calc(var(--p) * (50vw + 100%))); +} + +`; + +function ensureStyle() { + if (document.getElementById(STYLE_ID)) return; + const style = document.createElement('style'); + style.id = STYLE_ID; + style.textContent = CSS; + document.head.appendChild(style); +} + +const clamp01 = (x) => Math.min(1, Math.max(0, x)); +// Gentle in, gentle out: the halves start moving as the scroll does, +// without a kick. +const ease = (p) => (p < 0.5 ? 2 * p * p : 1 - 2 * (1 - p) * (1 - p)); + +// "#rgb" / "#rrggbb" from a custom property, as a 0..1 triple. +function rgb(value, fallback) { + const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec((value || '').trim()); + if (!m) return fallback; + let h = m[1]; + if (h.length === 3) h = h.replace(/./g, (c) => c + c); + return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255); +} + +class WestraHero { + constructor(container) { + ensureStyle(); + this.stopped = false; + this.hero = container.closest('.hero') || container.parentElement; + this.copy = this.hero ? this.hero.querySelector('.hero-copy') : null; + this.reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); + this.dark = window.matchMedia('(prefers-color-scheme: dark)'); + + this.canvas = document.createElement('canvas'); + this.canvas.className = 'westra-line'; + this.name = document.createElement('div'); + this.name.className = 'westra-name'; + const mark = document.createElement('div'); + mark.className = 'westra-mark'; + const a = document.createElement('span'); + a.className = 'westra-a'; + a.textContent = 'Westra'; + const b = document.createElement('span'); + b.className = 'westra-b'; + b.textContent = 'Engineering'; + mark.append(a, b); + this.name.append(mark); + this.mark = mark; + // The canvas covers the whole sticky header, under everything + // in it; the name sits in the piece, the space above the copy. + (this.hero || container).prepend(this.canvas); + container.append(this.name); + + this.width = 0; + this.height = 1; + this.boxHeight = 1; + this.progress = 0; + this.t0 = performance.now(); + this.last = this.t0; + this.raf = 0; + + this.gl = this.initGl(); + this.size(); + + this._onResize = () => this.onResize(); + this._onScroll = () => this.wake(); + this._onTheme = () => { + this.recolor(); + this.wake(); + }; + this._onLost = (e) => { + e.preventDefault(); + this.gl = null; + }; + this._onRestored = () => { + this.gl = this.initGl(); + this.size(); + this.wake(); + }; + window.addEventListener('resize', this._onResize); + window.addEventListener('scroll', this._onScroll, { passive: true }); + if (this.hero && typeof ResizeObserver === 'function') { + this.observer = new ResizeObserver(() => { + if (this.stopped) return; + if (this.hero.offsetHeight !== this.boxHeight) { + this.fit(); + this.wake(); + } + }); + this.observer.observe(this.hero); + } + this.dark.addEventListener('change', this._onTheme); + this.reduced.addEventListener('change', this._onTheme); + this.canvas.addEventListener('webglcontextlost', this._onLost); + this.canvas.addEventListener('webglcontextrestored', this._onRestored); + + this.wake(); + } + + // The frame loop runs while the line animates; with reduced motion + // the line stands still and frames are drawn only when the scroll, + // size or theme changes, until the transform has settled. + wake() { + if (this.stopped || this.raf) return; + this.raf = requestAnimationFrame((now) => this.frame(now)); + } + + frame(now) { + this.raf = 0; + if (this.stopped) return; + const dt = Math.min(100, now - this.last); + this.last = now; + + const target = clamp01(window.scrollY / (this.height * SPLIT_RANGE)); + if (this.reduced.matches) { + this.progress = target; + } else { + this.progress += (target - this.progress) * (1 - Math.exp(-dt / TAU_MS)); + if (Math.abs(target - this.progress) < 0.0005) this.progress = target; + } + const p = ease(this.progress); + this.name.style.setProperty('--p', p.toFixed(4)); + // The copy goes sooner than the name: it has to be gone before + // the first card reaches it, on a short screen too. + if (this.copy) { + const visible = 1 - clamp01(this.progress * 2.5); + this.copy.style.opacity = visible.toFixed(3); + this.copy.style.visibility = visible <= 0.01 ? 'hidden' : ''; + } + + this.draw(this.reduced.matches ? 0 : (now - this.t0) / 1000); + + const settled = this.progress === target; + if (!this.reduced.matches || !settled) this.wake(); + } + + // The header's height is frozen from one measurement of the + // viewport rather than left to svh: on phones the box would + // otherwise follow the address bar in and out and the name with + // it. Re-measured when the width changes (a rotation, a window + // resize), not when only the height moves a little (that bar). + onResize() { + const dw = window.innerWidth !== this.width; + const dh = Math.abs(window.innerHeight - this.height) > this.height * 0.25; + if (dw || dh) this.size(); + this.wake(); + } + + size() { + this.width = window.innerWidth; + this.height = Math.max(1, window.innerHeight); + if (this.hero) this.hero.style.minHeight = this.height + 'px'; + this.fit(); + } + + // The canvas covers the header's real box, which is the screen + // unless the strip and the copy need more; measured, since the + // copy's height is the fonts' and the strip's is its own. + fit() { + this.boxHeight = Math.max(1, this.hero ? this.hero.offsetHeight : this.height); + const dpr = window.devicePixelRatio || 1; + const scale = Math.min(dpr, Math.sqrt(MAX_PIXELS / (this.width * this.boxHeight))); + this.canvas.width = Math.max(1, Math.round(this.width * scale)); + this.canvas.height = Math.max(1, Math.round(this.boxHeight * scale)); + if (this.gl) { + this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); + this.aim(); + } + } + + // Where the logo's ribbon would be on screen (its lower edge, y + // 116 of the box's 128), as the shader's uv.y (+1 at the top, -1 + // at the foot): the line's own zero crossing sits about 0.45 + // below its frame's middle, so the frame is shifted up by that + // much less. + aim() { + const hero = this.hero ? this.hero.getBoundingClientRect() : { top: 0 }; + const mark = this.mark.getBoundingClientRect(); + const ribbonY = mark.top - hero.top + mark.height * (116 / 128); + const uvY = 1 - 2 * (ribbonY / this.boxHeight); + this.gl.uniform1f(this.u.shift, uvY + 0.45); + } + + initGl() { + const gl = this.canvas.getContext('webgl', { + alpha: true, + antialias: false, + depth: false, + stencil: false, + premultipliedAlpha: true, + powerPreference: 'low-power', + }); + if (!gl) return null; + + const highp = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); + const precision = highp && highp.precision > 0 ? 'highp' : 'mediump'; + let frag = LIGHT_LINE_FRAG; + for (const [from, to] of FRAG_EDITS) { + if (!frag.includes(from)) { + console.warn('westra hero: light_line.frag no longer matches its edits; line not drawn'); + return null; + } + frag = frag.replace(from, to.replace('PRECISION', precision)); + } + + const compile = (type, src) => { + const s = gl.createShader(type); + gl.shaderSource(s, src); + gl.compileShader(s); + if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { + console.warn('westra hero: shader', gl.getShaderInfoLog(s)); + return null; + } + return s; + }; + const vs = compile(gl.VERTEX_SHADER, VERT); + const fs = compile(gl.FRAGMENT_SHADER, frag); + if (!vs || !fs) return null; + const program = gl.createProgram(); + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.warn('westra hero: link', gl.getProgramInfoLog(program)); + return null; + } + gl.useProgram(program); + + // One triangle that covers the clip space; the shader only + // reads gl_FragCoord. + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); + const position = gl.getAttribLocation(program, 'position'); + gl.enableVertexAttribArray(position); + gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); + + this.u = { + time: gl.getUniformLocation(program, 'time'), + resolution: gl.getUniformLocation(program, 'resolution'), + tint: gl.getUniformLocation(program, 'tint'), + shift: gl.getUniformLocation(program, 'shift'), + }; + gl.clearColor(0, 0, 0, 0); + this.gl = gl; + this.recolor(); + this.aim(); + return gl; + } + + // The line is the words' blue: --accent from the page's own + // palette (custom.css sets it for both schemes), read when the + // scheme changes rather than every frame. + recolor() { + if (!this.gl) return; + const cs = getComputedStyle(this.hero || document.documentElement); + const tint = rgb(cs.getPropertyValue('--accent'), [0.13, 0.19, 0.31]); + this.gl.uniform3f(this.u.tint, tint[0], tint[1], tint[2]); + } + + draw(time) { + const gl = this.gl; + if (!gl) return; + gl.uniform1f(this.u.time, time); + gl.uniform2f(this.u.resolution, this.canvas.width, this.canvas.height); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.drawArrays(gl.TRIANGLES, 0, 3); + } + + stop() { + this.stopped = true; + if (this.raf) cancelAnimationFrame(this.raf); + window.removeEventListener('resize', this._onResize); + window.removeEventListener('scroll', this._onScroll); + if (this.observer) this.observer.disconnect(); + this.dark.removeEventListener('change', this._onTheme); + this.reduced.removeEventListener('change', this._onTheme); + this.canvas.removeEventListener('webglcontextlost', this._onLost); + this.canvas.removeEventListener('webglcontextrestored', this._onRestored); + if (this.gl) { + const lose = this.gl.getExtension('WEBGL_lose_context'); + if (lose) lose.loseContext(); + } + if (this.hero) this.hero.style.minHeight = ''; + if (this.copy) { + this.copy.style.opacity = ''; + this.copy.style.visibility = ''; + } + this.canvas.remove(); + this.name.remove(); + } +} + +export function mount(container) { + return new WestraHero(container); +} diff --git a/questions/index.yaml b/questions/index.yaml index 6972e54..ca31b81 100644 --- a/questions/index.yaml +++ b/questions/index.yaml @@ -2,7 +2,7 @@ # as intact as possible, in the portal's question/alternative form. # Norwegian is the source language; the original site's wording is # kept verbatim where it existed. -name: Hva kan Westra Engineering gjøre for prosjektet deres? +name: Hva kan vi gjøre for prosjektet deres? description: > Westra Engineering AS er et serviceorientert og fleksibelt rådgiverfirma innen bygg- og anleggsbransjen, som tar seg av hele @@ -11,6 +11,36 @@ description: > responsible: name: Jørn Wenger contact: jorn@westra-engineering.no +# Bildeserien: prosjektene tvers over skjermen, under navnet og linjen +# i heroen (hero.js). Bildene er de fra westra-engineering.no i sin +# egen størrelse; bildetekstene er prosjektenes navn fra /prosjekter. +slideshow: + max_height: 32 + slides: + - image: images/aarvoll-flerbrukshall.jpg + width: 768 + height: 576 + caption: Årvoll Flerbrukshall — prosjekteringsledelse og byggeoppfølging + - image: images/edvard-griegs-vei.jpg + width: 640 + height: 480 + caption: Sykehjem i Edvard Griegs vei, Kolbotn — prosjektledelse og byggeoppfølging + - image: images/whru-eldfisk.jpg + width: 768 + height: 576 + caption: Waste Heat Recovery-unit på Eldfisk Ester — byggeoppfølging + - image: images/moylandsenteret.jpg + width: 768 + height: 576 + caption: Møylandsenteret i Andebu — prosjekteringsledelse og byggoppfølging + - image: images/m343-modul.jpg + width: 603 + height: 452 + caption: M343-modulen for Heidrun — strukturanalyse + - image: images/oppdriftstanker.jpg + width: 604 + height: 402 + caption: Oppdriftstanker for riser base — design alternatives: - name: Hvorfor velge Westra Engineering som partner description: > @@ -88,8 +118,6 @@ alternatives: description: > Prosjekteringsledelse og byggeoppfølging for formålsbygg, bruer og offshore-relaterte konstruksjoner. - - ![Årvoll Flerbrukshall](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/aarvoll-flerbrukshall.jpg) icon: lucide:hard-hat - name: Ta kontakt description: > diff --git a/questions/prosjekter.yaml b/questions/prosjekter.yaml index 5e98af7..1c25e7d 100644 --- a/questions/prosjekter.yaml +++ b/questions/prosjekter.yaml @@ -3,6 +3,60 @@ name: Hva har Westra Engineering bygget? description: > Et utvalg gjennomførte prosjekter — landbaserte bygg, bru, og offshore-relaterte konstruksjoner. +# Bildeserien: alle prosjektbildene fra westra-engineering.no, i sidens +# rekkefølge, tvers over skjermen under spørsmålet. Nordenga bru har +# ikke noe bilde. +slideshow: + max_height: 50 + slides: + - image: images/edvard-griegs-vei.jpg + width: 640 + height: 480 + caption: Sykehjem i Edvard Griegs vei, Kolbotn — prosjektledelse og byggeoppfølging + - image: images/aarvoll-flerbrukshall.jpg + width: 768 + height: 576 + caption: Årvoll Flerbrukshall — prosjekteringsledelse og byggeoppfølging + - image: images/moylandsenteret.jpg + width: 768 + height: 576 + caption: Møylandsenteret i Andebu, Sandefjord — prosjekteringsledelse og byggoppfølging + - image: images/aurskog-holand.jpg + width: 768 + height: 576 + caption: Aurskog-Høland helsesenter — prosjekteringsledelse og byggeoppfølging + - image: images/whru-eldfisk.jpg + width: 768 + height: 576 + caption: Waste Heat Recovery-unit på Eldfisk Ester, for ConocoPhillips — byggeoppfølging + - image: images/utlasting-whru-1.jpg + width: 768 + height: 461 + caption: Utlasting av WHRU for offshore-installasjon + - image: images/utlasting-whru-2.jpg + width: 768 + height: 461 + caption: Utlasting av WHRU, del to + - image: images/m343-modul.jpg + width: 603 + height: 452 + caption: M343-modulen for vanninjeksjon og sulfatfjerning — strukturanalyse, installert på Heidrun 2002 + - image: images/c17-statfjord.jpg + width: 602 + height: 397 + caption: C17-modulen på Statfjord C — strukturanalyse 1997–1998 + - image: images/ror-sjosikring.jpg + width: 602 + height: 397 + caption: Løfting og sjøsikring av rør for offshore-installasjon + - image: images/klammer-loft.jpg + width: 608 + height: 397 + caption: Klammer for offshore-løft + - image: images/oppdriftstanker.jpg + width: 604 + height: 402 + caption: Oppdriftstanker ved installasjon av offshore riser base alternatives: - name: Landbaserte prosjekter description: > @@ -14,26 +68,18 @@ alternatives: - name: Sykehjem i Edvard Griegs vei, Kolbotn description: > Oppegård kommune. Prosjektledelse og byggeoppfølging. - - ![Sykehjem i Edvard Griegs vei](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/edvard-griegs-vei.jpg) icon: lucide:building - name: Årvoll Flerbrukshall description: > Prosjekteringsledelse og byggeoppfølging. - - ![Årvoll Flerbrukshall](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/aarvoll-flerbrukshall.jpg) icon: lucide:warehouse - name: Møylandsenteret i Andebu, Sandefjord kommune description: > Prosjekteringsledelse og byggoppfølging. - - ![Møylandsenteret](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/moylandsenteret.jpg) icon: lucide:building-2 - name: Aurskog-Høland helsesenter description: > Prosjekteringsledelse og byggeoppfølging. - - ![Aurskog-Høland helsesenter](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/aurskog-holand.jpg) icon: lucide:hospital - name: Nordenga Bru, Østre Tangent description: > @@ -48,16 +94,10 @@ alternatives: - name: Waste Heat Recovery-unit på Eldfisk Ester description: > Installert for ConocoPhillips. Byggeoppfølging. - - ![WHRU på Eldfisk Ester](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/whru-eldfisk.jpg) icon: lucide:flame - name: Utlasting og offshore-installasjon description: > Prosjekttjenester relatert til utlasting og offshore-installasjon. - - ![Utlasting av WHRU](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/utlasting-whru-1.jpg) - - ![Utlasting av WHRU, del to](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/utlasting-whru-2.jpg) icon: lucide:ship - name: Design og strukturanalyse av offshore-moduler description: > @@ -69,14 +109,10 @@ alternatives: description: > Strukturanalyse, 2001. Modulen ble installert på Heidrun i 2002. - - ![M343-modulen](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/m343-modul.jpg) icon: lucide:box - name: C17-modulen på Statfjord C description: > Strukturanalyse, 1997–1998. - - ![C17-modulen](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/c17-statfjord.jpg) icon: lucide:box - name: Strukturdesign av offshore-relatert utstyr description: > @@ -88,21 +124,15 @@ alternatives: description: > Design relatert til løfting og sjøsikring av rør for offshore-installasjon. - - ![Sjøsikring av rør](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/ror-sjosikring.jpg) icon: lucide:cable - name: Klammer for offshore-løft description: > Design av klammer for offshore-løft. - - ![Klammer for offshore-løft](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/klammer-loft.jpg) icon: lucide:grip - name: Oppdriftstanker for riser base description: > Design av oppdriftstanker ved installasjon av offshore riser base. - - ![Oppdriftstanker](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/oppdriftstanker.jpg) icon: lucide:database - name: Tilbake description: > diff --git a/site.yaml b/site.yaml index 2b36621..1797502 100644 --- a/site.yaml +++ b/site.yaml @@ -3,3 +3,6 @@ wordmark: https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/i wordmark_height: 3 lang: no stylesheet: custom.css +hero: + kind: module + module: hero.js # the name over a lightning line, served at /site/hero.js