|
<!DOCTYPE html> |
|
<html><body> |
|
<template id="app"><div> |
|
<aside> |
|
<!-- I can't just use a style tag --> |
|
<!-- ref: https://stackoverflow.com/questions/69746591/vue-3-warning-tags-with-side-effects-is-breaking-production --> |
|
<component :is="'style'">aside span{ |
|
display: inline-block; |
|
}</component> |
|
<span>框寬|Frame Width:<input v-model="width" type="number" /></span> |
|
<span>框高|Frame Height:<input v-model="height" type="number" /></span> |
|
<span>圖X|Image's X-Axis:<input v-model="imgX" type="number" /></span> |
|
<span>圖Y|Image's Y-Axis:<input v-model="imgY" type="number" /></span> |
|
<span>圖寬|Image's Width:<input v-model="imgWidth" type="number" /></span> |
|
<span>圖高|Image's Height:<input v-model="imgHeight" type="number" /></span> |
|
<span>傳入圖檔|Image Input:<input @change="onFile" type="file" /></span> |
|
<span>標題字|Title Text:<input v-model="titleText" /></span> |
|
<span>縮放|Zoom:<input v-model="scale" type="number" /><input v-model="scale" type="range" min="0.01" max="1" step="0.001" /></span> |
|
<button @click="download">另存 SVG|Save SVG</button> |
|
</aside> |
|
<main :style="` |
|
--scale: ${scale}; |
|
transform: |
|
scale(var(--scale)) |
|
translate( calc(-50% / var(--scale) + 50%), calc(-50% / var(--scale) + 50%)); |
|
display: grid; |
|
|
|
grid-template-columns: auto ${20 / scale}px; |
|
grid-template-rows: 290px auto ${20 / scale}px; |
|
`" |
|
> |
|
<div id="svgWrapper" style="grid-area: 1 / 1 / 4 / 3"><svg :width="width" :height="height" version="1.1" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink"> |
|
<metadata> |
|
<rdf:RDF> |
|
<cc:Work rdf:about=""> |
|
<dc:format>image/svg+xml</dc:format> |
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> |
|
<dc:title/> |
|
</cc:Work> |
|
</rdf:RDF> |
|
</metadata> |
|
<defs> |
|
<clipPath id="imageViewport"> |
|
<rect x="30" y="30" :width="width - 60" :height="height - 60" ry="145" style="fill-opacity:.502463;fill:#ffffff"/> |
|
</clipPath> |
|
</defs> |
|
<g> |
|
<rect id="windowBackground" x="5" y="5" :width="width - 10" :height="height - 10" ry="145" style="fill:#4d4d4d;paint-order:normal;stroke-width:10;stroke:#808080" /> |
|
<image id="imageInWindow" :x="imgX" :y="imgY" :width="imgWidth" :height="imgHeight" clip-path="url(#imageViewport)" style="stroke-width:.0561151" preserveAspectRatio="none" :xlink:href="imgSrc" /> |
|
<rect id="titleBar" x="5" y="5" :width="width - 10" height="290" ry="145" style="fill:#323232;stroke-width:9.99997;stroke:#999999" /> |
|
<text id="titleText" x="145" y="200" style="fill:#000000;font-family:sans-serif;font-size:150px;line-height:1;stroke-width:.999999" xml:space="preserve"><tspan x="145" y="200" dominant-baseline="text-bottom" style="fill:#ffffff;font-family:'Noto Sans JP';font-size:150px;stroke-width:.999999">{{ titleText }}</tspan></text> |
|
<g id="windowButton"> |
|
<ellipse id="windowButton_greenLeft" :cx="width - 210 - 180*2" cy="150" rx="60" ry="60" style="fill:#55d400"/> |
|
<ellipse id="windowButton_yellowMid" :cx="width - 210 - 180" cy="150" rx="60" ry="60" style="fill:#ffcc00"/> |
|
<ellipse id="windowButton_redRight" :cx="width - 210" cy="150" rx="60" ry="60" style="fill:#ff0000"/> |
|
</g> |
|
</g> |
|
</svg></div> |
|
<div id="titleBar" style="grid-area: 1 / 1 / 2 / 3" ></div> |
|
<div id="imgArea" style="grid-area: 2 / 1 / 3 / 2" |
|
@wheel="imgResize" @mousedown="imgMove"></div> |
|
<div id="widthResize" style="grid-area: 2 / 2 / 3 / 3; cursor: ew-resize;" |
|
@mousedown="resizer" :data-resizer-cfg="JSON.stringify({w: 1/scale, h: 0})"></div> |
|
<div id="heightResize" style="grid-area: 3 / 1 / 4 / 2; cursor: ns-resize;" |
|
@mousedown="resizer" :data-resizer-cfg="JSON.stringify({w: 0, h: 1/scale})"></div> |
|
<div id="complexResize" style="grid-area: 3 / 2 / 4 / 3; cursor: se-resize;" |
|
@mousedown="resizer" :data-resizer-cfg="JSON.stringify({w: 1/scale, h: 1/scale})"></div> |
|
</main> |
|
</div></template> |
|
</body><head> |
|
<?xml version="1.0" encoding="UTF-8"?> |
|
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script> |
|
<script defer> |
|
let mntPoint = document.querySelector("template#app").content.firstElementChild.cloneNode(true); |
|
document.body.appendChild(mntPoint); |
|
Vue.createApp({ |
|
data: () => ({ |
|
width: 4200, height: 2850, |
|
imgX: 30, imgY: 30, imgWidth: 0, imgHeight: 0, imgSrc: "", |
|
titleText: "範例文字.jpg", imgName: "", |
|
|
|
mntPoint: mntPoint, scale: 0.1 |
|
}), |
|
methods: { |
|
onFile(e){ |
|
let file = e.target.files[0]; |
|
|
|
try{ |
|
const reader = new FileReader(); |
|
reader.addEventListener("loadend", ()=>{ |
|
this.imgSrc = reader.result; |
|
this.imgName = file.name; |
|
|
|
/* use the real resolution of the image |
|
* ref: https://stackoverflow.com/questions/7460272/getting-image-dimensions-using-javascript-file-api |
|
*/ |
|
{ |
|
let img = new Image(); |
|
img.addEventListener("load", ()=>{ |
|
this.imgWidth = img.width; |
|
this.imgHeight = img.height; |
|
}) |
|
img.src = this.imgSrc; |
|
} |
|
}, false); |
|
|
|
reader.readAsDataURL(file); |
|
}catch(e){ |
|
console.log(e); |
|
} |
|
}, |
|
download(e){ |
|
const svgStr = `<?xml version="1.0" encoding="UTF-8"?> |
|
${this.mntPoint.querySelector("#svgWrapper").innerHTML} |
|
`; |
|
|
|
const dwLink = document.createElement("a"); |
|
document.body.appendChild(dwLink); |
|
|
|
dwLink.href = `data:text/plain;charset=utf-8,${encodeURIComponent(svgStr)}`; |
|
dwLink.download = `${this.imgName}_${this.titleText}_framed.svg`; |
|
|
|
dwLink.click(); |
|
|
|
dwLink.remove(); |
|
}, |
|
resizer(e){ //mousedown |
|
const THIS = this; |
|
|
|
/*ref: https://stackoverflow.com/questions/8960193/how-to-make-html-element-resizable-using-pure-javascript*/ |
|
/*data-resizer-cfg is used to reduce the code and enable the extendability*/ |
|
|
|
const cfg = { |
|
h: 1, |
|
w: 1, |
|
minH: 290, |
|
minW: 780, |
|
...JSON.parse(e.target.getAttribute("data-resizer-cfg")) |
|
}; |
|
|
|
const doDrag = (e) => { |
|
let newW = (THIS.width + e.movementX * cfg.w); |
|
let newH = (THIS.height + e.movementY * cfg.h); |
|
if(newW >= cfg.minW) THIS.width = newW; |
|
if(newH >= cfg.minH) THIS.height = newH; |
|
}; |
|
|
|
document.body.addEventListener("mousemove", doDrag); |
|
document.body.addEventListener( |
|
"mouseup", |
|
() => document.body.removeEventListener("mousemove", doDrag), |
|
{once: true} |
|
); |
|
}, |
|
imgResize(e){ //wheel |
|
e.preventDefault(); |
|
let imgOldWidth = this.imgWidth, |
|
imgNewWidth = this.imgWidth += e.deltaY * 0.5; |
|
this.imgHeight *= (imgNewWidth / imgOldWidth); |
|
}, |
|
imgMove(e){ //mousedown |
|
const THIS = this; |
|
|
|
const cfg = { |
|
h: 1/THIS.scale, |
|
w: 1/THIS.scale, |
|
}; |
|
|
|
const doMove = (e) => { |
|
THIS.imgX += e.movementX * cfg.w; |
|
THIS.imgY += e.movementY * cfg.h; |
|
}; |
|
|
|
document.body.addEventListener("mousemove", doMove); |
|
document.body.addEventListener( |
|
"mouseup", |
|
() => document.body.removeEventListener("mousemove", doMove), |
|
{once: true} |
|
) |
|
} |
|
} |
|
}).mount(mntPoint); |
|
</script> |
|
</head></html> |