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 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) => {
|
||||
|
||||
<div class="preview-section" v-if="text" ref="previewRef" :style="{ height: previewHeight }">
|
||||
<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 class="download-settings">
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
? `<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>`
|
||||
: `<radialGradient id="qr-fg-grad" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="${fgColor1}" /><stop offset="100%" stop-color="${fgColor2}" /></radialGradient>`
|
||||
? `<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="${pos.x1}%" cy="${pos.y1}%" r="${r}%"><stop offset="0%" stop-color="${fgColor1}" /><stop offset="100%" stop-color="${fgColor2}" /></radialGradient>`
|
||||
}
|
||||
|
||||
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
|
||||
? `<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>`
|
||||
: `<radialGradient id="qr-bg-grad" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="${bgColor1}" /><stop offset="100%" stop-color="${bgColor2}" /></radialGradient>`
|
||||
? `<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="${pos.x1}%" cy="${pos.y1}%" r="${r}%"><stop offset="0%" stop-color="${bgColor1}" /><stop offset="100%" stop-color="${bgColor2}" /></radialGradient>`
|
||||
}
|
||||
|
||||
if (defsHtml) {
|
||||
|
||||
Reference in New Issue
Block a user