openvibers/openvibe.shared app-icon.js

app-icon.js

main 6.5 KB · 84 lines · javascript

1'use strict';
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// ═══════════════════════════════════════════════════════════════
15const BG = '#0a0f1c', BG_EDGE = '#060912';
16const 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};
26const of = (site) => SITES[site] || SITES.network;
27
28const 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.
31function 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
37function 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. */
45function 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. */
54function 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
58function 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.
73const 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. */
77function 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
83module.exports = { svg, favicon, manifest, headTags, CRITICAL, SITES, BG };
84