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