1// Draws the auto-generated default event banner (used when an event has no header image). 2// Shared by the single event template, event cards, the view-event popup, and card listings. 3// Deliberately plain: solid/gradient colour + title + date, no decoration â never touches 4// the real uploaded-header-image path (that's always a plain <img>/background-image, untouched). 5 6function gwp_eventbanner_shadehex(hex, percent) { 7 hex = (hex || '#2196F3').replace('#', ''); 8 if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 9 var num = parseInt(hex, 16); 10 if (isNaN(num)) return '#2196F3'; 11 var r = Math.min(255, Math.max(0, (num >> 16) + Math.round(255 * percent))); 12 var g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) + Math.round(255 * percent))); 13 var b = Math.min(255, Math.max(0, (num & 0xff) + Math.round(255 * percent))); 14 return '#' + (0x1000000 + r * 0x10000 + g * 0x100 + b).toString(16).slice(1); 15} 16 17function gwp_eventbanner_wraplines(ctx, text, maxWidth) { 18 var words = (text || '').split(' '); 19 var lines = []; 20 var line = ''; 21 for (var i = 0; i < words.length; i++) { 22 var test = line ? line + ' ' + words[i] : words[i]; 23 if (ctx.measureText(test).width > maxWidth && line) { 24 lines.push(line); 25 line = words[i]; 26 } else { 27 line = test; 28 } 29 } 30 if (line) lines.push(line); 31 return lines; 32} 33 34/** 35 * Draws the banner into `canvas` at its actual on-screen size (read from the DOM) and at 36 * full device pixel density. The element's CSS always owns the visible box (width/height, 37 * position) â this never sets inline width/height styles, so it can't fight the page's own 38 * layout. If the element isn't laid out yet (e.g. inside a popup that's still display:none), 39 * it waits a couple of frames and retries, rather than guessing a size and locking it in. 40 */ 41function gwp_renderEventBanner(canvas, opts) { 42 if (!canvas || typeof canvas.getContext !== 'function') return; 43 opts = opts || {}; 44 var w = Math.round(opts.width || canvas.clientWidth || 0); 45 var h = Math.round(opts.height || canvas.clientHeight || 0); 46 47 if (!w || !h) { 48 var retries = (opts._retries || 0) + 1; 49 if (retries > 6) { w = w || 400; h = h || 180; } else { 50 var nextOpts = Object.assign({}, opts, { _retries: retries }); 51 requestAnimationFrame(function () { 52 requestAnimationFrame(function () { gwp_renderEventBanner(canvas, nextOpts); }); 53 }); 54 return; 55 } 56 } 57 58 // Remember how this canvas was last drawn so a resize/orientation-change redraw 59 // (below) can reproduce it at the new box size â popups and cards get viewed on 60 // everything from phones to desktops, and a fixed-size draw would go stale. 61 canvas._gwpBannerOpts = opts; 62 63 var dpr = window.devicePixelRatio || 1; 64 canvas.width = Math.round(w * dpr); 65 canvas.height = Math.round(h * dpr); 66 67 var accent = opts.accentColor || '#2196F3'; 68 var ctx = canvas.getContext('2d'); 69 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 70 71 // Always left-to-right, never corner-to-corner â a diagonal gradient's angle changes with 72 // the box's aspect ratio, which looks inconsistent across a masonry grid where every card 73 // is a different shape. Horizontal reads the same regardless of width/height. 74 var grad = ctx.createLinearGradient(0, 0, w, 0); 75 grad.addColorStop(0, gwp_eventbanner_shadehex(accent, 0.08)); 76 grad.addColorStop(1, gwp_eventbanner_shadehex(accent, -0.14)); 77 ctx.fillStyle = grad; 78 ctx.fillRect(0, 0, w, h); 79 80 var align = opts.align === 'left' ? 'left' : 'center'; 81 var padX = opts.padX != null ? opts.padX : Math.max(14, w * 0.06); 82 var textX = align === 'left' ? padX : w / 2; 83 // Normally scales with the box (h * 0.17) so a single event page's big banner gets big 84 // text â but pass opts.titleSize for a fixed size instead, e.g. a masonry gallery where 85 // every card is a different height and matching font sizes reads more consistently than 86 // each card getting its own proportional size. 87 var titleSize = opts.titleSize || Math.round(h * 0.17); 88 var minTitleSize = opts.minTitleSize || Math.round(h * 0.09); 89 var maxLines = 2; 90 var lines = []; 91 92 ctx.textBaseline = 'alphabetic'; 93 ctx.textAlign = align; 94 while (titleSize >= minTitleSize) { 95 ctx.font = '800 ' + titleSize + 'px Arial, Helvetica, sans-serif'; 96 lines = gwp_eventbanner_wraplines(ctx, opts.title || '', w - padX * 2); 97 if (lines.length <= maxLines) break; 98 titleSize -= 1; 99 } 100 lines = lines.slice(0, maxLines); 101
102 var subtitleText = [opts.dateText, opts.timeText].filter(Boolean).join(' ⢠'); 103 var subtitleSize = Math.max(11, Math.round(titleSize * 0.42)); 104 var lineHeight = Math.round(titleSize * 1.22); 105 var gapBetween = Math.round(subtitleSize * 0.9); 106 var chipPadY = Math.round(subtitleSize * 0.4); 107 var chipH = subtitleText ? (subtitleSize + chipPadY * 2) : 0; 108 109 // Strict top-down stacking â title lines, then a fixed gap, then the date chip. 110 // Never computed as an overlapping "centered around a midpoint" guess. 111 var totalH = lines.length * lineHeight + (subtitleText ? gapBetween + chipH : 0); 112 var top = Math.max(padX * 0.6, (h - totalH) / 2); 113 114 ctx.fillStyle = '#ffffff'; 115 ctx.shadowColor = 'rgba(0,0,0,0.3)'; 116 ctx.shadowBlur = Math.max(1, titleSize * 0.05); 117 for (var i = 0; i < lines.length; i++) { 118 ctx.fillText(lines[i], textX, top + lineHeight * i + titleSize * 0.9); 119 } 120 ctx.shadowBlur = 0; 121 122 if (subtitleText) { 123 var chipTop = top + lines.length * lineHeight + gapBetween; 124 ctx.font = '700 ' + subtitleSize + 'px Arial, Helvetica, sans-serif'; 125 var textW = ctx.measureText(subtitleText).width; 126 var chipPadX = Math.round(subtitleSize * 0.55); 127 var chipW = Math.min(textW + chipPadX * 2, w - padX * 2); 128 var chipX = align === 'left' ? padX : (w - chipW) / 2; 129 130 ctx.fillStyle = 'rgba(0,0,0,0.28)'; 131 ctx.beginPath(); 132 if (typeof ctx.roundRect === 'function') { 133 ctx.roundRect(chipX, chipTop, chipW, chipH, chipH / 2); 134 } else { 135 ctx.rect(chipX, chipTop, chipW, chipH); 136 } 137 ctx.fill(); 138 139 ctx.fillStyle = '#ffffff'; 140 var subtitleX = align === 'left' ? chipX + chipPadX : w / 2; 141 ctx.fillText(subtitleText, subtitleX, chipTop + chipH / 2 + subtitleSize * 0.35); 142 } 143 144 if (opts.logoUrl) { 145 var img = new Image(); 146 img.crossOrigin = 'anonymous'; 147 img.onload = function () { 148 var logoH = h * 0.16; 149 var logoW = logoH * (img.width / img.height); 150 ctx.drawImage(img, padX, h - logoH - h * 0.08, logoW, logoH); 151 }; 152 img.src = opts.logoUrl; 153 } 154} 155 156function gwp_downloadEventBannerPNG(canvas, filename) { 157 if (!canvas) return; 158 var a = document.createElement('a'); 159 a.href = canvas.toDataURL('image/png'); 160 a.download = filename || 'event-banner.png'; 161 document.body.appendChild(a); 162 a.click(); 163 document.body.removeChild(a); 164} 165 166function gwp_resolveEventAccentColor(explicitColor, groupNames, groupColorMap, fallback) { 167 if (explicitColor) return explicitColor; 168 if (groupNames && groupNames.length && groupColorMap) { 169 for (var i = 0; i < groupNames.length; i++) { 170 if (groupColorMap[groupNames[i]]) return groupColorMap[groupNames[i]]; 171 } 172 } 173 return fallback || '#2196F3'; 174} 175 176/** 177 * Finds every not-yet-rendered banner canvas matching `selector` and draws it â but waits 178 * for the whole page (styles, fonts, everything) to finish loading first, plus a couple of 179 * animation frames after that. Measuring a canvas's box too early (e.g. from an inline 180 * <script> that runs while the stylesheet is still being applied, or before a masonry-style 181 * grid has settled) locks in the wrong resolution â the box later renders at its real size, 182 * but the bitmap drawn into it is still the old (usually smaller/differently-shaped) one, 183 * which is what a stretched, blurry banner actually is. 184 */ 185function gwp_initEventBannerCanvases(selector, extraOpts) { 186 function run() { 187 var list = document.querySelectorAll(selector + ':not([data-rendered])'); 188 for (var i = 0; i < list.length; i++) { 189 var c = list[i]; 190 c.setAttribute('data-rendered', '1'); 191 var opts = { 192 title: c.getAttribute('data-title') || '', 193 dateText: c.getAttribute('data-date') || '', 194 accentColor: c.getAttribute('data-accent') || '#2196F3' 195 }; 196 if (extraOpts) { for (var k in extraOpts) { opts[k] = extraOpts[k]; } } 197 gwp_renderEventBanner(c, opts); 198 } 199 } 200 function runAfterLayoutSettles() { 201 requestAnimationFrame(function () { requestAnimationFrame(run); }); 202 } 203 if (document.readyState === 'complete') { 204 runAfterLayoutSettles(); 205 } else { 206 window.addEventListener('load', runAfterLayoutSettles); 207 } 208} 209 210// Redraw every already-rendered banner on resize/orientation-change so a popup or card 211// left open across a rotation or window resize doesn't show a stale, wrongly-sized banner. 212(function () { 213 var _gwpBannerResizeTimer; 214 window.addEventListener('resize', function () { 215 clearTimeout(_gwpBannerResizeTimer); 216 _gwpBannerResizeTimer = setTimeout(function () {
217 var canvases = document.querySelectorAll('canvas'); 218 for (var i = 0; i < canvases.length; i++) { 219 if (canvases[i]._gwpBannerOpts) gwp_renderEventBanner(canvases[i], canvases[i]._gwpBannerOpts); 220 } 221 }, 150); 222 }); 223})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.