[{"data":1,"prerenderedAt":804},["ShallowReactive",2],{"project-en-gabonsa":3},{"id":4,"title":5,"body":6,"category":744,"cover":745,"date":746,"description":747,"draft":748,"extension":749,"gallery":750,"liveUrl":763,"locale":764,"meta":765,"navigation":325,"palette":766,"path":785,"repoUrl":786,"role":787,"seo":791,"stack":792,"stem":800,"tagline":801,"year":802,"__hash__":803},"projects\u002Fprojects\u002Fen\u002Fgabonsa.md","Gabonsa",{"type":7,"value":8,"toc":733},"minimark",[9,14,21,38,41,44,48,56,67,70,81,84,87,183,187,194,197,201,208,211,215,226,229,232,452,456,467,470,473,515,522,526,533,536,539,606,610,624,631,634,718,722,729],[10,11,13],"h2",{"id":12},"the-vision","The vision",[15,16,17],"blockquote",{},[18,19,20],"p",{},"imagine it's the future and everything is good.",[18,22,23,24,28,29,33,34,37],{},"Gabonsa isn't a festival that merely needs a website — it's a ",[25,26,27],"strong",{},"space of possibility",". Two independent Innsbruck collectives, ",[30,31,32],"em",{},"Gans Anders"," and the ",[30,35,36],{},"Bonanza Kollektiv",", merge into a single cultural platform. Two days, an international line-up, a reclaimed old driving range nestled between mountains and gravel lakes.",[18,39,40],{},"The site had to feel the same way: idealistic without cynicism, poetic, values-driven. No investor model, car-free travel by conviction, zero tolerance for discrimination — all wrapped in a design that flatly refuses the template. Loud, italic lettering, warm chocolate brown against acid lime, floating editorial collages instead of rigid grids.",[18,42,43],{},"My brief: translate that feeling into a fast, accessible, technically resilient experience — without ever letting the engineering get louder than the idea.",[10,45,47],{"id":46},"editorial-design-system","Editorial design system",[18,49,50,51,55],{},"Everything scales fluidly via ",[52,53,54],"code",{},"clamp()"," — no breakpoint jumps, just type that breathes from mobile to big screen. Teal is visible in every section; lime and magenta are pure exclamation marks (one or two per page at most). Over it all sits procedural film grain as an animated SVG filter — warm noise that keeps the surfaces alive.",[18,57,58,59,62,63,66],{},"The headline carries a subtle ",[25,60,61],{},"chromatic aberration",": two offset colour copies (cyan + magenta) in a ",[52,64,65],{},"screen"," blend, plus a fine flicker — a 90s glitch that makes the lettering vibrate. Hover it:",[68,69],"demo-chromatic",{},[18,71,72,73,76,77,80],{},"Motion as connective tissue: a seamless ",[25,74,75],{},"marquee"," that runs without any JavaScript — pure CSS keyframes with ",[52,78,79],{},"will-change: transform",", pausing on hover.",[82,83],"demo-marquee",{},[18,85,86],{},"The grain itself isn't an image but math — an animated SVG turbulence filter (the very same one sits over the demo stages above):",[88,89,94],"pre",{"className":90,"code":91,"language":92,"meta":93,"style":93},"language-css shiki shiki-themes github-light github-dark","\u002F* Film grain as an animated SVG filter — no asset, pure math *\u002F\n.grain::after {\n  mix-blend-mode: overlay;\n  background-image: url(\"data:image\u002Fsvg+xml;utf8,\u003Csvg ...>\\\n    \u003Cfilter id='n'>\u003CfeTurbulence type='fractalNoise' \\\n    baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'\u002F>\\\n    \u003CfeColorMatrix type='saturate' values='0'\u002F>\u003C\u002Ffilter>\\\n    \u003Crect width='100%' height='100%' filter='url(%23n)'\u002F>\u003C\u002Fsvg>\");\n}\n","css","",[52,95,96,105,116,132,153,159,165,171,177],{"__ignoreMap":93},[97,98,101],"span",{"class":99,"line":100},"line",1,[97,102,104],{"class":103},"sJ8bj","\u002F* Film grain as an animated SVG filter — no asset, pure math *\u002F\n",[97,106,108,112],{"class":99,"line":107},2,[97,109,111],{"class":110},"sScJk",".grain::after",[97,113,115],{"class":114},"sVt8B"," {\n",[97,117,119,123,126,129],{"class":99,"line":118},3,[97,120,122],{"class":121},"sj4cs","  mix-blend-mode",[97,124,125],{"class":114},": ",[97,127,128],{"class":121},"overlay",[97,130,131],{"class":114},";\n",[97,133,135,138,140,143,146,150],{"class":99,"line":134},4,[97,136,137],{"class":121},"  background-image",[97,139,125],{"class":114},[97,141,142],{"class":121},"url",[97,144,145],{"class":114},"(",[97,147,149],{"class":148},"sZZnC","\"data:image\u002Fsvg+xml;utf8,\u003Csvg ...>",[97,151,152],{"class":121},"\\\n",[97,154,156],{"class":99,"line":155},5,[97,157,158],{"class":121},"    \u003Cfilter id='n'>\u003CfeTurbulence type='fractalNoise' \\\n",[97,160,162],{"class":99,"line":161},6,[97,163,164],{"class":121},"    baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'\u002F>\\\n",[97,166,168],{"class":99,"line":167},7,[97,169,170],{"class":121},"    \u003CfeColorMatrix type='saturate' values='0'\u002F>\u003C\u002Ffilter>\\\n",[97,172,174],{"class":99,"line":173},8,[97,175,176],{"class":121},"    \u003Crect width='100%' height='100%' filter='url(%23n)'\u002F>\u003C\u002Fsvg>\");\n",[97,178,180],{"class":99,"line":179},9,[97,181,182],{"class":121},"}\n",[10,184,186],{"id":185},"a-navigation-thats-actually-fun","A navigation that's actually fun",[18,188,189,190,193],{},"Clearly structured through a numbering system (01, 01.1 …), yet anything but boring. One click opens the full menu of big categories; pick one with sub-items and the list slides aside while a numbered panel glides in from the right — flower scatter behind it. ",[25,191,192],{},"A navigation that's clear and fun."," Try it (the sub-items open the real pages):",[195,196],"demo-nav",{},[10,198,200],{"id":199},"the-flower-scatter","The flower scatter",[18,202,203,204,207],{},"Monochrome flower halftones (© Anna Rogler) drift across the page — feathered, blended into one another, loosely strewn. They're placed ",[25,205,206],{},"deterministically from a seed",": same word, same arrangement. This very scatter drifts — large and calm — behind this entire page. Click to reshuffle the seed:",[209,210],"demo-scatter",{},[10,212,214],{"id":213},"photo-shuffle-as-a-physics-driven-game","Photo shuffle as a physics-driven game",[18,216,217,218,221,222,225],{},"Photos drift across the page from a 39-image pool. The clever bit: a ",[25,219,220],{},"central registry with exclusive assignment"," — no two active slots ever show the same image. Each slot \"borrows\" a photo; when it advances, it returns the photo and draws a new one nobody else holds. The tick is monotonic, fed by scroll distance, pointer travel, clicks ",[25,223,224],{},"and"," device tilt.",[18,227,228],{},"Recreated here in miniature — three slots, one shared pool, never a duplicate. Tap a frame to advance its slot:",[230,231],"demo-photo-shuffle",{},[88,233,237],{"className":234,"code":235,"language":236,"meta":93,"style":93},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F Exclusive photo pool: nothing a slot currently holds appears twice\nconst available = new Set\u003Cnumber>(\n  Array.from({ length: PHOTO_COUNT }, (_, i) => i + 1),\n)\n\nfunction drawFromPool(rng: () => number): number {\n  const arr = Array.from(available)\n  const idx = Math.floor(rng() * arr.length)\n  const photo = arr[idx]\n  available.delete(photo) \u002F\u002F out of the pool until the slot returns it\n  return photo\n}\n","ts",[52,238,239,244,271,316,321,327,360,378,411,423,438,447],{"__ignoreMap":93},[97,240,241],{"class":99,"line":100},[97,242,243],{"class":103},"\u002F\u002F Exclusive photo pool: nothing a slot currently holds appears twice\n",[97,245,246,250,253,256,259,262,265,268],{"class":99,"line":107},[97,247,249],{"class":248},"szBVR","const",[97,251,252],{"class":121}," available",[97,254,255],{"class":248}," =",[97,257,258],{"class":248}," new",[97,260,261],{"class":110}," Set",[97,263,264],{"class":114},"\u003C",[97,266,267],{"class":121},"number",[97,269,270],{"class":114},">(\n",[97,272,273,276,279,282,285,288,292,295,298,301,304,307,310,313],{"class":99,"line":118},[97,274,275],{"class":114},"  Array.",[97,277,278],{"class":110},"from",[97,280,281],{"class":114},"({ length: ",[97,283,284],{"class":121},"PHOTO_COUNT",[97,286,287],{"class":114}," }, (",[97,289,291],{"class":290},"s4XuR","_",[97,293,294],{"class":114},", ",[97,296,297],{"class":290},"i",[97,299,300],{"class":114},") ",[97,302,303],{"class":248},"=>",[97,305,306],{"class":114}," i ",[97,308,309],{"class":248},"+",[97,311,312],{"class":121}," 1",[97,314,315],{"class":114},"),\n",[97,317,318],{"class":99,"line":134},[97,319,320],{"class":114},")\n",[97,322,323],{"class":99,"line":155},[97,324,326],{"emptyLinePlaceholder":325},true,"\n",[97,328,329,332,335,337,340,343,346,348,351,354,356,358],{"class":99,"line":161},[97,330,331],{"class":248},"function",[97,333,334],{"class":110}," drawFromPool",[97,336,145],{"class":114},[97,338,339],{"class":110},"rng",[97,341,342],{"class":248},":",[97,344,345],{"class":114}," () ",[97,347,303],{"class":248},[97,349,350],{"class":121}," number",[97,352,353],{"class":114},")",[97,355,342],{"class":248},[97,357,350],{"class":121},[97,359,115],{"class":114},[97,361,362,365,368,370,373,375],{"class":99,"line":167},[97,363,364],{"class":248},"  const",[97,366,367],{"class":121}," arr",[97,369,255],{"class":248},[97,371,372],{"class":114}," Array.",[97,374,278],{"class":110},[97,376,377],{"class":114},"(available)\n",[97,379,380,382,385,387,390,393,395,397,400,403,406,409],{"class":99,"line":173},[97,381,364],{"class":248},[97,383,384],{"class":121}," idx",[97,386,255],{"class":248},[97,388,389],{"class":114}," Math.",[97,391,392],{"class":110},"floor",[97,394,145],{"class":114},[97,396,339],{"class":110},[97,398,399],{"class":114},"() ",[97,401,402],{"class":248},"*",[97,404,405],{"class":114}," arr.",[97,407,408],{"class":121},"length",[97,410,320],{"class":114},[97,412,413,415,418,420],{"class":99,"line":179},[97,414,364],{"class":248},[97,416,417],{"class":121}," photo",[97,419,255],{"class":248},[97,421,422],{"class":114}," arr[idx]\n",[97,424,426,429,432,435],{"class":99,"line":425},10,[97,427,428],{"class":114},"  available.",[97,430,431],{"class":110},"delete",[97,433,434],{"class":114},"(photo) ",[97,436,437],{"class":103},"\u002F\u002F out of the pool until the slot returns it\n",[97,439,441,444],{"class":99,"line":440},11,[97,442,443],{"class":248},"  return",[97,445,446],{"class":114}," photo\n",[97,448,450],{"class":99,"line":449},12,[97,451,182],{"class":114},[10,453,455],{"id":454},"webgl-photo-dissolve","WebGL photo dissolve",[18,457,458,459,462,463,466],{},"Every photo slot renders its transition in WebGL: the new image ",[30,460,461],{},"dissolves"," in through an organic, luminance-weighted noise mask — a melt rather than a hard cut. A pre-baked 128×128 noise texture spares expensive per-pixel math in the shader; shaders compile asynchronously via ",[52,464,465],{},"KHR_parallel_shader_compile"," so the main thread never stalls.",[18,468,469],{},"The principle works without a GPU too — drag across the frame to scrub the reveal along the noise mask:",[471,472],"demo-dissolve",{},[88,474,478],{"className":475,"code":476,"language":477,"meta":93,"style":93},"language-glsl shiki shiki-themes github-light github-dark","\u002F\u002F Both images fully opaque — the reveal happens through the noise mask\nfloat nMask = texture2D(uNoise, vUv).r;\nfloat reveal = smoothstep(nMask - 0.28, nMask + 0.28, ep);\n\u002F\u002F Source-image brightness gently bends the mask\nfloat lumBias = mix(0.0, lumA * 0.15, sin(ep * 3.14159));\nfloat finalReveal = clamp(reveal + lumBias, 0.0, 1.0);\ngl_FragColor = mix(colA, colB, finalReveal);\n","glsl",[52,479,480,485,490,495,500,505,510],{"__ignoreMap":93},[97,481,482],{"class":99,"line":100},[97,483,484],{},"\u002F\u002F Both images fully opaque — the reveal happens through the noise mask\n",[97,486,487],{"class":99,"line":107},[97,488,489],{},"float nMask = texture2D(uNoise, vUv).r;\n",[97,491,492],{"class":99,"line":118},[97,493,494],{},"float reveal = smoothstep(nMask - 0.28, nMask + 0.28, ep);\n",[97,496,497],{"class":99,"line":134},[97,498,499],{},"\u002F\u002F Source-image brightness gently bends the mask\n",[97,501,502],{"class":99,"line":155},[97,503,504],{},"float lumBias = mix(0.0, lumA * 0.15, sin(ep * 3.14159));\n",[97,506,507],{"class":99,"line":161},[97,508,509],{},"float finalReveal = clamp(reveal + lumBias, 0.0, 1.0);\n",[97,511,512],{"class":99,"line":167},[97,513,514],{},"gl_FragColor = mix(colA, colB, finalReveal);\n",[18,516,517,518,521],{},"On iOS the number of concurrent WebGL contexts is hard-capped at eight — an LRU eviction disposes the oldest off-screen renderers before the system crashes, and a lost context falls back cleanly to an ",[52,519,520],{},"\u003Cimg>",".",[10,523,525],{"id":524},"magnetic-cursor-magnetic-buttons","Magnetic cursor & magnetic buttons",[18,527,528,529,532],{},"On desktop a custom cursor replaces the system pointer: a hard dot that follows instantly, and a soft ring that trails with spring physics (Framer Motion) and swells over interactive elements. Buttons \"pull\" toward the cursor — a tactile sense of attraction that only activates on ",[52,530,531],{},"(hover: hover) and (pointer: fine)",", leaving touch devices untouched.",[18,534,535],{},"Bring your cursor near the button — it drifts to meet you, the ring swells:",[537,538],"demo-magnetic",{},[88,540,544],{"className":541,"code":542,"language":543,"meta":93,"style":93},"language-tsx shiki shiki-themes github-light github-dark","const x = useMotionValue(-200)\nconst sx = useSpring(x, { stiffness: 320, damping: 28, mass: 0.4 })\n\u002F\u002F small dot follows instantly (x), big ring trails on the spring (sx)\n","tsx",[52,545,546,568,601],{"__ignoreMap":93},[97,547,548,550,553,555,558,560,563,566],{"class":99,"line":100},[97,549,249],{"class":248},[97,551,552],{"class":121}," x",[97,554,255],{"class":248},[97,556,557],{"class":110}," useMotionValue",[97,559,145],{"class":114},[97,561,562],{"class":248},"-",[97,564,565],{"class":121},"200",[97,567,320],{"class":114},[97,569,570,572,575,577,580,583,586,589,592,595,598],{"class":99,"line":107},[97,571,249],{"class":248},[97,573,574],{"class":121}," sx",[97,576,255],{"class":248},[97,578,579],{"class":110}," useSpring",[97,581,582],{"class":114},"(x, { stiffness: ",[97,584,585],{"class":121},"320",[97,587,588],{"class":114},", damping: ",[97,590,591],{"class":121},"28",[97,593,594],{"class":114},", mass: ",[97,596,597],{"class":121},"0.4",[97,599,600],{"class":114}," })\n",[97,602,603],{"class":99,"line":118},[97,604,605],{"class":103},"\u002F\u002F small dot follows instantly (x), big ring trails on the spring (sx)\n",[10,607,609],{"id":608},"scroll-choreography-without-a-plugin","Scroll choreography without a plugin",[18,611,612,613,616,617,620,621,521],{},"Text reveals on scroll, word by word and character by character — with a hand-built splitter instead of the paid GSAP ",[52,614,615],{},"SplitText",". Lenis delivers silky smooth scrolling and is bridged to GSAP via ",[52,618,619],{},"useLenis(ScrollTrigger.update)"," so every animation stays frame-locked. Every motion respects ",[52,622,623],{},"prefers-reduced-motion",[18,625,626,627,630],{},"Word by word, with a staggered offset — hit ",[30,628,629],{},"replay"," to see it again:",[632,633],"demo-reveal",{},[88,635,637],{"className":541,"code":636,"language":543,"meta":93,"style":93},"const tween = gsap.to(targets, {\n  y: 0, opacity: 1, duration: 0.9,\n  ease: 'expo.out', stagger: 0.04,\n  scrollTrigger: { trigger: el, start: 'top 85%', once: true },\n})\n",[52,638,639,657,680,696,713],{"__ignoreMap":93},[97,640,641,643,646,648,651,654],{"class":99,"line":100},[97,642,249],{"class":248},[97,644,645],{"class":121}," tween",[97,647,255],{"class":248},[97,649,650],{"class":114}," gsap.",[97,652,653],{"class":110},"to",[97,655,656],{"class":114},"(targets, {\n",[97,658,659,662,665,668,671,674,677],{"class":99,"line":107},[97,660,661],{"class":114},"  y: ",[97,663,664],{"class":121},"0",[97,666,667],{"class":114},", opacity: ",[97,669,670],{"class":121},"1",[97,672,673],{"class":114},", duration: ",[97,675,676],{"class":121},"0.9",[97,678,679],{"class":114},",\n",[97,681,682,685,688,691,694],{"class":99,"line":118},[97,683,684],{"class":114},"  ease: ",[97,686,687],{"class":148},"'expo.out'",[97,689,690],{"class":114},", stagger: ",[97,692,693],{"class":121},"0.04",[97,695,679],{"class":114},[97,697,698,701,704,707,710],{"class":99,"line":134},[97,699,700],{"class":114},"  scrollTrigger: { trigger: el, start: ",[97,702,703],{"class":148},"'top 85%'",[97,705,706],{"class":114},", once: ",[97,708,709],{"class":121},"true",[97,711,712],{"class":114}," },\n",[97,714,715],{"class":99,"line":155},[97,716,717],{"class":114},"})\n",[10,719,721],{"id":720},"the-result","The result",[18,723,724,725,728],{},"A website that feels playful but is built for the hard cases: WebGL context loss, fullscreen quirks and motion sensors are all caught gracefully. German-first, fully accessible, schema.org ",[52,726,727],{},"Event"," markup for search — and an image language that stands on its own. Best explored live, right next door.",[730,731,732],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":93,"searchDepth":107,"depth":107,"links":734},[735,736,737,738,739,740,741,742,743],{"id":12,"depth":107,"text":13},{"id":46,"depth":107,"text":47},{"id":185,"depth":107,"text":186},{"id":199,"depth":107,"text":200},{"id":213,"depth":107,"text":214},{"id":454,"depth":107,"text":455},{"id":524,"depth":107,"text":525},{"id":608,"depth":107,"text":609},{"id":720,"depth":107,"text":721},"Festival · Creative Development","\u002Fimages\u002Fwork\u002Fgabonsa\u002Fcover.jpg","2026-06-16","Case study for the Gabonsa Festival website in Innsbruck — an editorial Next.js 15 experience with WebGL photo transitions, a physics-driven photo shuffle, a magnetic cursor and GSAP scroll choreography.",false,"md",[751,754,757,760],{"src":752,"alt":753},"\u002Fimages\u002Fwork\u002Fgabonsa\u002Flineup.jpg","Line-up page with hand-drawn underline and teal accent",{"src":755,"alt":756},"\u002Fimages\u002Fwork\u002Fgabonsa\u002Fvalues.jpg","Values page — awareness, sustainability, car-free travel",{"src":758,"alt":759},"\u002Fimages\u002Fwork\u002Fgabonsa\u002Fabout.jpg","About page: two Innsbruck collectives becoming one",{"src":761,"alt":762},"\u002Fimages\u002Fwork\u002Fgabonsa\u002Faftermovie.jpg","Aftermovie page with a custom video player","https:\u002F\u002Fgabonsa.vercel.app","en",{},[767,770,773,776,779,782],{"name":768,"hex":769},"Brown","#3B2314",{"name":771,"hex":772},"Teal","#00857C",{"name":774,"hex":775},"Paper","#E8DCC8",{"name":777,"hex":778},"Ink","#0A0A08",{"name":780,"hex":781},"Acid","#B8E61D",{"name":783,"hex":784},"Magenta","#E6007E","\u002Fprojects\u002Fen\u002Fgabonsa",null,[788,789,790],"Concept & Creative Direction","Design","Development",{"title":5,"description":747},[793,794,795,796,797,798,799],"Next.js 15","React 19","Tailwind CSS 4","GSAP + ScrollTrigger","Lenis","Framer Motion","WebGL","projects\u002Fen\u002Fgabonsa","A festival website as a space of possibility — loud typography, loose grids, warm noise.","2026","Ir3VCTEJmHeAdpzaQVxqVETG1hsF_J5kFaIZcyprpIc",1787633289822]