diff --git a/src/components/tools/QrCode.vue b/src/components/tools/QrCode.vue index 8547cfa..73b2660 100644 --- a/src/components/tools/QrCode.vue +++ b/src/components/tools/QrCode.vue @@ -17,12 +17,53 @@ const isBgTransparent = useLocalStorage('isBgTransparent', true, 'qr-code') const bgType = useLocalStorage('bgType', 'solid', 'qr-code') const bgColor1 = useLocalStorage('bgColor1', '#ffffff', 'qr-code') const bgColor2 = useLocalStorage('bgColor2', '#e2e8f0', 'qr-code') +const bgGradPos = useLocalStorage('bgGradPos', { x1: 50, y1: 50, x2: 100, y2: 100 }, 'qr-code') const fgType = useLocalStorage('fgType', 'solid', 'qr-code') const fgColor1 = useLocalStorage('fgColor1', '#000000', 'qr-code') const fgColor2 = useLocalStorage('fgColor2', '#10b981', 'qr-code') +const fgGradPos = useLocalStorage('fgGradPos', { x1: 0, y1: 0, x2: 100, y2: 100 }, 'qr-code') const svgContent = ref('') const previewRef = ref(null) +const qrFrameRef = ref(null) + +const activeHandle = ref(null) + +const startDrag = (e, handleStr) => { + e.preventDefault() + activeHandle.value = handleStr + window.addEventListener('mousemove', onDrag) + window.addEventListener('mouseup', stopDrag) + window.addEventListener('touchmove', onDrag, { passive: false }) + window.addEventListener('touchend', stopDrag) +} + +const onDrag = (e) => { + if (!activeHandle.value || !qrFrameRef.value) return + if (e.type === 'touchmove') e.preventDefault() + + const rect = qrFrameRef.value.getBoundingClientRect() + const clientX = e.touches ? e.touches[0].clientX : e.clientX + const clientY = e.touches ? e.touches[0].clientY : e.clientY + + let x = ((clientX - rect.left) / rect.width) * 100 + let y = ((clientY - rect.top) / rect.height) * 100 + x = Math.max(0, Math.min(100, x)) + y = Math.max(0, Math.min(100, y)) + + const [type, point] = activeHandle.value.split(':') + const posRef = type === 'fg' ? fgGradPos : bgGradPos + posRef.value[`x${point}`] = Math.round(x) + posRef.value[`y${point}`] = Math.round(y) +} + +const stopDrag = () => { + activeHandle.value = null + window.removeEventListener('mousemove', onDrag) + window.removeEventListener('mouseup', stopDrag) + window.removeEventListener('touchmove', onDrag) + window.removeEventListener('touchend', stopDrag) +} const { height: previewHeight } = useFillHeight(previewRef, 40) // 40px extra margin @@ -64,15 +105,17 @@ const generateQR = () => { bgType: bgType.value, bgColor1: bgColor1.value, bgColor2: bgColor2.value, + bgGradPos: { ...bgGradPos.value }, fgType: fgType.value, fgColor1: fgColor1.value, - fgColor2: fgColor2.value + fgColor2: fgColor2.value, + fgGradPos: { ...fgGradPos.value } }) } -watch([text, ecc, isBgTransparent, bgType, bgColor1, bgColor2, fgType, fgColor1, fgColor2], () => { +watch([text, ecc, isBgTransparent, bgType, bgColor1, bgColor2, bgGradPos, fgType, fgColor1, fgColor2, fgGradPos], () => { generateQR() -}) +}, { deep: true }) watch(text, (newText) => { if (newText) { @@ -234,7 +277,25 @@ const triggerDownload = (blob, filename) => {
-
+
+
+
+ + + + + + +
+
@@ -391,6 +452,73 @@ const triggerDownload = (blob, filename) => { overflow: hidden; } +.svg-wrapper { + position: relative; + width: 100%; + height: 100%; +} + +.svg-content-box { + width: 100%; + height: 100%; + display: block; +} + +.svg-content-box :deep(svg) { + width: 100%; + height: 100%; + display: block; +} + +.grad-line-svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 5; +} + +.grad-handle { + position: absolute; + width: 20px; + height: 20px; + transform: translate(-50%, -50%); + border-radius: 50%; + cursor: grab; + z-index: 10; + touch-action: none; +} + +.grad-handle:active { + cursor: grabbing; +} + +.fg-handle { + border: 2px solid var(--primary-accent); + background: white; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); +} + +.bg-handle { + border: 2px dotted #888; + background: rgba(255,255,255,0.9); + box-shadow: 0 2px 5px rgba(0,0,0,0.3); +} + +.fg-line { + stroke: var(--primary-accent); + stroke-width: 2; + stroke-dasharray: 4; +} + +.bg-line { + stroke: #888; + stroke-width: 2; + stroke-dasharray: 2; +} + .download-settings { display: flex; gap: 1.5rem; diff --git a/src/workers/qrcode.worker.js b/src/workers/qrcode.worker.js index 956bc94..3fe93b3 100644 --- a/src/workers/qrcode.worker.js +++ b/src/workers/qrcode.worker.js @@ -1,7 +1,7 @@ import QRCode from 'qrcode' self.onmessage = async (e) => { - const { id, text, ecc, isBgTransparent, bgType, bgColor1, bgColor2, fgType, fgColor1, fgColor2 } = e.data + const { id, text, ecc, isBgTransparent, bgType, bgColor1, bgColor2, bgGradPos, fgType, fgColor1, fgColor2, fgGradPos } = e.data if (!text) { self.postMessage({ id, svgContent: '' }) @@ -23,16 +23,20 @@ self.onmessage = async (e) => { if (fgType !== 'solid') { const isLinear = fgType === 'linear' + const pos = fgGradPos || { x1: 0, y1: 0, x2: 100, y2: 100 } + const r = Math.sqrt(Math.pow(pos.x2 - pos.x1, 2) + Math.pow(pos.y2 - pos.y1, 2)) defsHtml += isLinear - ? `` - : `` + ? `` + : `` } if (!isBgTransparent && bgType !== 'solid') { const isLinear = bgType === 'linear' + const pos = bgGradPos || { x1: 0, y1: 0, x2: 100, y2: 100 } + const r = Math.sqrt(Math.pow(pos.x2 - pos.x1, 2) + Math.pow(pos.y2 - pos.y1, 2)) defsHtml += isLinear - ? `` - : `` + ? `` + : `` } if (defsHtml) {