diff --git a/src/components/WinModal.vue b/src/components/WinModal.vue index d0b69ca..04b2934 100644 --- a/src/components/WinModal.vue +++ b/src/components/WinModal.vue @@ -4,7 +4,7 @@ import { Fireworks } from 'fireworks-js'; import { usePuzzleStore } from '@/stores/puzzle'; import { useI18n } from '@/composables/useI18n'; import { useTimer } from '@/composables/useTimer'; -import { Download } from 'lucide-vue-next'; +import { Download, FileCode } from 'lucide-vue-next'; import { calculateDifficulty } from '@/utils/puzzleUtils'; const store = usePuzzleStore(); @@ -41,30 +41,43 @@ const playFanfare = async () => { } } masterGain = audioContext.createGain(); - masterGain.gain.value = 0.18; + masterGain.gain.value = 0.25; // Slightly louder but softer tone masterGain.connect(audioContext.destination); - const notes = [ - { time: 0.0, dur: 0.18, freqs: [523.25, 659.25, 783.99] }, - { time: 0.2, dur: 0.18, freqs: [587.33, 740.0, 880.0] }, - { time: 0.4, dur: 0.22, freqs: [659.25, 830.61, 987.77] }, - { time: 0.7, dur: 0.35, freqs: [698.46, 880.0, 1046.5] } - ]; + const now = audioContext.currentTime; - notes.forEach(({ time, dur, freqs }) => { - freqs.forEach((freq) => { - const osc = audioContext.createOscillator(); - const gain = audioContext.createGain(); - osc.type = 'triangle'; - osc.frequency.value = freq; - gain.gain.setValueAtTime(0.0001, now + time); - gain.gain.linearRampToValueAtTime(0.8, now + time + 0.02); - gain.gain.exponentialRampToValueAtTime(0.0001, now + time + dur); - osc.connect(gain); - gain.connect(masterGain); - osc.start(now + time); - osc.stop(now + time + dur + 0.05); - }); - }); + + const playNote = (freq, startTime, duration) => { + const osc = audioContext.createOscillator(); + const gain = audioContext.createGain(); + + // Mix of sine and triangle for a bell-like quality + osc.type = 'sine'; + osc.frequency.value = freq; + + // Envelope for elegant bell/chime sound + gain.gain.setValueAtTime(0, startTime); + gain.gain.linearRampToValueAtTime(0.4, startTime + 0.05); // Soft attack + gain.gain.exponentialRampToValueAtTime(0.01, startTime + duration); // Long release + + osc.connect(gain); + gain.connect(masterGain); + + osc.start(startTime); + osc.stop(startTime + duration + 0.1); + }; + + // C Major 7 Arpeggio sequence (C5, E5, G5, B5, C6) - Elegant & Uplifting + const sequence = [ + { freq: 523.25, time: 0.0, dur: 0.8 }, // C5 + { freq: 659.25, time: 0.1, dur: 0.8 }, // E5 + { freq: 783.99, time: 0.2, dur: 0.8 }, // G5 + { freq: 987.77, time: 0.3, dur: 0.8 }, // B5 (Maj7) + { freq: 1046.50, time: 0.4, dur: 2.0 }, // C6 (High C resolve) + // Add a bass root note at the end for fullness + { freq: 523.25, time: 0.4, dur: 2.0 } // C5 + ]; + + sequence.forEach(note => playNote(note.freq, now + note.time, note.dur)); }; const triggerVibration = () => { @@ -189,6 +202,124 @@ const buildShareCanvas = () => { return canvas; }; +const buildShareSVG = () => { + const grid = store.playerGrid; + if (!grid || !grid.length) return null; + + const appUrl = 'https://nonograms.7u.pl/'; + const size = store.size; + const maxBoard = 640; + const cellSize = Math.max(8, Math.floor(maxBoard / size)); + const boardSize = cellSize * size; + const padding = 28; + const headerHeight = 64; + const footerHeight = 28; + const infoHeight = 40; + const width = boardSize + padding * 2; + const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight; + + // Colors + const bgGradientStart = '#1b2a4a'; + const bgGradientEnd = '#0a1324'; + const overlayColor = 'rgba(0, 0, 0, 0.35)'; + const textColor = '#e8fbff'; + const gridColor = 'rgba(255, 255, 255, 0.06)'; + const gridLineColor = 'rgba(255, 255, 255, 0.12)'; + const filledColor = '#00f2fe'; + const crossColor = 'rgba(255, 255, 255, 0.5)'; + const urlColor = 'rgba(255, 255, 255, 0.75)'; + + // Difficulty Logic + const densityPercent = Math.round(store.currentDensity * 100); + const difficultyKey = calculateDifficulty(store.currentDensity); + let diffColor = '#33ff33'; + if (difficultyKey === 'extreme') diffColor = '#ff3333'; + else if (difficultyKey === 'hardest') diffColor = '#ff9933'; + else if (difficultyKey === 'harder') diffColor = '#ffff33'; + const difficultyText = t(`difficulty.${difficultyKey}`); + const diffLabel = `${t('win.difficulty')} ${difficultyText} (${densityPercent}%)`; + + let svgContent = ``; + + // Background + svgContent += ` + + + + + + + + + `; + + // Text: Title & Time + svgContent += ` + ${t('app.title')} + ${t('win.time')} ${formattedTime.value} + `; + + // Text: Difficulty (Right Aligned - manual approx or end anchor) + svgContent += ` + ${diffLabel} + `; + + const gridX = padding; + const gridY = padding + headerHeight; + + // Grid Background + svgContent += ``; + + // Grid Lines + let gridLines = ''; + for (let i = 0; i <= size; i++) { + const pos = i * cellSize; + // Vertical + gridLines += ``; + // Horizontal + gridLines += ``; + } + svgContent += gridLines; + + // Cells + let cells = ''; + const lineWidth = Math.max(1.5, Math.floor(cellSize * 0.12)); + + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + const state = grid[r]?.[c]; + const cx = gridX + c * cellSize; + const cy = gridY + r * cellSize; + + if (state === 1) { // Filled + cells += ``; + } else if (state === 2) { // Cross + const d = cellSize * 0.6; + const off = cellSize * 0.2; + cells += ` + + `; + } + } + } + svgContent += cells; + + // Guide Usage + if (store.guideUsageCount > 0) { + const totalCells = store.size * store.size; + const percent = Math.min(100, Math.round((store.guideUsageCount / totalCells) * 100)); + const guideText = t('win.usedGuide', { count: store.guideUsageCount, percent }); + svgContent += `⚠️ ${guideText}`; + } + + // URL + svgContent += `${appUrl}`; + + svgContent += ''; + return svgContent; +}; + const canvasToBlob = (canvas) => new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), 'image/png')); const createShareBlob = async () => { @@ -197,6 +328,20 @@ const createShareBlob = async () => { return canvasToBlob(canvas); }; +const downloadShareSVG = () => { + const svgString = buildShareSVG(); + if (!svgString) return; + const blob = new Blob([svgString], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `nonogram-${store.size}x${store.size}.svg`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +}; + const downloadShareImage = async () => { const blob = await createShareBlob(); if (!blob) return; @@ -345,6 +490,10 @@ onUnmounted(() => { + +