feat(qr): add draggable gradient handles for background and foreground styling
This commit is contained in:
@@ -17,12 +17,53 @@ const isBgTransparent = useLocalStorage('isBgTransparent', true, 'qr-code')
|
|||||||
const bgType = useLocalStorage('bgType', 'solid', 'qr-code')
|
const bgType = useLocalStorage('bgType', 'solid', 'qr-code')
|
||||||
const bgColor1 = useLocalStorage('bgColor1', '#ffffff', 'qr-code')
|
const bgColor1 = useLocalStorage('bgColor1', '#ffffff', 'qr-code')
|
||||||
const bgColor2 = useLocalStorage('bgColor2', '#e2e8f0', '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 fgType = useLocalStorage('fgType', 'solid', 'qr-code')
|
||||||
const fgColor1 = useLocalStorage('fgColor1', '#000000', 'qr-code')
|
const fgColor1 = useLocalStorage('fgColor1', '#000000', 'qr-code')
|
||||||
const fgColor2 = useLocalStorage('fgColor2', '#10b981', '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 svgContent = ref('')
|
||||||
const previewRef = ref(null)
|
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
|
const { height: previewHeight } = useFillHeight(previewRef, 40) // 40px extra margin
|
||||||
|
|
||||||
@@ -64,15 +105,17 @@ const generateQR = () => {
|
|||||||
bgType: bgType.value,
|
bgType: bgType.value,
|
||||||
bgColor1: bgColor1.value,
|
bgColor1: bgColor1.value,
|
||||||
bgColor2: bgColor2.value,
|
bgColor2: bgColor2.value,
|
||||||
|
bgGradPos: { ...bgGradPos.value },
|
||||||
fgType: fgType.value,
|
fgType: fgType.value,
|
||||||
fgColor1: fgColor1.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()
|
generateQR()
|
||||||
})
|
}, { deep: true })
|
||||||
|
|
||||||
watch(text, (newText) => {
|
watch(text, (newText) => {
|
||||||
if (newText) {
|
if (newText) {
|
||||||
@@ -234,7 +277,25 @@ const triggerDownload = (blob, filename) => {
|
|||||||
|
|
||||||
<div class="preview-section" v-if="text" ref="previewRef" :style="{ height: previewHeight }">
|
<div class="preview-section" v-if="text" ref="previewRef" :style="{ height: previewHeight }">
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<div class="qr-frame" :style="{ background: isBgTransparent ? 'white' : bgType === 'solid' ? bgColor1 : (bgType === 'linear' ? `linear-gradient(to bottom right, ${bgColor1}, ${bgColor2})` : `radial-gradient(circle, ${bgColor1}, ${bgColor2})`) }" v-html="svgContent"></div>
|
<div class="qr-frame" :style="{ background: isBgTransparent ? 'white' : bgType === 'solid' ? bgColor1 : (bgType === 'linear' ? `linear-gradient(to bottom right, ${bgColor1}, ${bgColor2})` : `radial-gradient(circle, ${bgColor1}, ${bgColor2})`) }">
|
||||||
|
<div class="svg-wrapper" ref="qrFrameRef">
|
||||||
|
<div v-html="svgContent" class="svg-content-box"></div>
|
||||||
|
|
||||||
|
<!-- Background Gradient Handles -->
|
||||||
|
<template v-if="!isBgTransparent && bgType !== 'solid'">
|
||||||
|
<svg class="grad-line-svg"><line :x1="bgGradPos.x1 + '%'" :y1="bgGradPos.y1 + '%'" :x2="bgGradPos.x2 + '%'" :y2="bgGradPos.y2 + '%'" class="bg-line" /></svg>
|
||||||
|
<div class="grad-handle bg-handle handle-1" :style="{ left: bgGradPos.x1 + '%', top: bgGradPos.y1 + '%' }" @mousedown="startDrag($event, 'bg:1')" @touchstart.prevent="startDrag($event, 'bg:1')"></div>
|
||||||
|
<div class="grad-handle bg-handle handle-2" :style="{ left: bgGradPos.x2 + '%', top: bgGradPos.y2 + '%' }" @mousedown="startDrag($event, 'bg:2')" @touchstart.prevent="startDrag($event, 'bg:2')"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Foreground Gradient Handles -->
|
||||||
|
<template v-if="fgType !== 'solid'">
|
||||||
|
<svg class="grad-line-svg"><line :x1="fgGradPos.x1 + '%'" :y1="fgGradPos.y1 + '%'" :x2="fgGradPos.x2 + '%'" :y2="fgGradPos.y2 + '%'" class="fg-line" /></svg>
|
||||||
|
<div class="grad-handle fg-handle handle-1" :style="{ left: fgGradPos.x1 + '%', top: fgGradPos.y1 + '%' }" @mousedown="startDrag($event, 'fg:1')" @touchstart.prevent="startDrag($event, 'fg:1')"></div>
|
||||||
|
<div class="grad-handle fg-handle handle-2" :style="{ left: fgGradPos.x2 + '%', top: fgGradPos.y2 + '%' }" @mousedown="startDrag($event, 'fg:2')" @touchstart.prevent="startDrag($event, 'fg:2')"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="download-settings">
|
<div class="download-settings">
|
||||||
@@ -391,6 +452,73 @@ const triggerDownload = (blob, filename) => {
|
|||||||
overflow: hidden;
|
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 {
|
.download-settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
|
|
||||||
self.onmessage = async (e) => {
|
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) {
|
if (!text) {
|
||||||
self.postMessage({ id, svgContent: '' })
|
self.postMessage({ id, svgContent: '' })
|
||||||
@@ -23,16 +23,20 @@ self.onmessage = async (e) => {
|
|||||||
|
|
||||||
if (fgType !== 'solid') {
|
if (fgType !== 'solid') {
|
||||||
const isLinear = fgType === 'linear'
|
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
|
defsHtml += isLinear
|
||||||
? `<linearGradient id="qr-fg-grad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="${fgColor1}" /><stop offset="100%" stop-color="${fgColor2}" /></linearGradient>`
|
? `<linearGradient id="qr-fg-grad" x1="${pos.x1}%" y1="${pos.y1}%" x2="${pos.x2}%" y2="${pos.y2}%"><stop offset="0%" stop-color="${fgColor1}" /><stop offset="100%" stop-color="${fgColor2}" /></linearGradient>`
|
||||||
: `<radialGradient id="qr-fg-grad" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="${fgColor1}" /><stop offset="100%" stop-color="${fgColor2}" /></radialGradient>`
|
: `<radialGradient id="qr-fg-grad" cx="${pos.x1}%" cy="${pos.y1}%" r="${r}%"><stop offset="0%" stop-color="${fgColor1}" /><stop offset="100%" stop-color="${fgColor2}" /></radialGradient>`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isBgTransparent && bgType !== 'solid') {
|
if (!isBgTransparent && bgType !== 'solid') {
|
||||||
const isLinear = bgType === 'linear'
|
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
|
defsHtml += isLinear
|
||||||
? `<linearGradient id="qr-bg-grad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="${bgColor1}" /><stop offset="100%" stop-color="${bgColor2}" /></linearGradient>`
|
? `<linearGradient id="qr-bg-grad" x1="${pos.x1}%" y1="${pos.y1}%" x2="${pos.x2}%" y2="${pos.y2}%"><stop offset="0%" stop-color="${bgColor1}" /><stop offset="100%" stop-color="${bgColor2}" /></linearGradient>`
|
||||||
: `<radialGradient id="qr-bg-grad" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="${bgColor1}" /><stop offset="100%" stop-color="${bgColor2}" /></radialGradient>`
|
: `<radialGradient id="qr-bg-grad" cx="${pos.x1}%" cy="${pos.y1}%" r="${r}%"><stop offset="0%" stop-color="${bgColor1}" /><stop offset="100%" stop-color="${bgColor2}" /></radialGradient>`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defsHtml) {
|
if (defsHtml) {
|
||||||
|
|||||||
Reference in New Issue
Block a user