├── .gitignore ├── LICENSE ├── README.md ├── html ├── bezier-easing.js ├── color.js ├── document.js ├── document.less ├── icon.jpg ├── images │ ├── asuka-11.jpg │ ├── asuka-8.jpg │ └── asuka.jpg ├── index.html ├── logo-loading.svg ├── louvre.js ├── lyric.js ├── manifest.json ├── one-last-image-logo2.png ├── one-last-image-sans.svg ├── one-last-kiss.lrc ├── pencil-texture.jpg ├── ui-switch.vue.js └── ui-tabs.vue.js ├── one-last-image-logo-color.png ├── one-last-image-logo.png ├── one-last-image-logo.psd ├── pencil-texture.psd ├── simple.jpg └── 扫描图 ├── IMG_20220814_0001.png ├── IMG_20220814_0002.png ├── IMG_20220814_0003.png ├── IMG_20220814_0004.png ├── IMG_20220814_0005.png └── IMG_20220814_0006.png /.gitignore: -------------------------------------------------------------------------------- 1 | one.psd 2 | html/images/* 3 | 参考/* 4 | note.md 5 | 演示用截图/* 6 | vue.2.6.11.min.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 卜卜口 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![小明日香真可爱啊啊啊啊啊啊](simple.jpg) 2 | 3 | # 🧸「One Last Image」卢浮宫生成器 4 | 5 | One Last Image 卢浮宫生成器 是一个 将 **赛璐珞风格** **动画截图** 或 **插画**,转换成 One Last Kiss 封面风格的在线生成器 6 | 7 | ## 地址 8 | https://lab.magiconch.com/one-last-image/ 9 | 10 | 11 | ## 功能 12 | 在转换时,可以自定义 13 | - 线条处理方案 14 | - 开关 One Last Kiss 风格(仅将图片转换为线稿) 15 | - 给画面暗部排铅笔调子 16 | - 叠加类似 One Last Kiss 光碟封面水印 17 | - 初回限定盘面效果选项 18 | - 线迹轻重 19 | - 调子数量 20 | - 叠加歌词 (未完成) 21 | 22 | 点按图片可以和原图对比生成效果,也可以直接输出对比图 23 | 24 | 手机端请使用自带浏览器进行保存 25 | 26 | 27 | ## GitHub 28 | https://github.com/itorr/one-last-image 29 | 30 | ## 微博 31 | https://weibo.com/1197780522/M19X18EGP 32 | 33 | ## 使用了 34 | ITC Avant Garde Gothic Bold [#3](https://github.com/itorr/one-last-image/issues/3) -------------------------------------------------------------------------------- /html/bezier-easing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/gre/bezier-easing 3 | * BezierEasing - use bezier curve for transition easing function 4 | * by Gaëtan Renaudeau 2014 - 2015 – MIT License 5 | */ 6 | 7 | // These values are established by empiricism with tests (tradeoff: performance VS precision) 8 | var NEWTON_ITERATIONS = 4; 9 | var NEWTON_MIN_SLOPE = 0.001; 10 | var SUBDIVISION_PRECISION = 0.0000001; 11 | var SUBDIVISION_MAX_ITERATIONS = 10; 12 | 13 | var kSplineTableSize = 11; 14 | var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); 15 | 16 | var float32ArraySupported = typeof Float32Array === 'function'; 17 | 18 | function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } 19 | function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } 20 | function C (aA1) { return 3.0 * aA1; } 21 | 22 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. 23 | function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; } 24 | 25 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. 26 | function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); } 27 | 28 | function binarySubdivide (aX, aA, aB, mX1, mX2) { 29 | var currentX, currentT, i = 0; 30 | do { 31 | currentT = aA + (aB - aA) / 2.0; 32 | currentX = calcBezier(currentT, mX1, mX2) - aX; 33 | if (currentX > 0.0) { 34 | aB = currentT; 35 | } else { 36 | aA = currentT; 37 | } 38 | } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); 39 | return currentT; 40 | } 41 | 42 | function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) { 43 | for (var i = 0; i < NEWTON_ITERATIONS; ++i) { 44 | var currentSlope = getSlope(aGuessT, mX1, mX2); 45 | if (currentSlope === 0.0) { 46 | return aGuessT; 47 | } 48 | var currentX = calcBezier(aGuessT, mX1, mX2) - aX; 49 | aGuessT -= currentX / currentSlope; 50 | } 51 | return aGuessT; 52 | } 53 | 54 | function LinearEasing (x) { 55 | return x; 56 | } 57 | 58 | function bezier (mX1, mY1, mX2, mY2) { 59 | if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { 60 | throw new Error('bezier x values must be in [0, 1] range'); 61 | } 62 | 63 | if (mX1 === mY1 && mX2 === mY2) { 64 | return LinearEasing; 65 | } 66 | 67 | // Precompute samples table 68 | var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); 69 | for (var i = 0; i < kSplineTableSize; ++i) { 70 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); 71 | } 72 | 73 | function getTForX (aX) { 74 | var intervalStart = 0.0; 75 | var currentSample = 1; 76 | var lastSample = kSplineTableSize - 1; 77 | 78 | for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { 79 | intervalStart += kSampleStepSize; 80 | } 81 | --currentSample; 82 | 83 | // Interpolate to provide an initial guess for t 84 | var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); 85 | var guessForT = intervalStart + dist * kSampleStepSize; 86 | 87 | var initialSlope = getSlope(guessForT, mX1, mX2); 88 | if (initialSlope >= NEWTON_MIN_SLOPE) { 89 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2); 90 | } else if (initialSlope === 0.0) { 91 | return guessForT; 92 | } else { 93 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); 94 | } 95 | } 96 | 97 | return function BezierEasing (x) { 98 | // Because JavaScript number are imprecise, we should guarantee the extremes are right. 99 | if (x === 0 || x === 1) { 100 | return x; 101 | } 102 | return calcBezier(getTForX(x), mY1, mY2); 103 | }; 104 | }; -------------------------------------------------------------------------------- /html/color.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const 转换十进制=function(num){ 4 | return parseInt(num,16) 5 | }; 6 | const 十六进制转换十进制=function(color){ 7 | if(!color) 8 | color='EEEEEE' 9 | return color.match(/\w\w/g).map(转换十进制) 10 | }; 11 | const 转换十六进制=function(num){ 12 | num=num.toString(16); 13 | return num.length==2?num:('0'+num) 14 | }; 15 | const 十进制颜色转换十六进制=function(arr){ 16 | return arr.map(转换十六进制).join(''); 17 | }; 18 | 19 | 20 | 21 | 22 | const rgb2hsl=function(o){ 23 | var 24 | r=o[0], 25 | g=o[1], 26 | b=o[2]; 27 | 28 | r /= 255, g /= 255, b /= 255; 29 | var 30 | max = Math.max(r, g, b), 31 | min = Math.min(r, g, b); 32 | var 33 | h, 34 | s, 35 | l = (max + min) / 2; 36 | 37 | if(max == min){ 38 | h = s = 0; // achromatic 39 | }else{ 40 | var d = max - min; 41 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 42 | switch(max){ 43 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 44 | case g: h = (b - r) / d + 2; break; 45 | case b: h = (r - g) / d + 4; break; 46 | } 47 | h /= 6; 48 | //h *= 60; 49 | } 50 | 51 | // h=Math.round(h*14); 52 | // s=Math.round(s*7); 53 | // l=Math.round(l*7); 54 | 55 | 56 | return [h, s, l]; 57 | }; 58 | const hsl2rgb=function(hsl) { 59 | // const hsl = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(hslValue); 60 | var h = hsl[0];// / 360; 61 | var s = hsl[1];// / 100; 62 | var l = hsl[2];// / 100; 63 | // console.log(h,s,l); 64 | function hue2rgb(p, q, t) { 65 | if (t < 0) t += 1; 66 | if (t > 1) t -= 1; 67 | if (t < 1/6) return p + (q - p) * 6 * t; 68 | if (t < 1/2) return q; 69 | if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; 70 | return p; 71 | } 72 | var r, g, b; 73 | if (s == 0) { 74 | r = g = b = l; 75 | } else { 76 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 77 | var p = 2 * l - q; 78 | r = hue2rgb(p, q, h + 1/3); 79 | g = hue2rgb(p, q, h); 80 | b = hue2rgb(p, q, h - 1/3); 81 | } 82 | return [ 83 | Math.round(r*255), 84 | Math.round(g*255), 85 | Math.round(b*255) 86 | ] 87 | // return `rgb(${r * 255},${g * 255},${b * 255})`; 88 | }; 89 | 90 | const hax2burn=function(hax,burn){ 91 | const hsl=rgb2hsl(十六进制转换十进制(hax)) 92 | 93 | hsl[1] = hsl[1]/5 + 0.6 94 | hsl[2]= burn 95 | // console.log(hsl) 96 | return 十进制颜色转换十六进制(hsl2rgb(hsl)) 97 | }; 98 | const hax2light=function(hax,l){ 99 | const hsl=rgb2hsl(十六进制转换十进制(hax)) 100 | 101 | hsl[2]= l 102 | // console.log(hsl) 103 | return 十进制颜色转换十六进制(hsl2rgb(hsl)) 104 | }; 105 | 106 | 107 | // export { 108 | // 十六进制转换十进制, 109 | // hax2burn, 110 | // hax2light 111 | // } 112 | 113 | 114 | 115 | 116 | const rgb2yuv = (r, g, b) => { 117 | var y, u, v; 118 | 119 | y = r * .299000 + g * .587000 + b * .114000; 120 | u = r * -.168736 + g * -.331264 + b * .500000 + 128; 121 | v = r * .500000 + g * -.418688 + b * -.081312 + 128; 122 | 123 | y = Math.floor(y); 124 | u = Math.floor(u); 125 | v = Math.floor(v); 126 | 127 | return [y, u, v]; 128 | }; 129 | 130 | const yuv2rgb = (y, u, v) => { 131 | var r, g, b; 132 | 133 | r = y + 1.4075 * (v - 128); 134 | g = y - 0.3455 * (u - 128) - (0.7169 * (v - 128)); 135 | b = y + 1.7790 * (u - 128); 136 | 137 | r = Math.floor(r); 138 | g = Math.floor(g); 139 | b = Math.floor(b); 140 | 141 | r = (r < 0) ? 0 : r; 142 | r = (r > 255) ? 255 : r; 143 | 144 | g = (g < 0) ? 0 : g; 145 | g = (g > 255) ? 255 : g; 146 | 147 | b = (b < 0) ? 0 : b; 148 | b = (b > 255) ? 255 : b; 149 | 150 | return [r, g, b]; 151 | }; -------------------------------------------------------------------------------- /html/document.js: -------------------------------------------------------------------------------- 1 | 2 | const readFileToURL = (file,onOver)=>{ 3 | var reader = new FileReader(); 4 | reader.onload = ()=>{ 5 | const src = reader.result; 6 | onOver(src); 7 | }; 8 | reader.readAsDataURL(file); 9 | }; 10 | 11 | const readFileAndSetIMGSrc = file=>{ 12 | readFileToURL(file,src=>{ 13 | app.src = src; 14 | }); 15 | }; 16 | 17 | const isImageRegex = /^image\/(jpeg|gif|png|bmp|webp)$/; 18 | 19 | document.addEventListener('paste',e=>{ 20 | // console.log(e.clipboardData,e.clipboardData.files); 21 | 22 | const clipboardData = e.clipboardData; 23 | if(clipboardData.items[0]){ 24 | let file = clipboardData.items[0].getAsFile(); 25 | 26 | if(file && isImageRegex.test(file.type)){ 27 | return readFileAndSetIMGSrc(file); 28 | } 29 | } 30 | 31 | if(clipboardData.files.length){ 32 | for(let i = 0;i{ 42 | e.preventDefault(); 43 | }); 44 | document.addEventListener('drop',e=>{ 45 | e.preventDefault(); 46 | 47 | const file = e.dataTransfer.files[0]; 48 | 49 | if(file && file.type.match(isImageRegex)){ 50 | readFileAndSetIMGSrc(file); 51 | } 52 | }); 53 | 54 | const _louvre = (img,style,callback)=>{ 55 | 56 | clearTimeout(louvre.T); 57 | louvre.T = setTimeout(()=>{ 58 | louvre(img,style,callback); 59 | app.saveData(); 60 | },100); 61 | }; 62 | 63 | const deepCopy = o=>JSON.parse(JSON.stringify(o)); 64 | 65 | 66 | 67 | 68 | 69 | const creatConvoluteCenterHigh = (w,centerV)=>{ 70 | const arr = []; 71 | const c = Math.floor((w*w)/2); 72 | 73 | for(let x = 0; x < w; x++){ 74 | for(let y = 0; y < w; y++){ 75 | let i = x * w + y; 76 | arr[i] = -1; 77 | 78 | if(i===c){ 79 | arr[i] = centerV; 80 | } 81 | } 82 | } 83 | return arr; 84 | } 85 | const creatConvoluteAverage = (w)=>new Array(w*w).fill(1/(w*w)) 86 | 87 | 88 | const Convolutes = { 89 | // '右倾': [ 90 | // 0, -1, 0, 91 | // -1, 2, 2, 92 | // 0, -1, 0 93 | // ], 94 | // '左倾': [ 95 | // 0, -1, 0, 96 | // 3, 2, -2, 97 | // 0, -1, 0 98 | // ], 99 | // '极细': creatConvoluteAverage(3), 100 | '精细': creatConvoluteAverage(5), 101 | '一般': creatConvoluteAverage(7), 102 | '稍粗': creatConvoluteAverage(9), 103 | '超粗': creatConvoluteAverage(11), 104 | '极粗': creatConvoluteAverage(13), 105 | // '12421': [ 106 | // -3,2,-3, 107 | // 2,4, 2, 108 | // -3,2,-3, 109 | // ], 110 | // '9,-1,8': [ 111 | // -1 ,-1 ,-1 , 112 | // -1 , 9 ,-1 , 113 | // -1 ,-1 ,-1 , 114 | // ], 115 | // '25,-1,24':creatConvoluteCenterHigh(5,24), 116 | // '25,-1,25': creatConvoluteCenterHigh(5,25), 117 | // '25,-1,26': [ 118 | // -1 , -1 , -1 , -1 , -1 , 119 | // -1 , -1 , -1 , -1 , -1 , 120 | // -1 , -1 , 26 , -1 , -1 , 121 | // -1 , -1 , -1 , -1 , -1 , 122 | // -1 , -1 , -1 , -1 , -1 , 123 | // ], 124 | // '-1,0,16': [ 125 | // -1 , -1 , -1 , -1 , -1 , 126 | // -1 , 0 , 0 , 0 , -1 , 127 | // -1 , 0 , 17 , 0 , -1 , 128 | // -1 , 0 , 0 , 0 , -1 , 129 | // -1 , -1 , -1 , -1 , -1 , 130 | // ], 131 | '浮雕': [ 132 | 1, 1, 1, 133 | 1, 1, -1, 134 | -1, -1, -1 135 | ], 136 | '线稿': null, 137 | } 138 | 139 | const style = { 140 | zoom:1, 141 | light:0, 142 | shadeLimit: 108, 143 | shadeLight: 80, 144 | // s:80, 145 | // l:50, 146 | shade: true, 147 | kuma: true, 148 | hajimei: false, 149 | watermark: true, 150 | convoluteName: '一般', 151 | convolute1Diff: true, 152 | convoluteName2: null, 153 | Convolutes, 154 | // contrast: 30, 155 | // invertLight: false, 156 | // hue:false, 157 | // hueGroup: 255, 158 | // lightGroup: 1, 159 | lightCut: 128, 160 | darkCut: 118, 161 | denoise: true, 162 | }; 163 | 164 | 165 | const convolutes = Object.keys(Convolutes); 166 | 167 | 168 | const defaultImageURL = 'images/asuka-8.jpg'; 169 | 170 | 171 | const maxPreviewWidth = Math.min(800,document.body.offsetWidth); 172 | let previewWidth = maxPreviewWidth; 173 | let previewHeight = Math.round(previewWidth * 0.593); 174 | 175 | const data = { 176 | src: defaultImageURL, 177 | defaultImageURL, 178 | style, 179 | runing: true, 180 | convolutes, 181 | diff: false, 182 | output: '', 183 | downloadFilename: '[One-Last-Image].jpg', 184 | previewWidth, 185 | previewHeight, 186 | lyrics: null, 187 | loading: true, 188 | lyricIndex: 0, 189 | 190 | bevelPosition:20, 191 | }; 192 | 193 | 194 | const chooseFileForm = document.createElement('form'); 195 | const chooseFileInput = document.createElement('input'); 196 | chooseFileInput.type = 'file'; 197 | chooseFileInput.accept = 'image/*'; 198 | chooseFileForm.appendChild(chooseFileInput); 199 | 200 | const chooseFile = callback=>{ 201 | chooseFileForm.reset(); 202 | chooseFileInput.onchange = function(){ 203 | if(!this.files||!this.files[0])return; 204 | callback(this.files[0]); 205 | }; 206 | chooseFileInput.click(); 207 | }; 208 | 209 | 210 | const init= _=>{ 211 | app.loading = false; 212 | louvreInit( _=>{ 213 | const { img } = app.$refs; 214 | img.onload = app.setImageAndDraw; 215 | if(img.complete) img.onload(); 216 | }); 217 | } 218 | 219 | 220 | app = new Vue({ 221 | el:'.app', 222 | data, 223 | methods: { 224 | init, 225 | _louvre(ms=300){ 226 | app.runing = true; 227 | clearTimeout(app.T) 228 | app.T = setTimeout(app.louvre,ms) 229 | }, 230 | async louvre(){ 231 | app.runing = true; 232 | this.$nextTick(async _=>{ 233 | await louvre({ 234 | img: app.$refs['img'], 235 | outputCanvas: app.$refs['canvas'], 236 | config: { 237 | ...app.style, 238 | Convolutes, 239 | } 240 | }); 241 | app.runing = false; 242 | }) 243 | }, 244 | async setImageAndDraw(){ 245 | const { img } = app.$refs; 246 | const { naturalWidth, naturalHeight } = img; 247 | 248 | const previewWidth = Math.min(maxPreviewWidth, naturalWidth); 249 | const previewHeight = Math.floor(previewWidth / naturalWidth * naturalHeight); 250 | 251 | app.previewWidth = previewWidth; 252 | app.previewHeight = previewHeight; 253 | await app.louvre(); 254 | }, 255 | chooseFile(){ 256 | chooseFile(readFileAndSetIMGSrc) 257 | }, 258 | save(){ 259 | const { canvas } = app.$refs; 260 | // URL.createObjectURL() 261 | app.output = canvas.toDataURL('image/jpeg',.9); 262 | app.downloadFilename = `[lab.magiconch.com][One-Last-Image]-${+Date.now()}.jpg`; 263 | }, 264 | saveDiff(){ 265 | const { img,canvas } = app.$refs; 266 | const mixCanvas = document.createElement('canvas'); 267 | const mixCanvasCtx = mixCanvas.getContext('2d'); 268 | mixCanvas.width = canvas.width; 269 | mixCanvas.height = canvas.height * 2; 270 | mixCanvasCtx.drawImage( 271 | canvas, 272 | 0,0, 273 | canvas.width,canvas.height 274 | ); 275 | mixCanvasCtx.drawImage( 276 | img, 277 | 0,0, 278 | img.naturalWidth,img.naturalHeight, 279 | 0,canvas.height, 280 | canvas.width,canvas.height, 281 | ); 282 | app.output = mixCanvas.toDataURL('image/jpeg',.9); 283 | app.downloadFilename = `[lab.magiconch.com][One-Last-Image]-diff-${+Date.now()}.jpg`; 284 | 285 | }, 286 | saveDiff2(){ 287 | const { img,canvas } = app.$refs; 288 | const mixCanvas = document.createElement('canvas'); 289 | const mixCanvasCtx = mixCanvas.getContext('2d'); 290 | mixCanvas.width = canvas.width; 291 | mixCanvas.height = canvas.height; 292 | mixCanvasCtx.drawImage( 293 | canvas, 294 | 0,0, 295 | canvas.width,canvas.height 296 | ); 297 | 298 | const { bevelPosition } = app; 299 | 300 | const topXScale = bevelPosition/100 + 0.24; 301 | const bottomXScale = bevelPosition/100 + 0.04; 302 | 303 | const topX = Math.floor(canvas.width * topXScale); 304 | const bottomX = Math.floor(canvas.width * bottomXScale); 305 | 306 | mixCanvasCtx.beginPath(); 307 | mixCanvasCtx.moveTo(0,0); 308 | mixCanvasCtx.lineTo(topX,0); 309 | mixCanvasCtx.lineTo(bottomX,canvas.height); 310 | mixCanvasCtx.lineTo(0,canvas.height); 311 | mixCanvasCtx.closePath(); 312 | 313 | const pattern = mixCanvasCtx.createPattern(img, 'no-repeat'); 314 | mixCanvasCtx.fillStyle = pattern; 315 | mixCanvasCtx.fill(); 316 | app.output = mixCanvas.toDataURL('image/jpeg',.9); 317 | app.downloadFilename = `[lab.magiconch.com][One-Last-Image]-diff2-${+Date.now()}.jpg`; 318 | 319 | }, 320 | _saveDiff2(ms = 100){ 321 | const { saveDiff2 } = app; 322 | 323 | clearTimeout(saveDiff2.timer); 324 | saveDiff2.timer = setTimeout(saveDiff2,ms); 325 | }, 326 | toDiff(){ 327 | this.diff = true; 328 | 329 | document.activeElement = null; 330 | } 331 | }, 332 | computed: { 333 | sizeStyle(){ 334 | return { 335 | width: `${this.previewWidth}px`, 336 | height: `${this.previewHeight}px`, 337 | } 338 | }, 339 | isDefaultImageURL(){ 340 | return this.src !== this.defaultImageURL 341 | } 342 | }, 343 | watch:{ 344 | style:{ 345 | deep:true, 346 | handler(){ 347 | this._louvre(); 348 | } 349 | }, 350 | loading(v){ 351 | document.documentElement.setAttribute('data-loading',v); 352 | }, 353 | output(v){ 354 | document.documentElement.setAttribute('data-output',!!v); 355 | }, 356 | } 357 | }); 358 | 359 | setTimeout(_=>{ 360 | 361 | fetch('one-last-kiss.lrc').then(r=>r.text()).then(r=>{ 362 | const lyrics = lyricParse(r); 363 | app.lyrics = lyrics; 364 | const lastLyric = lyrics[lyrics.length-1]; 365 | const duration = lastLyric[0]; 366 | 367 | 368 | const getCurrentTime = _=>{ 369 | const now = +new Date()/1000; 370 | const currentTime = now % duration; 371 | 372 | return currentTime; 373 | }; 374 | const getCurrentIndex = _=>{ 375 | const currentTime = getCurrentTime(); 376 | 377 | 378 | for(let i = lyrics.length - 1;i >= 0 ;i--){ 379 | let lyric = lyrics[i] 380 | if(lyric[0] < currentTime){ 381 | return i; 382 | } 383 | } 384 | return 0; 385 | }; 386 | const getCurrentLyric = _=>{ 387 | return lyrics[getCurrentIndex()] 388 | } 389 | setInterval(_=>{ 390 | const index = getCurrentIndex(); 391 | // const lyric = getCurrentLyric(); 392 | app.lyricIndex = index; 393 | 394 | },500); 395 | }) 396 | },2000); -------------------------------------------------------------------------------- /html/document.less: -------------------------------------------------------------------------------- 1 | :root{ 2 | --background-color: #c3c3c3; 3 | --background-color-transparent-half: rgba(195,195,195,.5); 4 | --background-color-transparent: rgba(195,195,195,0); 5 | --color-font: #111; 6 | } 7 | html{ 8 | background:var(--background-color); 9 | text-align: center; 10 | font: 14px sans-serif; 11 | &[data-loading="true"]{ 12 | overflow: hidden; 13 | // background:#FFF; 14 | } 15 | &[data-output="true"]{ 16 | overflow: hidden; 17 | } 18 | } 19 | body{ 20 | margin: 0; 21 | } 22 | button{ 23 | cursor: pointer; 24 | } 25 | a{ 26 | color: #666; 27 | text-decoration: none; 28 | cursor: pointer; 29 | } 30 | hr{ 31 | border:0; 32 | border-top:1px solid rgba(0,0,0,.08); 33 | margin:10px auto; 34 | max-width: 100px; 35 | display: block; 36 | } 37 | header{ 38 | padding: 40px 0 30px; 39 | p{ 40 | opacity: 0.5; 41 | } 42 | } 43 | .loading-box{ 44 | position: fixed; 45 | 46 | top:0; 47 | right:0; 48 | left:0; 49 | bottom:0; 50 | z-index:1; 51 | background:#FFF; 52 | h2{ 53 | line-height: 100px; 54 | width: 100%; 55 | } 56 | animation: loadingBoxopacityIn 4s ease; 57 | svg{ 58 | width: 320px; 59 | height: 40px; 60 | margin:auto; 61 | position: absolute; 62 | 63 | top:0; 64 | right:0; 65 | left:0; 66 | bottom:0; 67 | opacity: 0; 68 | animation: opacityIn 3s ease 1s; 69 | } 70 | } 71 | // .app[data-loading="true"]{ 72 | // .loading-box{ 73 | // display: block; 74 | // } 75 | // } 76 | @keyframes loadingBoxopacityIn { 77 | 0%, 78 | 90%{ 79 | opacity: 1; 80 | } 81 | 100%{ 82 | opacity: 0; 83 | } 84 | } 85 | @keyframes opacityIn { 86 | 0%, 87 | 80%, 88 | 100% { 89 | opacity: 0; 90 | } 91 | 40%,60%{ 92 | opacity: 1; 93 | } 94 | } 95 | h1{ 96 | margin:0 auto; 97 | 98 | // width: 200px; 99 | // height: 34px; 100 | // font-size:26px; 101 | // line-height: 34px; 102 | 103 | 104 | width: 300px; 105 | height: 52px; 106 | font-size:39px; 107 | line-height: 52px; 108 | 109 | word-spacing: -1px; 110 | white-space: nowrap; 111 | overflow: hidden; 112 | color:transparent; 113 | 114 | background: url(one-last-image-sans.svg) no-repeat; 115 | background-size: contain; 116 | } 117 | @media (min-width:800px){ 118 | h1{ 119 | width: 400px; 120 | height: 68px; 121 | font-size:52px; 122 | line-height: 68px; 123 | } 124 | } 125 | 126 | h2{ 127 | margin:0; 128 | } 129 | 130 | canvas{ 131 | // background:#FFF; 132 | } 133 | .app{ 134 | max-width:800px; 135 | margin:0 auto; 136 | // &[data-runing="true"]{ 137 | // background: red; 138 | // } 139 | } 140 | [v-clock]{ 141 | visibility: hidden; 142 | } 143 | .main-box{ 144 | .ctrl-box{ 145 | padding:40px 0; 146 | } 147 | } 148 | .preview-box{ 149 | position: relative; 150 | cursor: pointer; 151 | img,canvas{ 152 | display: block; 153 | max-width:100%; 154 | margin:0 auto; 155 | } 156 | img{ 157 | position: absolute; 158 | left: 0; 159 | right: 0; 160 | opacity: 0; 161 | pointer-events: none; 162 | transition: opacity .1s ease; 163 | } 164 | canvas{ 165 | 166 | } 167 | &[data-diff]{ 168 | img{ 169 | opacity: 1; 170 | } 171 | } 172 | &[data-runing]{ 173 | img{ 174 | opacity: 1; 175 | } 176 | } 177 | --cover-width: 480px; 178 | &[data-cover="true"]{ 179 | width:var(--cover-width); 180 | height:var(--cover-width); 181 | margin:0 auto; 182 | img,canvas{ 183 | width:var(--cover-width); 184 | height:var(--cover-width); 185 | object-fit: cover; 186 | } 187 | } 188 | @media (max-width:480px) { 189 | --cover-width: 100vw; 190 | } 191 | } 192 | 193 | .generator-btn{ 194 | &:before{ 195 | content: '生成'; 196 | } 197 | } 198 | .app[data-runing="true"]{ 199 | .generator-btn{ 200 | pointer-events: none; 201 | &:before{ 202 | content: '生成中...'; 203 | } 204 | } 205 | } 206 | .config-box{ 207 | padding: 40px 0; 208 | // font-family: monospace; 209 | } 210 | .tips-box{ 211 | padding:14px; 212 | // font-weight:bold; 213 | // color:#666; 214 | p{ 215 | margin:0; 216 | padding:4px 0; 217 | } 218 | } 219 | .range-box{ 220 | padding: 4px 0; 221 | width:320px; 222 | max-width:100%; 223 | margin:0 auto; 224 | font-size:12px; 225 | .head{ 226 | overflow: hidden; 227 | b{float: left;} 228 | span{float: right;} 229 | } 230 | } 231 | input[type="range"]{ 232 | display: block; 233 | margin:0 auto; 234 | width:100%; 235 | } 236 | /* .app[data-cover="true"] canvas{ 237 | width:480px; 238 | height:480px; 239 | } */ 240 | h1{ 241 | 242 | } 243 | .ui-shadow{ 244 | position: fixed; 245 | top:0; 246 | right:0; 247 | left:0; 248 | bottom:0; 249 | z-index:1; 250 | background:rgba(0,0,0,.5); 251 | overflow: auto; 252 | overflow-y: scroll; 253 | overscroll-behavior: contain; 254 | } 255 | .output-box{ 256 | background:#FFF; 257 | width: 800px; 258 | max-width:100%; 259 | margin:40px auto; 260 | padding:20px; 261 | box-sizing: border-box; 262 | box-shadow: 0 0 40px rgba(0,10,40,.4); 263 | h2{ 264 | padding-bottom:10px; 265 | } 266 | img{ 267 | display: block; 268 | width:800px; 269 | max-width:100%; 270 | box-shadow: 0 0 20px rgba(40,80,200,.2); 271 | margin:10px 0 40px; 272 | } 273 | .ctrl-box{ 274 | padding:10px 0; 275 | } 276 | } 277 | 278 | footer{ 279 | padding:40px 0 80px; 280 | line-height: 3; 281 | a{ 282 | display: inline-block; 283 | padding:0 .5em; 284 | } 285 | } 286 | 287 | 288 | 289 | [data-text]{ 290 | &:before{ 291 | content:attr(data-text); 292 | } 293 | } 294 | 295 | .ui-tabs-box{ 296 | display: inline-block; 297 | // margin:10px 0; 298 | overflow: hidden; 299 | text-align: center; 300 | 301 | a{ 302 | color: currentColor; 303 | float: left; 304 | cursor: pointer; 305 | line-height: 1; 306 | padding:6px; 307 | border-radius:3px; 308 | transition: color .3s ease, background-color .3s ease; 309 | &[data-checked="true"]{ 310 | background: currentColor; 311 | &:before{ 312 | color: #FFF; 313 | } 314 | } 315 | } 316 | } 317 | 318 | .ui-switch-box { 319 | cursor: pointer; 320 | margin: .2em; 321 | 322 | display: inline-block; 323 | vertical-align: middle; 324 | 325 | text-align: left; 326 | .switch { 327 | display: inline-block; 328 | vertical-align: middle; 329 | 330 | margin-top: -.3em; 331 | width: 2.4em; 332 | height: 1.4em; 333 | border-radius: 9em; 334 | background: rgba(0,0,0,.2); 335 | background: #999; 336 | transition: background-color .3s ease; 337 | .slider { 338 | display: inline-block; 339 | vertical-align: top; 340 | width: 1em; 341 | height: 1em; 342 | border-radius: 9em; 343 | background: #FFF; 344 | margin: .2em; 345 | position: relative; 346 | transition: transform .3s ease; 347 | } 348 | } 349 | &[data-checked]{ 350 | .switch { 351 | background: #000; 352 | .slider { 353 | transform: translateX(1em) 354 | } 355 | } 356 | } 357 | transition: opacity .3s ease;; 358 | &[data-disabled="true"]{ 359 | opacity: 0.5; 360 | pointer-events: none; 361 | } 362 | } 363 | 364 | .btn{ 365 | display: inline-block; 366 | border:0; 367 | margin:4px; 368 | font-size:15px; 369 | line-height: 1.4; 370 | padding: 10px 16px; 371 | border-radius: 3px; 372 | background:#666; 373 | color:#DDD; 374 | transition: background-color .3s ease,color .3s ease; 375 | &.current{ 376 | background: #000; 377 | color:#EEE; 378 | } 379 | &[disabled]{ 380 | background:#999; 381 | pointer-events: none; 382 | } 383 | } 384 | 385 | 386 | 387 | .lyric-box{ 388 | --lyric-padding: 100px; 389 | height: 48px; 390 | overflow: hidden; 391 | margin: 20px 0 140px; 392 | padding: var(--lyric-padding) 0; 393 | position: relative; 394 | z-index:0; 395 | &:before, 396 | &:after{ 397 | content: ''; 398 | display: block; 399 | height:var(--lyric-padding); 400 | position: absolute; 401 | left:0; 402 | right:0; 403 | z-index:1; 404 | } 405 | &:before{ 406 | top:0; 407 | background-image: linear-gradient( 408 | 180deg, 409 | var(--background-color), 410 | var(--background-color-transparent-half), 411 | var(--background-color-transparent) 412 | ); 413 | } 414 | &:after{ 415 | bottom:0; 416 | background-image: linear-gradient( 417 | 0deg, 418 | var(--background-color), 419 | var(--background-color-transparent-half), 420 | var(--background-color-transparent) 421 | ); 422 | } 423 | .list{ 424 | transition: transform .3s ease; 425 | } 426 | .item{ 427 | box-sizing: border-box; 428 | height:48px; 429 | line-height: 20px; 430 | padding:14px 0; 431 | &[data-have-cn="true"]{ 432 | padding:4px 0; 433 | } 434 | .cn{ 435 | opacity: 0.4; 436 | font-size:12px; 437 | } 438 | 439 | transition: opacity .3s ease; 440 | opacity: 0.3; 441 | 442 | &[data-current="true"]{ 443 | opacity: 1; 444 | } 445 | } 446 | } -------------------------------------------------------------------------------- /html/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/html/icon.jpg -------------------------------------------------------------------------------- /html/images/asuka-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/html/images/asuka-11.jpg -------------------------------------------------------------------------------- /html/images/asuka-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/html/images/asuka-8.jpg -------------------------------------------------------------------------------- /html/images/asuka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/html/images/asuka.jpg -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | One Last Image - 卢浮宫生成器 - One Last Kiss 风格 封面生成 图片转线稿 - 神奇海螺实验室 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

