openvibers/openvibe.shared app-icon.js
app-icon.js
| 1 | ; |
| 2 | // ═══════════════════════════════════════════════════════════════ |
| 3 | // openvibe-shared/app-icon — one app icon design for every OpenVibe site. |
| 4 | // |
| 5 | // The icon is the OpenVibe mark (openvibe-shared/mark: the Pair, a ring O and a round V) on the network's deep navy, |
| 6 | // with a soft glow behind it. No text: a home-screen icon is a few millimetres wide. |
| 7 | // svg({ site, maskable }) full-bleed square SVG. maskable keeps the mark inside the 40 % radius |
| 8 | // safe zone, so round, squircle and teardrop masks never clip it. |
| 9 | // favicon({ site }) the heavy form on a rounded tile, tuned for 16–32 px. |
| 10 | // manifest({ site, name, shortName, description, startUrl }) web app manifest with shared colours. |
| 11 | // SITES accent (the glow and the share cards) and the light in the O per site: Live's is the |
| 12 | // red record light, as in ov-mark.js's variants; white means none. |
| 13 | // PNG sets are rendered from these by scripts/build-app-icons.js. |
| 14 | // ═══════════════════════════════════════════════════════════════ |
| 15 | const BG = '#0a0f1c', BG_EDGE = '#060912'; |
| 16 | const SITES = { |
| 17 | network: { accent: ['#60a5fa', '#2563eb'], dot: '#ffffff' }, |
| 18 | live: { accent: ['#60a5fa', '#2563eb'], dot: '#ef4444' }, |
| 19 | tools: { accent: ['#38bdf8', '#2563eb'], dot: '#ffffff' }, |
| 20 | community: { accent: ['#34d399', '#2563eb'], dot: '#ffffff' }, |
| 21 | games: { accent: ['#a78bfa', '#2563eb'], dot: '#facc15' }, |
| 22 | media: { accent: ['#fbbf24', '#2563eb'], dot: '#ffffff' }, |
| 23 | space: { accent: ['#818cf8', '#4f46e5'], dot: '#ffffff' }, |
| 24 | ai: { accent: ['#22d3ee', '#2563eb'], dot: '#ffffff' }, |
| 25 | }; |
| 26 | const of = (site) => SITES[site] || SITES.network; |
| 27 | |
| 28 | const MARK = require('./mark'); |
| 29 | |
| 30 | // The mark centred on 0,0 (openvibe-shared/mark draws it centred on 50,50 in a 100-unit box), scaled. |
| 31 | function mark(site, scale, form = 'pair') { |
| 32 | const c = of(site), g = MARK.geo(form), { body } = MARK.art({ form, id: 'ov', depth: true }); |
| 33 | const light = c.dot && c.dot.toLowerCase() !== '#ffffff' ? `<circle cx="${g.cx}" cy="${g.cy}" r="${+((g.r - g.sw / 2) * 0.62).toFixed(2)}" fill="${c.dot}"/>` : ''; |
| 34 | return `<g transform="scale(${scale}) translate(-50 -50)"><ellipse cx="50" cy="50" rx="52" ry="40" fill="url(#glow)"/>${body}${light}</g>`; |
| 35 | } |
| 36 | |
| 37 | function defs(site, form = 'pair') { |
| 38 | const [a] = of(site).accent; |
| 39 | return `<defs>${MARK.art({ form, id: 'ov', depth: true }).defs} |
| 40 | <radialGradient id="glow"><stop offset="0" stop-color="${a}" stop-opacity=".3"/><stop offset="1" stop-color="${a}" stop-opacity="0"/></radialGradient> |
| 41 | <radialGradient id="bg" cx=".5" cy=".38" r=".8"><stop offset="0" stop-color="#16223a"/><stop offset=".6" stop-color="${BG}"/><stop offset="1" stop-color="${BG_EDGE}"/></radialGradient></defs>`; |
| 42 | } |
| 43 | |
| 44 | /** Full-bleed square. `maskable`: mark radius ≤ 40 % of the half-width; otherwise it fills more of the tile. */ |
| 45 | function svg({ site = 'network', maskable = true, size = 512 } = {}) { |
| 46 | // The mark's farthest point (the V's caps) is 50 units from its centre: × 4 is 200 of the 256 half-width, inside |
| 47 | // the maskable safe zone (40 % of the width, 204.8). A rounded tile carries it at three quarters of its width. |
| 48 | const scale = maskable ? 4 : 4.2; |
| 49 | return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-256 -256 512 512" width="${size}" height="${size}">${defs(site)} |
| 50 | <rect x="-256" y="-256" width="512" height="512" fill="url(#bg)"${maskable ? '' : ' rx="112"'}/>${mark(site, scale)}</svg>`; |
| 51 | } |
| 52 | |
| 53 | /** Small sizes: the heavy form, as wide as the tile allows, and the tile rounded because nothing masks a favicon. */ |
| 54 | function favicon({ site = 'network' } = {}) { |
| 55 | return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 -32 64 64">${defs(site, 'heavy')}<rect x="-32" y="-32" width="64" height="64" rx="15" fill="url(#bg)"/>${mark(site, 0.58, 'heavy')}</svg>`; |
| 56 | } |
| 57 | |
| 58 | function manifest({ site = 'network', name, shortName, description, startUrl = '/', iconBase = '/assets' }) { |
| 59 | const sizes = [72, 192, 512]; |
| 60 | return { |
| 61 | name, short_name: shortName || name, description, start_url: startUrl, scope: '/', display: 'standalone', |
| 62 | background_color: BG, theme_color: '#3b82f6', |
| 63 | icons: [...sizes.map(s => ({ src: `${iconBase}/logo-${s}.png`, sizes: `${s}x${s}`, type: 'image/png', purpose: 'any' })), |
| 64 | ...[192, 512].map(s => ({ src: `${iconBase}/logo-maskable-${s}.png`, sizes: `${s}x${s}`, type: 'image/png', purpose: 'maskable' })), |
| 65 | { src: `${iconBase}/logo.svg`, sizes: 'any', type: 'image/svg+xml', purpose: 'any' }], |
| 66 | }; |
| 67 | } |
| 68 | |
| 69 | // First paint must never depend on another server. These two inline tags give every page the network's |
| 70 | // dark canvas and the visitor's cached theme immediately; theme-loader.js (deferred, from the Network) |
| 71 | // reconciles with the account afterwards. If the Network is slow or restarting, the page is still |
| 72 | // styled and readable instead of blank white. |
| 73 | const CRITICAL = `<style id="ov-critical">html{background:${BG};color:#e6edf7;color-scheme:dark}</style>` |
| 74 | + `<script>(function(){try{var r=localStorage.getItem('ov_theme');if(!r)return;var t=JSON.parse(r),v=t&&t.variables;if(!v)return;var e=document.documentElement;for(var k in v)if(k.charAt(0)==='-')e.style.setProperty(k,v[k]);if(v['--bg-primary'])e.style.background=v['--bg-primary'];if(v['--text-primary'])e.style.color=v['--text-primary'];if(v['--color-scheme']==='light'||v['--color-scheme']==='dark')e.style.colorScheme=v['--color-scheme'];if(t.id)e.setAttribute('data-theme',t.id)}catch(_){}})();</script>`; |
| 75 | |
| 76 | /** <head> tags every site shares: SVG favicon (inline, no request), theme colour, and the PNG touch icon when the site ships one. */ |
| 77 | function headTags({ site = 'network', iconBase = null } = {}) { |
| 78 | const uri = 'data:image/svg+xml,' + encodeURIComponent(favicon({ site })).replace(/'/g, '%27'); |
| 79 | return [`<link rel="icon" type="image/svg+xml" data-ov-icon="${String(site).replace(/[^a-z]/g, '')}" href="${uri}">`, '<meta name="theme-color" content="#0d131d">', '<meta name="color-scheme" content="dark">', CRITICAL, |
| 80 | iconBase ? `<link rel="apple-touch-icon" href="${iconBase}/logo-192.png">` : '', iconBase ? `<link rel="manifest" href="/manifest.webmanifest">` : ''].filter(Boolean).join('\n'); |
| 81 | } |
| 82 | |
| 83 | module.exports = { svg, favicon, manifest, headTags, CRITICAL, SITES, BG }; |
| 84 |