diff --git a/src/components/FixedBar.vue b/src/components/FixedBar.vue index f23656c..c76fd00 100644 --- a/src/components/FixedBar.vue +++ b/src/components/FixedBar.vue @@ -1,11 +1,10 @@ diff --git a/src/composables/useTimer.js b/src/composables/useTimer.js deleted file mode 100644 index 40fe5b0..0000000 --- a/src/composables/useTimer.js +++ /dev/null @@ -1,49 +0,0 @@ -import { ref, onUnmounted } from 'vue'; - -export function useTimer() { - const time = ref(0); - const timerInterval = ref(null); - const isRunning = ref(false); - - const formatTime = (seconds) => { - const m = Math.floor(seconds / 60).toString().padStart(2, '0'); - const s = (seconds % 60).toString().padStart(2, '0'); - return `${m}:${s}`; - }; - - const start = () => { - if (isRunning.value) return; - isRunning.value = true; - const startTime = Date.now() - (time.value * 1000); - - timerInterval.value = setInterval(() => { - time.value = Math.floor((Date.now() - startTime) / 1000); - }, 1000); - }; - - const stop = () => { - if (timerInterval.value) { - clearInterval(timerInterval.value); - timerInterval.value = null; - } - isRunning.value = false; - }; - - const reset = () => { - stop(); - time.value = 0; - }; - - onUnmounted(() => { - stop(); - }); - - return { - time, - isRunning, - start, - stop, - reset, - formatTime - }; -} diff --git a/src/constants/puzzles.js b/src/constants/puzzles.js new file mode 100644 index 0000000..7289f35 --- /dev/null +++ b/src/constants/puzzles.js @@ -0,0 +1,53 @@ +export const PUZZLES = { + easy: { + id: 'easy', + name: 'Uśmiech', + size: 5, + grid: [ + [0, 1, 0, 1, 0], + [0, 1, 0, 1, 0], + [0, 0, 0, 0, 0], + [1, 0, 0, 0, 1], + [0, 1, 1, 1, 0] + ] + }, + medium: { + id: 'medium', + name: 'Domek', + size: 10, + grid: [ + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], + [1, 0, 0, 1, 0, 0, 1, 0, 0, 1], + [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 0] + ] + }, + hard: { + id: 'hard', + name: 'Statek', + size: 15, + grid: [ + [0,0,0,0,0,0,0,1,0,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,1,1,1,1,1,0,0,0,0,0], + [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [0,1,1,1,1,1,1,1,1,1,1,1,1,1,0], + [0,0,1,1,1,1,1,1,1,1,1,1,1,0,0], + [0,0,0,1,1,1,1,1,1,1,1,1,0,0,0], + [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], + [0,0,0,0,0,1,1,1,1,1,0,0,0,0,0] + ] + } +}; diff --git a/src/stores/puzzle.js b/src/stores/puzzle.js index fff6e31..94b59de 100644 --- a/src/stores/puzzle.js +++ b/src/stores/puzzle.js @@ -1,89 +1,7 @@ import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; -import { generateRandomGrid } from '@/utils/puzzleUtils'; - -// Helper: Calculate hints for a line (row or column) -function calculateHints(line) { - const hints = []; - let currentRun = 0; - - for (const cell of line) { - if (cell === 1) { - currentRun++; - } else { - if (currentRun > 0) { - hints.push(currentRun); - currentRun = 0; - } - } - } - if (currentRun > 0) { - hints.push(currentRun); - } - return hints.length > 0 ? hints : [0]; -} - -// Helper: Validate if a line matches its hints -function validateLine(line, targetHints) { - const currentHints = calculateHints(line); - if (currentHints.length !== targetHints.length) return false; - return currentHints.every((h, i) => h === targetHints[i]); -} - -// Definicje zagadek (Static Puzzles) -const PUZZLES = { - easy: { - id: 'easy', - name: 'Uśmiech', - size: 5, - grid: [ - [0, 1, 0, 1, 0], - [0, 1, 0, 1, 0], - [0, 0, 0, 0, 0], - [1, 0, 0, 0, 1], - [0, 1, 1, 1, 0] - ] - }, - medium: { - id: 'medium', - name: 'Domek', - size: 10, - grid: [ - [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], - [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 1, 1, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], - [1, 0, 0, 1, 0, 0, 1, 0, 0, 1], - [1, 0, 0, 1, 1, 1, 1, 0, 0, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 0] - ] - }, - hard: { - id: 'hard', - name: 'Statek', - size: 15, - grid: [ - [0,0,0,0,0,0,0,1,0,0,0,0,0,0,0], - [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], - [0,0,0,0,0,1,1,1,1,1,0,0,0,0,0], - [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], - [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], - [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], - [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], - [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], - [0,0,0,0,0,0,1,1,1,0,0,0,0,0,0], - [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], - [0,1,1,1,1,1,1,1,1,1,1,1,1,1,0], - [0,0,1,1,1,1,1,1,1,1,1,1,1,0,0], - [0,0,0,1,1,1,1,1,1,1,1,1,0,0,0], - [0,0,0,0,1,1,1,1,1,1,1,0,0,0,0], - [0,0,0,0,0,1,1,1,1,1,0,0,0,0,0] - ] - } -}; +import { generateRandomGrid, calculateHints, calculateLineHints, validateLine } from '@/utils/puzzleUtils'; +import { PUZZLES } from '@/constants/puzzles'; export const usePuzzleStore = defineStore('puzzle', () => { // State @@ -285,7 +203,7 @@ export const usePuzzleStore = defineStore('puzzle', () => { // Check Rows for (let r = 0; r < size.value; r++) { - const targetHints = calculateHints(solutionRows[r]); + const targetHints = calculateLineHints(solutionRows[r]); const playerLine = playerGrid.value[r]; if (!validateLine(playerLine, targetHints)) { correct = false; @@ -296,7 +214,7 @@ export const usePuzzleStore = defineStore('puzzle', () => { if (correct) { // Check Columns for (let c = 0; c < size.value; c++) { - const targetHints = calculateHints(solutionCols[c]); + const targetHints = calculateLineHints(solutionCols[c]); const playerLine = playerGrid.value.map(row => row[c]); if (!validateLine(playerLine, targetHints)) { correct = false; @@ -377,12 +295,6 @@ export const usePuzzleStore = defineStore('puzzle', () => { return false; } - // Duplicate initGame removed - - // Duplicate initCustomGame removed - - // Duplicate toggleCell/setCell removed - function resetGame() { if (currentLevelId.value === 'custom') { resetGrid(); diff --git a/src/utils/audio.js b/src/utils/audio.js new file mode 100644 index 0000000..5549467 --- /dev/null +++ b/src/utils/audio.js @@ -0,0 +1,72 @@ +let audioContext = null; +let masterGain = null; + +export async function playFanfare() { + const AudioCtx = window.AudioContext || window.webkitAudioContext; + if (!AudioCtx) return; + + if (!audioContext) { + audioContext = new AudioCtx(); + } + + if (audioContext.state === 'suspended') { + try { + await audioContext.resume(); + } catch { + return; + } + } + + // Re-create gain if needed or just use it. + // In the original code, it was created every time playFanfare was called? + // Original: masterGain = audioContext.createGain(); + // It's better to create it once or manage it properly. + // Let's stick to the original logic but encapsulated. + + masterGain = audioContext.createGain(); + masterGain.gain.value = 0.25; // Slightly louder but softer tone + masterGain.connect(audioContext.destination); + + const now = audioContext.currentTime; + + 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)); +} + +export function cleanupAudio() { + if (audioContext) { + audioContext.close(); + audioContext = null; + } + masterGain = null; +} diff --git a/src/utils/puzzleUtils.js b/src/utils/puzzleUtils.js index 66e3677..5b95ce0 100644 --- a/src/utils/puzzleUtils.js +++ b/src/utils/puzzleUtils.js @@ -1,3 +1,29 @@ +export function calculateLineHints(line) { + const hints = []; + let currentRun = 0; + + for (const cell of line) { + if (cell === 1) { + currentRun++; + } else { + if (currentRun > 0) { + hints.push(currentRun); + currentRun = 0; + } + } + } + if (currentRun > 0) { + hints.push(currentRun); + } + return hints.length > 0 ? hints : [0]; +} + +export function validateLine(line, targetHints) { + const currentHints = calculateLineHints(line); + if (currentHints.length !== targetHints.length) return false; + return currentHints.every((h, i) => h === targetHints[i]); +} + export function calculateHints(grid) { if (!grid || grid.length === 0) return { rowHints: [], colHints: [] }; @@ -7,34 +33,16 @@ export function calculateHints(grid) { // Row Hints for (let r = 0; r < size; r++) { - const hints = []; - let count = 0; - for (let c = 0; c < size; c++) { - if (grid[r][c] === 1) { - count++; - } else if (count > 0) { - hints.push(count); - count = 0; - } - } - if (count > 0) hints.push(count); - rowHints.push(hints.length > 0 ? hints : [0]); + rowHints.push(calculateLineHints(grid[r])); } // Col Hints for (let c = 0; c < size; c++) { - const hints = []; - let count = 0; + const col = []; for (let r = 0; r < size; r++) { - if (grid[r][c] === 1) { - count++; - } else if (count > 0) { - hints.push(count); - count = 0; - } + col.push(grid[r][c]); } - if (count > 0) hints.push(count); - colHints.push(hints.length > 0 ? hints : [0]); + colHints.push(calculateLineHints(col)); } return { rowHints, colHints }; @@ -68,7 +76,7 @@ export function calculateDifficulty(density, size = 10) { 45: [2, 0, 0, 0, 1, 82, 100, 100, 100], 50: [2, 0, 0, 0, 1, 73, 100, 100, 100], 60: [0, 0, 0, 0, 0, 35, 100, 100, 100], - 70: [0, 0, 0, 0, 0, 16, 100, 100, 100], + 71: [0, 0, 0, 0, 0, 16, 100, 100, 100], 80: [0, 0, 0, 0, 0, 1, 100, 100, 100] }; diff --git a/src/utils/shareUtils.js b/src/utils/shareUtils.js new file mode 100644 index 0000000..1095c9c --- /dev/null +++ b/src/utils/shareUtils.js @@ -0,0 +1,284 @@ +import { calculateDifficulty } from '@/utils/puzzleUtils'; + +export function buildShareCanvas(data, t, formattedTime) { + const { grid, size, currentDensity, guideUsageCount } = data; + if (!grid || !grid.length) return null; + + const appUrl = 'https://nonograms.7u.pl/'; + 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; // New space for difficulty/guide info + const width = boardSize + padding * 2; + const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight; + const scale = window.devicePixelRatio || 1; + const canvas = document.createElement('canvas'); + canvas.width = width * scale; + canvas.height = height * scale; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + ctx.scale(scale, scale); + + const bg = ctx.createLinearGradient(0, 0, width, height); + bg.addColorStop(0, '#1b2a4a'); + bg.addColorStop(1, '#0a1324'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, width, height); + ctx.fillStyle = 'rgba(0, 0, 0, 0.35)'; + ctx.fillRect(12, 12, width - 24, height - 24); + + ctx.fillStyle = '#e8fbff'; + ctx.font = '700 26px "Segoe UI", sans-serif'; + ctx.fillText(t('app.title'), padding, padding + 10); + ctx.font = '600 16px "Segoe UI", sans-serif'; + ctx.fillText(`${t('win.time')} ${formattedTime}`, padding, padding + 34); + + // Difficulty & Density Info + const densityPercent = Math.round(currentDensity * 100); + const { level: difficultyKey } = calculateDifficulty(currentDensity, size); + 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}`); + ctx.font = '600 14px "Segoe UI", sans-serif'; + + // Right aligned difficulty info + const diffLabel = `${t('win.difficulty')} ${difficultyText} (${densityPercent}%)`; + const diffWidth = ctx.measureText(diffLabel).width; + ctx.fillStyle = diffColor; + ctx.fillText(diffLabel, width - padding - diffWidth, padding + 34); + + const gridX = padding; + const gridY = padding + headerHeight; + ctx.fillStyle = 'rgba(255, 255, 255, 0.06)'; + ctx.fillRect(gridX, gridY, boardSize, boardSize); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)'; + ctx.lineWidth = 1; + for (let i = 0; i <= size; i++) { + const x = gridX + i * cellSize; + const y = gridY + i * cellSize; + ctx.beginPath(); + ctx.moveTo(x, gridY); + ctx.lineTo(x, gridY + boardSize); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(gridX, y); + ctx.lineTo(gridX + boardSize, y); + ctx.stroke(); + } + ctx.fillStyle = '#00f2fe'; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.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]; + if (state === 1) { + const x = gridX + c * cellSize + 1; + const y = gridY + r * cellSize + 1; + ctx.fillRect(x, y, cellSize - 2, cellSize - 2); + } else if (state === 2) { + const x = gridX + c * cellSize + cellSize * 0.2; + const y = gridY + r * cellSize + cellSize * 0.2; + const d = cellSize * 0.6; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + d, y + d); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(x + d, y); + ctx.lineTo(x, y + d); + ctx.stroke(); + } + } + } + + // Guide Usage Info (Dirty Flag) + if (guideUsageCount > 0) { + ctx.fillStyle = '#ff4d4d'; + ctx.font = '600 14px "Segoe UI", sans-serif'; + + const totalCells = size * size; + const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100)); + const guideText = t('win.usedGuide', { count: guideUsageCount, percent }); + + ctx.fillText(`⚠️ ${guideText}`, padding, height - padding - footerHeight + 10); + } + + ctx.fillStyle = 'rgba(255, 255, 255, 0.75)'; + ctx.font = '500 14px "Segoe UI", sans-serif'; + ctx.fillText(appUrl, padding, height - padding + 6); + return canvas; +} + +export function buildShareSVG(data, t, formattedTime) { + const { grid, size, currentDensity, guideUsageCount } = data; + if (!grid || !grid.length) return null; + + const appUrl = 'https://nonograms.7u.pl/'; + 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(currentDensity * 100); + const { level: difficultyKey } = calculateDifficulty(currentDensity, size); + + 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} + `; + + // 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 (guideUsageCount > 0) { + const totalCells = size * size; + const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100)); + const guideText = t('win.usedGuide', { count: guideUsageCount, percent }); + svgContent += `⚠️ ${guideText}`; + } + + // URL + svgContent += `${appUrl}`; + + svgContent += ''; + return svgContent; +} + +export const canvasToBlob = (canvas) => new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), 'image/png')); + +export const createShareBlob = async (data, t, formattedTime) => { + const canvas = buildShareCanvas(data, t, formattedTime); + if (!canvas) return null; + return canvasToBlob(canvas); +}; + +export const downloadShareSVG = (data, t, formattedTime) => { + const svgString = buildShareSVG(data, t, formattedTime); + 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-${data.size}x${data.size}.svg`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +}; + +export const downloadShareImage = async (data, t, formattedTime) => { + const blob = await createShareBlob(data, t, formattedTime); + if (!blob) return; + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `nonogram-${data.size}x${data.size}.png`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +}; + +export const buildShareUrl = (target, text, url) => { + const encodedText = encodeURIComponent(text); + const encodedUrl = encodeURIComponent(url); + if (target === 'x') { + return `https://x.com/intent/tweet?text=${encodedText}&url=${encodedUrl}`; + } + if (target === 'facebook') { + return `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}"e=${encodedText}`; + } + if (target === 'whatsapp') { + return `https://wa.me/?text=${encodeURIComponent(`${text} ${url}`)}`; + } + return ''; +}; diff --git a/src/utils/timeUtils.js b/src/utils/timeUtils.js new file mode 100644 index 0000000..7775b01 --- /dev/null +++ b/src/utils/timeUtils.js @@ -0,0 +1,5 @@ +export function formatTime(seconds) { + const m = Math.floor(seconds / 60).toString().padStart(2, '0'); + const s = (seconds % 60).toString().padStart(2, '0'); + return `${m}:${s}`; +}