19 | One Last Image 20 |

21 | 22 | 23 |
24 |
25 |
35 | 39 | 41 |
42 |
43 | 46 | 54 | 55 |
56 | 62 | 75 | 76 | 77 | 82 |
83 | 84 | 降噪 85 | Kiss 86 | 87 | 水印 88 | 初回 89 |
90 | 108 | 109 | 115 | 116 | 125 | 126 | 131 | 132 | 138 |
139 |
140 | 线迹轻重 141 | 142 |
143 | 145 |
146 |
147 |
148 | 调子数量 149 | 150 |
151 | 153 |
154 | 162 | 172 |
173 |
174 |

175 | 建议上传 176 | 赛璐珞风格 的 177 | 动画截图插画 178 | 等,效果最佳 179 |

180 |

181 | 高清图请务必 182 | 关闭降噪 183 | 线条更精致 184 |

185 |

186 | 也可以 187 | 生成对比图 188 | 方便分享 189 |

190 | 195 |
196 |
197 |
198 |
199 | 204 | 205 |
206 |
207 |
208 |

生成好啦

209 | 210 | 213 |

手机端保存失败时可尝试长按图片 “添加到照片”

214 |

如果能在发布生成图时,标注当前项目信息会很开心🤒

215 |
216 | 217 |
218 |
219 | 不对比、 220 | 上下对比图、 221 | 斜切对比图 222 |
223 |
224 |
225 | 斜切位置 226 | 227 |
228 | 230 |
231 |
232 |
233 |
234 | 245 | 246 |
247 |
250 |
256 |
257 | 262 |
263 |
264 |
265 |
266 |
267 |
268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /html/logo-loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/louvre.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author itorr 3 | * @date 2022-06-01 4 | * @Description One Last Image 5 | * */ 6 | 7 | 8 | 9 | const randRange = (a, b) => Math.floor(Math.random() * (b - a) + a); 10 | 11 | const inputImageEl = document.querySelector('#input'); 12 | 13 | 14 | 15 | let width = 640; 16 | let height = 480; 17 | let scale = width / height; 18 | 19 | 20 | 21 | let lastConfigString = null; 22 | 23 | const canvas = document.createElement('canvas'); 24 | const ctx = canvas.getContext('2d'); 25 | const canvasShade = document.createElement('canvas'); 26 | const canvasShadeMin = document.createElement('canvas'); 27 | const canvasMin = document.createElement('canvas'); 28 | const pencilTextureCanvas = document.createElement('canvas'); 29 | 30 | const louvre = async ({img, outputCanvas, config, callback}) => { 31 | if (!img || !config) return; 32 | 33 | const configString = [ 34 | JSON.stringify(config), 35 | img.src, 36 | ].join('-'); 37 | 38 | if (lastConfigString === configString) return; 39 | 40 | console.time('louvre'); 41 | 42 | lastConfigString = configString; 43 | 44 | const oriWidth = img.naturalWidth; 45 | const oriHeight = img.naturalHeight; 46 | 47 | let oriScale = oriWidth / oriHeight; 48 | 49 | 50 | 51 | // const _width = Math.floor( width / config.zoom ); 52 | // const _height = Math.floor( height / config.zoom ); 53 | 54 | let _width = Math.round( oriWidth / config.zoom ); 55 | let _height = Math.round( oriHeight / config.zoom ); 56 | 57 | const maxWidth = 1920; 58 | if(_width > maxWidth){ 59 | _height = _height * maxWidth / _width 60 | _width = maxWidth 61 | } 62 | // const _width = 800; 63 | // const _height = 800; 64 | 65 | 66 | let cutLeft = 0; 67 | let cutTop = 0; 68 | 69 | let calcWidth = oriWidth; 70 | let calcHeight = oriHeight; 71 | 72 | if(config.cover){ 73 | 74 | if(oriScale > 1){ 75 | cutLeft = (oriScale - 1) * oriHeight / 2; 76 | cutLeft = Math.round(cutLeft); 77 | calcWidth = oriHeight; 78 | _width = _height; 79 | }else{ 80 | cutTop = (1 - oriScale) * oriHeight / 2; 81 | cutTop = Math.round(cutTop); 82 | calcHeight = oriWidth; 83 | _height = _width; 84 | } 85 | } 86 | 87 | 88 | let setLeft = 0; 89 | let setTop = 0; 90 | 91 | let setWidth = _width; 92 | let setHeight = _height; 93 | 94 | 95 | canvas.width = _width; 96 | canvas.height = _height; 97 | 98 | 99 | 100 | ctx.drawImage( 101 | img, 102 | cutLeft, cutTop, 103 | calcWidth, calcHeight, 104 | 105 | setLeft, setTop, 106 | setWidth, setHeight 107 | ); 108 | // ctx.font = '200px sans-serif' 109 | // ctx.fillText('123233',50,200); 110 | 111 | let pixel = ctx.getImageData(0, 0, _width, _height); 112 | 113 | 114 | 115 | let pixelData = pixel.data; 116 | 117 | // 测试图像数据读取正常与否 118 | // alert(pixel.data.slice(0,10); 119 | 120 | 121 | for (let i = 0; i < pixelData.length; i += 4) { 122 | // let yuv = rgb2yuv( 123 | // pixelData[i], 124 | // pixelData[i + 1], 125 | // pixelData[i + 2], 126 | // ); 127 | const r = pixelData[i]; 128 | const g = pixelData[i + 1]; 129 | const b = pixelData[i + 2]; 130 | 131 | let y = r * .299000 + g * .587000 + b * .114000; 132 | y = Math.floor(y); 133 | 134 | // if(i%10000) console.log(y); 135 | 136 | pixelData[i ] = y; 137 | pixelData[i + 1] = y; 138 | pixelData[i + 2] = y; 139 | } 140 | let shadePixel; 141 | 142 | const { 143 | shadeLimit = 80, 144 | shadeLight = 40 145 | } = config; 146 | let pencilTexturePixel; 147 | if(config.shade){ 148 | 149 | // 载入纹理 150 | pencilTextureCanvas.width = _width; 151 | pencilTextureCanvas.height = _height; 152 | const pencilTextureCtx = pencilTextureCanvas.getContext('2d'); 153 | const pencilSetWidthHeight = Math.max(_width,_height); 154 | pencilTextureCtx.drawImage( 155 | pencilTextureEl, 156 | 0,0, 157 | 1200,1200, 158 | 0,0, 159 | pencilSetWidthHeight,pencilSetWidthHeight 160 | ); 161 | pencilTexturePixel = pencilTextureCtx.getImageData(0,0,_width,_height); 162 | 163 | 164 | // 处理暗面 165 | shadePixel = ctx.createImageData(_width, _height); 166 | 167 | for (let i = 0; i < pixelData.length; i += 4) { 168 | let y = pixelData[i]; 169 | 170 | y = y > shadeLimit ? 0 : 255; //((255 - pencilTexturePixel.data[i]) + Math.random() * 40 - 20); 171 | 172 | // y = Math.max(255-y) * 0.6; 173 | 174 | shadePixel.data[i ] = y; 175 | shadePixel.data[i + 1] = 128; 176 | shadePixel.data[i + 2] = 128; 177 | shadePixel.data[i + 3] = Math.floor(Math.random() * 255)//Math.ceil( y + Math.random() * 40 - 20); 178 | } 179 | 180 | // /* 181 | // document.body.appendChild(canvasShade) 182 | 183 | const ctxShade = canvasShade.getContext('2d'); 184 | const ctxShadeMin = canvasShadeMin.getContext('2d'); 185 | 186 | canvasShade.width = _width; 187 | canvasShade.height = _height; 188 | 189 | // console.log({shadePixel}) 190 | 191 | ctxShade.putImageData(shadePixel, 0, 0); 192 | 193 | // ctxShade.fillText('123233',50,50); 194 | const shadeZoom = 4; 195 | canvasShadeMin.width = Math.floor(_width/shadeZoom); 196 | canvasShadeMin.height = Math.floor(_height/shadeZoom); 197 | 198 | ctxShadeMin.drawImage( 199 | canvasShade, 200 | 0,0, 201 | canvasShadeMin.width,canvasShadeMin.height 202 | ); 203 | 204 | ctxShade.clearRect(0,0,_width,_height) 205 | ctxShade.drawImage( 206 | canvasShadeMin, 207 | 0,0, 208 | _width,_height 209 | ); 210 | shadePixel = ctxShade.getImageData(0,0,_width,_height); 211 | 212 | for (let i = 0; i < shadePixel.data.length; i += 4) { 213 | let y = shadePixel.data[i]; 214 | 215 | y = Math.round((255-pencilTexturePixel.data[i]) / 255 * y / 255 * shadeLight); //((255 - pencilTexturePixel.data[i]) + Math.random() * 40 - 20); 216 | 217 | // y = Math.max(255-y) * 0.6; 218 | 219 | shadePixel.data[i ] = y; 220 | } 221 | 222 | } 223 | 224 | const { 225 | light = 0, 226 | } = config; 227 | if(light){ 228 | 229 | 230 | for (let i = 0; i < pixelData.length; i += 4) { 231 | let y = pixelData[i]; 232 | 233 | y = y + y * (light/100); 234 | 235 | pixelData[i ] = y; 236 | pixelData[i + 1] = y; 237 | pixelData[i + 2] = y; 238 | } 239 | 240 | // ctx.putImageData(pixel, 0, 0); 241 | // pixel = ctx.getImageData(0, 0, _width, _height); 242 | } 243 | 244 | 245 | if(config.denoise){ 246 | pixel = convoluteY( 247 | pixel, 248 | [ 249 | 1/9, 1/9, 1/9, 250 | 1/9, 1/9, 1/9, 251 | 1/9, 1/9, 1/9 252 | ], 253 | ctx 254 | ); 255 | } 256 | 257 | const convoluteMatrix = config.Convolutes[config.convoluteName]; 258 | let pixel1 = convoluteMatrix ? convoluteY( 259 | pixel, 260 | convoluteMatrix, 261 | ctx 262 | ) : pixel; 263 | 264 | // if(config.contrast){ 265 | // for (let i = 0; i < pixel1.data.length; i +=4) { 266 | // let r = (pixel1.data[i]-128) * config.contrast + 128; 267 | // pixel1.data[i ] = r; 268 | // pixel1.data[i+1] = r; 269 | // pixel1.data[i+2] = r; 270 | // pixel1.data[i+3] = 255; 271 | // } 272 | // } 273 | 274 | if(convoluteMatrix && config.convolute1Diff){ 275 | let pixel2 = config.convoluteName2 ? convoluteY( 276 | pixel, 277 | config.Convolutes[config.convoluteName2], 278 | ctx 279 | ) : pixel; 280 | 281 | // console.log(/pixel2/,config.Convolutes[config.convoluteName2],pixel2); 282 | // pixelData 283 | for (let i = 0; i < pixel2.data.length; i +=4) { 284 | let r = 128 + pixel2.data[i ] - pixel1.data[i ]; 285 | pixel2.data[i ] = r; 286 | pixel2.data[i+1] = r; 287 | pixel2.data[i+2] = r; 288 | pixel2.data[i+3] = 255; 289 | } 290 | pixel = pixel2; 291 | }else{ 292 | // 不对比 293 | pixel = pixel1; 294 | } 295 | 296 | pixelData = pixel.data; 297 | 298 | 299 | if(convoluteMatrix) 300 | if(config.lightCut || config.darkCut){ 301 | const scale = 255 / (255 - config.lightCut - config.darkCut); 302 | for (let i = 0; i < pixelData.length; i += 4) { 303 | let y = pixelData[i]; 304 | 305 | y = (y - config.darkCut) * scale; 306 | 307 | y = Math.max(0,y); 308 | 309 | pixelData[i+0 ] = y 310 | pixelData[i+1 ] = y 311 | pixelData[i+2 ] = y 312 | pixelData[i+3 ] = 255 313 | } 314 | } 315 | 316 | if(config.kuma){ 317 | 318 | const hStart = 30; 319 | const hEnd = -184; 320 | 321 | // const be = bezier(0.57, 0.01, 0.43, 0.99); 322 | // const s = config.s/100; 323 | 324 | 325 | const gradient = ctx.createLinearGradient(0,0, _width,_height); 326 | 327 | gradient.addColorStop(0, '#fbba30'); 328 | gradient.addColorStop(0.4, '#fc7235'); 329 | gradient.addColorStop(.6, '#fc354e'); 330 | gradient.addColorStop(.7, '#cf36df'); 331 | gradient.addColorStop(.8, '#37b5d9'); 332 | gradient.addColorStop(1, '#3eb6da'); 333 | 334 | ctx.fillStyle = gradient; 335 | ctx.fillRect(0, 0, _width, _height); 336 | let gradientPixel = ctx.getImageData(0, 0, _width, _height); 337 | 338 | for (let i = 0; i < pixelData.length; i += 4) { 339 | let y = pixelData[i]; 340 | let p = Math.floor(i / 4); 341 | 342 | let _h = Math.floor(p/_width); 343 | let _w = p % _width; 344 | 345 | /* 346 | 347 | // const 348 | // hScale = hScale * hScale; 349 | 350 | let hScale = (_h + _w)/(_width + _height); 351 | 352 | hScale = hScale * hScale; 353 | hScale = be(hScale); 354 | 355 | // let h = Math.floor((hStart + (hScale) * (hEnd - hStart))); 356 | let [h] = rgb2hsl([ 357 | gradientPixel.data[i + 0], 358 | gradientPixel.data[i + 1], 359 | gradientPixel.data[i + 2], 360 | ]); 361 | const l = y/255; 362 | const rgb = hsl2rgb([h, s, l * (1 - config.l/100) + (config.l/100)]); 363 | 364 | if(i%5677===0){ 365 | // console.log(h,y,l,l * (config.l/100) + (1 - config.l/100)) 366 | // console.log((_h + _w)/(_width + _height),hScale) 367 | } 368 | 369 | pixelData[i+0 ] = rgb[0]; 370 | pixelData[i+1 ] = rgb[1]; 371 | pixelData[i+2 ] = rgb[2]; 372 | pixelData[i+3 ] = 255; 373 | */ 374 | 375 | pixelData[i+0 ] = gradientPixel.data[i + 0]; 376 | pixelData[i+1 ] = gradientPixel.data[i + 1]; 377 | pixelData[i+2 ] = gradientPixel.data[i + 2]; 378 | 379 | y = 255 - y; 380 | if(config.shade){ 381 | y = Math.max( 382 | y, 383 | shadePixel.data[i] 384 | ); 385 | } 386 | pixelData[i+3 ] = y 387 | } 388 | 389 | } 390 | 391 | 392 | // for(let i = 0;i < pixelData.length;i += 4){ 393 | 394 | // let _rgb = yuv2rgb( 395 | // pixelData[i], 396 | // pixelData[i+1], 397 | // pixelData[i+2], 398 | // ); 399 | 400 | // pixelData[i ] = _rgb[0]; 401 | // pixelData[i+1 ] = _rgb[1]; 402 | // pixelData[i+2 ] = _rgb[2]; 403 | // } 404 | 405 | // blurC(); 406 | ctx.putImageData(pixel, 0, 0); 407 | 408 | const ctxMin = canvasMin.getContext('2d'); 409 | 410 | canvasMin.width = Math.floor(_width/1.4); 411 | canvasMin.height = Math.floor(_height/1.3); 412 | 413 | ctxMin.clearRect(0,0,canvasMin.width,canvasMin.height) 414 | ctxMin.drawImage( 415 | canvas, 416 | 0,0, 417 | canvasMin.width,canvasMin.height 418 | ); 419 | 420 | ctx.clearRect(0,0,_width,_height) 421 | ctx.drawImage( 422 | canvasMin, 423 | 0,0, 424 | canvasMin.width,canvasMin.height, 425 | 0,0,_width,_height 426 | ); 427 | 428 | // one-last-image-logo-color.png 429 | if(config.watermark){ 430 | // const watermarkImageEl = await loadImagePromise('one-last-image-logo2.png'); 431 | 432 | const watermarkImageWidth = watermarkImageEl.naturalWidth; 433 | const watermarkImageHeight = watermarkImageEl.naturalHeight / 2; 434 | let setWidth = _width * 0.3; 435 | let setHeight = setWidth / watermarkImageWidth * watermarkImageHeight; 436 | 437 | if( _width / _height > 1.1 ){ 438 | setHeight = _height * 0.15; 439 | setWidth = setHeight / watermarkImageHeight * watermarkImageWidth; 440 | } 441 | 442 | let cutTop = 0; 443 | 444 | if(config.hajimei){ 445 | cutTop = watermarkImageHeight; 446 | } 447 | 448 | let setLeft = _width - setWidth - setHeight * 0.2; 449 | let setTop = _height - setHeight - setHeight * 0.16; 450 | ctx.drawImage( 451 | watermarkImageEl, 452 | 0,cutTop, 453 | watermarkImageWidth,watermarkImageHeight, 454 | setLeft, setTop, 455 | setWidth, setHeight 456 | ); 457 | } 458 | 459 | const outputCtx = outputCanvas.getContext('2d'); 460 | 461 | outputCanvas.width = _width; 462 | outputCanvas.height = _height; 463 | outputCtx.fillStyle = '#FFF'; 464 | outputCtx.fillRect(0,0,_width,_height); 465 | outputCtx.drawImage( 466 | canvas, 467 | 0,0,_width,_height 468 | ); 469 | 470 | console.timeEnd('louvre'); 471 | // return canvas.toDataURL('image/png'); 472 | 473 | }; 474 | 475 | let loadImage = (url,onOver)=>{ 476 | const el = new Image(); 477 | el.onload = _=>onOver(el); 478 | el.src = url; 479 | }; 480 | let loadImagePromise = async url=>{ 481 | return new Promise(function(resolve, reject){ 482 | setTimeout(function(){ 483 | const el = new Image(); 484 | el.onload = _=>resolve(el); 485 | el.onerror = e=>reject(e); 486 | el.src = url; 487 | }, 2000); 488 | }); 489 | } 490 | 491 | let watermarkImageEl; 492 | let pencilTextureEl; 493 | const louvreInit = onOver=>{ 494 | loadImage('pencil-texture.jpg',el=>{ 495 | pencilTextureEl = el; 496 | loadImage('one-last-image-logo2.png',el=>{ 497 | watermarkImageEl = el; 498 | onOver(); 499 | }); 500 | }); 501 | }; 502 | 503 | 504 | let convolute = (pixels, weights, ctx) => { 505 | const side = Math.round(Math.sqrt(weights.length)); 506 | const halfSide = Math.floor(side / 2); 507 | 508 | const src = pixels.data; 509 | const sw = pixels.width; 510 | const sh = pixels.height; 511 | 512 | const w = sw; 513 | const h = sh; 514 | const output = ctx.createImageData(w, h); 515 | const dst = output.data; 516 | 517 | 518 | for (let y = 0; y < h; y++) { 519 | for (let x = 0; x < w; x++) { 520 | const sy = y; 521 | const sx = x; 522 | const dstOff = (y * w + x) * 4; 523 | let r = 0, g = 0, b = 0; 524 | for (let cy = 0; cy < side; cy++) { 525 | for (let cx = 0; cx < side; cx++) { 526 | const scy = Math.min(sh - 1, Math.max(0, sy + cy - halfSide)); 527 | const scx = Math.min(sw - 1, Math.max(0, sx + cx - halfSide)); 528 | const srcOff = (scy * sw + scx) * 4; 529 | const wt = weights[cy * side + cx]; 530 | r += src[srcOff] * wt; 531 | g += src[srcOff + 1] * wt; 532 | b += src[srcOff + 2] * wt; 533 | } 534 | } 535 | dst[dstOff] = r; 536 | dst[dstOff + 1] = g; 537 | dst[dstOff + 2] = b; 538 | dst[dstOff + 3] = 255; 539 | } 540 | } 541 | 542 | 543 | // for (let y=0; y { 556 | const side = Math.round( Math.sqrt( weights.length ) ); 557 | const halfSide = Math.floor(side / 2); 558 | 559 | const src = pixels.data; 560 | 561 | const w = pixels.width; 562 | const h = pixels.height; 563 | const output = ctx.createImageData(w, h); 564 | const dst = output.data; 565 | 566 | for (let sy = 0; sy < h; sy++) { 567 | for (let sx = 0; sx < w; sx++) { 568 | const dstOff = (sy * w + sx) * 4; 569 | let r = 0, g = 0, b = 0; 570 | 571 | for (let cy = 0; cy < side; cy++) { 572 | for (let cx = 0; cx < side; cx++) { 573 | 574 | const scy = Math.min(h - 1, Math.max(0, sy + cy - halfSide)); 575 | const scx = Math.min(w - 1, Math.max(0, sx + cx - halfSide)); 576 | 577 | const srcOff = (scy * w + scx) * 4; 578 | const wt = weights[cy * side + cx]; 579 | 580 | r += src[srcOff] * wt; 581 | // g += src[srcOff + 1] * wt; 582 | // b += src[srcOff + 2] * wt; 583 | } 584 | } 585 | dst[dstOff] = r; 586 | dst[dstOff + 1] = r; 587 | dst[dstOff + 2] = r; 588 | dst[dstOff + 3] = 255; 589 | } 590 | } 591 | 592 | 593 | // for (let y=0; y{ 4 | let lyrics = []; 5 | text.trim().split(/\s{0,}\n\s{0,}/g).forEach(t=>{ 6 | const match = t.match(isLyricLine); 7 | if(!match) return; 8 | const [_,mm,ss,text,cn] = match; 9 | const s = mm * 60 + Number(ss); 10 | lyrics.push([ 11 | s, 12 | text.trim(), 13 | cn?cn.trim():'' 14 | ]); 15 | }); 16 | return lyrics.sort((a,b)=>b[0] 2 | 3 | -------------------------------------------------------------------------------- /html/one-last-kiss.lrc: -------------------------------------------------------------------------------- 1 | [ti:One Last Kiss] 2 | [ar:宇多田光 (宇多田ヒカル)] 3 | [al:One Last Kiss] 4 | [by:itorr] 5 | [offset:0] 6 | [00:18.610] 7 | [00:20.542]初めてのルーブルは「第一次参观卢浮宫」 8 | [00:22.559]なんてことはなかったわ「却并不觉得震撼」 9 | [00:24.765]私だけのモナリザ「因为我早已遇见」 10 | [00:26.713]もうとっくに出会ってたから「独属于我的蒙娜丽莎」 11 | [00:29.125]初めてあなたを見た「初遇你的那天起」 12 | [00:30.973]あの日動き出した歯車「齿轮便开始转动」 13 | [00:33.327]止められない喪失の予感「却无法阻止丧失的预感」 14 | [00:37.257] 15 | [00:37.728]もういっぱいあるけど「尽管已经拥有了很多」 16 | [00:43.372]もう一つ増やしましょう「但让我们再多加一个吧」 17 | [00:47.700]Can you give me one last kiss?「可以给我最后一个吻吗?」 18 | [00:51.975]忘れたくないこと「我不愿忘却」 19 | [00:54.407] 20 | [00:54.809]Oh oh oh oh oh… 21 | [01:00.578]忘れたくないこと「我不愿忘却」 22 | [01:03.356]Oh oh oh oh oh… 23 | [01:09.181]I love you more than you'll ever know「我比你想象中更爱你」 24 | [01:12.591] 25 | [01:20.498]「写真は苦手なんだ」「“我不擅长摄影”」 26 | [01:22.381]でもそんなものはいらないわ「但我并不需要那种东西」 27 | [01:24.745]あなたが焼きついたまま「在我内心的投影仪中」 28 | [01:26.677]私の心のプロジェクター「早已将你的身影深深烙印」 29 | [01:28.996]寂しくないふりしてた「一直以来我都在假装不寂寞」 30 | [01:30.812]まあ、そんなのお互い様か「但其实 在这方面我们都一样」 31 | [01:33.243]誰かを求めることは「对某个人的渴望」 32 | [01:35.107]即ち傷つくことだった「说到底就是一种伤」 33 | [01:37.381] 34 | [01:37.796]Oh, can you give me one last kiss?「可以给我最后一个吻吗?」 35 | [01:43.364]燃えるようなキスをしよう「来个火热的拥吻吧」 36 | [01:47.611]忘れたくても「让这个吻刻骨铭心」 37 | [01:51.927]忘れられないほど「永远无法忘怀」 38 | [01:54.379] 39 | [01:54.809]Oh oh oh oh oh… 40 | [02:00.643]I love you more than you'll ever know「我比你想象中更爱你」 41 | [02:03.272]Oh oh oh oh oh… 42 | [02:09.161]I love you more than you'll ever know「我比你想象中更爱你」 43 | [02:12.394] 44 | [02:29.060]もう分かっているよ「我已经彻底明白」 45 | [02:34.761]この世の終わりでも「即使这世界终结」 46 | [02:39.113]年をとっても「即使年华老去」 47 | [02:45.707]忘れられない人「无法忘记的人」 48 | [02:47.617] 49 | [02:47.964]Oh oh oh oh oh… 50 | [02:49.953]忘れられない人「无法忘记的人」 51 | [02:58.990]Oh oh oh oh oh… 52 | [03:00.809]I love you more than you'll ever know「我比你想象中更爱你」 53 | [03:03.760]Oh oh oh oh oh… 54 | [03:09.242]忘れられない人「无法忘记的人」 55 | [03:12.367]Oh oh oh oh oh… 56 | [03:17.854]I love you more than you'll ever know「我比你想象中更爱你」 57 | [03:20.604] 58 | [03:46.026]吹いていった風の後を「追随着风的轨迹」 59 | [03:50.237]追いかけた 眩しい午後「在那耀眼的午后」 60 | [03:55.411] 61 | -------------------------------------------------------------------------------- /html/pencil-texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/html/pencil-texture.jpg -------------------------------------------------------------------------------- /html/ui-switch.vue.js: -------------------------------------------------------------------------------- 1 | Vue.component('ui-switch',{ 2 | template: ` 7 | 8 | 9 | 10 | 11 | `, 12 | props:{ 13 | value: Boolean, 14 | color: String, 15 | disabled: Boolean, 16 | }, 17 | methods:{ 18 | _switch(){ 19 | this.$emit('input',!this.value); 20 | } 21 | } 22 | }) -------------------------------------------------------------------------------- /html/ui-tabs.vue.js: -------------------------------------------------------------------------------- 1 | Vue.component('ui-tabs',{ 2 | template: ` 3 | 4 | `, 5 | props:{ 6 | value: [String,Number], 7 | options: Array 8 | }, 9 | computed: { 10 | _options(){ 11 | const {options} = this; 12 | if(options.constructor === Object){ 13 | return Object.entries(options).map(option=>({ 14 | value: option[0], 15 | text: option[1], 16 | })) 17 | } 18 | return options.map(option=>{ 19 | if(option.constructor === String){ 20 | return { 21 | value: option, 22 | text: option 23 | }; 24 | } 25 | 26 | return option 27 | }) 28 | } 29 | }, 30 | methods:{ 31 | set(v){ 32 | this.$emit('input',v); 33 | } 34 | } 35 | }) -------------------------------------------------------------------------------- /one-last-image-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/one-last-image-logo-color.png -------------------------------------------------------------------------------- /one-last-image-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/one-last-image-logo.png -------------------------------------------------------------------------------- /one-last-image-logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/one-last-image-logo.psd -------------------------------------------------------------------------------- /pencil-texture.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/pencil-texture.psd -------------------------------------------------------------------------------- /simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/simple.jpg -------------------------------------------------------------------------------- /扫描图/IMG_20220814_0001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/扫描图/IMG_20220814_0001.png -------------------------------------------------------------------------------- /扫描图/IMG_20220814_0002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/扫描图/IMG_20220814_0002.png -------------------------------------------------------------------------------- /扫描图/IMG_20220814_0003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/扫描图/IMG_20220814_0003.png -------------------------------------------------------------------------------- /扫描图/IMG_20220814_0004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/扫描图/IMG_20220814_0004.png -------------------------------------------------------------------------------- /扫描图/IMG_20220814_0005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/扫描图/IMG_20220814_0005.png -------------------------------------------------------------------------------- /扫描图/IMG_20220814_0006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itorr/one-last-image/918fccdbfaf86512b08f9964ba56a4c8d1d31b05/扫描图/IMG_20220814_0006.png --------------------------------------------------------------------------------