Skip to content

Instantly share code, notes, and snippets.

@XiaoPanPanKevinPan
Last active April 3, 2024 15:23
Show Gist options
  • Save XiaoPanPanKevinPan/751f221267e5c95bf303d8bb108f6eb4 to your computer and use it in GitHub Desktop.
Save XiaoPanPanKevinPan/751f221267e5c95bf303d8bb108f6eb4 to your computer and use it in GitHub Desktop.
Framing images with SVG where geometries are changed with `vue.js`. 將照片透過 SVG 錶框,其中的長寬參數以 `vue.js` 動態調整。

Image Framing

Framing images with SVG where geometries are changed with vue.js.

將照片透過 SVG 裱框,其中的長寬參數以 vue.js 動態調整。

The frame resembles an application window.

此相框形似一應用程式視窗。

You can also drag the bottom-right corner to resize the window, drag on image to move it inside the window, and scroll the wheel on the image to resize it.

你也可以拖引右下角以縮放視窗,拖引圖片以移動它,以及在圖片上滾動滑鼠滾輪以調整大小。

Files Here 檔案於此

  • readme.md is this file noting the info of this gist.
  • imageFraming.html is the main file trying to work as described above.
  • basement.svg is the simplified SVG file which later was modified to be in the imageFraming.html.
  • template.svg is the original SVG file created in Inkscape and later was saved as basement.svg with the "Optimised SVG" option.
  • example.png shows the UI
  • example_output.svg shows a result

  • readme.md 即此檔,用以說明此 gist
  • imageFraming.html 為此 gist 之集大成者,用以達成如上所述
  • basement.svg 乃簡化之 SVG 檔,已另行修改、放入 imageFraming.html
  • template.svg 是初始的 SVG 檔,透過 Inkscape 創建,並已使用「Optimised SVG」選項匯出成前述之 basement.svg
  • example.png 顯示了界面
  • example_output.svg 展示了一個成果

Notes

This was my first time using Vue.js. During the development, I've learnt the most basic part of it, and also some specifications about the SVG format. The motivation was to ease my process dealing with image cutting and decorating (I'm helping making the graduational memorial books). Though I'm not quite sure whether I'll use it or not, it is possible that some others will need it.

這是我第一次使用 Vue.js。在開發過程中,我學到它最基礎的功能,以及一些 SVG 格式的規範。完成此作品的動機,本來是我想要簡化剪裁與裝飾圖片的流程(我目前在幫忙製作畢業紀念冊)。即使我並不太確定我是否會使用到此作品,我依然將它傳到這裡,因為未來可能有人需要它。

Some SVG Problems 一些 SVG 問題

Maybe it's due to my lack of experience in Inkscape, the "title bar" part gets rendered without rounded corners in the browsers while being normal in Inkscape. (See basement.svg and template.svg) After inspecting the element, I found its rx attribute was set to 0 for unknown reason. Since the specification says it defaults to ry if specified, I just deleted it in imageFraming.html.

也許因我使用 Inkscape 的經驗不足,故「標題欄」部分在被渲染後,會失去應有的圓角——然而在 Inkscape 中完全正常。在稍微研究之下,我發現該元素的 rx 值被設為 0,而原因未知。既據標準所言,若 ry 已存,則 rx 自動代入其值,故在 imageFraming.html 中我斷然將其刪除。

Copyright Info / Credit 版權訊息 / 功歸其屬

The image used in example.png and example_output.svg is a cropped and lighten version of 하콘이 (@yewonlee1999 on Twitter)'s masterpeice. You can find it here.

用於 example.pngexample_output.svg 中的圖片,是 하콘이(推特上 @yewonlee1999)的偉大著作。欲覽之者,且往 此處

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!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/vue@3.2.37/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>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment