import { findMedia } from '~/data/repositories/media' import { renderPng, renderVariant } from '~/lib/media-images' import { uploadMedia } from '~/lib/media-upload' import { getStorage, objectPath } from '~/lib/storage' export const sampleWidth = 2000 export const sampleHeight = 1125 export type SampleImage = { key: string id: string project: string filename: string alt: string caption: string svg: () => string } function rng(seed: number): () => number { let state = seed % 4294967296 return () => { state = (state * 1664525 + 1013904223) % 4294967296 return state / 4294967296 } } function round(value: number): number { return Math.round(value * 100) / 100 } function frame(body: string, tint: string): string { return [ ``, '', ``, '', body, '', ].join('') } function ruleGrid(): string { const lines: string[] = [] for (let y = 150; y < 1040; y += 74) { lines.push(``) } return lines.join('') } function ruleEngine(): string { const tint = '#2e7d5b' const parts: string[] = [ruleGrid()] for (let index = 0; index < 3; index += 1) { const y = 240 + index * 250 parts.push(``) parts.push(``) parts.push(``) parts.push(``) parts.push(``) parts.push( ``, ) parts.push(``) } return frame(parts.join(''), tint) } function columnMenu(): string { const tint = '#2b4a9b' const parts: string[] = [] const columns = [180, 560, 940, 1320, 1620] const widths = [340, 340, 340, 260, 220] parts.push('') columns.forEach((x, column) => { const width = widths[column] ?? 300 parts.push(``) for (let row = 0; row < 9; row += 1) { const y = 260 + row * 84 const bar = Math.round(width * (0.45 + ((row + column) % 4) * 0.13)) const fill = column === 3 ? tint : '#dedcd3' const opacity = column === 3 ? round(0.85 - (row % 4) * 0.15) : 1 parts.push(``) } }) for (let row = 0; row < 9; row += 1) { const y = 244 + row * 84 parts.push(``) } parts.push(``) for (let entry = 0; entry < 4; entry += 1) { parts.push(``) } return frame(parts.join(''), tint) } function trackerMap(): string { const tint = '#b4761a' const parts: string[] = [] const random = rng(20260730) for (let x = 180; x < 1880; x += 106) { parts.push(``) } for (let y = 150; y < 1020; y += 96) { parts.push(``) } for (let index = 0; index < 220; index += 1) { const x = round(160 + random() * 1700) const y = round(140 + random() * 860) const radius = round(3 + random() * 4) parts.push(``) } const clusters = [ { x: 520, y: 400, size: 96 }, { x: 1180, y: 620, size: 132 }, { x: 1560, y: 320, size: 74 }, ] for (const cluster of clusters) { parts.push(``) parts.push(``) parts.push(``) } parts.push(``) return frame(parts.join(''), tint) } function slaClock(): string { const tint = '#2e7d5b' const parts: string[] = [] const pauses = [ { x: 700, width: 220 }, { x: 1420, width: 220 }, ] for (const pause of pauses) { parts.push(``) } for (let index = 0; index < 7; index += 1) { const y = 250 + index * 110 const start = 180 + index * 42 const width = 420 + ((index * 7) % 5) * 190 parts.push(``) parts.push(``) parts.push(``) } parts.push('') for (let tick = 0; tick < 12; tick += 1) { parts.push(``) } return frame(parts.join(''), tint) } function inbox(): string { const tint = '#6b4ba8' const parts: string[] = [] const lanes = [200, 700, 1200] lanes.forEach((x, lane) => { for (let row = 0; row < 4; row += 1) { const y = 210 + row * 120 parts.push(``) parts.push(``) parts.push(``) parts.push(``) } parts.push( ``, ) }) parts.push(``) parts.push('') parts.push('') return frame(parts.join(''), tint) } export const sampleImages: SampleImage[] = [ { key: 'regel-engine', id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00001', project: 'trakk', filename: 'regel-engine-bedingung-aktion.png', alt: 'Schema der Regel-Engine: links drei Bedingungen, rechts drei Aktionen, jeweils mit einer Linie verbunden', caption: 'Jede Bedingung zeigt auf genau eine Aktion.', svg: ruleEngine, }, { key: 'spaltenmenue', id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00002', project: 'mta360', filename: 'spaltenmenue-rechtsklick.png', alt: 'Tabelle mit fünf Spalten, über der vierten Spalte ist ein Menü mit vier Einträgen geöffnet', caption: 'Das Spaltenmenü öffnet sich über der Spalte, die es betrifft.', svg: columnMenu, }, { key: 'tracker-karte', id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00003', project: 'mta-telematik', filename: 'tracker-karte-cluster.png', alt: 'Kartenausschnitt mit vielen kleinen Punkten und drei hervorgehobenen Ballungen, die mit einer gestrichelten Linie verbunden sind', caption: 'Dicht beieinander liegende Tracker werden zu Ballungen zusammengefasst.', svg: trackerMap, }, { key: 'sla-uhr', id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00004', project: 'trakk', filename: 'sla-uhr-pausiert.png', alt: 'Zeitleiste mit sieben Balken, zwei helle senkrechte Bänder markieren die Zeiten, in denen die Uhr steht', caption: 'Die hellen Bänder sind Wochenenden und Feiertage, dort läuft die Uhr nicht.', svg: slaClock, }, { key: 'eingang', id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00005', project: 'orbit', filename: 'kommentare-ein-eingang.png', alt: 'Drei Spalten mit Kommentaren laufen über geschwungene Linien in einen gemeinsamen Eingang unten zusammen', caption: 'Drei Kanäle, ein Eingang.', svg: inbox, }, ] export async function ensureSampleMedia(projectIds: Map): Promise> { const storage = getStorage() const ids = new Map() for (const sample of sampleImages) { const projectId = projectIds.get(sample.project) if (!projectId) { continue } ids.set(sample.key, sample.id) const existing = await findMedia(sample.id) if (existing) { if (!(await storage.exists(existing.path))) { const body = await renderPng(sample.svg()) await storage.put(existing.path, body, 'image/png') for (const variant of existing.variants) { const rendered = await renderVariant(body, variant.width) await storage.put(variant.path, rendered.body, 'image/webp') } } continue } const path = objectPath(projectId, sample.id, 'original.png') const stored = await storage.read(path) const body = stored ? stored.body : await renderPng(sample.svg()) await uploadMedia({ id: sample.id, projectId, filename: sample.filename, body, alt: sample.alt, caption: sample.caption, reuse: true, }) } return ids }