Desktop Monitor:
Tablet:
<!-- Default -->
<div class="productMockup">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-desktop.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
<!-- Three Quarter -->
<div class="productMockup" data-view="three-quarter">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-desktop.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
<!-- Tablet -->
<div class="productMockup" data-device="tablet">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-tablet.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
<!-- Tablet Three Quarter -->
<div class="productMockup" data-device="tablet" data-view="three-quarter">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-tablet.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
<!-- Documentation only -->
<section class="doc-variant">
<header class="doc-variant-header">
<h2 class="doc-variant-title">Desktop Front View (Default)</h2>
<div class="doc-variant-configuration">
</div>
<div class="doc-variant-configuration">
<label>
Theme:
<select class="doc-select" onchange="fractal.setTheme(this, {target: '.doc-variant-samples', value: this.value});">
<option value="default">default</option>
<option value="accent">accent</option>
<option value="dark">dark</option>
</select>
</label>
</div>
</header>
<div class="doc-variant-samples" data-theme="" style="overflow: hidden;">
<div class="productMockup">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-desktop.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
</div>
</section>
<section class="doc-variant">
<header class="doc-variant-header">
<h2 class="doc-variant-title">Desktop Three-quarter View</h2>
<div class="doc-variant-configuration">
</div>
<div class="doc-variant-configuration">
<label>
Theme:
<select class="doc-select" onchange="fractal.setTheme(this, {target: '.doc-variant-samples', value: this.value});">
<option value="default">default</option>
<option value="accent">accent</option>
<option value="dark">dark</option>
</select>
</label>
</div>
</header>
<div class="doc-variant-samples" data-theme="" style="overflow: hidden;">
<div class="productMockup" data-view="three-quarter">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-desktop.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
</div>
</section>
<section class="doc-variant">
<header class="doc-variant-header">
<h2 class="doc-variant-title">Tablet Front View</h2>
<div class="doc-variant-configuration">
</div>
<div class="doc-variant-configuration">
<label>
Theme:
<select class="doc-select" onchange="fractal.setTheme(this, {target: '.doc-variant-samples', value: this.value});">
<option value="default">default</option>
<option value="accent">accent</option>
<option value="dark">dark</option>
</select>
</label>
</div>
</header>
<div class="doc-variant-samples" data-theme="" style="overflow: hidden;">
<div class="productMockup" data-device="tablet">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-tablet.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
</div>
</section>
<section class="doc-variant">
<header class="doc-variant-header">
<h2 class="doc-variant-title">Tablet Three-quarter View</h2>
<div class="doc-variant-configuration">
</div>
<div class="doc-variant-configuration">
<label>
Theme:
<select class="doc-select" onchange="fractal.setTheme(this, {target: '.doc-variant-samples', value: this.value});">
<option value="default">default</option>
<option value="accent">accent</option>
<option value="dark">dark</option>
</select>
</label>
</div>
</header>
<div class="doc-variant-samples" data-theme="" style="overflow: hidden;">
<div class="productMockup" data-device="tablet" data-view="three-quarter">
<div class="productMockup-figure">
<figure class="figure" style="">
<a href="../../#.html">
<img class="figure-image" src="../../images/productMockup/sample-screenshot-tablet.png" alt="Ich bin eine Produktabbildung" />
</a>
<figcaption class="figure-caption"><span>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</span></figcaption>
</figure>
</div>
</div>
</div>
</section>
<!-- Default -->
<div class="productMockup{{#modifier}} {{.}}{{/modifier}}"{{#device}} data-device="{{.}}"{{/device}}{{#perspective}} data-view="{{.}}"{{/perspective}}>
<div class="productMockup-figure">
{{#figure}}
{{render '@figure' (contextData '@productmockup' this) merge=false}}
{{/figure}}
</div>
</div>
<!-- Three Quarter -->
<div class="productMockup{{#modifier}} {{.}}{{/modifier}}"{{#device}} data-device="{{.}}"{{/device}}{{#perspective}} data-view="{{.}}"{{/perspective}}>
<div class="productMockup-figure">
{{#figure}}
{{render '@figure' (contextData '@productmockup' this) merge=false}}
{{/figure}}
</div>
</div>
<!-- Tablet -->
<div class="productMockup{{#modifier}} {{.}}{{/modifier}}"{{#device}} data-device="{{.}}"{{/device}}{{#perspective}} data-view="{{.}}"{{/perspective}}>
<div class="productMockup-figure">
{{#figure}}
{{render '@figure' (contextData '@productmockup' this) merge=false}}
{{/figure}}
</div>
</div>
<!-- Tablet Three Quarter -->
<div class="productMockup{{#modifier}} {{.}}{{/modifier}}"{{#device}} data-device="{{.}}"{{/device}}{{#perspective}} data-view="{{.}}"{{/perspective}}>
<div class="productMockup-figure">
{{#figure}}
{{render '@figure' (contextData '@productmockup' this) merge=false}}
{{/figure}}
</div>
</div>
<!-- Documentation only -->
{{#variants}}
<section class="doc-variant">
<header class="doc-variant-header">
{{#title}}
<h2 class="doc-variant-title">{{{.}}}</h2>
{{/title}}
<div class="doc-variant-configuration">
{{#if modifiers}}
<ul data-label="Modifier">
{{#modifiers}}
<li>{{{.}}}</li>
{{/modifiers}}
</ul>
{{/if}}
</div>
<div class="doc-variant-configuration">
{{#if themes}}
<label>
Theme:
<select class="doc-select" onchange="fractal.setTheme(this, {target: '.doc-variant-samples', value: this.value});">
{{#themes}}
<option value="{{{.}}}">{{{.}}}</option>
{{/themes}}
</select>
</label>
{{/if}}
</div>
</header>
<div{{#if themes}} class="doc-variant-samples" data-theme="" style="overflow: hidden;"{{/if}}>
{{#samples}}
{{render '@productmockup' (contextData '@productmockup' this) merge=true}}
{{/samples}}
</div>
</section>
{{/variants}}
/* Default */
{
"figure": {
"src": "/images/productMockup/sample-screenshot-desktop.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
}
}
}
/* Three Quarter */
{
"figure": {
"src": "/images/productMockup/sample-screenshot-desktop.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
}
},
"perspective": "three-quarter"
}
/* Tablet */
{
"figure": {
"src": "/images/productMockup/sample-screenshot-tablet.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
}
},
"device": "tablet"
}
/* Tablet Three Quarter */
{
"figure": {
"src": "/images/productMockup/sample-screenshot-tablet.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
},
"url": "#"
},
"device": "tablet",
"perspective": "three-quarter"
}
/* Documentation only */
{
"figure": {
"src": "/images/productMockup/sample-screenshot-desktop.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
}
},
"variants": [
{
"title": "Desktop Front View (Default)",
"themes": [
"default",
"accent",
"dark"
],
"samples": [
{
"figure": {
"src": "/images/productMockup/sample-screenshot-desktop.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
}
}
}
]
},
{
"title": "Desktop Three-quarter View",
"themes": [
"default",
"accent",
"dark"
],
"samples": [
{
"figure": {
"src": "/images/productMockup/sample-screenshot-desktop.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
}
},
"perspective": "three-quarter"
}
]
},
{
"title": "Tablet Front View",
"themes": [
"default",
"accent",
"dark"
],
"samples": [
{
"figure": {
"src": "/images/productMockup/sample-screenshot-tablet.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
}
},
"device": "tablet"
}
]
},
{
"title": "Tablet Three-quarter View",
"themes": [
"default",
"accent",
"dark"
],
"samples": [
{
"figure": {
"src": "/images/productMockup/sample-screenshot-tablet.png",
"alt": "Ich bin eine Produktabbildung",
"caption": "Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.",
"link": {
"label": "Details anzeigen",
"url": "#"
},
"url": "#"
},
"device": "tablet",
"perspective": "three-quarter"
}
]
}
]
}
import productMockup from "./_productMockup.script.js";
productMockup.init();
import Perspective from "./vendor/_perspective";
export default (function (){
const defaults = {
views: {
desktop: {
default: {
backdropWidth: 900,
rect: [
[44, 10],
[854, 10],
[854, 468],
[44, 468]
],
},
'three-quarter': {
backdropWidth: 900,
rect: [
[127, 66],
[775, 12],
[775, 474],
[127, 440]
],
},
},
tablet: {
default: {
backdropWidth: 900,
rect: [
[82, 24],
[818, 24],
[818, 576],
[82, 576]
],
},
'three-quarter': {
backdropWidth: 900,
rect: [
[242, 19],
[812, 34],
[700, 574],
[126, 487]
],
},
},
},
};
function init() {
const elements = document.querySelectorAll(".productMockup");
elements.forEach((element) => {
new Mockup(element);
});
}
const Mockup = function(container) {
const figure = container.querySelector(".productMockup-figure"),
sourceImage = figure.querySelector("img");
if (! sourceImage) {
console.warn("productMockup: No source image found!");
return;
}
this.container = container;
const frame = document.createElement("div");
frame.classList.add('productMockup-frame');
figure.appendChild(frame);
const canvas = document.createElement("canvas");
frame.appendChild(canvas);
let view = defaults.views.desktop;
if (container.dataset.device && typeof defaults.views[container.dataset.device] !== "undefined") {
view = defaults.views[container.dataset.device];
}
if (typeof view.default !== "object") {
console.error(`productMockup: No default view for device "${container.dataset.device}" defined!`);
return;
}
let rect = view.default.rect;
this.backdropWidth = view.default.backdropWidth;
if (container.dataset.view && typeof view[container.dataset.view] !== "undefined") {
rect = view[container.dataset.view].rect;
this.backdropWidth = view[container.dataset.view].backdropWidth;
}
this.width = sourceImage.offsetWidth;
this.height = sourceImage.offsetHeight;
canvas.width = this.width;
canvas.height = this.height;
this.onElementInViewport(this.container, () => {
const screen = new Image();
screen.onload = () => {
const ratio = this.width / this.backdropWidth;
const context = canvas.getContext("2d"),
p = new Perspective(context, screen),
rectScaled = this.scaleRect(rect, ratio);
p.draw(rectScaled);
this.container.setAttribute("data-rendered", true);
}
this.clipLink(rect);
screen.src = sourceImage.src;
});
}
Mockup.prototype.onElementInViewport = function(element, callback) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback();
observer.unobserve(entry.target);
}
});
});
observer.observe(element);
}
Mockup.prototype.scaleRect = function(rect, ratio) {
if (typeof ratio !== "number") {
ratio = 1;
}
let rectScaled = [];
rect.forEach((values, rowIndex) => {
values.forEach((value, columnIndex) => {
if (typeof rectScaled[rowIndex] !== "object") {
rectScaled[rowIndex] = [];
}
rectScaled[rowIndex][columnIndex] = value * ratio;
});
});
return rectScaled;
}
Mockup.prototype.clipLink = function(rect) {
const link = this.container.querySelector(".productMockup-figure .figure > a");
if (! link) {
return;
}
const ratio = this.width / this.height,
width = this.backdropWidth,
height = this.backdropWidth * 1 / ratio;
let points = [];
rect.forEach(point => {
let [x, y] = point;
x = x / width * 100;
y = y / height * 100;
points.push(`${x}% ${y}%`);
});
let clipPath = points.join(", ");
link.style.clipPath = `polygon(${clipPath})`;
}
return {
init: init
}
})();
@import "_productMockup.styles";
.productMockup {
$aspect-ratio: 1.5;
@include stack-spacing(component);
width: 100%;
opacity: 1;
transition: opacity $_transition-duration--in;
&:not([data-rendered="true"]) {
opacity: 0;
}
&-figure {
position: relative;
z-index: 1;
.figure {
@include stack-spacing(0);
position: relative;
z-index: 1;
&-image {
visibility: hidden;
}
&-caption {
@include stack-spacing(large);
text-align: center;
margin-left: auto;
margin-right: auto;
text-wrap: pretty;
}
}
}
&-frame {
position: absolute;
top: 0;
width: 100%;
&::before,
&::after {
content: "";
display: block;
width: 200%;
padding-bottom: (math.div(1, $aspect-ratio) * 200%);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: transparent center center no-repeat;
background-size: 100% auto;
pointer-events: none;
}
&::before {
z-index: -1;
background-image: url(../images/productMockup/display-front-view--ambient.png);
}
&::after {
z-index: 1;
background-image: url(../images/productMockup/display-front-view--reflection.png);
}
canvas {
display: block;
width: 100%;
aspect-ratio: #{$aspect-ratio};
background: url(../images/productMockup/display-front-view.png) center center no-repeat;
background-size: 200% auto;
position: relative;
}
}
&-link {
display: block;
position: absolute;
inset: 0;
z-index: 2;
}
&[data-device="three-quarter"] &-figure,
&[data-view="three-quarter"] &-figure {
.figure-caption {
@include stack-spacing();
}
}
&[data-view="three-quarter"] &-frame {
&::before {
background-image: url(../images/productMockup/display-three-quarter-view--ambient.png);
}
&::after {
background-image: url(../images/productMockup/display-three-quarter-view--reflection.png);
}
canvas {
background-image: url(../images/productMockup/display-three-quarter-view.png);
}
}
&[data-device="tablet"] &-frame {
&::before {
background-image: url(../images/productMockup/tablet-front-view--ambient.png);
}
&::after {
background-image: url(../images/productMockup/tablet-front-view--reflection.png);
}
canvas {
background-image: url(../images/productMockup/tablet-front-view.png);
}
}
&[data-device="tablet"][data-view="three-quarter"] &-frame {
&::before {
background-image: url(../images/productMockup/tablet-three-quarter-view--ambient.png);
}
&::after {
background-image: url(../images/productMockup/tablet-three-quarter-view--reflection.png);
}
canvas {
background-image: url(../images/productMockup/tablet-three-quarter-view.png);
}
}
}