Skip to content

Instantly share code, notes, and snippets.

@LoueeD
Created November 15, 2017 22:30
Show Gist options
  • Save LoueeD/8213851e6501e9e5d9637574c65c7ada to your computer and use it in GitHub Desktop.
Save LoueeD/8213851e6501e9e5d9637574c65c7ada to your computer and use it in GitHub Desktop.
Notchless Photo Generator
<div class="site-canvas" id="app">
<div class="column">
<h1>Notchless</h1>
<h2>
Simply click to add a photo and
then download a notchless version.
</h2>
<div class="download">
<span v-if="!photoName">Upload Photo</span>
<span v-if="photoName">{{photoName}}</span>
<form action="">
<input type="file" v-on:change="onFileChange">
</form>
</div>
<br>
<a class="author" target="_blank" href="https://twitter.com/imlwi">Made by Louis Dickinson</a>
</div>
<div class="column">
<div class="phone">
<div class="wallpaper" v-on:click="saveImage()">
<div class="photo" v-for="(photo, index) in photos" v-bind:style="{'background-image' : 'url(' + photo +')', 'opacity' : index === activePhoto ? 1 : 0 }"></div>
<div class="photo" v-if="href" v-bind:style="{'background-image' : 'url(' + href +')', 'background-size' : '110%', opacity: 1 }">
<img v-if="href" v-bind:src="href" alt="">
</div>
</div>
<div class="upload"></div>
</div>
</div>
<canvas id="original"></canvas>
</div>
(function(){
var app = new Vue({
el: '#app',
data: {
loaded: false,
photoName: '',
modal: false,
photos: [
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/363972/photo-1504247580158-1e411863c8f7.jpeg',
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/363972/photo-1484589065579-248aad0d8b13.jpeg',
'https://images.unsplash.com/photo-1502624210716-bf8740533f4c?auto=format&fit=crop&w=934&q=60&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D',
'https://images.unsplash.com/photo-1489321336462-efe12c02d099?auto=format&fit=crop&w=934&q=60&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D',
'https://images.unsplash.com/photo-1503864664483-e8a499e2eb22?auto=format&fit=crop&w=856&q=60&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D',
'https://images.unsplash.com/photo-1508031100502-f2f679ea78eb?auto=format&fit=crop&w=934&q=60&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D'
],
activePhoto: -1,
urlInput: '',
notch: '',
dimentions: {w: 1125,h: 2436},
canvas: undefined,
ctx: undefined,
href: ''
},
methods: {
saveImage: function(){
if(!this.href) return;
var link = document.createElement("a");
link.download = 'Notchless';
link.href = this.href;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
delete link;
},
onFileChange: function(e){
var files = e.target.files || e.dataTransfer.files;
if (!files.length){
return;
}
this.loadImage(URL.createObjectURL(files[0]));
console.log(files[0]);
this.photoName = files[0].name;
},
loadImage: function(url) {
var img = new Image();
var notchlessImg = new Image();
notchlessImg.src = this.notch;
var width = this.dimentions.w;
var height = this.dimentions.h;
img.onload = () => {
this.drawImageProp(this.ctx, img, 0,0, width, height,0,0);
this.ctx.drawImage(notchlessImg, 0, 0, width, 161);
this.downloadImage();
};
img.src = url;
},
changePlaceholderPhoto: function() {
if(!this.href){
this.activePhoto++;
this.activePhoto >= this.photos.length ? this.activePhoto = 0 : null;
setTimeout(() => { this.changePlaceholderPhoto() }, 3000);
}
},
downloadImage: function() {
var dataUri = this.canvas.toDataURL('image/png');
/* Change MIME type to trick the browser to downlaod the file instead of displaying it */
// dataUri = dataUri.replace(/^data:image\/[^;]*/, 'data:application/octet-stream');
// /* In addition to <a>'s "download" attribute, you can define HTTP-style headers */
// dataUri = dataUri.replace(/^data:application\/octet-stream/, 'data:application/octet-stream;headers=Content-Disposition%3A%20attachment%3B%20filename=Canvas.png');
this.href = dataUri;
if(!dataUri){
setTimeout(this.downloadImage(), 1000);
}
console.log('loaded');
},
drawImageProp: function(ctx, img, x, y, w, h, offsetX, offsetY) {
if (arguments.length === 2) {
x = y = 0;
w = ctx.canvas.width;
h = ctx.canvas.height;
}
// default offset is center
offsetX = offsetX ? offsetX : 0.5;
offsetY = offsetY ? offsetY : 0.5;
// keep bounds [0.0, 1.0]
if (offsetX < 0) offsetX = 0;
if (offsetY < 0) offsetY = 0;
if (offsetX > 1) offsetX = 1;
if (offsetY > 1) offsetY = 1;
var iw = img.width,
ih = img.height,
r = Math.min(w / iw, h / ih),
nw = iw * r, /// new prop. width
nh = ih * r, /// new prop. height
cx, cy, cw, ch, ar = 1;
// decide which gap to fill
if (nw < w) ar = w / nw;
if (nh < h) ar = h / nh;
nw *= ar;
nh *= ar;
// calc source rectangle
cw = iw / (nw / w);
ch = ih / (nh / h);
cx = (iw - cw) * offsetX;
cy = (ih - ch) * offsetY;
// make sure source rectangle is valid
if (cx < 0) cx = 0;
if (cy < 0) cy = 0;
if (cw > iw) cw = iw;
if (ch > ih) ch = ih;
// fill image in dest. rectangle
ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
},
setNotch: function() {
this.notch = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABGYAAAChCAYAAACF67nsAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTM4IDc5LjE1OTgyNCwgMjAxNi8wOS8xNC0wMTowOTowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTcgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MUE2QzVENkZDMUFBMTFFNzkyQzI5NDM3OTJCN0QzNDIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MUE2QzVENzBDMUFBMTFFNzkyQzI5NDM3OTJCN0QzNDIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxQTZDNUQ2REMxQUExMUU3OTJDMjk0Mzc5MkI3RDM0MiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxQTZDNUQ2RUMxQUExMUU3OTJDMjk0Mzc5MkI3RDM0MiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PmMDNkcAAA0rSURBVHja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';
}
},
mounted() {
this.changePlaceholderPhoto();
this.setNotch();
this.canvas = document.getElementById("original");
this.ctx = this.canvas.getContext("2d");
this.canvas.width = this.dimentions.w;
this.canvas.height = this.dimentions.h;
this.loaded = true;
}
});
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.4/vue.min.js"></script>
:root {
--blue: #278dff;
}
body {
font-family:'Montserrat';
text-align:center;
min-height:100vh;
}
@media (min-width:1000px) {
.column {
position:fixed;
top:50%;
left:30%;
transform:translate(-50%,-50%);
width:500px;
}
.column:nth-child(2) {
left:70%;
}
}
@media (min-width:1400px) {
.column {
position:fixed;
top:50%;
left:35%;
transform:translate(-50%,-50%);
width:500px;
}
.column:nth-child(2) {
left:65%;
}
}
h1,
h2 {
margin:auto;
font-weight:700;
font-size:3em;
padding:60px 0 20px;
line-height:1.3;
}
h2 {
padding:0 0 40px;
font-size:1em;
font-weight:400;
width:300px;
}
@media (min-width:460px) {
h1 {font-size:4.4em}
h2 {
width:400px;
font-size:1.4em
}
}
h2 img {
width:100px;
}
.site-canvas {
position:relative;
}
.phone {
position:relative;
margin:0 auto;
width:310px;
height:640px;
background:url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/363972/apple-iphone-x.png) 50% 50% / contain no-repeat;
overflow:hidden;
cursor:pointer;
}
@media (min-width:460px) {
.phone {
margin-bottom:60px;
width:380px;
height:760px;
}
}
@media (min-width:460px) {
.phone {
margin-bottom:0;
}
}
.wallpaper {
position:relative;
margin:55px auto 0;
width:265px;
height:556px;
border-radius:16px 16px 28px 28px;
display:block;
overflow:hidden;
content:'';
}
@media (min-width:460px) {
.wallpaper {
width:330px;
height:680px;
}
}
.wallpaper img {
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
opacity:0;
}
.wallpaper a {
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
}
.photo {
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
background:url() 50% 50% / cover no-repeat;
opacity:0;
transition:350ms ease-in-out;
}
.photo.active {
opacity:1;
}
.download {
position:relative;
margin:0 auto 30px;
padding:15px 50px;
background:var(--blue);
color:#fff;
font-weight:400;
font-size:1em;
border-radius:50px;
box-sizing:border-box;
text-align:center;
border-bottom:5px solid rgba(0,0,0,0.2);
cursor:pointer;
display:inline-block;
white-space:nowrap;
overflow:hidden;
}
.download span {
text-overflow: ellipsis;
max-width: 210px;
overflow: hidden;
display: block;
}
.download form {
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
opacity:0;
}
.download form input {
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
cursor:pointer;
}
.upload {
position:absolute;
margin:auto;
top:50%;
left:50%;
transform:translate(-50%, -50%);
width:90%;
color:#fff;
font-size:1.2em;
}
.upload svg {
margin-bottom:20px;
width:60px;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0
}
.author {
margin:0 0 40px;
padding:5px 0;
color:var(--blue);
text-decoration:none;
border-bottom:1px solid transparent;
display:inline-block;
}
.author:hover {
border-bottom-color:var(--blue);
}
canvas {
position:fixed;
margin:auto;
top:0;
right:100%;
background:#111;
/* z-index:-1; */
}
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment